Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions crates/cyber-tools/tests/worktree_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
10 changes: 10 additions & 0 deletions openspec/changes/complete-plugin-hook-runtime/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading