diff --git a/crates/cyber-tools/tests/worktree_setup.rs b/crates/cyber-tools/tests/worktree_setup.rs index cceb3887..942659bd 100644 --- a/crates/cyber-tools/tests/worktree_setup.rs +++ b/crates/cyber-tools/tests/worktree_setup.rs @@ -2690,6 +2690,104 @@ async fn cwd_changed_idle_exit_is_committed_once_and_replay_is_silent() { assert_eq!(cwd_history(&flow, &id), before); } +#[cfg(unix)] +#[tokio::test] +async fn cwd_changed_failed_idle_exit_commit_is_silent_and_retry_commits_once() { + use std::sync::atomic::{AtomicBool, Ordering}; + use support::flow::{call, text}; + + let flow = worktree_flow(vec![ + call("enter", "enter_worktree", json!({"name":"failed-cwd-exit"})), + text("done"), + ]); + let recorder = CwdRecorder::start(false).await; + recorder.attach(&flow); + let id = flow.session("default").await; + flow.prompt(&id, "enter and finish").await; + flow.settle_within(&id, Duration::from_secs(60)).await; + let entered = recorder.events(); + assert_eq!(entered.len(), 1); + assert_cwd_transition(&flow, &id, &entered[0]); + let directory = flow.runtime.state(&id).await.unwrap().info.directory; + assert_ne!(directory, flow.f.repo.display().to_string()); + + let attempted = Arc::new(AtomicBool::new(false)); + let observed = Arc::clone(&attempted); + let session = id.clone(); + flow.f + .store + .set_append_fault(Some(Arc::new(move |owner, event| { + let refuse = owner == session && event.kind == "session.worktree.exited.1"; + if refuse { + observed.store(true, Ordering::SeqCst); + } + refuse + }))); + let failed = tokio::time::timeout( + Duration::from_secs(30), + flow.runtime.exit_idle_worktree(&id), + ) + .await; + flow.f.store.set_append_fault(None); + let error = failed.expect("failed commit settles").unwrap_err(); + assert!(attempted.load(Ordering::SeqCst)); + assert!( + error.to_string().contains("injected append fault"), + "{error}" + ); + assert_eq!( + flow.runtime.state(&id).await.unwrap().info.directory, + directory + ); + assert_eq!( + recorder.events(), + entered, + "a failed commit emits no CwdChanged" + ); + assert!( + cwd_history(&flow, &id) + .iter() + .all(|event| event.kind != "session.worktree.exited.1") + ); + + tokio::time::timeout( + Duration::from_secs(30), + flow.runtime.exit_idle_worktree(&id), + ) + .await + .expect("retry settles") + .unwrap() + .expect("retry commits the idle exit"); + assert_eq!( + flow.runtime.state(&id).await.unwrap().info.directory, + flow.f.repo.display().to_string() + ); + let events = recorder.events(); + assert_eq!(events.len(), 2, "{events:?}"); + assert_cwd_transition(&flow, &id, &events[1]); + assert_eq!(events[1]["transition"], "session.worktree.exited.1"); + assert_eq!(events[1]["previous_directory"], directory); + assert_eq!( + cwd_history(&flow, &id) + .iter() + .filter(|event| event.kind == "session.worktree.exited.1") + .count(), + 1 + ); + assert!( + flow.runtime + .exit_idle_worktree(&id) + .await + .unwrap() + .is_none() + ); + assert_eq!( + recorder.events(), + events, + "an already exited Session is silent" + ); +} + #[cfg(unix)] #[tokio::test] async fn cwd_changed_does_not_observe_permission_refusal() { diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index f4428033..6c5bd4b3 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -80,6 +80,16 @@ required; this correction does not close tasks 2.1–2.4. - [ ] 5.2 Implement bounded `FileChanged` observation/reconciliation for tools, formatters, shell/background and external edits, including overflow and self-write loop prevention. Verify alias-aware Session admission and resource/lifecycle behavior before closing this task. See [file-observation-implementation.md](file-observation-implementation.md) for implementation and verification evidence. - [ ] 5.3 Test no false post-effect success for refused/failed/unknown transitions, pre-effect WorktreeCreate veto/no Git mutation, stable event identity, correct scope/order, rename/delete, multiple Sessions, shutdown/restart and unknown handler outcomes. +The CwdChanged regression +`cwd_changed_failed_idle_exit_commit_is_silent_and_retry_commits_once` in +`crates/cyber-tools/tests/worktree_setup.rs` exercises a real failed idle-exit +append and HTTP hook delivery. It verifies that the fault was reached, the +Location stays unchanged, and no durable exit or CwdChanged appears. After the +fault is cleared, retry commits one exit with the matching hook identity; another +exit is silent. The exact case passes locally on macOS. This is a known +before-append failure, not proof of an unknown commit outcome. The test is +Unix-only and hosted acceptance remains required; task 5.3 stays open. + Task 5.2 is in progress. The full focused suite at `3e852288` passed Linux/macOS/Windows. At `c0cc40e3`, complete Linux/Windows jobs also passed all 18 HTTP/runtime scenarios, including idle-return ownership, archive/resume and native burst recovery; macOS remained queued. Review then reproduced an oversized metadata baseline within the count/path limits. The new publication/batch byte-budget fix passes 63 local source-library tests and scoped Clippy, and its focused owner/application regression workflow at `5aa08018` now passes on Linux/macOS/Windows. Combined producer acceptance is pending on this integration branch. Remaining resource/lifecycle acceptance, broader workspace checks and substantive review are still open. See [the scope integration design](file-observation-scope-design.md) and [implementation notes](file-observation-implementation.md). Setup ownership is expanded in [setup-design.md](setup-design.md). Its Store