From aee5263aefcb460f4b051baef7008400a5a0db2b Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 8 Apr 2026 04:38:38 +0900 Subject: [PATCH] test(tools): prove recovery loop against .claw/worker-state.json directly recovery_loop_state_file_reflects_transitions reads the actual state file after each transition to verify the canonical observability surface reflects the full stall->resolve->ready progression: spawning (state file exists, seconds_since_update present) -> trust_required (is_ready=false, trust_gate_cleared=false in file) -> spawning (trust_gate_cleared=true after WorkerResolveTrust) -> ready_for_prompt (is_ready=true after ready screen observe) This is the end-to-end proof gaebal-gajae called for: clawhip polling .claw/worker-state.json will see truthful state at every step of the recovery loop, including the seconds_since_update staleness signal. 90 tool tests passing, 0 failing. --- rust/crates/tools/src/lib.rs | 74 ++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index 37145fc..4d9d612 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -5656,6 +5656,80 @@ mod tests { ); } + #[test] + fn recovery_loop_state_file_reflects_transitions() { + // End-to-end proof: .claw/worker-state.json reflects every transition + // through the stall-detect -> resolve-trust -> ready loop. + use std::fs; + + // Use a real temp CWD so state file can be written + let worktree = temp_path("recovery-loop-state"); + fs::create_dir_all(&worktree).expect("create worktree"); + let cwd = worktree.to_str().expect("utf-8").to_string(); + let state_path = worktree.join(".claw").join("worker-state.json"); + + // 1. Create worker WITHOUT trusted_roots + let created = execute_tool( + "WorkerCreate", + &json!({"cwd": cwd}), + ) + .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(); + // State file should exist after create + assert!(state_path.exists(), "state file should be written after WorkerCreate"); + let state: serde_json::Value = serde_json::from_str( + &fs::read_to_string(&state_path).expect("read state") + ).expect("parse state"); + assert_eq!(state["status"], "spawning"); + assert_eq!(state["is_ready"], false); + assert!(state["seconds_since_update"].is_number(), "seconds_since_update must be present"); + + // 2. Force trust_required via observe + execute_tool( + "WorkerObserve", + &json!({"worker_id": worker_id, "screen_text": "Do you trust the files in this folder?"}), + ) + .expect("WorkerObserve should succeed"); + let state: serde_json::Value = serde_json::from_str( + &fs::read_to_string(&state_path).expect("read state") + ).expect("parse state"); + assert_eq!(state["status"], "trust_required", + "state file must reflect trust_required stall"); + assert_eq!(state["is_ready"], false); + assert_eq!(state["trust_gate_cleared"], false); + assert!(state["seconds_since_update"].is_number()); + + // 3. WorkerResolveTrust -> state file reflects recovery + execute_tool( + "WorkerResolveTrust", + &json!({"worker_id": worker_id}), + ) + .expect("WorkerResolveTrust should succeed"); + let state: serde_json::Value = serde_json::from_str( + &fs::read_to_string(&state_path).expect("read state") + ).expect("parse state"); + assert_eq!(state["status"], "spawning", + "state file must show spawning after trust resolved"); + assert_eq!(state["trust_gate_cleared"], true); + + // 4. Observe ready screen -> state file shows ready_for_prompt + execute_tool( + "WorkerObserve", + &json!({"worker_id": worker_id, "screen_text": "Ready for input\n>"}), + ) + .expect("WorkerObserve ready should succeed"); + let state: serde_json::Value = serde_json::from_str( + &fs::read_to_string(&state_path).expect("read state") + ).expect("parse state"); + assert_eq!(state["status"], "ready_for_prompt", + "state file must show ready_for_prompt after ready screen"); + assert_eq!(state["is_ready"], true, + "is_ready must be true in state file at ready_for_prompt"); + + fs::remove_dir_all(&worktree).ok(); + } + #[test] fn stall_detect_and_resolve_trust_end_to_end() { // 1. Create worker WITHOUT trusted_roots so trust won't auto-resolve