@team-agent/installer 0.5.110 → 0.5.112

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 (54) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +231 -812
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +493 -451
  6. package/crates/team-agent/src/cli/leader.rs +4 -9
  7. package/crates/team-agent/src/cli/leaders.rs +90 -29
  8. package/crates/team-agent/src/cli/mod.rs +348 -169
  9. package/crates/team-agent/src/cli/models.rs +80 -24
  10. package/crates/team-agent/src/cli/named_address.rs +114 -25
  11. package/crates/team-agent/src/cli/route.rs +10 -5
  12. package/crates/team-agent/src/cli/send/presentation.rs +60 -41
  13. package/crates/team-agent/src/cli/send/resolve.rs +25 -18
  14. package/crates/team-agent/src/cli/send.rs +4 -4
  15. package/crates/team-agent/src/cli/spec.rs +53 -109
  16. package/crates/team-agent/src/cli/tests/base.rs +189 -82
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
  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 +19 -22
  23. package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
  24. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
  25. package/crates/team-agent/src/cli/types.rs +26 -121
  26. package/crates/team-agent/src/coordinator/health.rs +3 -283
  27. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  28. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  29. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  30. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  31. package/crates/team-agent/src/coordinator/types.rs +1 -21
  32. package/crates/team-agent/src/db/message_store.rs +0 -42
  33. package/crates/team-agent/src/db/migration.rs +49 -9
  34. package/crates/team-agent/src/db/schema.rs +27 -20
  35. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  36. package/crates/team-agent/src/leader/registry.rs +3 -3
  37. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  38. package/crates/team-agent/src/leader/tests/identity.rs +0 -32
  39. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  40. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
  41. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  42. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
  43. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  44. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  45. package/crates/team-agent/src/messaging/mod.rs +0 -2
  46. package/crates/team-agent/src/messaging/results.rs +0 -58
  47. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  48. package/crates/team-agent/src/state/repository.rs +1 -5
  49. package/package.json +4 -4
  50. package/templates/team_state.md +1 -1
  51. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  52. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  53. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  54. package/crates/team-agent/src/messaging/peers.rs +0 -34
@@ -23,7 +23,7 @@ use std::time::{Duration, Instant};
23
23
 
24
24
  use serde_json::{json, Value};
25
25
  use serial_test::serial;
26
- use team_agent::cli::{cmd_preflight, emit, CmdOutput, PreflightArgs};
26
+ use team_agent::cli::{cmd_doctor, emit, CmdOutput, DoctorArgs};
27
27
  use team_agent::event_log::EventLog;
28
28
  use team_agent::message_store::MessageStore;
29
29
  use team_agent::state::persist::save_runtime_state;
@@ -334,11 +334,10 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
334
334
  env.scrub_tmux();
335
335
  env.assert_no_real_tmux();
336
336
  let workspace = env.workspace("red6");
337
- let spec = write_invalid_validate_spec(&workspace, &marker);
338
337
  let mut combined = String::new();
339
338
  for json_mode in [true, false] {
340
339
  let mut command = Command::new(crate::lifecycle::tests::test_binary_path());
341
- command.arg("validate").arg(&spec);
340
+ command.arg("profile").arg(credential_url(&marker)).arg("synthetic-profile");
342
341
  if json_mode {
343
342
  command.arg("--json");
344
343
  }
@@ -351,7 +350,7 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
351
350
  .expect("run top-level CLI error boundary");
352
351
  assert!(
353
352
  !output.status.success(),
354
- "invalid spec must reach CLI error"
353
+ "invalid profile subcommand must reach CLI error"
355
354
  );
356
355
  combined.push_str(&String::from_utf8_lossy(&output.stdout));
357
356
  combined.push_str(&String::from_utf8_lossy(&output.stderr));
@@ -391,15 +390,30 @@ fn red7_pane_and_profile_scrubbers_converge_without_losing_existing_policy() {
391
390
  let env = hermetic_guard::HermeticTestEnv::enter("redact-red7");
392
391
  let pane_policy_preserved = pane_excerpt_uses_shared_redaction(&env, &marker);
393
392
  let team = write_profile_fixture(env.root(), server.base_url(), &profile_token);
394
- let result = cmd_preflight(&PreflightArgs { team, json: true })
393
+ let result = cmd_doctor(&DoctorArgs {
394
+ spec: Some(team.join("TEAM.md")),
395
+ workspace: team,
396
+ gate: None,
397
+ comms: false,
398
+ team: None,
399
+ fix: false,
400
+ fix_schema: false,
401
+ cleanup_orphans: false,
402
+ confirm: false,
403
+ json: true,
404
+ })
395
405
  .expect("profile smoke returns a diagnostic report");
396
406
  let report = match result.output {
397
407
  CmdOutput::Json(value) => value,
398
- other => panic!("expected JSON preflight output, got {other:?}"),
408
+ other => panic!("expected JSON doctor output, got {other:?}"),
399
409
  };
400
- let text = report.to_string();
401
- let profile_policy_preserved = !text.contains(&marker)
402
- && !text.contains(&profile_token)
410
+ let report_text = report.to_string();
411
+ let text = report
412
+ .get("profile_smoke")
413
+ .unwrap_or_else(|| panic!("doctor must expose profile_smoke; report={report}"))
414
+ .to_string();
415
+ let profile_policy_preserved = !report_text.contains(&marker)
416
+ && !report_text.contains(&profile_token)
403
417
  && !text.contains(tail_sentinel)
404
418
  && text.contains(REDACTED)
405
419
  && text.contains("proxy.invalid:8443/path")
@@ -555,20 +569,6 @@ fn run_mcp_status_frames(
555
569
  .collect()
556
570
  }
557
571
 
558
- fn write_invalid_validate_spec(workspace: &Path, marker: &str) -> PathBuf {
559
- let path = workspace.join("invalid-provider.spec.yaml");
560
- fs::write(
561
- &path,
562
- format!(
563
- "version: 1\nteam:\n id: current\n name: current\n session_name: team-redaction-contract\n workspace: \"{}\"\nleader:\n provider: \"{}\"\nagents:\n - id: worker\n provider: fake\n model: fake\n role: Worker\n window: worker\ntasks: []\n",
564
- workspace.display(),
565
- credential_url(marker)
566
- ),
567
- )
568
- .unwrap();
569
- path
570
- }
571
-
572
572
  fn read_cli_error_logs(workspace: &Path) -> String {
573
573
  let dir = workspace.join(".team/logs");
574
574
  fs::read_dir(dir)
@@ -92,15 +92,10 @@ const SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART: &str = r#"> You are in /private/
92
92
  │ directory: /private/tmp/…/teamdir │
93
93
  ╰───────────────────────────────────────╯
94
94
 
95
-
96
95
  › Find and fix a bug in @filename
97
96
 
98
97
  gpt-5.5 default · /private/tmp/ta-subroot-SUB-PROBE-1-trust-startup.uL9Boc/te…
99
98
 
100
-
101
-
102
-
103
-
104
99
  "#;
105
100
 
106
101
  fn tmp_dir(tag: &str) -> TempWorkspace {
@@ -419,8 +414,7 @@ fn contract_a_own_workspace_trust_is_actionable_not_ready_and_foreign_is_pending
419
414
  }
420
415
 
421
416
  #[test]
422
- fn contract_a_probe5_quick_start_ready_is_invalid_while_actionable_trust_remains() {
423
- use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
417
+ fn contract_a_runtime_readiness_is_false_while_actionable_trust_remains() {
424
418
  use team_agent::provider::{classify_codex_startup_screen, StartupScreenDecision};
425
419
 
426
420
  assert_eq!(
@@ -429,67 +423,47 @@ fn contract_a_probe5_quick_start_ready_is_invalid_while_actionable_trust_remains
429
423
  "fixture sanity: probe5 pane text is an actionable own-workspace trust prompt"
430
424
  );
431
425
 
432
- let ws = tmp_dir("a-wait-ready-trust");
426
+ let ws = tmp_dir("a-runtime-readiness-trust");
433
427
  let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
434
428
  std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
435
429
  std::fs::write(&mcp_config, "{}").unwrap();
436
- team_agent::state::persist::save_runtime_state(
437
- &ws,
438
- &json!({
439
- "session_name": "team-subprep",
440
- "agents": {
441
- "w1": {
442
- "provider": "codex",
443
- "status": "awaiting_trust_prompt",
444
- "startup_prompt_status": "awaiting_trust_prompt",
445
- "startup_prompts": "awaiting_trust_prompt",
446
- "pane_id": "%7",
447
- "window": "w1",
448
- "mcp_config": mcp_config.to_string_lossy().to_string(),
449
- "task_prompt_delivered": true
450
- }
430
+ // Keep every other readiness gate satisfied; an unattached leader would
431
+ // make ready=false independently of the trust prompt under test.
432
+ let mut state = json!({
433
+ "session_name": "team-subprep",
434
+ "tmux_session_present": true,
435
+ "leader_receiver": {"status": "attached", "pane_id": "%9"},
436
+ "agents": {
437
+ "w1": {
438
+ "provider": "codex",
439
+ "status": "awaiting_trust_prompt",
440
+ "startup_prompt_status": "awaiting_trust_prompt",
441
+ "startup_prompts": "awaiting_trust_prompt",
442
+ "pane_id": "%7",
443
+ "window": "w1",
444
+ "mcp_config": mcp_config.to_string_lossy().to_string(),
445
+ "task_prompt_delivered": true
451
446
  }
452
- }),
453
- )
454
- .unwrap();
455
-
456
- let result = cmd_wait_ready(&WaitReadyArgs {
457
- workspace: ws.to_path_buf(),
458
- timeout: 0.0,
459
- json: true,
460
- team: None,
461
- })
462
- .expect("wait-ready SUT should return shaped JSON");
463
- let value = match result.output {
464
- CmdOutput::Json(value) => value,
465
- other => panic!("wait-ready --json must return JSON output, got {other:?}"),
466
- };
447
+ }
448
+ });
467
449
 
450
+ let value = crate::cli::diagnose::wait_readiness(&state);
468
451
  assert_eq!(
469
- value["ok"],
470
- json!(false),
471
- "Contract A: quick-start/wait-ready readiness must not report ok=true while actionable trust remains; got {value}"
472
- );
473
- assert_eq!(
474
- value["status"],
475
- json!("pending"),
476
- "Contract A: actionable trust prompt should keep readiness pending, not ready; got {value}"
477
- );
478
- assert_eq!(
479
- value["reason"],
480
- json!("awaiting_trust_prompt"),
481
- "Contract A: not-ready reason must identify the startup trust boundary; got {value}"
482
- );
483
- assert_eq!(
484
- value["readiness"]["awaiting_trust_prompt"],
452
+ value["awaiting_trust_prompt"],
485
453
  json!(true),
486
454
  "Contract A: ready logic must surface awaiting_trust_prompt=true; got {value}"
487
455
  );
488
456
  assert_eq!(
489
- value["readiness"]["ready"],
457
+ value["ready"],
490
458
  json!(false),
491
459
  "Contract A: ready must stay false until the actionable trust prompt is cleared; got {value}"
492
460
  );
461
+ state["agents"]["w1"]["status"] = json!("running");
462
+ state["agents"]["w1"]["startup_prompt_status"] = json!("complete");
463
+ state["agents"]["w1"]["startup_prompts"] = json!("complete");
464
+ let cleared = crate::cli::diagnose::wait_readiness(&state);
465
+ assert_eq!(cleared["awaiting_trust_prompt"], json!(false));
466
+ assert_eq!(cleared["ready"], json!(true), "positive control: {cleared}");
493
467
  }
494
468
 
495
469
  #[test]
@@ -67,7 +67,6 @@ pub mod delivery;
67
67
  pub mod helpers;
68
68
  pub mod leader_channel;
69
69
  pub mod leader_receiver;
70
- pub mod peers;
71
70
  pub mod persist;
72
71
  pub mod presentation;
73
72
  pub mod results;
@@ -96,7 +95,6 @@ pub use leader_receiver::{
96
95
  mirror_peer_message_to_leader, send_to_leader_receiver,
97
96
  send_to_leader_receiver_with_presentation,
98
97
  };
99
- pub use peers::allow_peer_talk;
100
98
  pub use persist::{
101
99
  persist_resolved_send, DeliveryBlocker, InitialDisposition, InternalSendKind,
102
100
  LogicalRecipient, PersistResolution, PersistedSend, ResolvedSendIntent, SendOrigin,
@@ -45,64 +45,6 @@ impl Default for ResultRoute {
45
45
  }
46
46
  }
47
47
 
48
- pub fn results_for_case(
49
- workspace: &Path,
50
- case_id: &str,
51
- owner_team_id: Option<&str>,
52
- requested_owner_team_id: Option<&str>,
53
- ) -> Result<Vec<serde_json::Value>, MessagingError> {
54
- let current_db = workspace.join(".team/runtime/team.db");
55
- let legacy_db = workspace.join(".team/team.db");
56
- if current_db.exists() {
57
- return results_for_case_in_db(
58
- &current_db,
59
- case_id,
60
- owner_team_id,
61
- requested_owner_team_id,
62
- );
63
- }
64
- if legacy_db.exists() {
65
- return results_for_case_in_db(&legacy_db, case_id, owner_team_id, requested_owner_team_id);
66
- }
67
- Ok(Vec::new())
68
- }
69
-
70
- fn results_for_case_in_db(
71
- db_path: &Path,
72
- case_id: &str,
73
- owner_team_id: Option<&str>,
74
- requested_owner_team_id: Option<&str>,
75
- ) -> Result<Vec<serde_json::Value>, MessagingError> {
76
- let conn = crate::db::schema::open_db(db_path)?;
77
- // The second predicate works around
78
- // `.team/bugs/BUG-caseid-writable-when-route-undeclared-20260730.md`;
79
- // it is not design intent.
80
- let case_predicate = "task_id = ?1 or json_extract(envelope,'$.presentation.case_id') = ?1";
81
- let sql = match owner_team_id {
82
- Some(_) => format!(
83
- "select envelope from results where ({case_predicate}) \
84
- and (owner_team_id = ?2 or owner_team_id = ?3) \
85
- order by created_at, result_id"
86
- ),
87
- None => format!(
88
- "select envelope from results where {case_predicate} order by created_at, result_id"
89
- ),
90
- };
91
- let mut stmt = conn.prepare(&sql)?;
92
- let row_mapper = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
93
- let rows = match (owner_team_id, requested_owner_team_id) {
94
- (Some(team), Some(requested_team)) => {
95
- stmt.query_map(params![case_id, team, requested_team], row_mapper)
96
- }
97
- (Some(team), None) => stmt.query_map(params![case_id, team, team], row_mapper),
98
- (None, _) => stmt.query_map(params![case_id], row_mapper),
99
- }?
100
- .collect::<Result<Vec<_>, _>>()?;
101
- rows.into_iter()
102
- .map(|envelope| serde_json::from_str(&envelope).map_err(MessagingError::from))
103
- .collect()
104
- }
105
-
106
48
  /// `collect` (`results.py:45`):投递 pending、捞 uncollected results、校验 envelope、更新任务态、
107
49
  /// 写 team_state、ensure coordinator。CLI `collect` + coordinator tick 调。
108
50
  pub fn collect(
@@ -1090,52 +1090,6 @@ fn collect_rejects_message_scoped_result_without_matching_recipient() {
1090
1090
  );
1091
1091
  }
1092
1092
 
1093
- #[test]
1094
- fn allow_peer_talk_records_bidirectional_allowlist_and_event() {
1095
- let ws = tmp_ws("allowpeer");
1096
- let out = allow_peer_talk(&ws, "alice", "bob").unwrap();
1097
- assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true));
1098
- assert_eq!(out.get("a").and_then(|v| v.as_str()), Some("alice"));
1099
- assert_eq!(out.get("b").and_then(|v| v.as_str()), Some("bob"));
1100
- assert_eq!(
1101
- out.get("status").and_then(|v| v.as_str()),
1102
- Some("compat_noop")
1103
- );
1104
- assert_eq!(
1105
- out.get("reason").and_then(|v| v.as_str()),
1106
- Some("team_scoped_peer_messages_enabled")
1107
- );
1108
-
1109
- let store = store_for(&ws);
1110
- let conn = seed_conn(&store);
1111
- let rows = conn
1112
- .prepare("select a, b from peer_allowlist order by a, b")
1113
- .unwrap()
1114
- .query_map([], |row| {
1115
- Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1116
- })
1117
- .unwrap()
1118
- .collect::<Result<Vec<_>, _>>()
1119
- .unwrap();
1120
- assert_eq!(
1121
- rows,
1122
- vec![
1123
- ("alice".to_string(), "bob".to_string()),
1124
- ("bob".to_string(), "alice".to_string()),
1125
- ]
1126
- );
1127
-
1128
- let events = EventLog::new(&ws).tail(10).unwrap();
1129
- let event = events
1130
- .iter()
1131
- .find(|event| {
1132
- event.get("event").and_then(|v| v.as_str()) == Some("communication.peer_allowed")
1133
- })
1134
- .expect("communication.peer_allowed event");
1135
- assert_eq!(event.get("a").and_then(|v| v.as_str()), Some("alice"));
1136
- assert_eq!(event.get("b").and_then(|v| v.as_str()), Some("bob"));
1137
- }
1138
-
1139
1093
  // ════════════════════════════════════════════════════════════════════════
1140
1094
  // GROUP R — run_comms_selftest: §84 / MUST-NOT-13 zero-provider-SDK gate.
1141
1095
  // diagnose/comms.py:21-47. The whole point: assert zero provider client calls.
@@ -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.110",
3
+ "version": "0.5.112",
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.110",
24
- "@team-agent/cli-darwin-x64": "0.5.110",
25
- "@team-agent/cli-linux-x64": "0.5.110"
23
+ "@team-agent/cli-darwin-arm64": "0.5.112",
24
+ "@team-agent/cli-darwin-x64": "0.5.112",
25
+ "@team-agent/cli-linux-x64": "0.5.112"
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
- }