@team-agent/installer 0.5.113 → 0.5.115
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +57 -22
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +157 -8
- package/crates/team-agent/src/cli/models.rs +119 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +959 -0
- package/crates/team-agent/src/contract_runtime/commands.rs +245 -0
- package/crates/team-agent/src/contract_runtime/config.rs +340 -0
- package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +383 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +294 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +16 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +269 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +197 -0
- package/crates/team-agent/src/contract_runtime/workspace.rs +160 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +353 -21
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +571 -389
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +15 -0
- package/crates/team-agent/src/lifecycle/launch.rs +45 -28
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +34 -0
- package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -120,7 +120,12 @@ impl FreshQuickStartLeaderBindingOps for RuntimeFreshQuickStartLeaderBindingOps<
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
fn tmux_endpoint(&mut self) -> Option<String> {
|
|
123
|
-
|
|
123
|
+
// Bind the endpoint we use to observe the caller, not the worker server.
|
|
124
|
+
let caller_transport = caller_read_transport();
|
|
125
|
+
caller_transport
|
|
126
|
+
.as_deref()
|
|
127
|
+
.unwrap_or(self.transport)
|
|
128
|
+
.tmux_endpoint()
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
fn observe_command(&mut self, pane: &PaneId) -> Option<String> {
|
|
@@ -192,7 +197,9 @@ impl FreshQuickStartLeaderBindingOps for RuntimeFreshQuickStartLeaderBindingOps<
|
|
|
192
197
|
let (error, applied_grant) = error.into_parts();
|
|
193
198
|
self.applied_grant = applied_grant.map(|grant| (grant.owner, grant.receiver));
|
|
194
199
|
self.cas_conflict = match &error {
|
|
195
|
-
crate::leader::LeaderError::State(crate::state::StateError::SaveConflict(
|
|
200
|
+
crate::leader::LeaderError::State(crate::state::StateError::SaveConflict(
|
|
201
|
+
_,
|
|
202
|
+
)) => true,
|
|
196
203
|
_ => false,
|
|
197
204
|
};
|
|
198
205
|
self.last_failure_reason = Some(if self.cas_conflict {
|
|
@@ -206,7 +213,8 @@ impl FreshQuickStartLeaderBindingOps for RuntimeFreshQuickStartLeaderBindingOps<
|
|
|
206
213
|
crate::leader::LeaderError::Io(_)
|
|
207
214
|
| crate::leader::LeaderError::Json(_)
|
|
208
215
|
| crate::leader::LeaderError::MessageStore(_)
|
|
209
|
-
| crate::leader::LeaderError::Start(_)
|
|
216
|
+
| crate::leader::LeaderError::Start(_)
|
|
217
|
+
| crate::leader::LeaderError::Prompt(_) => "attach_failed",
|
|
210
218
|
crate::leader::LeaderError::EventLog(_) => "attach_event_log_failed",
|
|
211
219
|
crate::leader::LeaderError::Messaging(_) => "attach_watcher_failed",
|
|
212
220
|
}
|
|
@@ -273,16 +281,13 @@ impl FreshQuickStartLeaderBindingOps for RuntimeFreshQuickStartLeaderBindingOps<
|
|
|
273
281
|
});
|
|
274
282
|
return false;
|
|
275
283
|
}
|
|
276
|
-
let Ok(resolved) =
|
|
277
|
-
workspace,
|
|
278
|
-
|
|
279
|
-
) else {
|
|
284
|
+
let Ok(resolved) =
|
|
285
|
+
crate::state::projection::resolve_runtime_team_scope(workspace, Some(team_key))
|
|
286
|
+
else {
|
|
280
287
|
self.last_failure_reason = Some("receiver_scope_unavailable");
|
|
281
288
|
return false;
|
|
282
289
|
};
|
|
283
|
-
let Some(receiver) =
|
|
284
|
-
super::selected_team_leader_receiver(&resolved.state, team_key)
|
|
285
|
-
else {
|
|
290
|
+
let Some(receiver) = super::selected_team_leader_receiver(&resolved.state, team_key) else {
|
|
286
291
|
self.last_failure_reason = Some("receiver_missing");
|
|
287
292
|
return false;
|
|
288
293
|
};
|
|
@@ -291,9 +296,8 @@ impl FreshQuickStartLeaderBindingOps for RuntimeFreshQuickStartLeaderBindingOps<
|
|
|
291
296
|
.and_then(serde_json::Value::as_str)
|
|
292
297
|
.filter(|endpoint| !endpoint.is_empty())
|
|
293
298
|
.map(crate::transport_factory::leader_endpoint_transport);
|
|
294
|
-
let live_channel_transport: &dyn Transport =
|
|
295
|
-
.as_deref()
|
|
296
|
-
.unwrap_or(self.transport);
|
|
299
|
+
let live_channel_transport: &dyn Transport =
|
|
300
|
+
leader_transport.as_deref().unwrap_or(self.transport);
|
|
297
301
|
if !matches!(
|
|
298
302
|
crate::messaging::resolve_live_leader_channel(
|
|
299
303
|
workspace,
|
|
@@ -314,9 +318,7 @@ impl FreshQuickStartLeaderBindingOps for RuntimeFreshQuickStartLeaderBindingOps<
|
|
|
314
318
|
}
|
|
315
319
|
|
|
316
320
|
fn frozen_seed_receipt(&self) -> Option<(serde_json::Value, serde_json::Value)> {
|
|
317
|
-
self.frozen_owner
|
|
318
|
-
.clone()
|
|
319
|
-
.zip(self.frozen_receiver.clone())
|
|
321
|
+
self.frozen_owner.clone().zip(self.frozen_receiver.clone())
|
|
320
322
|
}
|
|
321
323
|
}
|
|
322
324
|
|
|
@@ -376,7 +378,6 @@ impl BindingFileSnapshot {
|
|
|
376
378
|
let bytes = std::fs::read(&path).ok();
|
|
377
379
|
Self { path, bytes }
|
|
378
380
|
}
|
|
379
|
-
|
|
380
381
|
}
|
|
381
382
|
|
|
382
383
|
fn binding_snapshots(workspace: &Path, team_key: &str) -> Vec<BindingFileSnapshot> {
|
|
@@ -434,7 +435,10 @@ fn persisted_binding_matches_verified_pane(
|
|
|
434
435
|
{
|
|
435
436
|
return false;
|
|
436
437
|
}
|
|
437
|
-
let Some(owner) = state
|
|
438
|
+
let Some(owner) = state
|
|
439
|
+
.get("team_owner")
|
|
440
|
+
.and_then(serde_json::Value::as_object)
|
|
441
|
+
else {
|
|
438
442
|
return false;
|
|
439
443
|
};
|
|
440
444
|
let Some(receiver) = state
|
|
@@ -488,10 +492,9 @@ fn bind_fresh_quick_start_leader_with<O: FreshQuickStartLeaderBindingOps>(
|
|
|
488
492
|
seeded_owner: Option<&serde_json::Value>,
|
|
489
493
|
ops: &mut O,
|
|
490
494
|
) -> Result<bool, LifecycleError> {
|
|
491
|
-
let Ok(resolved) =
|
|
492
|
-
workspace,
|
|
493
|
-
|
|
494
|
-
) else {
|
|
495
|
+
let Ok(resolved) =
|
|
496
|
+
crate::state::projection::resolve_runtime_team_scope(workspace, Some(team_key))
|
|
497
|
+
else {
|
|
495
498
|
return refuse_fresh_bind(workspace, team_key, "scope", "scope_unresolved");
|
|
496
499
|
};
|
|
497
500
|
if resolved.canonical_team_key != team_key {
|
|
@@ -550,27 +553,27 @@ fn bind_fresh_quick_start_leader_with<O: FreshQuickStartLeaderBindingOps>(
|
|
|
550
553
|
// projected state still contains that seed.
|
|
551
554
|
let current_receiver = state.get("leader_receiver");
|
|
552
555
|
let frozen_receipt = ops.frozen_seed_receipt();
|
|
553
|
-
let fresh_seeded_binding =
|
|
554
|
-
|
|
555
|
-
state
|
|
556
|
-
.get("team_owner")
|
|
557
|
-
.is_some_and(|current| current == frozen_owner)
|
|
558
|
-
&& current_receiver.is_some_and(|current| current == frozen_receiver)
|
|
559
|
-
} else {
|
|
560
|
-
let pending_seed_receiver = current_receiver.is_some_and(|receiver| {
|
|
561
|
-
receiver.get("status").and_then(serde_json::Value::as_str) == Some("pending")
|
|
562
|
-
&& receiver
|
|
563
|
-
.get("discovery")
|
|
564
|
-
.and_then(serde_json::Value::as_str)
|
|
565
|
-
== Some("quick_start_seed")
|
|
566
|
-
});
|
|
567
|
-
seeded_owner.is_some_and(|seed| {
|
|
556
|
+
let fresh_seeded_binding =
|
|
557
|
+
if let Some((frozen_owner, frozen_receiver)) = frozen_receipt.as_ref() {
|
|
568
558
|
state
|
|
569
559
|
.get("team_owner")
|
|
570
|
-
.is_some_and(|current| current ==
|
|
571
|
-
&&
|
|
572
|
-
}
|
|
573
|
-
|
|
560
|
+
.is_some_and(|current| current == frozen_owner)
|
|
561
|
+
&& current_receiver.is_some_and(|current| current == frozen_receiver)
|
|
562
|
+
} else {
|
|
563
|
+
let pending_seed_receiver = current_receiver.is_some_and(|receiver| {
|
|
564
|
+
receiver.get("status").and_then(serde_json::Value::as_str) == Some("pending")
|
|
565
|
+
&& receiver
|
|
566
|
+
.get("discovery")
|
|
567
|
+
.and_then(serde_json::Value::as_str)
|
|
568
|
+
== Some("quick_start_seed")
|
|
569
|
+
});
|
|
570
|
+
seeded_owner.is_some_and(|seed| {
|
|
571
|
+
state
|
|
572
|
+
.get("team_owner")
|
|
573
|
+
.is_some_and(|current| current == seed)
|
|
574
|
+
&& pending_seed_receiver
|
|
575
|
+
})
|
|
576
|
+
};
|
|
574
577
|
let seeded_receiver = frozen_receipt
|
|
575
578
|
.as_ref()
|
|
576
579
|
.map(|(_, receiver)| receiver.clone())
|
|
@@ -721,9 +724,9 @@ fn combine_rollback_errors(
|
|
|
721
724
|
match (state, registry) {
|
|
722
725
|
(None, None) => None,
|
|
723
726
|
(Some(error), None) | (None, Some(error)) => Some(error),
|
|
724
|
-
(Some(state), Some(registry)) =>
|
|
725
|
-
"{state}; {registry}"
|
|
726
|
-
|
|
727
|
+
(Some(state), Some(registry)) => {
|
|
728
|
+
Some(LifecycleError::StatePersist(format!("{state}; {registry}")))
|
|
729
|
+
}
|
|
727
730
|
}
|
|
728
731
|
}
|
|
729
732
|
|
|
@@ -757,9 +760,11 @@ fn restore_this_attempt_state(
|
|
|
757
760
|
});
|
|
758
761
|
};
|
|
759
762
|
let previous = match snapshot.map(|snapshot| snapshot.bytes.as_deref()) {
|
|
760
|
-
Some(Some(bytes)) =>
|
|
761
|
-
|
|
762
|
-
|
|
763
|
+
Some(Some(bytes)) => {
|
|
764
|
+
serde_json::from_slice::<serde_json::Value>(bytes).map_err(|error| {
|
|
765
|
+
LifecycleError::StatePersist(format!("quick-start binding cleanup failed: {error}"))
|
|
766
|
+
})?
|
|
767
|
+
}
|
|
763
768
|
Some(None) | None => serde_json::Value::Null,
|
|
764
769
|
};
|
|
765
770
|
let previous_owner = previous
|
|
@@ -879,9 +884,9 @@ fn clear_fresh_binding_on_refusal(
|
|
|
879
884
|
};
|
|
880
885
|
crate::state::repository::StateRepository::new(workspace)
|
|
881
886
|
.save(intent, state)
|
|
882
|
-
.map_err(|error|
|
|
883
|
-
"quick-start binding cleanup failed: {error}"
|
|
884
|
-
)
|
|
887
|
+
.map_err(|error| {
|
|
888
|
+
LifecycleError::StatePersist(format!("quick-start binding cleanup failed: {error}"))
|
|
889
|
+
})
|
|
885
890
|
}
|
|
886
891
|
|
|
887
892
|
fn existing_runtime_identity_view(
|
|
@@ -907,16 +912,14 @@ fn caller_read_transport() -> Option<Box<dyn Transport>> {
|
|
|
907
912
|
if !Path::new(&endpoint).exists() {
|
|
908
913
|
return None;
|
|
909
914
|
}
|
|
910
|
-
Some(Box::new(
|
|
911
|
-
&endpoint,
|
|
912
|
-
))
|
|
915
|
+
Some(Box::new(
|
|
916
|
+
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint),
|
|
917
|
+
))
|
|
913
918
|
}
|
|
914
919
|
|
|
915
920
|
fn query_caller_pane_command(transport: &dyn Transport, pane: &PaneId) -> Option<String> {
|
|
916
921
|
let caller_transport = caller_read_transport();
|
|
917
|
-
let query_transport: &dyn Transport = caller_transport
|
|
918
|
-
.as_deref()
|
|
919
|
-
.unwrap_or(transport);
|
|
922
|
+
let query_transport: &dyn Transport = caller_transport.as_deref().unwrap_or(transport);
|
|
920
923
|
query_transport
|
|
921
924
|
.query(&Target::Pane(pane.clone()), PaneField::PaneCurrentCommand)
|
|
922
925
|
.ok()
|
|
@@ -959,8 +962,8 @@ fn preflight_fresh_leader_identity(
|
|
|
959
962
|
let Some(pane) = pane else {
|
|
960
963
|
return None;
|
|
961
964
|
};
|
|
962
|
-
let command =
|
|
963
|
-
.unwrap_or_default();
|
|
965
|
+
let command =
|
|
966
|
+
query_caller_pane_command(transport, &PaneId::new(pane)).unwrap_or_default();
|
|
964
967
|
let explicit = std::env::var("TEAM_AGENT_LEADER_PROVIDER")
|
|
965
968
|
.ok()
|
|
966
969
|
.filter(|provider| !provider.is_empty());
|
|
@@ -968,16 +971,11 @@ fn preflight_fresh_leader_identity(
|
|
|
968
971
|
return Some((
|
|
969
972
|
"quick-start refused before spawn: command unobservable".to_string(),
|
|
970
973
|
vec!["stage=command_observation reason=command_unobservable".to_string()],
|
|
971
|
-
vec![
|
|
972
|
-
"run from a live provider pane; do not run claim-leader".to_string(),
|
|
973
|
-
],
|
|
974
|
+
vec!["run from a live provider pane; do not run claim-leader".to_string()],
|
|
974
975
|
));
|
|
975
976
|
}
|
|
976
|
-
if crate::leader::owner_bind::strict_owner_bind_provider(
|
|
977
|
-
|
|
978
|
-
&command,
|
|
979
|
-
)
|
|
980
|
-
.is_some()
|
|
977
|
+
if crate::leader::owner_bind::strict_owner_bind_provider(explicit.as_deref(), &command)
|
|
978
|
+
.is_some()
|
|
981
979
|
{
|
|
982
980
|
None
|
|
983
981
|
} else {
|
|
@@ -1097,12 +1095,7 @@ pub fn quick_start_in_workspace_with_backend(
|
|
|
1097
1095
|
if is_tmux_or_default {
|
|
1098
1096
|
let transport = quick_start_tmux_backend(&workspace);
|
|
1099
1097
|
return quick_start_with_transport_in_workspace(
|
|
1100
|
-
&workspace,
|
|
1101
|
-
agents_dir,
|
|
1102
|
-
name,
|
|
1103
|
-
yes,
|
|
1104
|
-
team_id,
|
|
1105
|
-
&transport,
|
|
1098
|
+
&workspace, agents_dir, name, yes, team_id, &transport,
|
|
1106
1099
|
);
|
|
1107
1100
|
}
|
|
1108
1101
|
// Explicit non-tmux backend: route through the factory. Parse the
|
|
@@ -1275,6 +1268,32 @@ pub(crate) fn quick_start_with_transport_in_workspace_pi_preflight(
|
|
|
1275
1268
|
)));
|
|
1276
1269
|
}
|
|
1277
1270
|
crate::lifecycle::launch::run_pi_catalog_preflight(agents_dir, discover)?;
|
|
1271
|
+
#[cfg(unix)]
|
|
1272
|
+
{
|
|
1273
|
+
let input = crate::cli::QuickStartArgs {
|
|
1274
|
+
workspace: workspace.into(),
|
|
1275
|
+
agents_dir: agents_dir.into(),
|
|
1276
|
+
name: name.map(str::to_owned),
|
|
1277
|
+
team_id: team_id.map(str::to_owned),
|
|
1278
|
+
yes: false,
|
|
1279
|
+
json: false,
|
|
1280
|
+
detail: false,
|
|
1281
|
+
backend: Some(
|
|
1282
|
+
if matches!(transport.kind(), crate::transport::BackendKind::Tmux) {
|
|
1283
|
+
"tmux"
|
|
1284
|
+
} else {
|
|
1285
|
+
"conpty"
|
|
1286
|
+
}
|
|
1287
|
+
.into(),
|
|
1288
|
+
),
|
|
1289
|
+
};
|
|
1290
|
+
if let Some(config) = crate::contract_runtime::config::read_team(&input)
|
|
1291
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?
|
|
1292
|
+
{
|
|
1293
|
+
crate::contract_runtime::backend::preflight(&config)
|
|
1294
|
+
.map_err(|error| LifecycleError::Provider(error.to_string()))?;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1278
1297
|
let workspace = workspace.to_path_buf();
|
|
1279
1298
|
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
1280
1299
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
@@ -1328,7 +1347,9 @@ pub(crate) fn quick_start_with_transport_in_workspace_pi_preflight(
|
|
|
1328
1347
|
.get("tmux_endpoint")
|
|
1329
1348
|
.and_then(serde_json::Value::as_str)
|
|
1330
1349
|
.or_else(|| {
|
|
1331
|
-
identity
|
|
1350
|
+
identity
|
|
1351
|
+
.get("tmux_socket")
|
|
1352
|
+
.and_then(serde_json::Value::as_str)
|
|
1332
1353
|
})
|
|
1333
1354
|
.or_else(|| {
|
|
1334
1355
|
state
|
|
@@ -1450,10 +1471,7 @@ pub(crate) fn quick_start_with_transport_in_workspace_pi_preflight(
|
|
|
1450
1471
|
.and_then(|teams| teams.get(&state_team_key))
|
|
1451
1472
|
.and_then(|team| team.get("team_owner"))
|
|
1452
1473
|
.filter(|owner| {
|
|
1453
|
-
owner
|
|
1454
|
-
.get("claimed_via")
|
|
1455
|
-
.and_then(serde_json::Value::as_str)
|
|
1456
|
-
== Some("quick-start")
|
|
1474
|
+
owner.get("claimed_via").and_then(serde_json::Value::as_str) == Some("quick-start")
|
|
1457
1475
|
})
|
|
1458
1476
|
.cloned();
|
|
1459
1477
|
let seeded_receiver = state
|
|
@@ -1517,12 +1535,18 @@ pub(crate) fn quick_start_with_transport_in_workspace_pi_preflight(
|
|
|
1517
1535
|
quick_start_session_capture_incomplete_agents(&workspace, &state_team_key);
|
|
1518
1536
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
1519
1537
|
let coordinator_report = crate::coordinator::start_coordinator(&coordinator_workspace)
|
|
1520
|
-
.map_err(|error|
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1538
|
+
.map_err(|error| {
|
|
1539
|
+
LifecycleError::StatePersist(format!(
|
|
1540
|
+
"coordinator start failed for {}: {error}; inspect {} before retrying",
|
|
1541
|
+
workspace.display(),
|
|
1542
|
+
crate::coordinator::coordinator_log_path(&coordinator_workspace).display()
|
|
1543
|
+
))
|
|
1544
|
+
})?;
|
|
1545
|
+
quick_start_phase_timer.emit(
|
|
1546
|
+
&workspace,
|
|
1547
|
+
"launch.phase",
|
|
1548
|
+
crate::lifecycle::restart::LifecyclePhase::CoordinatorStart,
|
|
1549
|
+
);
|
|
1526
1550
|
require_quick_start_coordinator_started(&coordinator_report, &workspace)?;
|
|
1527
1551
|
let coordinator_action = "coordinator started";
|
|
1528
1552
|
// BUG-7: build an honest readiness verdict from the post-spawn runtime state.
|
|
@@ -1532,7 +1556,11 @@ pub(crate) fn quick_start_with_transport_in_workspace_pi_preflight(
|
|
|
1532
1556
|
// loaded successfully (provider-side codex/claude schema rejections happen
|
|
1533
1557
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
1534
1558
|
// bare Ready.
|
|
1535
|
-
quick_start_phase_timer.emit(
|
|
1559
|
+
quick_start_phase_timer.emit(
|
|
1560
|
+
&workspace,
|
|
1561
|
+
"launch.phase",
|
|
1562
|
+
crate::lifecycle::restart::LifecyclePhase::ReadinessWait,
|
|
1563
|
+
);
|
|
1536
1564
|
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
1537
1565
|
let attach_windows = load_runtime_state(&workspace)
|
|
1538
1566
|
.ok()
|
|
@@ -1572,7 +1600,11 @@ pub(crate) fn quick_start_with_transport_in_workspace_pi_preflight(
|
|
|
1572
1600
|
));
|
|
1573
1601
|
}
|
|
1574
1602
|
next_actions.extend(attach_commands.iter().cloned());
|
|
1575
|
-
quick_start_phase_timer.emit(
|
|
1603
|
+
quick_start_phase_timer.emit(
|
|
1604
|
+
&workspace,
|
|
1605
|
+
"launch.phase",
|
|
1606
|
+
crate::lifecycle::restart::LifecyclePhase::Completed,
|
|
1607
|
+
);
|
|
1576
1608
|
Ok(QuickStartReport::Ready {
|
|
1577
1609
|
session_name,
|
|
1578
1610
|
launch: Box::new(launch),
|
|
@@ -1595,8 +1627,15 @@ fn preserve_quick_start_bootstrap_binding(
|
|
|
1595
1627
|
if let Some(receiver) = bootstrap.get("leader_receiver") {
|
|
1596
1628
|
binding = binding.with_leader_receiver(receiver.clone());
|
|
1597
1629
|
}
|
|
1598
|
-
if let Some(epoch) = bootstrap
|
|
1599
|
-
.
|
|
1630
|
+
if let Some(epoch) = bootstrap
|
|
1631
|
+
.get("owner_epoch")
|
|
1632
|
+
.and_then(serde_json::Value::as_u64)
|
|
1633
|
+
.or_else(|| {
|
|
1634
|
+
bootstrap
|
|
1635
|
+
.get("team_owner")
|
|
1636
|
+
.and_then(|owner| owner.get("owner_epoch"))
|
|
1637
|
+
.and_then(serde_json::Value::as_u64)
|
|
1638
|
+
})
|
|
1600
1639
|
{
|
|
1601
1640
|
binding = binding.with_owner_epoch(epoch);
|
|
1602
1641
|
}
|
|
@@ -1682,13 +1721,19 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
1682
1721
|
assert_eq!(launched.get(key), bootstrap.get(key));
|
|
1683
1722
|
assert_eq!(launched["teams"]["current"].get(key), bootstrap.get(key));
|
|
1684
1723
|
}
|
|
1685
|
-
assert_eq!(
|
|
1724
|
+
assert_eq!(
|
|
1725
|
+
launched.get("leader_client"),
|
|
1726
|
+
bootstrap.get("leader_client")
|
|
1727
|
+
);
|
|
1686
1728
|
assert_eq!(launched.get("is_external_leader"), Some(&json!(true)));
|
|
1687
1729
|
assert_eq!(launched["agents"], json!({"worker": {}}));
|
|
1688
1730
|
let merged = super::super::state_projection::merge_workspace_team_state_with_key(
|
|
1689
1731
|
&bootstrap, &launched, "current",
|
|
1690
1732
|
);
|
|
1691
|
-
assert_eq!(
|
|
1733
|
+
assert_eq!(
|
|
1734
|
+
merged["teams"]["current"]["team_owner"],
|
|
1735
|
+
bootstrap["team_owner"]
|
|
1736
|
+
);
|
|
1692
1737
|
assert_eq!(merged["teams"]["current"]["owner_epoch"], json!(7));
|
|
1693
1738
|
}
|
|
1694
1739
|
|
|
@@ -1702,20 +1747,34 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
1702
1747
|
binary_path: Some("/tmp/candidate/team-agent".to_string()),
|
|
1703
1748
|
binary_version: Some("0.5.112".to_string()),
|
|
1704
1749
|
rotation_reason: Some("original rotation reason".to_string()),
|
|
1705
|
-
binary_identity_relation:
|
|
1750
|
+
binary_identity_relation:
|
|
1751
|
+
crate::coordinator::CoordinatorBinaryIdentityRelation::Unknown,
|
|
1706
1752
|
log: Some(PathBuf::from("/tmp/current/.team/runtime/coordinator.log")),
|
|
1707
|
-
schema_error: Some(crate::coordinator::SchemaError::InitFailed {
|
|
1753
|
+
schema_error: Some(crate::coordinator::SchemaError::InitFailed {
|
|
1754
|
+
message: "original database permission denied".to_string(),
|
|
1755
|
+
}),
|
|
1708
1756
|
action: Some("repair the exact database permissions".to_string()),
|
|
1709
1757
|
};
|
|
1710
1758
|
let error = require_quick_start_coordinator_started(&report, Path::new("/tmp/current"))
|
|
1711
|
-
.unwrap_err()
|
|
1712
|
-
|
|
1759
|
+
.unwrap_err()
|
|
1760
|
+
.to_string();
|
|
1761
|
+
for detail in [
|
|
1762
|
+
"schema_incompatible",
|
|
1763
|
+
"original database permission denied",
|
|
1764
|
+
"repair the exact database permissions",
|
|
1765
|
+
"/tmp/candidate/team-agent",
|
|
1766
|
+
"/tmp/current/.team/runtime/coordinator.log",
|
|
1767
|
+
"workers may already be started",
|
|
1768
|
+
] {
|
|
1713
1769
|
assert!(error.contains(detail), "lost {detail}: {error}");
|
|
1714
1770
|
}
|
|
1715
1771
|
report.ok = true;
|
|
1716
1772
|
report.status = crate::coordinator::StartOutcome::AlreadyRunning;
|
|
1717
|
-
report.binary_identity_relation =
|
|
1718
|
-
|
|
1773
|
+
report.binary_identity_relation =
|
|
1774
|
+
crate::coordinator::CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller;
|
|
1775
|
+
assert!(
|
|
1776
|
+
require_quick_start_coordinator_started(&report, Path::new("/tmp/current")).is_ok()
|
|
1777
|
+
);
|
|
1719
1778
|
}
|
|
1720
1779
|
|
|
1721
1780
|
#[test]
|
|
@@ -1725,11 +1784,17 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
1725
1784
|
env.scrub_tmux();
|
|
1726
1785
|
env.assert_no_real_tmux();
|
|
1727
1786
|
let workspace = std::env::temp_dir().join(format!(
|
|
1728
|
-
"ta-current-bootstrap-{}-{}",
|
|
1787
|
+
"ta-current-bootstrap-{}-{}",
|
|
1788
|
+
std::process::id(),
|
|
1789
|
+
SEQUENCE.fetch_add(1, Ordering::Relaxed)
|
|
1729
1790
|
));
|
|
1730
1791
|
let team = workspace.join(".team/current");
|
|
1731
1792
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
1732
|
-
std::fs::write(
|
|
1793
|
+
std::fs::write(
|
|
1794
|
+
team.join("TEAM.md"),
|
|
1795
|
+
"---\nname: current\nobjective: Bootstrap fixture.\n---\n",
|
|
1796
|
+
)
|
|
1797
|
+
.unwrap();
|
|
1733
1798
|
std::fs::write(team.join("agents/worker.md"), "---\nname: worker\nrole: Worker\nprovider: codex\nauth_mode: subscription\ndangerously_skip_permissions: false\n---\nBounded fixture.\n").unwrap();
|
|
1734
1799
|
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
1735
1800
|
std::fs::create_dir_all(state_path.parent().unwrap()).unwrap();
|
|
@@ -1749,10 +1814,20 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
1749
1814
|
std::env::set_var("TMUX_PANE", "%0");
|
|
1750
1815
|
std::env::set_var(CALLER_PANE_ENV, "%0");
|
|
1751
1816
|
std::env::set_var(CALLER_PROVIDER_ENV, "pi");
|
|
1752
|
-
std::env::set_var(
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1817
|
+
std::env::set_var(
|
|
1818
|
+
CALLER_ENDPOINT_ENV,
|
|
1819
|
+
"/private/tmp/different-bootstrap/default",
|
|
1820
|
+
);
|
|
1821
|
+
let transport =
|
|
1822
|
+
OfflineTransport::new().with_tmux_endpoint("/private/tmp/bootstrap-test/default");
|
|
1823
|
+
let result = quick_start_with_transport_in_workspace(
|
|
1824
|
+
&workspace, &team, None, true, None, &transport,
|
|
1825
|
+
)
|
|
1826
|
+
.unwrap();
|
|
1827
|
+
assert!(
|
|
1828
|
+
matches!(result, QuickStartReport::PreflightBlocked { .. }),
|
|
1829
|
+
"unexpected outcome: {result:?}"
|
|
1830
|
+
);
|
|
1756
1831
|
assert_eq!(std::fs::read(&state_path).unwrap(), before);
|
|
1757
1832
|
assert!(!workspace.join(".team/team.db").exists());
|
|
1758
1833
|
assert!(!crate::model::paths::runtime_spec_path(&workspace, "current").exists());
|
|
@@ -1922,10 +1997,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
1922
1997
|
)
|
|
1923
1998
|
}
|
|
1924
1999
|
_ => crate::state::persist::save_runtime_state_with_receiver_authority(
|
|
1925
|
-
workspace,
|
|
1926
|
-
state,
|
|
1927
|
-
&team_key,
|
|
1928
|
-
None,
|
|
2000
|
+
workspace, state, &team_key, None,
|
|
1929
2001
|
),
|
|
1930
2002
|
};
|
|
1931
2003
|
if saved.is_ok() {
|
|
@@ -1941,9 +2013,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
1941
2013
|
}
|
|
1942
2014
|
|
|
1943
2015
|
fn frozen_seed_receipt(&self) -> Option<(serde_json::Value, serde_json::Value)> {
|
|
1944
|
-
self.frozen_owner
|
|
1945
|
-
.clone()
|
|
1946
|
-
.zip(self.frozen_receiver.clone())
|
|
2016
|
+
self.frozen_owner.clone().zip(self.frozen_receiver.clone())
|
|
1947
2017
|
}
|
|
1948
2018
|
|
|
1949
2019
|
fn this_attempt_extra_state_keys(&self) -> Vec<&'static str> {
|
|
@@ -1990,12 +2060,15 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
1990
2060
|
}
|
|
1991
2061
|
|
|
1992
2062
|
#[test]
|
|
1993
|
-
fn fresh_binding_persists_then_registers_then_requires_canonical_readback_provider_independent()
|
|
2063
|
+
fn fresh_binding_persists_then_registers_then_requires_canonical_readback_provider_independent()
|
|
2064
|
+
{
|
|
1994
2065
|
let workspace = workspace("positive");
|
|
1995
2066
|
let mut ops = MockOps::default();
|
|
1996
|
-
assert!(bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops)
|
|
1997
|
-
|
|
1998
|
-
|
|
2067
|
+
assert!(bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops).unwrap());
|
|
2068
|
+
assert_eq!(
|
|
2069
|
+
ops.attached_provider,
|
|
2070
|
+
Some(crate::provider::Provider::Codex)
|
|
2071
|
+
);
|
|
1999
2072
|
assert_eq!(ops.attach_calls, 1);
|
|
2000
2073
|
assert_eq!(ops.register_calls, 1);
|
|
2001
2074
|
assert_eq!(ops.readback_calls, 1);
|
|
@@ -2030,11 +2103,13 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2030
2103
|
leader_session_uuid: "uuid-fresh".to_string(),
|
|
2031
2104
|
leader_session_uuid_source: "env".to_string(),
|
|
2032
2105
|
};
|
|
2033
|
-
assert!(
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2106
|
+
assert!(
|
|
2107
|
+
super::spec_state::seed_launched_owner_from_caller_with_provider_lookup(
|
|
2108
|
+
&mut state,
|
|
2109
|
+
caller,
|
|
2110
|
+
|_| None
|
|
2111
|
+
)
|
|
2112
|
+
);
|
|
2038
2113
|
let owner = state
|
|
2039
2114
|
.pointer("/teams/fresh/team_owner")
|
|
2040
2115
|
.cloned()
|
|
@@ -2061,13 +2136,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2061
2136
|
frozen_receiver: Some(frozen_receiver.clone()),
|
|
2062
2137
|
..MockOps::default()
|
|
2063
2138
|
};
|
|
2064
|
-
assert!(
|
|
2065
|
-
&workspace,
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
&mut ops
|
|
2069
|
-
)
|
|
2070
|
-
.unwrap());
|
|
2139
|
+
assert!(
|
|
2140
|
+
bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&owner), &mut ops)
|
|
2141
|
+
.unwrap()
|
|
2142
|
+
);
|
|
2071
2143
|
assert_eq!(ops.attach_calls, 1);
|
|
2072
2144
|
assert_eq!(ops.last_expected_receiver, Some(frozen_receiver));
|
|
2073
2145
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
@@ -2084,10 +2156,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2084
2156
|
state["leader_receiver"] = newer.clone();
|
|
2085
2157
|
state["teams"]["fresh"]["leader_receiver"] = newer.clone();
|
|
2086
2158
|
crate::state::persist::save_runtime_state_with_receiver_authority(
|
|
2087
|
-
&workspace,
|
|
2088
|
-
&state,
|
|
2089
|
-
"fresh",
|
|
2090
|
-
None,
|
|
2159
|
+
&workspace, &state, "fresh", None,
|
|
2091
2160
|
)
|
|
2092
2161
|
.unwrap();
|
|
2093
2162
|
let on_disk = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
@@ -2104,13 +2173,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2104
2173
|
frozen_receiver: Some(frozen_receiver),
|
|
2105
2174
|
..MockOps::default()
|
|
2106
2175
|
};
|
|
2107
|
-
assert!(
|
|
2108
|
-
&workspace,
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
&mut ops
|
|
2112
|
-
)
|
|
2113
|
-
.unwrap());
|
|
2176
|
+
assert!(
|
|
2177
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&owner), &mut ops)
|
|
2178
|
+
.unwrap()
|
|
2179
|
+
);
|
|
2114
2180
|
assert_eq!(ops.attach_calls, 0);
|
|
2115
2181
|
assert_eq!(ops.last_expected_receiver, None);
|
|
2116
2182
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
@@ -2127,10 +2193,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2127
2193
|
state["leader_receiver"] = winner.clone();
|
|
2128
2194
|
state["teams"]["fresh"]["leader_receiver"] = winner.clone();
|
|
2129
2195
|
crate::state::persist::save_runtime_state_with_receiver_authority(
|
|
2130
|
-
&workspace,
|
|
2131
|
-
&state,
|
|
2132
|
-
"fresh",
|
|
2133
|
-
None,
|
|
2196
|
+
&workspace, &state, "fresh", None,
|
|
2134
2197
|
)
|
|
2135
2198
|
.unwrap();
|
|
2136
2199
|
let on_disk = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
@@ -2147,13 +2210,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2147
2210
|
frozen_receiver: Some(frozen_receiver),
|
|
2148
2211
|
..MockOps::default()
|
|
2149
2212
|
};
|
|
2150
|
-
assert!(
|
|
2151
|
-
&workspace,
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
&mut ops
|
|
2155
|
-
)
|
|
2156
|
-
.unwrap());
|
|
2213
|
+
assert!(
|
|
2214
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&owner), &mut ops)
|
|
2215
|
+
.unwrap()
|
|
2216
|
+
);
|
|
2157
2217
|
assert_eq!(ops.attach_calls, 0);
|
|
2158
2218
|
assert_eq!(ops.last_expected_receiver, None);
|
|
2159
2219
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
@@ -2163,7 +2223,11 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2163
2223
|
fn fresh_bind_refusal_event_keeps_first_safe_stage_and_reason() {
|
|
2164
2224
|
for (case, expected_stage, expected_reason) in [
|
|
2165
2225
|
("missing_pane", "caller_pane", "caller_pane_missing"),
|
|
2166
|
-
(
|
|
2226
|
+
(
|
|
2227
|
+
"empty_command",
|
|
2228
|
+
"command_observation",
|
|
2229
|
+
"command_unobservable",
|
|
2230
|
+
),
|
|
2167
2231
|
("unknown_provider", "strict_provider", "provider_unresolved"),
|
|
2168
2232
|
(
|
|
2169
2233
|
"existing_owner",
|
|
@@ -2200,12 +2264,16 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2200
2264
|
}
|
|
2201
2265
|
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
2202
2266
|
|
|
2203
|
-
assert!(
|
|
2204
|
-
.unwrap()
|
|
2267
|
+
assert!(
|
|
2268
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops).unwrap()
|
|
2269
|
+
);
|
|
2205
2270
|
let events = crate::event_log::EventLog::new(&workspace).tail(0).unwrap();
|
|
2206
2271
|
assert_eq!(events.len(), 1, "{case} must emit one first-refusal event");
|
|
2207
2272
|
assert_eq!(events[0]["event"], json!(FRESH_BIND_REFUSAL_EVENT));
|
|
2208
|
-
assert_eq!(
|
|
2273
|
+
assert_eq!(
|
|
2274
|
+
events[0]["operation"],
|
|
2275
|
+
json!("fresh_quick_start_leader_bind")
|
|
2276
|
+
);
|
|
2209
2277
|
assert_eq!(events[0]["team_key"], json!("fresh"));
|
|
2210
2278
|
assert_eq!(events[0]["stage"], json!(expected_stage));
|
|
2211
2279
|
assert_eq!(events[0]["reason"], json!(expected_reason));
|
|
@@ -2223,9 +2291,12 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2223
2291
|
));
|
|
2224
2292
|
std::fs::create_dir_all(&empty_workspace).unwrap();
|
|
2225
2293
|
let mut ops = MockOps::default();
|
|
2226
|
-
assert!(
|
|
2227
|
-
.unwrap()
|
|
2228
|
-
|
|
2294
|
+
assert!(
|
|
2295
|
+
!bind_fresh_quick_start_leader_with(&empty_workspace, "fresh", None, &mut ops).unwrap()
|
|
2296
|
+
);
|
|
2297
|
+
let events = crate::event_log::EventLog::new(&empty_workspace)
|
|
2298
|
+
.tail(0)
|
|
2299
|
+
.unwrap();
|
|
2229
2300
|
assert_eq!(events.len(), 1);
|
|
2230
2301
|
assert_eq!(events[0]["stage"], json!("scope"));
|
|
2231
2302
|
assert_eq!(events[0]["reason"], json!("scope_unresolved"));
|
|
@@ -2240,8 +2311,9 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2240
2311
|
state["teams"] = json!({"fresh": {"team_key": "fresh", "agents": {}}});
|
|
2241
2312
|
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
2242
2313
|
let mut ops = MockOps::default();
|
|
2243
|
-
assert!(
|
|
2244
|
-
.unwrap()
|
|
2314
|
+
assert!(
|
|
2315
|
+
!bind_fresh_quick_start_leader_with(&workspace, "current", None, &mut ops).unwrap()
|
|
2316
|
+
);
|
|
2245
2317
|
let events = crate::event_log::EventLog::new(&workspace).tail(0).unwrap();
|
|
2246
2318
|
assert_eq!(events.len(), 1);
|
|
2247
2319
|
assert_eq!(events[0]["stage"], json!("scope"));
|
|
@@ -2285,10 +2357,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2285
2357
|
ops.command = Some("codex".to_string());
|
|
2286
2358
|
}
|
|
2287
2359
|
"existing_owner" => state["team_owner"] = json!({"pane_id": "%old"}),
|
|
2288
|
-
"existing_receiver" => {
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2360
|
+
"existing_receiver" => state["leader_receiver"] = json!({"pane_id": "%old"}),
|
|
2361
|
+
"provider_mismatch"
|
|
2362
|
+
| "socket_mismatch"
|
|
2363
|
+
| "workspace_mismatch"
|
|
2292
2364
|
| "dual_state_mismatch" => {
|
|
2293
2365
|
state["workspace"] = json!(workspace);
|
|
2294
2366
|
state["team_owner"] = json!({
|
|
@@ -2303,8 +2375,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2303
2375
|
match case {
|
|
2304
2376
|
"provider_mismatch" => {
|
|
2305
2377
|
state["leader_receiver"]["provider"] = json!("claude")
|
|
2306
|
-
}
|
|
2307
|
-
"socket_mismatch" =>
|
|
2378
|
+
}
|
|
2379
|
+
"socket_mismatch" => {
|
|
2380
|
+
state["leader_receiver"]["tmux_socket"] = json!("/tmp/other")
|
|
2381
|
+
}
|
|
2308
2382
|
"workspace_mismatch" => state["workspace"] = json!(workspace.join("other")),
|
|
2309
2383
|
"dual_state_mismatch" => state["team_owner"]["owner_epoch"] = json!(2),
|
|
2310
2384
|
_ => unreachable!(),
|
|
@@ -2314,11 +2388,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2314
2388
|
_ => unreachable!(),
|
|
2315
2389
|
}
|
|
2316
2390
|
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
2317
|
-
let before =
|
|
2318
|
-
.unwrap();
|
|
2391
|
+
let before =
|
|
2392
|
+
std::fs::read(crate::state::persist::runtime_state_path(&workspace)).unwrap();
|
|
2319
2393
|
assert!(
|
|
2320
|
-
!bind_fresh_quick_start_leader_with(&workspace, team_key, None, &mut ops)
|
|
2321
|
-
.unwrap(),
|
|
2394
|
+
!bind_fresh_quick_start_leader_with(&workspace, team_key, None, &mut ops).unwrap(),
|
|
2322
2395
|
"{case} must refuse"
|
|
2323
2396
|
);
|
|
2324
2397
|
assert_eq!(
|
|
@@ -2490,15 +2563,23 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2490
2563
|
|| bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops)
|
|
2491
2564
|
)
|
|
2492
2565
|
.unwrap());
|
|
2493
|
-
assert_eq!(
|
|
2494
|
-
|
|
2495
|
-
"
|
|
2496
|
-
|
|
2497
|
-
)
|
|
2566
|
+
assert_eq!(
|
|
2567
|
+
ops.attach_calls, 0,
|
|
2568
|
+
"preseeded dual state must not reattach"
|
|
2569
|
+
);
|
|
2570
|
+
let path = crate::leader::registry::registry_dir()
|
|
2571
|
+
.unwrap()
|
|
2572
|
+
.join(format!(
|
|
2573
|
+
"{}__fresh.json",
|
|
2574
|
+
crate::leader::registry::workspace_hash(&workspace)
|
|
2575
|
+
));
|
|
2498
2576
|
let entry: crate::leader::registry::LeaderRegistryEntry =
|
|
2499
2577
|
serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
|
|
2500
2578
|
assert_eq!(
|
|
2501
|
-
entry
|
|
2579
|
+
entry
|
|
2580
|
+
.channel
|
|
2581
|
+
.get("tmux_socket")
|
|
2582
|
+
.and_then(serde_json::Value::as_str),
|
|
2502
2583
|
Some("/private/tmp/tmux-test/default")
|
|
2503
2584
|
);
|
|
2504
2585
|
}
|
|
@@ -2528,8 +2609,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2528
2609
|
let before = std::fs::read(crate::state::persist::runtime_state_path(&workspace)).unwrap();
|
|
2529
2610
|
let mut ops = MockOps::default();
|
|
2530
2611
|
|
|
2531
|
-
assert!(!bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops)
|
|
2532
|
-
.unwrap());
|
|
2612
|
+
assert!(!bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops).unwrap());
|
|
2533
2613
|
assert_eq!(
|
|
2534
2614
|
before,
|
|
2535
2615
|
std::fs::read(crate::state::persist::runtime_state_path(&workspace)).unwrap(),
|
|
@@ -2709,10 +2789,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2709
2789
|
persisted["teams"][team_key]["leader_receiver"] = takeover.receiver.clone();
|
|
2710
2790
|
persisted["teams"][team_key]["owner_epoch"] = json!(takeover.epoch);
|
|
2711
2791
|
crate::state::persist::save_runtime_state_with_receiver_authority(
|
|
2712
|
-
workspace,
|
|
2713
|
-
&persisted,
|
|
2714
|
-
team_key,
|
|
2715
|
-
None,
|
|
2792
|
+
workspace, &persisted, team_key, None,
|
|
2716
2793
|
)
|
|
2717
2794
|
.unwrap();
|
|
2718
2795
|
assert_fresh_binding_kept(
|
|
@@ -2744,18 +2821,13 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2744
2821
|
}
|
|
2745
2822
|
}
|
|
2746
2823
|
|
|
2747
|
-
fn assert_fresh_binding_kept(
|
|
2748
|
-
persisted: &serde_json::Value,
|
|
2749
|
-
takeover: &ConcurrentTakeover,
|
|
2750
|
-
) {
|
|
2824
|
+
fn assert_fresh_binding_kept(persisted: &serde_json::Value, takeover: &ConcurrentTakeover) {
|
|
2751
2825
|
assert_eq!(
|
|
2752
|
-
persisted["teams"]["fresh"]["team_owner"],
|
|
2753
|
-
takeover.owner,
|
|
2826
|
+
persisted["teams"]["fresh"]["team_owner"], takeover.owner,
|
|
2754
2827
|
"concurrent team_owner must survive seeded cleanup"
|
|
2755
2828
|
);
|
|
2756
2829
|
assert_eq!(
|
|
2757
|
-
persisted["teams"]["fresh"]["leader_receiver"],
|
|
2758
|
-
takeover.receiver,
|
|
2830
|
+
persisted["teams"]["fresh"]["leader_receiver"], takeover.receiver,
|
|
2759
2831
|
"concurrent leader_receiver must survive seeded cleanup"
|
|
2760
2832
|
);
|
|
2761
2833
|
assert_eq!(
|
|
@@ -2769,13 +2841,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2769
2841
|
fn seeded_cleanup_clears_exact_seed_through_bind_repository_entry() {
|
|
2770
2842
|
let (workspace, seed) = workspace_with_fresh_seed("seed-cleanup-success");
|
|
2771
2843
|
let mut ops = failed_register_ops(None, false);
|
|
2772
|
-
assert!(
|
|
2773
|
-
&workspace,
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
&mut ops
|
|
2777
|
-
)
|
|
2778
|
-
.unwrap());
|
|
2844
|
+
assert!(
|
|
2845
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
2846
|
+
.unwrap()
|
|
2847
|
+
);
|
|
2779
2848
|
assert_eq!(ops.inner.attach_calls, 1);
|
|
2780
2849
|
assert_eq!(ops.inner.register_calls, 1);
|
|
2781
2850
|
assert!(ops
|
|
@@ -2794,11 +2863,9 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2794
2863
|
"matching seed must be cleared via bind/repository; got {}",
|
|
2795
2864
|
persisted["teams"]["fresh"]["team_owner"]
|
|
2796
2865
|
);
|
|
2797
|
-
assert!(
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
.is_some_and(serde_json::Value::is_null)
|
|
2801
|
-
);
|
|
2866
|
+
assert!(persisted
|
|
2867
|
+
.pointer("/teams/fresh/leader_receiver")
|
|
2868
|
+
.is_some_and(serde_json::Value::is_null));
|
|
2802
2869
|
assert_eq!(
|
|
2803
2870
|
persisted["teams"]["fresh"]["owner_epoch"],
|
|
2804
2871
|
json!(1),
|
|
@@ -2811,13 +2878,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2811
2878
|
let (workspace, seed) = workspace_with_matching_seed("seed-cleanup-concurrent");
|
|
2812
2879
|
let takeover = takeover_binding(1);
|
|
2813
2880
|
let mut ops = failed_register_ops(Some(takeover_binding(1)), false);
|
|
2814
|
-
assert!(
|
|
2815
|
-
&workspace,
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
&mut ops
|
|
2819
|
-
)
|
|
2820
|
-
.unwrap());
|
|
2881
|
+
assert!(
|
|
2882
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
2883
|
+
.unwrap()
|
|
2884
|
+
);
|
|
2821
2885
|
assert_fresh_binding_kept(
|
|
2822
2886
|
&crate::state::persist::load_runtime_state(&workspace).unwrap(),
|
|
2823
2887
|
&takeover,
|
|
@@ -2829,13 +2893,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2829
2893
|
let (workspace, seed) = workspace_with_matching_seed("seed-cleanup-epoch2");
|
|
2830
2894
|
let takeover = takeover_binding(2);
|
|
2831
2895
|
let mut ops = failed_register_ops(Some(takeover_binding(2)), false);
|
|
2832
|
-
assert!(
|
|
2833
|
-
&workspace,
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
&mut ops
|
|
2837
|
-
)
|
|
2838
|
-
.unwrap());
|
|
2896
|
+
assert!(
|
|
2897
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
2898
|
+
.unwrap()
|
|
2899
|
+
);
|
|
2839
2900
|
assert_fresh_binding_kept(
|
|
2840
2901
|
&crate::state::persist::load_runtime_state(&workspace).unwrap(),
|
|
2841
2902
|
&takeover,
|
|
@@ -2846,13 +2907,8 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2846
2907
|
fn seeded_cleanup_propagates_persist_error() {
|
|
2847
2908
|
let (workspace, seed) = workspace_with_fresh_seed("seed-cleanup-persist-err");
|
|
2848
2909
|
let mut ops = failed_register_ops(None, true);
|
|
2849
|
-
let error = bind_fresh_quick_start_leader_with(
|
|
2850
|
-
|
|
2851
|
-
"fresh",
|
|
2852
|
-
Some(&seed),
|
|
2853
|
-
&mut ops,
|
|
2854
|
-
)
|
|
2855
|
-
.expect_err("poisoned state.json must surface cleanup persist failure");
|
|
2910
|
+
let error = bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
2911
|
+
.expect_err("poisoned state.json must surface cleanup persist failure");
|
|
2856
2912
|
assert_eq!(ops.inner.attach_calls, 1);
|
|
2857
2913
|
assert_eq!(ops.inner.register_calls, 1);
|
|
2858
2914
|
assert!(ops
|
|
@@ -2932,7 +2988,9 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2932
2988
|
"owner_epoch": 1
|
|
2933
2989
|
}
|
|
2934
2990
|
});
|
|
2935
|
-
serde_json::to_string_pretty(&expected)
|
|
2991
|
+
serde_json::to_string_pretty(&expected)
|
|
2992
|
+
.unwrap()
|
|
2993
|
+
.into_bytes()
|
|
2936
2994
|
}
|
|
2937
2995
|
|
|
2938
2996
|
fn assert_failure_rolls_back_this_attempt(
|
|
@@ -3031,8 +3089,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3031
3089
|
attach_ok: false,
|
|
3032
3090
|
..MockOps::default()
|
|
3033
3091
|
};
|
|
3034
|
-
assert!(!bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops)
|
|
3035
|
-
.unwrap());
|
|
3092
|
+
assert!(!bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops).unwrap());
|
|
3036
3093
|
let after = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
3037
3094
|
assert_eq!(after.get("tasks"), Some(&json!({"keep": true})));
|
|
3038
3095
|
assert!(after.get("failed_attach_mutation").is_none());
|
|
@@ -3076,6 +3133,99 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3076
3133
|
})
|
|
3077
3134
|
}
|
|
3078
3135
|
|
|
3136
|
+
#[cfg(unix)]
|
|
3137
|
+
#[test]
|
|
3138
|
+
#[serial_test::serial(env)]
|
|
3139
|
+
fn runtime_binding_endpoint_matches_caller_socket_without_redirecting_workers() {
|
|
3140
|
+
let hermetic = HermeticTestEnv::enter("runtime-caller-endpoint");
|
|
3141
|
+
let workspace = hermetic.root();
|
|
3142
|
+
let caller_socket = workspace.join("leader.sock");
|
|
3143
|
+
// Select the real caller-read backend without depending on a host tmux
|
|
3144
|
+
// server. This marker is not evidence of pane liveness.
|
|
3145
|
+
std::fs::write(&caller_socket, "fixture endpoint; no tmux server").unwrap();
|
|
3146
|
+
let caller_endpoint = caller_socket.to_str().unwrap();
|
|
3147
|
+
let tmux = hermetic.with_env("TMUX", &format!("{caller_endpoint},1,0"));
|
|
3148
|
+
let _pane = hermetic.with_env("TMUX_PANE", "%0");
|
|
3149
|
+
let worker_endpoint = "ta-fixture-workers";
|
|
3150
|
+
let transport = OfflineTransport::new()
|
|
3151
|
+
.with_tmux_endpoint(worker_endpoint)
|
|
3152
|
+
.with_pane_current_command("%0", "pi");
|
|
3153
|
+
let mut ops = RuntimeFreshQuickStartLeaderBindingOps {
|
|
3154
|
+
transport: &transport,
|
|
3155
|
+
last_failure_reason: None,
|
|
3156
|
+
cas_conflict: false,
|
|
3157
|
+
applied_grant: None,
|
|
3158
|
+
registry_receipt: None,
|
|
3159
|
+
frozen_owner: None,
|
|
3160
|
+
frozen_receiver: None,
|
|
3161
|
+
nonce_writer: None,
|
|
3162
|
+
};
|
|
3163
|
+
let endpoint = ops.tmux_endpoint().unwrap();
|
|
3164
|
+
assert_eq!(endpoint, caller_endpoint);
|
|
3165
|
+
assert_eq!(
|
|
3166
|
+
caller_read_transport().unwrap().tmux_endpoint().as_deref(),
|
|
3167
|
+
Some(endpoint.as_str())
|
|
3168
|
+
);
|
|
3169
|
+
assert_eq!(transport.tmux_endpoint().as_deref(), Some(worker_endpoint));
|
|
3170
|
+
let state = native_persisted_binding(workspace, caller_endpoint);
|
|
3171
|
+
let before = state.clone();
|
|
3172
|
+
assert!(persisted_binding_matches_verified_pane(
|
|
3173
|
+
&state,
|
|
3174
|
+
workspace,
|
|
3175
|
+
&PaneId::new("%0"),
|
|
3176
|
+
Provider::Pi,
|
|
3177
|
+
Some(&endpoint),
|
|
3178
|
+
));
|
|
3179
|
+
assert_eq!(state, before);
|
|
3180
|
+
drop(tmux);
|
|
3181
|
+
assert!(caller_read_transport().is_none());
|
|
3182
|
+
assert_eq!(ops.tmux_endpoint().as_deref(), Some(worker_endpoint));
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
#[cfg(unix)]
|
|
3186
|
+
#[test]
|
|
3187
|
+
#[serial_test::serial(env)]
|
|
3188
|
+
fn runtime_binding_endpoint_rejects_foreign_socket_with_same_pane_identity() {
|
|
3189
|
+
let hermetic = HermeticTestEnv::enter("runtime-foreign-endpoint");
|
|
3190
|
+
let workspace = hermetic.root();
|
|
3191
|
+
let caller_socket = workspace.join("leader.sock");
|
|
3192
|
+
std::fs::write(&caller_socket, "fixture endpoint; no tmux server").unwrap();
|
|
3193
|
+
let caller_endpoint = caller_socket.to_str().unwrap();
|
|
3194
|
+
let _tmux = hermetic.with_env("TMUX", &format!("{caller_endpoint},1,0"));
|
|
3195
|
+
let _pane = hermetic.with_env("TMUX_PANE", "%0");
|
|
3196
|
+
let worker_endpoint = "ta-fixture-workers";
|
|
3197
|
+
let transport = OfflineTransport::new()
|
|
3198
|
+
.with_tmux_endpoint(worker_endpoint)
|
|
3199
|
+
.with_pane_current_command("%0", "pi");
|
|
3200
|
+
let mut ops = RuntimeFreshQuickStartLeaderBindingOps {
|
|
3201
|
+
transport: &transport,
|
|
3202
|
+
last_failure_reason: None,
|
|
3203
|
+
cas_conflict: false,
|
|
3204
|
+
applied_grant: None,
|
|
3205
|
+
registry_receipt: None,
|
|
3206
|
+
frozen_owner: None,
|
|
3207
|
+
frozen_receiver: None,
|
|
3208
|
+
nonce_writer: None,
|
|
3209
|
+
};
|
|
3210
|
+
let endpoint = ops.tmux_endpoint().unwrap();
|
|
3211
|
+
assert_eq!(endpoint, caller_endpoint);
|
|
3212
|
+
let foreign_socket = workspace.join("foreign").join("leader.sock");
|
|
3213
|
+
for socket in [worker_endpoint, foreign_socket.to_str().unwrap()] {
|
|
3214
|
+
let state = native_persisted_binding(workspace, socket);
|
|
3215
|
+
let before = state.clone();
|
|
3216
|
+
// Both servers may have %0; the shared owner/provider/UUID/epoch
|
|
3217
|
+
// cannot make a foreign endpoint authoritative.
|
|
3218
|
+
assert!(!persisted_binding_matches_verified_pane(
|
|
3219
|
+
&state,
|
|
3220
|
+
workspace,
|
|
3221
|
+
&PaneId::new("%0"),
|
|
3222
|
+
Provider::Pi,
|
|
3223
|
+
Some(&endpoint),
|
|
3224
|
+
));
|
|
3225
|
+
assert_eq!(state, before);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3079
3229
|
#[cfg(unix)]
|
|
3080
3230
|
#[test]
|
|
3081
3231
|
#[serial_test::serial(env)]
|
|
@@ -3090,18 +3240,34 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3090
3240
|
let pane = PaneId::new("%0");
|
|
3091
3241
|
assert_ne!(socket.to_str(), Some(endpoint.as_str()));
|
|
3092
3242
|
assert!(persisted_binding_matches_verified_pane(
|
|
3093
|
-
&state,
|
|
3243
|
+
&state,
|
|
3244
|
+
workspace,
|
|
3245
|
+
&pane,
|
|
3246
|
+
Provider::Pi,
|
|
3247
|
+
Some(&endpoint),
|
|
3094
3248
|
));
|
|
3095
3249
|
assert_eq!(state, before);
|
|
3096
3250
|
assert!(!persisted_binding_matches_verified_pane(
|
|
3097
|
-
&state,
|
|
3251
|
+
&state,
|
|
3252
|
+
workspace,
|
|
3253
|
+
&PaneId::new("%1"),
|
|
3254
|
+
Provider::Pi,
|
|
3255
|
+
Some(&endpoint),
|
|
3098
3256
|
));
|
|
3099
3257
|
assert!(!persisted_binding_matches_verified_pane(
|
|
3100
|
-
&state,
|
|
3258
|
+
&state,
|
|
3259
|
+
workspace,
|
|
3260
|
+
&pane,
|
|
3261
|
+
Provider::Codex,
|
|
3262
|
+
Some(&endpoint),
|
|
3101
3263
|
));
|
|
3102
3264
|
for endpoint in [None, Some("")] {
|
|
3103
3265
|
assert!(!persisted_binding_matches_verified_pane(
|
|
3104
|
-
&state,
|
|
3266
|
+
&state,
|
|
3267
|
+
workspace,
|
|
3268
|
+
&pane,
|
|
3269
|
+
Provider::Pi,
|
|
3270
|
+
endpoint,
|
|
3105
3271
|
));
|
|
3106
3272
|
}
|
|
3107
3273
|
for (field, value) in [
|
|
@@ -3121,15 +3287,26 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3121
3287
|
] {
|
|
3122
3288
|
let mut conflict = state.clone();
|
|
3123
3289
|
*conflict.pointer_mut(field).unwrap() = value;
|
|
3124
|
-
assert!(
|
|
3125
|
-
|
|
3126
|
-
|
|
3290
|
+
assert!(
|
|
3291
|
+
!persisted_binding_matches_verified_pane(
|
|
3292
|
+
&conflict,
|
|
3293
|
+
workspace,
|
|
3294
|
+
&pane,
|
|
3295
|
+
Provider::Pi,
|
|
3296
|
+
Some(&endpoint),
|
|
3297
|
+
),
|
|
3298
|
+
"conflicting {field} must refuse"
|
|
3299
|
+
);
|
|
3127
3300
|
}
|
|
3128
3301
|
let mut unauthorized = state.clone();
|
|
3129
3302
|
unauthorized["leader_receiver"]["authorized_team_workspace"] =
|
|
3130
3303
|
json!(workspace.join("foreign"));
|
|
3131
3304
|
assert!(!persisted_binding_matches_verified_pane(
|
|
3132
|
-
&unauthorized,
|
|
3305
|
+
&unauthorized,
|
|
3306
|
+
workspace,
|
|
3307
|
+
&pane,
|
|
3308
|
+
Provider::Pi,
|
|
3309
|
+
Some(&endpoint),
|
|
3133
3310
|
));
|
|
3134
3311
|
}
|
|
3135
3312
|
|
|
@@ -3143,15 +3320,24 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3143
3320
|
let endpoint = transport.tmux_endpoint().unwrap();
|
|
3144
3321
|
let foreign_socket = workspace.join("foreign-socket-root").join(&endpoint);
|
|
3145
3322
|
let state = native_persisted_binding(workspace, foreign_socket.to_str().unwrap());
|
|
3146
|
-
assert_eq!(
|
|
3323
|
+
assert_eq!(
|
|
3324
|
+
foreign_socket.file_name().unwrap().to_str(),
|
|
3325
|
+
Some(endpoint.as_str())
|
|
3326
|
+
);
|
|
3147
3327
|
assert!(!persisted_binding_matches_verified_pane(
|
|
3148
|
-
&state,
|
|
3328
|
+
&state,
|
|
3329
|
+
workspace,
|
|
3330
|
+
&PaneId::new("%0"),
|
|
3331
|
+
Provider::Pi,
|
|
3332
|
+
Some(&endpoint),
|
|
3149
3333
|
));
|
|
3150
3334
|
assert!(!quick_start_tmux_endpoints_match(
|
|
3151
|
-
foreign_socket.to_str().unwrap(),
|
|
3335
|
+
foreign_socket.to_str().unwrap(),
|
|
3336
|
+
"default",
|
|
3152
3337
|
));
|
|
3153
3338
|
assert!(!quick_start_tmux_endpoints_match(
|
|
3154
|
-
foreign_socket.to_str().unwrap(),
|
|
3339
|
+
foreign_socket.to_str().unwrap(),
|
|
3340
|
+
"",
|
|
3155
3341
|
));
|
|
3156
3342
|
}
|
|
3157
3343
|
|
|
@@ -3169,7 +3355,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3169
3355
|
std::fs::write(&socket, "fixture endpoint; no tmux server").unwrap();
|
|
3170
3356
|
let state = native_persisted_binding(workspace, socket.to_str().unwrap());
|
|
3171
3357
|
assert!(persisted_binding_matches_verified_pane(
|
|
3172
|
-
&state,
|
|
3358
|
+
&state,
|
|
3359
|
+
workspace,
|
|
3360
|
+
&PaneId::new("%0"),
|
|
3361
|
+
Provider::Pi,
|
|
3173
3362
|
alias.join("ta-native.sock").to_str(),
|
|
3174
3363
|
));
|
|
3175
3364
|
}
|
|
@@ -3185,10 +3374,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3185
3374
|
state["teams"]["fresh"]["leader_receiver"] = receiver.clone();
|
|
3186
3375
|
state["teams"]["fresh"]["owner_epoch"] = json!(epoch);
|
|
3187
3376
|
crate::state::persist::save_runtime_state_with_receiver_authority(
|
|
3188
|
-
workspace,
|
|
3189
|
-
&state,
|
|
3190
|
-
"fresh",
|
|
3191
|
-
None,
|
|
3377
|
+
workspace, &state, "fresh", None,
|
|
3192
3378
|
)
|
|
3193
3379
|
.unwrap();
|
|
3194
3380
|
let raw = std::fs::read(crate::state::persist::runtime_state_path(workspace)).unwrap();
|
|
@@ -3248,7 +3434,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3248
3434
|
.unwrap();
|
|
3249
3435
|
let after = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
3250
3436
|
assert_eq!(after["teams"]["fresh"]["team_owner"], previous_owner);
|
|
3251
|
-
assert_eq!(
|
|
3437
|
+
assert_eq!(
|
|
3438
|
+
after["teams"]["fresh"]["leader_receiver"],
|
|
3439
|
+
previous_receiver
|
|
3440
|
+
);
|
|
3252
3441
|
assert_eq!(after["teams"]["fresh"]["owner_epoch"], json!(1));
|
|
3253
3442
|
assert_eq!(after.get("tasks"), Some(&json!({"keep": true})));
|
|
3254
3443
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
@@ -3298,7 +3487,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3298
3487
|
assert_eq!(after["teams"]["fresh"]["leader_receiver"], winner_receiver);
|
|
3299
3488
|
assert_eq!(after["teams"]["fresh"]["owner_epoch"], json!(2));
|
|
3300
3489
|
assert_ne!(after["teams"]["fresh"]["team_owner"], previous_owner);
|
|
3301
|
-
assert_ne!(
|
|
3490
|
+
assert_ne!(
|
|
3491
|
+
after["teams"]["fresh"]["leader_receiver"],
|
|
3492
|
+
previous_receiver
|
|
3493
|
+
);
|
|
3302
3494
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
3303
3495
|
}
|
|
3304
3496
|
|
|
@@ -3343,19 +3535,23 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3343
3535
|
let _home = HomeGuard::set(&home);
|
|
3344
3536
|
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
3345
3537
|
let before = std::fs::read(&state_path).unwrap();
|
|
3346
|
-
let registry_path = crate::leader::registry::registry_dir()
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3538
|
+
let registry_path = crate::leader::registry::registry_dir()
|
|
3539
|
+
.unwrap()
|
|
3540
|
+
.join(format!(
|
|
3541
|
+
"{}__fresh.json",
|
|
3542
|
+
crate::leader::registry::workspace_hash(&workspace)
|
|
3543
|
+
));
|
|
3350
3544
|
let mut ops = AmbientRegistryOps {
|
|
3351
3545
|
readback_ok: false,
|
|
3352
3546
|
attach_calls: 0,
|
|
3353
3547
|
registry_receipt: None,
|
|
3354
3548
|
};
|
|
3355
|
-
assert!(!bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops)
|
|
3356
|
-
.unwrap());
|
|
3549
|
+
assert!(!bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops).unwrap());
|
|
3357
3550
|
assert_eq!(std::fs::read(state_path).unwrap(), before);
|
|
3358
|
-
assert!(
|
|
3551
|
+
assert!(
|
|
3552
|
+
!registry_path.exists(),
|
|
3553
|
+
"failed readback left registry bytes"
|
|
3554
|
+
);
|
|
3359
3555
|
}
|
|
3360
3556
|
|
|
3361
3557
|
struct RecordingRuntimeOps<'a> {
|
|
@@ -3563,7 +3759,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3563
3759
|
let mut focused = caller_target(parent.clone(), Some("shared-live-nonce"));
|
|
3564
3760
|
focused.active = true;
|
|
3565
3761
|
let (active_ok, active_reason) = bind_seeded_caller(focused, "runtime-focus-active");
|
|
3566
|
-
assert!(
|
|
3762
|
+
assert!(
|
|
3763
|
+
active_ok,
|
|
3764
|
+
"focused live caller must bind: {active_reason:?}"
|
|
3765
|
+
);
|
|
3567
3766
|
|
|
3568
3767
|
let mut background = caller_target(parent, Some("shared-live-nonce"));
|
|
3569
3768
|
background.active = false;
|
|
@@ -3599,8 +3798,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3599
3798
|
frozen_receiver: None,
|
|
3600
3799
|
nonce_writer: None,
|
|
3601
3800
|
};
|
|
3602
|
-
assert!(
|
|
3603
|
-
|
|
3801
|
+
assert!(
|
|
3802
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
3803
|
+
.unwrap()
|
|
3804
|
+
);
|
|
3604
3805
|
assert_eq!(ops.last_failure_reason, Some("caller_pane_not_live"));
|
|
3605
3806
|
}
|
|
3606
3807
|
|
|
@@ -3647,7 +3848,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3647
3848
|
.map(|agents| agents.keys().cloned().collect::<Vec<_>>())
|
|
3648
3849
|
.unwrap_or_default();
|
|
3649
3850
|
assert_eq!(ids, vec!["bob".to_string()]);
|
|
3650
|
-
assert_eq!(
|
|
3851
|
+
assert_eq!(
|
|
3852
|
+
view.get("session_name").and_then(serde_json::Value::as_str),
|
|
3853
|
+
Some("team-bob")
|
|
3854
|
+
);
|
|
3651
3855
|
}
|
|
3652
3856
|
|
|
3653
3857
|
#[test]
|
|
@@ -3750,11 +3954,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3750
3954
|
.with_tmux_endpoint("/tmp/tmux.sock")
|
|
3751
3955
|
.with_targets(vec![stale_target]);
|
|
3752
3956
|
assert!(matches!(
|
|
3753
|
-
crate::messaging::resolve_live_leader_channel(
|
|
3754
|
-
&workspace,
|
|
3755
|
-
&receiver,
|
|
3756
|
-
&stale_transport
|
|
3757
|
-
),
|
|
3957
|
+
crate::messaging::resolve_live_leader_channel(&workspace, &receiver, &stale_transport),
|
|
3758
3958
|
crate::messaging::LeaderChannelResolution::Unbound(
|
|
3759
3959
|
crate::messaging::LeaderChannelUnbound::PaneWorkspaceMismatch(_)
|
|
3760
3960
|
)
|
|
@@ -3792,10 +3992,17 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3792
3992
|
));
|
|
3793
3993
|
|
|
3794
3994
|
let mut foreign_receiver = receiver.clone();
|
|
3795
|
-
foreign_receiver["authorized_team_workspace"] =
|
|
3796
|
-
|
|
3995
|
+
foreign_receiver["authorized_team_workspace"] = json!(hermetic
|
|
3996
|
+
.root()
|
|
3997
|
+
.join("foreign")
|
|
3998
|
+
.to_string_lossy()
|
|
3999
|
+
.to_string());
|
|
3797
4000
|
assert!(matches!(
|
|
3798
|
-
crate::messaging::resolve_live_leader_channel(
|
|
4001
|
+
crate::messaging::resolve_live_leader_channel(
|
|
4002
|
+
&workspace,
|
|
4003
|
+
&foreign_receiver,
|
|
4004
|
+
&transport
|
|
4005
|
+
),
|
|
3799
4006
|
crate::messaging::LeaderChannelResolution::Unbound(
|
|
3800
4007
|
crate::messaging::LeaderChannelUnbound::PaneWorkspaceMismatch(_)
|
|
3801
4008
|
)
|
|
@@ -3998,10 +4205,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
3998
4205
|
let mut winner = crate::state::persist::load_runtime_state(workspace).unwrap();
|
|
3999
4206
|
winner["teams"][team_key]["leader_receiver"]["pane_id"] = json!("%winner");
|
|
4000
4207
|
crate::state::persist::save_runtime_state_with_receiver_authority(
|
|
4001
|
-
workspace,
|
|
4002
|
-
&winner,
|
|
4003
|
-
team_key,
|
|
4004
|
-
None,
|
|
4208
|
+
workspace, &winner, team_key, None,
|
|
4005
4209
|
)
|
|
4006
4210
|
.unwrap();
|
|
4007
4211
|
let _ = crate::leader::registry::register_binding_from_state_best_effort(
|
|
@@ -4037,10 +4241,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4037
4241
|
.with_tmux_endpoint("/tmp/tmux.sock")
|
|
4038
4242
|
.with_pane_current_command("%1", "bash")
|
|
4039
4243
|
.with_targets(vec![target_with_nonce.clone()])
|
|
4040
|
-
.with_target_snapshots(vec![
|
|
4041
|
-
vec![target_without_nonce],
|
|
4042
|
-
vec![target_with_nonce],
|
|
4043
|
-
]);
|
|
4244
|
+
.with_target_snapshots(vec![vec![target_without_nonce], vec![target_with_nonce]]);
|
|
4044
4245
|
let mut ops = RuntimeCommitFailureOps {
|
|
4045
4246
|
inner: RuntimeFreshQuickStartLeaderBindingOps {
|
|
4046
4247
|
transport: &transport,
|
|
@@ -4059,13 +4260,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4059
4260
|
break_registry_lock: false,
|
|
4060
4261
|
break_registry_read: false,
|
|
4061
4262
|
};
|
|
4062
|
-
assert!(
|
|
4063
|
-
&workspace,
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
&mut ops
|
|
4067
|
-
)
|
|
4068
|
-
.unwrap());
|
|
4263
|
+
assert!(
|
|
4264
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
4265
|
+
.unwrap()
|
|
4266
|
+
);
|
|
4069
4267
|
let persisted = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4070
4268
|
assert!(persisted
|
|
4071
4269
|
.pointer("/teams/fresh/team_owner")
|
|
@@ -4080,7 +4278,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4080
4278
|
"{}__fresh.json",
|
|
4081
4279
|
crate::leader::registry::workspace_hash(&workspace)
|
|
4082
4280
|
));
|
|
4083
|
-
assert!(
|
|
4281
|
+
assert!(
|
|
4282
|
+
!registry_path.exists(),
|
|
4283
|
+
"failed child grant left registry row"
|
|
4284
|
+
);
|
|
4084
4285
|
let observed = transport.list_targets().unwrap();
|
|
4085
4286
|
assert_eq!(
|
|
4086
4287
|
observed[0]
|
|
@@ -4128,14 +4329,14 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4128
4329
|
break_registry_lock: false,
|
|
4129
4330
|
break_registry_read: false,
|
|
4130
4331
|
};
|
|
4131
|
-
assert!(
|
|
4132
|
-
&workspace,
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4332
|
+
assert!(
|
|
4333
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops,)
|
|
4334
|
+
.unwrap()
|
|
4335
|
+
);
|
|
4336
|
+
assert_eq!(
|
|
4337
|
+
ops.inner.last_failure_reason,
|
|
4338
|
+
Some("attach_event_log_failed")
|
|
4339
|
+
);
|
|
4139
4340
|
let persisted = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4140
4341
|
assert!(persisted
|
|
4141
4342
|
.pointer("/teams/fresh/team_owner")
|
|
@@ -4192,7 +4393,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4192
4393
|
|| bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
4193
4394
|
)
|
|
4194
4395
|
.unwrap());
|
|
4195
|
-
assert_eq!(
|
|
4396
|
+
assert_eq!(
|
|
4397
|
+
ops.last_failure_reason,
|
|
4398
|
+
Some("receiver_live_channel_unavailable")
|
|
4399
|
+
);
|
|
4196
4400
|
let persisted = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4197
4401
|
assert!(persisted
|
|
4198
4402
|
.pointer("/teams/fresh/team_owner")
|
|
@@ -4245,13 +4449,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4245
4449
|
break_registry_lock: false,
|
|
4246
4450
|
break_registry_read: false,
|
|
4247
4451
|
};
|
|
4248
|
-
assert!(
|
|
4249
|
-
&workspace,
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
&mut ops
|
|
4253
|
-
)
|
|
4254
|
-
.unwrap());
|
|
4452
|
+
assert!(
|
|
4453
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
4454
|
+
.unwrap()
|
|
4455
|
+
);
|
|
4255
4456
|
let persisted = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4256
4457
|
assert!(persisted
|
|
4257
4458
|
.pointer("/teams/fresh/team_owner")
|
|
@@ -4265,7 +4466,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4265
4466
|
"{}__fresh.json",
|
|
4266
4467
|
crate::leader::registry::workspace_hash(&workspace)
|
|
4267
4468
|
));
|
|
4268
|
-
assert!(
|
|
4469
|
+
assert!(
|
|
4470
|
+
!registry_path.exists(),
|
|
4471
|
+
"readback failure left registry row"
|
|
4472
|
+
);
|
|
4269
4473
|
}
|
|
4270
4474
|
|
|
4271
4475
|
#[test]
|
|
@@ -4304,13 +4508,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4304
4508
|
break_registry_lock: false,
|
|
4305
4509
|
break_registry_read: false,
|
|
4306
4510
|
};
|
|
4307
|
-
assert!(
|
|
4308
|
-
&workspace,
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
&mut ops,
|
|
4312
|
-
)
|
|
4313
|
-
.unwrap());
|
|
4511
|
+
assert!(
|
|
4512
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops,)
|
|
4513
|
+
.unwrap()
|
|
4514
|
+
);
|
|
4314
4515
|
let persisted = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4315
4516
|
assert_eq!(
|
|
4316
4517
|
persisted
|
|
@@ -4348,13 +4549,11 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4348
4549
|
#[test]
|
|
4349
4550
|
#[serial_test::serial(env)]
|
|
4350
4551
|
fn seeded_runtime_registry_rollback_errors_are_visible_after_state_cleanup() {
|
|
4351
|
-
for (tag, break_registry_lock, break_registry_read) in
|
|
4352
|
-
("lock", true, false),
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
"runtime-seeded-registry-rollback-{tag}"
|
|
4357
|
-
));
|
|
4552
|
+
for (tag, break_registry_lock, break_registry_read) in
|
|
4553
|
+
[("lock", true, false), ("read", false, true)]
|
|
4554
|
+
{
|
|
4555
|
+
let hermetic =
|
|
4556
|
+
HermeticTestEnv::enter(&format!("runtime-seeded-registry-rollback-{tag}"));
|
|
4358
4557
|
let workspace = runtime_workspace(&hermetic, "fresh");
|
|
4359
4558
|
let parent = hermetic.root().join("parent");
|
|
4360
4559
|
std::fs::create_dir_all(&parent).unwrap();
|
|
@@ -4387,13 +4586,9 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4387
4586
|
break_registry_lock,
|
|
4388
4587
|
break_registry_read,
|
|
4389
4588
|
};
|
|
4390
|
-
let error =
|
|
4391
|
-
&workspace,
|
|
4392
|
-
|
|
4393
|
-
Some(&seed),
|
|
4394
|
-
&mut ops,
|
|
4395
|
-
)
|
|
4396
|
-
.expect_err("registry rollback failure must be visible");
|
|
4589
|
+
let error =
|
|
4590
|
+
bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
4591
|
+
.expect_err("registry rollback failure must be visible");
|
|
4397
4592
|
assert!(error
|
|
4398
4593
|
.to_string()
|
|
4399
4594
|
.contains("quick-start registry rollback failed"));
|
|
@@ -4477,7 +4672,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4477
4672
|
)
|
|
4478
4673
|
.unwrap());
|
|
4479
4674
|
assert_eq!(ops.last_failure_reason, Some(expected_reason));
|
|
4480
|
-
assert_eq!(
|
|
4675
|
+
assert_eq!(
|
|
4676
|
+
refusal_reason(&workspace).map(|(_, reason)| reason),
|
|
4677
|
+
Some(expected_reason.to_string())
|
|
4678
|
+
);
|
|
4481
4679
|
let persisted = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4482
4680
|
assert!(persisted
|
|
4483
4681
|
.pointer("/teams/fresh/team_owner")
|
|
@@ -4501,12 +4699,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4501
4699
|
let _caller_pane = hermetic.with_env(CALLER_PANE_ENV, "%1");
|
|
4502
4700
|
let _caller_endpoint = hermetic.with_env(CALLER_ENDPOINT_ENV, "/tmp/tmux.sock");
|
|
4503
4701
|
let seed = seeded_runtime_owner(&workspace);
|
|
4504
|
-
let mut state =
|
|
4505
|
-
&workspace,
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
.unwrap()
|
|
4509
|
-
.state;
|
|
4702
|
+
let mut state =
|
|
4703
|
+
crate::state::projection::resolve_runtime_team_scope(&workspace, Some("fresh"))
|
|
4704
|
+
.unwrap()
|
|
4705
|
+
.state;
|
|
4510
4706
|
let expected_owner = state["team_owner"].clone();
|
|
4511
4707
|
let expected_receiver = state["leader_receiver"].clone();
|
|
4512
4708
|
let target = caller_target(parent.clone(), None);
|
|
@@ -4538,12 +4734,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4538
4734
|
|
|
4539
4735
|
let workspace = runtime_workspace(&hermetic, "writer-failure");
|
|
4540
4736
|
let seed = seeded_runtime_owner(&workspace);
|
|
4541
|
-
let mut state =
|
|
4542
|
-
&workspace,
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
.unwrap()
|
|
4546
|
-
.state;
|
|
4737
|
+
let mut state =
|
|
4738
|
+
crate::state::projection::resolve_runtime_team_scope(&workspace, Some("fresh"))
|
|
4739
|
+
.unwrap()
|
|
4740
|
+
.state;
|
|
4547
4741
|
let expected_owner = state["team_owner"].clone();
|
|
4548
4742
|
let expected_receiver = state["leader_receiver"].clone();
|
|
4549
4743
|
let target = caller_target(hermetic.root().join("parent"), None);
|
|
@@ -4583,27 +4777,22 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4583
4777
|
let _caller_pane = hermetic.with_env(CALLER_PANE_ENV, "%1");
|
|
4584
4778
|
let _caller_endpoint = hermetic.with_env(CALLER_ENDPOINT_ENV, "/tmp/tmux.sock");
|
|
4585
4779
|
let seed = seeded_runtime_owner(&workspace);
|
|
4586
|
-
let concurrent_writer =
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4780
|
+
let concurrent_writer =
|
|
4781
|
+
|_: &str, _: &PaneId, _: &str| -> Result<String, crate::leader::LeaderError> {
|
|
4782
|
+
let mut winner = crate::state::persist::load_runtime_state(&workspace)
|
|
4783
|
+
.map_err(crate::leader::LeaderError::State)?;
|
|
4784
|
+
winner["teams"]["fresh"]["leader_receiver"]["pane_id"] = json!("%winner");
|
|
4785
|
+
crate::state::persist::save_runtime_state_with_receiver_authority(
|
|
4786
|
+
&workspace, &winner, "fresh", None,
|
|
4787
|
+
)
|
|
4591
4788
|
.map_err(crate::leader::LeaderError::State)?;
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
.map_err(crate::leader::LeaderError::State)?;
|
|
4600
|
-
let _ = crate::leader::registry::register_binding_from_state_best_effort(
|
|
4601
|
-
&workspace,
|
|
4602
|
-
Some("fresh"),
|
|
4603
|
-
"concurrent",
|
|
4604
|
-
);
|
|
4605
|
-
Ok("writer-winner".to_string())
|
|
4606
|
-
};
|
|
4789
|
+
let _ = crate::leader::registry::register_binding_from_state_best_effort(
|
|
4790
|
+
&workspace,
|
|
4791
|
+
Some("fresh"),
|
|
4792
|
+
"concurrent",
|
|
4793
|
+
);
|
|
4794
|
+
Ok("writer-winner".to_string())
|
|
4795
|
+
};
|
|
4607
4796
|
let target = caller_target(parent, None);
|
|
4608
4797
|
let transport = OfflineTransport::new()
|
|
4609
4798
|
.with_tmux_endpoint("/tmp/tmux.sock")
|
|
@@ -4619,30 +4808,35 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4619
4808
|
frozen_receiver: None,
|
|
4620
4809
|
nonce_writer: Some(&concurrent_writer),
|
|
4621
4810
|
};
|
|
4622
|
-
assert!(
|
|
4623
|
-
&workspace,
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
&mut ops
|
|
4627
|
-
)
|
|
4628
|
-
.unwrap());
|
|
4811
|
+
assert!(
|
|
4812
|
+
!bind_fresh_quick_start_leader_with(&workspace, "fresh", Some(&seed), &mut ops)
|
|
4813
|
+
.unwrap()
|
|
4814
|
+
);
|
|
4629
4815
|
assert!(ops.cas_conflict);
|
|
4630
4816
|
assert_eq!(ops.last_failure_reason, Some("attach_cas_conflict"));
|
|
4631
4817
|
let persisted = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4632
4818
|
assert_eq!(
|
|
4633
|
-
persisted
|
|
4819
|
+
persisted
|
|
4820
|
+
.pointer("/teams/fresh/team_owner/pane_id")
|
|
4821
|
+
.and_then(serde_json::Value::as_str),
|
|
4634
4822
|
Some("%1")
|
|
4635
4823
|
);
|
|
4636
4824
|
assert_eq!(
|
|
4637
|
-
persisted
|
|
4825
|
+
persisted
|
|
4826
|
+
.pointer("/teams/fresh/leader_receiver/pane_id")
|
|
4827
|
+
.and_then(serde_json::Value::as_str),
|
|
4638
4828
|
Some("%winner")
|
|
4639
4829
|
);
|
|
4640
4830
|
assert_eq!(
|
|
4641
|
-
persisted
|
|
4831
|
+
persisted
|
|
4832
|
+
.pointer("/teams/fresh/team_owner/owner_epoch")
|
|
4833
|
+
.and_then(serde_json::Value::as_u64),
|
|
4642
4834
|
Some(1)
|
|
4643
4835
|
);
|
|
4644
4836
|
assert_eq!(
|
|
4645
|
-
persisted
|
|
4837
|
+
persisted
|
|
4838
|
+
.pointer("/teams/fresh/leader_receiver/owner_epoch")
|
|
4839
|
+
.and_then(serde_json::Value::as_u64),
|
|
4646
4840
|
Some(1)
|
|
4647
4841
|
);
|
|
4648
4842
|
let registry_path = crate::leader::registry::registry_dir()
|
|
@@ -4651,7 +4845,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4651
4845
|
"{}__fresh.json",
|
|
4652
4846
|
crate::leader::registry::workspace_hash(&workspace)
|
|
4653
4847
|
));
|
|
4654
|
-
assert!(
|
|
4848
|
+
assert!(
|
|
4849
|
+
registry_path.exists(),
|
|
4850
|
+
"concurrent winner registry was removed"
|
|
4851
|
+
);
|
|
4655
4852
|
let winner_entry: crate::leader::registry::LeaderRegistryEntry =
|
|
4656
4853
|
serde_json::from_slice(&std::fs::read(registry_path).unwrap()).unwrap();
|
|
4657
4854
|
assert_eq!(winner_entry.channel["pane_id"], json!("%winner"));
|
|
@@ -4661,18 +4858,8 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4661
4858
|
#[serial_test::serial(env)]
|
|
4662
4859
|
fn runtime_socket_and_explicit_conflicts_refuse_without_command_fallback() {
|
|
4663
4860
|
for (tag, endpoint, leader, scoped) in [
|
|
4664
|
-
(
|
|
4665
|
-
|
|
4666
|
-
"/tmp/other.sock",
|
|
4667
|
-
None,
|
|
4668
|
-
"/tmp/tmux.sock",
|
|
4669
|
-
),
|
|
4670
|
-
(
|
|
4671
|
-
"scoped",
|
|
4672
|
-
"/tmp/tmux.sock",
|
|
4673
|
-
None,
|
|
4674
|
-
"/tmp/product.sock",
|
|
4675
|
-
),
|
|
4861
|
+
("socket", "/tmp/other.sock", None, "/tmp/tmux.sock"),
|
|
4862
|
+
("scoped", "/tmp/tmux.sock", None, "/tmp/product.sock"),
|
|
4676
4863
|
(
|
|
4677
4864
|
// The explicit leader provider intentionally conflicts with the
|
|
4678
4865
|
// caller provider; worker_env must reject this before command fallback.
|
|
@@ -4689,7 +4876,8 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4689
4876
|
let _provider = hermetic.with_env(CALLER_PROVIDER_ENV, "codex");
|
|
4690
4877
|
let _caller_pane = hermetic.with_env(CALLER_PANE_ENV, "%1");
|
|
4691
4878
|
let _caller_endpoint = hermetic.with_env(CALLER_ENDPOINT_ENV, endpoint);
|
|
4692
|
-
let _leader =
|
|
4879
|
+
let _leader =
|
|
4880
|
+
leader.map(|value| hermetic.with_env("TEAM_AGENT_LEADER_PROVIDER", value));
|
|
4693
4881
|
let transport = OfflineTransport::new()
|
|
4694
4882
|
.with_tmux_endpoint(scoped)
|
|
4695
4883
|
.with_pane_current_command("%1", "codex");
|
|
@@ -4702,7 +4890,6 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4702
4890
|
}
|
|
4703
4891
|
}
|
|
4704
4892
|
|
|
4705
|
-
|
|
4706
4893
|
#[test]
|
|
4707
4894
|
fn pi_tool_category_is_transparently_accepted_by_compiler() {
|
|
4708
4895
|
let root = std::env::temp_dir().join(format!(
|
|
@@ -4729,9 +4916,9 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4729
4916
|
.get("agents")
|
|
4730
4917
|
.and_then(Value::as_list)
|
|
4731
4918
|
.and_then(|agents| {
|
|
4732
|
-
agents
|
|
4733
|
-
|
|
4734
|
-
|
|
4919
|
+
agents
|
|
4920
|
+
.iter()
|
|
4921
|
+
.find(|agent| agent.get("id").and_then(Value::as_str) == Some("worker"))
|
|
4735
4922
|
})
|
|
4736
4923
|
.expect("compiled spec must retain the worker role");
|
|
4737
4924
|
assert!(
|
|
@@ -4745,8 +4932,7 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4745
4932
|
fn fresh_binding_persists_then_registers_then_requires_canonical_readback() {
|
|
4746
4933
|
let workspace = workspace_with_provider("positive-pi", "pi");
|
|
4747
4934
|
let mut ops = MockOps::with_provider("pi");
|
|
4748
|
-
assert!(bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops)
|
|
4749
|
-
.unwrap());
|
|
4935
|
+
assert!(bind_fresh_quick_start_leader_with(&workspace, "fresh", None, &mut ops).unwrap());
|
|
4750
4936
|
assert_eq!(ops.attached_provider, Some(crate::provider::Provider::Pi));
|
|
4751
4937
|
assert_eq!(ops.attach_calls, 1);
|
|
4752
4938
|
assert_eq!(ops.register_calls, 1);
|
|
@@ -4803,12 +4989,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4803
4989
|
let _caller_pane = hermetic.with_env(CALLER_PANE_ENV, "%1");
|
|
4804
4990
|
let _caller_endpoint = hermetic.with_env(CALLER_ENDPOINT_ENV, "/tmp/tmux.sock");
|
|
4805
4991
|
let seed = seeded_runtime_owner(&workspace);
|
|
4806
|
-
let mut state =
|
|
4807
|
-
&workspace,
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
.unwrap()
|
|
4811
|
-
.state;
|
|
4992
|
+
let mut state =
|
|
4993
|
+
crate::state::projection::resolve_runtime_team_scope(&workspace, Some("fresh"))
|
|
4994
|
+
.unwrap()
|
|
4995
|
+
.state;
|
|
4812
4996
|
let expected_owner = state["team_owner"].clone();
|
|
4813
4997
|
let expected_receiver = state["leader_receiver"].clone();
|
|
4814
4998
|
let target = caller_target(parent.clone(), None);
|
|
@@ -4840,12 +5024,10 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
4840
5024
|
|
|
4841
5025
|
let workspace = runtime_workspace_with_provider(&hermetic, "writer-failure", "pi");
|
|
4842
5026
|
let seed = seeded_runtime_owner(&workspace);
|
|
4843
|
-
let mut state =
|
|
4844
|
-
&workspace,
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
.unwrap()
|
|
4848
|
-
.state;
|
|
5027
|
+
let mut state =
|
|
5028
|
+
crate::state::projection::resolve_runtime_team_scope(&workspace, Some("fresh"))
|
|
5029
|
+
.unwrap()
|
|
5030
|
+
.state;
|
|
4849
5031
|
let expected_owner = state["team_owner"].clone();
|
|
4850
5032
|
let expected_receiver = state["leader_receiver"].clone();
|
|
4851
5033
|
let target = caller_target(hermetic.root().join("parent"), None);
|