@team-agent/installer 0.5.60 → 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 +49 -3
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +184 -0
- package/crates/team-agent/src/cli/emit.rs +104 -21
- package/crates/team-agent/src/cli/helpers.rs +1 -0
- package/crates/team-agent/src/cli/leader.rs +14 -2
- package/crates/team-agent/src/cli/leaders.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +296 -8
- package/crates/team-agent/src/cli/named_address.rs +26 -2
- 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 +3 -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 +5 -1
- 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/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
- package/crates/team-agent/src/cli/types.rs +36 -2
- package/crates/team-agent/src/communication_mode/mod.rs +54 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +54 -1
- 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 +47 -2
- 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/fake_worker.rs +21 -1
- 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 +1072 -186
- 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 +71 -59
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- 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 +7 -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_agent.rs +1 -1
- 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 +2 -28
- 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 +4 -4
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- 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/lifecycle/worker_command_context.rs +29 -11
- 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 +5 -3
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +101 -66
- package/crates/team-agent/src/mcp_server/types.rs +6 -18
- package/crates/team-agent/src/mcp_server/wire.rs +32 -26
- 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 +109 -41
- package/crates/team-agent/src/messaging/helpers.rs +2 -1
- package/crates/team-agent/src/messaging/leader_channel.rs +34 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +42 -24
- 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 +111 -1
- package/crates/team-agent/src/messaging/results.rs +166 -17
- 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 +135 -89
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +7 -2
- package/crates/team-agent/src/messaging/watchers.rs +24 -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 +2 -0
- package/crates/team-agent/src/model/name_similarity.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +359 -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 +17 -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 +41 -22
- 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 +93 -3
- package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -7
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +6 -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/topology.rs +3 -0
- package/crates/team-agent/src/transport.rs +15 -0
- package/package.json +4 -4
- package/skills/team-agent/command-coverage.json +379 -0
- package/crates/team-agent/src/leader/inject.rs +0 -33
- package/crates/team-agent/src/provider/command.rs +0 -80
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
//! 0.3.6 A-batch: T2 data-preservation + T3 false-green-truth (batch 2) + T5 thread
|
|
2
|
+
//! leak contracts.
|
|
3
|
+
//!
|
|
4
|
+
//! Basis: `.team/artifacts/audit-harvest-2-dev-tasks.md` §1 T2/T3/T5 (each with a
|
|
5
|
+
//! minimal-fix locate + file:line in the underlying code-audit docs). All contracts
|
|
6
|
+
//! are deterministic; any external CLI is PATH-shimmed (no real-binary dependency).
|
|
7
|
+
//!
|
|
8
|
+
//! T2 (data loss on failure path): install_skill pre-wipes the dest dir then copies
|
|
9
|
+
//! (copy failure = user skill dir already gone); save_team_runtime_snapshot must not
|
|
10
|
+
//! leave a stale .tmp on a failed replace. The contracts assert the original data
|
|
11
|
+
//! survives / no residue.
|
|
12
|
+
//! T3 (false green = unrecognized/failed input silently maps to a success value,
|
|
13
|
+
//! MUST-NOT-13): normalize_result_status unrecognized -> Success; claude_auth_hint
|
|
14
|
+
//! parse failure -> Present; schema_diagnosis missing db -> ok:true; tmux fabricated
|
|
15
|
+
//! `%0` pane id; packaging install/update constant DoctorStatus::Ok.
|
|
16
|
+
//! T5: stop_coordinator_bounded spawns a thread + recv_timeout and never joins it —
|
|
17
|
+
//! a timed-out stop leaves a detached thread. Source grep guard (the bounded-stop
|
|
18
|
+
//! path is not hermetically exercisable without a live coordinator).
|
|
19
|
+
|
|
20
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
21
|
+
|
|
22
|
+
#[path = "../../../tests/support/mcp_sim_harness.rs"]
|
|
23
|
+
#[allow(dead_code)]
|
|
24
|
+
mod mcp_sim_harness;
|
|
25
|
+
|
|
26
|
+
use std::path::{Path, PathBuf};
|
|
27
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
28
|
+
|
|
29
|
+
use mcp_sim_harness::McpSimHarness;
|
|
30
|
+
use serial_test::serial;
|
|
31
|
+
|
|
32
|
+
use team_agent::db::migration::schema_diagnosis_workspace;
|
|
33
|
+
use team_agent::mcp_server::normalize::normalize_result_status;
|
|
34
|
+
use team_agent::model::enums::ResultStatus;
|
|
35
|
+
use team_agent::packaging::{install_skill, SkillInstallOptions, SkillTarget};
|
|
36
|
+
|
|
37
|
+
// ───────────────────────────── T2 · data preservation ─────────────────────────────
|
|
38
|
+
|
|
39
|
+
/// T2-1 (packaging install_skill): copying into the dest must not pre-wipe the user's
|
|
40
|
+
/// existing skill dir before the copy succeeds — a copy failure currently leaves the
|
|
41
|
+
/// user with an EMPTY skill dir (remove_dir_all then copy_tree). The contract: when the
|
|
42
|
+
/// copy source is invalid (copy must fail), the pre-existing dest content must survive.
|
|
43
|
+
#[test]
|
|
44
|
+
fn t2_install_skill_does_not_destroy_existing_dir_on_copy_failure() {
|
|
45
|
+
let ws = tmp_ws("t2-install");
|
|
46
|
+
let dest = ws.join("existing-skill");
|
|
47
|
+
std::fs::create_dir_all(&dest).unwrap();
|
|
48
|
+
std::fs::write(
|
|
49
|
+
dest.join("USER_DATA.md"),
|
|
50
|
+
"irreplaceable user skill content",
|
|
51
|
+
)
|
|
52
|
+
.unwrap();
|
|
53
|
+
|
|
54
|
+
// Source that does not exist → copy_tree must fail.
|
|
55
|
+
let bad_source = ws.join("no-such-source");
|
|
56
|
+
let result = install_skill(&SkillInstallOptions {
|
|
57
|
+
target: SkillTarget::Codex,
|
|
58
|
+
dest: Some(dest.clone()),
|
|
59
|
+
dry_run: false,
|
|
60
|
+
source: bad_source,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let mut failures = Vec::new();
|
|
64
|
+
if result.is_ok() {
|
|
65
|
+
failures.push(
|
|
66
|
+
"T2-1: install_skill with a missing source must fail, not silently succeed".to_string(),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
// The pre-existing user content MUST survive a failed install.
|
|
70
|
+
if !dest.join("USER_DATA.md").exists() {
|
|
71
|
+
failures.push(
|
|
72
|
+
"T2-1: a failed copy must not leave the user's skill dir wiped — install_skill \
|
|
73
|
+
must stage into a temp dir and only swap after a successful copy (write_worker_mcp_config \
|
|
74
|
+
tmp+rename范式); the user data was destroyed"
|
|
75
|
+
.to_string(),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
assert!(
|
|
79
|
+
failures.is_empty(),
|
|
80
|
+
"T2-1 install_skill data-preservation contract failed:\n{}",
|
|
81
|
+
failures.join("\n")
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// T2-4 (lifecycle snapshot): save_team_runtime_snapshot stages via a fixed
|
|
86
|
+
/// `state.json.tmp` then renames. On a failed write/replace it must not leave a stale
|
|
87
|
+
/// `.tmp` behind (dirty-read residue). Here we pre-create the tmp path AS A DIRECTORY so
|
|
88
|
+
/// the `fs::write(&tmp, ..)` fails, and assert no stale tmp file is left as residue.
|
|
89
|
+
#[test]
|
|
90
|
+
fn t2_snapshot_no_stale_tmp_residue_on_failed_write() {
|
|
91
|
+
let ws = tmp_ws("t2-snapshot");
|
|
92
|
+
let snap_dir = ws.join(".team/runtime/teams/team-x");
|
|
93
|
+
std::fs::create_dir_all(&snap_dir).unwrap();
|
|
94
|
+
// Make the tmp target un-writable-as-file: create it as a directory.
|
|
95
|
+
std::fs::create_dir_all(snap_dir.join("state.json.tmp")).unwrap();
|
|
96
|
+
|
|
97
|
+
let result = team_agent::lifecycle::save_team_runtime_snapshot(
|
|
98
|
+
&ws,
|
|
99
|
+
&serde_json::json!({"session_name": "team-x", "agents": {}}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
assert!(
|
|
103
|
+
result.is_err(),
|
|
104
|
+
"fixture: the snapshot write must fail when tmp is a dir"
|
|
105
|
+
);
|
|
106
|
+
// A failed snapshot must not leave a stale FILE tmp residue (the dir we created is
|
|
107
|
+
// the fixture's, not residue; the contract is that no stray state.json.tmp FILE is
|
|
108
|
+
// written elsewhere and the real state.json was never half-written).
|
|
109
|
+
assert!(
|
|
110
|
+
!snap_dir.join("state.json").exists(),
|
|
111
|
+
"T2-4: a failed snapshot must not leave a partially-written state.json \
|
|
112
|
+
(atomicity); found one at {}",
|
|
113
|
+
snap_dir.join("state.json").display()
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// T2-2 (restart-remove atomicity): the remove path must not pre-delete on-disk
|
|
118
|
+
/// artifacts (role file / spec / state) before the operation is committed — a mid-way
|
|
119
|
+
/// failure must leave the agent fully present (rollback) or fully removed, never
|
|
120
|
+
/// half-deleted. Grep guard on the minimal-fix shape: removal of user files must be
|
|
121
|
+
/// staged/rollback-guarded, not a bare `remove_file`/`remove_dir_all` ahead of commit.
|
|
122
|
+
#[test]
|
|
123
|
+
fn t2_remove_agent_role_file_delete_is_rollback_guarded() {
|
|
124
|
+
let src = std::fs::read_to_string(concat!(
|
|
125
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
126
|
+
"/src/lifecycle/restart/remove.rs"
|
|
127
|
+
))
|
|
128
|
+
.unwrap();
|
|
129
|
+
// The remove path must capture a rollback before mutating, and restore on failure
|
|
130
|
+
// (RemoveRollback::capture + .restore). Lock that the guard is present so a future
|
|
131
|
+
// refactor cannot reintroduce a bare pre-commit delete.
|
|
132
|
+
assert!(
|
|
133
|
+
src.contains("RemoveRollback") && src.contains(".restore(") && src.contains("rollback_ok"),
|
|
134
|
+
"T2-2: agent removal must stage a rollback (RemoveRollback::capture/.restore + \
|
|
135
|
+
rollback_ok event) so a mid-way failure leaves no half-deleted state/file — the \
|
|
136
|
+
two-line state+file delete must be atomic-or-rolled-back"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// T2-3 (state-persist no lossy overwrite, A0-adjacent): a save whose atomic replace
|
|
141
|
+
/// fails must NOT leave the workspace with a truncated/empty state.json — the original
|
|
142
|
+
/// content must survive (self_heal rebuilds the inode via heal-tmp + backup, never an
|
|
143
|
+
/// in-place truncate). Grep guard on the self_heal data-preservation shape.
|
|
144
|
+
#[test]
|
|
145
|
+
fn t2_persist_failed_save_never_truncates_in_place() {
|
|
146
|
+
let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/state/persist.rs"))
|
|
147
|
+
.unwrap();
|
|
148
|
+
let heal = fn_body(&src, "fn self_heal");
|
|
149
|
+
let stages_via_tmp = heal.contains("heal.tmp") && heal.contains("atomic_replace");
|
|
150
|
+
let no_inplace_truncate =
|
|
151
|
+
!heal.contains("File::create(path)") && !heal.contains("truncate(true)");
|
|
152
|
+
assert!(
|
|
153
|
+
stages_via_tmp && no_inplace_truncate,
|
|
154
|
+
"T2-3: a failed state save must rebuild via heal-tmp + atomic_replace (+ backup), \
|
|
155
|
+
never truncate the original in place — the original state.json must survive a failed \
|
|
156
|
+
save (A0 chokepoint data preservation); self_heal body={heal}"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ───────────────────────────── T3 · false-green truth ─────────────────────────────
|
|
161
|
+
|
|
162
|
+
/// T3-1 (mcp normalize): an UNRECOGNIZED result status must not silently become Success
|
|
163
|
+
/// (MUST-NOT-13: a typo / unknown status is a semantic error swallowed as success).
|
|
164
|
+
/// The harvest §3 note that some defaults are Python-parity does NOT cover this — this
|
|
165
|
+
/// is the "错→成功" directional false-green the batch targets.
|
|
166
|
+
#[test]
|
|
167
|
+
fn t3_normalize_unrecognized_nonempty_status_is_partial_not_success() {
|
|
168
|
+
// cr refined 2026-06-10 (path 2 only): a NON-EMPTY unrecognized status string must
|
|
169
|
+
// NOT default to Success — it normalizes to `Partial` (RS does not follow Python
|
|
170
|
+
// normalize.py:123 else->success). missing/null/empty are NOT covered here (path 1
|
|
171
|
+
// = parity-locked implicit success, see the parity-lock test below).
|
|
172
|
+
// Note: if dev extends the mapping (e.g. cancelled->failed), that case becomes
|
|
173
|
+
// `failed` + no event; the assertion below tracks the Partial-default contract and
|
|
174
|
+
// is re-scoped to the dev mapping at green time.
|
|
175
|
+
let mut failures = Vec::new();
|
|
176
|
+
for unknown in ["garbage_string", "cancelled", "weird", "partiallydone"] {
|
|
177
|
+
let status = normalize_result_status(Some(unknown));
|
|
178
|
+
// "partiallydone" must hit the partial alias (Python :121 partially_done parity);
|
|
179
|
+
// the others must default to Partial (RS non-success direction).
|
|
180
|
+
if status == ResultStatus::Success {
|
|
181
|
+
failures.push(format!(
|
|
182
|
+
"T3-1: non-empty unrecognized status {unknown:?} must NOT default to Success \
|
|
183
|
+
(false green, MUST-NOT-13/P7); got Success"
|
|
184
|
+
));
|
|
185
|
+
}
|
|
186
|
+
if status != ResultStatus::Partial {
|
|
187
|
+
failures.push(format!(
|
|
188
|
+
"T3-1: status {unknown:?} must normalize to Partial (cr: RS does not follow \
|
|
189
|
+
Python's else->success); got {status:?}"
|
|
190
|
+
));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
assert!(
|
|
194
|
+
failures.is_empty(),
|
|
195
|
+
"T3-1 unknown-status normalization failed:\n{}",
|
|
196
|
+
failures.join("\n")
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// T3-1 parity lock (cr path 1): a MISSING / null / empty status is the
|
|
201
|
+
/// parity-locked implicit-success convention (like exit code 0) — it must default to
|
|
202
|
+
/// Success and RS must NOT accidentally change it. Anchor: N38 parity.
|
|
203
|
+
#[test]
|
|
204
|
+
fn t3_normalize_missing_or_empty_status_is_success_parity_lock() {
|
|
205
|
+
let mut failures = Vec::new();
|
|
206
|
+
for (label, input) in [("missing/null", None), ("empty", Some(""))] {
|
|
207
|
+
let status = normalize_result_status(input);
|
|
208
|
+
if status != ResultStatus::Success {
|
|
209
|
+
failures.push(format!(
|
|
210
|
+
"T3-1 parity lock: {label} status ({input:?}) must default to Success \
|
|
211
|
+
(Python normalize.py:106 `_text(value) or \"success\"`, implicit-success convention); \
|
|
212
|
+
got {status:?}"
|
|
213
|
+
));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
assert!(
|
|
217
|
+
failures.is_empty(),
|
|
218
|
+
"T3-1 parity lock failed:\n{}",
|
|
219
|
+
failures.join("\n")
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// T3-1 ingress arm, re-signed for the 0.5.61 B06/C12 result contract:
|
|
224
|
+
/// task-local business status is opaque transport. The real MCP report_result
|
|
225
|
+
/// entrypoint must preserve an unknown value in the durable result and must not emit
|
|
226
|
+
/// the historical `provider.result.unknown_status_normalized` event. The pure
|
|
227
|
+
/// `normalize_result_status` compatibility locks above remain separate from this
|
|
228
|
+
/// public business-status input domain.
|
|
229
|
+
#[test]
|
|
230
|
+
#[serial(t3_event)]
|
|
231
|
+
fn t3_unknown_business_status_is_preserved_without_normalized_event() {
|
|
232
|
+
let harness = McpSimHarness::new();
|
|
233
|
+
let mut worker = harness.spawn_mcp_client("worker_a", "teamA");
|
|
234
|
+
let status = "garbage-status-xyz";
|
|
235
|
+
let call = worker.call_tool(
|
|
236
|
+
"report_result",
|
|
237
|
+
serde_json::json!({
|
|
238
|
+
"task_id": "task_mcp",
|
|
239
|
+
"agent_id": "worker_a",
|
|
240
|
+
"status": status,
|
|
241
|
+
"summary": "did a thing",
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
assert!(
|
|
245
|
+
!call.is_error,
|
|
246
|
+
"T3-1 ingress: an opaque business status must be accepted; body={} raw={}",
|
|
247
|
+
call.body, call.raw
|
|
248
|
+
);
|
|
249
|
+
let result_id = call.body["result_id"]
|
|
250
|
+
.as_str()
|
|
251
|
+
.expect("T3-1 ingress must return the durable result_id");
|
|
252
|
+
let row = harness
|
|
253
|
+
.result_row(result_id)
|
|
254
|
+
.unwrap_or_else(|| panic!("T3-1 ingress missing durable result {result_id}"));
|
|
255
|
+
let envelope: serde_json::Value =
|
|
256
|
+
serde_json::from_str(&row.envelope).expect("T3-1 durable envelope JSON");
|
|
257
|
+
assert_eq!(
|
|
258
|
+
row.status, status,
|
|
259
|
+
"T3-1 ingress: results.status must preserve the task-local value byte-for-byte"
|
|
260
|
+
);
|
|
261
|
+
assert_eq!(
|
|
262
|
+
envelope["status"],
|
|
263
|
+
serde_json::json!(status),
|
|
264
|
+
"T3-1 ingress: canonical envelope status must preserve the same value"
|
|
265
|
+
);
|
|
266
|
+
let events = harness.events_text();
|
|
267
|
+
assert!(
|
|
268
|
+
!events.contains("provider.result.unknown_status_normalized"),
|
|
269
|
+
"T3-1 ingress: opaque business status must not produce the historical \
|
|
270
|
+
provider.result.unknown_status_normalized event; events tail={}",
|
|
271
|
+
events.lines().rev().take(4).collect::<Vec<_>>().join(" | ")
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// T3-1b (normalize summary) — RE-SCOPED to a Python-parity lock (cr 2026-06-10, same
|
|
276
|
+
/// as the R2 precedent): a missing summary MUST default to the literal "completed"
|
|
277
|
+
/// (Python normalize.py:68 `_text(env.get("summary")) or "completed"`). Any other
|
|
278
|
+
/// default string is a parity divergence = FAIL. Anchor: N38 parity, not honesty.
|
|
279
|
+
#[test]
|
|
280
|
+
fn t3_normalize_missing_summary_defaults_to_completed_python_parity() {
|
|
281
|
+
let env = team_agent::mcp_server::normalize::normalize_report_envelope(&serde_json::json!({
|
|
282
|
+
"task_id": "t1",
|
|
283
|
+
"agent_id": "w1",
|
|
284
|
+
"status": "failed",
|
|
285
|
+
}));
|
|
286
|
+
assert_eq!(
|
|
287
|
+
env.summary, "completed",
|
|
288
|
+
"T3-1b (parity lock): a missing summary must default to the literal \"completed\" \
|
|
289
|
+
(Python normalize.py:68 byte-for-byte); any other default diverges from parity. got {:?}",
|
|
290
|
+
env.summary
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// T3-3 (db schema_diagnosis) — RE-SCOPED per cr 2026-06-10 to a layered-parity lock
|
|
295
|
+
/// (N38 + MUST-NOT-13 layered-truth, NOT a swallow): a missing db legitimately reports
|
|
296
|
+
/// `ok:true` (ok = legal for the next step) WITH `status:"missing"` (explicit state
|
|
297
|
+
/// axis) AND a `recommended_action` carrying initialize_schema guidance (Python
|
|
298
|
+
/// schema_migration.py missing branch). The truth is fully laid out — not a silent
|
|
299
|
+
/// fake-green. The reverse cases below catch the REAL fake-greens this layering must not
|
|
300
|
+
/// hide.
|
|
301
|
+
#[test]
|
|
302
|
+
fn t3_schema_diagnosis_missing_is_layered_parity_not_swallow() {
|
|
303
|
+
let ws = tmp_ws("t3-schema-missing");
|
|
304
|
+
let diag = schema_diagnosis_workspace(&ws)
|
|
305
|
+
.expect("diagnosis should not error on a missing workspace db");
|
|
306
|
+
let mut failures = Vec::new();
|
|
307
|
+
|
|
308
|
+
// Value axes (compile-safe struct fields): ok=true + status="missing" are the parity
|
|
309
|
+
// GREEN part — the missing state is legal for the next step and explicitly named.
|
|
310
|
+
if !diag.ok {
|
|
311
|
+
failures.push(format!(
|
|
312
|
+
"T3-3 parity: missing db must report ok=true (legal next step); diag={diag:?}"
|
|
313
|
+
));
|
|
314
|
+
}
|
|
315
|
+
if diag.status != "missing" {
|
|
316
|
+
failures.push(format!(
|
|
317
|
+
"T3-3 parity: missing db must carry status=\"missing\" (explicit state axis); got {:?}",
|
|
318
|
+
diag.status
|
|
319
|
+
));
|
|
320
|
+
}
|
|
321
|
+
// recommended_action axis: Python's missing branch carries an initialize_schema
|
|
322
|
+
// guidance string; RS's Diagnosis omits the field entirely (parity gap). Grep guard
|
|
323
|
+
// the missing branch so the layered guidance is surfaced (the field is part of the
|
|
324
|
+
// honest layering — without it the ok:true is bare). Turns green when dev adds it.
|
|
325
|
+
let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/db/migration.rs"))
|
|
326
|
+
.unwrap();
|
|
327
|
+
if !src.contains("recommended_action") || !src.contains("initialize_schema") {
|
|
328
|
+
failures.push(
|
|
329
|
+
"T3-3 parity: the missing-db diagnosis must carry a recommended_action with \
|
|
330
|
+
initialize_schema guidance (Python schema_migration.py missing branch: 'initialize_schema \
|
|
331
|
+
will create it on first use'); RS Diagnosis currently omits the field — the ok:true is \
|
|
332
|
+
bare without the layered guidance"
|
|
333
|
+
.to_string(),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
assert!(
|
|
337
|
+
failures.is_empty(),
|
|
338
|
+
"T3-3 layered-parity contract failed:\n{}",
|
|
339
|
+
failures.join("\n")
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// T3-3 reverse cases (cr): the layered missing-state must not become a cover for a
|
|
344
|
+
/// REAL fake-green. A non-existent db can't carry layout drift, so we drive these as
|
|
345
|
+
/// invariants over the diagnosis envelope shape:
|
|
346
|
+
/// - ok:true + status:"missing" + layout_diffs non-empty → FAIL (claims first-use but
|
|
347
|
+
/// actually has drift = silent concealment)
|
|
348
|
+
/// - ok:true + status field missing → FAIL (silent status drop)
|
|
349
|
+
/// - missing + recommended_action empty → FAIL (guidance is part of
|
|
350
|
+
/// honest layering — covered by the main test above)
|
|
351
|
+
#[test]
|
|
352
|
+
fn t3_schema_diagnosis_layering_must_not_hide_real_fake_green() {
|
|
353
|
+
// Build a real db that HAS layout drift, and confirm a drift state never reports the
|
|
354
|
+
// missing-style "ok:true + status:missing" combination (that combo is reserved for a
|
|
355
|
+
// genuinely absent db).
|
|
356
|
+
let ws = tmp_ws("t3-schema-drift");
|
|
357
|
+
let runtime = ws.join(".team/runtime");
|
|
358
|
+
std::fs::create_dir_all(&runtime).unwrap();
|
|
359
|
+
let db = runtime.join("team.db");
|
|
360
|
+
// A db file that exists but is empty/incompatible → must NOT diagnose as missing.
|
|
361
|
+
rusqlite::Connection::open(&db)
|
|
362
|
+
.unwrap()
|
|
363
|
+
.execute_batch("create table messages (id integer primary key)")
|
|
364
|
+
.unwrap();
|
|
365
|
+
let diag = schema_diagnosis_workspace(&ws).expect("diagnosis on an existing workspace db");
|
|
366
|
+
let mut failures = Vec::new();
|
|
367
|
+
if diag.status == "missing" {
|
|
368
|
+
failures.push(format!(
|
|
369
|
+
"T3-3 reverse: an EXISTING db (with drift) must never report status=\"missing\" \
|
|
370
|
+
(the missing layering is reserved for a genuinely absent db; claiming missing over real \
|
|
371
|
+
drift is the concealment fake-green); diag={diag:?}"
|
|
372
|
+
));
|
|
373
|
+
}
|
|
374
|
+
if diag.ok && !diag.layout_diffs.is_empty() {
|
|
375
|
+
failures.push(format!(
|
|
376
|
+
"T3-3 reverse: ok=true must not coexist with non-empty layout_diffs (claims \
|
|
377
|
+
fine while drift exists = silent concealment); diag={diag:?}"
|
|
378
|
+
));
|
|
379
|
+
}
|
|
380
|
+
assert!(
|
|
381
|
+
failures.is_empty(),
|
|
382
|
+
"T3-3 fake-green-concealment reverse contract failed:\n{}",
|
|
383
|
+
failures.join("\n")
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// T3-5 (tmux pane id fallback): the tmux backend must not fabricate a `%0` pane id
|
|
388
|
+
/// when the spawn reply is empty — a fake pane id makes every later addressing target
|
|
389
|
+
/// the wrong pane. Source grep guard (the empty-reply path needs the real tmux edge;
|
|
390
|
+
/// the fabricated constant is the audited defect).
|
|
391
|
+
#[test]
|
|
392
|
+
fn t3_tmux_pane_id_not_fabricated_as_percent_zero() {
|
|
393
|
+
let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/tmux_backend.rs"))
|
|
394
|
+
.unwrap();
|
|
395
|
+
assert!(
|
|
396
|
+
!src.contains("if pane.is_empty() { \"%0\" }")
|
|
397
|
+
&& !src.contains("if pane.is_empty() {\n \"%0\""),
|
|
398
|
+
"T3-5: the tmux backend must not fabricate a `%0` pane id on an empty spawn reply \
|
|
399
|
+
(a fake pane id mis-addresses every later op); it must surface the missing pane instead"
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/// T3-6 (packaging install/update): install/update must not return a CONSTANT
|
|
404
|
+
/// DoctorStatus::Ok with no real doctor check behind it (false green — nothing was
|
|
405
|
+
/// verified). Source grep guard (the audited defect is the literal constant).
|
|
406
|
+
#[test]
|
|
407
|
+
fn t3_packaging_install_update_doctor_status_not_constant_ok() {
|
|
408
|
+
let src = std::fs::read_to_string(concat!(
|
|
409
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
410
|
+
"/src/packaging/install.rs"
|
|
411
|
+
))
|
|
412
|
+
.unwrap();
|
|
413
|
+
// The install/update bodies must not hardcode `doctor: DoctorStatus::Ok` without a
|
|
414
|
+
// real check; assert the literal constant assignment is gone.
|
|
415
|
+
assert!(
|
|
416
|
+
!src.contains("doctor: DoctorStatus::Ok,"),
|
|
417
|
+
"T3-6: install/update must not hardcode doctor: DoctorStatus::Ok (no real doctor \
|
|
418
|
+
check behind it = false green); it must reflect an actual doctor result"
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// T3-2 (claude_auth_hint parse failure): when `claude auth status` output is
|
|
423
|
+
/// unparseable / unsuccessful, the hint must not default to Present (auth misreported
|
|
424
|
+
/// as available). PATH-shims a `claude` that prints garbage and exits non-zero.
|
|
425
|
+
#[test]
|
|
426
|
+
fn t3_claude_auth_hint_parse_failure_is_not_present() {
|
|
427
|
+
let home = tmp_ws("t3-auth-home");
|
|
428
|
+
let shim_dir = home.join("bin");
|
|
429
|
+
std::fs::create_dir_all(&shim_dir).unwrap();
|
|
430
|
+
// claude shim: garbage stdout + non-zero exit (auth status unparseable / failed).
|
|
431
|
+
write_shim(
|
|
432
|
+
&shim_dir.join("claude"),
|
|
433
|
+
"#!/bin/sh\nprintf 'not json at all\\n'\nexit 7\n",
|
|
434
|
+
);
|
|
435
|
+
let old_path = std::env::var("PATH").unwrap_or_default();
|
|
436
|
+
let old_home = std::env::var("HOME").ok();
|
|
437
|
+
std::env::set_var("HOME", &home);
|
|
438
|
+
std::env::set_var("PATH", format!("{}:{old_path}", shim_dir.to_string_lossy()));
|
|
439
|
+
|
|
440
|
+
let status = team_agent::provider::get_adapter(team_agent::provider::Provider::Claude)
|
|
441
|
+
.auth_hint(team_agent::provider::AuthMode::Subscription);
|
|
442
|
+
|
|
443
|
+
std::env::set_var("PATH", &old_path);
|
|
444
|
+
match old_home {
|
|
445
|
+
Some(home) => std::env::set_var("HOME", home),
|
|
446
|
+
None => std::env::remove_var("HOME"),
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
assert_ne!(
|
|
450
|
+
format!("{status:?}"),
|
|
451
|
+
"Present",
|
|
452
|
+
"T3-2: a `claude auth status` that prints unparseable output and exits non-zero \
|
|
453
|
+
must NOT report Present (auth misreported as available is a false green); got {status:?}"
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ───────────────────────────────── T5 · thread leak ─────────────────────────────────
|
|
458
|
+
|
|
459
|
+
/// T5 (A2 shutdown thread leak): stop_coordinator_bounded spawns a worker thread and
|
|
460
|
+
/// recv_timeouts on it, but never joins — on timeout the thread is detached and leaks
|
|
461
|
+
/// (repeated shutdowns re-enter and race). The fix is a join+timeout reclaim (or a
|
|
462
|
+
/// cancellable synchronous stop). Source grep guard: the bounded-stop path must not
|
|
463
|
+
/// drop a detached thread handle.
|
|
464
|
+
#[test]
|
|
465
|
+
fn t5_stop_coordinator_bounded_must_not_leak_detached_thread() {
|
|
466
|
+
let src =
|
|
467
|
+
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/cli/mod.rs")).unwrap();
|
|
468
|
+
let body = fn_body(&src, "fn stop_coordinator_bounded");
|
|
469
|
+
let spawns = body.contains("thread::spawn") || body.contains("std::thread::spawn");
|
|
470
|
+
let reclaims =
|
|
471
|
+
body.contains(".join(") || body.contains("JoinHandle") || body.contains("join_timeout");
|
|
472
|
+
assert!(
|
|
473
|
+
!spawns || reclaims,
|
|
474
|
+
"T5: stop_coordinator_bounded spawns a thread but never joins it — a timed-out \
|
|
475
|
+
stop leaves a detached, leaking thread (and repeated shutdowns race). It must retain \
|
|
476
|
+
the JoinHandle and reclaim it (join with timeout), or use a cancellable synchronous \
|
|
477
|
+
stop; body={body}"
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ───────────────────────────────────── helpers ─────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
fn write_shim(path: &Path, body: &str) {
|
|
484
|
+
std::fs::write(path, body).unwrap();
|
|
485
|
+
#[cfg(unix)]
|
|
486
|
+
{
|
|
487
|
+
use std::os::unix::fs::PermissionsExt;
|
|
488
|
+
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/// Crude single-function body extractor: from the `fn name` line to the next
|
|
493
|
+
/// same-indent ` fn ` (good enough for grep-guard scoping).
|
|
494
|
+
fn fn_body(src: &str, marker: &str) -> String {
|
|
495
|
+
let Some(start) = src.find(marker) else {
|
|
496
|
+
return String::new();
|
|
497
|
+
};
|
|
498
|
+
let rest = &src[start..];
|
|
499
|
+
let end = rest[marker.len()..]
|
|
500
|
+
.find("\n fn ")
|
|
501
|
+
.map(|offset| offset + marker.len())
|
|
502
|
+
.unwrap_or(rest.len());
|
|
503
|
+
rest[..end].to_string()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
fn tmp_ws(tag: &str) -> PathBuf {
|
|
507
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
508
|
+
let dir = std::env::temp_dir().join(format!(
|
|
509
|
+
"ta-rs-036a-{tag}-{}-{}",
|
|
510
|
+
std::process::id(),
|
|
511
|
+
N.fetch_add(1, Ordering::Relaxed)
|
|
512
|
+
));
|
|
513
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
514
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
515
|
+
std::fs::canonicalize(dir).unwrap()
|
|
516
|
+
}
|