@team-agent/installer 0.5.61 → 0.5.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +5 -2
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +41 -7
- package/crates/team-agent/src/cli/helpers.rs +1 -0
- package/crates/team-agent/src/cli/leader.rs +6 -1
- package/crates/team-agent/src/cli/leaders.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +33 -3
- package/crates/team-agent/src/cli/named_address.rs +8 -1
- package/crates/team-agent/src/cli/profile.rs +2 -1
- package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send/resolve.rs +206 -1
- package/crates/team-agent/src/cli/send.rs +2 -1
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/cli/status.rs +2 -1
- package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
- package/crates/team-agent/src/cli/status_port/format.rs +1 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/communication_mode/mod.rs +1 -0
- package/crates/team-agent/src/compiler.rs +1 -0
- package/crates/team-agent/src/conpty/backend.rs +1 -0
- package/crates/team-agent/src/conpty/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/backoff.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
- package/crates/team-agent/src/coordinator/health.rs +1 -0
- package/crates/team-agent/src/coordinator/mod.rs +2 -2
- package/crates/team-agent/src/coordinator/orphan.rs +4 -116
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
- package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
- package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
- package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
- package/crates/team-agent/src/coordinator/tick.rs +24 -28
- package/crates/team-agent/src/coordinator/types.rs +3 -176
- package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
- package/crates/team-agent/src/db/message_store.rs +2 -1
- package/crates/team-agent/src/db/migration.rs +5 -4
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/db/schema.rs +1 -0
- package/crates/team-agent/src/diagnose/comms.rs +2 -1
- package/crates/team-agent/src/diagnose/mod.rs +1 -0
- package/crates/team-agent/src/diagnose/orphans.rs +1 -0
- package/crates/team-agent/src/layout/manager.rs +3 -7
- package/crates/team-agent/src/layout/mod.rs +2 -2
- package/crates/team-agent/src/layout/overlay.rs +6 -1
- package/crates/team-agent/src/layout/placement.rs +1 -0
- package/crates/team-agent/src/layout/recovery.rs +3 -0
- package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
- package/crates/team-agent/src/layout/sessions.rs +8 -1
- package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
- package/crates/team-agent/src/layout/worker_env.rs +3 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/helpers.rs +1 -0
- package/crates/team-agent/src/leader/incident.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +27 -22
- package/crates/team-agent/src/leader/mod.rs +2 -3
- package/crates/team-agent/src/leader/owner_bind.rs +13 -3
- package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +4 -17
- package/crates/team-agent/src/leader/registry.rs +3 -15
- package/crates/team-agent/src/leader/start.rs +373 -125
- package/crates/team-agent/src/leader/takeover.rs +10 -232
- package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
- package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
- package/crates/team-agent/src/leader/tests/identity.rs +7 -2
- package/crates/team-agent/src/leader/tests/idle.rs +0 -193
- package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
- package/crates/team-agent/src/leader/tests/mod.rs +4 -41
- package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
- package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
- package/crates/team-agent/src/leader/types.rs +1 -28
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +7 -1
- package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +11 -12
- package/crates/team-agent/src/lifecycle/mod.rs +2 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
- package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart.rs +13 -11
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
- package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
- package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
- package/crates/team-agent/src/lifecycle/tests.rs +59 -0
- package/crates/team-agent/src/lifecycle/types.rs +0 -9
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
- package/crates/team-agent/src/mcp_server/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
- package/crates/team-agent/src/mcp_server/tools.rs +1 -0
- package/crates/team-agent/src/mcp_server/types.rs +1 -18
- package/crates/team-agent/src/mcp_server/wire.rs +5 -5
- package/crates/team-agent/src/messaging/activity.rs +1 -0
- package/crates/team-agent/src/messaging/address.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +4 -3
- package/crates/team-agent/src/messaging/helpers.rs +2 -1
- package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
- package/crates/team-agent/src/messaging/mod.rs +10 -14
- package/crates/team-agent/src/messaging/peers.rs +2 -0
- package/crates/team-agent/src/messaging/persist.rs +3 -1
- package/crates/team-agent/src/messaging/presentation.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +1 -0
- package/crates/team-agent/src/messaging/selftest.rs +1 -1
- package/crates/team-agent/src/messaging/send.rs +41 -8
- package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
- package/crates/team-agent/src/messaging/types.rs +2 -2
- package/crates/team-agent/src/messaging/watchers.rs +19 -13
- package/crates/team-agent/src/model/enums.rs +1 -0
- package/crates/team-agent/src/model/errors.rs +1 -0
- package/crates/team-agent/src/model/ids.rs +1 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/name_similarity.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
- package/crates/team-agent/src/model/paths.rs +1 -0
- package/crates/team-agent/src/model/permissions.rs +3 -2
- package/crates/team-agent/src/model/routing.rs +1 -0
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/model/task_graph.rs +1 -0
- package/crates/team-agent/src/model/yaml/tests.rs +1 -0
- package/crates/team-agent/src/model/yaml.rs +1 -0
- package/crates/team-agent/src/packaging/install.rs +9 -147
- package/crates/team-agent/src/packaging/migrate.rs +2 -0
- package/crates/team-agent/src/packaging/mod.rs +1 -0
- package/crates/team-agent/src/packaging/repair.rs +2 -0
- package/crates/team-agent/src/packaging/tests.rs +29 -218
- package/crates/team-agent/src/packaging/types.rs +8 -15
- package/crates/team-agent/src/platform/argv.rs +4 -0
- package/crates/team-agent/src/platform/errors.rs +10 -1
- package/crates/team-agent/src/platform/file_lock.rs +4 -157
- package/crates/team-agent/src/platform/mod.rs +0 -61
- package/crates/team-agent/src/platform/process.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
- package/crates/team-agent/src/provider/classify.rs +1 -0
- package/crates/team-agent/src/provider/faults.rs +1 -26
- package/crates/team-agent/src/provider/helpers.rs +1 -0
- package/crates/team-agent/src/provider/mod.rs +1 -1
- package/crates/team-agent/src/provider/session/capture.rs +1 -4
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
- package/crates/team-agent/src/provider/session/mod.rs +2 -4
- package/crates/team-agent/src/provider/session/resume.rs +2 -209
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -42
- package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +1 -0
- package/crates/team-agent/src/provider/wire.rs +1 -0
- package/crates/team-agent/src/state/identity.rs +16 -5
- package/crates/team-agent/src/state/identity_keys.rs +1 -0
- package/crates/team-agent/src/state/mod.rs +2 -0
- package/crates/team-agent/src/state/owner_gate.rs +5 -0
- package/crates/team-agent/src/state/ownership.rs +10 -4
- package/crates/team-agent/src/state/paths.rs +12 -0
- package/crates/team-agent/src/state/persist.rs +9 -3
- package/crates/team-agent/src/state/projection.rs +15 -2
- package/crates/team-agent/src/state/repository/intent.rs +1 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +7 -0
- package/crates/team-agent/src/state/selector.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
- package/crates/team-agent/src/tmux_backend.rs +63 -0
- package/crates/team-agent/src/transport.rs +15 -0
- package/package.json +4 -4
- package/crates/team-agent/src/leader/inject.rs +0 -33
- package/crates/team-agent/src/provider/command.rs +0 -80
|
@@ -0,0 +1,1249 @@
|
|
|
1
|
+
//! SUB prep Codex trust + roundtrip contracts.
|
|
2
|
+
//!
|
|
3
|
+
//! Anchors:
|
|
4
|
+
//! - `.team/artifacts/rs-subprep-codex-trust-roundtrip-root-cause.md`
|
|
5
|
+
//! - `.team/artifacts/macmini-e2e/subprep-codex-probe5-20260605T071044Z/`
|
|
6
|
+
//!
|
|
7
|
+
//! Real Codex subscription behavior still needs rt smoke for the provider pane/report_result parts;
|
|
8
|
+
//! these cargo contracts lock the local recognizer, delivery state, command-shape, and rollout
|
|
9
|
+
//! capture surfaces that caused the probe failures.
|
|
10
|
+
|
|
11
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
12
|
+
|
|
13
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
14
|
+
mod hermetic_guard;
|
|
15
|
+
#[allow(dead_code)]
|
|
16
|
+
fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
|
|
17
|
+
|
|
18
|
+
#[path = "../../../tests/support/composite_source.rs"]
|
|
19
|
+
mod composite_source;
|
|
20
|
+
use std::collections::BTreeMap;
|
|
21
|
+
use std::path::{Path, PathBuf};
|
|
22
|
+
use std::sync::Mutex;
|
|
23
|
+
|
|
24
|
+
#[path = "../../../tests/support/temp_workspace.rs"]
|
|
25
|
+
mod temp_workspace;
|
|
26
|
+
|
|
27
|
+
use rusqlite::params;
|
|
28
|
+
use serde_json::json;
|
|
29
|
+
use team_agent::event_log::EventLog;
|
|
30
|
+
use team_agent::lifecycle::launch_with_transport;
|
|
31
|
+
use team_agent::message_store::MessageStore;
|
|
32
|
+
use team_agent::messaging::delivery::deliver_pending_message;
|
|
33
|
+
use team_agent::messaging::trust::attempt_trust_auto_answer;
|
|
34
|
+
use team_agent::messaging::PaneWidthQuery;
|
|
35
|
+
use team_agent::model::enums::{AuthMode, Provider};
|
|
36
|
+
use team_agent::provider::{classify, get_adapter, ProcessLiveness, TurnState};
|
|
37
|
+
use team_agent::transport::{
|
|
38
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
39
|
+
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome,
|
|
40
|
+
SpawnResult, SubmitVerification, Target, Transport, TransportError, TurnVerification,
|
|
41
|
+
WindowName,
|
|
42
|
+
};
|
|
43
|
+
use temp_workspace::TempWorkspace;
|
|
44
|
+
|
|
45
|
+
const PROBE5_TRUST_PANE: &str = r#"> You are in /private/tmp/ta-subprep-SUB-PROBE-1-trust-startup.Ey2Xk6/teamdir
|
|
46
|
+
|
|
47
|
+
Do you trust the contents of this directory? Working with untrusted contents
|
|
48
|
+
comes with higher risk of prompt injection. Trusting the directory allows
|
|
49
|
+
project-local config, hooks, and exec policies to load.
|
|
50
|
+
|
|
51
|
+
› 1. Yes, continue
|
|
52
|
+
2. No, quit
|
|
53
|
+
|
|
54
|
+
Press enter to continue
|
|
55
|
+
"#;
|
|
56
|
+
|
|
57
|
+
const PROBE5_READY_AFTER_TRUST_CONSUMED: &str = r#"> You are in /private/tmp/ta-subprep-SUB-PROBE-2-send-turn-idle.uxarHT/teamdir
|
|
58
|
+
|
|
59
|
+
╭─────────────────────────────────────────────╮
|
|
60
|
+
│ >_ OpenAI Codex (v0.136.0) │
|
|
61
|
+
│ │
|
|
62
|
+
│ model: gpt-5.5 xhigh /model to change │
|
|
63
|
+
│ directory: /private/tmp/…/teamdir │
|
|
64
|
+
╰─────────────────────────────────────────────╯
|
|
65
|
+
|
|
66
|
+
› Run /review on my current changes
|
|
67
|
+
"#;
|
|
68
|
+
|
|
69
|
+
const SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART: &str = r#"> You are in /private/tmp/ta-subroot-SUB-PROBE-1-trust-startup.uL9Boc/teamdir
|
|
70
|
+
|
|
71
|
+
Do you trust the contents of this directory? Working with untrusted contents
|
|
72
|
+
comes with higher risk of prompt injection. Trusting the directory allows
|
|
73
|
+
project-local config, hooks, and exec policies to load.
|
|
74
|
+
|
|
75
|
+
› 1. Yes, continue
|
|
76
|
+
2. No, quit
|
|
77
|
+
|
|
78
|
+
Press enter to continue
|
|
79
|
+
|
|
80
|
+
╭─────────────────────────────────────────────────╮
|
|
81
|
+
│ ✨ Update available! 0.136.0 -> 0.137.0 │
|
|
82
|
+
│ Run npm install -g @openai/codex to update. │
|
|
83
|
+
│ │
|
|
84
|
+
│ See full release notes: │
|
|
85
|
+
│ https://github.com/openai/codex/releases/latest │
|
|
86
|
+
╰─────────────────────────────────────────────────╯
|
|
87
|
+
|
|
88
|
+
╭───────────────────────────────────────╮
|
|
89
|
+
│ >_ OpenAI Codex (v0.136.0) │
|
|
90
|
+
│ │
|
|
91
|
+
│ model: loading /model to change │
|
|
92
|
+
│ directory: /private/tmp/…/teamdir │
|
|
93
|
+
╰───────────────────────────────────────╯
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
› Find and fix a bug in @filename
|
|
97
|
+
|
|
98
|
+
gpt-5.5 default · /private/tmp/ta-subroot-SUB-PROBE-1-trust-startup.uL9Boc/te…
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
"#;
|
|
105
|
+
|
|
106
|
+
fn tmp_dir(tag: &str) -> TempWorkspace {
|
|
107
|
+
TempWorkspace::new("ta-rs-subprep", tag)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn temp_workspace_guard_cleans_after_panic_unwind() {
|
|
112
|
+
if TempWorkspace::keep_enabled() {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let mut created = None;
|
|
117
|
+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
118
|
+
let ws = tmp_dir("guard-panic");
|
|
119
|
+
std::fs::write(ws.join("marker"), "created").unwrap();
|
|
120
|
+
created = Some(ws.to_path_buf());
|
|
121
|
+
panic!("intentional panic to prove TempWorkspace Drop cleanup");
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
assert!(
|
|
125
|
+
result.is_err(),
|
|
126
|
+
"fixture sanity: panic branch must actually unwind for cleanup verification"
|
|
127
|
+
);
|
|
128
|
+
let created = created.expect("fixture sanity: panic branch should record temp path");
|
|
129
|
+
assert!(
|
|
130
|
+
!created.exists(),
|
|
131
|
+
"TempWorkspace must remove its root during panic unwinding unless TEAM_AGENT_KEEP_TEST_TMP=1; leaked={}",
|
|
132
|
+
created.display()
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn manifest_dir() -> PathBuf {
|
|
137
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn truncated_workspace_token(canonical_teamdir: &Path) -> String {
|
|
141
|
+
let tail = canonical_teamdir
|
|
142
|
+
.file_name()
|
|
143
|
+
.and_then(|name| name.to_str())
|
|
144
|
+
.unwrap_or("teamdir");
|
|
145
|
+
let head = canonical_teamdir
|
|
146
|
+
.parent()
|
|
147
|
+
.and_then(Path::parent)
|
|
148
|
+
.unwrap_or_else(|| canonical_teamdir.parent().unwrap_or(canonical_teamdir));
|
|
149
|
+
let mut head = head.to_string_lossy().to_string();
|
|
150
|
+
if !head.ends_with(std::path::MAIN_SEPARATOR) {
|
|
151
|
+
head.push(std::path::MAIN_SEPARATOR);
|
|
152
|
+
}
|
|
153
|
+
format!("{head}…/{tail}")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn foreign_workspace_path(canonical_teamdir: &Path) -> PathBuf {
|
|
157
|
+
canonical_teamdir
|
|
158
|
+
.parent()
|
|
159
|
+
.and_then(Path::parent)
|
|
160
|
+
.unwrap_or_else(|| canonical_teamdir.parent().unwrap_or(canonical_teamdir))
|
|
161
|
+
.join("foreign-team")
|
|
162
|
+
.join(
|
|
163
|
+
canonical_teamdir
|
|
164
|
+
.file_name()
|
|
165
|
+
.unwrap_or_else(|| std::ffi::OsStr::new("teamdir")),
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[test]
|
|
170
|
+
fn contract_a_subroot_full_pane_trust_menu_still_actionable_despite_bottom_ready_prompt() {
|
|
171
|
+
use team_agent::provider::{classify_codex_startup_screen, StartupScreenDecision};
|
|
172
|
+
|
|
173
|
+
let decision = classify_codex_startup_screen(SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART);
|
|
174
|
+
|
|
175
|
+
assert_eq!(
|
|
176
|
+
decision,
|
|
177
|
+
StartupScreenDecision::AnswerWorkspaceTrust,
|
|
178
|
+
"Contract A real-machine fixture: the full Codex pane keeps an actionable trust menu even when an update box, Codex banner, and bottom `› Find...` input prompt are visible in the same capture"
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn contract_a_subroot_tick_answers_full_pane_trust_and_persists_startup_handled() {
|
|
184
|
+
use team_agent::coordinator::{Coordinator, ProviderRegistry, WorkspacePath};
|
|
185
|
+
use team_agent::provider::ProviderAdapter;
|
|
186
|
+
|
|
187
|
+
struct RealAdapterRegistry;
|
|
188
|
+
|
|
189
|
+
impl ProviderRegistry for RealAdapterRegistry {
|
|
190
|
+
fn adapter_for(&self, provider: Provider) -> Box<dyn ProviderAdapter> {
|
|
191
|
+
team_agent::provider::get_adapter(provider)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let ws = tmp_dir("a-subroot-tick-full-pane");
|
|
196
|
+
let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
|
|
197
|
+
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
198
|
+
std::fs::write(&mcp_config, "{}").unwrap();
|
|
199
|
+
team_agent::state::persist::save_runtime_state(
|
|
200
|
+
&ws,
|
|
201
|
+
&json!({
|
|
202
|
+
"session_name": "team-ta-subroot-SUB-PROBE-1-trust-startup",
|
|
203
|
+
"team_dir": ws.join("teamdir").to_string_lossy().to_string(),
|
|
204
|
+
"agents": {
|
|
205
|
+
"w1": {
|
|
206
|
+
"provider": "codex",
|
|
207
|
+
"status": "running",
|
|
208
|
+
"startup_prompts": "pending",
|
|
209
|
+
"startup_prompt_status": "pending",
|
|
210
|
+
"window": "w1",
|
|
211
|
+
"pane_id": "%7",
|
|
212
|
+
"mcp_config": mcp_config.to_string_lossy().to_string()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}),
|
|
216
|
+
)
|
|
217
|
+
.unwrap();
|
|
218
|
+
|
|
219
|
+
let transport = RecordingTransport::new(vec![
|
|
220
|
+
SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART.to_string(),
|
|
221
|
+
SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART.to_string(),
|
|
222
|
+
])
|
|
223
|
+
.with_session_present(true)
|
|
224
|
+
.with_windows(vec![WindowName::new("w1")]);
|
|
225
|
+
let coord = Coordinator::new(
|
|
226
|
+
WorkspacePath::new(ws.to_path_buf()),
|
|
227
|
+
Box::new(RealAdapterRegistry),
|
|
228
|
+
Box::new(transport.clone()),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
let report = coord
|
|
232
|
+
.tick()
|
|
233
|
+
.expect("coordinator tick should stay typed/fallible");
|
|
234
|
+
let state = team_agent::state::persist::load_runtime_state(&ws).unwrap();
|
|
235
|
+
let agent = &state["agents"]["w1"];
|
|
236
|
+
let events = read_events(&ws);
|
|
237
|
+
|
|
238
|
+
assert!(
|
|
239
|
+
report.ok && !report.stop,
|
|
240
|
+
"Contract A driver: startup trust handling must not stop or degrade the tick; report={report:?}"
|
|
241
|
+
);
|
|
242
|
+
assert_eq!(
|
|
243
|
+
transport.send_keys_count(),
|
|
244
|
+
1,
|
|
245
|
+
"Contract A driver: full-pane own-workspace trust prompt must send exactly one Enter"
|
|
246
|
+
);
|
|
247
|
+
assert_eq!(
|
|
248
|
+
agent["startup_prompts"],
|
|
249
|
+
json!("handled"),
|
|
250
|
+
"Contract A driver: tick must persist startup_prompts=handled after answering trust; state={state}"
|
|
251
|
+
);
|
|
252
|
+
assert_eq!(
|
|
253
|
+
agent["startup_prompt_handled"][0]["prompt"],
|
|
254
|
+
json!("codex_workspace_trust"),
|
|
255
|
+
"Contract A driver: handled prompt must identify codex workspace trust; state={state}"
|
|
256
|
+
);
|
|
257
|
+
assert_eq!(
|
|
258
|
+
agent["startup_prompt_handled"][0]["action"],
|
|
259
|
+
json!("sent_enter"),
|
|
260
|
+
"Contract A driver: handled prompt action must be sent_enter; state={state}"
|
|
261
|
+
);
|
|
262
|
+
assert!(
|
|
263
|
+
events.contains("leader_panes.trust_auto_answered")
|
|
264
|
+
|| events.contains("startup_prompt_handled"),
|
|
265
|
+
"Contract A driver: trust answer must be visible in events as leader_panes.trust_auto_answered or startup_prompt_handled; events={events}"
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#[test]
|
|
270
|
+
fn contract_a_step1_trust_auto_answer_is_idempotent_across_repeated_ticks() {
|
|
271
|
+
use team_agent::coordinator::{Coordinator, ProviderRegistry, WorkspacePath};
|
|
272
|
+
use team_agent::provider::ProviderAdapter;
|
|
273
|
+
|
|
274
|
+
struct RealAdapterRegistry;
|
|
275
|
+
|
|
276
|
+
impl ProviderRegistry for RealAdapterRegistry {
|
|
277
|
+
fn adapter_for(&self, provider: Provider) -> Box<dyn ProviderAdapter> {
|
|
278
|
+
team_agent::provider::get_adapter(provider)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let ws = tmp_dir("a-subroot-tick-idempotent");
|
|
283
|
+
let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
|
|
284
|
+
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
285
|
+
std::fs::write(&mcp_config, "{}").unwrap();
|
|
286
|
+
team_agent::state::persist::save_runtime_state(
|
|
287
|
+
&ws,
|
|
288
|
+
&json!({
|
|
289
|
+
"session_name": "team-ta-subroot-SUB-PROBE-1-trust-startup",
|
|
290
|
+
"team_dir": ws.join("teamdir").to_string_lossy().to_string(),
|
|
291
|
+
"agents": {
|
|
292
|
+
"w1": {
|
|
293
|
+
"provider": "codex",
|
|
294
|
+
"status": "running",
|
|
295
|
+
"startup_prompts": "pending",
|
|
296
|
+
"startup_prompt_status": "pending",
|
|
297
|
+
"window": "w1",
|
|
298
|
+
"pane_id": "%7",
|
|
299
|
+
"mcp_config": mcp_config.to_string_lossy().to_string()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}),
|
|
303
|
+
)
|
|
304
|
+
.unwrap();
|
|
305
|
+
|
|
306
|
+
let transport = RecordingTransport::new(vec![
|
|
307
|
+
SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART.to_string(),
|
|
308
|
+
SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART.to_string(),
|
|
309
|
+
SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART.to_string(),
|
|
310
|
+
])
|
|
311
|
+
.with_session_present(true)
|
|
312
|
+
.with_windows(vec![WindowName::new("w1")]);
|
|
313
|
+
let coord = Coordinator::new(
|
|
314
|
+
WorkspacePath::new(ws.to_path_buf()),
|
|
315
|
+
Box::new(RealAdapterRegistry),
|
|
316
|
+
Box::new(transport.clone()),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
coord
|
|
320
|
+
.tick()
|
|
321
|
+
.expect("first tick should answer the startup trust prompt");
|
|
322
|
+
coord
|
|
323
|
+
.tick()
|
|
324
|
+
.expect("second tick over the same pane must not answer trust again");
|
|
325
|
+
|
|
326
|
+
let state = team_agent::state::persist::load_runtime_state(&ws).unwrap();
|
|
327
|
+
let agent = &state["agents"]["w1"];
|
|
328
|
+
let events = read_events(&ws);
|
|
329
|
+
let handled_event_count = count_event(&events, "startup_prompt_handled");
|
|
330
|
+
let sent_enter_count = count_handled_action(&events, "sent_enter");
|
|
331
|
+
|
|
332
|
+
assert_eq!(
|
|
333
|
+
transport.send_keys_count(),
|
|
334
|
+
1,
|
|
335
|
+
"Contract A step1/idempotency: the same actionable trust prompt across repeated coordinator ticks must receive Enter exactly once"
|
|
336
|
+
);
|
|
337
|
+
assert_eq!(
|
|
338
|
+
handled_event_count, 1,
|
|
339
|
+
"Contract A step1/idempotency: startup_prompt_handled must be emitted exactly once after trust is answered; events={events}"
|
|
340
|
+
);
|
|
341
|
+
assert_eq!(
|
|
342
|
+
sent_enter_count, 1,
|
|
343
|
+
"Contract A step1/idempotency: sent_enter handling for codex_workspace_trust must be recorded exactly once; events={events}"
|
|
344
|
+
);
|
|
345
|
+
assert_eq!(
|
|
346
|
+
agent["startup_prompts"],
|
|
347
|
+
json!("handled"),
|
|
348
|
+
"Contract A step1/idempotency: answering trust must persist startup_prompts=handled/complete so later ticks skip re-answering; state={state}"
|
|
349
|
+
);
|
|
350
|
+
assert_eq!(
|
|
351
|
+
agent["startup_prompt_handled"].as_array().map(Vec::len),
|
|
352
|
+
Some(1),
|
|
353
|
+
"Contract A step1/idempotency: state must retain one handled prompt record, not append duplicates; state={state}"
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#[test]
|
|
358
|
+
fn contract_a_own_workspace_trust_is_actionable_not_ready_and_foreign_is_pending() {
|
|
359
|
+
use team_agent::provider::{classify_codex_startup_screen, StartupScreenDecision};
|
|
360
|
+
|
|
361
|
+
assert_eq!(
|
|
362
|
+
classify_codex_startup_screen(PROBE5_TRUST_PANE),
|
|
363
|
+
StartupScreenDecision::AnswerWorkspaceTrust,
|
|
364
|
+
"Contract A/N15: real Codex trust menu with `› 1. Yes, continue` is an actionable trust prompt, not Ready from a bare single-glyph marker"
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
let root = tmp_dir("a-truncated-root");
|
|
368
|
+
let teamdir = root.join("teamdir");
|
|
369
|
+
std::fs::create_dir_all(&teamdir).unwrap();
|
|
370
|
+
let canonical_teamdir = std::fs::canonicalize(&teamdir).unwrap();
|
|
371
|
+
let own_tail = format!(
|
|
372
|
+
"Do you trust the contents of this directory?\n directory: {}",
|
|
373
|
+
truncated_workspace_token(&canonical_teamdir)
|
|
374
|
+
);
|
|
375
|
+
let own_transport = RecordingTransport::new(vec![own_tail.to_string()]);
|
|
376
|
+
let own_log = EventLog::new(&root);
|
|
377
|
+
let own = attempt_trust_auto_answer(
|
|
378
|
+
&canonical_teamdir,
|
|
379
|
+
&own_transport,
|
|
380
|
+
Some(&PaneId::new("%7")),
|
|
381
|
+
&own_tail,
|
|
382
|
+
&PaneWidthQuery::Ok { pane_width: 80 },
|
|
383
|
+
&own_log,
|
|
384
|
+
)
|
|
385
|
+
.unwrap();
|
|
386
|
+
assert!(
|
|
387
|
+
own.answered,
|
|
388
|
+
"Contract A/#167: own-workspace matcher must accept platform-local Codex truncated directory display via the shared realpath/truncation matcher; canonical_teamdir={} tail={own_tail:?} got {own:?}",
|
|
389
|
+
canonical_teamdir.display()
|
|
390
|
+
);
|
|
391
|
+
assert_eq!(
|
|
392
|
+
own_transport.inject_count(),
|
|
393
|
+
1,
|
|
394
|
+
"Contract A: own trust answer is exactly one local Enter/inject action"
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
let foreign_transport = RecordingTransport::new(vec![PROBE5_TRUST_PANE.to_string()]);
|
|
398
|
+
let foreign = attempt_trust_auto_answer(
|
|
399
|
+
&canonical_teamdir,
|
|
400
|
+
&foreign_transport,
|
|
401
|
+
Some(&PaneId::new("%8")),
|
|
402
|
+
&format!(
|
|
403
|
+
"Do you trust the contents of this directory?\n> You are in {}",
|
|
404
|
+
foreign_workspace_path(&canonical_teamdir).display()
|
|
405
|
+
),
|
|
406
|
+
&PaneWidthQuery::Ok { pane_width: 120 },
|
|
407
|
+
&own_log,
|
|
408
|
+
)
|
|
409
|
+
.unwrap();
|
|
410
|
+
assert!(
|
|
411
|
+
!foreign.answered && foreign.reason == "workspace_dir_mismatch",
|
|
412
|
+
"Contract A: foreign workspace trust prompt stays pending and is never auto-answered; got {foreign:?}"
|
|
413
|
+
);
|
|
414
|
+
assert_eq!(
|
|
415
|
+
foreign_transport.inject_count(),
|
|
416
|
+
0,
|
|
417
|
+
"Contract A: foreign trust prompt must not receive Enter"
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
#[test]
|
|
422
|
+
fn contract_a_probe5_quick_start_ready_is_invalid_while_actionable_trust_remains() {
|
|
423
|
+
use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
|
|
424
|
+
use team_agent::provider::{classify_codex_startup_screen, StartupScreenDecision};
|
|
425
|
+
|
|
426
|
+
assert_eq!(
|
|
427
|
+
classify_codex_startup_screen(PROBE5_TRUST_PANE),
|
|
428
|
+
StartupScreenDecision::AnswerWorkspaceTrust,
|
|
429
|
+
"fixture sanity: probe5 pane text is an actionable own-workspace trust prompt"
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
let ws = tmp_dir("a-wait-ready-trust");
|
|
433
|
+
let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
|
|
434
|
+
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
435
|
+
std::fs::write(&mcp_config, "{}").unwrap();
|
|
436
|
+
team_agent::state::persist::save_runtime_state(
|
|
437
|
+
&ws,
|
|
438
|
+
&json!({
|
|
439
|
+
"session_name": "team-subprep",
|
|
440
|
+
"agents": {
|
|
441
|
+
"w1": {
|
|
442
|
+
"provider": "codex",
|
|
443
|
+
"status": "awaiting_trust_prompt",
|
|
444
|
+
"startup_prompt_status": "awaiting_trust_prompt",
|
|
445
|
+
"startup_prompts": "awaiting_trust_prompt",
|
|
446
|
+
"pane_id": "%7",
|
|
447
|
+
"window": "w1",
|
|
448
|
+
"mcp_config": mcp_config.to_string_lossy().to_string(),
|
|
449
|
+
"task_prompt_delivered": true
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}),
|
|
453
|
+
)
|
|
454
|
+
.unwrap();
|
|
455
|
+
|
|
456
|
+
let result = cmd_wait_ready(&WaitReadyArgs {
|
|
457
|
+
workspace: ws.to_path_buf(),
|
|
458
|
+
timeout: 0.0,
|
|
459
|
+
json: true,
|
|
460
|
+
team: None,
|
|
461
|
+
})
|
|
462
|
+
.expect("wait-ready SUT should return shaped JSON");
|
|
463
|
+
let value = match result.output {
|
|
464
|
+
CmdOutput::Json(value) => value,
|
|
465
|
+
other => panic!("wait-ready --json must return JSON output, got {other:?}"),
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
assert_eq!(
|
|
469
|
+
value["ok"],
|
|
470
|
+
json!(false),
|
|
471
|
+
"Contract A: quick-start/wait-ready readiness must not report ok=true while actionable trust remains; got {value}"
|
|
472
|
+
);
|
|
473
|
+
assert_eq!(
|
|
474
|
+
value["status"],
|
|
475
|
+
json!("pending"),
|
|
476
|
+
"Contract A: actionable trust prompt should keep readiness pending, not ready; got {value}"
|
|
477
|
+
);
|
|
478
|
+
assert_eq!(
|
|
479
|
+
value["reason"],
|
|
480
|
+
json!("awaiting_trust_prompt"),
|
|
481
|
+
"Contract A: not-ready reason must identify the startup trust boundary; got {value}"
|
|
482
|
+
);
|
|
483
|
+
assert_eq!(
|
|
484
|
+
value["readiness"]["awaiting_trust_prompt"],
|
|
485
|
+
json!(true),
|
|
486
|
+
"Contract A: ready logic must surface awaiting_trust_prompt=true; got {value}"
|
|
487
|
+
);
|
|
488
|
+
assert_eq!(
|
|
489
|
+
value["readiness"]["ready"],
|
|
490
|
+
json!(false),
|
|
491
|
+
"Contract A: ready must stay false until the actionable trust prompt is cleared; got {value}"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#[test]
|
|
496
|
+
fn contract_b_delivery_does_not_mark_delivered_or_inject_task_while_trust_prompt_is_present() {
|
|
497
|
+
let ws = tmp_dir("b-delivery-trust");
|
|
498
|
+
let store = MessageStore::open(&ws).unwrap();
|
|
499
|
+
let message_id = store
|
|
500
|
+
.create_message_with_id(
|
|
501
|
+
"msg_subprep_trust",
|
|
502
|
+
None,
|
|
503
|
+
"leader",
|
|
504
|
+
"w1",
|
|
505
|
+
"SUB prep probe: reply with one sentence and call report_result exactly once.",
|
|
506
|
+
None,
|
|
507
|
+
true,
|
|
508
|
+
None,
|
|
509
|
+
)
|
|
510
|
+
.unwrap();
|
|
511
|
+
let state = json!({
|
|
512
|
+
"session_name": "team-subprep",
|
|
513
|
+
"agents": {
|
|
514
|
+
"w1": {
|
|
515
|
+
"provider": "codex",
|
|
516
|
+
"status": "running",
|
|
517
|
+
"pane_id": "%7",
|
|
518
|
+
"window": "w1"
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
team_agent::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
523
|
+
let transport = RecordingTransport::new(vec![PROBE5_TRUST_PANE.to_string()]);
|
|
524
|
+
let event_log = EventLog::new(&ws);
|
|
525
|
+
|
|
526
|
+
let outcome =
|
|
527
|
+
deliver_pending_message(&ws, &store, &transport, &message_id, &event_log, &state).unwrap();
|
|
528
|
+
let status = message_status(store.db_path(), &message_id);
|
|
529
|
+
let events = read_events(&ws);
|
|
530
|
+
|
|
531
|
+
assert!(
|
|
532
|
+
!matches!(outcome.status, team_agent::messaging::DeliveryStatus::Delivered),
|
|
533
|
+
"Contract B/MUST-10: physical injection into a trust menu is not provider delivery; outcome={outcome:?}"
|
|
534
|
+
);
|
|
535
|
+
assert!(
|
|
536
|
+
matches!(status.as_str(), "accepted" | "pending" | "queued_until_trust"),
|
|
537
|
+
"Contract B: same message_id must remain retryable/pending until trust clears, not delivered; db_status={status}"
|
|
538
|
+
);
|
|
539
|
+
assert_eq!(
|
|
540
|
+
transport.inject_count(),
|
|
541
|
+
0,
|
|
542
|
+
"Contract B: task text must not be injected into the trust menu; delivery should answer/queue trust first"
|
|
543
|
+
);
|
|
544
|
+
assert!(
|
|
545
|
+
!events.contains("\"message.delivered\"") && !events.contains("turn_open.armed_after_delivery"),
|
|
546
|
+
"Contract B: no message.delivered or turn-open arm before provider main input is ready; events={events}"
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[test]
|
|
551
|
+
fn contract_b_probe5_delivered_without_result_is_rejected_as_lost_task_evidence() {
|
|
552
|
+
let ws = tmp_dir("b-probe5-delivery-sut");
|
|
553
|
+
let store = MessageStore::open(&ws).unwrap();
|
|
554
|
+
let message_id = store
|
|
555
|
+
.create_message_with_id(
|
|
556
|
+
"msg_97eaf80c4958",
|
|
557
|
+
None,
|
|
558
|
+
"leader",
|
|
559
|
+
"w1",
|
|
560
|
+
"SUB prep probe: reply with one sentence and call report_result exactly once.",
|
|
561
|
+
None,
|
|
562
|
+
true,
|
|
563
|
+
None,
|
|
564
|
+
)
|
|
565
|
+
.unwrap();
|
|
566
|
+
let state = json!({
|
|
567
|
+
"session_name": "team-ta-subprep-SUB-PROBE-2-send-turn-idle",
|
|
568
|
+
"agents": {
|
|
569
|
+
"w1": {
|
|
570
|
+
"provider": "codex",
|
|
571
|
+
"status": "running",
|
|
572
|
+
"pane_id": "%7",
|
|
573
|
+
"window": "w1"
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
team_agent::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
578
|
+
let transport = RecordingTransport::new(vec![PROBE5_TRUST_PANE.to_string()]);
|
|
579
|
+
let event_log = EventLog::new(&ws);
|
|
580
|
+
|
|
581
|
+
let outcome =
|
|
582
|
+
deliver_pending_message(&ws, &store, &transport, &message_id, &event_log, &state).unwrap();
|
|
583
|
+
let status = message_status(store.db_path(), &message_id);
|
|
584
|
+
let events = read_events(&ws);
|
|
585
|
+
|
|
586
|
+
assert_eq!(
|
|
587
|
+
outcome.message_id.as_deref(),
|
|
588
|
+
Some("msg_97eaf80c4958"),
|
|
589
|
+
"Contract B: delivery trust gate must preserve the same probe5 message_id for retry; got {outcome:?}"
|
|
590
|
+
);
|
|
591
|
+
assert_eq!(
|
|
592
|
+
status,
|
|
593
|
+
"queued_until_trust",
|
|
594
|
+
"Contract B: worker-at-trust-menu must keep the message retryable, not mark delivered; db_status={status}"
|
|
595
|
+
);
|
|
596
|
+
assert_eq!(
|
|
597
|
+
transport.inject_count(),
|
|
598
|
+
0,
|
|
599
|
+
"Contract B: task protocol block must not be injected into the trust menu"
|
|
600
|
+
);
|
|
601
|
+
assert!(
|
|
602
|
+
!events.contains("\"message.delivered\"") && !events.contains("turn_open.armed_after_delivery"),
|
|
603
|
+
"Contract B: no false delivered event or turn-open arm before trust clears; events={events}"
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#[test]
|
|
608
|
+
fn contract_b_delivery_gate_trusts_startup_handled_state_over_pane_scrollback() {
|
|
609
|
+
let pending_ws = tmp_dir("b-gate-pending-scrollback");
|
|
610
|
+
let pending_store = MessageStore::open(&pending_ws).unwrap();
|
|
611
|
+
let pending_message_id = pending_store
|
|
612
|
+
.create_message_with_id(
|
|
613
|
+
"msg_scrollback_pending",
|
|
614
|
+
None,
|
|
615
|
+
"leader",
|
|
616
|
+
"w1",
|
|
617
|
+
"SUB step2 probe: pending startup trust should defer this task.",
|
|
618
|
+
None,
|
|
619
|
+
true,
|
|
620
|
+
None,
|
|
621
|
+
)
|
|
622
|
+
.unwrap();
|
|
623
|
+
let pending_state = json!({
|
|
624
|
+
"session_name": "team-ta-subroot-SUB-PROBE-2-send-turn-idle",
|
|
625
|
+
"agents": {
|
|
626
|
+
"w1": {
|
|
627
|
+
"provider": "codex",
|
|
628
|
+
"status": "running",
|
|
629
|
+
"startup_prompts": "pending",
|
|
630
|
+
"startup_prompt_status": "pending",
|
|
631
|
+
"pane_id": "%7",
|
|
632
|
+
"window": "w1"
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
team_agent::state::persist::save_runtime_state(&pending_ws, &pending_state).unwrap();
|
|
637
|
+
let pending_transport =
|
|
638
|
+
RecordingTransport::new(vec![SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART.to_string()]);
|
|
639
|
+
let pending_log = EventLog::new(&pending_ws);
|
|
640
|
+
let pending = deliver_pending_message(
|
|
641
|
+
&pending_ws,
|
|
642
|
+
&pending_store,
|
|
643
|
+
&pending_transport,
|
|
644
|
+
&pending_message_id,
|
|
645
|
+
&pending_log,
|
|
646
|
+
&pending_state,
|
|
647
|
+
)
|
|
648
|
+
.unwrap();
|
|
649
|
+
|
|
650
|
+
assert!(
|
|
651
|
+
!matches!(pending.status, team_agent::messaging::DeliveryStatus::Delivered),
|
|
652
|
+
"Contract B reverse: pending startup_prompts with actionable trust text must still defer; outcome={pending:?}"
|
|
653
|
+
);
|
|
654
|
+
assert_eq!(
|
|
655
|
+
message_status(pending_store.db_path(), &pending_message_id),
|
|
656
|
+
"queued_until_trust",
|
|
657
|
+
"Contract B reverse: pending trust must leave message queued_until_trust"
|
|
658
|
+
);
|
|
659
|
+
assert_eq!(
|
|
660
|
+
pending_transport.inject_count(),
|
|
661
|
+
0,
|
|
662
|
+
"Contract B reverse: pending trust must not inject task text into the trust menu"
|
|
663
|
+
);
|
|
664
|
+
assert!(
|
|
665
|
+
read_events(&pending_ws).contains("delivery.deferred_startup_prompt"),
|
|
666
|
+
"Contract B reverse: pending trust defer must remain observable"
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
let handled_ws = tmp_dir("b-gate-handled-scrollback");
|
|
670
|
+
let handled_store = MessageStore::open(&handled_ws).unwrap();
|
|
671
|
+
let handled_message_id = handled_store
|
|
672
|
+
.create_message_with_id(
|
|
673
|
+
"msg_scrollback_handled",
|
|
674
|
+
None,
|
|
675
|
+
"leader",
|
|
676
|
+
"w1",
|
|
677
|
+
"SUB step2 probe: handled startup trust must deliver despite scrollback residue.",
|
|
678
|
+
None,
|
|
679
|
+
true,
|
|
680
|
+
None,
|
|
681
|
+
)
|
|
682
|
+
.unwrap();
|
|
683
|
+
let handled_state = json!({
|
|
684
|
+
"session_name": "team-ta-subroot-SUB-PROBE-2-send-turn-idle",
|
|
685
|
+
"agents": {
|
|
686
|
+
"w1": {
|
|
687
|
+
"provider": "codex",
|
|
688
|
+
"status": "running",
|
|
689
|
+
"startup_prompts": "handled",
|
|
690
|
+
"startup_prompt_status": "handled",
|
|
691
|
+
"startup_prompt_handled": [
|
|
692
|
+
{"prompt": "codex_workspace_trust", "action": "sent_enter"}
|
|
693
|
+
],
|
|
694
|
+
"pane_id": "%7",
|
|
695
|
+
"window": "w1"
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
team_agent::state::persist::save_runtime_state(&handled_ws, &handled_state).unwrap();
|
|
700
|
+
let handled_transport =
|
|
701
|
+
RecordingTransport::new(vec![SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART.to_string()]);
|
|
702
|
+
let handled_log = EventLog::new(&handled_ws);
|
|
703
|
+
let handled = deliver_pending_message(
|
|
704
|
+
&handled_ws,
|
|
705
|
+
&handled_store,
|
|
706
|
+
&handled_transport,
|
|
707
|
+
&handled_message_id,
|
|
708
|
+
&handled_log,
|
|
709
|
+
&handled_state,
|
|
710
|
+
)
|
|
711
|
+
.unwrap();
|
|
712
|
+
|
|
713
|
+
assert!(
|
|
714
|
+
matches!(handled.status, team_agent::messaging::DeliveryStatus::Delivered),
|
|
715
|
+
"Contract B step2/scrollback: startup_prompts=handled must trust state and deliver, even if full pane scrollback still contains `Do you trust` and `› 1. Yes, continue`; outcome={handled:?}"
|
|
716
|
+
);
|
|
717
|
+
assert_eq!(
|
|
718
|
+
message_status(handled_store.db_path(), &handled_message_id),
|
|
719
|
+
"delivered",
|
|
720
|
+
"Contract B step2/scrollback: handled startup prompt state must prevent permanent queued_until_trust; outcome={handled:?}"
|
|
721
|
+
);
|
|
722
|
+
assert_eq!(
|
|
723
|
+
handled_transport.inject_count(),
|
|
724
|
+
1,
|
|
725
|
+
"Contract B step2/scrollback: handled state must allow exactly one physical task inject"
|
|
726
|
+
);
|
|
727
|
+
assert!(
|
|
728
|
+
read_events(&handled_ws).contains("\"message.delivered\""),
|
|
729
|
+
"Contract B step2/scrollback: successful handled-state delivery must emit message.delivered"
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
#[test]
|
|
734
|
+
fn contract_b_step2_retry_delivers_queued_message_after_startup_trust_is_handled() {
|
|
735
|
+
use team_agent::coordinator::{Coordinator, ProviderRegistry, WorkspacePath};
|
|
736
|
+
use team_agent::provider::ProviderAdapter;
|
|
737
|
+
|
|
738
|
+
struct RealAdapterRegistry;
|
|
739
|
+
|
|
740
|
+
impl ProviderRegistry for RealAdapterRegistry {
|
|
741
|
+
fn adapter_for(&self, provider: Provider) -> Box<dyn ProviderAdapter> {
|
|
742
|
+
team_agent::provider::get_adapter(provider)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let ws = tmp_dir("b-step2-trust-retry");
|
|
747
|
+
let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
|
|
748
|
+
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
749
|
+
std::fs::write(&mcp_config, "{}").unwrap();
|
|
750
|
+
let state = json!({
|
|
751
|
+
"session_name": "team-ta-subroot-SUB-PROBE-2-send-turn-idle",
|
|
752
|
+
"team_dir": ws.join("teamdir").to_string_lossy().to_string(),
|
|
753
|
+
"agents": {
|
|
754
|
+
"w1": {
|
|
755
|
+
"provider": "codex",
|
|
756
|
+
"status": "running",
|
|
757
|
+
"startup_prompts": "pending",
|
|
758
|
+
"startup_prompt_status": "pending",
|
|
759
|
+
"pane_id": "%7",
|
|
760
|
+
"window": "w1",
|
|
761
|
+
"mcp_config": mcp_config.to_string_lossy().to_string()
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
team_agent::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
766
|
+
|
|
767
|
+
let store = MessageStore::open(&ws).unwrap();
|
|
768
|
+
let message_id = store
|
|
769
|
+
.create_message_with_id(
|
|
770
|
+
"msg_subroot_step2_retry",
|
|
771
|
+
None,
|
|
772
|
+
"leader",
|
|
773
|
+
"w1",
|
|
774
|
+
"SUB step2 probe: reply with one sentence and call report_result exactly once.",
|
|
775
|
+
None,
|
|
776
|
+
true,
|
|
777
|
+
None,
|
|
778
|
+
)
|
|
779
|
+
.unwrap();
|
|
780
|
+
let transport = RecordingTransport::new(vec![
|
|
781
|
+
PROBE5_TRUST_PANE.to_string(),
|
|
782
|
+
SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART.to_string(),
|
|
783
|
+
PROBE5_READY_AFTER_TRUST_CONSUMED.to_string(),
|
|
784
|
+
PROBE5_READY_AFTER_TRUST_CONSUMED.to_string(),
|
|
785
|
+
])
|
|
786
|
+
.with_session_present(true)
|
|
787
|
+
.with_windows(vec![WindowName::new("w1")]);
|
|
788
|
+
let event_log = EventLog::new(&ws);
|
|
789
|
+
|
|
790
|
+
let deferred =
|
|
791
|
+
deliver_pending_message(&ws, &store, &transport, &message_id, &event_log, &state).unwrap();
|
|
792
|
+
assert!(
|
|
793
|
+
!matches!(deferred.status, team_agent::messaging::DeliveryStatus::Delivered),
|
|
794
|
+
"fixture sanity: the first delivery attempt must defer while the trust prompt is actionable; outcome={deferred:?}"
|
|
795
|
+
);
|
|
796
|
+
assert_eq!(
|
|
797
|
+
message_status(store.db_path(), &message_id),
|
|
798
|
+
"queued_until_trust",
|
|
799
|
+
"fixture sanity: send while trust is present must leave the message queued_until_trust"
|
|
800
|
+
);
|
|
801
|
+
assert_eq!(
|
|
802
|
+
message_delivery_attempts(store.db_path(), &message_id),
|
|
803
|
+
1,
|
|
804
|
+
"fixture sanity: the deferred send consumed exactly one delivery attempt"
|
|
805
|
+
);
|
|
806
|
+
assert_eq!(
|
|
807
|
+
transport.inject_count(),
|
|
808
|
+
0,
|
|
809
|
+
"fixture sanity: task content must not be injected into the startup trust menu"
|
|
810
|
+
);
|
|
811
|
+
assert!(
|
|
812
|
+
read_events(&ws).contains("delivery.deferred_startup_prompt"),
|
|
813
|
+
"fixture sanity: delivery defer must be observable as delivery.deferred_startup_prompt"
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
let coord = Coordinator::new(
|
|
817
|
+
WorkspacePath::new(ws.to_path_buf()),
|
|
818
|
+
Box::new(RealAdapterRegistry),
|
|
819
|
+
Box::new(transport.clone()),
|
|
820
|
+
);
|
|
821
|
+
coord
|
|
822
|
+
.tick()
|
|
823
|
+
.expect("coordinator tick should answer trust and retry queued delivery");
|
|
824
|
+
|
|
825
|
+
let events = read_events(&ws);
|
|
826
|
+
assert_eq!(
|
|
827
|
+
count_event(&events, "startup_prompt_handled"),
|
|
828
|
+
1,
|
|
829
|
+
"Contract B step2 fixture: coordinator tick must handle the startup trust prompt once before retrying delivery; events={events}"
|
|
830
|
+
);
|
|
831
|
+
assert_eq!(
|
|
832
|
+
message_status(store.db_path(), &message_id),
|
|
833
|
+
"delivered",
|
|
834
|
+
"Contract B step2/retry: once startup trust is handled, the same queued_until_trust message_id must be redelivered instead of remaining stuck; events={events}"
|
|
835
|
+
);
|
|
836
|
+
assert!(
|
|
837
|
+
message_delivery_attempts(store.db_path(), &message_id) > 1,
|
|
838
|
+
"Contract B step2/retry: retry after startup_prompt_handled must increase delivery_attempts; attempts={} events={events}",
|
|
839
|
+
message_delivery_attempts(store.db_path(), &message_id)
|
|
840
|
+
);
|
|
841
|
+
assert_eq!(
|
|
842
|
+
transport.inject_count(),
|
|
843
|
+
1,
|
|
844
|
+
"Contract B step2/retry: retry must physically inject the original task into the worker pane exactly once after trust clears"
|
|
845
|
+
);
|
|
846
|
+
assert!(
|
|
847
|
+
events.contains("\"message.delivered\""),
|
|
848
|
+
"Contract B step2/retry: retry must emit message.delivered for the original message_id; events={events}"
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
#[test]
|
|
853
|
+
fn contract_c_lifecycle_launch_command_carries_role_tools_and_real_mcp_context() {
|
|
854
|
+
let root = tmp_dir("c-launch-context");
|
|
855
|
+
let teamdir = root.join("teamdir");
|
|
856
|
+
std::fs::create_dir_all(teamdir.join("agents")).unwrap();
|
|
857
|
+
std::fs::write(
|
|
858
|
+
teamdir.join("TEAM.md"),
|
|
859
|
+
"---\nname: subprep\nobjective: Verify worker can report results.\nprovider: codex\n---\n\nTeam.\n",
|
|
860
|
+
)
|
|
861
|
+
.unwrap();
|
|
862
|
+
std::fs::write(
|
|
863
|
+
teamdir.join("agents").join("w1.md"),
|
|
864
|
+
"---\nname: w1\nrole: Worker must call report_result exactly once.\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nAlways use report_result for completion.\n",
|
|
865
|
+
)
|
|
866
|
+
.unwrap();
|
|
867
|
+
let spec = team_agent::compiler::compile_team(&teamdir).unwrap();
|
|
868
|
+
let spec_path = teamdir.join("team.spec.yaml");
|
|
869
|
+
std::fs::write(&spec_path, team_agent::model::yaml::dumps(&spec)).unwrap();
|
|
870
|
+
|
|
871
|
+
let transport = RecordingTransport::new(vec![PROBE5_READY_AFTER_TRUST_CONSUMED.to_string()]);
|
|
872
|
+
let report = launch_with_transport(&spec_path, false, true, true, &transport)
|
|
873
|
+
.expect("launch should reach transport spawn");
|
|
874
|
+
assert_eq!(
|
|
875
|
+
report.started.len(),
|
|
876
|
+
1,
|
|
877
|
+
"fixture sanity: one Codex worker starts"
|
|
878
|
+
);
|
|
879
|
+
let argv = transport.spawn_argv().join(" ");
|
|
880
|
+
|
|
881
|
+
assert!(
|
|
882
|
+
argv.contains("report_result") || argv.contains("Worker must call report_result"),
|
|
883
|
+
"Contract C/F6.4: lifecycle must pass resolved role/developer instructions into Codex command construction; argv={argv:?}"
|
|
884
|
+
);
|
|
885
|
+
assert!(
|
|
886
|
+
argv.contains("mcp") || argv.contains("team_orchestrator") || argv.contains("team-agent"),
|
|
887
|
+
"Contract C/MUST-8: Codex worker command must include a real Team Agent tool/MCP capability mechanism, not prompt-only state metadata; argv={argv:?}"
|
|
888
|
+
);
|
|
889
|
+
assert!(
|
|
890
|
+
!argv.contains("/tmp/ta-provider-ws")
|
|
891
|
+
&& !argv.contains("worker1")
|
|
892
|
+
&& !argv.contains("teamA"),
|
|
893
|
+
"Contract C: provider command/config must not leak placeholder MCP context; argv={argv:?}"
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
#[test]
|
|
898
|
+
fn contract_c_lifecycle_sources_use_context_aware_provider_builders() {
|
|
899
|
+
let launch = composite_source::composite_source("src/lifecycle/launch.rs");
|
|
900
|
+
let restart_common =
|
|
901
|
+
std::fs::read_to_string(manifest_dir().join("src/lifecycle/restart/common.rs")).unwrap();
|
|
902
|
+
let restart_agent =
|
|
903
|
+
std::fs::read_to_string(manifest_dir().join("src/lifecycle/restart/agent.rs")).unwrap();
|
|
904
|
+
|
|
905
|
+
let offenders = [
|
|
906
|
+
("launch.rs", &launch),
|
|
907
|
+
("restart/common.rs", &restart_common),
|
|
908
|
+
("restart/agent.rs", &restart_agent),
|
|
909
|
+
]
|
|
910
|
+
.into_iter()
|
|
911
|
+
.filter_map(|(name, src)| {
|
|
912
|
+
src.contains(".build_command(auth_mode, None, None, model)")
|
|
913
|
+
.then_some(name)
|
|
914
|
+
})
|
|
915
|
+
.collect::<Vec<_>>();
|
|
916
|
+
|
|
917
|
+
assert!(
|
|
918
|
+
offenders.is_empty(),
|
|
919
|
+
"Contract C: lifecycle launch/restart/start-agent must use build_command_with_tools/build_resume_command_with_context with resolved system_prompt, tools, and per-worker MCP config; context-free callsites remain: {offenders:?}"
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
#[test]
|
|
924
|
+
fn contract_c_provider_mcp_config_is_parameterized_not_placeholder() {
|
|
925
|
+
let adapter = get_adapter(Provider::Codex);
|
|
926
|
+
let config = adapter.mcp_config(AuthMode::Subscription).unwrap();
|
|
927
|
+
let raw = config.raw.to_string();
|
|
928
|
+
let command = config.raw["team_orchestrator"]["command"]
|
|
929
|
+
.as_str()
|
|
930
|
+
.expect("Contract C step3: team_orchestrator MCP config must expose a command string");
|
|
931
|
+
|
|
932
|
+
assert!(
|
|
933
|
+
!raw.contains("/tmp/ta-provider-ws") && !raw.contains("worker1") && !raw.contains("teamA"),
|
|
934
|
+
"Contract C: provider MCP config must be parameterized by real workspace/agent/team, never placeholder-shaped; raw={raw}"
|
|
935
|
+
);
|
|
936
|
+
assert!(
|
|
937
|
+
raw.contains("TEAM_AGENT_WORKSPACE")
|
|
938
|
+
|| raw.contains("--workspace")
|
|
939
|
+
|| raw.contains("team-agent"),
|
|
940
|
+
"Contract C: provider MCP config must point at a real Team Agent MCP server mechanism; raw={raw}"
|
|
941
|
+
);
|
|
942
|
+
assert_ne!(
|
|
943
|
+
command, "team-agent",
|
|
944
|
+
"Contract C step3/MCP startup: provider config must not use bare `team-agent`, because PATH can resolve to an older installed Python CLI; raw={raw}"
|
|
945
|
+
);
|
|
946
|
+
assert!(
|
|
947
|
+
Path::new(command).is_absolute(),
|
|
948
|
+
"Contract C step3/MCP startup: provider config command must be the current team-agent binary as an absolute path (`std::env::current_exe` semantics), not PATH-dependent; command={command:?} raw={raw}"
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
#[test]
|
|
953
|
+
fn contract_d_codex_capture_rejects_team_agent_events_jsonl_as_rollout() {
|
|
954
|
+
let ws = tmp_dir("d-events-rollout");
|
|
955
|
+
let log_dir = ws.join(".team").join("logs");
|
|
956
|
+
std::fs::create_dir_all(&log_dir).unwrap();
|
|
957
|
+
let events_path = log_dir.join("events.jsonl");
|
|
958
|
+
std::fs::write(
|
|
959
|
+
&events_path,
|
|
960
|
+
r#"{"event":"coordinator.boot","workspace":"/tmp/ta-subprep"}"#,
|
|
961
|
+
)
|
|
962
|
+
.unwrap();
|
|
963
|
+
|
|
964
|
+
let adapter = get_adapter(Provider::Codex);
|
|
965
|
+
let captured = adapter.capture_session_id("w1", &ws, 0).unwrap();
|
|
966
|
+
|
|
967
|
+
assert!(
|
|
968
|
+
captured.is_none(),
|
|
969
|
+
"Contract D/F5/N11: Team Agent `.team/logs/events.jsonl` is not a Codex provider transcript and must not become rollout_path; captured={captured:?}"
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
#[test]
|
|
974
|
+
fn contract_d_codex_rollout_requires_provider_lifecycle_shape_or_stays_unknown() {
|
|
975
|
+
let team_agent_events = r#"{"event":"message.delivered","message_id":"msg_1"}"#;
|
|
976
|
+
let classified = classify(
|
|
977
|
+
Provider::Codex,
|
|
978
|
+
team_agent_events,
|
|
979
|
+
ProcessLiveness::Unverifiable,
|
|
980
|
+
0.0,
|
|
981
|
+
)
|
|
982
|
+
.unwrap();
|
|
983
|
+
assert_eq!(
|
|
984
|
+
classified.state,
|
|
985
|
+
TurnState::Unknown,
|
|
986
|
+
"Contract D: Team Agent event rows do not contain Codex lifecycle facts and must classify unknown"
|
|
987
|
+
);
|
|
988
|
+
assert!(
|
|
989
|
+
!classified.state.is_idle_for_takeover(),
|
|
990
|
+
"Contract D/F5/N11: no valid Codex transcript means honest unknown, never fake idle"
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
let ws = tmp_dir("d-valid-rollout");
|
|
994
|
+
let valid = ws.join("codex-session.jsonl");
|
|
995
|
+
let session_meta = serde_json::json!({
|
|
996
|
+
"session_meta": {"payload": {
|
|
997
|
+
"id": "codex-contract-d",
|
|
998
|
+
"cwd": ws.to_string_lossy(),
|
|
999
|
+
"created_at": chrono::Utc::now().to_rfc3339(),
|
|
1000
|
+
}}
|
|
1001
|
+
});
|
|
1002
|
+
std::fs::write(
|
|
1003
|
+
&valid,
|
|
1004
|
+
format!(
|
|
1005
|
+
"{session_meta}\n{}\n",
|
|
1006
|
+
r#"{"type":"event_msg","payload":{"type":"task_complete","turn_id":"ct1"}}"#
|
|
1007
|
+
),
|
|
1008
|
+
)
|
|
1009
|
+
.unwrap();
|
|
1010
|
+
let adapter = get_adapter(Provider::Codex);
|
|
1011
|
+
let captured = adapter.capture_session_id("w1", &ws, 0).unwrap();
|
|
1012
|
+
assert!(
|
|
1013
|
+
captured
|
|
1014
|
+
.as_ref()
|
|
1015
|
+
.and_then(|c| c.rollout_path.as_ref())
|
|
1016
|
+
.is_some_and(|p| p.as_path() == valid),
|
|
1017
|
+
"Contract D: valid Codex lifecycle-shaped transcript may be persisted as rollout_path; captured={captured:?}"
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
fn message_status(db_path: &Path, message_id: &str) -> String {
|
|
1022
|
+
let conn = team_agent::db::schema::open_db(db_path).unwrap();
|
|
1023
|
+
conn.query_row(
|
|
1024
|
+
"select status from messages where message_id = ?1",
|
|
1025
|
+
params![message_id],
|
|
1026
|
+
|row| row.get::<_, String>(0),
|
|
1027
|
+
)
|
|
1028
|
+
.unwrap()
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
fn message_delivery_attempts(db_path: &Path, message_id: &str) -> i64 {
|
|
1032
|
+
let conn = team_agent::db::schema::open_db(db_path).unwrap();
|
|
1033
|
+
conn.query_row(
|
|
1034
|
+
"select delivery_attempts from messages where message_id = ?1",
|
|
1035
|
+
params![message_id],
|
|
1036
|
+
|row| row.get::<_, i64>(0),
|
|
1037
|
+
)
|
|
1038
|
+
.unwrap()
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
fn read_events(workspace: &Path) -> String {
|
|
1042
|
+
std::fs::read_to_string(workspace.join(".team").join("logs").join("events.jsonl"))
|
|
1043
|
+
.unwrap_or_default()
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
fn count_event(events: &str, event_name: &str) -> usize {
|
|
1047
|
+
events
|
|
1048
|
+
.lines()
|
|
1049
|
+
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
|
|
1050
|
+
.filter(|event| event["event"] == event_name)
|
|
1051
|
+
.count()
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
fn count_handled_action(events: &str, action: &str) -> usize {
|
|
1055
|
+
events
|
|
1056
|
+
.lines()
|
|
1057
|
+
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
|
|
1058
|
+
.filter(|event| event["event"] == "startup_prompt_handled")
|
|
1059
|
+
.filter_map(|event| event["handled"].as_array().cloned())
|
|
1060
|
+
.flat_map(|handled| handled.into_iter())
|
|
1061
|
+
.filter(|handled| handled["action"] == action)
|
|
1062
|
+
.count()
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
#[derive(Debug, Default)]
|
|
1066
|
+
struct TransportState {
|
|
1067
|
+
captures: Vec<String>,
|
|
1068
|
+
capture_idx: usize,
|
|
1069
|
+
inject_count: usize,
|
|
1070
|
+
send_keys_count: usize,
|
|
1071
|
+
session_present: bool,
|
|
1072
|
+
windows: Vec<WindowName>,
|
|
1073
|
+
spawn_argv: Vec<Vec<String>>,
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
#[derive(Debug, Clone)]
|
|
1077
|
+
struct RecordingTransport {
|
|
1078
|
+
state: std::sync::Arc<Mutex<TransportState>>,
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
impl RecordingTransport {
|
|
1082
|
+
fn new(captures: Vec<String>) -> Self {
|
|
1083
|
+
Self {
|
|
1084
|
+
state: std::sync::Arc::new(Mutex::new(TransportState {
|
|
1085
|
+
captures,
|
|
1086
|
+
..TransportState::default()
|
|
1087
|
+
})),
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
fn with_session_present(self, present: bool) -> Self {
|
|
1092
|
+
self.state.lock().unwrap().session_present = present;
|
|
1093
|
+
self
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
fn with_windows(self, windows: Vec<WindowName>) -> Self {
|
|
1097
|
+
self.state.lock().unwrap().windows = windows;
|
|
1098
|
+
self
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
fn inject_count(&self) -> usize {
|
|
1102
|
+
self.state.lock().unwrap().inject_count
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
fn send_keys_count(&self) -> usize {
|
|
1106
|
+
self.state.lock().unwrap().send_keys_count
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
fn spawn_argv(&self) -> Vec<String> {
|
|
1110
|
+
self.state
|
|
1111
|
+
.lock()
|
|
1112
|
+
.unwrap()
|
|
1113
|
+
.spawn_argv
|
|
1114
|
+
.first()
|
|
1115
|
+
.cloned()
|
|
1116
|
+
.unwrap_or_default()
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
fn spawn_result(
|
|
1120
|
+
&self,
|
|
1121
|
+
session: &SessionName,
|
|
1122
|
+
window: &WindowName,
|
|
1123
|
+
argv: &[String],
|
|
1124
|
+
pane_prefix: &str,
|
|
1125
|
+
) -> SpawnResult {
|
|
1126
|
+
let mut state = self.state.lock().unwrap();
|
|
1127
|
+
state.spawn_argv.push(argv.to_vec());
|
|
1128
|
+
SpawnResult {
|
|
1129
|
+
pane_id: PaneId::new(format!("%{}{}", pane_prefix, state.spawn_argv.len())),
|
|
1130
|
+
session: session.clone(),
|
|
1131
|
+
window: window.clone(),
|
|
1132
|
+
child_pid: None,
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
impl Transport for RecordingTransport {
|
|
1138
|
+
fn kind(&self) -> BackendKind {
|
|
1139
|
+
BackendKind::Tmux
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
fn spawn_first(
|
|
1143
|
+
&self,
|
|
1144
|
+
session: &SessionName,
|
|
1145
|
+
window: &WindowName,
|
|
1146
|
+
argv: &[String],
|
|
1147
|
+
_cwd: &Path,
|
|
1148
|
+
_env: &BTreeMap<String, String>,
|
|
1149
|
+
) -> Result<SpawnResult, TransportError> {
|
|
1150
|
+
Ok(self.spawn_result(session, window, argv, "first"))
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
fn spawn_into(
|
|
1154
|
+
&self,
|
|
1155
|
+
session: &SessionName,
|
|
1156
|
+
window: &WindowName,
|
|
1157
|
+
argv: &[String],
|
|
1158
|
+
_cwd: &Path,
|
|
1159
|
+
_env: &BTreeMap<String, String>,
|
|
1160
|
+
) -> Result<SpawnResult, TransportError> {
|
|
1161
|
+
Ok(self.spawn_result(session, window, argv, "into"))
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
fn inject(
|
|
1165
|
+
&self,
|
|
1166
|
+
_target: &Target,
|
|
1167
|
+
_payload: &InjectPayload,
|
|
1168
|
+
_submit: Key,
|
|
1169
|
+
_bracketed: bool,
|
|
1170
|
+
) -> Result<InjectReport, TransportError> {
|
|
1171
|
+
self.state.lock().unwrap().inject_count += 1;
|
|
1172
|
+
Ok(InjectReport {
|
|
1173
|
+
stage_reached: InjectStage::Submit,
|
|
1174
|
+
inject_verification: InjectVerification::CaptureContainsToken,
|
|
1175
|
+
submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
1176
|
+
turn_verification: TurnVerification::NotYetObserved,
|
|
1177
|
+
attempts: 1,
|
|
1178
|
+
submit_diagnostics: None,
|
|
1179
|
+
})
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
|
|
1183
|
+
self.state.lock().unwrap().send_keys_count += 1;
|
|
1184
|
+
Ok(())
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
fn capture(
|
|
1188
|
+
&self,
|
|
1189
|
+
_target: &Target,
|
|
1190
|
+
range: CaptureRange,
|
|
1191
|
+
) -> Result<CapturedText, TransportError> {
|
|
1192
|
+
let mut state = self.state.lock().unwrap();
|
|
1193
|
+
let text = state
|
|
1194
|
+
.captures
|
|
1195
|
+
.get(state.capture_idx)
|
|
1196
|
+
.or_else(|| state.captures.last())
|
|
1197
|
+
.cloned()
|
|
1198
|
+
.unwrap_or_default();
|
|
1199
|
+
state.capture_idx = state.capture_idx.saturating_add(1);
|
|
1200
|
+
Ok(CapturedText { text, range })
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
|
|
1204
|
+
match field {
|
|
1205
|
+
PaneField::PaneWidth => Ok(Some("120".to_string())),
|
|
1206
|
+
_ => Ok(None),
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
fn liveness(
|
|
1211
|
+
&self,
|
|
1212
|
+
_pane: &PaneId,
|
|
1213
|
+
) -> Result<team_agent::transport::PaneLiveness, TransportError> {
|
|
1214
|
+
Ok(team_agent::transport::PaneLiveness::Live)
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
1218
|
+
Ok(Vec::new())
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
1222
|
+
Ok(self.state.lock().unwrap().session_present)
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
1226
|
+
Ok(self.state.lock().unwrap().windows.clone())
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
fn set_session_env(
|
|
1230
|
+
&self,
|
|
1231
|
+
_session: &SessionName,
|
|
1232
|
+
_key: &str,
|
|
1233
|
+
_value: &str,
|
|
1234
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
1235
|
+
Ok(SetEnvOutcome::Applied)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
1239
|
+
Ok(())
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
1243
|
+
Ok(())
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
1247
|
+
Ok(AttachOutcome::Attached)
|
|
1248
|
+
}
|
|
1249
|
+
}
|