@team-agent/installer 0.3.12 → 0.3.14
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 +23 -0
- package/crates/team-agent/src/cli/leader.rs +2 -5
- package/crates/team-agent/src/cli/mod.rs +258 -79
- package/crates/team-agent/src/cli/tests/compile.rs +69 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +68 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +160 -2
- package/crates/team-agent/src/compiler.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +16 -83
- package/crates/team-agent/src/leader/lease.rs +4 -20
- package/crates/team-agent/src/leader/mod.rs +3 -1
- package/crates/team-agent/src/leader/owner_bind.rs +46 -48
- package/crates/team-agent/src/leader/provider_attribution.rs +214 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -3
- package/crates/team-agent/src/leader/rediscover.rs +34 -17
- package/crates/team-agent/src/leader/start.rs +279 -4
- package/crates/team-agent/src/lifecycle/launch.rs +279 -24
- package/crates/team-agent/src/lifecycle/restart/common.rs +50 -40
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +343 -49
- package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
- package/crates/team-agent/src/lifecycle/tests/core.rs +181 -45
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +390 -61
- package/crates/team-agent/src/lifecycle/types.rs +28 -0
- package/crates/team-agent/src/messaging/delivery.rs +5 -1
- package/crates/team-agent/src/messaging/helpers.rs +1 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +14 -0
- package/crates/team-agent/src/provider/adapter.rs +6 -3
- package/crates/team-agent/src/provider/startup_prompt.rs +173 -0
- package/crates/team-agent/src/provider/testdata/copilot-ready-marker.txt +5 -0
- package/crates/team-agent/src/provider/testdata/copilot-trust-prompt.txt +19 -0
- package/crates/team-agent/src/tmux_backend.rs +5 -0
- package/crates/team-agent/src/transport/test_support.rs +22 -7
- package/crates/team-agent/src/transport.rs +4 -0
- package/package.json +4 -4
|
@@ -121,12 +121,23 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
121
121
|
|
|
122
122
|
let transport = OfflineTransport::new();
|
|
123
123
|
let result = quick_start_with_transport(&team, None, true, true, None, &transport);
|
|
124
|
-
assert!(
|
|
124
|
+
assert!(
|
|
125
|
+
matches!(result, Ok(QuickStartReport::Ready { .. })),
|
|
126
|
+
"quick-start failed: {result:?}"
|
|
127
|
+
);
|
|
125
128
|
|
|
126
129
|
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
127
|
-
assert!(state_path.exists(), "quick-start .team/current must persist runtime state under project root");
|
|
128
130
|
assert!(
|
|
129
|
-
|
|
131
|
+
state_path.exists(),
|
|
132
|
+
"quick-start .team/current must persist runtime state under project root"
|
|
133
|
+
);
|
|
134
|
+
assert!(
|
|
135
|
+
!workspace
|
|
136
|
+
.join(".team")
|
|
137
|
+
.join(".team")
|
|
138
|
+
.join("runtime")
|
|
139
|
+
.join("state.json")
|
|
140
|
+
.exists(),
|
|
130
141
|
"quick-start .team/current must not create nested .team/.team runtime state"
|
|
131
142
|
);
|
|
132
143
|
|
|
@@ -137,11 +148,21 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
137
148
|
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
138
149
|
)
|
|
139
150
|
.expect("status/collect selector should resolve project root");
|
|
140
|
-
assert_eq!(
|
|
151
|
+
assert_eq!(
|
|
152
|
+
selected.run_workspace,
|
|
153
|
+
workspace,
|
|
154
|
+
"input={}",
|
|
155
|
+
input.display()
|
|
156
|
+
);
|
|
141
157
|
// E5: selector resolves spec_path to .team/runtime/<team_key>/ (team_key="current").
|
|
142
158
|
let expected_spec = crate::model::paths::runtime_spec_path(&workspace, "current");
|
|
143
159
|
assert_eq!(
|
|
144
|
-
selected
|
|
160
|
+
selected
|
|
161
|
+
.spec_path
|
|
162
|
+
.as_deref()
|
|
163
|
+
.map(std::fs::canonicalize)
|
|
164
|
+
.transpose()
|
|
165
|
+
.unwrap(),
|
|
145
166
|
Some(std::fs::canonicalize(&expected_spec).unwrap()),
|
|
146
167
|
"input={}",
|
|
147
168
|
input.display()
|
|
@@ -155,14 +176,20 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
155
176
|
summary: false,
|
|
156
177
|
json: true,
|
|
157
178
|
});
|
|
158
|
-
assert!(
|
|
179
|
+
assert!(
|
|
180
|
+
status.is_ok(),
|
|
181
|
+
"status should normalize teamdir to project-root runtime state: {status:?}"
|
|
182
|
+
);
|
|
159
183
|
|
|
160
184
|
let collect = crate::cli::cmd_collect(&crate::cli::CollectArgs {
|
|
161
185
|
result_file: None,
|
|
162
186
|
workspace: team.clone(),
|
|
163
187
|
json: true,
|
|
164
188
|
});
|
|
165
|
-
assert!(
|
|
189
|
+
assert!(
|
|
190
|
+
collect.is_ok(),
|
|
191
|
+
"collect should normalize teamdir to the same project-root state/spec: {collect:?}"
|
|
192
|
+
);
|
|
166
193
|
}
|
|
167
194
|
|
|
168
195
|
// P0 — quick_start over an INVALID role doc (missing `provider`) must surface the REAL compile
|
|
@@ -222,7 +249,15 @@ fn launch_dry_run_resolves_real_plan_not_stub_error() {
|
|
|
222
249
|
fn start_agent_proceeds_past_owner_gate_for_unowned_workspace() {
|
|
223
250
|
let ws = unowned_running_ws();
|
|
224
251
|
let transport = OfflineTransport::new();
|
|
225
|
-
let result = start_agent_with_transport(
|
|
252
|
+
let result = start_agent_with_transport(
|
|
253
|
+
&ws,
|
|
254
|
+
&AgentId::new("w1"),
|
|
255
|
+
false,
|
|
256
|
+
false,
|
|
257
|
+
true,
|
|
258
|
+
None,
|
|
259
|
+
&transport,
|
|
260
|
+
);
|
|
226
261
|
assert!(
|
|
227
262
|
!matches!(result, Err(LifecycleError::OwnerRefused(_))),
|
|
228
263
|
"start_agent must reach the real chain for a no-owner workspace, not hard-refuse owner; got {result:?}"
|
|
@@ -292,7 +327,10 @@ fn fork_agent_proceeds_past_owner_gate_for_unowned_workspace() {
|
|
|
292
327
|
fn quick_start_full_ready_real_spawn() {
|
|
293
328
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
294
329
|
let report = quick_start(&team, None, true, true, None).expect("quick_start launches the team");
|
|
295
|
-
assert!(
|
|
330
|
+
assert!(
|
|
331
|
+
matches!(report, QuickStartReport::Ready { .. }),
|
|
332
|
+
"got {report:?}"
|
|
333
|
+
);
|
|
296
334
|
}
|
|
297
335
|
|
|
298
336
|
// ═════════════════════════════════════════════════════════════════════════
|
|
@@ -375,7 +413,9 @@ fn compiled_spec_path_with_paused_agent(team: &std::path::Path) -> PathBuf {
|
|
|
375
413
|
_ => None,
|
|
376
414
|
})
|
|
377
415
|
.expect("compiled spec must contain agents");
|
|
378
|
-
let first = agents
|
|
416
|
+
let first = agents
|
|
417
|
+
.first_mut()
|
|
418
|
+
.expect("compiled spec must contain an agent");
|
|
379
419
|
let crate::model::yaml::Value::Map(agent) = first else {
|
|
380
420
|
panic!("compiled agent must be a map");
|
|
381
421
|
};
|
|
@@ -422,7 +462,10 @@ fn spine_launch_dry_run_safety_reflects_spec_dangerous() {
|
|
|
422
462
|
let team = quick_start_team_dir_custom(QS_TEAM_MD_DANGEROUS, QS_VALID_ROLE);
|
|
423
463
|
let spec_path = compiled_spec_path(&team);
|
|
424
464
|
let report = launch(&spec_path, true, false, true).expect("dry_run launch");
|
|
425
|
-
assert!(
|
|
465
|
+
assert!(
|
|
466
|
+
report.safety.enabled,
|
|
467
|
+
"safety must reflect spec.runtime.dangerous_auto_approve=true"
|
|
468
|
+
);
|
|
426
469
|
assert_eq!(
|
|
427
470
|
report.safety.source,
|
|
428
471
|
DangerousApprovalSource::RuntimeConfig,
|
|
@@ -471,7 +514,14 @@ fn spine_add_agent_rejects_duplicate_agent_id() {
|
|
|
471
514
|
let role = team.join("dup-role.md");
|
|
472
515
|
std::fs::write(&role, QS_VALID_ROLE).unwrap(); // a role doc named 'implementer' = duplicate
|
|
473
516
|
let transport = OfflineTransport::new();
|
|
474
|
-
let result = add_agent_with_transport(
|
|
517
|
+
let result = add_agent_with_transport(
|
|
518
|
+
&team,
|
|
519
|
+
&AgentId::new("implementer"),
|
|
520
|
+
&role,
|
|
521
|
+
false,
|
|
522
|
+
None,
|
|
523
|
+
&transport,
|
|
524
|
+
);
|
|
475
525
|
let text = format!("{result:?}");
|
|
476
526
|
assert!(
|
|
477
527
|
text.contains("already exists"),
|
|
@@ -524,7 +574,7 @@ fn e5_add_agent_does_not_copy_role_into_platform_dir_and_injects_into_spec() {
|
|
|
524
574
|
#[test]
|
|
525
575
|
fn e5_restart_rebuilds_runtime_spec_from_role_docs() {
|
|
526
576
|
let ws = restart_ws_two_resumable_workers(); // has TEAM.md + agents/{alpha,bravo}.md + state
|
|
527
|
-
|
|
577
|
+
// Edit a role doc AFTER initial compile: change alpha's role text.
|
|
528
578
|
std::fs::write(
|
|
529
579
|
ws.join("agents").join("alpha.md"),
|
|
530
580
|
"---\nname: alpha\nrole: RENAMED Alpha Role\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nAlpha edited.\n",
|
|
@@ -551,10 +601,13 @@ fn e5_restart_rebuilds_runtime_spec_from_role_docs() {
|
|
|
551
601
|
#[test]
|
|
552
602
|
fn red2_restart_finds_runtime_spec_not_missing_when_user_dir_has_no_spec() {
|
|
553
603
|
let ws = restart_ws_two_resumable_workers(); // has TEAM.md + agents + state
|
|
554
|
-
|
|
555
|
-
|
|
604
|
+
// Simulate spec-demote: remove any user-dir spec the fixture wrote; restart must still find
|
|
605
|
+
// the runtime spec (rebuilt from role docs / read-order B), NOT report "missing spec".
|
|
556
606
|
let _ = std::fs::remove_file(ws.join("team.spec.yaml"));
|
|
557
|
-
assert!(
|
|
607
|
+
assert!(
|
|
608
|
+
!ws.join("team.spec.yaml").exists(),
|
|
609
|
+
"user-dir spec removed (spec-demote condition)"
|
|
610
|
+
);
|
|
558
611
|
let transport = OfflineTransport::new();
|
|
559
612
|
let result = restart_with_transport(&ws, false, None, &transport);
|
|
560
613
|
// The gate must NOT short-circuit with "missing spec for restart" — restart must proceed
|
|
@@ -575,7 +628,10 @@ fn red2_restart_empty_workspace_still_reports_missing() {
|
|
|
575
628
|
let result = restart_with_transport(&ws, false, None, &transport);
|
|
576
629
|
let text = format!("{result:?}");
|
|
577
630
|
assert!(
|
|
578
|
-
text.contains("missing spec")
|
|
631
|
+
text.contains("missing spec")
|
|
632
|
+
|| text.to_lowercase().contains("not found")
|
|
633
|
+
|| text.contains("no_local_team_context")
|
|
634
|
+
|| text.to_lowercase().contains("invalid workspace"),
|
|
579
635
|
"RED-2 negative: empty workspace restart must still error (missing/not-found); got {text}"
|
|
580
636
|
);
|
|
581
637
|
let _ = std::fs::remove_dir_all(&ws);
|
|
@@ -595,7 +651,11 @@ fn red2still_entry_gate_resolves_parent_dot_team_form_a() {
|
|
|
595
651
|
let base = temp_ws();
|
|
596
652
|
let team_dir = base.join("teamdir");
|
|
597
653
|
std::fs::create_dir_all(team_dir.join("agents")).unwrap();
|
|
598
|
-
std::fs::write(
|
|
654
|
+
std::fs::write(
|
|
655
|
+
team_dir.join("TEAM.md"),
|
|
656
|
+
"---\nname: teamdir\nobjective: o\nprovider: codex\n---\nt.\n",
|
|
657
|
+
)
|
|
658
|
+
.unwrap();
|
|
599
659
|
std::fs::write(team_dir.join("agents").join("w1.md"), QS_VALID_ROLE).unwrap();
|
|
600
660
|
// .team lives in the PARENT (base), with a runtime spec — mirrors quick-start <dir> from base cwd.
|
|
601
661
|
let spec = crate::compiler::compile_team(&team_dir).unwrap();
|
|
@@ -622,7 +682,10 @@ fn red2still_entry_gate_resolves_parent_dot_team_form_a() {
|
|
|
622
682
|
);
|
|
623
683
|
// Full restart on team_dir must NOT early-exit with missing spec.
|
|
624
684
|
let transport = OfflineTransport::new();
|
|
625
|
-
let text = format!(
|
|
685
|
+
let text = format!(
|
|
686
|
+
"{:?}",
|
|
687
|
+
restart_with_transport(&team_dir, false, None, &transport)
|
|
688
|
+
);
|
|
626
689
|
assert!(
|
|
627
690
|
!text.contains("missing spec for restart") && !text.contains("active team spec not found"),
|
|
628
691
|
"RED-2-STILL Form A: restart <team_dir> (.team in parent) must not report missing; got {text}"
|
|
@@ -635,7 +698,11 @@ fn red2still_entry_gate_same_dir_dot_team_form_b() {
|
|
|
635
698
|
// Form B: .team in team_dir itself (cwd == team_dir at quick-start time).
|
|
636
699
|
let team_dir = temp_ws();
|
|
637
700
|
std::fs::create_dir_all(team_dir.join("agents")).unwrap();
|
|
638
|
-
std::fs::write(
|
|
701
|
+
std::fs::write(
|
|
702
|
+
team_dir.join("TEAM.md"),
|
|
703
|
+
"---\nname: tb\nobjective: o\nprovider: codex\n---\nt.\n",
|
|
704
|
+
)
|
|
705
|
+
.unwrap();
|
|
639
706
|
std::fs::write(team_dir.join("agents").join("w1.md"), QS_VALID_ROLE).unwrap();
|
|
640
707
|
let spec = crate::compiler::compile_team(&team_dir).unwrap();
|
|
641
708
|
let runtime_spec = crate::model::paths::runtime_spec_path(&team_dir, "tb");
|
|
@@ -649,7 +716,10 @@ fn red2still_entry_gate_same_dir_dot_team_form_b() {
|
|
|
649
716
|
)
|
|
650
717
|
.unwrap();
|
|
651
718
|
let transport = OfflineTransport::new();
|
|
652
|
-
let text = format!(
|
|
719
|
+
let text = format!(
|
|
720
|
+
"{:?}",
|
|
721
|
+
restart_with_transport(&team_dir, false, None, &transport)
|
|
722
|
+
);
|
|
653
723
|
assert!(
|
|
654
724
|
!text.contains("missing spec for restart") && !text.contains("active team spec not found"),
|
|
655
725
|
"RED-2-STILL Form B: restart <team_dir> (.team in team_dir) must not report missing; got {text}"
|
|
@@ -703,7 +773,8 @@ fn e5_add_agent_resolves_team_dir_to_role_dir_when_runtime_spec_exists() {
|
|
|
703
773
|
)
|
|
704
774
|
.unwrap();
|
|
705
775
|
let transport = OfflineTransport::new();
|
|
706
|
-
let result =
|
|
776
|
+
let result =
|
|
777
|
+
add_agent_with_transport(&team, &AgentId::new("w2"), &role, false, None, &transport);
|
|
707
778
|
// Core decoupling signature: must NOT fail because compile_team looked for TEAM.md/agents under
|
|
708
779
|
// the runtime spec dir. Before the fix, team_dir=spec_workspace(runtime) → "missing TEAM.md".
|
|
709
780
|
let text = format!("{result:?}");
|
|
@@ -825,15 +896,29 @@ fn launch_with_transport_records_one_spawn_per_agent_carrying_build_command() {
|
|
|
825
896
|
"exactly one spawn per started agent; recorded={recorded:?} started={:?}",
|
|
826
897
|
report.started
|
|
827
898
|
);
|
|
828
|
-
assert!(!report.started.is_empty(), "a real (non-dry-run) launch must spawn >=1 worker");
|
|
829
|
-
assert!(!report.dry_run, "launch_with_transport(dry_run=false) must report dry_run=false");
|
|
830
|
-
assert_eq!(recorded[0].0, "spawn_first", "the first worker uses new-session (spawn_first)");
|
|
831
899
|
assert!(
|
|
832
|
-
|
|
900
|
+
!report.started.is_empty(),
|
|
901
|
+
"a real (non-dry-run) launch must spawn >=1 worker"
|
|
902
|
+
);
|
|
903
|
+
assert!(
|
|
904
|
+
!report.dry_run,
|
|
905
|
+
"launch_with_transport(dry_run=false) must report dry_run=false"
|
|
906
|
+
);
|
|
907
|
+
assert_eq!(
|
|
908
|
+
recorded[0].0, "spawn_first",
|
|
909
|
+
"the first worker uses new-session (spawn_first)"
|
|
910
|
+
);
|
|
911
|
+
assert!(
|
|
912
|
+
recorded
|
|
913
|
+
.iter()
|
|
914
|
+
.any(|(_, argv)| argv.iter().any(|a| a == "codex")),
|
|
833
915
|
"each spawn argv must carry the agent's provider build_command (codex); got {recorded:?}"
|
|
834
916
|
);
|
|
835
917
|
assert!(
|
|
836
|
-
report
|
|
918
|
+
report
|
|
919
|
+
.started
|
|
920
|
+
.iter()
|
|
921
|
+
.any(|s| s.agent_id.as_str() == "implementer"),
|
|
837
922
|
"LaunchReport.started must list the compiled agent; got {:?}",
|
|
838
923
|
report.started
|
|
839
924
|
);
|
|
@@ -853,8 +938,17 @@ pub(super) fn seed_healthy_coordinator(workspace: &std::path::Path) {
|
|
|
853
938
|
std::fs::create_dir_all(crate::model::paths::runtime_dir(workspace)).unwrap();
|
|
854
939
|
let _ = crate::message_store::MessageStore::open(workspace).unwrap(); // create db schema (schema.ok)
|
|
855
940
|
let me = crate::coordinator::Pid::new(std::process::id());
|
|
856
|
-
crate::coordinator::write_coordinator_metadata(
|
|
857
|
-
|
|
941
|
+
crate::coordinator::write_coordinator_metadata(
|
|
942
|
+
&wp,
|
|
943
|
+
me,
|
|
944
|
+
crate::coordinator::MetadataSource::Boot,
|
|
945
|
+
)
|
|
946
|
+
.unwrap();
|
|
947
|
+
std::fs::write(
|
|
948
|
+
crate::coordinator::coordinator_pid_path(&wp),
|
|
949
|
+
me.to_string(),
|
|
950
|
+
)
|
|
951
|
+
.unwrap();
|
|
858
952
|
}
|
|
859
953
|
|
|
860
954
|
// RED — quick_start_with_transport must drive the REAL spawn path: launch.dry_run==false, started
|
|
@@ -889,7 +983,10 @@ fn quick_start_with_transport_spawns_workers_not_dry_run() {
|
|
|
889
983
|
launch.started
|
|
890
984
|
);
|
|
891
985
|
assert!(
|
|
892
|
-
launch
|
|
986
|
+
launch
|
|
987
|
+
.started
|
|
988
|
+
.iter()
|
|
989
|
+
.any(|s| s.agent_id.as_str() == "implementer"),
|
|
893
990
|
"launch.started must list the compiled agent 'implementer'; got {:?}",
|
|
894
991
|
launch.started
|
|
895
992
|
);
|
|
@@ -899,9 +996,14 @@ fn quick_start_with_transport_spawns_workers_not_dry_run() {
|
|
|
899
996
|
!recorded.is_empty(),
|
|
900
997
|
"quick_start must drive the transport spawn path (>=1 spawn recorded); the dry-run bug records ZERO spawns"
|
|
901
998
|
);
|
|
902
|
-
assert_eq!(
|
|
999
|
+
assert_eq!(
|
|
1000
|
+
recorded[0].0, "spawn_first",
|
|
1001
|
+
"the first worker uses new-session (spawn_first); got {recorded:?}"
|
|
1002
|
+
);
|
|
903
1003
|
assert!(
|
|
904
|
-
recorded
|
|
1004
|
+
recorded
|
|
1005
|
+
.iter()
|
|
1006
|
+
.any(|(_, argv)| argv.iter().any(|a| a == "codex")),
|
|
905
1007
|
"the spawn argv must carry the agent's provider build_command (codex); got {recorded:?}"
|
|
906
1008
|
);
|
|
907
1009
|
}
|
|
@@ -917,7 +1019,10 @@ fn quick_start_fresh_ws_spawns_resident_tmux_and_coordinator() {
|
|
|
917
1019
|
let report = quick_start(&team, None, true, true, None).expect("quick_start");
|
|
918
1020
|
match report {
|
|
919
1021
|
QuickStartReport::Ready { launch, .. } => {
|
|
920
|
-
assert!(
|
|
1022
|
+
assert!(
|
|
1023
|
+
!launch.dry_run,
|
|
1024
|
+
"a real quick_start must spawn (not dry-run)"
|
|
1025
|
+
);
|
|
921
1026
|
assert!(
|
|
922
1027
|
!launch.started.is_empty(),
|
|
923
1028
|
"a real quick_start must spawn >=1 worker into a live tmux session; got {:?}",
|
|
@@ -949,7 +1054,11 @@ pub(super) fn restart_ws_two_resumable_workers() -> PathBuf {
|
|
|
949
1054
|
let bravo_rollout = ws.join("bravo-rollout.jsonl");
|
|
950
1055
|
std::fs::write(&alpha_rollout, "{}\n").unwrap();
|
|
951
1056
|
std::fs::write(&bravo_rollout, "{}\n").unwrap();
|
|
952
|
-
std::fs::write(
|
|
1057
|
+
std::fs::write(
|
|
1058
|
+
ws.join("TEAM.md"),
|
|
1059
|
+
"---\nname: restartteam\nobjective: Restart probe.\nprovider: codex\n---\n\nteam.\n",
|
|
1060
|
+
)
|
|
1061
|
+
.unwrap();
|
|
953
1062
|
std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
954
1063
|
std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
|
|
955
1064
|
let spec = crate::compiler::compile_team(&ws).expect("compile 2-agent team");
|
|
@@ -1030,24 +1139,186 @@ fn restart_with_transport_spawns_resumable_workers_not_stub() {
|
|
|
1030
1139
|
matches!(result, Ok(RestartReport::Restarted { coordinator_started: true, .. })),
|
|
1031
1140
|
"restart must reach RestartReport::Restarted with coordinator_started=true (AlreadyRunning, seeded); got {result:?}"
|
|
1032
1141
|
);
|
|
1142
|
+
let events = crate::event_log::EventLog::new(&ws).tail(20).unwrap();
|
|
1143
|
+
let completed = events
|
|
1144
|
+
.iter()
|
|
1145
|
+
.find(|event| event.get("event").and_then(|v| v.as_str()) == Some("restart.completed"))
|
|
1146
|
+
.expect("restart.completed event for successful restart");
|
|
1147
|
+
assert_eq!(completed.get("rc").and_then(|v| v.as_str()), Some("ok"));
|
|
1033
1148
|
|
|
1034
1149
|
let ws = restart_ws_two_resumable_workers();
|
|
1035
1150
|
let dead_after_first = OfflineTransport::new().with_session_absent_after_spawn_first();
|
|
1036
|
-
let result =
|
|
1151
|
+
let result = restart_with_transport_with_readiness_deadline(
|
|
1152
|
+
&ws,
|
|
1153
|
+
false,
|
|
1154
|
+
None,
|
|
1155
|
+
&dead_after_first,
|
|
1156
|
+
Some(0),
|
|
1157
|
+
);
|
|
1037
1158
|
let recorded = dead_after_first.spawn_records();
|
|
1038
1159
|
assert_eq!(
|
|
1039
1160
|
recorded.len(),
|
|
1040
1161
|
1,
|
|
1041
|
-
"if
|
|
1162
|
+
"if a resumed session disappears after alpha, restart must fail fast before bravo; got result={result:?} recorded={recorded:?}"
|
|
1042
1163
|
);
|
|
1043
1164
|
assert!(
|
|
1044
|
-
recorded.iter().all(|(kind, _)| kind
|
|
1045
|
-
"restart must not call spawn_into/new-window
|
|
1165
|
+
recorded.iter().all(|(kind, _)| kind == "spawn_first"),
|
|
1166
|
+
"restart must not call spawn_into/new-window against a dead server; result={result:?} recorded={recorded:?}"
|
|
1046
1167
|
);
|
|
1047
|
-
let
|
|
1168
|
+
let result_debug = format!("{result:?}");
|
|
1169
|
+
let report = result.expect("resume integrity failure should return a typed failed report");
|
|
1170
|
+
let RestartReport::Failed { failed_agents, .. } = report else {
|
|
1171
|
+
panic!("resume integrity failure must not be Partial/Restarted; got {report:?}");
|
|
1172
|
+
};
|
|
1173
|
+
assert_eq!(failed_agents.len(), 1);
|
|
1174
|
+
assert_eq!(failed_agents[0].agent_id.as_str(), "alpha");
|
|
1175
|
+
assert_eq!(failed_agents[0].phase, "resume");
|
|
1048
1176
|
assert!(
|
|
1049
|
-
|
|
1050
|
-
"
|
|
1177
|
+
dead_after_first.calls().iter().all(|call| *call != "kill_session"),
|
|
1178
|
+
"a single worker/session disappearance must not trigger another shared-session kill; calls={:?}",
|
|
1179
|
+
dead_after_first.calls()
|
|
1180
|
+
);
|
|
1181
|
+
let events = crate::event_log::EventLog::new(&ws).tail(20).unwrap();
|
|
1182
|
+
let failed = events
|
|
1183
|
+
.iter()
|
|
1184
|
+
.find(|event| event.get("event").and_then(|v| v.as_str()) == Some("restart.agent_failed"))
|
|
1185
|
+
.expect("restart.agent_failed event for isolated alpha");
|
|
1186
|
+
assert_eq!(
|
|
1187
|
+
failed.get("agent_id").and_then(|v| v.as_str()),
|
|
1188
|
+
Some("alpha")
|
|
1189
|
+
);
|
|
1190
|
+
assert!(
|
|
1191
|
+
failed
|
|
1192
|
+
.get("error")
|
|
1193
|
+
.and_then(|v| v.as_str())
|
|
1194
|
+
.is_some_and(|error| error.contains("session_disappeared_after_spawn")),
|
|
1195
|
+
"restart must report the first resumed agent/session disappearance explicitly; event={failed} result={result_debug}"
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
#[test]
|
|
1200
|
+
fn restart_spawn_failure_isolated_to_partial_report() {
|
|
1201
|
+
let ws = restart_ws_two_resumable_workers();
|
|
1202
|
+
let transport = OfflineTransport::new().with_spawn_failure("bravo", "injected bravo failure");
|
|
1203
|
+
|
|
1204
|
+
let result = restart_with_transport(&ws, false, None, &transport);
|
|
1205
|
+
|
|
1206
|
+
let report = result.expect("single worker failure must be represented as partial report");
|
|
1207
|
+
let RestartReport::Partial {
|
|
1208
|
+
agents,
|
|
1209
|
+
failed_agents,
|
|
1210
|
+
coordinator_started,
|
|
1211
|
+
..
|
|
1212
|
+
} = report
|
|
1213
|
+
else {
|
|
1214
|
+
panic!("single worker failure must return RestartReport::Partial; got {report:?}");
|
|
1215
|
+
};
|
|
1216
|
+
assert!(
|
|
1217
|
+
coordinator_started,
|
|
1218
|
+
"partial restart still starts coordinator for successful agents"
|
|
1219
|
+
);
|
|
1220
|
+
assert_eq!(
|
|
1221
|
+
agents
|
|
1222
|
+
.iter()
|
|
1223
|
+
.map(|agent| agent.agent_id.as_str())
|
|
1224
|
+
.collect::<Vec<_>>(),
|
|
1225
|
+
vec!["alpha"]
|
|
1226
|
+
);
|
|
1227
|
+
assert_eq!(failed_agents.len(), 1);
|
|
1228
|
+
assert_eq!(failed_agents[0].agent_id.as_str(), "bravo");
|
|
1229
|
+
assert_eq!(failed_agents[0].phase, "spawn");
|
|
1230
|
+
|
|
1231
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1232
|
+
assert_eq!(
|
|
1233
|
+
state
|
|
1234
|
+
.pointer("/agents/alpha/status")
|
|
1235
|
+
.and_then(|v| v.as_str()),
|
|
1236
|
+
Some("running")
|
|
1237
|
+
);
|
|
1238
|
+
assert_eq!(
|
|
1239
|
+
state
|
|
1240
|
+
.pointer("/agents/bravo/status")
|
|
1241
|
+
.and_then(|v| v.as_str()),
|
|
1242
|
+
Some("failed")
|
|
1243
|
+
);
|
|
1244
|
+
assert!(
|
|
1245
|
+
state
|
|
1246
|
+
.pointer("/agents/bravo/restart_error")
|
|
1247
|
+
.and_then(|v| v.as_str())
|
|
1248
|
+
.is_some_and(|error| error.contains("injected bravo failure")),
|
|
1249
|
+
"failed agent must retain restart_error in state: {state}"
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
let events = crate::event_log::EventLog::new(&ws).tail(20).unwrap();
|
|
1253
|
+
let completed = events
|
|
1254
|
+
.iter()
|
|
1255
|
+
.find(|event| event.get("event").and_then(|v| v.as_str()) == Some("restart.completed"))
|
|
1256
|
+
.expect("restart.completed event for partial restart");
|
|
1257
|
+
assert_eq!(
|
|
1258
|
+
completed.get("rc").and_then(|v| v.as_str()),
|
|
1259
|
+
Some("partial")
|
|
1260
|
+
);
|
|
1261
|
+
assert!(
|
|
1262
|
+
completed
|
|
1263
|
+
.get("successful_agents")
|
|
1264
|
+
.and_then(|v| v.as_array())
|
|
1265
|
+
.is_some_and(|agents| agents.iter().any(|agent| agent.as_str() == Some("alpha"))),
|
|
1266
|
+
"completed event must list successful agents: {completed}"
|
|
1267
|
+
);
|
|
1268
|
+
assert!(
|
|
1269
|
+
completed
|
|
1270
|
+
.get("failed_agents")
|
|
1271
|
+
.and_then(|v| v.as_array())
|
|
1272
|
+
.is_some_and(|agents| agents.iter().any(|agent| {
|
|
1273
|
+
agent.get("agent_id").and_then(|v| v.as_str()) == Some("bravo")
|
|
1274
|
+
&& agent.get("phase").and_then(|v| v.as_str()) == Some("spawn")
|
|
1275
|
+
})),
|
|
1276
|
+
"completed event must list failed agents with phase: {completed}"
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
#[test]
|
|
1281
|
+
fn restart_all_spawn_failures_return_failed_report() {
|
|
1282
|
+
let ws = restart_ws_one_resumable_worker();
|
|
1283
|
+
let transport = OfflineTransport::new().with_spawn_failure("alpha", "injected alpha failure");
|
|
1284
|
+
|
|
1285
|
+
let result = restart_with_transport(&ws, false, None, &transport);
|
|
1286
|
+
|
|
1287
|
+
let report = result.expect("all worker failures should return typed failed report");
|
|
1288
|
+
let RestartReport::Failed { failed_agents, .. } = report else {
|
|
1289
|
+
panic!("all worker failures must return RestartReport::Failed; got {report:?}");
|
|
1290
|
+
};
|
|
1291
|
+
assert_eq!(failed_agents.len(), 1);
|
|
1292
|
+
assert_eq!(failed_agents[0].agent_id.as_str(), "alpha");
|
|
1293
|
+
assert_eq!(failed_agents[0].phase, "spawn");
|
|
1294
|
+
|
|
1295
|
+
let events = crate::event_log::EventLog::new(&ws).tail(20).unwrap();
|
|
1296
|
+
let completed = events
|
|
1297
|
+
.iter()
|
|
1298
|
+
.find(|event| event.get("event").and_then(|v| v.as_str()) == Some("restart.completed"))
|
|
1299
|
+
.expect("restart.completed event for failed restart");
|
|
1300
|
+
assert_eq!(completed.get("rc").and_then(|v| v.as_str()), Some("fail"));
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
#[test]
|
|
1304
|
+
fn restart_spawn_loop_keeps_failure_isolation_guard() {
|
|
1305
|
+
let source = include_str!("../restart/rebuild.rs");
|
|
1306
|
+
let start = source
|
|
1307
|
+
.find("BEGIN_B5_RESTART_ISOLATION_LOOP")
|
|
1308
|
+
.expect("B5 isolation loop start marker");
|
|
1309
|
+
let end = source
|
|
1310
|
+
.find("END_B5_RESTART_ISOLATION_LOOP")
|
|
1311
|
+
.expect("B5 isolation loop end marker");
|
|
1312
|
+
let body = &source[start..end];
|
|
1313
|
+
for forbidden in ["?;", "break", "return "] {
|
|
1314
|
+
assert!(
|
|
1315
|
+
!body.contains(forbidden),
|
|
1316
|
+
"B5 spawn loop must not fail-fast via {forbidden:?}; body={body}"
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
assert!(
|
|
1320
|
+
body.contains("continue"),
|
|
1321
|
+
"B5 spawn loop should isolate per-agent failures and continue; body={body}"
|
|
1051
1322
|
);
|
|
1052
1323
|
}
|
|
1053
1324
|
|
|
@@ -1074,10 +1345,14 @@ fn restart_times_out_when_spawned_worker_pane_is_not_addressable() {
|
|
|
1074
1345
|
let events = crate::event_log::EventLog::new(&ws).tail(20).unwrap();
|
|
1075
1346
|
let timeout = events
|
|
1076
1347
|
.iter()
|
|
1077
|
-
.find(|event|
|
|
1348
|
+
.find(|event| {
|
|
1349
|
+
event.get("event").and_then(|v| v.as_str()) == Some("restart.readiness_timeout")
|
|
1350
|
+
})
|
|
1078
1351
|
.expect("restart.readiness_timeout event");
|
|
1079
1352
|
assert_eq!(
|
|
1080
|
-
timeout
|
|
1353
|
+
timeout
|
|
1354
|
+
.get("worker_pane_addressable")
|
|
1355
|
+
.and_then(|v| v.as_bool()),
|
|
1081
1356
|
Some(false),
|
|
1082
1357
|
"timeout event must carry the failed readiness condition: {timeout}"
|
|
1083
1358
|
);
|
|
@@ -1103,7 +1378,15 @@ fn start_agent_with_transport_spawns_resume_not_stub() {
|
|
|
1103
1378
|
seed_healthy_coordinator(&ws);
|
|
1104
1379
|
let transport = OfflineTransport::new();
|
|
1105
1380
|
|
|
1106
|
-
let _result = start_agent_with_transport(
|
|
1381
|
+
let _result = start_agent_with_transport(
|
|
1382
|
+
&ws,
|
|
1383
|
+
&AgentId::new("alpha"),
|
|
1384
|
+
false,
|
|
1385
|
+
false,
|
|
1386
|
+
false,
|
|
1387
|
+
None,
|
|
1388
|
+
&transport,
|
|
1389
|
+
);
|
|
1107
1390
|
|
|
1108
1391
|
let recorded = transport.spawn_records();
|
|
1109
1392
|
assert_eq!(
|
|
@@ -1126,14 +1409,25 @@ fn start_agent_with_transport_spawns_resume_not_stub() {
|
|
|
1126
1409
|
fn add_agent_with_transport_spawns_new_worker_not_stub() {
|
|
1127
1410
|
let team = temp_ws().join("addteam");
|
|
1128
1411
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
1129
|
-
std::fs::write(
|
|
1412
|
+
std::fs::write(
|
|
1413
|
+
team.join("TEAM.md"),
|
|
1414
|
+
"---\nname: addteam\nobjective: Add probe.\nprovider: codex\n---\n\nteam.\n",
|
|
1415
|
+
)
|
|
1416
|
+
.unwrap();
|
|
1130
1417
|
std::fs::write(team.join("agents").join("implementer.md"), QS_VALID_ROLE).unwrap(); // existing agent
|
|
1131
1418
|
let role_file = team.join("worker2-role.md"); // OUTSIDE agents/ -> not a duplicate of an existing agent
|
|
1132
1419
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
1133
1420
|
seed_healthy_coordinator(&team);
|
|
1134
1421
|
let transport = OfflineTransport::new();
|
|
1135
1422
|
|
|
1136
|
-
let _result = add_agent_with_transport(
|
|
1423
|
+
let _result = add_agent_with_transport(
|
|
1424
|
+
&team,
|
|
1425
|
+
&AgentId::new("worker2"),
|
|
1426
|
+
&role_file,
|
|
1427
|
+
false,
|
|
1428
|
+
None,
|
|
1429
|
+
&transport,
|
|
1430
|
+
);
|
|
1137
1431
|
|
|
1138
1432
|
// (a) the recompiled spec was written (real subsystem step). E5: under .team/runtime/<team_key>/,
|
|
1139
1433
|
// NOT the user team dir.
|
|
@@ -1179,7 +1473,9 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
|
|
|
1179
1473
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1180
1474
|
let transport = OfflineTransport::new();
|
|
1181
1475
|
let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
|
|
1182
|
-
let workspace = team
|
|
1476
|
+
let workspace = team
|
|
1477
|
+
.parent()
|
|
1478
|
+
.expect("team_workspace(<base>/teamdir) = <base>");
|
|
1183
1479
|
let state = crate::state::persist::load_runtime_state(workspace)
|
|
1184
1480
|
.expect("runtime state.json must exist after quick_start");
|
|
1185
1481
|
// (b) the tasks KEY must always be present and a JSON array (golden launch/core.py:69 default []).
|
|
@@ -1191,10 +1487,14 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
|
|
|
1191
1487
|
state.as_object().map(|o| o.keys().cloned().collect::<Vec<_>>())
|
|
1192
1488
|
),
|
|
1193
1489
|
};
|
|
1194
|
-
let tasks = tasks
|
|
1490
|
+
let tasks = tasks
|
|
1491
|
+
.as_array()
|
|
1492
|
+
.expect("`tasks` must be a JSON array (golden default [])");
|
|
1195
1493
|
// (a) spec.tasks must be carried into runtime state — the doc compiler's default task id.
|
|
1196
1494
|
assert!(
|
|
1197
|
-
tasks
|
|
1495
|
+
tasks
|
|
1496
|
+
.iter()
|
|
1497
|
+
.any(|t| t.get("id").and_then(|v| v.as_str()) == Some("task_initial")),
|
|
1198
1498
|
"state.tasks must carry the compiled spec's task (id=task_initial); got {tasks:?}"
|
|
1199
1499
|
);
|
|
1200
1500
|
}
|
|
@@ -1228,11 +1528,18 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
1228
1528
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1229
1529
|
let transport = OfflineTransport::new();
|
|
1230
1530
|
let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
|
|
1231
|
-
let workspace = team
|
|
1531
|
+
let workspace = team
|
|
1532
|
+
.parent()
|
|
1533
|
+
.expect("team_workspace(<base>/teamdir) = <base>");
|
|
1232
1534
|
// E5: spec_path in state now points to .team/runtime/<team_key>/, team_dir stays the user role dir.
|
|
1233
1535
|
let spec_path = quick_start_runtime_spec_path(&team);
|
|
1234
1536
|
let (raw, state) = raw_runtime_state(workspace);
|
|
1235
|
-
let keys = state
|
|
1537
|
+
let keys = state
|
|
1538
|
+
.as_object()
|
|
1539
|
+
.expect("state root object")
|
|
1540
|
+
.keys()
|
|
1541
|
+
.cloned()
|
|
1542
|
+
.collect::<Vec<_>>();
|
|
1236
1543
|
assert_eq!(
|
|
1237
1544
|
keys,
|
|
1238
1545
|
vec![
|
|
@@ -1255,7 +1562,10 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
1255
1562
|
truth (no shadow copy on the root that callers could read across teams); \
|
|
1256
1563
|
raw={raw}"
|
|
1257
1564
|
);
|
|
1258
|
-
assert_eq!(
|
|
1565
|
+
assert_eq!(
|
|
1566
|
+
state["spec_path"],
|
|
1567
|
+
json!(spec_path.canonicalize().unwrap().to_string_lossy())
|
|
1568
|
+
);
|
|
1259
1569
|
assert_eq!(state["workspace"], json!(workspace.to_string_lossy()));
|
|
1260
1570
|
assert_eq!(state["team_dir"], json!(team.to_string_lossy()));
|
|
1261
1571
|
assert_eq!(state["session_name"], json!("team-quickteam"));
|
|
@@ -1265,13 +1575,17 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
1265
1575
|
"leader must be copied from compiled spec.leader"
|
|
1266
1576
|
);
|
|
1267
1577
|
assert!(
|
|
1268
|
-
state["agents"]
|
|
1578
|
+
state["agents"]
|
|
1579
|
+
.as_object()
|
|
1580
|
+
.is_some_and(|agents| agents.contains_key("implementer")),
|
|
1269
1581
|
"existing agents value must remain seeded from compiled spec; got {:?}",
|
|
1270
1582
|
state["agents"]
|
|
1271
1583
|
);
|
|
1272
1584
|
assert!(
|
|
1273
1585
|
state["tasks"].as_array().is_some_and(|tasks| {
|
|
1274
|
-
tasks
|
|
1586
|
+
tasks
|
|
1587
|
+
.iter()
|
|
1588
|
+
.any(|task| task.get("id").and_then(|id| id.as_str()) == Some("task_initial"))
|
|
1275
1589
|
}),
|
|
1276
1590
|
"existing tasks value must remain seeded from compiled spec; got {:?}",
|
|
1277
1591
|
state["tasks"]
|
|
@@ -1291,17 +1605,23 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
1291
1605
|
#[serial(env)]
|
|
1292
1606
|
fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
1293
1607
|
const FIXED_SPAWNED_AT: &str = "2026-06-04T00:00:00+00:00";
|
|
1294
|
-
let _clock_guard =
|
|
1295
|
-
EnvVarGuard::set("TEAM_AGENT_TEST_FIXED_SPAWNED_AT", FIXED_SPAWNED_AT);
|
|
1608
|
+
let _clock_guard = EnvVarGuard::set("TEAM_AGENT_TEST_FIXED_SPAWNED_AT", FIXED_SPAWNED_AT);
|
|
1296
1609
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1297
1610
|
let transport = OfflineTransport::new();
|
|
1298
1611
|
let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
|
|
1299
|
-
let workspace = team
|
|
1612
|
+
let workspace = team
|
|
1613
|
+
.parent()
|
|
1614
|
+
.expect("team_workspace(<base>/teamdir) = <base>");
|
|
1300
1615
|
let (raw, state) = raw_runtime_state(workspace);
|
|
1301
1616
|
let agent = state
|
|
1302
1617
|
.pointer("/agents/implementer")
|
|
1303
1618
|
.unwrap_or_else(|| panic!("implementer agent state missing; raw={raw}"));
|
|
1304
|
-
let keys = agent
|
|
1619
|
+
let keys = agent
|
|
1620
|
+
.as_object()
|
|
1621
|
+
.expect("agent state object")
|
|
1622
|
+
.keys()
|
|
1623
|
+
.cloned()
|
|
1624
|
+
.collect::<Vec<_>>();
|
|
1305
1625
|
assert_eq!(
|
|
1306
1626
|
keys,
|
|
1307
1627
|
vec![
|
|
@@ -1335,7 +1655,9 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
1335
1655
|
assert_eq!(agent["window"], json!("implementer"));
|
|
1336
1656
|
assert_eq!(
|
|
1337
1657
|
agent["mcp_config"],
|
|
1338
|
-
json!(workspace
|
|
1658
|
+
json!(workspace
|
|
1659
|
+
.join(".team/runtime/mcp/implementer.json")
|
|
1660
|
+
.to_string_lossy())
|
|
1339
1661
|
);
|
|
1340
1662
|
assert_eq!(
|
|
1341
1663
|
agent["permissions"],
|
|
@@ -1368,7 +1690,9 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
1368
1690
|
fn quick_start_paused_agent_state_is_paused_provider_only_and_not_spawned() {
|
|
1369
1691
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1370
1692
|
let spec_path = compiled_spec_path_with_paused_agent(&team);
|
|
1371
|
-
let workspace = team
|
|
1693
|
+
let workspace = team
|
|
1694
|
+
.parent()
|
|
1695
|
+
.expect("team_workspace(<base>/teamdir) = <base>");
|
|
1372
1696
|
crate::state::persist::save_runtime_state(
|
|
1373
1697
|
workspace,
|
|
1374
1698
|
&json!({
|
|
@@ -1392,7 +1716,12 @@ fn quick_start_paused_agent_state_is_paused_provider_only_and_not_spawned() {
|
|
|
1392
1716
|
let agent = state
|
|
1393
1717
|
.pointer("/agents/implementer")
|
|
1394
1718
|
.unwrap_or_else(|| panic!("implementer agent state missing; raw={raw}"));
|
|
1395
|
-
let keys = agent
|
|
1719
|
+
let keys = agent
|
|
1720
|
+
.as_object()
|
|
1721
|
+
.expect("agent state object")
|
|
1722
|
+
.keys()
|
|
1723
|
+
.cloned()
|
|
1724
|
+
.collect::<Vec<_>>();
|
|
1396
1725
|
assert_eq!(
|
|
1397
1726
|
keys,
|
|
1398
1727
|
vec!["status", "provider"],
|