diff --git a/packages/cultcache-rs/src/lib.rs b/packages/cultcache-rs/src/lib.rs index ecb69145..6c2c9030 100644 --- a/packages/cultcache-rs/src/lib.rs +++ b/packages/cultcache-rs/src/lib.rs @@ -5437,6 +5437,41 @@ mod tests { Ok(()) } + #[test] + fn push_all_refuses_a_store_in_a_format_this_runtime_does_not_read_and_leaves_it() -> Result<()> { + let vector = fs::read( + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/vectors/document-variants-c2a/v3-base.msgpack"), + )?; + let entry = CultCacheEnvelope { + key: "probe".into(), + r#type: "probe".into(), + payload: vec![0x80], + stored_at: "2026-10-22T00:00:00Z".into(), + schema_id: None, + }; + // A newer store whole, and cut short after its header: the header alone is judged. + for bytes in [&vector[..], &vector[..vector.len() / 2]] { + let temp = tempfile::tempdir()?; + let path = temp.path().join("store.msgpack"); + fs::write(&path, bytes)?; + let error = SingleFileMessagePackBackingStore::new(&path) + .push_all(std::slice::from_ref(&entry), PushAllOptions::default()) + .unwrap_err(); + assert_eq!(unreadable_kind(&error), Some(CultCacheStoreUnreadableKind::UnsupportedFormat), "{error:#}"); + assert!(format!("{error:#}").contains("cultcache.store.v3"), "{error:#}"); + assert_eq!(fs::read(&path)?, bytes); + } + // A file with no readable stamp is not a newer store: a whole write still heals it. + let temp = tempfile::tempdir()?; + let path = temp.path().join("store.msgpack"); + fs::write(&path, [0x93u8, 0xa0])?; + let mut store = SingleFileMessagePackBackingStore::new(&path); + store.push_all(std::slice::from_ref(&entry), PushAllOptions::default())?; + assert_eq!(store.pull_all()?.len(), 1); + Ok(()) + } + + #[test] fn a_store_that_cannot_be_read_is_an_io_error_and_never_unreadable() -> Result<()> { let temp = tempfile::tempdir()?; diff --git a/packages/cultcache-ts/test/cult-cache.test.ts b/packages/cultcache-ts/test/cult-cache.test.ts index 86d4a090..605c68d9 100644 --- a/packages/cultcache-ts/test/cult-cache.test.ts +++ b/packages/cultcache-ts/test/cult-cache.test.ts @@ -1481,6 +1481,22 @@ test("SingleFileMessagePackBackingStore refusals say what was found without blam ); }); +test("pushAll refuses a store in a format this runtime does not read and leaves it byte-identical", async () => { + const vector = await readFile(resolve(cultLibRoot, "tests", "vectors", "document-variants-c2a", "v3-base.msgpack")); + const envelope = { key: "probe", type: "probe", payload: documentPayloadFromBytes(new Uint8Array([0x80])), storedAt: "2026-10-22T00:00:00Z" }; + for (const bytes of [vector, vector.subarray(0, Math.floor(vector.length / 2))]) { + const file = join(await mkdtemp(join(tmpdir(), "cultcache-pushall-v3-")), "store.msgpack"); + await writeFile(file, bytes); + await assert.rejects(() => new SingleFileMessagePackBackingStore(file).pushAll([envelope]), /cultcache\.store\.v3/u); + assert.equal(Buffer.compare(await readFile(file), bytes), 0); + } + const healed = join(await mkdtemp(join(tmpdir(), "cultcache-pushall-heal-")), "store.msgpack"); + await writeFile(healed, new Uint8Array([0x93, 0xa0])); + const store = new SingleFileMessagePackBackingStore(healed); + await store.pushAll([envelope]); + assert.equal((await store.pullAll()).length, 1); +}); + test("SingleFileMessagePackBackingStore reads only a missing store as empty", async () => { const dir = await mkdtemp(join(tmpdir(), "cultcache-absent-")); assert.deepEqual(await new SingleFileMessagePackBackingStore(join(dir, "store.cc")).pullAll(), []); diff --git a/tests/vectors/document-variants-c2a/v3-base.msgpack b/tests/vectors/document-variants-c2a/v3-base.msgpack new file mode 100644 index 00000000..9fc9b64f --- /dev/null +++ b/tests/vectors/document-variants-c2a/v3-base.msgpack @@ -0,0 +1 @@ +“²cultcache.store.v3‘—¬vectors.item¬vectors.item¯vectors.item.v1¬vectors.itemÙL{"schemaName":"vectors.item","schemaVersion":"vectors.item.v1","members":[]}‘¬vectors.item’”¥alpha¬vectors.item¹2026-09-29T00:00:00+00:00Ä’¥alpha”€beta¬vectors.item¹2026-09-29T00:00:01+00:00Ä’€beta \ No newline at end of file