@team-agent/installer 0.5.111 → 0.5.113

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.
Files changed (73) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +93 -779
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +158 -373
  6. package/crates/team-agent/src/cli/leaders.rs +32 -32
  7. package/crates/team-agent/src/cli/mod.rs +8 -15
  8. package/crates/team-agent/src/cli/named_address.rs +17 -16
  9. package/crates/team-agent/src/cli/route.rs +4 -4
  10. package/crates/team-agent/src/cli/send/presentation.rs +19 -18
  11. package/crates/team-agent/src/cli/send/resolve.rs +11 -11
  12. package/crates/team-agent/src/cli/send.rs +4 -4
  13. package/crates/team-agent/src/cli/spec.rs +29 -32
  14. package/crates/team-agent/src/cli/status.rs +7 -7
  15. package/crates/team-agent/src/cli/tests/base.rs +6 -6
  16. package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
  21. package/crates/team-agent/src/cli/tests/mod.rs +0 -2
  22. package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
  23. package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
  24. package/crates/team-agent/src/cli/triage.rs +53 -2
  25. package/crates/team-agent/src/cli/types.rs +12 -116
  26. package/crates/team-agent/src/compiler/tests.rs +12 -0
  27. package/crates/team-agent/src/compiler.rs +0 -9
  28. package/crates/team-agent/src/coordinator/health.rs +3 -283
  29. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  30. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  31. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  32. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  33. package/crates/team-agent/src/coordinator/types.rs +1 -21
  34. package/crates/team-agent/src/db/message_store.rs +0 -42
  35. package/crates/team-agent/src/db/migration.rs +49 -9
  36. package/crates/team-agent/src/db/schema.rs +27 -20
  37. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  38. package/crates/team-agent/src/leader/start.rs +5 -0
  39. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  40. package/crates/team-agent/src/leader/tests/identity.rs +21 -32
  41. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  42. package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
  43. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
  44. package/crates/team-agent/src/lifecycle/launch.rs +2 -2
  45. package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
  46. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
  47. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  48. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  49. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
  50. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  51. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  52. package/crates/team-agent/src/messaging/mod.rs +0 -2
  53. package/crates/team-agent/src/messaging/results.rs +0 -58
  54. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  55. package/crates/team-agent/src/provider/adapter.rs +8 -59
  56. package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
  57. package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
  58. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
  59. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
  60. package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
  61. package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
  62. package/crates/team-agent/src/provider/command_helpers.rs +45 -0
  63. package/crates/team-agent/src/provider/mod.rs +1 -0
  64. package/crates/team-agent/src/provider/model_catalog.rs +21 -28
  65. package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
  66. package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
  67. package/crates/team-agent/src/state/repository.rs +1 -5
  68. package/package.json +4 -4
  69. package/templates/team_state.md +1 -1
  70. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  71. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  72. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  73. package/crates/team-agent/src/messaging/peers.rs +0 -34
@@ -51,6 +51,7 @@ pub mod argv_route;
51
51
  /// 0.5.66 bypass 单源:provider → bypass argv flag 配置表(唯一权威)。
52
52
  pub(crate) mod bypass_flags;
53
53
  pub mod classify;
54
+ pub(crate) mod command_helpers;
54
55
  pub mod faults;
55
56
  pub mod model_catalog;
56
57
  pub mod session;
@@ -9,6 +9,7 @@ use std::sync::mpsc;
9
9
  use std::time::{Duration, Instant};
10
10
 
11
11
  use crate::model::enums::Provider;
12
+ use crate::provider::wire::{parse_canonical_provider, provider_wire};
12
13
 
13
14
  const CATALOG_TIMEOUT: Duration = Duration::from_secs(10);
14
15
  const MAX_CATALOG_BYTES: u64 = 1024 * 1024;
@@ -126,29 +127,7 @@ fn display_provider(provider: &str) -> &str {
126
127
 
127
128
  /// Only the documented catalog names are accepted; launch aliases stay unchanged.
128
129
  pub fn parse_catalog_provider(name: &str) -> Option<Provider> {
129
- match name {
130
- "pi" => Some(Provider::Pi),
131
- "cursor_agent" => Some(Provider::CursorAgent),
132
- "codex" => Some(Provider::Codex),
133
- "claude" => Some(Provider::Claude),
134
- "claude_code" => Some(Provider::ClaudeCode),
135
- "grok" => Some(Provider::Grok),
136
- _ => None,
137
- }
138
- }
139
-
140
- fn provider_wire_name(provider: Provider) -> &'static str {
141
- match provider {
142
- Provider::Pi => "pi",
143
- Provider::CursorAgent => "cursor_agent",
144
- Provider::Codex => "codex",
145
- Provider::Claude => "claude",
146
- Provider::ClaudeCode => "claude_code",
147
- Provider::Copilot => "copilot",
148
- Provider::GeminiCli => "gemini_cli",
149
- Provider::Grok => "grok",
150
- Provider::Fake => "fake",
151
- }
130
+ parse_canonical_provider(name).filter(|provider| catalog_source(*provider).is_some())
152
131
  }
153
132
 
154
133
  #[derive(Clone, Copy)]
@@ -175,7 +154,7 @@ fn catalog_source(provider: Provider) -> Option<CatalogSource> {
175
154
  pub fn discover_model_catalog(provider_name: &str) -> Result<Vec<ModelRecord>, CatalogError> {
176
155
  let provider = parse_catalog_provider(provider_name).ok_or(CatalogError::UnsupportedProvider)?;
177
156
  let source = catalog_source(provider).ok_or(CatalogError::UnsupportedProvider)?;
178
- let requested_provider = provider_wire_name(provider);
157
+ let requested_provider = provider_wire(provider);
179
158
  let records = match source {
180
159
  CatalogSource::Pi => {
181
160
  let bytes = run_native("pi", &["--list-models"], None, None, &[], CATALOG_TIMEOUT, MAX_CATALOG_BYTES, &mut NoopObserver)?;
@@ -881,10 +860,24 @@ mod tests {
881
860
 
882
861
  #[test]
883
862
  fn provider_capability_is_explicit_and_exact() {
884
- assert_eq!(parse_catalog_provider("claude_code"), Some(Provider::ClaudeCode));
885
- assert_eq!(parse_catalog_provider("cloud"), None);
886
- assert!(catalog_source(Provider::Codex).is_some());
887
- assert!(catalog_source(Provider::Fake).is_none());
863
+ for (name, provider) in [
864
+ ("pi", Provider::Pi),
865
+ ("cursor_agent", Provider::CursorAgent),
866
+ ("codex", Provider::Codex),
867
+ ("claude", Provider::Claude),
868
+ ("claude_code", Provider::ClaudeCode),
869
+ ("grok", Provider::Grok),
870
+ ] {
871
+ assert_eq!(parse_catalog_provider(name), Some(provider));
872
+ assert_eq!(provider_wire(provider), name);
873
+ assert!(catalog_source(provider).is_some());
874
+ }
875
+ for name in ["", "cloud", "cursor", "agent", "claude-code", "copilot", "gemini_cli", "fake", "Pi", "CODEX", " pi", "pi ", "pi/model", "codex:model", "claude_code_extra"] {
876
+ assert_eq!(parse_catalog_provider(name), None);
877
+ }
878
+ for provider in [Provider::Copilot, Provider::GeminiCli, Provider::Fake] {
879
+ assert!(catalog_source(provider).is_none());
880
+ }
888
881
  }
889
882
 
890
883
  #[test]
@@ -1,3 +1,73 @@
1
+ #[test]
2
+ fn trimmed_provider_builders_preserve_fresh_and_resume_argv() {
3
+ for (provider, base) in [
4
+ (
5
+ Provider::Grok,
6
+ vec!["grok", "--model", "test-model", "--rules", "role"],
7
+ ),
8
+ (
9
+ Provider::CursorAgent,
10
+ vec!["agent", "--model", "test-model", "--workspace", "{workspace}"],
11
+ ),
12
+ (
13
+ Provider::Copilot,
14
+ vec![
15
+ "copilot", "--no-color", "--no-auto-update", "--no-remote",
16
+ "--disable-builtin-mcps", "--allow-tool", "team_orchestrator",
17
+ "--model", "test-model",
18
+ ],
19
+ ),
20
+ ] {
21
+ let adapter = get_adapter(provider);
22
+ let ctx = ProviderCommandContext {
23
+ auth_mode: AuthMode::Subscription,
24
+ mcp_config: None,
25
+ system_prompt: Some("role"),
26
+ model: Some("test-model"),
27
+ dangerously_skip_permissions: false,
28
+ profile_launch: None,
29
+ agent_id_hint: Some("worker"),
30
+ effort: None,
31
+ };
32
+ let mut expected = base.iter().map(|s| s.to_string()).collect::<Vec<_>>();
33
+ let fresh = adapter.build_command_plan(ctx).expect("fresh plan");
34
+ if provider != Provider::CursorAgent {
35
+ expected.push("--session-id".to_string());
36
+ expected.push(
37
+ fresh.expected_session_id.as_ref().expect("session hint").as_str().to_string(),
38
+ );
39
+ } else {
40
+ assert!(fresh.expected_session_id.is_none());
41
+ }
42
+ assert_eq!(fresh.argv, expected, "{provider:?} fresh argv");
43
+ let session = SessionId::new("existing-session");
44
+ let mut expected = base.iter().map(|s| s.to_string()).collect::<Vec<_>>();
45
+ expected.extend(["--resume".to_string(), session.as_str().to_string()]);
46
+ assert_eq!(
47
+ adapter
48
+ .build_resume_command_plan(Some(&session), ctx)
49
+ .expect("resume plan")
50
+ .argv,
51
+ expected,
52
+ "{provider:?} resume argv"
53
+ );
54
+ if provider == Provider::CursorAgent {
55
+ let mut expected = base.iter().map(|s| s.to_string()).collect::<Vec<_>>();
56
+ expected.insert(1, "--resume".to_string());
57
+ expected.insert(2, session.as_str().to_string());
58
+ assert_eq!(
59
+ adapter
60
+ .build_resume_command_with_context(
61
+ Some(&session), ctx.auth_mode, None, ctx.system_prompt, ctx.model, false,
62
+ )
63
+ .expect("legacy resume"),
64
+ expected,
65
+ "legacy Cursor resume must retain its distinct flag order"
66
+ );
67
+ }
68
+ }
69
+ }
70
+
1
71
  #[test]
2
72
  fn abnormal_dedup_key_uses_signature_and_optional_turn_id() {
3
73
  // probe(/tmp/probe_idle.py read_fault_facts): the C8 dedup key is
@@ -0,0 +1,55 @@
1
+ //! Exact argv characterization: no helper may trim, escape or reorder caller values.
2
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
3
+ use super::*;
4
+ use crate::model::enums::ProviderEffort;
5
+ use crate::provider::adapters::{claude, codex, copilot, cursor_agent, grok, pi};
6
+ use crate::provider::ProviderCommandOverrides;
7
+
8
+ #[test]
9
+ fn command_helpers_keep_provider_argv_bytes_and_order() {
10
+ let raw = " \"'\\\n ";
11
+ let mcp = McpConfig { raw: serde_json::json!({}) };
12
+ assert_eq!(claude::claude_base_command(
13
+ &BasicProviderAdapter { provider: Provider::Claude }, AuthMode::Subscription,
14
+ Some(&mcp), Some(raw), Some(""), false, false, Some(ProviderEffort::High),
15
+ ).expect("claude"), ["claude", "--permission-mode", "default", "--model", "", "--effort", "high", "--append-system-prompt", raw, "--mcp-config", "{\"mcpServers\":{}}"]);
16
+
17
+ let overrides = ProviderCommandOverrides {
18
+ codex_profile: Some("".to_string()),
19
+ codex_config: vec!["model_reasoning_effort=low".to_string()],
20
+ ..Default::default()
21
+ };
22
+ assert_eq!(codex::codex_base_command(
23
+ Some("resume"), AuthMode::Subscription, Some(&mcp), Some(raw), Some(""),
24
+ true, Some(&overrides), Some(ProviderEffort::Max),
25
+ ), ["codex", "resume", "--no-alt-screen", "--disable", "shell_snapshot", "--disable", "apps", "--profile", "", "--dangerously-bypass-approvals-and-sandbox", "--model", "", "-c", "model_reasoning_effort=low", "-c", "model_reasoning_effort=max", "-c", "developer_instructions=\" \\\"'\\\\\\n \""]);
26
+
27
+ assert_eq!(copilot::copilot_base_command(
28
+ AuthMode::Subscription, Some(&mcp), Some(raw), Some(""), true,
29
+ ), ["copilot", "--no-color", "--no-auto-update", "--no-remote", "--disable-builtin-mcps", "--allow-all", "--allow-tool", "team_orchestrator", "--model", "", "--additional-mcp-config", "{}"]);
30
+ assert_eq!(cursor_agent::cursor_agent_base_command(Some(raw), true).expect("cursor"),
31
+ ["agent", "--trust", "--sandbox", "disabled", "--force", "--model", raw, "--workspace", "{workspace}"]);
32
+ assert_eq!(grok::grok_base_command(Some(raw), Some(" model-id "), true, Some(ProviderEffort::Medium)).expect("grok"),
33
+ ["grok", "--always-approve", "--model", "model-id", "--effort", "medium", "--rules", raw]);
34
+ assert_eq!(grok::grok_base_command(Some(""), Some(" "), false, None).expect("grok empty"), ["grok", "--rules", ""]);
35
+ assert_eq!(pi::build_pi_command_argv(pi::PiCommandRequest {
36
+ extension: std::path::Path::new("extension.ts"), model: Some("vendor/model"),
37
+ effort: Some(ProviderEffort::XHigh), system_prompt: raw,
38
+ session_dir: Some(std::path::Path::new("sessions")),
39
+ session: pi::PiSessionSelector::Fresh { session_id: "session-id" }, agent_id: "worker",
40
+ }).expect("pi"), ["pi", "-e", "extension.ts", "--model", "vendor/model", "--thinking", "xhigh", "--append-system-prompt", raw, "--session-dir", "sessions", "--session-id", "session-id", "--name", "worker"]);
41
+ }
42
+
43
+ #[test]
44
+ fn command_helpers_keep_absent_options_and_safe_defaults() {
45
+ assert_eq!(claude::claude_base_command(
46
+ &BasicProviderAdapter { provider: Provider::Claude }, AuthMode::Subscription,
47
+ None, None, None, false, false, None,
48
+ ).expect("claude"), ["claude", "--permission-mode", "default"]);
49
+ assert_eq!(codex::codex_base_command(None, AuthMode::Subscription, None, None, None, false, None, None),
50
+ ["codex", "--no-alt-screen", "--disable", "shell_snapshot", "--disable", "apps"]);
51
+ assert_eq!(copilot::copilot_base_command(AuthMode::Subscription, None, None, None, false),
52
+ ["copilot", "--no-color", "--no-auto-update", "--no-remote", "--disable-builtin-mcps", "--allow-tool", "team_orchestrator"]);
53
+ assert_eq!(cursor_agent::cursor_agent_base_command(None, false).expect("cursor"), ["agent", "--workspace", "{workspace}"]);
54
+ assert_eq!(grok::grok_base_command(None, None, false, None).expect("grok"), ["grok"]);
55
+ }
@@ -527,7 +527,6 @@ pub enum StateWriteIntent<'a> {
527
527
  TaskRepair {
528
528
  team_key: Option<&'a str>,
529
529
  },
530
- FakeE2eSeed,
531
530
  SelfMigration,
532
531
  }
533
532
 
@@ -743,10 +742,7 @@ fn route_direct(
743
742
  },
744
743
  // TaskRepair -> `save_team_scoped_state` (cli/adapters.rs:675).
745
744
  StateWriteIntent::TaskRepair { .. } => helper_write_team_scoped(workspace, state),
746
- // FakeE2eSeed / SelfMigration are diagnostic seams. The current
747
- // callsites live at cli/adapters.rs:1035/1070 and
748
- // state/persist.rs:1127; S1a routes them to the root helper.
749
- StateWriteIntent::FakeE2eSeed => helper_write_root(workspace, state),
745
+ // SelfMigration persists through the root helper.
750
746
  StateWriteIntent::SelfMigration => helper_write_root(workspace, state),
751
747
  }
752
748
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.111",
3
+ "version": "0.5.113",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.111",
24
- "@team-agent/cli-darwin-x64": "0.5.111",
25
- "@team-agent/cli-linux-x64": "0.5.111"
23
+ "@team-agent/cli-darwin-arm64": "0.5.113",
24
+ "@team-agent/cli-darwin-x64": "0.5.113",
25
+ "@team-agent/cli-linux-x64": "0.5.113"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -29,4 +29,4 @@ Pending.
29
29
 
30
30
  ## Next Step
31
31
 
32
- - Run `team-agent validate team.spec.yaml`, review permissions, then run `team-agent launch team.spec.yaml --yes`.
32
+ - Review role files and permissions, then run `team-agent quick-start`; use `team-agent doctor` to diagnose the team.
@@ -1,164 +0,0 @@
1
- use super::*;
2
-
3
- // =============================================================================
4
- // P0-1 LANE — `allow-peer-talk` byte-lock (was ZERO-test; a SHIPPED compat-noop).
5
- //
6
- // golden: cli/commands.py:422 -> messaging/leader.py:25 allow_peer_talk(ws,a,b)
7
- // -> message_store/core.py:326 allow_peer (INSERT OR IGNORE BOTH (a,b) & (b,a))
8
- // + events.py:27-35 EventLog.write (sort_keys=True -> .team/logs/events.jsonl).
9
- //
10
- // Golden bytes captured live (PYTHONPATH=.../src python3 /tmp/probe_app*.py):
11
- // return (insertion order): {"ok":true,"a":..,"b":..,"status":"compat_noop",
12
- // "reason":"team_scoped_peer_messages_enabled"}
13
- // return (--json sort_keys): keys a,b,ok,reason,status
14
- // event line (sort_keys): {"a":..,"b":..,"event":"communication.peer_allowed","ts":..}
15
- // idempotent: 2nd call -> identical return + a SECOND event line
16
- // (event fires per-call; DB rows are insert-or-ignore)
17
- // unknown/undeclared agents: NO validation -> ok:true compat_noop, exit 0
18
- //
19
- // Rust impl: messaging/peers.rs:11 + message_store.rs:251 + adapters.rs:173.
20
- // serde_json `preserve_order` is ON (Cargo.toml:24) so the default (human) output
21
- // iterates dict keys in golden insertion order. These tests LOCK the contract so a
22
- // porter/refactor cannot silently drift this live verb.
23
- // =============================================================================
24
-
25
- fn app_out(r: CmdResult) -> serde_json::Value {
26
- match r.output {
27
- CmdOutput::Json(v) => v,
28
- _ => panic!("allow-peer-talk must emit a Json CmdOutput"),
29
- }
30
- }
31
-
32
- fn app_events(ws: &std::path::Path) -> Vec<String> {
33
- std::fs::read_to_string(ws.join(".team").join("logs").join("events.jsonl"))
34
- .unwrap_or_default()
35
- .lines()
36
- .map(str::to_string)
37
- .collect()
38
- }
39
-
40
- fn app_call(ws: &std::path::Path, a: &str, b: &str) -> CmdResult {
41
- cmd_allow_peer_talk(&AllowPeerTalkArgs {
42
- a: a.to_string(),
43
- b: b.to_string(),
44
- workspace: ws.to_path_buf(),
45
- json: false,
46
- team: None,
47
- })
48
- .expect("allow-peer-talk is a compat-noop and must not error")
49
- }
50
-
51
- // ── return dict: exact compat_noop shape + golden insertion order (drives human emit) ──
52
- #[test]
53
- fn allow_peer_talk_output_byte_locks_golden_compat_noop() {
54
- let ws = tmp_workspace();
55
- let v = app_out(app_call(&ws, "alpha", "bravo"));
56
- assert_eq!(
57
- v,
58
- json!({
59
- "ok": true,
60
- "a": "alpha",
61
- "b": "bravo",
62
- "status": "compat_noop",
63
- "reason": "team_scoped_peer_messages_enabled"
64
- }),
65
- "golden leader.py:28 return dict (key set + values); got {v:?}"
66
- );
67
- let order: Vec<&str> = v
68
- .as_object()
69
- .expect("object")
70
- .keys()
71
- .map(String::as_str)
72
- .collect();
73
- assert_eq!(
74
- order,
75
- vec!["ok", "a", "b", "status", "reason"],
76
- "golden dict insertion order ok,a,b,status,reason drives the default human emit key order \
77
- (helpers.py:16-19); got {order:?}"
78
- );
79
- let _ = std::fs::remove_dir_all(&ws);
80
- }
81
-
82
- // ── event: exactly one `communication.peer_allowed` line, sort_keys byte form, payload {a,b} ──
83
- #[test]
84
- fn allow_peer_talk_event_byte_locks_golden_line() {
85
- let ws = tmp_workspace();
86
- let _ = app_call(&ws, "alpha", "bravo");
87
- let lines = app_events(&ws);
88
- assert_eq!(lines.len(), 1, "exactly one event per call; got {lines:?}");
89
- let v: serde_json::Value = serde_json::from_str(&lines[0]).expect("event line must be json");
90
- let ts = v["ts"].as_str().expect("event has string ts").to_string();
91
- let expected = format!(
92
- "{{\"a\": \"alpha\", \"b\": \"bravo\", \"event\": \"communication.peer_allowed\", \"ts\": \"{ts}\"}}"
93
- );
94
- assert_eq!(
95
- lines[0], expected,
96
- "golden events.py:35 json.dumps(...,sort_keys=True) line: sorted keys a,b,event,ts with \
97
- Python `, `/`: ` spacing; got {}",
98
- lines[0]
99
- );
100
- let _ = std::fs::remove_dir_all(&ws);
101
- }
102
-
103
- // ── idempotency: identical return, event fires on EVERY call (rows are insert-or-ignore) ──
104
- #[test]
105
- fn allow_peer_talk_is_idempotent_and_fires_event_per_call() {
106
- let ws = tmp_workspace();
107
- let v1 = app_out(app_call(&ws, "alpha", "bravo"));
108
- let v2 = app_out(app_call(&ws, "alpha", "bravo")); // duplicate pair
109
- assert_eq!(
110
- v1, v2,
111
- "repeated allow returns identical compat_noop output"
112
- );
113
- let lines = app_events(&ws);
114
- assert_eq!(
115
- lines.len(),
116
- 2,
117
- "golden fires communication.peer_allowed on EVERY call (event NOT deduped; only DB rows \
118
- are insert-or-ignore); got {lines:?}"
119
- );
120
- let _ = std::fs::remove_dir_all(&ws);
121
- }
122
-
123
- // ── permissive: undeclared/unknown agents still succeed (golden has NO validation path) ──
124
- #[test]
125
- fn allow_peer_talk_unknown_agents_succeed_no_validation() {
126
- let ws = tmp_workspace(); // no state.json / no spec: agents never declared
127
- let r = app_call(&ws, "ghost", "phantom-x");
128
- assert_eq!(
129
- r.exit,
130
- ExitCode::Ok,
131
- "golden leader.py:25-28 never validates agent ids -> no error, exit 0"
132
- );
133
- let v = app_out(r);
134
- assert_eq!(
135
- v,
136
- json!({
137
- "ok": true,
138
- "a": "ghost",
139
- "b": "phantom-x",
140
- "status": "compat_noop",
141
- "reason": "team_scoped_peer_messages_enabled"
142
- }),
143
- "unknown agents still get compat_noop (no unknown-agent error string/exit); got {v:?}"
144
- );
145
- let _ = std::fs::remove_dir_all(&ws);
146
- }
147
-
148
- #[test]
149
- fn allow_peer_talk_explicit_team_is_rejected_until_backend_is_scoped() {
150
- let ws = tmp_workspace();
151
- let err = cmd_allow_peer_talk(&AllowPeerTalkArgs {
152
- a: "alpha".to_string(),
153
- b: "bravo".to_string(),
154
- workspace: ws.clone(),
155
- json: true,
156
- team: Some("current".to_string()),
157
- })
158
- .expect_err("explicit --team must not silently write the global peer allowlist");
159
- assert!(
160
- err.to_string().contains("not supported yet"),
161
- "allow-peer-talk --team must be an explicit refusal until backend supports scoped writes; got {err}"
162
- );
163
- let _ = std::fs::remove_dir_all(&ws);
164
- }
@@ -1,195 +0,0 @@
1
- use super::*;
2
-
3
- const VALIDATE_SPEC_TEMPLATE: &str = r#"version: 1
4
- team:
5
- name: "fake-e2e"
6
- mode: "supervisor_worker"
7
- objective: "Exercise validate."
8
- workspace: "__WS__"
9
- leader:
10
- id: "leader"
11
- role: "leader"
12
- provider: "fake"
13
- model: null
14
- tools:
15
- - "fs_read"
16
- - "fs_list"
17
- - "mcp_team"
18
- context_policy:
19
- keep_user_thread: true
20
- receive_worker_outputs: "structured_only"
21
- max_worker_result_tokens: 2000
22
- agents:
23
- - id: "fake_impl"
24
- role: "implementation_engineer"
25
- provider: "fake"
26
- model: null
27
- working_directory: "__WS__"
28
- system_prompt:
29
- inline: "Handle fake implementation tasks."
30
- file: null
31
- tools:
32
- - "fs_read"
33
- - "fs_write"
34
- - "fs_list"
35
- - "execute_bash"
36
- - "git_diff"
37
- - "mcp_team"
38
- - "provider_builtin"
39
- dangerously_skip_permissions: false
40
- preferred_for:
41
- - "implementation"
42
- avoid_for: []
43
- output_contract:
44
- format: "result_envelope_v1"
45
- required_fields:
46
- - "task_id"
47
- - "status"
48
- - "summary"
49
- - "artifacts"
50
- routing:
51
- default_assignee: "leader"
52
- rules:
53
- - id: "implementation-to-fake"
54
- match:
55
- type:
56
- - "implementation"
57
- assign_to: "fake_impl"
58
- priority: 10
59
- communication:
60
- protocol: "mcp_inbox"
61
- topology: "leader_centered"
62
- worker_to_worker: true
63
- ack_timeout_sec: 2
64
- result_format: "result_envelope_v1"
65
- message_store:
66
- sqlite: ".team/runtime/team.db"
67
- mirror_files: ".team/messages"
68
- runtime:
69
- backend: "tmux"
70
- session_name: "team-agent-fake-e2e"
71
- auto_launch: true
72
- require_user_approval_before_launch: false
73
- max_active_agents: 1
74
- startup_order:
75
- - "fake_impl"
76
- context:
77
- state_file: "team_state.md"
78
- artifact_dir: ".team/artifacts"
79
- log_dir: ".team/logs"
80
- summarization:
81
- worker_full_logs: "retain_outside_leader_context"
82
- state_update: "after_each_result"
83
- tasks:
84
- - id: "task_impl"
85
- title: "Fake implementation"
86
- type: "implementation"
87
- assignee: null
88
- deps: []
89
- acceptance:
90
- - "fake result collected"
91
- status: "pending"
92
- "#;
93
-
94
- fn write_validate_spec(
95
- ws: &std::path::Path,
96
- file_name: &str,
97
- provider: &str,
98
- ) -> std::path::PathBuf {
99
- let spec = VALIDATE_SPEC_TEMPLATE
100
- .replace("__WS__", &ws.to_string_lossy())
101
- .replacen(
102
- "provider: \"fake\"",
103
- &format!("provider: \"{provider}\""),
104
- 1,
105
- );
106
- let path = ws.join(file_name);
107
- std::fs::write(&path, spec).unwrap();
108
- path
109
- }
110
-
111
- // Golden source:
112
- // - cli/parser.py:120-123 registers `validate [spec=team.spec.yaml] --json`.
113
- // - cli/commands.py:38-39 delegates to `runtime.validate_file(Path(args.spec).resolve())`.
114
- // - launch/bootstrap.py:30-43: file specs return `{ok: True, workspace: str(workspace), team: name}`.
115
- // - cli/helpers.py:12-16 JSON emits `json.dumps(..., indent=2, ensure_ascii=False, sort_keys=True)`.
116
- //
117
- // Golden probe:
118
- // PYTHONPATH=/Users/alauda/Documents/code/team-agent-public/src python3 /tmp/probe_validate_cli.py
119
- // valid file --json rc=0 stdout:
120
- // {
121
- // "ok": true,
122
- // "team": "fake-e2e",
123
- // "workspace": "<ws>"
124
- // }
125
- #[test]
126
- fn validate_valid_spec_file_json_byte_shape() {
127
- let ws = tmp_workspace();
128
- let spec_path = write_validate_spec(&ws, "team.spec.yaml", "fake");
129
-
130
- let result = cmd_validate(&ValidateArgs {
131
- spec: spec_path.clone(),
132
- json: true,
133
- })
134
- .expect("valid spec must return an ok CmdResult");
135
-
136
- assert_eq!(result.exit, ExitCode::Ok);
137
- assert_eq!(
138
- emit(&result.output, true).unwrap(),
139
- format!(
140
- "{{\n \"ok\": true,\n \"team\": \"fake-e2e\",\n \"workspace\": \"{}\"\n}}",
141
- ws.to_string_lossy()
142
- ),
143
- "validate --json must match Python sort_keys+indent bytes for a valid file spec",
144
- );
145
- let _ = std::fs::remove_dir_all(&ws);
146
- }
147
-
148
- // Golden source:
149
- // - spec.py:85-91 accumulates validation messages and raises
150
- // `team.spec.yaml validation failed:\n- ...`.
151
- // - spec.py semantic provider check emits `/leader/provider: unknown provider 'bogus'`.
152
- // - cli/helpers.py:137-155 error payload insertion order is `ok,error,action,log`.
153
- // - cli/helpers.py:128-131 prints compact JSON, not pretty/sorted JSON, for `--json` errors.
154
- #[test]
155
- fn validate_invalid_spec_json_error_envelope_byte_shape() {
156
- let ws = tmp_workspace();
157
- let spec_path = write_validate_spec(&ws, "bad.spec.yaml", "bogus");
158
-
159
- let err = cmd_validate(&ValidateArgs {
160
- spec: spec_path,
161
- json: true,
162
- })
163
- .expect_err("invalid spec must surface a CLI/runtime validation error");
164
-
165
- assert_eq!(
166
- err.to_string(),
167
- "team.spec.yaml validation failed:\n- /leader/provider: unknown provider 'bogus'",
168
- );
169
- let payload = err.to_payload(&ws.join(".team/logs/cli-error-123.log"), "validate");
170
- assert_eq!(
171
- serde_json::to_string(&payload).unwrap(),
172
- format!(
173
- "{{\"ok\":false,\"error\":\"team.spec.yaml validation failed:\\n- /leader/provider: unknown provider 'bogus'\",\"action\":\"run `team-agent doctor` or inspect the log path shown here\",\"log\":\"{}\"}}",
174
- ws.join(".team/logs/cli-error-123.log").to_string_lossy()
175
- ),
176
- "validate --json errors must match Python compact error envelope key order and text",
177
- );
178
- let _ = std::fs::remove_dir_all(&ws);
179
- }
180
-
181
- // Golden source:
182
- // - cli/parser.py:120-123 default spec argument is `team.spec.yaml`.
183
- // Current Rust dispatch has no `validate` arm; this must be RED until the porter routes it.
184
- #[test]
185
- fn dispatch_routes_validate_default_spec() {
186
- let ws = tmp_workspace();
187
- let _spec_path = write_validate_spec(&ws, "team.spec.yaml", "fake");
188
- let code = run(&["validate".to_string(), "--json".to_string()], &ws);
189
- assert_eq!(
190
- code,
191
- ExitCode::Ok,
192
- "`validate --json` must route and exit 0 for default team.spec.yaml"
193
- );
194
- let _ = std::fs::remove_dir_all(&ws);
195
- }