Walk rows lows: Rust @directory error kind and delete, stale known-gaps (#116)

This commit is contained in:
Libby 2026-11-11 18:11:55 +00:00
commit 19b95ba2ab
4 changed files with 16 additions and 4 deletions

View file

@ -1609,7 +1609,7 @@ The one door per runtime, and who walks through it:
- **Shared paths.** cultnet-ts reaches the header decision only through `readSingleFileStore` (AUTH:4, :321) and checks only record count, schema id and type. cultnet-ts therefore needs a cultcache-ts release that exports it, which is the release that ships with this change; the manifests move to that version then (c0-ships-with-c2a), not before.
- **Cut line.** `merge-r1` h3 (Rust) and h4 (TS, cultnet-ts); spec `cut-merge-r1.r3` authority map.
- **Pinned by.** `tests/vectors/document-variants-c2a/readability/manifest.txt`, walked by `StoreReadabilityTests.OpenReadsExactlyTheFilesTheVectorsSayItReads` (C#), `CultMeshSingleFileReadabilityTests.AWriteReplacesAFileExactlyWhenItReads` (Mesh), `a_file_is_replaced_exactly_when_it_reads` (RS:5132), "SingleFileMessagePackBackingStore replaces a file exactly when it reads" (TS test :2321) and `test_single_file_replaces_a_file_exactly_when_it_reads` (Python). AUTH cases: `packages/cultnet-ts/test/idunn-runtime-authority.test.ts`. Declared divergences in the manifest: `legacy-envelopes.bin` (C# refuses; the others read and replace it) and `variant-slot-v3.bin` (C# reads; the others refuse).
- **Known gaps.** `variants:follow_up:c0-bad-canonical-in-manifest` (`current-catalog-bad-canonical.msgpack` is not in the manifest; TS reads it, Rust refuses). `variants:follow_up:legacy-vectors-all-runtimes` (the three `legacy-catalog-*` c0 vectors have a verdict only in C# tests; all four runtimes refuse them, probed). `variants:follow_up:range-check-in-ci` (`npm run test:pack:ts` builds every admitted tag and checks exports, but `.github/workflows/publish-packages.yml` does not run it).
- **Known gaps.** `variants:follow_up:range-check-in-ci` (`npm run test:pack:ts` builds every admitted tag and checks exports, but `.github/workflows/publish-packages.yml` does not run it).
### B. Existence and links (what "nothing at the path" means)
@ -1640,7 +1640,7 @@ The one door per runtime, and who walks through it:
- **Shared paths.** Open, commit, Mesh read and write.
- **Cut line.** `mesh-single-file-reader` h1 (the C# clause). Rust, TS and Python were correct by construction of their reads.
- **Pinned by.** C#: `ADirectoryAtTheStorePathIsAnIoErrorOnOpenAndOnCommitNotAnEmptyStore` (`StoreReadabilityTests.cs:236`). Mesh: `ADirectoryAtThePathIsAnIoErrorOnReadAndWriteAndIsLeftAlone` (:149). Rust: `a_write_over_a_directory_stops_at_the_read_and_leaves_no_staging_file` RS:5726.
- **Known gaps.** TS and Python have no test for a directory at the store path. No follow-up is registered; the natural home is `write-set-vectors` with the other readability pins.
- **Known gaps.** None open. The `@directory` manifest row is walked by all four runtimes: every operation including delete is refused and leaves the directory untouched; Rust pins an I/O failure rather than a typed unreadable store.
### E. Lock

View file

@ -6148,7 +6148,12 @@ mod tests {
let bytes = if directory { Vec::new() } else { std::fs::read(root.join(vector))? };
// A v3 store keeps its marker through a rewrite; every other file that reads is written v1.
let header = if vector.contains("v3") { "cultcache.store.v3" } else { STORE_FORMAT_V1 };
for operation in ["open", "soft_push_all", "push_all", "push"] {
let operations: &[&str] = if directory {
&["open", "soft_push_all", "push_all", "push", "delete"]
} else {
&["open", "soft_push_all", "push_all", "push"]
};
for &operation in operations {
let temp = tempfile::tempdir()?;
let path = temp.path().join("store.msgpack");
if directory {
@ -6161,11 +6166,15 @@ mod tests {
"open" => store.pull_all().map(|_| ()),
"soft_push_all" => store.push_all(&[snapshot_envelope("x", b"one")], PushAllOptions { soft: true }),
"push_all" => store.push_all(&[snapshot_envelope("x", b"one")], PushAllOptions::default()),
"delete" => store.delete(&snapshot_envelope("x", b"one")),
_ => store.push(&snapshot_envelope("x", b"one")),
};
let what = format!("{vector} {operation}");
assert_eq!(outcome.is_ok(), reads, "{what}: {outcome:?}");
if directory {
// A directory is an I/O failure, never a typed unreadable store.
let error = outcome.unwrap_err();
assert!(io_error_in(&error).is_some() && unreadable_kind(&error).is_none(), "{what}: {error:#}");
assert!(path.is_dir() && std::fs::read_dir(&path)?.next().is_none(), "{what} touched the directory");
continue;
}

View file

@ -2635,7 +2635,7 @@ test("SingleFileMessagePackBackingStore replaces a file exactly when it reads",
assert.ok(!keys.includes(envelopes[0]!.key), `${vector} ${operation}`);
}
} else if (isDirectory) {
await assert.rejects(run, `${vector} ${operation}`);
await assert.rejects(run, (error) => !(error instanceof StoreUnreadableError), `${vector} ${operation}: an I/O failure, not a typed refusal`);
assert.ok((await lstat(file)).isDirectory(), `${vector} ${operation} replaced the directory`);
assert.deepEqual(await readdir(file), [], `${vector} ${operation} wrote into the directory`);
} else {

View file

@ -314,6 +314,9 @@ namespace GameCult.Caching.Tests
Assert.That(() => cache.Commit(batch => batch.Upsert(typeof(IdDeck), new IdDeck { Name = "e" }, new CultRecordKey("e"))),
Throws.TypeOf<IOException>().With.Message.Contains("directory"));
Assert.That(() => cache.Commit(batch => batch.Remove(new CultRecordKey("e"))),
Throws.TypeOf<IOException>().With.Message.Contains("directory"));
Assert.That(Directory.GetFileSystemEntries(path), Is.Empty);
Assert.That(Directory.GetFileSystemEntries(later), Is.Empty);
}