@team-agent/installer 0.3.14 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +92 -11
- package/crates/team-agent/src/cli/leader.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +255 -37
- package/crates/team-agent/src/cli/send.rs +252 -0
- package/crates/team-agent/src/cli/status.rs +5 -1
- package/crates/team-agent/src/cli/status_port.rs +24 -0
- package/crates/team-agent/src/cli/tests/base.rs +21 -3
- package/crates/team-agent/src/cli/tests/divergence.rs +6 -6
- package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +285 -2
- package/crates/team-agent/src/cli/tests/status_send.rs +25 -0
- package/crates/team-agent/src/cli/types.rs +32 -1
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/fake_worker.rs +32 -145
- package/crates/team-agent/src/leader/start.rs +272 -8
- package/crates/team-agent/src/leader/tests/basics.rs +1 -0
- package/crates/team-agent/src/leader/tests/identity.rs +121 -25
- package/crates/team-agent/src/leader/types.rs +9 -0
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +631 -70
- package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
- package/crates/team-agent/src/lifecycle/restart/common.rs +38 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +79 -4
- package/crates/team-agent/src/lifecycle/tests/core.rs +14 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +731 -8
- package/crates/team-agent/src/lifecycle/types.rs +5 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +153 -3
- package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
- package/crates/team-agent/src/messaging/mod.rs +6 -3
- package/crates/team-agent/src/messaging/results.rs +240 -158
- package/crates/team-agent/src/messaging/send.rs +3 -2
- package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
- package/crates/team-agent/src/os_probe.rs +11 -0
- package/crates/team-agent/src/state/persist.rs +6 -0
- package/crates/team-agent/src/tmux_backend.rs +85 -0
- package/crates/team-agent/src/transport/test_support.rs +46 -1
- package/crates/team-agent/src/transport.rs +27 -0
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +277 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
use crate::transport::test_support::OfflineTransport;
|
|
3
3
|
use serial_test::serial;
|
|
4
|
+
use serde_json::json;
|
|
4
5
|
|
|
5
6
|
const QS_TEAM_MD: &str =
|
|
6
7
|
"---\nname: quickteam\nobjective: Quick start.\nprovider: codex\n---\n\nQuick-start team.\n";
|
|
@@ -18,6 +19,38 @@ pub(super) fn quick_start_team_dir(role_doc: &str) -> PathBuf {
|
|
|
18
19
|
team
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
fn quick_start_team_dir_with_roles(role_docs: &[(&str, &str)]) -> PathBuf {
|
|
23
|
+
let team = temp_ws().join("teamdir");
|
|
24
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
25
|
+
std::fs::write(team.join("TEAM.md"), QS_TEAM_MD).unwrap();
|
|
26
|
+
for (file, role_doc) in role_docs {
|
|
27
|
+
std::fs::write(team.join("agents").join(file), role_doc).unwrap();
|
|
28
|
+
}
|
|
29
|
+
team
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn role_doc(id: &str) -> String {
|
|
33
|
+
format!(
|
|
34
|
+
"---\nname: {id}\nrole: {id} Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{id} worker.\n"
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn layout_pane(session: &str, window: &str, pane: &str, pane_index: u32) -> crate::transport::PaneInfo {
|
|
39
|
+
crate::transport::PaneInfo {
|
|
40
|
+
pane_id: crate::transport::PaneId::new(pane),
|
|
41
|
+
session: crate::transport::SessionName::new(session),
|
|
42
|
+
window_index: None,
|
|
43
|
+
window_name: Some(crate::transport::WindowName::new(window)),
|
|
44
|
+
pane_index: Some(pane_index),
|
|
45
|
+
tty: None,
|
|
46
|
+
current_command: Some("codex".to_string()),
|
|
47
|
+
current_path: None,
|
|
48
|
+
active: pane_index == 0,
|
|
49
|
+
pane_pid: None,
|
|
50
|
+
leader_env: std::collections::BTreeMap::new(),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
/// E5 spec 迁移:spec 不再落 team_dir,而在 <team_workspace>/.team/runtime/<team_key>/。
|
|
22
55
|
/// team_key = team_dir.name(quick_start_team_dir → "teamdir");workspace = team_workspace(team)。
|
|
23
56
|
pub(super) fn quick_start_runtime_spec_path(team: &std::path::Path) -> PathBuf {
|
|
@@ -506,11 +539,20 @@ fn spine_restart_empty_team_is_not_atomic_refusal() {
|
|
|
506
539
|
);
|
|
507
540
|
}
|
|
508
541
|
|
|
509
|
-
//
|
|
510
|
-
// BEFORE any full recompile. Current does a full compile_team with no duplicate-id guard.
|
|
542
|
+
// E25 — add_agent duplicate membership is runtime-state based, not role-doc/spec-file based.
|
|
511
543
|
#[test]
|
|
512
544
|
fn spine_add_agent_rejects_duplicate_agent_id() {
|
|
513
545
|
let team = quick_start_team_dir(QS_VALID_ROLE); // team already has agent 'implementer'
|
|
546
|
+
crate::state::persist::save_runtime_state(
|
|
547
|
+
&team,
|
|
548
|
+
&json!({
|
|
549
|
+
"session_name": "team-quickteam",
|
|
550
|
+
"agents": {
|
|
551
|
+
"implementer": {"status": "running", "provider": "codex", "window": "implementer"}
|
|
552
|
+
}
|
|
553
|
+
}),
|
|
554
|
+
)
|
|
555
|
+
.unwrap();
|
|
514
556
|
let role = team.join("dup-role.md");
|
|
515
557
|
std::fs::write(&role, QS_VALID_ROLE).unwrap(); // a role doc named 'implementer' = duplicate
|
|
516
558
|
let transport = OfflineTransport::new();
|
|
@@ -525,7 +567,7 @@ fn spine_add_agent_rejects_duplicate_agent_id() {
|
|
|
525
567
|
let text = format!("{result:?}");
|
|
526
568
|
assert!(
|
|
527
569
|
text.contains("already exists"),
|
|
528
|
-
"add_agent must reject
|
|
570
|
+
"add_agent must reject an id already present in runtime state; got {text}"
|
|
529
571
|
);
|
|
530
572
|
}
|
|
531
573
|
|
|
@@ -631,7 +673,8 @@ fn red2_restart_empty_workspace_still_reports_missing() {
|
|
|
631
673
|
text.contains("missing spec")
|
|
632
674
|
|| text.to_lowercase().contains("not found")
|
|
633
675
|
|| text.contains("no_local_team_context")
|
|
634
|
-
|| text.to_lowercase().contains("invalid workspace")
|
|
676
|
+
|| text.to_lowercase().contains("invalid workspace")
|
|
677
|
+
|| text.contains("role definitions missing"),
|
|
635
678
|
"RED-2 negative: empty workspace restart must still error (missing/not-found); got {text}"
|
|
636
679
|
);
|
|
637
680
|
let _ = std::fs::remove_dir_all(&ws);
|
|
@@ -1008,6 +1051,263 @@ fn quick_start_with_transport_spawns_workers_not_dry_run() {
|
|
|
1008
1051
|
);
|
|
1009
1052
|
}
|
|
1010
1053
|
|
|
1054
|
+
#[test]
|
|
1055
|
+
fn adaptive_layout_plan_8_workers_is_3_3_2() {
|
|
1056
|
+
let agents = (1..=8)
|
|
1057
|
+
.map(|i| AgentId::new(format!("w{i}")))
|
|
1058
|
+
.collect::<Vec<_>>();
|
|
1059
|
+
let plan = adaptive_layout_plan(&agents, 3);
|
|
1060
|
+
let windows = plan
|
|
1061
|
+
.iter()
|
|
1062
|
+
.map(|placement| placement.layout_window.as_str().to_string())
|
|
1063
|
+
.collect::<Vec<_>>();
|
|
1064
|
+
assert_eq!(
|
|
1065
|
+
windows,
|
|
1066
|
+
vec![
|
|
1067
|
+
"team-w1", "team-w1", "team-w1", "team-w2", "team-w2", "team-w2", "team-w3",
|
|
1068
|
+
"team-w3",
|
|
1069
|
+
]
|
|
1070
|
+
);
|
|
1071
|
+
let pane_counts = ["team-w1", "team-w2", "team-w3"]
|
|
1072
|
+
.into_iter()
|
|
1073
|
+
.map(|window| windows.iter().filter(|actual| actual.as_str() == window).count())
|
|
1074
|
+
.collect::<Vec<_>>();
|
|
1075
|
+
assert_eq!(pane_counts, vec![3, 3, 2]);
|
|
1076
|
+
assert_eq!(
|
|
1077
|
+
plan.iter()
|
|
1078
|
+
.filter(|placement| placement.starts_window)
|
|
1079
|
+
.map(|placement| placement.agent_id.as_str().to_string())
|
|
1080
|
+
.collect::<Vec<_>>(),
|
|
1081
|
+
vec!["w1", "w4", "w7"]
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
#[test]
|
|
1086
|
+
fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
1087
|
+
let roles = (1..=4)
|
|
1088
|
+
.map(|i| {
|
|
1089
|
+
let id = format!("w{i}");
|
|
1090
|
+
(format!("{id}.md"), role_doc(&id))
|
|
1091
|
+
})
|
|
1092
|
+
.collect::<Vec<_>>();
|
|
1093
|
+
let role_refs = roles
|
|
1094
|
+
.iter()
|
|
1095
|
+
.map(|(file, doc)| (file.as_str(), doc.as_str()))
|
|
1096
|
+
.collect::<Vec<_>>();
|
|
1097
|
+
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
1098
|
+
let workspace = team.parent().expect("team_workspace(team_dir) = parent");
|
|
1099
|
+
seed_healthy_coordinator(workspace);
|
|
1100
|
+
let transport = OfflineTransport::new();
|
|
1101
|
+
|
|
1102
|
+
let report = quick_start_with_transport(&team, None, true, true, None, &transport)
|
|
1103
|
+
.expect("quick_start_with_transport must reach Ready");
|
|
1104
|
+
|
|
1105
|
+
let (launch, attach_commands, display_backend) = match report {
|
|
1106
|
+
QuickStartReport::Ready { launch, attach_commands, display_backend, .. } => {
|
|
1107
|
+
(*launch, attach_commands, display_backend)
|
|
1108
|
+
}
|
|
1109
|
+
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1110
|
+
};
|
|
1111
|
+
assert_eq!(display_backend, "adaptive");
|
|
1112
|
+
assert_eq!(
|
|
1113
|
+
transport
|
|
1114
|
+
.spawn_records()
|
|
1115
|
+
.iter()
|
|
1116
|
+
.map(|(kind, _)| kind.as_str())
|
|
1117
|
+
.collect::<Vec<_>>(),
|
|
1118
|
+
vec!["spawn_first", "spawn_split", "spawn_split", "spawn_into"],
|
|
1119
|
+
"4 default-adaptive workers should create team-w1 with 3 panes, then team-w2"
|
|
1120
|
+
);
|
|
1121
|
+
assert_eq!(
|
|
1122
|
+
launch
|
|
1123
|
+
.started
|
|
1124
|
+
.iter()
|
|
1125
|
+
.map(|started| {
|
|
1126
|
+
(
|
|
1127
|
+
started.agent_id.as_str().to_string(),
|
|
1128
|
+
started.layout_window.as_ref().map(|w| w.as_str().to_string()),
|
|
1129
|
+
started.pane_index,
|
|
1130
|
+
)
|
|
1131
|
+
})
|
|
1132
|
+
.collect::<Vec<_>>(),
|
|
1133
|
+
vec![
|
|
1134
|
+
("w1".to_string(), Some("team-w1".to_string()), Some(0)),
|
|
1135
|
+
("w2".to_string(), Some("team-w1".to_string()), Some(1)),
|
|
1136
|
+
("w3".to_string(), Some("team-w1".to_string()), Some(2)),
|
|
1137
|
+
("w4".to_string(), Some("team-w2".to_string()), Some(0)),
|
|
1138
|
+
]
|
|
1139
|
+
);
|
|
1140
|
+
assert!(
|
|
1141
|
+
attach_commands.iter().any(|cmd| cmd.contains(":team-w1")),
|
|
1142
|
+
"attach commands must include the first layout window: {attach_commands:?}"
|
|
1143
|
+
);
|
|
1144
|
+
assert!(
|
|
1145
|
+
attach_commands.iter().any(|cmd| cmd.contains(":team-w2")),
|
|
1146
|
+
"attach commands must include the second layout window: {attach_commands:?}"
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
let (_raw, state) = raw_runtime_state(workspace);
|
|
1150
|
+
assert_eq!(state["display_backend"], json!("adaptive"));
|
|
1151
|
+
assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("team-w1")));
|
|
1152
|
+
assert_eq!(state.pointer("/agents/w1/layout_window"), Some(&json!("team-w1")));
|
|
1153
|
+
assert_eq!(state.pointer("/agents/w1/pane_index"), Some(&json!(0)));
|
|
1154
|
+
assert_eq!(
|
|
1155
|
+
state.pointer("/agents/w1/display/backend"),
|
|
1156
|
+
Some(&json!("adaptive"))
|
|
1157
|
+
);
|
|
1158
|
+
assert_eq!(
|
|
1159
|
+
state.pointer("/agents/w4/layout_window"),
|
|
1160
|
+
Some(&json!("team-w2"))
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
#[test]
|
|
1165
|
+
#[serial(env)]
|
|
1166
|
+
fn quick_start_tmux_backend_prefers_absolute_tmux_env_endpoint() {
|
|
1167
|
+
let leader_socket = "/tmp/team-agent-layout-leader-socket";
|
|
1168
|
+
let _tmux = EnvVarGuard::set("TMUX", &format!("{leader_socket},123,0"));
|
|
1169
|
+
let backend = quick_start_tmux_backend(std::path::Path::new("/tmp/layout-workspace"));
|
|
1170
|
+
|
|
1171
|
+
assert_eq!(
|
|
1172
|
+
crate::transport::Transport::tmux_endpoint(&backend).as_deref(),
|
|
1173
|
+
Some(leader_socket),
|
|
1174
|
+
"quick-start from inside tmux must use the caller's full $TMUX socket endpoint"
|
|
1175
|
+
);
|
|
1176
|
+
assert_eq!(
|
|
1177
|
+
selected_tmux_socket_source(&backend, std::path::Path::new("/tmp/layout-workspace")),
|
|
1178
|
+
Some("leader_env")
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
#[test]
|
|
1183
|
+
fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
|
|
1184
|
+
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1185
|
+
let workspace = team.parent().expect("team_workspace(team_dir) = parent");
|
|
1186
|
+
seed_healthy_coordinator(workspace);
|
|
1187
|
+
let endpoint = "/tmp/team-agent-layout-selected-socket";
|
|
1188
|
+
let transport = OfflineTransport::new().with_tmux_endpoint(endpoint);
|
|
1189
|
+
|
|
1190
|
+
let report = quick_start_with_transport(&team, None, true, true, None, &transport)
|
|
1191
|
+
.expect("quick_start_with_transport must reach Ready");
|
|
1192
|
+
let attach_commands = match report {
|
|
1193
|
+
QuickStartReport::Ready { attach_commands, .. } => attach_commands,
|
|
1194
|
+
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
assert!(
|
|
1198
|
+
attach_commands
|
|
1199
|
+
.iter()
|
|
1200
|
+
.any(|cmd| cmd.contains(&format!("tmux -S {endpoint} attach -t "))),
|
|
1201
|
+
"attach commands must use the selected socket endpoint: {attach_commands:?}"
|
|
1202
|
+
);
|
|
1203
|
+
let (_raw, state) = raw_runtime_state(workspace);
|
|
1204
|
+
assert_eq!(state["tmux_endpoint"], json!(endpoint));
|
|
1205
|
+
assert_eq!(state["tmux_socket"], json!(endpoint));
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
#[test]
|
|
1209
|
+
fn quick_start_preserves_managed_leader_topology_and_emits_leader_attach_command() {
|
|
1210
|
+
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1211
|
+
let workspace = team.parent().expect("team_workspace(team_dir) = parent");
|
|
1212
|
+
seed_healthy_coordinator(workspace);
|
|
1213
|
+
crate::state::persist::save_runtime_state(
|
|
1214
|
+
workspace,
|
|
1215
|
+
&json!({
|
|
1216
|
+
"active_team_key": "teamdir",
|
|
1217
|
+
"session_name": "team-teamdir",
|
|
1218
|
+
"is_external_leader": false,
|
|
1219
|
+
"leader_client": {"diagnostic_only": true, "attach_mode": "attach-session"},
|
|
1220
|
+
"leader_receiver": {"mode": "direct_tmux", "status": "attached", "pane_id": "%42"},
|
|
1221
|
+
"team_owner": {"pane_id": "%42", "owner_epoch": 1},
|
|
1222
|
+
"agents": {},
|
|
1223
|
+
"teams": {
|
|
1224
|
+
"teamdir": {
|
|
1225
|
+
"session_name": "team-teamdir",
|
|
1226
|
+
"is_external_leader": false,
|
|
1227
|
+
"leader_client": {"diagnostic_only": true, "attach_mode": "attach-session"},
|
|
1228
|
+
"agents": {}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}),
|
|
1232
|
+
)
|
|
1233
|
+
.expect("seed managed leader state");
|
|
1234
|
+
let transport = OfflineTransport::new();
|
|
1235
|
+
|
|
1236
|
+
let report = quick_start_with_transport(&team, None, true, true, None, &transport)
|
|
1237
|
+
.expect("quick_start_with_transport must reach Ready");
|
|
1238
|
+
let attach_commands = match report {
|
|
1239
|
+
QuickStartReport::Ready { attach_commands, .. } => attach_commands,
|
|
1240
|
+
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
assert!(
|
|
1244
|
+
attach_commands.iter().any(|cmd| cmd.contains(":leader")),
|
|
1245
|
+
"managed topology quick-start output must include the leader window attach command: {attach_commands:?}"
|
|
1246
|
+
);
|
|
1247
|
+
let (_raw, state) = raw_runtime_state(workspace);
|
|
1248
|
+
assert_eq!(state["is_external_leader"], json!(false));
|
|
1249
|
+
assert_eq!(state["leader_client"]["diagnostic_only"], json!(true));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
#[test]
|
|
1253
|
+
fn attach_window_names_for_state_agents_include_managed_leader_and_layout_windows() {
|
|
1254
|
+
let state = json!({
|
|
1255
|
+
"is_external_leader": false,
|
|
1256
|
+
"agents": {
|
|
1257
|
+
"w1": {"layout_window": "team-w1", "window": "w1"},
|
|
1258
|
+
"w2": {"layout_window": "team-w1", "window": "w2"},
|
|
1259
|
+
"w4": {"layout_window": "team-w2", "window": "w4"}
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
let windows = crate::lifecycle::launch::attach_window_names_for_state_agents(
|
|
1264
|
+
&state,
|
|
1265
|
+
["w1", "w2", "w4"].into_iter(),
|
|
1266
|
+
);
|
|
1267
|
+
|
|
1268
|
+
assert_eq!(windows, vec!["leader", "team-w1", "team-w2"]);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
#[test]
|
|
1272
|
+
fn quick_start_no_display_keeps_one_window_per_agent() {
|
|
1273
|
+
let roles = ["w1", "w2"]
|
|
1274
|
+
.into_iter()
|
|
1275
|
+
.map(|id| (format!("{id}.md"), role_doc(id)))
|
|
1276
|
+
.collect::<Vec<_>>();
|
|
1277
|
+
let role_refs = roles
|
|
1278
|
+
.iter()
|
|
1279
|
+
.map(|(file, doc)| (file.as_str(), doc.as_str()))
|
|
1280
|
+
.collect::<Vec<_>>();
|
|
1281
|
+
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
1282
|
+
let workspace = team.parent().expect("team_workspace(team_dir) = parent");
|
|
1283
|
+
seed_healthy_coordinator(workspace);
|
|
1284
|
+
let transport = OfflineTransport::new();
|
|
1285
|
+
|
|
1286
|
+
let report = quick_start_with_transport_in_workspace_with_display(
|
|
1287
|
+
workspace, &team, None, true, true, None, &transport, false,
|
|
1288
|
+
)
|
|
1289
|
+
.expect("quick_start_with_transport must reach Ready");
|
|
1290
|
+
|
|
1291
|
+
let display_backend = match report {
|
|
1292
|
+
QuickStartReport::Ready { display_backend, .. } => display_backend,
|
|
1293
|
+
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1294
|
+
};
|
|
1295
|
+
assert_eq!(display_backend, "none");
|
|
1296
|
+
assert_eq!(
|
|
1297
|
+
transport
|
|
1298
|
+
.spawn_records()
|
|
1299
|
+
.iter()
|
|
1300
|
+
.map(|(kind, _)| kind.as_str())
|
|
1301
|
+
.collect::<Vec<_>>(),
|
|
1302
|
+
vec!["spawn_first", "spawn_into"],
|
|
1303
|
+
"--no-display must use the legacy one-worker-window spawn path"
|
|
1304
|
+
);
|
|
1305
|
+
let (_raw, state) = raw_runtime_state(workspace);
|
|
1306
|
+
assert_eq!(state["display_backend"], json!("none"));
|
|
1307
|
+
assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("w1")));
|
|
1308
|
+
assert_eq!(state.pointer("/agents/w1/layout_window"), None);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1011
1311
|
// REAL-MACHINE residency boundary (acceptance framework): the PUBLIC quick_start (real TmuxBackend +
|
|
1012
1312
|
// real start_coordinator) on a fresh ws must leave a LIVE tmux session AND a ps-verifiable resident
|
|
1013
1313
|
// coordinator daemon (start_coordinator -> live pid). The framework verifies residency via ps; here we
|
|
@@ -1453,6 +1753,419 @@ fn add_agent_with_transport_spawns_new_worker_not_stub() {
|
|
|
1453
1753
|
);
|
|
1454
1754
|
}
|
|
1455
1755
|
|
|
1756
|
+
#[test]
|
|
1757
|
+
fn add_agent_role_doc_exists_runtime_missing_succeeds() {
|
|
1758
|
+
let team = temp_ws().join("add_role_doc_team");
|
|
1759
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
1760
|
+
std::fs::write(
|
|
1761
|
+
team.join("TEAM.md"),
|
|
1762
|
+
"---\nname: addroledoc\nobjective: Add role-doc probe.\nprovider: codex\n---\n\nteam.\n",
|
|
1763
|
+
)
|
|
1764
|
+
.unwrap();
|
|
1765
|
+
std::fs::write(team.join("agents").join("implementer.md"), QS_VALID_ROLE).unwrap();
|
|
1766
|
+
let role_file = team.join("agents").join("worker2.md");
|
|
1767
|
+
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
1768
|
+
crate::state::persist::save_runtime_state(
|
|
1769
|
+
&team,
|
|
1770
|
+
&json!({
|
|
1771
|
+
"session_name": "team-addroledoc",
|
|
1772
|
+
"agents": {
|
|
1773
|
+
"implementer": {"status": "running", "provider": "codex", "window": "implementer"}
|
|
1774
|
+
}
|
|
1775
|
+
}),
|
|
1776
|
+
)
|
|
1777
|
+
.unwrap();
|
|
1778
|
+
seed_healthy_coordinator(&team);
|
|
1779
|
+
let transport = OfflineTransport::new();
|
|
1780
|
+
|
|
1781
|
+
let result = add_agent_with_transport(
|
|
1782
|
+
&team,
|
|
1783
|
+
&AgentId::new("worker2"),
|
|
1784
|
+
&role_file,
|
|
1785
|
+
false,
|
|
1786
|
+
None,
|
|
1787
|
+
&transport,
|
|
1788
|
+
);
|
|
1789
|
+
|
|
1790
|
+
assert!(
|
|
1791
|
+
result.is_ok(),
|
|
1792
|
+
"a prepared agents/<id>.md role doc is not membership; runtime state is authoritative: {result:?}"
|
|
1793
|
+
);
|
|
1794
|
+
let state = crate::state::persist::load_runtime_state(&team).expect("load state");
|
|
1795
|
+
assert!(
|
|
1796
|
+
state.pointer("/agents/worker2").is_some(),
|
|
1797
|
+
"add-agent must add worker2 to runtime state; state={state}"
|
|
1798
|
+
);
|
|
1799
|
+
assert_eq!(
|
|
1800
|
+
transport.spawn_records().len(),
|
|
1801
|
+
1,
|
|
1802
|
+
"add-agent must spawn exactly the new worker"
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
#[test]
|
|
1807
|
+
fn start_agent_runtime_missing_role_doc_exists_no_side_effect() {
|
|
1808
|
+
let team = temp_ws().join("start_missing_team");
|
|
1809
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
1810
|
+
std::fs::write(
|
|
1811
|
+
team.join("TEAM.md"),
|
|
1812
|
+
"---\nname: startmissing\nobjective: Start missing probe.\nprovider: codex\n---\n\nteam.\n",
|
|
1813
|
+
)
|
|
1814
|
+
.unwrap();
|
|
1815
|
+
std::fs::write(team.join("agents").join("reviewer.md"), DELEG_ROLE_WORKER2).unwrap();
|
|
1816
|
+
crate::state::persist::save_runtime_state(
|
|
1817
|
+
&team,
|
|
1818
|
+
&json!({
|
|
1819
|
+
"session_name": "team-startmissing",
|
|
1820
|
+
"agents": {
|
|
1821
|
+
"implementer": {"status": "running", "provider": "codex", "window": "implementer"}
|
|
1822
|
+
}
|
|
1823
|
+
}),
|
|
1824
|
+
)
|
|
1825
|
+
.unwrap();
|
|
1826
|
+
let transport = OfflineTransport::new();
|
|
1827
|
+
|
|
1828
|
+
let result = start_agent_with_transport(
|
|
1829
|
+
&team,
|
|
1830
|
+
&AgentId::new("reviewer"),
|
|
1831
|
+
false,
|
|
1832
|
+
false,
|
|
1833
|
+
true,
|
|
1834
|
+
None,
|
|
1835
|
+
&transport,
|
|
1836
|
+
);
|
|
1837
|
+
|
|
1838
|
+
let text = format!("{result:?}");
|
|
1839
|
+
assert!(
|
|
1840
|
+
text.contains("not found"),
|
|
1841
|
+
"start-agent must refuse runtime-missing agents before side effects; got {text}"
|
|
1842
|
+
);
|
|
1843
|
+
assert!(
|
|
1844
|
+
transport.spawn_records().is_empty(),
|
|
1845
|
+
"runtime-missing start-agent must not spawn a pane"
|
|
1846
|
+
);
|
|
1847
|
+
assert!(
|
|
1848
|
+
!team
|
|
1849
|
+
.join(".team")
|
|
1850
|
+
.join("runtime")
|
|
1851
|
+
.join("mcp")
|
|
1852
|
+
.join("reviewer.json")
|
|
1853
|
+
.exists(),
|
|
1854
|
+
"runtime-missing start-agent must not write worker MCP config"
|
|
1855
|
+
);
|
|
1856
|
+
let events = crate::event_log::EventLog::new(&team)
|
|
1857
|
+
.tail(20)
|
|
1858
|
+
.unwrap_or_default();
|
|
1859
|
+
assert!(
|
|
1860
|
+
!events.iter().any(|event| {
|
|
1861
|
+
event.get("event").and_then(|v| v.as_str()) == Some("start_agent.agent_start")
|
|
1862
|
+
}),
|
|
1863
|
+
"runtime-missing start-agent must not emit start_agent.agent_start; events={events:?}"
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
#[test]
|
|
1868
|
+
fn restart_allow_fresh_copilot_persists_expected_session_id() {
|
|
1869
|
+
let ws = temp_ws().join("restartcopilot");
|
|
1870
|
+
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
1871
|
+
std::fs::write(
|
|
1872
|
+
ws.join("TEAM.md"),
|
|
1873
|
+
"---\nname: restartcopilot\nobjective: Restart copilot probe.\nprovider: copilot\n---\n\nteam.\n",
|
|
1874
|
+
)
|
|
1875
|
+
.unwrap();
|
|
1876
|
+
std::fs::write(
|
|
1877
|
+
ws.join("agents").join("alpha.md"),
|
|
1878
|
+
"---\nname: alpha\nrole: Alpha Worker\nprovider: copilot\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nAlpha.\n",
|
|
1879
|
+
)
|
|
1880
|
+
.unwrap();
|
|
1881
|
+
let spec = crate::compiler::compile_team(&ws).expect("compile copilot team");
|
|
1882
|
+
std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
|
|
1883
|
+
crate::state::persist::save_runtime_state(
|
|
1884
|
+
&ws,
|
|
1885
|
+
&json!({
|
|
1886
|
+
"session_name": "team-restartcopilot",
|
|
1887
|
+
"agents": {
|
|
1888
|
+
"alpha": {
|
|
1889
|
+
"status": "running",
|
|
1890
|
+
"provider": "copilot",
|
|
1891
|
+
"window": "alpha",
|
|
1892
|
+
"first_send_at": "2026-05-27T10:00:00+00:00",
|
|
1893
|
+
"session_id": null,
|
|
1894
|
+
"rollout_path": "/tmp/stale-copilot-rollout.jsonl",
|
|
1895
|
+
"captured_at": "2026-05-27T10:00:00+00:00",
|
|
1896
|
+
"captured_via": "stale",
|
|
1897
|
+
"attribution_confidence": "stale"
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}),
|
|
1901
|
+
)
|
|
1902
|
+
.unwrap();
|
|
1903
|
+
seed_healthy_coordinator(&ws);
|
|
1904
|
+
let transport = OfflineTransport::new();
|
|
1905
|
+
|
|
1906
|
+
let result = restart_with_transport(&ws, true, None, &transport);
|
|
1907
|
+
|
|
1908
|
+
assert!(
|
|
1909
|
+
matches!(result, Ok(RestartReport::Restarted { .. })),
|
|
1910
|
+
"allow-fresh restart should fresh-spawn the copilot worker; got {result:?}"
|
|
1911
|
+
);
|
|
1912
|
+
let recorded = transport.spawn_records();
|
|
1913
|
+
assert_eq!(
|
|
1914
|
+
recorded.len(),
|
|
1915
|
+
1,
|
|
1916
|
+
"expected one copilot spawn; got {recorded:?}"
|
|
1917
|
+
);
|
|
1918
|
+
let session_arg = recorded[0]
|
|
1919
|
+
.1
|
|
1920
|
+
.windows(2)
|
|
1921
|
+
.find_map(|pair| (pair[0] == "--session-id").then(|| pair[1].clone()))
|
|
1922
|
+
.expect("copilot fresh spawn argv must include --session-id");
|
|
1923
|
+
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
1924
|
+
let agent = state.pointer("/agents/alpha").expect("alpha state");
|
|
1925
|
+
assert_eq!(
|
|
1926
|
+
agent.get("session_id").and_then(serde_json::Value::as_str),
|
|
1927
|
+
Some(session_arg.as_str()),
|
|
1928
|
+
"fresh restart must promote expected_session_id to persisted session_id"
|
|
1929
|
+
);
|
|
1930
|
+
assert_eq!(
|
|
1931
|
+
agent
|
|
1932
|
+
.get("_pending_session_id")
|
|
1933
|
+
.and_then(serde_json::Value::as_str),
|
|
1934
|
+
Some(session_arg.as_str()),
|
|
1935
|
+
"fresh restart must retain pending session audit state"
|
|
1936
|
+
);
|
|
1937
|
+
for stale in [
|
|
1938
|
+
"rollout_path",
|
|
1939
|
+
"captured_at",
|
|
1940
|
+
"captured_via",
|
|
1941
|
+
"attribution_confidence",
|
|
1942
|
+
] {
|
|
1943
|
+
assert_eq!(
|
|
1944
|
+
agent.get(stale),
|
|
1945
|
+
Some(&serde_json::Value::Null),
|
|
1946
|
+
"fresh restart must clear stale capture field {stale}: {agent}"
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
let plan = crate::lifecycle::restart::classify_restart_plan(&state, false)
|
|
1950
|
+
.expect("classify second restart");
|
|
1951
|
+
assert!(
|
|
1952
|
+
plan.unresumable
|
|
1953
|
+
.iter()
|
|
1954
|
+
.all(|worker| worker.reason != "no_persisted_session_id"),
|
|
1955
|
+
"second restart must not loop as no_persisted_session_id; unresumable={:?}",
|
|
1956
|
+
plan.unresumable
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
#[test]
|
|
1961
|
+
fn add_agent_adaptive_splits_last_non_full_layout_window() {
|
|
1962
|
+
let roles = [("w1.md", role_doc("w1")), ("w2.md", role_doc("w2"))];
|
|
1963
|
+
let role_refs = roles
|
|
1964
|
+
.iter()
|
|
1965
|
+
.map(|(file, doc)| (*file, doc.as_str()))
|
|
1966
|
+
.collect::<Vec<_>>();
|
|
1967
|
+
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
1968
|
+
let session = "team-layout-add";
|
|
1969
|
+
crate::state::persist::save_runtime_state(
|
|
1970
|
+
&team,
|
|
1971
|
+
&json!({
|
|
1972
|
+
"session_name": session,
|
|
1973
|
+
"display_backend": "adaptive",
|
|
1974
|
+
"active_team_key": "teamdir",
|
|
1975
|
+
"agents": {
|
|
1976
|
+
"w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
|
|
1977
|
+
"w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"}
|
|
1978
|
+
}
|
|
1979
|
+
}),
|
|
1980
|
+
)
|
|
1981
|
+
.unwrap();
|
|
1982
|
+
seed_healthy_coordinator(&team);
|
|
1983
|
+
let role_file = team.join("w3-role.md");
|
|
1984
|
+
std::fs::write(&role_file, role_doc("w3")).unwrap();
|
|
1985
|
+
let transport = OfflineTransport::new()
|
|
1986
|
+
.with_session_present(true)
|
|
1987
|
+
.with_targets(vec![
|
|
1988
|
+
layout_pane(session, "team-w1", "%1", 0),
|
|
1989
|
+
layout_pane(session, "team-w1", "%2", 1),
|
|
1990
|
+
])
|
|
1991
|
+
.with_pane_presence("%1", true)
|
|
1992
|
+
.with_pane_presence("%2", true);
|
|
1993
|
+
|
|
1994
|
+
add_agent_with_transport(
|
|
1995
|
+
&team,
|
|
1996
|
+
&AgentId::new("w3"),
|
|
1997
|
+
&role_file,
|
|
1998
|
+
true,
|
|
1999
|
+
None,
|
|
2000
|
+
&transport,
|
|
2001
|
+
)
|
|
2002
|
+
.expect("adaptive add-agent should start");
|
|
2003
|
+
|
|
2004
|
+
assert_eq!(
|
|
2005
|
+
transport.spawn_window_records().last(),
|
|
2006
|
+
Some(&(String::from("spawn_split"), String::from("team-w1"))),
|
|
2007
|
+
"last non-full layout window should be split"
|
|
2008
|
+
);
|
|
2009
|
+
let (_raw, state) = raw_runtime_state(&team);
|
|
2010
|
+
assert_eq!(state.pointer("/agents/w3/layout_window"), Some(&json!("team-w1")));
|
|
2011
|
+
assert_eq!(state.pointer("/agents/w3/pane_index"), Some(&json!(2)));
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
#[test]
|
|
2015
|
+
fn add_agent_adaptive_creates_suffix_window_when_last_layout_full_and_name_collides() {
|
|
2016
|
+
let roles = [
|
|
2017
|
+
("w1.md", role_doc("w1")),
|
|
2018
|
+
("w2.md", role_doc("w2")),
|
|
2019
|
+
("w3.md", role_doc("w3")),
|
|
2020
|
+
];
|
|
2021
|
+
let role_refs = roles
|
|
2022
|
+
.iter()
|
|
2023
|
+
.map(|(file, doc)| (*file, doc.as_str()))
|
|
2024
|
+
.collect::<Vec<_>>();
|
|
2025
|
+
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
2026
|
+
let session = "team-layout-add-full";
|
|
2027
|
+
crate::state::persist::save_runtime_state(
|
|
2028
|
+
&team,
|
|
2029
|
+
&json!({
|
|
2030
|
+
"session_name": session,
|
|
2031
|
+
"display_backend": "adaptive",
|
|
2032
|
+
"active_team_key": "teamdir",
|
|
2033
|
+
"agents": {
|
|
2034
|
+
"w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
|
|
2035
|
+
"w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"},
|
|
2036
|
+
"w3": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 2, "pane_id": "%3"}
|
|
2037
|
+
}
|
|
2038
|
+
}),
|
|
2039
|
+
)
|
|
2040
|
+
.unwrap();
|
|
2041
|
+
seed_healthy_coordinator(&team);
|
|
2042
|
+
let role_file = team.join("w4-role.md");
|
|
2043
|
+
std::fs::write(&role_file, role_doc("w4")).unwrap();
|
|
2044
|
+
let transport = OfflineTransport::new()
|
|
2045
|
+
.with_session_present(true)
|
|
2046
|
+
.with_targets(vec![
|
|
2047
|
+
layout_pane(session, "team-w1", "%1", 0),
|
|
2048
|
+
layout_pane(session, "team-w1", "%2", 1),
|
|
2049
|
+
layout_pane(session, "team-w1", "%3", 2),
|
|
2050
|
+
layout_pane(session, "team-w2", "%9", 0),
|
|
2051
|
+
])
|
|
2052
|
+
.with_pane_presence("%1", true)
|
|
2053
|
+
.with_pane_presence("%2", true)
|
|
2054
|
+
.with_pane_presence("%3", true);
|
|
2055
|
+
|
|
2056
|
+
add_agent_with_transport(
|
|
2057
|
+
&team,
|
|
2058
|
+
&AgentId::new("w4"),
|
|
2059
|
+
&role_file,
|
|
2060
|
+
true,
|
|
2061
|
+
None,
|
|
2062
|
+
&transport,
|
|
2063
|
+
)
|
|
2064
|
+
.expect("adaptive add-agent should start");
|
|
2065
|
+
|
|
2066
|
+
assert_eq!(
|
|
2067
|
+
transport.spawn_window_records().last(),
|
|
2068
|
+
Some(&(String::from("spawn_into"), String::from("team-w2-2"))),
|
|
2069
|
+
"full last layout window should create next window with collision suffix"
|
|
2070
|
+
);
|
|
2071
|
+
let (_raw, state) = raw_runtime_state(&team);
|
|
2072
|
+
assert_eq!(state.pointer("/agents/w4/layout_window"), Some(&json!("team-w2-2")));
|
|
2073
|
+
assert_eq!(state.pointer("/agents/w4/pane_index"), Some(&json!(0)));
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
#[test]
|
|
2077
|
+
fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
|
|
2078
|
+
let roles = [("w1.md", role_doc("w1")), ("w2.md", role_doc("w2"))];
|
|
2079
|
+
let role_refs = roles
|
|
2080
|
+
.iter()
|
|
2081
|
+
.map(|(file, doc)| (*file, doc.as_str()))
|
|
2082
|
+
.collect::<Vec<_>>();
|
|
2083
|
+
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
2084
|
+
let spec = crate::compiler::compile_team(&team).unwrap();
|
|
2085
|
+
std::fs::write(team.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
|
|
2086
|
+
let session = "team-layout-stop";
|
|
2087
|
+
crate::state::persist::save_runtime_state(
|
|
2088
|
+
&team,
|
|
2089
|
+
&json!({
|
|
2090
|
+
"session_name": session,
|
|
2091
|
+
"display_backend": "adaptive",
|
|
2092
|
+
"agents": {
|
|
2093
|
+
"w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
|
|
2094
|
+
"w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"}
|
|
2095
|
+
}
|
|
2096
|
+
}),
|
|
2097
|
+
)
|
|
2098
|
+
.unwrap();
|
|
2099
|
+
let transport = OfflineTransport::new()
|
|
2100
|
+
.with_targets(vec![
|
|
2101
|
+
layout_pane(session, "team-w1", "%1", 0),
|
|
2102
|
+
layout_pane(session, "team-w1", "%2", 1),
|
|
2103
|
+
])
|
|
2104
|
+
.with_pane_presence("%2", true);
|
|
2105
|
+
|
|
2106
|
+
let report =
|
|
2107
|
+
stop_agent_with_transport(&team, &AgentId::new("w2"), None, &transport).unwrap();
|
|
2108
|
+
|
|
2109
|
+
assert!(report.stopped);
|
|
2110
|
+
assert_eq!(report.target, "%2");
|
|
2111
|
+
assert!(
|
|
2112
|
+
transport.calls().iter().any(|call| *call == "kill_pane"),
|
|
2113
|
+
"adaptive stop must kill only the target pane"
|
|
2114
|
+
);
|
|
2115
|
+
assert!(
|
|
2116
|
+
!transport.calls().iter().any(|call| *call == "kill_window"),
|
|
2117
|
+
"adaptive stop must not kill the shared layout window"
|
|
2118
|
+
);
|
|
2119
|
+
let (_raw, state) = raw_runtime_state(&team);
|
|
2120
|
+
assert_eq!(state.pointer("/agents/w1/pane_id"), Some(&json!("%1")));
|
|
2121
|
+
assert_eq!(state.pointer("/agents/w1/layout_window"), Some(&json!("team-w1")));
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
#[test]
|
|
2125
|
+
fn start_agent_adaptive_restarts_missing_pane_in_existing_layout_window() {
|
|
2126
|
+
let ws = temp_ws();
|
|
2127
|
+
crate::state::persist::save_runtime_state(
|
|
2128
|
+
&ws,
|
|
2129
|
+
&json!({
|
|
2130
|
+
"session_name": "team-layout-restart",
|
|
2131
|
+
"display_backend": "adaptive",
|
|
2132
|
+
"agents": {
|
|
2133
|
+
"w1": {"status": "running", "provider": "codex", "role": "w1", "model": "gpt-5.5", "auth_mode": "subscription", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
|
|
2134
|
+
"w2": {"status": "running", "provider": "codex", "role": "w2", "model": "gpt-5.5", "auth_mode": "subscription", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"}
|
|
2135
|
+
}
|
|
2136
|
+
}),
|
|
2137
|
+
)
|
|
2138
|
+
.unwrap();
|
|
2139
|
+
seed_healthy_coordinator(&ws);
|
|
2140
|
+
let transport = OfflineTransport::new()
|
|
2141
|
+
.with_session_present(true)
|
|
2142
|
+
.with_targets(vec![layout_pane("team-layout-restart", "team-w1", "%2", 1)])
|
|
2143
|
+
.with_pane_presence("%1", false)
|
|
2144
|
+
.with_pane_presence("%2", true);
|
|
2145
|
+
|
|
2146
|
+
let outcome = start_agent_with_transport(
|
|
2147
|
+
&ws,
|
|
2148
|
+
&AgentId::new("w1"),
|
|
2149
|
+
false,
|
|
2150
|
+
true,
|
|
2151
|
+
true,
|
|
2152
|
+
None,
|
|
2153
|
+
&transport,
|
|
2154
|
+
)
|
|
2155
|
+
.expect("adaptive restart should spawn");
|
|
2156
|
+
|
|
2157
|
+
assert!(matches!(outcome, StartAgentOutcome::Running { .. }));
|
|
2158
|
+
assert_eq!(
|
|
2159
|
+
transport.spawn_window_records().last(),
|
|
2160
|
+
Some(&(String::from("spawn_split"), String::from("team-w1"))),
|
|
2161
|
+
"missing worker pane should be rebuilt in its existing layout window"
|
|
2162
|
+
);
|
|
2163
|
+
let (_raw, state) = raw_runtime_state(&ws);
|
|
2164
|
+
assert_eq!(state.pointer("/agents/w1/layout_window"), Some(&json!("team-w1")));
|
|
2165
|
+
assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("team-w1")));
|
|
2166
|
+
assert_eq!(state.pointer("/agents/w2/pane_id"), Some(&json!("%2")));
|
|
2167
|
+
}
|
|
2168
|
+
|
|
1456
2169
|
// ═════════════════════════════════════════════════════════════════════════
|
|
1457
2170
|
// WAVE 2 · LANE A — agent-lifecycle byte-parity contracts. stop_agent / reset_agent / remove_agent /
|
|
1458
2171
|
// fork_agent are stubs (OwnerRefused / RequirementUnmet / "session_id missing"). These RED contracts
|
|
@@ -1592,8 +2305,8 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
1592
2305
|
);
|
|
1593
2306
|
assert_eq!(
|
|
1594
2307
|
state["display_backend"],
|
|
1595
|
-
json!("
|
|
1596
|
-
"
|
|
2308
|
+
json!("adaptive"),
|
|
2309
|
+
"adaptive layout directive: resolve_display_backend(None, source='launch') defaults to adaptive"
|
|
1597
2310
|
);
|
|
1598
2311
|
}
|
|
1599
2312
|
|
|
@@ -1640,11 +2353,15 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
1640
2353
|
"captured_at",
|
|
1641
2354
|
"captured_via",
|
|
1642
2355
|
"attribution_confidence",
|
|
2356
|
+
"layout_window",
|
|
2357
|
+
"layout_index",
|
|
2358
|
+
"pane_index",
|
|
2359
|
+
"display",
|
|
1643
2360
|
"spawn_cwd",
|
|
1644
2361
|
"spawned_at",
|
|
1645
2362
|
"pane_id",
|
|
1646
2363
|
],
|
|
1647
|
-
"running agent state key order must
|
|
2364
|
+
"running agent state key order must include adaptive layout fields; raw={raw}"
|
|
1648
2365
|
);
|
|
1649
2366
|
assert_eq!(agent["status"], json!("running"));
|
|
1650
2367
|
assert_eq!(agent["provider"], json!("codex"));
|
|
@@ -1652,7 +2369,7 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
1652
2369
|
assert_eq!(agent["model"], json!("gpt-5.5"));
|
|
1653
2370
|
assert_eq!(agent["auth_mode"], json!("subscription"));
|
|
1654
2371
|
assert!(agent["profile"].is_null());
|
|
1655
|
-
assert_eq!(agent["window"], json!("
|
|
2372
|
+
assert_eq!(agent["window"], json!("team-w1"));
|
|
1656
2373
|
assert_eq!(
|
|
1657
2374
|
agent["mcp_config"],
|
|
1658
2375
|
json!(workspace
|
|
@@ -1674,6 +2391,12 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
1674
2391
|
assert!(agent["captured_at"].is_null());
|
|
1675
2392
|
assert!(agent["captured_via"].is_null());
|
|
1676
2393
|
assert!(agent["attribution_confidence"].is_null());
|
|
2394
|
+
assert_eq!(agent["layout_window"], json!("team-w1"));
|
|
2395
|
+
assert_eq!(agent["layout_index"], json!(0));
|
|
2396
|
+
assert_eq!(agent["pane_index"], json!(0));
|
|
2397
|
+
assert_eq!(agent.pointer("/display/backend"), Some(&json!("adaptive")));
|
|
2398
|
+
assert_eq!(agent.pointer("/display/window"), Some(&json!("team-w1")));
|
|
2399
|
+
assert_eq!(agent.pointer("/display/pane_id"), Some(&json!("%0")));
|
|
1677
2400
|
// D5 (#264) / Python launch/core.py:253 — fresh launch persists spawn_cwd=workspace.
|
|
1678
2401
|
assert_eq!(agent["spawn_cwd"], json!(workspace.to_string_lossy()));
|
|
1679
2402
|
assert_eq!(agent["spawned_at"], json!(FIXED_SPAWNED_AT));
|