mirror of
https://github.com/block/buzz.git
synced 2026-11-12 12:22:06 +00:00
test(hosts): verify ordinary restart and immutable Stop receipt recovery
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -238,3 +238,34 @@ fn destination_rejection_reports_source_stopped_never_moved_or_restarted() {
|
||||
assert_eq!(f.store.journal.sent.len(), 2);
|
||||
super::super::host_start::validate_journal(&f.store, &f.owner, RELAY).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renewed_stop_receipt_preserves_observation_and_releases_move_once() {
|
||||
let mut f = Fixture::new();
|
||||
f.receipt(Outcome::Stopped);
|
||||
let intent = f.store.journal.moves[&f.id].clone();
|
||||
let stop = f.store.journal.sent.get_mut(&intent.stop).unwrap();
|
||||
let command = stop.command.clone();
|
||||
let original = stop.receipt.clone().unwrap();
|
||||
super::super::host_start_store::prepare_receipt(stop, &f.owner, &f.source, RELAY, &[], 1200)
|
||||
.unwrap();
|
||||
let renewed = stop.receipt.as_ref().unwrap();
|
||||
assert_ne!(renewed.id, original.id);
|
||||
assert_eq!(renewed.content, original.content);
|
||||
assert_eq!(renewed.tags, original.tags);
|
||||
assert_eq!(stop.command, command);
|
||||
let req = request(&f.owner, stop, RELAY).unwrap();
|
||||
let observation =
|
||||
host_execution::decrypt_receipt(&f.owner, &stop.registration, renewed, &command, &req)
|
||||
.unwrap();
|
||||
assert_eq!(observation.observed_at, 110);
|
||||
assert_eq!(observation.outcome, Outcome::Stopped);
|
||||
f.store.save().unwrap();
|
||||
drop(f.store);
|
||||
f.store = Store::open_dir(f.dir.path(), &f.owner.public_key().to_hex(), RELAY).unwrap();
|
||||
release_to_outbox(&mut f.store, &f.owner, RELAY, &f.id, &intent, 1201).unwrap();
|
||||
let released = f.store.journal.moves[&f.id].clone();
|
||||
assert!(release_to_outbox(&mut f.store, &f.owner, RELAY, &f.id, &released, 1202).is_err());
|
||||
assert_eq!(f.store.journal.sent.len(), 2);
|
||||
super::super::host_start::validate_journal(&f.store, &f.owner, RELAY).unwrap();
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ pub(super) async fn trace(
|
||||
"ordinary-stop-success.json",
|
||||
&serde_json::to_value(peer_stop).map_err(|_| "Stop status")?,
|
||||
)?;
|
||||
exercise_ordinary_restart(app, root, &peer, relay_url, &peer_run)?;
|
||||
exercise_ordinary_restart(app, root, &peer, relay_url, &peer_run).await?;
|
||||
save(root, "move-finish", &json!({"done":true}))?;
|
||||
cleanup(app)?;
|
||||
return Ok(());
|
||||
@@ -361,7 +361,7 @@ pub(super) async fn trace(
|
||||
|
||||
// Exercise the existing pair Start/Restart entry points, not a fixture-only
|
||||
// ledger shortcut. A stale Stop retry must not label a live successor stopped.
|
||||
fn exercise_ordinary_restart(
|
||||
async fn exercise_ordinary_restart(
|
||||
app: &tauri::AppHandle,
|
||||
root: &Path,
|
||||
peer: &str,
|
||||
@@ -387,6 +387,12 @@ fn exercise_ordinary_restart(
|
||||
{
|
||||
return Err("old successful Stop retry misreported successor as stopped".into());
|
||||
}
|
||||
save(
|
||||
root,
|
||||
"ordinary-start-success.json",
|
||||
&serde_json::to_value(&started).map_err(|_| "Start status")?,
|
||||
)?;
|
||||
wait_live_run(app, peer, relay_url, &started_run).await?;
|
||||
let restarted = managed_agents::restart_managed_agent_runtime(
|
||||
peer.into(),
|
||||
relay_url.into(),
|
||||
@@ -400,6 +406,7 @@ fn exercise_ordinary_restart(
|
||||
if restarted_run == started_run || restarted.pid.is_none() {
|
||||
return Err("ordinary Restart did not create a fresh runtime".into());
|
||||
}
|
||||
wait_live_run(app, peer, relay_url, &restarted_run).await?;
|
||||
let stopped = managed_agents::stop_managed_agent_runtime(
|
||||
peer.into(),
|
||||
relay_url.into(),
|
||||
@@ -416,6 +423,36 @@ fn exercise_ordinary_restart(
|
||||
)
|
||||
}
|
||||
|
||||
// Spawn is explicitly not Ready. Exercise the happy path only after observing
|
||||
// this exact new runtime on the relay; an immediate startup Stop may fail closed.
|
||||
async fn wait_live_run(
|
||||
app: &tauri::AppHandle,
|
||||
agent: &str,
|
||||
relay_url: &str,
|
||||
run: &str,
|
||||
) -> Result<(), String> {
|
||||
let state = app.state::<AppState>();
|
||||
let owner = state.signing_keys()?.public_key().to_hex();
|
||||
for _ in 0..60 {
|
||||
let runs = commands::get_presence_runs(
|
||||
state.clone(),
|
||||
owner.clone(),
|
||||
relay_url.into(),
|
||||
vec![agent.into()],
|
||||
)
|
||||
.await?;
|
||||
let now = nostr::Timestamp::now().as_secs();
|
||||
if runs.get(agent).is_some_and(|runs| {
|
||||
runs.iter()
|
||||
.any(|r| r.run == run && r.status != "offline" && r.expires_at > now)
|
||||
}) {
|
||||
return Ok(());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
Err("ordinary restart generation not observed live".into())
|
||||
}
|
||||
|
||||
async fn pulse(
|
||||
app: &tauri::AppHandle,
|
||||
owner: &str,
|
||||
|
||||
@@ -91,6 +91,10 @@ pub fn run() -> Result<(), String> {
|
||||
.map_err(|_| "native app build failed")?;
|
||||
let handle = app.handle().clone();
|
||||
let relay = args[3].clone();
|
||||
// Use run_return: Wry's non-returning run can discard AppHandle::exit's
|
||||
// requested code. Success also requires that the trace actually completed.
|
||||
let completed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let task_completed = completed.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let result = if role.starts_with("move-") {
|
||||
move_trace::trace(&handle, &role, &root, &relay).await
|
||||
@@ -98,15 +102,22 @@ pub fn run() -> Result<(), String> {
|
||||
trace(&handle, &role, &root, &relay).await
|
||||
};
|
||||
match result {
|
||||
Ok(()) => handle.exit(0),
|
||||
Ok(()) => {
|
||||
task_completed.store(true, std::sync::atomic::Ordering::Release);
|
||||
handle.exit(0);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Start tracer failed: {error}");
|
||||
handle.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
app.run(|_, _| {});
|
||||
Ok(())
|
||||
let exit_code = app.run_return(|_, _| {});
|
||||
if exit_code == 0 && completed.load(std::sync::atomic::Ordering::Acquire) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("tracer did not complete successfully; inspect result files and logs".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn keys(root: &Path, name: &str) -> Result<Keys, String> {
|
||||
|
||||
@@ -117,8 +117,13 @@ export async function stopManagedAgentRuntime(
|
||||
export async function restartManagedAgentRuntime(
|
||||
pubkey: string,
|
||||
relayUrl: string,
|
||||
selectedRunId?: string | null,
|
||||
): Promise<ManagedAgentRuntimeStatus> {
|
||||
return invokeTauri("restart_managed_agent_runtime", { pubkey, relayUrl });
|
||||
return invokeTauri("restart_managed_agent_runtime", {
|
||||
pubkey,
|
||||
relayUrl,
|
||||
selectedRunId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function putManagedAgentRuntimeLifecycle(
|
||||
|
||||
@@ -124,7 +124,10 @@ cannot be reported as stopped by replaying that earlier success.
|
||||
|
||||
An explicit ordinary Start after confirmed exact Stop, and the existing pair
|
||||
Restart action, enter the same ledger for a fresh generation. Automatic reconcile
|
||||
remains fenced. Provider `!shutdown` behavior is unchanged. Config-edit, delete
|
||||
remains fenced. Spawn is not Ready: an immediate Stop during ACP pool initialization
|
||||
can return RootExited, even when partial children were drained. It cannot certify
|
||||
replacement; the actual tracer preserves this negative case rather than forcing
|
||||
Stopped. Provider `!shutdown` behavior is unchanged. Config-edit, delete
|
||||
and application cleanup retain their separate best-effort behavior and are not
|
||||
certified Stop claims. No new run authority or provider control channel is added.
|
||||
|
||||
@@ -260,8 +263,13 @@ Cleanup reaped only the fixture's tracked root, not a certified Stop.
|
||||
The 2026-08-31 Move tracer additionally completed confirmed selected-source Stop
|
||||
followed by destination spawn with the same agent and matching fresh run/host.
|
||||
The unrelated source peer survived Move and then completed its own ordinary Stop.
|
||||
An old source token was rejected at the destination. Both executors exited 0;
|
||||
fixture cleanup was not used as certification evidence.
|
||||
An old source token was rejected at the destination. The expanded tracer also
|
||||
exercised ordinary Start after confirmed Stop, waited for that exact new run's
|
||||
signed live presence, then used ordinary Restart and finally its own confirmed
|
||||
Stop. Retrying the earlier successful Stop could not report the successor stopped.
|
||||
Both executors completed with the required result files; fixture cleanup was not
|
||||
used as certification evidence. A debug tracer failure is checked from its result
|
||||
and log as well as exit code (the tracer uses the returning native event loop).
|
||||
|
||||
Remaining gates include real-provider and second-physical-host validation,
|
||||
actual Hosts native UI capture, authenticated asynchronous lifecycle receipt
|
||||
|
||||
Reference in New Issue
Block a user