Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e4b0d51
fix(fixtures): recover crash images without live Windows copies
cursoragent Sep 6, 2026
f910365
style(fixtures): backtick SQLite in crash-copy docs
cursoragent Sep 6, 2026
7c1973f
Merge branch 'codex/tracedecay-total-redesign-plan-reopened' into cur…
ScriptedAlchemy Sep 6, 2026
8518e08
Merge branch 'codex/tracedecay-total-redesign-plan-reopened' into cur…
ScriptedAlchemy Sep 6, 2026
f24ab49
fix(snapshot): prove live backup and bound crash children
cursoragent Sep 7, 2026
f18eda9
Merge remote-tracking branch 'origin/codex/tracedecay-total-redesign-…
ScriptedAlchemy Sep 7, 2026
560054a
Merge remote-tracking branch 'origin/codex/tracedecay-total-redesign-…
ScriptedAlchemy Sep 7, 2026
a3e902b
fix(snapshot): preserve dest and contend Busy correctly
cursoragent Sep 7, 2026
c6dbf47
fix(snapshot): retire failed backup families
ScriptedAlchemy Sep 7, 2026
9604339
fix(snapshot): reserve scratch; refuse non-atomic replace
cursoragent Sep 7, 2026
90e2255
Merge remote-tracking branch 'origin/codex/tracedecay-total-redesign-…
ScriptedAlchemy Sep 7, 2026
ab543d3
Merge remote-tracking branch 'origin/cursor/crash-snapshot-fixtures-9…
ScriptedAlchemy Sep 7, 2026
d1f98a1
Merge remote-tracking branch 'origin/codex/tracedecay-total-redesign-…
ScriptedAlchemy Sep 7, 2026
f03951f
Merge remote-tracking branch 'origin/codex/tracedecay-total-redesign-…
ScriptedAlchemy Sep 7, 2026
1379690
Merge remote-tracking branch 'origin/codex/tracedecay-total-redesign-…
ScriptedAlchemy Sep 7, 2026
7b8a85f
Merge remote-tracking branch 'origin/codex/tracedecay-total-redesign-…
ScriptedAlchemy Sep 7, 2026
e0a9789
Merge remote-tracking branch 'origin/codex/tracedecay-total-redesign-…
ScriptedAlchemy Sep 7, 2026
cdf9d0e
Merge remote-tracking branch 'origin/codex/tracedecay-total-redesign-…
ScriptedAlchemy Sep 7, 2026
fc060be
test(snapshot): prove staging collisions are skipped, not deleted
ScriptedAlchemy Sep 7, 2026
2897dd4
Merge branch 'codex/tracedecay-total-redesign-plan-reopened' into cur…
ScriptedAlchemy Sep 7, 2026
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
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,6 @@ jobs:
path: ${{ runner.temp }}

- name: Run Windows tests
env:
TRACEDECAY_SQLITE_UNSAFE_FAST: "1"
shell: pwsh
# --extract-to must be the workspace so target/ lands at the same
# absolute path as on the build job: integration tests bake
Expand Down
124 changes: 106 additions & 18 deletions crates/tracedecay-graph-db/src/runtime/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, mpsc};
use std::time::Duration;
Expand Down Expand Up @@ -529,9 +530,18 @@ fn snapshots_can_cross_daemon_worker_boundaries() {
assert_send_sync::<super::GraphSnapshot>();
}

const SQLITE_UNSAFE_FAST_ENV: &str = "TRACEDECAY_SQLITE_UNSAFE_FAST";
const GRAPH_CRASH_CHILD_ROOT_ENV: &str = "TRACEDECAY_GRAPH_CRASH_CHILD_ROOT";
const GRAPH_CRASH_CHILD_READY: &str = "durable-phase.ready";
const GRAPH_CRASH_CHILD_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(45);

fn walsync_db(dir: &tempfile::TempDir) -> GraphDbLeaseV1 {
walsync_db_at(dir.path())
}

fn walsync_db_at(root: &Path) -> GraphDbLeaseV1 {
GraphDbOwner::open(GraphDbOpenOptions {
location: GraphDbLocation::Persistent(dir.path().join("graph.grafeo")),
location: GraphDbLocation::Persistent(root.join("graph.grafeo")),
expected_format: GraphFormatVersion::new(2).unwrap(),
durability: GraphDurability::WalSync,
cancellation: Arc::new(NeverCancelled),
Expand All @@ -541,6 +551,62 @@ fn walsync_db(dir: &tempfile::TempDir) -> GraphDbLeaseV1 {
.unwrap()
}

fn capture_unclean_walsync_image(destination: &Path) {
let source = tempfile::tempdir().unwrap();
let test_name = std::env::var("NEXTEST_TEST_NAME")
.ok()
.or_else(|| std::thread::current().name().map(str::to_owned))
.expect("crash child spawn needs the libtest/nextest test name");
let mut child = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", &test_name, "--nocapture"])
.env(GRAPH_CRASH_CHILD_ROOT_ENV, source.path())
.env_remove(SQLITE_UNSAFE_FAST_ENV)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
.unwrap();
let started = std::time::Instant::now();
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Ok(None) if started.elapsed() < GRAPH_CRASH_CHILD_TIMEOUT => {
std::thread::sleep(std::time::Duration::from_millis(20));
}
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
panic!(
"crash child {test_name} exceeded {GRAPH_CRASH_CHILD_TIMEOUT:?} without exiting; reaped"
);
}
Err(error) => {
let _ = child.kill();
let _ = child.wait();
panic!("crash child {test_name} wait failed: {error}");
}
}
};
assert!(
status.success(),
"crash child {test_name} failed ({status:?})"
);
assert!(
source.path().join(GRAPH_CRASH_CHILD_READY).is_file(),
"crash child must reach the durable phase before the parent copies"
);
let source_store = source.path().join("graph.grafeo");
let target = destination.join("graph.grafeo");
std::fs::copy(&source_store, &target).unwrap_or_else(|error| {
panic!(
"copy abandoned crash container {} -> {}: {error}",
source_store.display(),
target.display()
);
});
copy_directory(&sidecar_wal_path(&source_store), &sidecar_wal_path(&target));
}

fn vector_batch(value: &str) -> GraphWriteBatch {
GraphWriteBatch::new(
GraphNamespace::new("project").unwrap(),
Expand Down Expand Up @@ -665,33 +731,55 @@ fn clean_shutdown_checkpoints_the_wal_so_reopen_has_no_journal_to_replay() {
reopened.close().unwrap();
}

/// A hard stop leaves the container without its latest sections and only the
/// synced WAL sidecar beside it. Opening that on-disk shape must replay the
/// journal and serve the committed write; dropping it would silently lose a
/// commit `WalSync` already acknowledged.
/// Subprocess unclean-exit, not machine power-loss: a child writes a WalSync
/// commit, exits without a clean close, and leaves the container without its
/// latest sections plus a synced WAL sidecar. Opening that on-disk shape must
/// replay the journal and serve the committed write; dropping it would
/// silently lose a commit `WalSync` already acknowledged.
#[test]
fn reopen_of_a_dirty_store_copy_replays_the_walled_commit() {
let dir = tempfile::tempdir().unwrap();
let live = walsync_db(&dir);
live.apply_unverified(scalar_batch("unclean")).unwrap();
if let Some(root) = std::env::var_os(GRAPH_CRASH_CHILD_ROOT_ENV) {
let live = walsync_db_at(Path::new(&root));
assert_eq!(live.inner.durability, GraphDurability::WalSync);
live.apply_unverified(scalar_batch("unclean")).unwrap();
std::fs::write(
Path::new(&root).join(GRAPH_CRASH_CHILD_READY),
b"wal-synced",
)
.unwrap();
std::mem::forget(live);
std::process::exit(0);
}

// Simulate the crash by snapshotting the on-disk state while the store is
// still open: nothing has checkpointed yet, so the committed batch exists
// only in the WAL sidecar the copy carries along.
let source = dir.path().join("graph.grafeo");
let source_wal = sidecar_wal_path(&source);
// Child writes the WalSync commit and exits without a clean close. The
// parent bounds that process, reaps it, and only then copies the abandoned
// image. Copying the live parent handle is illegal on Windows (lock 33).
let crash_dir = tempfile::tempdir().unwrap();
capture_unclean_walsync_image(crash_dir.path());
let target = crash_dir.path().join("graph.grafeo");
assert!(
source_wal.is_dir(),
sidecar_wal_path(&target).is_dir(),
"a WalSync store must journal into its sidecar before checkpoint"
);
let crash_dir = tempfile::tempdir().unwrap();
let target = crash_dir.path().join("graph.grafeo");
std::fs::copy(&source, &target).unwrap();
copy_directory(&source_wal, &sidecar_wal_path(&target));

let recovered = walsync_db(&crash_dir);
assert_eq!(entity_marker(&recovered), Some("unclean".to_owned()));
recovered.close().unwrap();
}

#[cfg(windows)]
#[test]
fn live_walsync_store_refuses_raw_byte_copy() {
let dir = tempfile::tempdir().unwrap();
let live = walsync_db(&dir);
live.apply_unverified(scalar_batch("live")).unwrap();
let source = dir.path().join("graph.grafeo");
let dest = dir.path().join("illegal-copy.grafeo");
let error = std::fs::copy(&source, &dest).expect_err("copying a live Windows store must fail");
assert!(
matches!(error.raw_os_error(), Some(32 | 33)),
"expected sharing/lock violation, got {error}"
);
live.close().unwrap();
}

Expand Down
Loading
Loading