@team-agent/installer 0.5.119 → 0.5.121
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/README.md +46 -1
- package/README.zh.md +46 -1
- package/crates/team-agent/src/cli/adapters.rs +11 -0
- package/crates/team-agent/src/cli/arguments.rs +401 -0
- package/crates/team-agent/src/cli/emit.rs +241 -142
- package/crates/team-agent/src/cli/intent/runtime.rs +1175 -0
- package/crates/team-agent/src/cli/intent/runtime_tests.rs +698 -0
- package/crates/team-agent/src/cli/intent/tests.rs +654 -0
- package/crates/team-agent/src/cli/intent.rs +642 -0
- package/crates/team-agent/src/cli/mod.rs +53 -6
- package/crates/team-agent/src/cli/named_address.rs +10 -1
- package/crates/team-agent/src/cli/spec.rs +22 -10
- package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
- package/crates/team-agent/src/cli/tests/base.rs +6 -1
- package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
- package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
- package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +65 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
- package/crates/team-agent/src/lifecycle/launch.rs +29 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +7 -1
- package/crates/team-agent/src/lifecycle/restart.rs +17 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +64 -19
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +34 -10
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +47 -4
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
- package/crates/team-agent/src/lifecycle/types.rs +10 -0
- package/crates/team-agent/src/mcp_server/mod.rs +2 -0
- package/crates/team-agent/src/mcp_server/readiness.rs +432 -0
- package/crates/team-agent/src/mcp_server/readiness_tests.rs +242 -0
- package/crates/team-agent/src/mcp_server/wire.rs +96 -11
- package/crates/team-agent/src/messaging/delivery.rs +62 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +41 -1
- package/crates/team-agent/src/provider/adapter.rs +7 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
- package/crates/team-agent/src/tmux_backend.rs +8 -1
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +71 -54
- package/skills/team-agent/command-coverage.json +283 -13
- package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
- package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
- package/skills/team-agent/references/recovery-runbook.md +61 -212
- package/skills/team-agent/references/team-agent-operator.md +181 -48
- package/skills/team-agent/references/team-in-team.md +47 -80
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
//! Public Phase-0 parser regressions. No model, real team or external pane is used.
|
|
2
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
3
|
+
use super::*;
|
|
4
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
5
|
+
|
|
6
|
+
fn strings(args: &[&str]) -> Vec<String> {
|
|
7
|
+
args.iter().map(|arg| (*arg).to_string()).collect()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
struct Sandbox(PathBuf);
|
|
11
|
+
impl Sandbox {
|
|
12
|
+
fn new() -> Self {
|
|
13
|
+
static NEXT: AtomicU64 = AtomicU64::new(0);
|
|
14
|
+
let dir = std::env::temp_dir().join(format!(
|
|
15
|
+
"ta-cli-parser334-{}-{}",
|
|
16
|
+
std::process::id(),
|
|
17
|
+
NEXT.fetch_add(1, Ordering::Relaxed)
|
|
18
|
+
));
|
|
19
|
+
std::fs::create_dir(&dir).unwrap();
|
|
20
|
+
Self(dir)
|
|
21
|
+
}
|
|
22
|
+
fn reject(&self, args: &[&str]) {
|
|
23
|
+
assert_eq!(run(&strings(args), &self.0), ExitCode::Usage, "{args:?}");
|
|
24
|
+
assert!(
|
|
25
|
+
std::fs::read_dir(&self.0).unwrap().next().is_none(),
|
|
26
|
+
"parser errors must not create state, logs, roles or a database: {args:?}"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
impl Drop for Sandbox {
|
|
31
|
+
fn drop(&mut self) {
|
|
32
|
+
let _ = std::fs::remove_dir_all(&self.0);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn p1_read_and_lifecycle_unknown_flags_are_usage_without_writes() {
|
|
38
|
+
for command in [
|
|
39
|
+
"status",
|
|
40
|
+
"shutdown",
|
|
41
|
+
"restart",
|
|
42
|
+
"approvals",
|
|
43
|
+
"doctor",
|
|
44
|
+
"models",
|
|
45
|
+
] {
|
|
46
|
+
let sandbox = Sandbox::new();
|
|
47
|
+
sandbox.reject(&[command, "--worksapce", "never-selected", "--json"]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn p1_missing_duplicate_extra_and_invalid_numeric_arguments_refuse() {
|
|
53
|
+
for args in [
|
|
54
|
+
vec!["status", "--workspace"],
|
|
55
|
+
vec!["status", "--workspace", "--team", "x"],
|
|
56
|
+
vec!["status", "--team=x", "--team", "y"],
|
|
57
|
+
vec!["status", "worker", "extra"],
|
|
58
|
+
vec!["shutdown", "extra"],
|
|
59
|
+
vec!["inbox", "leader", "--limit", "NaN"],
|
|
60
|
+
vec!["inbox", "leader", "-n", "3", "--limit=4"],
|
|
61
|
+
vec!["restart", "--session-converge-deadline=inf"],
|
|
62
|
+
vec!["status", "--detail", "--summary"],
|
|
63
|
+
] {
|
|
64
|
+
Sandbox::new().reject(&args);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[test]
|
|
69
|
+
fn p2_equal_and_split_values_have_identical_fields_without_losing_equals() {
|
|
70
|
+
let sandbox = Sandbox::new();
|
|
71
|
+
let path = sandbox.0.join("space = directory");
|
|
72
|
+
std::fs::create_dir(&path).unwrap();
|
|
73
|
+
let path = path.to_string_lossy();
|
|
74
|
+
let split = parse_args(&strings(&[
|
|
75
|
+
"--workspace",
|
|
76
|
+
&path,
|
|
77
|
+
"--name",
|
|
78
|
+
"x=y",
|
|
79
|
+
"--timeout",
|
|
80
|
+
"1.25",
|
|
81
|
+
]));
|
|
82
|
+
let inline = parse_args(&strings(&[
|
|
83
|
+
&format!("--workspace={path}"),
|
|
84
|
+
"--name=x=y",
|
|
85
|
+
"--timeout=1.25",
|
|
86
|
+
]));
|
|
87
|
+
assert_eq!(inline.workspace, split.workspace);
|
|
88
|
+
assert_eq!(inline.name, split.name);
|
|
89
|
+
assert_eq!(inline.timeout, split.timeout);
|
|
90
|
+
for path in [r"C:\owned dir\a=b", r"\\server\share\a=b"] {
|
|
91
|
+
assert_eq!(
|
|
92
|
+
parse_args(&strings(&[&format!("--workspace={path}")])).workspace,
|
|
93
|
+
Some(PathBuf::from(path))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
fn p2_missing_explicit_workspace_never_falls_back_to_cwd_or_writes_logs() {
|
|
100
|
+
for inline in [false, true] {
|
|
101
|
+
let sandbox = Sandbox::new();
|
|
102
|
+
let missing = sandbox.0.join("missing=a b").to_string_lossy().to_string();
|
|
103
|
+
let mut args = vec!["status".to_string()];
|
|
104
|
+
if inline {
|
|
105
|
+
args.push(format!("--workspace={missing}"));
|
|
106
|
+
} else {
|
|
107
|
+
args.extend(["--workspace".to_string(), missing]);
|
|
108
|
+
}
|
|
109
|
+
assert_eq!(run(&args, &sandbox.0), ExitCode::Usage);
|
|
110
|
+
assert!(std::fs::read_dir(&sandbox.0).unwrap().next().is_none());
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[test]
|
|
115
|
+
fn p3_bare_restart_is_never_interpreted_as_workspace_even_if_directory_exists() {
|
|
116
|
+
for exists in [false, true] {
|
|
117
|
+
let sandbox = Sandbox::new();
|
|
118
|
+
if exists {
|
|
119
|
+
std::fs::create_dir(sandbox.0.join("worker")).unwrap();
|
|
120
|
+
}
|
|
121
|
+
assert!(matches!(
|
|
122
|
+
restart_args(&strings(&["worker"]), &sandbox.0),
|
|
123
|
+
Err(CliError::Usage(_))
|
|
124
|
+
));
|
|
125
|
+
assert!(matches!(
|
|
126
|
+
restart_args(&strings(&["reviewer.v2"]), &sandbox.0),
|
|
127
|
+
Err(CliError::Usage(_))
|
|
128
|
+
));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[test]
|
|
133
|
+
fn p4_attach_requires_explicit_nonempty_provider_but_keeps_declared_identity() {
|
|
134
|
+
let sandbox = Sandbox::new();
|
|
135
|
+
for args in [vec![], vec!["--provider="], vec!["--provider", ""]] {
|
|
136
|
+
assert!(matches!(
|
|
137
|
+
attach_leader_args(&strings(&args), &sandbox.0),
|
|
138
|
+
Err(CliError::Usage(_))
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
for provider in ["pi", "claude", "codex"] {
|
|
142
|
+
let expected = parse_cli_provider(provider).unwrap();
|
|
143
|
+
assert_eq!(
|
|
144
|
+
attach_leader_args(&strings(&["--provider", provider]), &sandbox.0)
|
|
145
|
+
.unwrap()
|
|
146
|
+
.provider,
|
|
147
|
+
expected
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn p5_start_force_refuses_and_guides_to_existing_stop_start_commands() {
|
|
154
|
+
let sandbox = Sandbox::new();
|
|
155
|
+
match start_agent_args(&strings(&["worker", "--force"]), &sandbox.0) {
|
|
156
|
+
Err(CliError::Usage(message)) => {
|
|
157
|
+
assert!(
|
|
158
|
+
message.contains("stop-agent") && message.contains("start-agent"),
|
|
159
|
+
"{message}"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
_ => panic!("--force must not be accepted and then silently discarded"),
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#[test]
|
|
167
|
+
fn payload_delimiter_preserves_help_no_display_and_equal_bytes() {
|
|
168
|
+
let parsed = parse_args(&strings(&[
|
|
169
|
+
"worker",
|
|
170
|
+
"--",
|
|
171
|
+
"--help",
|
|
172
|
+
"--no-display",
|
|
173
|
+
"--workspace=a=b",
|
|
174
|
+
]));
|
|
175
|
+
assert_eq!(
|
|
176
|
+
parsed.positionals,
|
|
177
|
+
strings(&["worker", "--help", "--no-display", "--workspace=a=b"])
|
|
178
|
+
);
|
|
179
|
+
assert!(parsed.workspace.is_none());
|
|
180
|
+
let literal = strings(&["--", "--help", "--no-display", "--workspace=a=b"]);
|
|
181
|
+
assert_eq!(
|
|
182
|
+
leader_launcher_args(&literal).unwrap().provider_args,
|
|
183
|
+
literal[1..]
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[test]
|
|
188
|
+
fn consumed_prompt_help_does_not_trigger_global_help_or_scope_fallback() {
|
|
189
|
+
let sandbox = Sandbox::new();
|
|
190
|
+
sandbox.reject(&[
|
|
191
|
+
"add-agent",
|
|
192
|
+
"worker",
|
|
193
|
+
"--prompt",
|
|
194
|
+
"--help",
|
|
195
|
+
"--provider",
|
|
196
|
+
"pi",
|
|
197
|
+
"--bypass",
|
|
198
|
+
"false",
|
|
199
|
+
"--workspace",
|
|
200
|
+
"missing",
|
|
201
|
+
]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn real_control_help_remains_an_effect_free_short_circuit() {
|
|
206
|
+
let sandbox = Sandbox::new();
|
|
207
|
+
assert_eq!(
|
|
208
|
+
run(&strings(&["status", "--unknown", "--help"]), &sandbox.0),
|
|
209
|
+
ExitCode::Ok
|
|
210
|
+
);
|
|
211
|
+
assert!(std::fs::read_dir(&sandbox.0).unwrap().next().is_none());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#[test]
|
|
215
|
+
fn every_dispatch_command_has_a_closed_control_schema() {
|
|
216
|
+
for spec in COMMAND_SPECS {
|
|
217
|
+
if !matches!(spec.kind, CommandKind::Dispatch(_))
|
|
218
|
+
|| matches!(spec.name, "route" | "leader-prompt")
|
|
219
|
+
{
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Family grammars select the concrete operation before its closed
|
|
223
|
+
// schema; the family itself has no permissive catch-all flag parser.
|
|
224
|
+
let schema = if matches!(spec.name, "team" | "agent") {
|
|
225
|
+
"intent-start"
|
|
226
|
+
} else {
|
|
227
|
+
spec.name
|
|
228
|
+
};
|
|
229
|
+
let checked = super::super::arguments::parse(schema, &strings(&["--help"])).unwrap();
|
|
230
|
+
assert!(checked.help, "{} must have a schema", spec.name);
|
|
231
|
+
let compatibility =
|
|
232
|
+
super::super::arguments::parse(schema, &strings(&["--no-display"])).unwrap();
|
|
233
|
+
assert!(
|
|
234
|
+
compatibility.tokens.is_empty(),
|
|
235
|
+
"explicit legacy no-display control: {}",
|
|
236
|
+
spec.name
|
|
237
|
+
);
|
|
238
|
+
assert!(super::super::arguments::parse(schema, &strings(&["--unknown"])).is_err());
|
|
239
|
+
}
|
|
240
|
+
for command in ["wait", "coordinator", "attach-app-server-leader"] {
|
|
241
|
+
assert!(
|
|
242
|
+
super::super::arguments::parse(command, &strings(&["--workspace", "--once"])).is_err()
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn canonical_scope_equal_and_split_share_one_directory_and_reject_conflicts() {
|
|
249
|
+
let sandbox = Sandbox::new();
|
|
250
|
+
let selected = sandbox.0.join("scope=a b");
|
|
251
|
+
std::fs::create_dir(&selected).unwrap();
|
|
252
|
+
let canonical = std::fs::canonicalize(&selected).unwrap();
|
|
253
|
+
for args in [
|
|
254
|
+
strings(&["--workspace", "scope=a b"]),
|
|
255
|
+
strings(&["--workspace=scope=a b"]),
|
|
256
|
+
] {
|
|
257
|
+
let checked = super::super::arguments::parse("status", &args).unwrap();
|
|
258
|
+
let args = resolve_command_scope("status", checked.tokens, &sandbox.0).unwrap();
|
|
259
|
+
assert_eq!(parse_args(&args).workspace, Some(canonical.clone()));
|
|
260
|
+
}
|
|
261
|
+
let args =
|
|
262
|
+
super::super::arguments::parse("restart", &strings(&["./scope=a b", "--workspace=."]))
|
|
263
|
+
.unwrap();
|
|
264
|
+
assert!(matches!(
|
|
265
|
+
resolve_command_scope("restart", args.tokens, &sandbox.0),
|
|
266
|
+
Err(CliError::Usage(_))
|
|
267
|
+
));
|
|
268
|
+
let args = super::super::arguments::parse(
|
|
269
|
+
"restart",
|
|
270
|
+
&strings(&["./scope=a b", "--workspace=scope=a b"]),
|
|
271
|
+
)
|
|
272
|
+
.unwrap();
|
|
273
|
+
let args = resolve_command_scope("restart", args.tokens, &sandbox.0).unwrap();
|
|
274
|
+
assert_eq!(
|
|
275
|
+
restart_args(&args, &sandbox.0).unwrap().workspace,
|
|
276
|
+
canonical
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[test]
|
|
281
|
+
fn payload_help_is_not_control_and_compatibility_flags_do_not_delete_literals() {
|
|
282
|
+
let checked = super::super::arguments::parse(
|
|
283
|
+
"add-agent",
|
|
284
|
+
&strings(&["w", "--provider=pi", "--bypass=false", "--prompt", "--help"]),
|
|
285
|
+
)
|
|
286
|
+
.unwrap();
|
|
287
|
+
assert!(!checked.help);
|
|
288
|
+
let (_, patch) = role_agent_args(&checked.tokens, true).unwrap();
|
|
289
|
+
assert_eq!(patch.prompt.as_deref(), Some("--help"));
|
|
290
|
+
let checked = super::super::arguments::parse(
|
|
291
|
+
"send",
|
|
292
|
+
&strings(&["w", "--", "--help", "--no-display", "--workspace=a=b"]),
|
|
293
|
+
)
|
|
294
|
+
.unwrap();
|
|
295
|
+
assert!(!checked.help);
|
|
296
|
+
assert_eq!(
|
|
297
|
+
parse_args(&checked.tokens).positionals,
|
|
298
|
+
strings(&["w", "--help", "--no-display", "--workspace=a=b"])
|
|
299
|
+
);
|
|
300
|
+
let checked = super::super::arguments::parse(
|
|
301
|
+
"quick-start",
|
|
302
|
+
&strings(&["--no-display", "--", "--no-display"]),
|
|
303
|
+
)
|
|
304
|
+
.unwrap();
|
|
305
|
+
assert_eq!(checked.tokens, strings(&["--", "--no-display"]));
|
|
306
|
+
assert!(super::super::arguments::parse("status", &strings(&["--yes"])).is_err());
|
|
307
|
+
assert!(!super::super::arguments::has_flag(
|
|
308
|
+
"add-agent",
|
|
309
|
+
&strings(&["--prompt", "--json"]),
|
|
310
|
+
"--json"
|
|
311
|
+
));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[test]
|
|
315
|
+
fn restart_node_directory_phase1_migration_refuses_without_mutating_state() {
|
|
316
|
+
for agent in [false, true] {
|
|
317
|
+
for directory in [false, true] {
|
|
318
|
+
let sandbox = Sandbox::new();
|
|
319
|
+
let runtime = sandbox.0.join(".team/runtime");
|
|
320
|
+
std::fs::create_dir_all(&runtime).unwrap();
|
|
321
|
+
let state = if agent {
|
|
322
|
+
r#"{"agents":{"worker":{"status":"stopped"}}}"#
|
|
323
|
+
} else {
|
|
324
|
+
r#"{"agents":{}}"#
|
|
325
|
+
};
|
|
326
|
+
std::fs::write(runtime.join("state.json"), state).unwrap();
|
|
327
|
+
if directory {
|
|
328
|
+
std::fs::create_dir(sandbox.0.join("worker")).unwrap();
|
|
329
|
+
}
|
|
330
|
+
assert_eq!(
|
|
331
|
+
run(&strings(&["restart", "worker"]), &sandbox.0),
|
|
332
|
+
// P3 Phase1: a plain ID is an agent intent; same-name directory
|
|
333
|
+
// remains a syntax ambiguity. Incomplete runtime still refuses.
|
|
334
|
+
if directory {
|
|
335
|
+
ExitCode::Usage
|
|
336
|
+
} else {
|
|
337
|
+
ExitCode::Error
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
assert_eq!(
|
|
341
|
+
std::fs::read_to_string(runtime.join("state.json")).unwrap(),
|
|
342
|
+
state
|
|
343
|
+
);
|
|
344
|
+
// Canonical planning refusals are effect-free envelopes; legacy
|
|
345
|
+
// restart diagnostics remain on their frozen entry point.
|
|
346
|
+
assert!(!sandbox.0.join(".team/logs").exists());
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
#[test]
|
|
352
|
+
fn text_queries_and_native_rendering_switches_keep_literal_boundaries() {
|
|
353
|
+
for backend in ["tmux", "TMUX", " conpty "] {
|
|
354
|
+
let input = format!("--backend={backend}");
|
|
355
|
+
let checked = super::super::arguments::parse("quick-start", &[input]).unwrap();
|
|
356
|
+
let parsed = quick_start_args(&checked.tokens, Path::new(".")).unwrap();
|
|
357
|
+
assert_eq!(parsed.backend.as_deref(), Some(backend));
|
|
358
|
+
}
|
|
359
|
+
for query in ["", " ", "--help", "a=b"] {
|
|
360
|
+
let checked =
|
|
361
|
+
super::super::arguments::parse("models", &strings(&[&format!("--search={query}")]))
|
|
362
|
+
.unwrap();
|
|
363
|
+
assert!(!checked.help);
|
|
364
|
+
let parsed = models_args(&checked.tokens).unwrap();
|
|
365
|
+
assert_eq!(
|
|
366
|
+
parsed.search.as_deref(),
|
|
367
|
+
(!query.trim().is_empty()).then_some(query)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
let checked =
|
|
371
|
+
super::super::arguments::parse("models", &strings(&["--search", "--help"])).unwrap();
|
|
372
|
+
assert!(!checked.help);
|
|
373
|
+
assert!(super::super::arguments::has_flag(
|
|
374
|
+
"pi",
|
|
375
|
+
&strings(&["--json", "--", "--help"]),
|
|
376
|
+
"--json"
|
|
377
|
+
));
|
|
378
|
+
assert!(!super::super::arguments::has_flag(
|
|
379
|
+
"pi",
|
|
380
|
+
&strings(&["--", "--json"]),
|
|
381
|
+
"--json"
|
|
382
|
+
));
|
|
383
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
4
|
+
mod hermetic_guard;
|
|
5
|
+
|
|
3
6
|
struct TeamSocketGuard {
|
|
4
7
|
ws: std::path::PathBuf,
|
|
5
8
|
}
|
|
@@ -212,10 +215,16 @@ fn run_dispatches_quick_start_compiles_spec() {
|
|
|
212
215
|
// coordinator/health.rs, message_store.
|
|
213
216
|
// =========================================================================
|
|
214
217
|
#[test]
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
let
|
|
218
|
-
|
|
218
|
+
#[serial_test::serial(env)]
|
|
219
|
+
fn cli_quick_start_invokes_real_lifecycle_admission_before_runtime_effects() {
|
|
220
|
+
let env = hermetic_guard::HermeticTestEnv::enter("issue337-cli-admission");
|
|
221
|
+
env.scrub_tmux();
|
|
222
|
+
env.assert_no_real_tmux();
|
|
223
|
+
let workspace = env.workspace("ws");
|
|
224
|
+
let team = workspace.join("roles");
|
|
225
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
226
|
+
std::fs::write(team.join("TEAM.md"), DELEG_TEAM_MD).unwrap();
|
|
227
|
+
std::fs::write(team.join("agents/implementer.md"), DELEG_VALID_ROLE).unwrap();
|
|
219
228
|
let args = QuickStartArgs {
|
|
220
229
|
workspace: crate::model::paths::team_workspace(&team).unwrap(),
|
|
221
230
|
agents_dir: team.clone(),
|
|
@@ -226,16 +235,17 @@ fn cli_quick_start_invokes_real_lifecycle_compiles_spec() {
|
|
|
226
235
|
json: true,
|
|
227
236
|
detail: false,
|
|
228
237
|
};
|
|
229
|
-
let
|
|
230
|
-
|
|
231
|
-
let
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
let result = cmd_quick_start(&args).expect("real lifecycle admission");
|
|
239
|
+
assert_eq!(result.exit, ExitCode::Error);
|
|
240
|
+
let CmdOutput::Json(value) = result.output else { panic!() };
|
|
241
|
+
assert_eq!(value["status"], "preflight_blocked");
|
|
242
|
+
assert!(value["blockers"].to_string().contains("caller_pane_missing"));
|
|
243
|
+
assert_eq!(value["effects"]["worker_panes_started"], json!([]));
|
|
244
|
+
assert_eq!(value["effects"]["coordinator_start"], "not_attempted");
|
|
245
|
+
let runtime_spec = crate::model::paths::runtime_spec_path(&args.workspace, "clidelegteam");
|
|
246
|
+
assert!(!runtime_spec.exists(), "#337 admission precedes runtime spec publication");
|
|
247
|
+
assert!(!crate::state::persist::runtime_state_path(&args.workspace).exists());
|
|
248
|
+
assert!(!args.workspace.join(".team/team.db").exists());
|
|
239
249
|
assert!(
|
|
240
250
|
!team.join("team.spec.yaml").exists(),
|
|
241
251
|
"E5: quick_start must NOT write spec into the user team dir"
|
|
@@ -333,12 +343,10 @@ fn cli_add_agent_duplicate_id_surfaces_real_error() {
|
|
|
333
343
|
);
|
|
334
344
|
}
|
|
335
345
|
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
// an explicit unknown projection. The fixture still seeds a real store to ensure the status path
|
|
339
|
-
// does not accidentally depend on coordinator or mailbox reads.
|
|
346
|
+
// Nine base fields remain; #338 adds only observation reason and scoped action
|
|
347
|
+
// when uncertain. No coordinator/db/history/payload data enters the node.
|
|
340
348
|
#[test]
|
|
341
|
-
fn
|
|
349
|
+
fn cli_status_emits_brief_with_scoped_unknown_diagnostics() {
|
|
342
350
|
let ws = seed_status_workspace(); // writes .team/runtime/state.json
|
|
343
351
|
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
344
352
|
let _ = store
|
|
@@ -406,10 +414,46 @@ fn cli_status_emits_nine_field_brief_without_diagnostics() {
|
|
|
406
414
|
"health",
|
|
407
415
|
"session_name",
|
|
408
416
|
"tmux_command",
|
|
417
|
+
"observation_reason",
|
|
418
|
+
"next_action",
|
|
409
419
|
]
|
|
410
420
|
.into_iter()
|
|
411
421
|
.collect()
|
|
412
422
|
);
|
|
423
|
+
assert_eq!(nodes[0]["runtime_status"], "unknown");
|
|
424
|
+
assert!(!nodes[0]["observation_reason"].as_str().unwrap().is_empty());
|
|
425
|
+
assert!(nodes[0]["next_action"].as_str().unwrap().contains(&*args.workspace.to_string_lossy()));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#[test]
|
|
429
|
+
#[serial_test::serial(env)]
|
|
430
|
+
fn issue338_doctor_reports_same_scope_unknown_observations_without_state_writes() {
|
|
431
|
+
let env = hermetic_guard::HermeticTestEnv::enter("issue338-doctor");
|
|
432
|
+
env.scrub_tmux();
|
|
433
|
+
env.assert_no_real_tmux();
|
|
434
|
+
let workspace = env.workspace("ws");
|
|
435
|
+
crate::state::persist::save_runtime_state(&workspace, &json!({
|
|
436
|
+
"team_key": "current", "session_name": "test-workers",
|
|
437
|
+
"agents": {"worker": {"provider": "pi", "status": "running"}},
|
|
438
|
+
})).unwrap();
|
|
439
|
+
// Complete the existing legacy-to-current state normalization as fixture
|
|
440
|
+
// preparation; this test covers observation of an already-current runtime.
|
|
441
|
+
crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
442
|
+
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
443
|
+
let before = std::fs::read(&state_path).unwrap();
|
|
444
|
+
let result = cmd_doctor(&DoctorArgs {
|
|
445
|
+
spec: None, workspace: workspace.clone(), gate: None, comms: false,
|
|
446
|
+
team: Some("current".into()), fix: false, fix_schema: false,
|
|
447
|
+
cleanup_orphans: false, confirm: false, json: true,
|
|
448
|
+
}).unwrap();
|
|
449
|
+
let CmdOutput::Json(value) = result.output else { panic!() };
|
|
450
|
+
let node = &value["runtime"]["observations"][0];
|
|
451
|
+
assert_eq!(node["name"], "worker");
|
|
452
|
+
assert_eq!(node["observation_reason"], "endpoint_unregistered");
|
|
453
|
+
let action = node["next_action"].as_str().unwrap();
|
|
454
|
+
assert!(action.contains("--team 'current'"));
|
|
455
|
+
assert!(action.contains(&*workspace.to_string_lossy()));
|
|
456
|
+
assert_eq!(std::fs::read(state_path).unwrap(), before);
|
|
413
457
|
}
|
|
414
458
|
|
|
415
459
|
// 6 [P1] — cmd_shutdown delegates to the REAL crate::coordinator::stop_coordinator (+ tmux
|
|
@@ -1254,6 +1254,62 @@ fn bounded_coordinator_stop_returns_grace_window_late_success() {
|
|
|
1254
1254
|
assert_eq!(report.status, crate::coordinator::StopOutcome::Stopped);
|
|
1255
1255
|
}
|
|
1256
1256
|
|
|
1257
|
+
#[test]
|
|
1258
|
+
fn bounded_coordinator_stop_retains_completion_beyond_old_1150ms_cap() {
|
|
1259
|
+
let ws = tmp_shutdown_workspace("coordinator-stop-after-old-cap");
|
|
1260
|
+
let cleanup = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
1261
|
+
let worker_cleanup = cleanup.clone();
|
|
1262
|
+
let report = crate::cli::lifecycle_port::stop_coordinator_bounded_with(
|
|
1263
|
+
crate::coordinator::WorkspacePath::new(ws),
|
|
1264
|
+
std::time::Duration::from_secs(20),
|
|
1265
|
+
move |_workspace| {
|
|
1266
|
+
std::thread::sleep(std::time::Duration::from_millis(1300));
|
|
1267
|
+
worker_cleanup.store(true, std::sync::atomic::Ordering::SeqCst);
|
|
1268
|
+
Ok(crate::coordinator::StopReport {
|
|
1269
|
+
ok: true,
|
|
1270
|
+
status: crate::coordinator::StopOutcome::Stopped,
|
|
1271
|
+
pid: Some(crate::coordinator::Pid::new(12345)),
|
|
1272
|
+
})
|
|
1273
|
+
},
|
|
1274
|
+
)
|
|
1275
|
+
.expect("completion inside the shutdown budget must not be abandoned")
|
|
1276
|
+
.expect("owned stop completed successfully");
|
|
1277
|
+
|
|
1278
|
+
assert!(cleanup.load(std::sync::atomic::Ordering::SeqCst));
|
|
1279
|
+
assert!(report.ok);
|
|
1280
|
+
assert_eq!(report.status, crate::coordinator::StopOutcome::Stopped);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
#[test]
|
|
1284
|
+
fn bounded_coordinator_stop_does_not_claim_completion_after_budget_exhaustion() {
|
|
1285
|
+
let ws = tmp_shutdown_workspace("coordinator-stop-real-timeout");
|
|
1286
|
+
let (release_tx, release_rx) = std::sync::mpsc::channel();
|
|
1287
|
+
let (completed_tx, completed_rx) = std::sync::mpsc::channel();
|
|
1288
|
+
let report = crate::cli::lifecycle_port::stop_coordinator_bounded_with(
|
|
1289
|
+
crate::coordinator::WorkspacePath::new(ws),
|
|
1290
|
+
std::time::Duration::from_millis(1),
|
|
1291
|
+
move |_workspace| {
|
|
1292
|
+
release_rx.recv().unwrap();
|
|
1293
|
+
completed_tx.send(()).unwrap();
|
|
1294
|
+
Ok(crate::coordinator::StopReport {
|
|
1295
|
+
ok: true,
|
|
1296
|
+
status: crate::coordinator::StopOutcome::Stopped,
|
|
1297
|
+
pid: Some(crate::coordinator::Pid::new(12345)),
|
|
1298
|
+
})
|
|
1299
|
+
},
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
assert!(report.is_none(), "an unfinished stop must remain unknown");
|
|
1303
|
+
assert!(matches!(
|
|
1304
|
+
completed_rx.try_recv(),
|
|
1305
|
+
Err(std::sync::mpsc::TryRecvError::Empty)
|
|
1306
|
+
));
|
|
1307
|
+
release_tx.send(()).unwrap();
|
|
1308
|
+
completed_rx
|
|
1309
|
+
.recv_timeout(std::time::Duration::from_secs(5))
|
|
1310
|
+
.expect("release the controlled worker without leaving a blocked thread");
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1257
1313
|
#[test]
|
|
1258
1314
|
fn shutdown_outcome_late_or_postcheck_gone_is_ok_with_lsof_diagnostic() {
|
|
1259
1315
|
let out = crate::cli::lifecycle_port::classify_shutdown_outcome(
|
|
@@ -84,9 +84,10 @@ fn install_skill_invalid_target_is_usage_error() {
|
|
|
84
84
|
);
|
|
85
85
|
assert_eq!(
|
|
86
86
|
code,
|
|
87
|
-
ExitCode::
|
|
88
|
-
"
|
|
87
|
+
ExitCode::Usage,
|
|
88
|
+
"invalid control values must fail as Usage2 before installation"
|
|
89
89
|
);
|
|
90
|
+
assert!(!ws.join(".team/logs").exists());
|
|
90
91
|
let _ = std::fs::remove_dir_all(&ws);
|
|
91
92
|
let _ = std::fs::remove_dir_all(source.parent().unwrap());
|
|
92
93
|
}
|
|
@@ -194,7 +194,9 @@ fn profile_init_accepts_local_proxy_mode_and_rejects_unknown_values() {
|
|
|
194
194
|
]),
|
|
195
195
|
&ws,
|
|
196
196
|
);
|
|
197
|
-
assert_eq!(invalid, ExitCode::
|
|
197
|
+
assert_eq!(invalid, ExitCode::Usage, "unknown proxy mode must fail as Usage2");
|
|
198
|
+
assert!(!profiles_dir(&ws).join("bad_proxy.env").exists());
|
|
199
|
+
assert!(!ws.join(".team/logs").exists());
|
|
198
200
|
let _ = std::fs::remove_dir_all(&ws);
|
|
199
201
|
}
|
|
200
202
|
|
|
@@ -689,7 +689,7 @@ fn write_role(binding: &Binding, identity: &InstanceIdentity, role: &RoleConfig)
|
|
|
689
689
|
file.sync_all()?;
|
|
690
690
|
Ok(())
|
|
691
691
|
}
|
|
692
|
-
pub(
|
|
692
|
+
pub(crate) fn read_role(binding: &Binding, identity: &InstanceIdentity) -> Result<RoleConfig, BackendError> {
|
|
693
693
|
let mut path = role_path(binding, identity);
|
|
694
694
|
// Existing generation-one deployments retain their immutable source.
|
|
695
695
|
if identity.generation == Generation(1) {
|
|
@@ -585,6 +585,30 @@ impl MessageStore {
|
|
|
585
585
|
Ok(rows == 1)
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
/// Release only an owned claim that has performed no input. Readiness
|
|
589
|
+
/// waiting must remain retryable without consuming the input-attempt budget.
|
|
590
|
+
/// A changed attempt, status, or error means this caller no longer owns it.
|
|
591
|
+
pub(crate) fn release_delivery_before_input(
|
|
592
|
+
&self,
|
|
593
|
+
message_id: &str,
|
|
594
|
+
attempt: u32,
|
|
595
|
+
reason: &str,
|
|
596
|
+
) -> Result<bool, MessageStoreError> {
|
|
597
|
+
if attempt == 0 || reason.is_empty() {
|
|
598
|
+
return Ok(false);
|
|
599
|
+
}
|
|
600
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
601
|
+
let rows = conn.execute(
|
|
602
|
+
"update messages
|
|
603
|
+
set delivery_attempts = delivery_attempts - 1,
|
|
604
|
+
error = ?3, updated_at = ?4
|
|
605
|
+
where message_id = ?1 and status = 'target_resolved'
|
|
606
|
+
and error is null and delivery_attempts = ?2",
|
|
607
|
+
params![message_id, attempt, reason, now_ts()],
|
|
608
|
+
)?;
|
|
609
|
+
Ok(rows == 1)
|
|
610
|
+
}
|
|
611
|
+
|
|
588
612
|
/// Read inbox rows through [`Self::inbox`]; recovery transitions below are
|
|
589
613
|
/// separate repository owners and are not claim eligibility.
|
|
590
614
|
pub fn requeue_blocked_leader_messages(
|
|
@@ -1168,6 +1192,47 @@ mod tests {
|
|
|
1168
1192
|
assert_eq!(col_i64(&read(&s), &mid, "delivery_attempts"), 1);
|
|
1169
1193
|
}
|
|
1170
1194
|
|
|
1195
|
+
#[test]
|
|
1196
|
+
fn release_delivery_before_input_preserves_budget_and_single_winner() {
|
|
1197
|
+
let s = store();
|
|
1198
|
+
let mid = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
1199
|
+
assert!(s.claim_for_delivery(&mid).unwrap());
|
|
1200
|
+
assert!(!s.claim_for_delivery(&mid).unwrap());
|
|
1201
|
+
assert!(s.release_delivery_before_input(&mid, 1, "mcp_handshake_missing").unwrap());
|
|
1202
|
+
assert_eq!(col_i64(&read(&s), &mid, "delivery_attempts"), 0);
|
|
1203
|
+
assert_eq!(col_str(&read(&s), &mid, "error").as_deref(), Some("mcp_handshake_missing"));
|
|
1204
|
+
assert!(!s.release_delivery_before_input(&mid, 1, "late").unwrap());
|
|
1205
|
+
assert!(s.claim_for_delivery(&mid).unwrap());
|
|
1206
|
+
assert!(!s.claim_for_delivery(&mid).unwrap());
|
|
1207
|
+
assert_eq!(col_i64(&read(&s), &mid, "delivery_attempts"), 1);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
#[test]
|
|
1211
|
+
fn release_delivery_before_input_cannot_overwrite_changed_claims() {
|
|
1212
|
+
let s = store();
|
|
1213
|
+
let mid = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
1214
|
+
assert!(s.claim_for_delivery(&mid).unwrap());
|
|
1215
|
+
assert!(!s.release_delivery_before_input(&mid, 0, "missing").unwrap());
|
|
1216
|
+
assert!(!s.release_delivery_before_input(&mid, 2, "stale").unwrap());
|
|
1217
|
+
s.mark(&mid, "target_resolved", Some("other_owner")).unwrap();
|
|
1218
|
+
assert!(!s.release_delivery_before_input(&mid, 1, "stale").unwrap());
|
|
1219
|
+
assert_eq!(col_str(&read(&s), &mid, "error").as_deref(), Some("other_owner"));
|
|
1220
|
+
assert!(s.claim_for_delivery(&mid).unwrap());
|
|
1221
|
+
assert!(!s.release_delivery_before_input(&mid, 1, "old_claim").unwrap());
|
|
1222
|
+
assert_eq!(col_i64(&read(&s), &mid, "delivery_attempts"), 2);
|
|
1223
|
+
assert_eq!(col_str(&read(&s), &mid, "error"), None);
|
|
1224
|
+
for status in ["cancelled", "delivered", "submitted_unverified"] {
|
|
1225
|
+
// Each terminal/effect state needs its own row: mark correctly
|
|
1226
|
+
// refuses to downgrade a previously delivered row.
|
|
1227
|
+
let changed = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
1228
|
+
assert!(s.claim_for_delivery(&changed).unwrap());
|
|
1229
|
+
s.mark(&changed, status, None).unwrap();
|
|
1230
|
+
assert!(!s.release_delivery_before_input(&changed, 1, "stale").unwrap());
|
|
1231
|
+
assert_eq!(status_of(&read(&s), &changed), status);
|
|
1232
|
+
assert_eq!(col_i64(&read(&s), &changed, "delivery_attempts"), 1);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1171
1236
|
#[test]
|
|
1172
1237
|
fn claim_for_delivery_never_claims_stored_only_presentation() {
|
|
1173
1238
|
let s = store();
|