From c08f060ca1455945d6747a4b30663046be0e7fa6 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 8 Apr 2026 04:09:55 +0900 Subject: [PATCH] test(tools): end-to-end stall-detect and recovery loop coverage Proves the clawhip restart/recover flow that gaebal-gajae flagged: 1. stall_detect_and_resolve_trust_end_to_end - Worker created without trusted_roots -> trust_auto_resolve=false - WorkerObserve with trust-prompt text -> status=trust_required, gate cleared=false - WorkerResolveTrust -> status=spawning, trust_gate_cleared=true - WorkerObserve with ready text -> status=ready_for_prompt Full resolve path verified end-to-end. 2. stall_detect_and_restart_recovery_end_to_end - Worker stalls at trust_required - WorkerRestart resets to spawning, trust_gate_cleared=false Documents the restart-then-re-acquire-trust flow. Note: seconds_since_update is in .claw/worker-state.json (state file), not in the Worker tool output struct. Staleness detection via state file is covered by emit_state_file_writes_worker_status_on_transition in worker_boot.rs tests. 87 tool tests passing, 0 failing. --- rust/crates/tools/src/lib.rs | 95 ++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index 29ac208..94bdf29 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -5618,6 +5618,101 @@ mod tests { ); } + #[test] + fn stall_detect_and_resolve_trust_end_to_end() { + // 1. Create worker WITHOUT trusted_roots so trust won't auto-resolve + let created = execute_tool( + "WorkerCreate", + &json!({"cwd": "/no/trusted/root/here"}), + ) + .expect("WorkerCreate should succeed"); + let created_output: serde_json::Value = serde_json::from_str(&created).expect("json"); + let worker_id = created_output["worker_id"].as_str().expect("worker_id").to_string(); + assert_eq!(created_output["trust_auto_resolve"], false); + + // 2. Observe trust prompt screen text -> worker stalls at trust_required + let stalled = execute_tool( + "WorkerObserve", + &json!({ + "worker_id": worker_id, + "screen_text": "Do you trust the files in this folder?\n[Allow] [Deny]" + }), + ) + .expect("WorkerObserve should succeed"); + let stalled_output: serde_json::Value = serde_json::from_str(&stalled).expect("json"); + assert_eq!( + stalled_output["status"], "trust_required", + "worker should stall at trust_required when trust prompt seen without allowlist" + ); + assert_eq!(stalled_output["trust_gate_cleared"], false); + // 3. Clawhip calls WorkerResolveTrust to unblock + let resolved = execute_tool( + "WorkerResolveTrust", + &json!({"worker_id": worker_id}), + ) + .expect("WorkerResolveTrust should succeed"); + let resolved_output: serde_json::Value = serde_json::from_str(&resolved).expect("json"); + assert_eq!( + resolved_output["status"], "spawning", + "worker should return to spawning after trust resolved" + ); + assert_eq!(resolved_output["trust_gate_cleared"], true); + + // 4. Ready screen text now advances worker normally + let ready = execute_tool( + "WorkerObserve", + &json!({ + "worker_id": worker_id, + "screen_text": "Ready for input\n>" + }), + ) + .expect("WorkerObserve should succeed after trust resolved"); + let ready_output: serde_json::Value = serde_json::from_str(&ready).expect("json"); + assert_eq!( + ready_output["status"], "ready_for_prompt", + "worker should reach ready_for_prompt after trust resolved and ready screen seen" + ); + } + + #[test] + fn stall_detect_and_restart_recovery_end_to_end() { + // Worker stalls at trust_required, clawhip restarts instead of resolving + let created = execute_tool( + "WorkerCreate", + &json!({"cwd": "/no/trusted/root/restart-test"}), + ) + .expect("WorkerCreate should succeed"); + let created_output: serde_json::Value = serde_json::from_str(&created).expect("json"); + let worker_id = created_output["worker_id"].as_str().expect("worker_id").to_string(); + + // Force trust_required + let stalled = execute_tool( + "WorkerObserve", + &json!({ + "worker_id": worker_id, + "screen_text": "trust this folder? [Yes] [No]" + }), + ) + .expect("WorkerObserve should succeed"); + let stalled_output: serde_json::Value = serde_json::from_str(&stalled).expect("json"); + assert_eq!(stalled_output["status"], "trust_required"); + + // WorkerRestart resets the worker + let restarted = execute_tool( + "WorkerRestart", + &json!({"worker_id": worker_id}), + ) + .expect("WorkerRestart should succeed"); + let restarted_output: serde_json::Value = serde_json::from_str(&restarted).expect("json"); + assert_eq!( + restarted_output["status"], "spawning", + "restarted worker should be back at spawning" + ); + assert_eq!(restarted_output["trust_gate_cleared"], false, + "restart clears trust — next observe loop must re-acquire trust" + ); + } + #[test] fn worker_terminate_on_unknown_id_returns_error() { let result = execute_tool(