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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/freshell-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,8 @@ reqwest = { version = "0.13", default-features = false, features = ["json", "rus
# `crates/freshell-opencode/Cargo.toml`'s dev-dependency comment). No new
# download — tokio is already a direct dependency above.
tokio = { version = "1", features = ["test-util"] }
# `test_trace_capture.rs` installs the test binary's one process-wide tracing
# subscriber before `main`, so no test thread can fire a callsite while it is
# being installed (tracing-core would cache that callsite as uninterested for
# the rest of the process). Already in the workspace lock via tauri-utils.
ctor = "0.8"
63 changes: 57 additions & 6 deletions crates/freshell-server/src/checkpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,9 @@ pub struct CheckpointService {
test_capture_calls: std::sync::atomic::AtomicUsize,
#[cfg(test)]
test_capture_entered: tokio::sync::Notify,
/// When the latest automatic attempt finished resolving its workspace.
#[cfg(test)]
test_workspace_resolved_at: StdMutex<Option<Instant>>,
}

tokio::task_local! {
Expand Down Expand Up @@ -899,6 +902,8 @@ impl CheckpointService {
test_capture_calls: std::sync::atomic::AtomicUsize::new(0),
#[cfg(test)]
test_capture_entered: tokio::sync::Notify::new(),
#[cfg(test)]
test_workspace_resolved_at: StdMutex::new(None),
}
}

Expand Down Expand Up @@ -2352,6 +2357,13 @@ impl CheckpointService {
return record;
}
};
#[cfg(test)]
{
*self
.test_workspace_resolved_at
.lock()
.expect("workspace resolution timestamp mutex poisoned") = Some(Instant::now());
}
if let Some(remaining) = self.active_cooldown(&workspace.path) {
log_automatic_checkpoint_failure(
Some(request_id),
Expand Down Expand Up @@ -9046,26 +9058,65 @@ mod tests {
.is_empty());
}

/// During a timeout cooldown an automatic attempt answers
/// `capture_timeout_cooldown` as soon as its workspace is known, and the
/// message is forwarded: the attempt neither waits for the workspace's
/// capture lock nor starts a capture, so it never repeats the capture
/// wait. The test holds that lock throughout, so an attempt that waited
/// for it (or captured under it) would settle as `workspace_lock_timeout`
/// instead. The answer's latency is measured from the moment workspace
/// resolution returns: every automatic attempt resolves its workspace
/// (which runs Git) before it can look up the cooldown, so that part
/// follows machine load, not the cooldown policy.
#[tokio::test]
async fn cooldown_sends_immediately_without_capture() {
let home = tempfile::tempdir().unwrap();
let workspace = tempfile::tempdir().unwrap();
let workspace_dir = tempfile::tempdir().unwrap();
// The cooldown and the capture lock are keyed by the resolved path.
let workspace = std::fs::canonicalize(workspace_dir.path()).unwrap();
let service = checkpoint_test_service(home.path());
service.establish_cooldown(workspace.path());
service.establish_cooldown(&workspace);
let _capture_lock = service
.lock_workspace(&workspace, Duration::from_secs(1))
.await
.expect("the test holds the workspace's capture lock");

let (outcome, dispatches, elapsed) = run_checkpoint_policy_send(
let (outcome, dispatches, _) = run_checkpoint_policy_send(
Arc::clone(&service),
checkpoint_policy_send(workspace.path(), "cooldown-send", "cooldown-attempt"),
checkpoint_policy_send(&workspace, "cooldown-send", "cooldown-attempt"),
)
.await;

assert_eq!(outcome["status"], "unavailable");
assert_eq!(outcome["code"], "capture_timeout_cooldown");
assert_eq!(dispatches, 1);
assert_eq!(service.test_capture_calls.load(Ordering::SeqCst), 0);

// Time the cooldown answer itself, from the resolved workspace on.
*service.test_workspace_resolved_at.lock().unwrap() = None;
let send = checkpoint_policy_send(&workspace, "cooldown-timed", "cooldown-timed-attempt");
let record = service
.capture_automatic(&send, "cooldown-timed", "cooldown-timed-attempt")
.await;
let answered_at = Instant::now();
let resolved_at = service
.test_workspace_resolved_at
.lock()
.unwrap()
.expect("the attempt resolved its workspace");
assert_eq!(
record.outcome.code.as_deref(),
Some("capture_timeout_cooldown")
);
assert!(
record.capture_settled,
"a cooldown answer forwards the send"
);
assert_eq!(service.test_capture_calls.load(Ordering::SeqCst), 0);
let answer = answered_at.duration_since(resolved_at);
assert!(
elapsed < Duration::from_secs(1),
"cooldown should not repeat the capture wait: {elapsed:?}"
answer < Duration::from_secs(1),
"cooldown should answer without waiting once the workspace is known: {answer:?}"
);
}

Expand Down
4 changes: 3 additions & 1 deletion crates/freshell-server/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,9 @@ mod tests {
// A shared process-global collector avoids competing test modules
// installing subscribers with disconnected event buffers. Thread-local
// set_default proved nondeterministic under parallel cargo test because
// of callsite interest caching; this test filters by its unique path.
// of callsite interest caching, and so did installing the collector
// lazily (see `test_trace_capture`: it is installed before `main`);
// this test filters by its unique path.
let events = crate::test_trace_capture::captured_trace_events();

let root = tmp();
Expand Down
Loading
Loading