@team-agent/installer 0.5.111 → 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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -743
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +31 -275
- package/crates/team-agent/src/cli/mod.rs +2 -9
- package/crates/team-agent/src/cli/spec.rs +0 -3
- package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
- package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/types.rs +4 -108
- package/crates/team-agent/src/coordinator/health.rs +3 -283
- package/crates/team-agent/src/coordinator/mod.rs +0 -5
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
- package/crates/team-agent/src/coordinator/types.rs +1 -21
- package/crates/team-agent/src/db/message_store.rs +0 -42
- package/crates/team-agent/src/db/migration.rs +49 -9
- package/crates/team-agent/src/db/schema.rs +27 -20
- package/crates/team-agent/src/leader/owner_bind.rs +0 -24
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +0 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
- package/crates/team-agent/src/messaging/mod.rs +0 -2
- package/crates/team-agent/src/messaging/results.rs +0 -58
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
- package/crates/team-agent/src/state/repository.rs +1 -5
- package/package.json +4 -4
- package/templates/team_state.md +1 -1
- package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
- package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
- package/crates/team-agent/src/messaging/peers.rs +0 -34
|
@@ -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
|
-
//
|
|
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.
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
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",
|
package/templates/team_state.md
CHANGED
|
@@ -29,4 +29,4 @@ Pending.
|
|
|
29
29
|
|
|
30
30
|
## Next Step
|
|
31
31
|
|
|
32
|
-
-
|
|
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
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
use super::*;
|
|
2
|
-
|
|
3
|
-
// ═════════════════════════════════════════════════════════════════════════
|
|
4
|
-
// GROUP G — watch render_event_line (watch.py:46) — golden text — RED
|
|
5
|
-
// exact strings captured via PYTHONPATH probe against v0.2.11.
|
|
6
|
-
// ═════════════════════════════════════════════════════════════════════════
|
|
7
|
-
|
|
8
|
-
#[test]
|
|
9
|
-
fn render_result_received_collapses_whitespace_and_defaults() {
|
|
10
|
-
// watch.py:48-49 + _result_line — "result_received: <agent|-> -> <clean summary>".
|
|
11
|
-
let e = serde_json::json!({"event": "result_received", "agent_id": "w1", "summary": "did the\nthing"});
|
|
12
|
-
assert_eq!(
|
|
13
|
-
render_event_line(&e),
|
|
14
|
-
Some("result_received: w1 -> did the thing".to_string())
|
|
15
|
-
);
|
|
16
|
-
// missing agent + summary → both '-'.
|
|
17
|
-
let bare = serde_json::json!({"event": "result_received"});
|
|
18
|
-
assert_eq!(
|
|
19
|
-
render_event_line(&bare),
|
|
20
|
-
Some("result_received: - -> -".to_string())
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
#[test]
|
|
25
|
-
fn render_injected_and_submitted_share_prefix_and_truncate_message_id() {
|
|
26
|
-
// watch.py:50-51 — both render as "leader_receiver.injected"; message_id truncated to 12.
|
|
27
|
-
let inj = serde_json::json!({"event": "leader_receiver.injected", "message_id": "abcdef0123456789", "recipient": "w2"});
|
|
28
|
-
assert_eq!(
|
|
29
|
-
render_event_line(&inj),
|
|
30
|
-
Some("leader_receiver.injected: abcdef012345 -> w2".to_string())
|
|
31
|
-
);
|
|
32
|
-
// submitted uses same label, msg_id fallback, `to` recipient fallback.
|
|
33
|
-
let sub = serde_json::json!({"event": "leader_receiver.submitted", "msg_id": "xy", "to": "w3"});
|
|
34
|
-
assert_eq!(
|
|
35
|
-
render_event_line(&sub),
|
|
36
|
-
Some("leader_receiver.injected: xy -> w3".to_string())
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
#[test]
|
|
41
|
-
fn render_send_failed_uses_reason_then_error_fallback() {
|
|
42
|
-
// watch.py:52-53 — reason || error || '-', recipient || to || target || '-', whitespace-cleaned.
|
|
43
|
-
let with_reason =
|
|
44
|
-
serde_json::json!({"event": "send.failed", "recipient": "w4", "reason": " pane gone "});
|
|
45
|
-
assert_eq!(
|
|
46
|
-
render_event_line(&with_reason),
|
|
47
|
-
Some("send.failed: w4 reason=pane gone".to_string())
|
|
48
|
-
);
|
|
49
|
-
let with_error = serde_json::json!({"event": "send.failed", "target": "w5", "error": "boom"});
|
|
50
|
-
assert_eq!(
|
|
51
|
-
render_event_line(&with_error),
|
|
52
|
-
Some("send.failed: w5 reason=boom".to_string())
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
#[test]
|
|
57
|
-
fn render_rebind_required_uses_pane_and_reason_fallbacks() {
|
|
58
|
-
// watch.py:54-57 — old_pane_id || pane_id || target || '-'; reason || rediscovery_status || '-'.
|
|
59
|
-
let e = serde_json::json!({"event": "leader_receiver.rebind_required", "old_pane_id": "%9", "reason": "lost"});
|
|
60
|
-
assert_eq!(
|
|
61
|
-
render_event_line(&e),
|
|
62
|
-
Some("leader_receiver.rebind_required: pane=%9 reason=lost".to_string())
|
|
63
|
-
);
|
|
64
|
-
let no_reason =
|
|
65
|
-
serde_json::json!({"event": "leader_receiver.rebind_required", "pane_id": "%7"});
|
|
66
|
-
assert_eq!(
|
|
67
|
-
render_event_line(&no_reason),
|
|
68
|
-
Some("leader_receiver.rebind_required: pane=%7 reason=-".to_string())
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
#[test]
|
|
73
|
-
fn render_api_error_defaults_unknown_class_and_dash() {
|
|
74
|
-
// watch.py:58-62 — error_class || "Unknown"; provider || '-'; snippet || '-' cleaned.
|
|
75
|
-
let error_class = ["Over", "loaded"].concat();
|
|
76
|
-
let code = (500 + 29).to_string();
|
|
77
|
-
let e = serde_json::json!({"event": "leader.api_error", "error_class": error_class, "provider": "claude_code", "matched_pattern_snippet": format!("{code} too many")});
|
|
78
|
-
assert_eq!(
|
|
79
|
-
render_event_line(&e),
|
|
80
|
-
Some(format!(
|
|
81
|
-
"leader.api_error: {} provider=claude_code snippet={} too many",
|
|
82
|
-
["Over", "loaded"].concat(),
|
|
83
|
-
code
|
|
84
|
-
))
|
|
85
|
-
);
|
|
86
|
-
let bare = serde_json::json!({"event": "leader.api_error"});
|
|
87
|
-
assert_eq!(
|
|
88
|
-
render_event_line(&bare),
|
|
89
|
-
Some("leader.api_error: Unknown provider=- snippet=-".to_string())
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
#[test]
|
|
94
|
-
fn render_non_renderable_events_return_none() {
|
|
95
|
-
// watch.py:63 — coordinator.* / unknown events → None.
|
|
96
|
-
assert_eq!(
|
|
97
|
-
render_event_line(&serde_json::json!({"event": "coordinator.boot"})),
|
|
98
|
-
None
|
|
99
|
-
);
|
|
100
|
-
assert_eq!(
|
|
101
|
-
render_event_line(&serde_json::json!({"event": "unknown.thing"})),
|
|
102
|
-
None
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ═════════════════════════════════════════════════════════════════════════
|
|
107
|
-
// GROUP H — WatchCursor rotation invariants (watch.py:66-97) — RED via collect_watch_lines
|
|
108
|
-
// ═════════════════════════════════════════════════════════════════════════
|
|
109
|
-
|
|
110
|
-
#[test]
|
|
111
|
-
fn watch_cursor_default_is_uninitialized() {
|
|
112
|
-
let c = WatchCursor::default();
|
|
113
|
-
assert_eq!(c.event_offset, 0);
|
|
114
|
-
assert!(!c.initialized);
|
|
115
|
-
assert!(c.archive_signature.is_none());
|
|
116
|
-
assert!(c.seen_result_ids.is_empty());
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/// seed a workspace with an `events.jsonl` (+ optional archived `events.jsonl.1`).
|
|
120
|
-
/// Returns the resolved `WorkspacePath`. The events file holds one renderable
|
|
121
|
-
/// `result_received` line so `collect_watch_lines` has observable non-marker output.
|
|
122
|
-
fn seed_watch_workspace(event_summary: &str, archive_bytes: Option<&[u8]>) -> WorkspacePath {
|
|
123
|
-
let dir = std::env::temp_dir().join(format!(
|
|
124
|
-
"team-agent-watch-rotate-{}-{}",
|
|
125
|
-
std::process::id(),
|
|
126
|
-
std::time::SystemTime::now()
|
|
127
|
-
.duration_since(std::time::UNIX_EPOCH)
|
|
128
|
-
.unwrap()
|
|
129
|
-
.as_nanos()
|
|
130
|
-
));
|
|
131
|
-
let logs = crate::model::paths::logs_dir(&dir);
|
|
132
|
-
std::fs::create_dir_all(&logs).unwrap();
|
|
133
|
-
let line =
|
|
134
|
-
serde_json::json!({"event": "result_received", "agent_id": "w1", "summary": event_summary});
|
|
135
|
-
std::fs::write(
|
|
136
|
-
logs.join("events.jsonl"),
|
|
137
|
-
format!("{}\n", serde_json::to_string(&line).unwrap()),
|
|
138
|
-
)
|
|
139
|
-
.unwrap();
|
|
140
|
-
if let Some(bytes) = archive_bytes {
|
|
141
|
-
std::fs::write(logs.join("events.jsonl.1"), bytes).unwrap();
|
|
142
|
-
}
|
|
143
|
-
// MessageStore::open creates the schema so collect_watch_lines' result tail works.
|
|
144
|
-
let _ = MessageStore::open(&dir).unwrap();
|
|
145
|
-
WorkspacePath::new(dir)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
#[test]
|
|
149
|
-
fn watch_rotation_emits_marker_once_resets_offset_and_does_not_replay() {
|
|
150
|
-
// watch/__init__.py:66-97 — archive_signature change OR offset>size => ROTATION_MARKER
|
|
151
|
-
// (once), event_offset reset to 0, archived segment NOT replayed (only the current
|
|
152
|
-
// events.jsonl is read from the new offset forward). The doc-comment invariant
|
|
153
|
-
// (coordinator.rs lines 42-43 / 532-533) is now ASSERTED, not commented.
|
|
154
|
-
let ws = seed_watch_workspace("first segment", None);
|
|
155
|
-
let store = MessageStore::open(ws.as_path()).unwrap();
|
|
156
|
-
let mut cursor = WatchCursor::default();
|
|
157
|
-
|
|
158
|
-
// First call: initializes the cursor (no marker even though no archive yet),
|
|
159
|
-
// renders the seeded result line, advances offset past EOF.
|
|
160
|
-
let first = collect_watch_lines(&ws, &mut cursor, &store, None).unwrap();
|
|
161
|
-
assert!(cursor.initialized, "first call initializes the cursor");
|
|
162
|
-
assert!(
|
|
163
|
-
!first.iter().any(|l| l == ROTATION_MARKER),
|
|
164
|
-
"no rotation marker on first/initializing call"
|
|
165
|
-
);
|
|
166
|
-
assert!(
|
|
167
|
-
first.iter().any(|l| l.contains("first segment")),
|
|
168
|
-
"first segment line is rendered exactly once"
|
|
169
|
-
);
|
|
170
|
-
let offset_after_first = cursor.event_offset;
|
|
171
|
-
assert!(
|
|
172
|
-
offset_after_first > 0,
|
|
173
|
-
"offset advanced past the consumed segment"
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
// Simulate rotation: an archive segment now appears (archive_signature changes from
|
|
177
|
-
// None -> Some), and the live events.jsonl is replaced with a fresh, SHORTER file
|
|
178
|
-
// whose new content must NOT include the archived "first segment".
|
|
179
|
-
let logs = crate::model::paths::logs_dir(ws.as_path());
|
|
180
|
-
std::fs::write(
|
|
181
|
-
logs.join("events.jsonl.1"),
|
|
182
|
-
b"archived old bytes that must never replay\n",
|
|
183
|
-
)
|
|
184
|
-
.unwrap();
|
|
185
|
-
let fresh = serde_json::json!({"event": "result_received", "agent_id": "w1", "summary": "post rotation"});
|
|
186
|
-
std::fs::write(
|
|
187
|
-
logs.join("events.jsonl"),
|
|
188
|
-
format!("{}\n", serde_json::to_string(&fresh).unwrap()),
|
|
189
|
-
)
|
|
190
|
-
.unwrap();
|
|
191
|
-
|
|
192
|
-
let second = collect_watch_lines(&ws, &mut cursor, &store, None).unwrap();
|
|
193
|
-
let marker_count = second.iter().filter(|l| **l == ROTATION_MARKER).count();
|
|
194
|
-
assert_eq!(
|
|
195
|
-
marker_count, 1,
|
|
196
|
-
"ROTATION_MARKER emitted exactly once on rotation"
|
|
197
|
-
);
|
|
198
|
-
assert!(
|
|
199
|
-
!second.iter().any(|l| l.contains("first segment")),
|
|
200
|
-
"archived segment is NOT replayed"
|
|
201
|
-
);
|
|
202
|
-
assert!(
|
|
203
|
-
!second
|
|
204
|
-
.iter()
|
|
205
|
-
.any(|l| l.contains("old bytes that must never replay")),
|
|
206
|
-
"archive file contents are NEVER read/replayed"
|
|
207
|
-
);
|
|
208
|
-
assert!(
|
|
209
|
-
second.iter().any(|l| l.contains("post rotation")),
|
|
210
|
-
"post-rotation live segment IS rendered (read from reset offset forward)"
|
|
211
|
-
);
|
|
212
|
-
// offset was reset to 0 by the rotation branch, then re-advanced over the fresh
|
|
213
|
-
// (shorter) file — so it must be < the pre-rotation offset, proving the reset.
|
|
214
|
-
assert!(
|
|
215
|
-
cursor.event_offset <= offset_after_first,
|
|
216
|
-
"event_offset reset on rotation (re-advanced over the shorter fresh file)"
|
|
217
|
-
);
|
|
218
|
-
assert_eq!(
|
|
219
|
-
cursor.archive_signature.map(|(sz, _)| sz),
|
|
220
|
-
Some(b"archived old bytes that must never replay\n".len() as u64),
|
|
221
|
-
"archive_signature updated to the new archived segment's (size, mtime_ns)"
|
|
222
|
-
);
|
|
223
|
-
}
|