@team-agent/installer 0.5.61 → 0.5.63
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 +15 -866
- 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 +139 -375
- 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 -49
- package/crates/team-agent/src/coordinator/tick.rs +23 -40
- 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 -66
- 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 -15
- 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 -13
- 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,849 @@
|
|
|
1
|
+
//! #264 behavioral diff regression contracts (D1-D9).
|
|
2
|
+
//!
|
|
3
|
+
//! Source of truth: Python runtime 0.2.11 (`~/.team-agent/runtime/0.2.11/src/team_agent/`).
|
|
4
|
+
//! Every asserted value below was grounded by executing the Python truth source
|
|
5
|
+
//! (CodexAdapter.build_command / compile_system_prompt / providers.shell_command) —
|
|
6
|
+
//! see `.team/artifacts/264-behavioral-diff-report.md` for the per-item file:line evidence.
|
|
7
|
+
//!
|
|
8
|
+
//! D1: codex developer_instructions triple escape (\\ then " then \n) — codex.py:120-121
|
|
9
|
+
//! D2: codex MCP `-c mcp_servers.<name>.tool_timeout_sec=600.0` — codex.py:129
|
|
10
|
+
//! D3: codex `--profile <CODEX_PROFILE>` + `-c <codex_config>` items — codex.py:105-107,117-118 / provider_env.py:62-79
|
|
11
|
+
//! D4: worker shell env TEAM_AGENT_ID=<worker self> (overrides parent) — providers.py:131
|
|
12
|
+
//! D5: fresh launch spawn cwd == workspace (tmux cwd + state.spawn_cwd) — providers.py:145 / launch/core.py:253
|
|
13
|
+
//! D6: compiled system prompt first section is identity — prompt.py:39
|
|
14
|
+
//! D7: runtime.fast → codex fast-mode toggle "/fast" after spawn — launch/core.py:235-237
|
|
15
|
+
//! D8: launch session_name fallback = team-{name}, not a constant — launch/core.py:56
|
|
16
|
+
//! D9: profile env_unset truly unsets in the worker shell line — providers.py:142-145 / provider_env.py:82-90
|
|
17
|
+
|
|
18
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
19
|
+
|
|
20
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
21
|
+
mod hermetic_guard;
|
|
22
|
+
#[allow(dead_code)]
|
|
23
|
+
fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
|
|
24
|
+
|
|
25
|
+
use std::collections::BTreeMap;
|
|
26
|
+
use std::path::{Path, PathBuf};
|
|
27
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
28
|
+
use std::sync::Mutex;
|
|
29
|
+
|
|
30
|
+
use serial_test::serial;
|
|
31
|
+
use team_agent::lifecycle::{launch_with_transport, quick_start_with_transport_in_workspace};
|
|
32
|
+
use team_agent::state::persist::load_runtime_state;
|
|
33
|
+
use team_agent::tmux_backend::{CommandOutput, CommandRunner, TmuxBackend};
|
|
34
|
+
use team_agent::transport::{
|
|
35
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
36
|
+
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome,
|
|
37
|
+
SpawnResult, SubmitVerification, Target, Transport, TransportError, TurnVerification,
|
|
38
|
+
WindowName,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const ANCESTRY_KEY: &str = "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON";
|
|
42
|
+
const NEUTRAL_ANCESTRY: &str = "[\"/bin/zsh\"]";
|
|
43
|
+
|
|
44
|
+
/// D1+D2+D3: one codex argv golden locks the three codex command-construction gaps.
|
|
45
|
+
///
|
|
46
|
+
/// Python golden (executed 0.2.11 CodexAdapter.build_command with the same profile values):
|
|
47
|
+
/// `codex --no-alt-screen --disable shell_snapshot --disable apps --profile relay-fast
|
|
48
|
+
/// --sandbox read-only --ask-for-approval on-request --model gpt-5.5
|
|
49
|
+
/// -c model_provider="relay" -c model_providers.relay.base_url="https://relay.example/v1"
|
|
50
|
+
/// -c model_providers.relay.env_key="TEAM_AGENT_PROVIDER_API_KEY"
|
|
51
|
+
/// -c model_providers.relay.wire_api="chat" -c model_providers.relay.name="Relay"
|
|
52
|
+
/// -c developer_instructions="...\n...ESCAPE-CHECK back\\slash \"dq\" end..."
|
|
53
|
+
/// -c mcp_servers.team_orchestrator.command="..." ... -c mcp_servers.team_orchestrator.tool_timeout_sec=600.0`
|
|
54
|
+
#[test]
|
|
55
|
+
#[serial(env)]
|
|
56
|
+
fn d1_d2_d3_codex_argv_matches_python_golden_escape_timeout_profile() {
|
|
57
|
+
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
58
|
+
let ws = tmp_dir("d123-codex-argv");
|
|
59
|
+
let team_dir = write_team(
|
|
60
|
+
&ws,
|
|
61
|
+
&TeamFixture {
|
|
62
|
+
name: "d123team",
|
|
63
|
+
provider: "codex",
|
|
64
|
+
fast: false,
|
|
65
|
+
agents: &[AgentDoc {
|
|
66
|
+
id: "worker_a",
|
|
67
|
+
role: "Codex Diff Worker",
|
|
68
|
+
provider: "codex",
|
|
69
|
+
auth_mode: "compatible_api",
|
|
70
|
+
model: Some("gpt-5.5"),
|
|
71
|
+
profile: Some("relay"),
|
|
72
|
+
tools: &["mcp_team"],
|
|
73
|
+
body: "ESCAPE-CHECK back\\slash \"dq\" end",
|
|
74
|
+
}],
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
write_relay_profile(&team_dir);
|
|
78
|
+
seed_healthy_coordinator(&ws);
|
|
79
|
+
let transport = RecordingTransport::new();
|
|
80
|
+
|
|
81
|
+
quick_start_with_transport_in_workspace(
|
|
82
|
+
&ws,
|
|
83
|
+
&team_dir,
|
|
84
|
+
None,
|
|
85
|
+
true,
|
|
86
|
+
Some("d123team"),
|
|
87
|
+
&transport,
|
|
88
|
+
)
|
|
89
|
+
.expect("quick-start should reach codex command construction");
|
|
90
|
+
let spawn = transport.single_spawn();
|
|
91
|
+
let argv = &spawn.argv;
|
|
92
|
+
let mut failures = Vec::new();
|
|
93
|
+
|
|
94
|
+
// D1: triple escape, Python order \\ -> \" -> \n (codex.py:120-121).
|
|
95
|
+
let dev = argv
|
|
96
|
+
.iter()
|
|
97
|
+
.find(|arg| arg.starts_with("developer_instructions=\""));
|
|
98
|
+
match dev {
|
|
99
|
+
None => failures
|
|
100
|
+
.push("D1: no `-c developer_instructions=\"...\"` value in codex argv".to_string()),
|
|
101
|
+
Some(dev) => {
|
|
102
|
+
if dev.contains('\n') {
|
|
103
|
+
failures.push(format!(
|
|
104
|
+
"D1: developer_instructions value contains RAW newlines; Python escapes them as literal \\n (codex.py:120). head={:?}",
|
|
105
|
+
dev.chars().take(120).collect::<String>()
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
// Raw marker `ESCAPE-CHECK back\slash "dq" end` must arrive as `back\\slash \"dq\" end`.
|
|
109
|
+
let escaped_marker = "ESCAPE-CHECK back\\\\slash \\\"dq\\\" end";
|
|
110
|
+
if !dev.contains(escaped_marker) {
|
|
111
|
+
failures.push(format!(
|
|
112
|
+
"D1: developer_instructions must contain the Python-escaped marker {escaped_marker:?} (backslash doubled before quote/newline escaping)"
|
|
113
|
+
));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// D2: codex.py:129 — every MCP server gets tool_timeout_sec=600.0.
|
|
119
|
+
let timeout_item = "mcp_servers.team_orchestrator.tool_timeout_sec=600.0";
|
|
120
|
+
if !argv.iter().any(|arg| arg == timeout_item) {
|
|
121
|
+
failures.push(format!("D2: codex argv missing `-c {timeout_item}`"));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// D3: profile command overrides (provider_env.py:62-79 + codex.py:105-107,117-118).
|
|
125
|
+
if flag_value(argv, "--profile").as_deref() != Some("relay-fast") {
|
|
126
|
+
failures.push(format!(
|
|
127
|
+
"D3: codex argv must carry `--profile relay-fast` from profile CODEX_PROFILE; got {:?}",
|
|
128
|
+
flag_value(argv, "--profile")
|
|
129
|
+
));
|
|
130
|
+
}
|
|
131
|
+
for config in [
|
|
132
|
+
"model_provider=\"relay\"",
|
|
133
|
+
"model_providers.relay.base_url=\"https://relay.example/v1\"",
|
|
134
|
+
"model_providers.relay.env_key=\"TEAM_AGENT_PROVIDER_API_KEY\"",
|
|
135
|
+
"model_providers.relay.wire_api=\"chat\"",
|
|
136
|
+
"model_providers.relay.name=\"Relay\"",
|
|
137
|
+
] {
|
|
138
|
+
if !argv.iter().any(|arg| arg == config) {
|
|
139
|
+
failures.push(format!(
|
|
140
|
+
"D3: codex argv missing `-c {config}` (profile codex_config)"
|
|
141
|
+
));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Python golden order: --profile before --sandbox; codex_config -c before developer_instructions.
|
|
145
|
+
if let (Some(profile_idx), Some(sandbox_idx)) = (
|
|
146
|
+
argv.iter().position(|arg| arg == "--profile"),
|
|
147
|
+
argv.iter().position(|arg| arg == "--sandbox"),
|
|
148
|
+
) {
|
|
149
|
+
if profile_idx > sandbox_idx {
|
|
150
|
+
failures
|
|
151
|
+
.push("D3: `--profile` must precede `--sandbox` (codex.py:105-113)".to_string());
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if let (Some(config_idx), Some(dev_idx)) = (
|
|
155
|
+
argv.iter()
|
|
156
|
+
.position(|arg| arg == "model_provider=\"relay\""),
|
|
157
|
+
argv.iter()
|
|
158
|
+
.position(|arg| arg.starts_with("developer_instructions=")),
|
|
159
|
+
) {
|
|
160
|
+
if config_idx > dev_idx {
|
|
161
|
+
failures.push(
|
|
162
|
+
"D3: profile codex_config `-c` items must precede developer_instructions (codex.py:117-121)"
|
|
163
|
+
.to_string(),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
assert!(
|
|
169
|
+
failures.is_empty(),
|
|
170
|
+
"D1/D2/D3 codex argv contract failed:\n{}\nargv={argv:?}",
|
|
171
|
+
failures.join("\n")
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// D4: providers.py:131 — the worker shell env must export TEAM_AGENT_ID=<the worker itself>,
|
|
176
|
+
/// overriding any TEAM_AGENT_ID inherited from the launching process (e.g. an add-agent issued
|
|
177
|
+
/// from another worker's MCP server, whose environ carries the CALLER's TEAM_AGENT_ID).
|
|
178
|
+
#[test]
|
|
179
|
+
#[serial(env)]
|
|
180
|
+
fn d4_worker_shell_env_sets_team_agent_id_to_worker_self() {
|
|
181
|
+
let _guard = EnvGuard::set(&[
|
|
182
|
+
(ANCESTRY_KEY, NEUTRAL_ANCESTRY),
|
|
183
|
+
("TEAM_AGENT_ID", "stale-mcp-caller"),
|
|
184
|
+
]);
|
|
185
|
+
let ws = tmp_dir("d4-spawn-env");
|
|
186
|
+
let team_dir = write_team(&ws, &simple_codex_team("d4team", "worker_a", "Env Worker"));
|
|
187
|
+
seed_healthy_coordinator(&ws);
|
|
188
|
+
let transport = RecordingTransport::new();
|
|
189
|
+
|
|
190
|
+
quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("d4team"), &transport)
|
|
191
|
+
.expect("quick-start should spawn the worker");
|
|
192
|
+
let spawn = transport.single_spawn();
|
|
193
|
+
let mut failures = Vec::new();
|
|
194
|
+
match spawn.env.get("TEAM_AGENT_ID").map(String::as_str) {
|
|
195
|
+
Some("worker_a") => {}
|
|
196
|
+
other => failures.push(format!(
|
|
197
|
+
"D4: worker spawn env must set TEAM_AGENT_ID=worker_a (Python providers.py:131), overriding the inherited caller value; got {other:?}"
|
|
198
|
+
)),
|
|
199
|
+
}
|
|
200
|
+
match spawn.env.get("TEAM_AGENT_AGENT_ID").map(String::as_str) {
|
|
201
|
+
Some("worker_a") => {}
|
|
202
|
+
other => failures.push(format!(
|
|
203
|
+
"D4: TEAM_AGENT_AGENT_ID=worker_a must stay alongside TEAM_AGENT_ID (#229 compat); got {other:?}"
|
|
204
|
+
)),
|
|
205
|
+
}
|
|
206
|
+
assert!(
|
|
207
|
+
failures.is_empty(),
|
|
208
|
+
"D4 worker shell env contract failed:\n{}\nenv_keys={:?}",
|
|
209
|
+
failures.join("\n"),
|
|
210
|
+
spawn.env.keys().collect::<Vec<_>>()
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// D5: providers.py:145 + launch/core.py:253 — fresh launch runs the worker with cwd=WORKSPACE
|
|
215
|
+
/// (`cd <workspace> && ... exec`), and persists state.agents[*].spawn_cwd = workspace.
|
|
216
|
+
/// RS fork/add (launch.rs:2304) and restart (restart/common.rs:125-133) already use workspace;
|
|
217
|
+
/// fresh launch passing team_dir is the odd one out.
|
|
218
|
+
#[test]
|
|
219
|
+
#[serial(env)]
|
|
220
|
+
fn d5_fresh_launch_spawn_cwd_and_state_use_workspace() {
|
|
221
|
+
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
222
|
+
let ws = tmp_dir("d5-spawn-cwd");
|
|
223
|
+
let team_dir = write_team(&ws, &simple_codex_team("d5team", "worker_a", "Cwd Worker"));
|
|
224
|
+
seed_healthy_coordinator(&ws);
|
|
225
|
+
let transport = RecordingTransport::new();
|
|
226
|
+
|
|
227
|
+
quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("d5team"), &transport)
|
|
228
|
+
.expect("quick-start should spawn the worker");
|
|
229
|
+
let spawn = transport.single_spawn();
|
|
230
|
+
let mut failures = Vec::new();
|
|
231
|
+
if spawn.cwd != ws {
|
|
232
|
+
failures.push(format!(
|
|
233
|
+
"D5: transport spawn cwd must be the workspace {} (Python providers.py:145 `cd <workspace>`); got {}",
|
|
234
|
+
ws.display(),
|
|
235
|
+
spawn.cwd.display()
|
|
236
|
+
));
|
|
237
|
+
}
|
|
238
|
+
let state = load_runtime_state(&ws).expect("load runtime state after quick-start");
|
|
239
|
+
let spawn_cwd = state
|
|
240
|
+
.pointer("/agents/worker_a/spawn_cwd")
|
|
241
|
+
.and_then(serde_json::Value::as_str)
|
|
242
|
+
.map(str::to_string);
|
|
243
|
+
if spawn_cwd.as_deref() != Some(&*ws.to_string_lossy()) {
|
|
244
|
+
failures.push(format!(
|
|
245
|
+
"D5: persisted agents.worker_a.spawn_cwd must equal the workspace {:?} (Python launch/core.py:253); got {spawn_cwd:?}",
|
|
246
|
+
ws.to_string_lossy()
|
|
247
|
+
));
|
|
248
|
+
}
|
|
249
|
+
assert!(
|
|
250
|
+
failures.is_empty(),
|
|
251
|
+
"D5 fresh-launch spawn cwd contract failed:\n{}",
|
|
252
|
+
failures.join("\n")
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// D6: prompt.py:39 — chunks = [identity, TEAMMATE_SYSTEM_PROMPT, ...]: the compiled system
|
|
257
|
+
/// prompt must START with the worker identity line (live Python worker argv confirms this),
|
|
258
|
+
/// with the shared runtime contract section second.
|
|
259
|
+
#[test]
|
|
260
|
+
#[serial(env)]
|
|
261
|
+
fn d6_compiled_system_prompt_first_section_is_identity() {
|
|
262
|
+
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
263
|
+
let ws = tmp_dir("d6-prompt-order");
|
|
264
|
+
let team_dir = write_team(
|
|
265
|
+
&ws,
|
|
266
|
+
&TeamFixture {
|
|
267
|
+
name: "d6team",
|
|
268
|
+
provider: "claude",
|
|
269
|
+
fast: false,
|
|
270
|
+
agents: &[AgentDoc {
|
|
271
|
+
id: "worker_a",
|
|
272
|
+
role: "Prompt Order Worker",
|
|
273
|
+
provider: "claude",
|
|
274
|
+
auth_mode: "subscription",
|
|
275
|
+
model: Some("fake"),
|
|
276
|
+
profile: None,
|
|
277
|
+
tools: &["mcp_team"],
|
|
278
|
+
body: "ROLE BODY SENTINEL for prompt ordering.",
|
|
279
|
+
}],
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
seed_healthy_coordinator(&ws);
|
|
283
|
+
let transport = RecordingTransport::new();
|
|
284
|
+
|
|
285
|
+
quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("d6team"), &transport)
|
|
286
|
+
.expect("quick-start should spawn the claude worker");
|
|
287
|
+
let spawn = transport.single_spawn();
|
|
288
|
+
let prompt = flag_value(&spawn.argv, "--append-system-prompt")
|
|
289
|
+
.expect("claude argv must carry --append-system-prompt");
|
|
290
|
+
|
|
291
|
+
let identity = "You are Team Agent worker `worker_a` with role `Prompt Order Worker`. \
|
|
292
|
+
When asked about your role or identity, answer with this Team Agent worker identity first, \
|
|
293
|
+
not only the generic provider product identity.";
|
|
294
|
+
assert!(
|
|
295
|
+
prompt.starts_with(identity),
|
|
296
|
+
"D6: compiled system prompt must START with the identity section (Python prompt.py:39 \
|
|
297
|
+
chunks=[identity, TEAMMATE_SYSTEM_PROMPT, ...]; live ps confirms identity-first); got head={:?}",
|
|
298
|
+
prompt.chars().take(220).collect::<String>()
|
|
299
|
+
);
|
|
300
|
+
let contract_idx = prompt
|
|
301
|
+
.find("# Team Agent Teammate Runtime Contract")
|
|
302
|
+
.expect("prompt must contain the shared runtime contract section");
|
|
303
|
+
assert!(
|
|
304
|
+
contract_idx >= identity.len(),
|
|
305
|
+
"D6: the runtime contract section must come AFTER the identity section; contract_idx={contract_idx}"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// D7: launch/core.py:235-237 — runtime.fast + provider==codex must toggle codex fast mode
|
|
310
|
+
/// after spawn by sending `/fast` + Enter to the worker pane (tmux_prompt.py:124-129).
|
|
311
|
+
#[test]
|
|
312
|
+
#[serial(env)]
|
|
313
|
+
fn d7_runtime_fast_sends_codex_fast_mode_toggle() {
|
|
314
|
+
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
315
|
+
let ws = tmp_dir("d7-fast-mode");
|
|
316
|
+
let mut fixture = simple_codex_team("d7team", "worker_a", "Fast Worker");
|
|
317
|
+
fixture.fast = true;
|
|
318
|
+
let team_dir = write_team(&ws, &fixture);
|
|
319
|
+
seed_healthy_coordinator(&ws);
|
|
320
|
+
let transport = RecordingTransport::new();
|
|
321
|
+
|
|
322
|
+
quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("d7team"), &transport)
|
|
323
|
+
.expect("quick-start should spawn the codex worker");
|
|
324
|
+
transport.single_spawn();
|
|
325
|
+
let sent = transport.sent_text();
|
|
326
|
+
assert!(
|
|
327
|
+
sent.iter().any(|text| text.contains("/fast")),
|
|
328
|
+
"D7: TEAM.md `fast: true` (compiled to runtime.fast) must send the `/fast` toggle to the \
|
|
329
|
+
codex worker after spawn (Python launch/core.py:235-237 + tmux_prompt.py:124-129); recorded \
|
|
330
|
+
keystrokes/injections={sent:?}"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// D8: launch/core.py:56 — when the spec carries no runtime.session_name, the session falls
|
|
335
|
+
/// back to `team-{team.name}`, never a fixed constant.
|
|
336
|
+
#[test]
|
|
337
|
+
#[serial(env)]
|
|
338
|
+
fn d8_launch_session_name_fallback_derives_from_team_name() {
|
|
339
|
+
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
340
|
+
let ws = tmp_dir("d8-session-name");
|
|
341
|
+
let team_dir = write_team(
|
|
342
|
+
&ws,
|
|
343
|
+
&simple_codex_team("d8team", "worker_a", "Session Worker"),
|
|
344
|
+
);
|
|
345
|
+
let spec = team_agent::compiler::compile_team(&team_dir).expect("compile fixture team");
|
|
346
|
+
let spec_text = team_agent::model::yaml::dumps(&spec);
|
|
347
|
+
let stripped: String = spec_text
|
|
348
|
+
.lines()
|
|
349
|
+
.filter(|line| !line.trim_start().starts_with("session_name:"))
|
|
350
|
+
.map(|line| format!("{line}\n"))
|
|
351
|
+
.collect();
|
|
352
|
+
assert!(
|
|
353
|
+
stripped.len() < spec_text.len(),
|
|
354
|
+
"fixture must actually remove the compiled session_name line"
|
|
355
|
+
);
|
|
356
|
+
let spec_path = team_dir.join("team.spec.yaml");
|
|
357
|
+
std::fs::write(&spec_path, stripped).unwrap();
|
|
358
|
+
let transport = RecordingTransport::new();
|
|
359
|
+
|
|
360
|
+
launch_with_transport(&spec_path, false, true, true, &transport)
|
|
361
|
+
.expect("launch should spawn the worker without an explicit session_name");
|
|
362
|
+
let spawn = transport.single_spawn();
|
|
363
|
+
assert_eq!(
|
|
364
|
+
spawn.session, "team-d8team",
|
|
365
|
+
"D8: session_name fallback must derive `team-{{team.name}}` (Python launch/core.py:56), \
|
|
366
|
+
not a constant; argv={:?}",
|
|
367
|
+
spawn.argv
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// D9: provider_env.py:82-90 + providers.py:142-145 — profile env_unset keys (codex
|
|
372
|
+
/// compatible_api → OPENAI_API_KEY / OPENAI_BASE_URL) must be REALLY unset in the worker
|
|
373
|
+
/// shell line (Python sources the env file whose first lines are `unset <KEY>`), because the
|
|
374
|
+
/// tmux server environment can still carry stale values that plain map-removal cannot clear.
|
|
375
|
+
#[test]
|
|
376
|
+
#[serial(env)]
|
|
377
|
+
fn d9_profile_env_unset_reaches_worker_shell_line() {
|
|
378
|
+
let _guard = EnvGuard::set(&[
|
|
379
|
+
(ANCESTRY_KEY, NEUTRAL_ANCESTRY),
|
|
380
|
+
("OPENAI_API_KEY", "stale-parent-key"),
|
|
381
|
+
]);
|
|
382
|
+
let ws = tmp_dir("d9-env-unset");
|
|
383
|
+
let team_dir = write_team(
|
|
384
|
+
&ws,
|
|
385
|
+
&TeamFixture {
|
|
386
|
+
name: "d9team",
|
|
387
|
+
provider: "codex",
|
|
388
|
+
fast: false,
|
|
389
|
+
agents: &[AgentDoc {
|
|
390
|
+
id: "worker_a",
|
|
391
|
+
role: "Unset Worker",
|
|
392
|
+
provider: "codex",
|
|
393
|
+
auth_mode: "compatible_api",
|
|
394
|
+
model: Some("gpt-5.5"),
|
|
395
|
+
profile: Some("relay-nokey"),
|
|
396
|
+
tools: &["mcp_team"],
|
|
397
|
+
body: "Unset worker.",
|
|
398
|
+
}],
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
// No API_KEY on purpose: OPENAI_API_KEY lands in env_unset (provider_env.py:51-52) and is
|
|
402
|
+
// never re-exported, so the stale tmux-server/parent value leaks unless the shell really
|
|
403
|
+
// unsets it. (With API_KEY present the overlay re-export would mask the missing unset.)
|
|
404
|
+
let profiles = team_dir.join("profiles");
|
|
405
|
+
std::fs::create_dir_all(&profiles).unwrap();
|
|
406
|
+
std::fs::write(
|
|
407
|
+
profiles.join("relay-nokey.env"),
|
|
408
|
+
"AUTH_MODE=compatible_api\nBASE_URL=https://relay.example/v1\n",
|
|
409
|
+
)
|
|
410
|
+
.unwrap();
|
|
411
|
+
let spec = team_agent::compiler::compile_team(&team_dir).expect("compile fixture team");
|
|
412
|
+
let spec_path = team_dir.join("team.spec.yaml");
|
|
413
|
+
std::fs::write(&spec_path, team_agent::model::yaml::dumps(&spec)).unwrap();
|
|
414
|
+
|
|
415
|
+
let runner = RecordingRunner::new();
|
|
416
|
+
let argv_log = runner.argv_log();
|
|
417
|
+
let backend = TmuxBackend::with_runner(Box::new(runner));
|
|
418
|
+
// The spawn may be marked dead by the canned runner after the shell line is built;
|
|
419
|
+
// the contract only needs the recorded `sh -lc` line, so the report itself is not asserted.
|
|
420
|
+
let _ = launch_with_transport(&spec_path, false, true, true, &backend);
|
|
421
|
+
|
|
422
|
+
let recorded = argv_log.lock().unwrap();
|
|
423
|
+
let shell_line = recorded
|
|
424
|
+
.iter()
|
|
425
|
+
.flatten()
|
|
426
|
+
.find(|arg| arg.starts_with("cd ") && arg.contains(" exec "))
|
|
427
|
+
.cloned()
|
|
428
|
+
.unwrap_or_else(|| {
|
|
429
|
+
panic!("no worker `cd … exec …` shell line recorded; tmux calls={recorded:?}")
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
let env_file = ws.join(".team/runtime/provider-env/worker_a.env");
|
|
433
|
+
let mut failures = Vec::new();
|
|
434
|
+
let env_file_text = std::fs::read_to_string(&env_file).unwrap_or_default();
|
|
435
|
+
if !env_file_text.contains("unset OPENAI_API_KEY") {
|
|
436
|
+
failures.push(format!(
|
|
437
|
+
"D9: provider env file must contain `unset OPENAI_API_KEY` (Python provider_env.py:86); path={} content={env_file_text:?}",
|
|
438
|
+
env_file.display()
|
|
439
|
+
));
|
|
440
|
+
}
|
|
441
|
+
let unsets_inline = shell_line.contains("unset OPENAI_API_KEY");
|
|
442
|
+
let sources_env_file = shell_line.contains(&*env_file.to_string_lossy());
|
|
443
|
+
if !(unsets_inline || sources_env_file) {
|
|
444
|
+
failures.push(format!(
|
|
445
|
+
"D9: worker shell line must really unset OPENAI_API_KEY — either inline `unset OPENAI_API_KEY` \
|
|
446
|
+
or by sourcing the env file `{}` before exec (Python providers.py:142-145); shell_line={shell_line:?}",
|
|
447
|
+
env_file.display()
|
|
448
|
+
));
|
|
449
|
+
}
|
|
450
|
+
assert!(
|
|
451
|
+
failures.is_empty(),
|
|
452
|
+
"D9 env_unset shell contract failed:\n{}",
|
|
453
|
+
failures.join("\n")
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// fixtures
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
struct TeamFixture<'a> {
|
|
462
|
+
name: &'a str,
|
|
463
|
+
provider: &'a str,
|
|
464
|
+
fast: bool,
|
|
465
|
+
agents: &'a [AgentDoc<'a>],
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
struct AgentDoc<'a> {
|
|
469
|
+
id: &'a str,
|
|
470
|
+
role: &'a str,
|
|
471
|
+
provider: &'a str,
|
|
472
|
+
auth_mode: &'a str,
|
|
473
|
+
model: Option<&'a str>,
|
|
474
|
+
profile: Option<&'a str>,
|
|
475
|
+
tools: &'a [&'a str],
|
|
476
|
+
body: &'a str,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
fn simple_codex_team(
|
|
480
|
+
name: &'static str,
|
|
481
|
+
agent: &'static str,
|
|
482
|
+
role: &'static str,
|
|
483
|
+
) -> TeamFixture<'static> {
|
|
484
|
+
let agents: &'static [AgentDoc<'static>] = Box::leak(Box::new([AgentDoc {
|
|
485
|
+
id: agent,
|
|
486
|
+
role,
|
|
487
|
+
provider: "codex",
|
|
488
|
+
auth_mode: "subscription",
|
|
489
|
+
model: Some("gpt-5.5"),
|
|
490
|
+
profile: None,
|
|
491
|
+
tools: &["mcp_team"],
|
|
492
|
+
body: Box::leak(format!("{role}.").into_boxed_str()),
|
|
493
|
+
}]));
|
|
494
|
+
TeamFixture {
|
|
495
|
+
name,
|
|
496
|
+
provider: "codex",
|
|
497
|
+
fast: false,
|
|
498
|
+
agents,
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fn write_team(workspace: &Path, fixture: &TeamFixture<'_>) -> PathBuf {
|
|
503
|
+
let team = workspace.join(fixture.name);
|
|
504
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
505
|
+
let fast = if fixture.fast { "fast: true\n" } else { "" };
|
|
506
|
+
std::fs::write(
|
|
507
|
+
team.join("TEAM.md"),
|
|
508
|
+
format!(
|
|
509
|
+
"---\nname: {}\nobjective: 264 behavioral diff fixture.\nprovider: {}\n{fast}---\n\nTeam fixture.\n",
|
|
510
|
+
fixture.name, fixture.provider
|
|
511
|
+
),
|
|
512
|
+
)
|
|
513
|
+
.unwrap();
|
|
514
|
+
for agent in fixture.agents {
|
|
515
|
+
let tools = agent
|
|
516
|
+
.tools
|
|
517
|
+
.iter()
|
|
518
|
+
.map(|tool| format!(" - {tool}\n"))
|
|
519
|
+
.collect::<String>();
|
|
520
|
+
let model = agent
|
|
521
|
+
.model
|
|
522
|
+
.map(|model| format!("model: {model}\n"))
|
|
523
|
+
.unwrap_or_default();
|
|
524
|
+
let profile = agent
|
|
525
|
+
.profile
|
|
526
|
+
.map(|profile| format!("profile: {profile}\n"))
|
|
527
|
+
.unwrap_or_default();
|
|
528
|
+
std::fs::write(
|
|
529
|
+
team.join("agents").join(format!("{}.md", agent.id)),
|
|
530
|
+
format!(
|
|
531
|
+
"---\nname: {}\nrole: {}\nprovider: {}\n{model}auth_mode: {}\n{profile}tools:\n{tools}---\n\n{}\n",
|
|
532
|
+
agent.id, agent.role, agent.provider, agent.auth_mode, agent.body
|
|
533
|
+
),
|
|
534
|
+
)
|
|
535
|
+
.unwrap();
|
|
536
|
+
}
|
|
537
|
+
team
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/// Profile values mirror the Python golden run: provider_env.py:62-79 turns these into
|
|
541
|
+
/// codex_profile=relay-fast plus the five `-c` codex_config items asserted in D3.
|
|
542
|
+
fn write_relay_profile(team_dir: &Path) {
|
|
543
|
+
let profiles = team_dir.join("profiles");
|
|
544
|
+
std::fs::create_dir_all(&profiles).unwrap();
|
|
545
|
+
std::fs::write(
|
|
546
|
+
profiles.join("relay.env"),
|
|
547
|
+
"AUTH_MODE=compatible_api\n\
|
|
548
|
+
BASE_URL=https://relay.example/v1\n\
|
|
549
|
+
API_KEY=sk-relay-264\n\
|
|
550
|
+
MODEL_PROVIDER=relay\n\
|
|
551
|
+
WIRE_API=chat\n\
|
|
552
|
+
PROVIDER_NAME=Relay\n\
|
|
553
|
+
CODEX_PROFILE=relay-fast\n",
|
|
554
|
+
)
|
|
555
|
+
.unwrap();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
fn seed_healthy_coordinator(workspace: &Path) {
|
|
559
|
+
let workspace = team_agent::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
560
|
+
let pid = team_agent::coordinator::Pid::new(std::process::id());
|
|
561
|
+
std::fs::create_dir_all(
|
|
562
|
+
team_agent::coordinator::coordinator_pid_path(&workspace)
|
|
563
|
+
.parent()
|
|
564
|
+
.unwrap(),
|
|
565
|
+
)
|
|
566
|
+
.unwrap();
|
|
567
|
+
team_agent::coordinator::write_coordinator_metadata(
|
|
568
|
+
&workspace,
|
|
569
|
+
pid,
|
|
570
|
+
team_agent::coordinator::MetadataSource::Boot,
|
|
571
|
+
)
|
|
572
|
+
.expect("write coordinator metadata");
|
|
573
|
+
std::fs::write(
|
|
574
|
+
team_agent::coordinator::coordinator_pid_path(&workspace),
|
|
575
|
+
pid.to_string(),
|
|
576
|
+
)
|
|
577
|
+
.expect("write coordinator pid");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
fn tmp_dir(tag: &str) -> PathBuf {
|
|
581
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
582
|
+
let dir = std::env::temp_dir().join(format!(
|
|
583
|
+
"ta-rs-264-{tag}-{}-{}",
|
|
584
|
+
std::process::id(),
|
|
585
|
+
N.fetch_add(1, Ordering::Relaxed)
|
|
586
|
+
));
|
|
587
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
588
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
589
|
+
std::fs::canonicalize(dir).unwrap()
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
fn flag_value(argv: &[String], flag: &str) -> Option<String> {
|
|
593
|
+
argv.windows(2)
|
|
594
|
+
.find_map(|pair| (pair[0] == flag).then(|| pair[1].clone()))
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
struct EnvGuard {
|
|
598
|
+
previous: Vec<(&'static str, Option<String>)>,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
impl EnvGuard {
|
|
602
|
+
fn set(values: &[(&'static str, &'static str)]) -> Self {
|
|
603
|
+
let previous = values
|
|
604
|
+
.iter()
|
|
605
|
+
.map(|(key, _)| (*key, std::env::var(key).ok()))
|
|
606
|
+
.collect::<Vec<_>>();
|
|
607
|
+
for (key, value) in values {
|
|
608
|
+
std::env::set_var(key, value);
|
|
609
|
+
}
|
|
610
|
+
Self { previous }
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
impl Drop for EnvGuard {
|
|
615
|
+
fn drop(&mut self) {
|
|
616
|
+
for (key, value) in self.previous.drain(..).rev() {
|
|
617
|
+
match value {
|
|
618
|
+
Some(value) => std::env::set_var(key, value),
|
|
619
|
+
None => std::env::remove_var(key),
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// recording transport (argv + env + cwd + session + keystrokes)
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
|
|
629
|
+
#[derive(Debug, Clone)]
|
|
630
|
+
struct RecordedSpawn {
|
|
631
|
+
argv: Vec<String>,
|
|
632
|
+
env: BTreeMap<String, String>,
|
|
633
|
+
cwd: PathBuf,
|
|
634
|
+
session: String,
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
#[derive(Default)]
|
|
638
|
+
struct RecordingTransport {
|
|
639
|
+
spawns: Mutex<Vec<RecordedSpawn>>,
|
|
640
|
+
sent_text: Mutex<Vec<String>>,
|
|
641
|
+
session_present: Mutex<bool>,
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
impl RecordingTransport {
|
|
645
|
+
fn new() -> Self {
|
|
646
|
+
Self::default()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
fn single_spawn(&self) -> RecordedSpawn {
|
|
650
|
+
let spawns = self.spawns.lock().unwrap();
|
|
651
|
+
assert_eq!(
|
|
652
|
+
spawns.len(),
|
|
653
|
+
1,
|
|
654
|
+
"fixture should record exactly one worker spawn; spawns={spawns:?}"
|
|
655
|
+
);
|
|
656
|
+
spawns[0].clone()
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
fn sent_text(&self) -> Vec<String> {
|
|
660
|
+
self.sent_text.lock().unwrap().clone()
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
fn record_spawn(
|
|
664
|
+
&self,
|
|
665
|
+
session: &SessionName,
|
|
666
|
+
window: &WindowName,
|
|
667
|
+
argv: &[String],
|
|
668
|
+
cwd: &Path,
|
|
669
|
+
env: &BTreeMap<String, String>,
|
|
670
|
+
) -> SpawnResult {
|
|
671
|
+
let mut spawns = self.spawns.lock().unwrap();
|
|
672
|
+
spawns.push(RecordedSpawn {
|
|
673
|
+
argv: argv.to_vec(),
|
|
674
|
+
env: env.clone(),
|
|
675
|
+
cwd: cwd.to_path_buf(),
|
|
676
|
+
session: session.as_str().to_string(),
|
|
677
|
+
});
|
|
678
|
+
*self.session_present.lock().unwrap() = true;
|
|
679
|
+
SpawnResult {
|
|
680
|
+
pane_id: PaneId::new(format!("%{}", spawns.len())),
|
|
681
|
+
session: session.clone(),
|
|
682
|
+
window: window.clone(),
|
|
683
|
+
child_pid: Some(20_000 + spawns.len() as u32),
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
impl Transport for RecordingTransport {
|
|
689
|
+
fn kind(&self) -> BackendKind {
|
|
690
|
+
BackendKind::Tmux
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
fn spawn_first(
|
|
694
|
+
&self,
|
|
695
|
+
session: &SessionName,
|
|
696
|
+
window: &WindowName,
|
|
697
|
+
argv: &[String],
|
|
698
|
+
cwd: &Path,
|
|
699
|
+
env: &BTreeMap<String, String>,
|
|
700
|
+
) -> Result<SpawnResult, TransportError> {
|
|
701
|
+
Ok(self.record_spawn(session, window, argv, cwd, env))
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
fn spawn_into(
|
|
705
|
+
&self,
|
|
706
|
+
session: &SessionName,
|
|
707
|
+
window: &WindowName,
|
|
708
|
+
argv: &[String],
|
|
709
|
+
cwd: &Path,
|
|
710
|
+
env: &BTreeMap<String, String>,
|
|
711
|
+
) -> Result<SpawnResult, TransportError> {
|
|
712
|
+
Ok(self.record_spawn(session, window, argv, cwd, env))
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
fn inject(
|
|
716
|
+
&self,
|
|
717
|
+
_target: &Target,
|
|
718
|
+
payload: &InjectPayload,
|
|
719
|
+
_submit: Key,
|
|
720
|
+
_bracketed: bool,
|
|
721
|
+
) -> Result<InjectReport, TransportError> {
|
|
722
|
+
if let Some(text) = payload.text() {
|
|
723
|
+
self.sent_text.lock().unwrap().push(text.to_string());
|
|
724
|
+
}
|
|
725
|
+
Ok(InjectReport {
|
|
726
|
+
stage_reached: InjectStage::Submit,
|
|
727
|
+
inject_verification: InjectVerification::CaptureContainsToken,
|
|
728
|
+
submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
729
|
+
turn_verification: TurnVerification::NotYetObserved,
|
|
730
|
+
attempts: 1,
|
|
731
|
+
submit_diagnostics: None,
|
|
732
|
+
})
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
fn send_keys(&self, _target: &Target, keys: &[Key]) -> Result<(), TransportError> {
|
|
736
|
+
let text: String = keys
|
|
737
|
+
.iter()
|
|
738
|
+
.map(|key| match key {
|
|
739
|
+
Key::Char(c) => *c,
|
|
740
|
+
Key::Enter => '\n',
|
|
741
|
+
_ => '\u{FFFD}',
|
|
742
|
+
})
|
|
743
|
+
.collect();
|
|
744
|
+
self.sent_text.lock().unwrap().push(text);
|
|
745
|
+
Ok(())
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
fn capture(
|
|
749
|
+
&self,
|
|
750
|
+
_target: &Target,
|
|
751
|
+
range: CaptureRange,
|
|
752
|
+
) -> Result<CapturedText, TransportError> {
|
|
753
|
+
Ok(CapturedText {
|
|
754
|
+
text: "Claude Code\n> \nOpenAI Codex\ncodex>".to_string(),
|
|
755
|
+
range,
|
|
756
|
+
})
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
|
|
760
|
+
match field {
|
|
761
|
+
PaneField::PaneWidth => Ok(Some("120".to_string())),
|
|
762
|
+
_ => Ok(None),
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
fn liveness(
|
|
767
|
+
&self,
|
|
768
|
+
_pane: &PaneId,
|
|
769
|
+
) -> Result<team_agent::transport::PaneLiveness, TransportError> {
|
|
770
|
+
Ok(team_agent::transport::PaneLiveness::Live)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
774
|
+
Ok(Vec::new())
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
778
|
+
Ok(*self.session_present.lock().unwrap())
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
782
|
+
Ok(Vec::new())
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
fn set_session_env(
|
|
786
|
+
&self,
|
|
787
|
+
_session: &SessionName,
|
|
788
|
+
_key: &str,
|
|
789
|
+
_value: &str,
|
|
790
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
791
|
+
Ok(SetEnvOutcome::Applied)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
795
|
+
*self.session_present.lock().unwrap() = false;
|
|
796
|
+
Ok(())
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
800
|
+
Ok(())
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
804
|
+
Ok(AttachOutcome::Attached)
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ---------------------------------------------------------------------------
|
|
809
|
+
// recording tmux runner (D9: observes the real TmuxBackend shell line)
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
|
|
812
|
+
type ArgvLog = std::sync::Arc<Mutex<Vec<Vec<String>>>>;
|
|
813
|
+
|
|
814
|
+
struct RecordingRunner {
|
|
815
|
+
argv_log: ArgvLog,
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
impl RecordingRunner {
|
|
819
|
+
fn new() -> Self {
|
|
820
|
+
Self {
|
|
821
|
+
argv_log: std::sync::Arc::new(Mutex::new(Vec::new())),
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
fn argv_log(&self) -> ArgvLog {
|
|
826
|
+
std::sync::Arc::clone(&self.argv_log)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
impl CommandRunner for RecordingRunner {
|
|
831
|
+
fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
|
|
832
|
+
self.argv_log.lock().unwrap().push(argv.to_vec());
|
|
833
|
+
let subcommand = argv
|
|
834
|
+
.iter()
|
|
835
|
+
.find(|arg| !arg.starts_with('-') && *arg != "tmux");
|
|
836
|
+
let (success, stdout) = match subcommand.map(String::as_str) {
|
|
837
|
+
Some("has-session") => (false, String::new()),
|
|
838
|
+
Some("display-message") => (true, "%1\n".to_string()),
|
|
839
|
+
Some("capture-pane") => (true, "OpenAI Codex\ncodex>\n".to_string()),
|
|
840
|
+
_ => (true, String::new()),
|
|
841
|
+
};
|
|
842
|
+
Ok(CommandOutput {
|
|
843
|
+
success,
|
|
844
|
+
code: Some(if success { 0 } else { 1 }),
|
|
845
|
+
stdout,
|
|
846
|
+
stderr: String::new(),
|
|
847
|
+
})
|
|
848
|
+
}
|
|
849
|
+
}
|