@team-agent/installer 0.2.11 → 0.3.1
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 +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1204 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1207 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +557 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1084 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +526 -0
- package/crates/team-agent/src/leader/rediscover.rs +1101 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +237 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +272 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +489 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +2109 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +985 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +710 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +187 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +468 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +743 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +329 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +553 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +578 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +659 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +765 -0
- package/crates/team-agent/src/tmux_backend.rs +810 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +118 -112
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -119
- package/src/team_agent/coordinator/lifecycle.py +0 -411
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -218
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -114
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -262
- package/src/team_agent/messaging/delivery.py +0 -504
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -503
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -91
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1243
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -144
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -693
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- package/src/team_agent/watch/__init__.py +0 -145
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
fn compile_team_dir(tag: &str) -> std::path::PathBuf {
|
|
4
|
+
let team = tmp_workspace().join(tag);
|
|
5
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
6
|
+
std::fs::write(
|
|
7
|
+
team.join("TEAM.md"),
|
|
8
|
+
"---\nname: compileteam\nobjective: Compile probe.\nprovider: fake\n---\n\nCompile team.\n",
|
|
9
|
+
)
|
|
10
|
+
.unwrap();
|
|
11
|
+
std::fs::write(
|
|
12
|
+
team.join("agents").join("worker.md"),
|
|
13
|
+
"---\nname: worker\nrole: Worker\nprovider: fake\nmodel: fake\ntools:\n - mcp_team\n---\n\nWorker role.\n",
|
|
14
|
+
)
|
|
15
|
+
.unwrap();
|
|
16
|
+
team
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn cmd_compile_json_and_human_match_golden_shape_and_writes_out() {
|
|
21
|
+
let team = compile_team_dir("compile-ok");
|
|
22
|
+
let out = team.parent().unwrap().join("out.yaml");
|
|
23
|
+
let args = CompileArgs { team: team.clone(), out: out.clone(), json: true };
|
|
24
|
+
|
|
25
|
+
let result = cmd_compile(&args).expect("compile");
|
|
26
|
+
assert_eq!(result.exit, ExitCode::Ok);
|
|
27
|
+
assert!(out.exists(), "compile must write the compiled spec to --out");
|
|
28
|
+
assert!(
|
|
29
|
+
std::fs::read_to_string(&out).unwrap().contains("version: 1"),
|
|
30
|
+
"compiled out file must contain the spec YAML"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
let json_text = emit(&result.output, true).unwrap();
|
|
34
|
+
let expected_json = format!(
|
|
35
|
+
"{{\n \"agents\": [\n \"worker\"\n ],\n \"ok\": true,\n \"out\": \"{}\",\n \"team_dir\": \"{}\"\n}}",
|
|
36
|
+
out.to_string_lossy(),
|
|
37
|
+
team.to_string_lossy()
|
|
38
|
+
);
|
|
39
|
+
assert_eq!(json_text, expected_json, "golden --json is sorted pretty JSON");
|
|
40
|
+
|
|
41
|
+
let human_text = emit(&result.output, false).unwrap();
|
|
42
|
+
let expected_human = format!(
|
|
43
|
+
"ok: True\nteam_dir: {}\nout: {}\nagents: [\"worker\"]",
|
|
44
|
+
team.to_string_lossy(),
|
|
45
|
+
out.to_string_lossy()
|
|
46
|
+
);
|
|
47
|
+
assert_eq!(human_text, expected_human, "golden human output preserves cmd_compile insertion order");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[test]
|
|
51
|
+
fn run_dispatches_compile_and_error_path_exits_error() {
|
|
52
|
+
let team = compile_team_dir("compile-dispatch");
|
|
53
|
+
let out = team.parent().unwrap().join("dispatch.yaml");
|
|
54
|
+
let argv = vec![
|
|
55
|
+
"compile".to_string(),
|
|
56
|
+
"--team".to_string(),
|
|
57
|
+
team.to_string_lossy().to_string(),
|
|
58
|
+
"--out".to_string(),
|
|
59
|
+
out.to_string_lossy().to_string(),
|
|
60
|
+
"--json".to_string(),
|
|
61
|
+
];
|
|
62
|
+
assert_eq!(run(&argv, team.parent().unwrap()), ExitCode::Ok);
|
|
63
|
+
assert!(out.exists(), "dispatch compile must route to cmd_compile and write --out");
|
|
64
|
+
|
|
65
|
+
let bad = tmp_workspace().join("compile-bad");
|
|
66
|
+
std::fs::create_dir_all(bad.join("agents")).unwrap();
|
|
67
|
+
std::fs::write(
|
|
68
|
+
bad.join("TEAM.md"),
|
|
69
|
+
"---\nname: badteam\nobjective: Bad.\nprovider: fake\n---\n",
|
|
70
|
+
)
|
|
71
|
+
.unwrap();
|
|
72
|
+
std::fs::write(
|
|
73
|
+
bad.join("agents").join("broken.md"),
|
|
74
|
+
"---\nname: broken\nrole: Broken\nmodel: fake\n---\n",
|
|
75
|
+
)
|
|
76
|
+
.unwrap();
|
|
77
|
+
let bad_out = bad.parent().unwrap().join("bad.yaml");
|
|
78
|
+
let bad_args = CompileArgs { team: bad.clone(), out: bad_out.clone(), json: true };
|
|
79
|
+
let err = cmd_compile(&bad_args).unwrap_err().to_string();
|
|
80
|
+
assert!(err.contains("missing front matter field provider"), "got {err}");
|
|
81
|
+
assert_eq!(
|
|
82
|
+
run(
|
|
83
|
+
&[
|
|
84
|
+
"compile".to_string(),
|
|
85
|
+
"--team".to_string(),
|
|
86
|
+
bad.to_string_lossy().to_string(),
|
|
87
|
+
"--out".to_string(),
|
|
88
|
+
bad_out.to_string_lossy().to_string(),
|
|
89
|
+
"--json".to_string(),
|
|
90
|
+
],
|
|
91
|
+
bad.parent().unwrap(),
|
|
92
|
+
),
|
|
93
|
+
ExitCode::Error,
|
|
94
|
+
"invalid compile input must exit 1, not fall through as an unknown subcommand"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
// =========================================================================
|
|
4
|
+
// STEP-14 DIVERGENCE RED LANE — golden-pinned tests that FAIL against the
|
|
5
|
+
// current Rust port. Golden = team-agent-public @ v0.2.11 (439bef8),
|
|
6
|
+
// probed via PYTHONPATH=.../src python3 /tmp/probe_cli_all.py.
|
|
7
|
+
// Each test encodes the EXACT golden value; the porters green these next.
|
|
8
|
+
// =========================================================================
|
|
9
|
+
|
|
10
|
+
// ---- #1 / #15: classify_agent_bucket — raw "idle" with NO health => Unknown ----
|
|
11
|
+
// golden _agent_summary_counts has NO `raw == "idle"` arm (commands.py:320):
|
|
12
|
+
// idle is gated SOLELY on health.status=="idle". A raw "idle" with empty health
|
|
13
|
+
// falls through every branch to the final `else: unknown += 1`.
|
|
14
|
+
// golden probe: _agent_summary_counts({"a":{"status":"idle"}},{}) -> unknown=1.
|
|
15
|
+
// Rust cli.rs:601 adds `|| raw == "idle"` => Idle (WRONG).
|
|
16
|
+
#[test]
|
|
17
|
+
fn red_classify_raw_idle_no_health_is_unknown_not_idle() {
|
|
18
|
+
// golden: classify("idle","") lands in Unknown (the §11 bug-071/077/085 rule).
|
|
19
|
+
assert_eq!(
|
|
20
|
+
classify_agent_bucket("idle", ""),
|
|
21
|
+
SummaryBucket::Unknown,
|
|
22
|
+
"raw 'idle' with empty health MUST be Unknown (golden gates idle on health only)"
|
|
23
|
+
);
|
|
24
|
+
// and the uppercase variant (str.lower() in golden) also Unknown.
|
|
25
|
+
assert_eq!(classify_agent_bucket("IDLE", ""), SummaryBucket::Unknown);
|
|
26
|
+
// full-path golden: agent_summary_counts({"a":{"status":"idle"}},{}) -> unknown=1, idle=0.
|
|
27
|
+
let got = agent_summary_counts(&json!({"a": {"status": "idle"}}), &json!({}));
|
|
28
|
+
assert_eq!(
|
|
29
|
+
got,
|
|
30
|
+
SummaryCounts { unknown: 1, ..Default::default() },
|
|
31
|
+
"golden: raw idle agent with no health => unknown=1 (not idle=1)"
|
|
32
|
+
);
|
|
33
|
+
// health=="idle" is still Idle (the only legitimate idle trigger).
|
|
34
|
+
assert_eq!(classify_agent_bucket("", "idle"), SummaryBucket::Idle);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---- #2 / #21 / #24: format_latest_result faithfulness ----
|
|
38
|
+
// golden _latest_result_line (commands.py:333-337):
|
|
39
|
+
// summary = str(summary or "").replace("\n"," ")[:80]; printed as `{summary or '-'}`;
|
|
40
|
+
// agent_id printed as `{agent_id or '-'}`;
|
|
41
|
+
// created_at rendered through runtime._age_text ('-' for None/'' /invalid ISO; 'Nh ago' for valid).
|
|
42
|
+
// Rust format_latest_result passes summary verbatim, uses unwrap_or("-") (empty stays empty),
|
|
43
|
+
// and prints created_at raw (no age_text).
|
|
44
|
+
#[test]
|
|
45
|
+
fn red_format_latest_result_empty_summary_and_agent_map_to_dash() {
|
|
46
|
+
// golden: summary='' + created_at invalid -> 'latest result: a1 -> - @ -'
|
|
47
|
+
let line = format_status_summary(&json!({
|
|
48
|
+
"latest_results": [{"agent_id": "a1", "summary": "", "created_at": "bad-date"}]
|
|
49
|
+
}));
|
|
50
|
+
let latest = line.lines().nth(4).unwrap();
|
|
51
|
+
assert_eq!(
|
|
52
|
+
latest, "latest result: a1 -> - @ -",
|
|
53
|
+
"empty summary -> '-', invalid created_at -> '-' (age_text); golden commands.py:337"
|
|
54
|
+
);
|
|
55
|
+
// golden: empty agent_id -> '-'
|
|
56
|
+
let line2 = format_status_summary(&json!({
|
|
57
|
+
"latest_results": [{"agent_id": "", "summary": "hi", "created_at": Value::Null}]
|
|
58
|
+
}));
|
|
59
|
+
assert_eq!(
|
|
60
|
+
line2.lines().nth(4).unwrap(),
|
|
61
|
+
"latest result: - -> hi @ -",
|
|
62
|
+
"empty agent_id -> '-' (golden `agent_id or '-'`)"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[test]
|
|
67
|
+
fn red_format_latest_result_newline_flattened_and_truncated_80() {
|
|
68
|
+
// golden: summary 'line1\nline2' -> '\n'->' ' -> 'line1 line2'
|
|
69
|
+
let line = format_status_summary(&json!({
|
|
70
|
+
"latest_results": [{"agent_id": "a1", "summary": "line1\nline2", "created_at": Value::Null}]
|
|
71
|
+
}));
|
|
72
|
+
assert_eq!(
|
|
73
|
+
line.lines().nth(4).unwrap(),
|
|
74
|
+
"latest result: a1 -> line1 line2 @ -",
|
|
75
|
+
"newline in summary MUST flatten to a space (golden .replace('\\n',' '))"
|
|
76
|
+
);
|
|
77
|
+
// golden: 100-char summary truncated to exactly 80 chars.
|
|
78
|
+
let line2 = format_status_summary(&json!({
|
|
79
|
+
"latest_results": [{"agent_id": "a1", "summary": "Z".repeat(100), "created_at": Value::Null}]
|
|
80
|
+
}));
|
|
81
|
+
let latest = line2.lines().nth(4).unwrap();
|
|
82
|
+
let kept = latest
|
|
83
|
+
.strip_prefix("latest result: a1 -> ")
|
|
84
|
+
.unwrap()
|
|
85
|
+
.strip_suffix(" @ -")
|
|
86
|
+
.unwrap();
|
|
87
|
+
assert_eq!(kept.chars().count(), 80, "summary MUST cap at 80 chars (golden [:80])");
|
|
88
|
+
assert_eq!(kept, "Z".repeat(80));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn red_format_latest_result_created_at_is_age_text_not_raw_iso() {
|
|
93
|
+
// golden: a valid ISO created_at renders as relative age ('Nh ago'), NEVER the raw string.
|
|
94
|
+
// (We avoid asserting the exact age — time-dependent — and assert it is NOT verbatim.)
|
|
95
|
+
let line = format_status_summary(&json!({
|
|
96
|
+
"latest_results": [{"agent_id": "a1", "summary": "done", "created_at": "2020-01-01T00:00:00Z"}]
|
|
97
|
+
}));
|
|
98
|
+
let latest = line.lines().nth(4).unwrap();
|
|
99
|
+
let tail = latest.rsplit(" @ ").next().unwrap();
|
|
100
|
+
assert_ne!(
|
|
101
|
+
tail, "2020-01-01T00:00:00Z",
|
|
102
|
+
"created_at MUST be rendered as age_text (e.g. 'Nh ago'), not the raw ISO string"
|
|
103
|
+
);
|
|
104
|
+
assert!(
|
|
105
|
+
tail.ends_with(" ago"),
|
|
106
|
+
"valid ISO created_at renders as a relative age ('... ago'); got: {tail:?}"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- #3: format_status_summary — falsy first latest_results element -> "none" ----
|
|
111
|
+
// golden (commands.py:268,333-335): latest = (latest_results or [{}])[0] if latest_results else None;
|
|
112
|
+
// _latest_result_line returns 'latest result: none' when the first element is falsy (None or {}).
|
|
113
|
+
// Rust .first() returns Some for [Null]/[{}] -> renders '- -> - @ -'.
|
|
114
|
+
#[test]
|
|
115
|
+
fn red_format_status_summary_falsy_first_latest_is_none() {
|
|
116
|
+
// golden: latest_results=[None] -> 'latest result: none'
|
|
117
|
+
let line_null = format_status_summary(&json!({"latest_results": [Value::Null]}));
|
|
118
|
+
assert_eq!(
|
|
119
|
+
line_null.lines().nth(4).unwrap(),
|
|
120
|
+
"latest result: none",
|
|
121
|
+
"a Null first element renders 'latest result: none' (golden falsy guard)"
|
|
122
|
+
);
|
|
123
|
+
// golden: latest_results=[{}] -> 'latest result: none'
|
|
124
|
+
let line_empty = format_status_summary(&json!({"latest_results": [{}]}));
|
|
125
|
+
assert_eq!(
|
|
126
|
+
line_empty.lines().nth(4).unwrap(),
|
|
127
|
+
"latest result: none",
|
|
128
|
+
"an empty-object first element renders 'latest result: none' (golden falsy guard)"
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---- #4: format_status_summary — empty-string falsy fallbacks + current_command ----
|
|
133
|
+
// golden: '' is falsy via Python `or`:
|
|
134
|
+
// coordinator status '' -> 'stopped' (commands.py:284)
|
|
135
|
+
// pane_id '' -> '-' (line 285)
|
|
136
|
+
// cmd = pane_current_command or current_command or '-' (line 285) — note the current_command fallback.
|
|
137
|
+
// Rust serde unwrap_or keeps '' verbatim and has NO current_command read.
|
|
138
|
+
#[test]
|
|
139
|
+
fn red_format_status_summary_empty_string_coordinator_status_is_stopped() {
|
|
140
|
+
// golden: coordinator.status='' -> 'coordinator: stopped schema_ok=False tmux=False'
|
|
141
|
+
let line = format_status_summary(&json!({"coordinator": {"status": ""}}));
|
|
142
|
+
assert_eq!(
|
|
143
|
+
line.lines().next().unwrap(),
|
|
144
|
+
"coordinator: stopped schema_ok=false tmux=false",
|
|
145
|
+
"empty-string coordinator status MUST fall back to 'stopped' (golden `status or 'stopped'`)"
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[test]
|
|
150
|
+
fn red_format_status_summary_empty_pane_id_is_dash() {
|
|
151
|
+
// golden: pane_id='' -> 'receiver: - cmd=x'
|
|
152
|
+
let line = format_status_summary(&json!({
|
|
153
|
+
"leader_receiver": {"pane_id": "", "pane_current_command": "x"}
|
|
154
|
+
}));
|
|
155
|
+
assert_eq!(
|
|
156
|
+
line.lines().nth(1).unwrap(),
|
|
157
|
+
"receiver: - cmd=x",
|
|
158
|
+
"empty-string pane_id MUST fall back to '-' (golden `pane_id or '-'`)"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn red_format_status_summary_cmd_falls_back_to_current_command() {
|
|
164
|
+
// golden: missing pane_current_command + current_command='claude' -> 'receiver: %3 cmd=claude'
|
|
165
|
+
let line_missing = format_status_summary(&json!({
|
|
166
|
+
"leader_receiver": {"pane_id": "%3", "current_command": "claude"}
|
|
167
|
+
}));
|
|
168
|
+
assert_eq!(
|
|
169
|
+
line_missing.lines().nth(1).unwrap(),
|
|
170
|
+
"receiver: %3 cmd=claude",
|
|
171
|
+
"cmd MUST fall back to current_command when pane_current_command is absent (golden line 285)"
|
|
172
|
+
);
|
|
173
|
+
// golden: empty pane_current_command + current_command='claude' -> 'receiver: %3 cmd=claude'
|
|
174
|
+
let line_empty = format_status_summary(&json!({
|
|
175
|
+
"leader_receiver": {"pane_id": "%3", "pane_current_command": "", "current_command": "claude"}
|
|
176
|
+
}));
|
|
177
|
+
assert_eq!(
|
|
178
|
+
line_empty.lines().nth(1).unwrap(),
|
|
179
|
+
"receiver: %3 cmd=claude",
|
|
180
|
+
"empty pane_current_command MUST fall through to current_command (golden falsy `or`)"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- #5 (P2): format_status_summary — Python bool() truthiness coercion ----
|
|
185
|
+
// golden: schema_ok / tmux via bool(...) — int 1 -> True, string 'yes' -> True (commands.py:284).
|
|
186
|
+
// Rust as_bool() returns None for int/str -> false.
|
|
187
|
+
#[test]
|
|
188
|
+
fn red_format_status_summary_bool_coercion_truthy_nonbool() {
|
|
189
|
+
// golden: schema_ok=1 (int) -> schema_ok=True (printed lowercase 'true' per the upstream re-spell).
|
|
190
|
+
let line_int = format_status_summary(&json!({
|
|
191
|
+
"coordinator": {"status": "running", "schema_ok": 1}
|
|
192
|
+
}));
|
|
193
|
+
assert_eq!(
|
|
194
|
+
line_int.lines().next().unwrap(),
|
|
195
|
+
"coordinator: running schema_ok=true tmux=false",
|
|
196
|
+
"int 1 MUST coerce to truthy schema_ok (golden bool(1)==True)"
|
|
197
|
+
);
|
|
198
|
+
// golden: tmux_session_present='yes' (non-empty string) -> tmux=True.
|
|
199
|
+
let line_str = format_status_summary(&json!({"tmux_session_present": "yes"}));
|
|
200
|
+
assert_eq!(
|
|
201
|
+
line_str.lines().next().unwrap(),
|
|
202
|
+
"coordinator: stopped schema_ok=false tmux=true",
|
|
203
|
+
"non-empty string 'yes' MUST coerce to truthy tmux (golden bool('yes')==True)"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- #6 / #18 / #26: parse_inbox_entries block grouping + title strip + [:80] cap ----
|
|
208
|
+
// golden _leader_inbox_entries groups blocks on `[` + 'fallback'; _leader_inbox_entry_title
|
|
209
|
+
// strips bracket/'Team Agent'/'Message id:'/'Task id:'/'From:'/'To:'/'Requires ack:'/'Artifacts:'
|
|
210
|
+
// lines, joins remaining content with single spaces, [:80] cap.
|
|
211
|
+
#[test]
|
|
212
|
+
fn red_inbox_realistic_two_message_grouping_and_metadata_strip() {
|
|
213
|
+
// golden full summary on a realistic 2-message fallback inbox (metadata + bodies):
|
|
214
|
+
let ws = tmp_workspace();
|
|
215
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
216
|
+
let raw = "[m1 fallback]\nTeam Agent\nMessage id: m1\nFrom: worker-1\nTo: leader\n\
|
|
217
|
+
Requires ack: yes\nPlease review the PR and approve the deploy. It is blocking the release.\n\
|
|
218
|
+
[m2 fallback]\nTeam Agent\nFrom: worker-2\nBuild failed on CI, see logs.";
|
|
219
|
+
std::fs::write(&inbox, raw).unwrap();
|
|
220
|
+
let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
|
|
221
|
+
let expected = "Leader inbox: 2 new fallback entries\n\
|
|
222
|
+
- Please review the PR and approve the deploy. It is blocking the release.\n\
|
|
223
|
+
- Build failed on CI, see logs.\n\
|
|
224
|
+
Hint: team-agent inbox leader";
|
|
225
|
+
assert_eq!(
|
|
226
|
+
summary, expected,
|
|
227
|
+
"golden groups into 2 entries, strips Team Agent/Message id/From/To/Requires ack metadata"
|
|
228
|
+
);
|
|
229
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[test]
|
|
233
|
+
fn red_inbox_non_fallback_bracket_stays_in_entry() {
|
|
234
|
+
// golden: a `[...]` WITHOUT 'fallback' is content, not a header:
|
|
235
|
+
// '[only bracket no kw]\nbody line' -> ONE entry titled '[only bracket no kw] body line'.
|
|
236
|
+
let ws = tmp_workspace();
|
|
237
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
238
|
+
std::fs::write(&inbox, "[only bracket no kw]\nbody line").unwrap();
|
|
239
|
+
let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
|
|
240
|
+
assert_eq!(
|
|
241
|
+
summary,
|
|
242
|
+
"Leader inbox: 1 new fallback entry\n- [only bracket no kw] body line\nHint: team-agent inbox leader",
|
|
243
|
+
"a non-'fallback' bracket line stays part of the entry body (golden grouping gate)"
|
|
244
|
+
);
|
|
245
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn red_inbox_plain_lines_join_into_single_entry() {
|
|
250
|
+
// golden: no fallback header at all -> the whole text is ONE entry, lines joined w/ spaces.
|
|
251
|
+
// 'alpha\nbeta\ngamma' -> 1 entry 'alpha beta gamma'.
|
|
252
|
+
let ws = tmp_workspace();
|
|
253
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
254
|
+
std::fs::write(&inbox, "alpha\nbeta\ngamma").unwrap();
|
|
255
|
+
let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
|
|
256
|
+
assert_eq!(
|
|
257
|
+
summary,
|
|
258
|
+
"Leader inbox: 1 new fallback entry\n- alpha beta gamma\nHint: team-agent inbox leader",
|
|
259
|
+
"with no fallback header golden collapses ALL lines into one space-joined entry"
|
|
260
|
+
);
|
|
261
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[test]
|
|
265
|
+
fn red_inbox_title_capped_at_80_chars() {
|
|
266
|
+
// golden: a 200-char body title is capped to exactly 80 chars ([:80] per entry).
|
|
267
|
+
let ws = tmp_workspace();
|
|
268
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
269
|
+
let body = "X".repeat(200);
|
|
270
|
+
std::fs::write(&inbox, format!("[x fallback]\n{body}")).unwrap();
|
|
271
|
+
let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
|
|
272
|
+
assert_eq!(
|
|
273
|
+
summary,
|
|
274
|
+
format!(
|
|
275
|
+
"Leader inbox: 1 new fallback entry\n- {}\nHint: team-agent inbox leader",
|
|
276
|
+
"X".repeat(80)
|
|
277
|
+
),
|
|
278
|
+
"each entry title MUST cap at 80 chars (golden [:80]); Rust applies no per-entry cap"
|
|
279
|
+
);
|
|
280
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---- #7 / #16: consume_leader_inbox_summary — mid-codepoint cursor must NOT panic ----
|
|
284
|
+
// golden seeks to a BYTE offset and decodes errors='replace': a cursor inside a multibyte
|
|
285
|
+
// char yields U+FFFD replacement chars and NEVER crashes (bug-084).
|
|
286
|
+
// Rust slices &text[offset..] which PANICS on a non-char-boundary. The panic IS the red.
|
|
287
|
+
#[test]
|
|
288
|
+
fn red_inbox_mid_codepoint_cursor_decodes_replacement_no_panic() {
|
|
289
|
+
// golden probe (/tmp/probe_cli_all.py): inbox '[a fallback]\n世界 message', cursor='14'
|
|
290
|
+
// (byte 14 is inside '世', whose bytes are 13..16) ->
|
|
291
|
+
// 'Leader inbox: 1 new fallback entry\n- ��界 message\nHint: team-agent inbox leader'
|
|
292
|
+
let ws = tmp_workspace();
|
|
293
|
+
let runtime = ws.join(".team").join("runtime");
|
|
294
|
+
std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\n世界 message").unwrap();
|
|
295
|
+
std::fs::write(runtime.join("leader-inbox.cursor"), "14").unwrap();
|
|
296
|
+
let summary = consume_leader_inbox_summary(&ws, 500)
|
|
297
|
+
.expect("mid-codepoint cursor MUST degrade gracefully, not crash");
|
|
298
|
+
assert_eq!(
|
|
299
|
+
summary,
|
|
300
|
+
"Leader inbox: 1 new fallback entry\n- \u{FFFD}\u{FFFD}界 message\nHint: team-agent inbox leader",
|
|
301
|
+
"mid-codepoint byte offset MUST yield U+FFFD replacement chars (golden errors='replace')"
|
|
302
|
+
);
|
|
303
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---- #8 / #17: consume_leader_inbox_summary — offset>size resets to 0; garbage cursor ----
|
|
307
|
+
// golden helpers.py:38-40: offset<0 or offset>size -> offset=0 (re-read whole file);
|
|
308
|
+
// a ValueError cursor ('abc') -> offset=0 AND size=0 -> offset==size -> None WITHOUT advancing.
|
|
309
|
+
#[test]
|
|
310
|
+
fn red_inbox_beyond_size_cursor_resets_to_zero_and_resummarizes() {
|
|
311
|
+
// golden: file 'hello' inbox, cursor='99999' (> size) -> re-read from 0 -> summary; cursor advances.
|
|
312
|
+
let ws = tmp_workspace();
|
|
313
|
+
let runtime = ws.join(".team").join("runtime");
|
|
314
|
+
std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\nhello").unwrap();
|
|
315
|
+
std::fs::write(runtime.join("leader-inbox.cursor"), "99999").unwrap();
|
|
316
|
+
let summary = consume_leader_inbox_summary(&ws, 500)
|
|
317
|
+
.expect("over-size cursor MUST reset to 0 and re-summarize (golden offset>size => 0)");
|
|
318
|
+
assert_eq!(
|
|
319
|
+
summary,
|
|
320
|
+
"Leader inbox: 1 new fallback entry\n- hello\nHint: team-agent inbox leader",
|
|
321
|
+
"cursor beyond file size MUST re-read the whole inbox (NOT clamp-to-len-then-None)"
|
|
322
|
+
);
|
|
323
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[test]
|
|
327
|
+
fn red_inbox_garbage_cursor_returns_none_without_advancing() {
|
|
328
|
+
// golden: cursor='abc' (ValueError) -> offset=0,size=0 -> offset==size -> None; cursor LEFT 'abc'.
|
|
329
|
+
let ws = tmp_workspace();
|
|
330
|
+
let runtime = ws.join(".team").join("runtime");
|
|
331
|
+
std::fs::write(runtime.join("leader-inbox.log"), "[a fallback]\nhello").unwrap();
|
|
332
|
+
let cursor_path = runtime.join("leader-inbox.cursor");
|
|
333
|
+
std::fs::write(&cursor_path, "abc").unwrap();
|
|
334
|
+
let result = consume_leader_inbox_summary(&ws, 500);
|
|
335
|
+
assert_eq!(
|
|
336
|
+
result, None,
|
|
337
|
+
"an unparseable cursor MUST treat size as 0 (offset==size==0) and return None (golden ValueError)"
|
|
338
|
+
);
|
|
339
|
+
let cursor_after = std::fs::read_to_string(&cursor_path).unwrap();
|
|
340
|
+
assert_eq!(
|
|
341
|
+
cursor_after, "abc",
|
|
342
|
+
"a garbage cursor MUST be left untouched (golden never advances it); Rust overwrites to len"
|
|
343
|
+
);
|
|
344
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---- #9: render_inbox_summary — budget measured in CHARS (code points), not bytes ----
|
|
348
|
+
// golden uses Python str length (code points). Rust compares byte lengths.
|
|
349
|
+
#[test]
|
|
350
|
+
fn red_inbox_budget_is_char_count_not_bytes() {
|
|
351
|
+
// golden probe: 30 CJK chars, budget=100 -> char-len 97 <= 100 so golden KEEPS the full title.
|
|
352
|
+
let ws = tmp_workspace();
|
|
353
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
354
|
+
std::fs::write(&inbox, format!("[x fallback]\n{}", "漢".repeat(30))).unwrap();
|
|
355
|
+
let summary = consume_leader_inbox_summary(&ws, 100).expect("Some");
|
|
356
|
+
assert_eq!(
|
|
357
|
+
summary,
|
|
358
|
+
format!(
|
|
359
|
+
"Leader inbox: 1 new fallback entry\n- {}\nHint: team-agent inbox leader",
|
|
360
|
+
"漢".repeat(30)
|
|
361
|
+
),
|
|
362
|
+
"budget MUST count code points (97 chars <= 100), not bytes; Rust byte-len drops it"
|
|
363
|
+
);
|
|
364
|
+
assert!(
|
|
365
|
+
!summary.contains("Truncated"),
|
|
366
|
+
"the CJK title fits the char budget and MUST NOT be truncated (golden char-len semantics)"
|
|
367
|
+
);
|
|
368
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---- #10: render_inbox_summary — hard-trim when header+footer already exceed budget ----
|
|
372
|
+
// golden post-assembly: if len(summary) > budget, body='\n'.join(lines)[:keep].rstrip()
|
|
373
|
+
// with keep=max(0,budget-len(footer)-6), then `{body} ...\n{footer}` — even the HEADER is trimmed.
|
|
374
|
+
#[test]
|
|
375
|
+
fn red_inbox_small_budget_hard_trims_header() {
|
|
376
|
+
// golden probe: budget=80 on 10 entries -> 'Lea ...\nTruncated: ...'
|
|
377
|
+
let ws = tmp_workspace();
|
|
378
|
+
let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
|
|
379
|
+
let many = (0..10)
|
|
380
|
+
.map(|i| format!("[e{i} fallback]\nMessage number {i} with some text padding here"))
|
|
381
|
+
.collect::<Vec<_>>()
|
|
382
|
+
.join("\n");
|
|
383
|
+
std::fs::write(&inbox, &many).unwrap();
|
|
384
|
+
let summary = consume_leader_inbox_summary(&ws, 80).expect("Some");
|
|
385
|
+
assert_eq!(
|
|
386
|
+
summary,
|
|
387
|
+
"Lea ...\nTruncated: more fallback entries available; run team-agent inbox leader",
|
|
388
|
+
"when header+footer alone exceed budget the body (incl header) MUST hard-trim (golden)"
|
|
389
|
+
);
|
|
390
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---- #19 / #25: emit human-dict scalar formatting (None/True/False) + nested string quotes ----
|
|
394
|
+
// golden emit (helpers.py:16-21): scalar via Python str() -> None->'None', True->'True', False->'False';
|
|
395
|
+
// dict/list via json.dumps(ensure_ascii=False) -> string ELEMENTS keep their double quotes.
|
|
396
|
+
#[test]
|
|
397
|
+
fn red_emit_human_dict_scalar_none_true_false_and_quoted_strings() {
|
|
398
|
+
// golden probe: emit({"k":{"a":1,"b":[1,2]},"s":"hi","u":"世界","n":None,"f":False,"t":True}, False)
|
|
399
|
+
let out = emit(
|
|
400
|
+
&CmdOutput::Json(json!({
|
|
401
|
+
"k": {"a": 1, "b": [1, 2]},
|
|
402
|
+
"s": "hi",
|
|
403
|
+
"u": "世界",
|
|
404
|
+
"n": Value::Null,
|
|
405
|
+
"f": false,
|
|
406
|
+
"t": true,
|
|
407
|
+
})),
|
|
408
|
+
false,
|
|
409
|
+
)
|
|
410
|
+
.expect("dict human emit returns Some");
|
|
411
|
+
assert_eq!(
|
|
412
|
+
out,
|
|
413
|
+
"k: {\"a\": 1, \"b\": [1, 2]}\ns: hi\nu: 世界\nn: None\nf: False\nt: True",
|
|
414
|
+
"top-level scalar Null/false/true MUST render Python-str 'None'/'False'/'True' (golden)"
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#[test]
|
|
419
|
+
fn red_emit_human_dict_string_elements_keep_quotes_in_collections() {
|
|
420
|
+
// golden probe: emit({"items":["a","b"],"mixed":["x",1,True]}, False)
|
|
421
|
+
// -> 'items: ["a", "b"]\nmixed: ["x", 1, true]' (string elements KEEP double quotes;
|
|
422
|
+
// bool lowercased to json 'true' INSIDE the json.dumps collection).
|
|
423
|
+
let out = emit(
|
|
424
|
+
&CmdOutput::Json(json!({"items": ["a", "b"], "mixed": ["x", 1, true]})),
|
|
425
|
+
false,
|
|
426
|
+
)
|
|
427
|
+
.expect("dict human emit returns Some");
|
|
428
|
+
assert_eq!(
|
|
429
|
+
out,
|
|
430
|
+
"items: [\"a\", \"b\"]\nmixed: [\"x\", 1, true]",
|
|
431
|
+
"string elements nested in a list MUST keep their double quotes (golden json.dumps)"
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ---- #20: send_target None routes to assignee, NEVER broadcast ----
|
|
436
|
+
// golden _send_target returns None for a no-target send; send_message(target=None) routes to
|
|
437
|
+
// the task assignee / leader receiver — '*' is the ONLY broadcast trigger.
|
|
438
|
+
// Rust maps None => MessageTarget::Broadcast (WRONG recipient set).
|
|
439
|
+
#[test]
|
|
440
|
+
fn red_send_target_none_is_not_broadcast() {
|
|
441
|
+
// golden: _send_target(targets=None, target=None) => None (single/assignee routing, NOT broadcast).
|
|
442
|
+
let got = send_target(None, None);
|
|
443
|
+
assert_ne!(
|
|
444
|
+
got,
|
|
445
|
+
MessageTarget::Broadcast,
|
|
446
|
+
"a no-target send MUST NOT broadcast to the whole team; golden routes to the assignee/leader. \
|
|
447
|
+
'*' is the only broadcast trigger."
|
|
448
|
+
);
|
|
449
|
+
// '*' remains the broadcast trigger (unchanged invariant).
|
|
450
|
+
assert_eq!(send_target(None, Some("*")), MessageTarget::Broadcast);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---- #23: cmd_doctor comms (human) returns COMMS_BOUNDARY_TEXT + sorted indented JSON ----
|
|
454
|
+
// golden: for --comms WITHOUT --json, cmd_doctor returns the STRING
|
|
455
|
+
// f"{COMMS_BOUNDARY_TEXT}\n{json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True)}".
|
|
456
|
+
// Rust always does CmdResult::from_json -> CmdOutput::Json (wrong shape).
|
|
457
|
+
#[test]
|
|
458
|
+
fn red_cmd_doctor_comms_human_is_boundary_text_plus_sorted_json() {
|
|
459
|
+
const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency. Does NOT perform live runtime message round-trip. comms contract suite deferred to 0.2.9 (test files not shipped). (zero token, zero pollution)";
|
|
460
|
+
let args = DoctorArgs {
|
|
461
|
+
spec: None,
|
|
462
|
+
workspace: PathBuf::from("."),
|
|
463
|
+
gate: None,
|
|
464
|
+
comms: true,
|
|
465
|
+
team: None,
|
|
466
|
+
fix: false,
|
|
467
|
+
fix_schema: false,
|
|
468
|
+
cleanup_orphans: false,
|
|
469
|
+
confirm: false,
|
|
470
|
+
json: false,
|
|
471
|
+
};
|
|
472
|
+
let result = cmd_doctor(&args).expect("comms doctor returns CmdResult");
|
|
473
|
+
let text = match result.output {
|
|
474
|
+
CmdOutput::Human(s) => s,
|
|
475
|
+
other => panic!(
|
|
476
|
+
"comms WITHOUT --json MUST be a Human boundary-text + JSON string, got {other:?}"
|
|
477
|
+
),
|
|
478
|
+
};
|
|
479
|
+
assert!(
|
|
480
|
+
text.starts_with(&format!("{COMMS_BOUNDARY_TEXT}\n")),
|
|
481
|
+
"comms human output MUST start with COMMS_BOUNDARY_TEXT then a newline (golden commands.py:231); got: {text:?}"
|
|
482
|
+
);
|
|
483
|
+
// the tail is the selftest result rendered as sort_keys+indent=2 JSON (parseable, sorted).
|
|
484
|
+
let json_tail = text.strip_prefix(&format!("{COMMS_BOUNDARY_TEXT}\n")).unwrap();
|
|
485
|
+
let parsed: Value = serde_json::from_str(json_tail)
|
|
486
|
+
.expect("comms human tail MUST be indent=2 sort_keys JSON of the selftest result");
|
|
487
|
+
assert!(parsed.is_object(), "comms selftest JSON tail is an object");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---- #13 / #27 (P2): run() must NOT treat 'claude_code' as a passthrough trigger ----
|
|
491
|
+
// golden parser.py:86: only raw_argv[0] in {'codex','claude'} triggers leader passthrough.
|
|
492
|
+
// 'claude_code' is the internal provider name, NOT a CLI subcommand (argparse would reject it).
|
|
493
|
+
// Rust run() (cli.rs:1305) adds a 'claude_code' arm.
|
|
494
|
+
#[test]
|
|
495
|
+
fn red_run_claude_code_is_not_a_passthrough_trigger() {
|
|
496
|
+
// golden: `team-agent claude_code -h` is an invalid choice (NOT a clean passthrough exit).
|
|
497
|
+
// Rust currently routes it to cmd_leader_passthrough("claude_code",["-h"]) -> CmdResult::none()
|
|
498
|
+
// -> ExitCode::Ok. Golden would NOT treat it as a valid leader passthrough.
|
|
499
|
+
let exit = run(&["claude_code".to_string(), "-h".to_string()], Path::new("."));
|
|
500
|
+
assert_ne!(
|
|
501
|
+
exit,
|
|
502
|
+
ExitCode::Ok,
|
|
503
|
+
"'claude_code' MUST NOT be a leader passthrough trigger (golden gate is {{codex,claude}} only)"
|
|
504
|
+
);
|
|
505
|
+
// codex/claude REMAIN valid passthrough triggers (the -h fast path returns Ok).
|
|
506
|
+
assert_eq!(run(&["codex".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
|
|
507
|
+
assert_eq!(run(&["claude".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
|
|
508
|
+
}
|
|
509
|
+
|