CultLib/packages/cultcache-ts/test/cult-cache.test.ts
Meta Vens 8dd5a45378 Give every cultcache-ts document type one home store
Cut 5 (TypeScript). The cache no longer mirrors: StoreRoute and its mirrors
list are gone, #resolveRoute returns the single home store (the store that
claims the type, else the generic store), and put, putEnvelope and delete write
exactly one store.

addBackingStore now refuses a second generic store and a type already claimed
by another store, attaching nothing when it refuses, matching C#. No bytes
change; nothing in CultLib attached more than one store.

Tests: rejects a second generic backing store, rejects a type registered to
two stores, routes each type to its home store. The writer-by-reader interop
test now runs the C# write-routed mode and checks that catalog.cc and run.cc
are each a complete v1 snapshot holding only their own record, and that every
reader (TS, Rust, C#, Python) reads the routed catalog store.

Version 0.13.5 -> 0.14.0; cultnet-ts and cultmesh-ts depend on ^0.14.0 so the
workspace link still satisfies their range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 00:37:31 +02:00

940 lines
31 KiB
TypeScript

import assert from "node:assert/strict";
import { exec, execFile } from "node:child_process";
import { existsSync } from "node:fs";
import { access, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { dirname, join, resolve, delimiter } from "node:path";
import { test } from "node:test";
import { promisify } from "node:util";
import { decode, encode } from "@msgpack/msgpack";
import { z } from "zod";
import { CultCache } from "../src/cult-cache";
import { inspectCultCacheBytes } from "../src/cult-cache-inspector";
import { defineDocumentRegistry, defineDocumentType } from "../src/document";
import { SingleFileMessagePackBackingStore } from "../src/single-file-messagepack-backing-store";
import type { CacheBackingStore, CultCacheEnvelope, CultCacheSchema } from "../src/types";
const execFileAsync = promisify(execFile);
const execAsync = promisify(exec);
const cargoCommand = process.env.CARGO ?? (process.platform === "win32" ? join(homedir(), ".cargo", "bin", "cargo.exe") : "cargo");
const dotnetCommand = process.env.DOTNET ?? (process.platform === "win32" ? join("C:", "Program Files", "dotnet", "dotnet.exe") : "dotnet");
const pythonCommand = process.env.PYTHON ?? "python";
const cultCacheTsRoot = resolve(__dirname, "../..");
const cultLibRoot = findAncestor(cultCacheTsRoot, "CultLib.sln") ?? resolve(cultCacheTsRoot, "..", "CultLib");
const cultcachePyRoot = resolve(cultLibRoot, "packages", "cultcache-py");
const cultcachePySrc = ["cultcache-py", "cultnet-py", "cultmesh-py"]
.map((name) => resolve(cultLibRoot, "packages", name, "src"))
.join(delimiter);
const cultcacheRsRoot = existsSync(resolve(cultLibRoot, "packages", "cultcache-rs"))
? resolve(cultLibRoot, "packages", "cultcache-rs")
: resolve(cultCacheTsRoot, "..", "cultcache-rs");
const rustInteropBinary = resolve(
cultcacheRsRoot,
"target",
"debug",
"examples",
process.platform === "win32" ? "cultcache_interop.exe" : "cultcache_interop",
);
const csharpInteropProject = resolve(
cultLibRoot,
"tests",
"GameCult.Caching.InteropPeer",
"GameCult.Caching.InteropPeer.csproj",
);
const csharpInteropDll = resolve(
cultLibRoot,
"bin",
"GameCult.Caching.InteropPeer",
"Debug",
"net10.0",
"GameCult.Caching.InteropPeer.dll",
);
function findAncestor(start: string, marker: string): string | undefined {
let current = start;
while (true) {
if (existsSync(resolve(current, marker))) {
return current;
}
const parent = dirname(current);
if (parent === current) {
return undefined;
}
current = parent;
}
}
test("CultCache supports registry bootstrap, global documents, and lookup by name and index", async () => {
const itemDocument = defineDocumentType({
type: "item",
schema: z.object({
name: z.string(),
category: z.string(),
value: z.number().int(),
}),
name: "name",
indexes: {
category: "category",
},
});
const settingsDocument = defineDocumentType({
type: "settings",
schema: z.object({
theme: z.string(),
retries: z.number().int().nonnegative(),
}),
global: true,
});
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "cache.msgpack");
const registry = defineDocumentRegistry(itemDocument, settingsDocument);
const cache = CultCache.builder()
.withRegistry(registry)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.put(itemDocument, "item:potion", {
name: "Potion",
category: "Consumable",
value: 50,
});
await cache.putGlobal(settingsDocument, {
theme: "ash",
retries: 3,
});
assert.deepEqual(cache.getByName(itemDocument, "Potion"), {
name: "Potion",
category: "Consumable",
value: 50,
});
assert.equal(cache.getKeyByIndex(itemDocument, "category", "Consumable"), "item:potion");
assert.deepEqual(cache.getGlobal(settingsDocument), {
theme: "ash",
retries: 3,
});
const snapshot = cache.snapshot();
assert.equal(snapshot.length, 2);
assert.ok(snapshot.every((entry) => entry.payload instanceof Uint8Array && entry.payload.length > 0));
const reloaded = CultCache.builder()
.withRegistry(registry)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await reloaded.pullAllBackingStores();
assert.deepEqual(reloaded.getByName(itemDocument, "Potion"), {
name: "Potion",
category: "Consumable",
value: 50,
});
assert.deepEqual(reloaded.getRequiredGlobal(settingsDocument), {
theme: "ash",
retries: 3,
});
});
test("CultCache can register name and index lookups after entries already exist", async () => {
const noteDocument = defineDocumentType({
type: "note",
schema: z.object({
title: z.string(),
author: z.string(),
body: z.string(),
}),
});
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "cache.msgpack");
const cache = CultCache.builder()
.withDocumentType(noteDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.put(noteDocument, "note:hello", {
title: "Hello",
author: "ari",
body: "world",
});
cache.registerNameLookup(noteDocument, "title");
cache.registerIndex(noteDocument, "author", "author");
assert.equal(cache.getIdByName(noteDocument, "Hello"), "note:hello");
assert.equal(cache.getIdByIndex(noteDocument, "author", "ari"), "note:hello");
});
test("CultCache fails closed on unknown or illegal persisted polymorphic state", async () => {
const settingsDocument = defineDocumentType({
type: "settings",
schema: z.object({
theme: z.string(),
}),
global: true,
});
class InMemoryStore implements CacheBackingStore {
constructor(private readonly entries: CultCacheEnvelope[]) {}
async pullAll(): Promise<CultCacheEnvelope[]> {
return this.entries;
}
async push(): Promise<void> {
throw new Error("not used");
}
async delete(): Promise<void> {
throw new Error("not used");
}
}
const cacheWithUnknownType = CultCache.builder()
.withDocumentType(settingsDocument)
.withGenericStore(new InMemoryStore([
{
key: "mystery",
type: "forbidden",
payload: encode({ theme: "ash" }),
storedAt: new Date().toISOString(),
},
]))
.build();
await assert.rejects(
async () => cacheWithUnknownType.pullAllBackingStores(),
/No schema is registered for persisted document type "forbidden"\./,
);
const cacheWithDuplicateGlobal = CultCache.builder()
.withDocumentType(settingsDocument)
.withGenericStore(new InMemoryStore([
{
key: "one",
type: "settings",
payload: encode({ theme: "ash" }),
storedAt: new Date().toISOString(),
},
{
key: "two",
type: "settings",
payload: encode({ theme: "ember" }),
storedAt: new Date().toISOString(),
},
]))
.build();
await assert.rejects(
async () => cacheWithDuplicateGlobal.pullAllBackingStores(),
/has multiple persisted entries/,
);
});
test("CultCache accepts generated parse-style schemas without a Zod mirror", async () => {
type GeneratedSettings = {
schema_version: "generated.settings.v0";
theme: string;
retries: number;
};
const generatedSchema: CultCacheSchema<GeneratedSettings> = {
parse(input: unknown): GeneratedSettings {
if (!input || typeof input !== "object") {
throw new Error("generated settings must be an object");
}
const value = input as Record<string, unknown>;
if (value.schema_version !== "generated.settings.v0") {
throw new Error("generated settings schema_version mismatch");
}
if (typeof value.theme !== "string") {
throw new Error("generated settings theme must be a string");
}
if (typeof value.retries !== "number") {
throw new Error("generated settings retries must be a number");
}
return {
schema_version: "generated.settings.v0",
theme: value.theme,
retries: value.retries,
};
},
};
const generatedDocument = defineDocumentType({
type: "generated-settings",
schema: generatedSchema,
global: true,
});
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "generated.msgpack");
const cache = CultCache.builder()
.withDocumentType(generatedDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.putGlobal(generatedDocument, {
schema_version: "generated.settings.v0",
theme: "ash",
retries: 3,
});
const settings = cache.getRequiredGlobal(generatedDocument);
assert.equal(settings.schema_version, "generated.settings.v0");
assert.equal(settings.theme, "ash");
assert.equal(settings.retries, 3);
});
test("CultCache can ingest raw envelopes without re-encoding the payload", async () => {
const noteDocument = defineDocumentType({
type: "note",
schema: z.object({
title: z.string(),
body: z.string(),
}),
});
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "cache.msgpack");
const origin = CultCache.builder()
.withDocumentType(noteDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
const target = CultCache.builder()
.withDocumentType(noteDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "target.msgpack")))
.build();
await origin.put(noteDocument, "note:hello", {
title: "Hello",
body: "world",
});
const envelope = origin.getRequiredEnvelope(noteDocument, "note:hello");
const applied = await target.putEnvelope(noteDocument, envelope);
assert.deepEqual(applied, {
title: "Hello",
body: "world",
});
assert.deepEqual(target.getRequired(noteDocument, "note:hello"), applied);
assert.deepEqual(target.getRequiredEnvelope(noteDocument, "note:hello").payload, envelope.payload);
});
test("SingleFileMessagePackBackingStore writes the CultCache v1 snapshot shape", async () => {
const namedDocument = defineDocumentType({
type: "tests.named_entry",
schema: z.object({
Name: z.string(),
Value: z.string(),
}),
schemaId: "sha256:e7b97801b94190f3159012ede45b0069bb09ebf7920f7432c971bc86a0e08de8",
schemaName: "tests.named_entry",
schemaVersion: "tests.named_entry.v1",
contentHash: "sha256:23150930afcc1d84f0cb3012ccc2debcb9b4685f62083033bbaab0083f1e832e",
canonicalSchemaJson: "{\"schemaName\":\"tests.named_entry\",\"schemaVersion\":\"tests.named_entry.v1\",\"members\":[{\"slot\":0,\"name\":\"Name\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":true},{\"slot\":1,\"name\":\"Value\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false}]}",
members: [
{
slot: 0,
memberName: "Name",
typeName: "System.String",
isName: true,
},
{
slot: 1,
memberName: "Value",
typeName: "System.String",
},
],
name: "Name",
});
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "snapshot.msgpack");
const cache = CultCache.builder()
.withDocumentType(namedDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.put(namedDocument, "record-1", {
Name: "Teeth",
Value: "slot-array",
});
const snapshot = decode(await readFile(storePath)) as unknown[];
assert.equal(snapshot[0], "cultcache.store.v1");
const catalog = snapshot[1] as unknown[][];
assert.equal(catalog.length, 1);
assert.equal(catalog[0]?.[0], "sha256:e7b97801b94190f3159012ede45b0069bb09ebf7920f7432c971bc86a0e08de8");
assert.equal(catalog[0]?.[1], "tests.named_entry");
assert.equal(catalog[0]?.[2], "tests.named_entry.v1");
const records = snapshot[2] as unknown[][];
assert.equal(records.length, 1);
assert.equal(records[0]?.[0], "record-1");
assert.equal(records[0]?.[1], "sha256:e7b97801b94190f3159012ede45b0069bb09ebf7920f7432c971bc86a0e08de8");
assert.ok(records[0]?.[3] instanceof Uint8Array);
});
test("SingleFileMessagePackBackingStore reads CultCache v1 snapshots by schema id", async () => {
const noteDocument = defineDocumentType({
type: "tests.named_entry",
schema: z.object({
Name: z.string(),
Value: z.string(),
}),
schemaId: "schema-1",
schemaName: "tests.named_entry",
schemaVersion: "tests.named_entry.v1",
contentHash: "hash-1",
canonicalSchemaJson: "{\"fields\":2}",
});
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "csharp.msgpack");
await writeFile(
storePath,
encode([
"cultcache.store.v1",
[
[
"schema-1",
"tests.named_entry",
"tests.named_entry.v1",
"hash-1",
"{\"fields\":2}",
["schema-1"],
[],
],
],
[
[
"record-1",
"schema-1",
"2026-05-08T12:00:00Z",
encode({
Name: "Teeth",
Value: "slot-array",
}),
],
],
]),
);
const cache = CultCache.builder()
.withDocumentType(noteDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.pullAllBackingStores();
assert.deepEqual(cache.getRequired(noteDocument, "record-1"), {
Name: "Teeth",
Value: "slot-array",
});
assert.equal(cache.getRequiredEnvelope(noteDocument, "record-1").schemaId, "schema-1");
});
test("SingleFileMessagePackBackingStore recovers schema-stamped records missing catalog entries", async () => {
const stampedDocument = defineDocumentType({
type: "runtime-policy",
schema: z.tuple([
z.literal("tests.schema_stamped_entry.v1"),
z.string(),
z.string(),
]),
schemaId: "schema-current",
schemaName: "tests.schema_stamped_entry",
schemaVersion: "tests.schema_stamped_entry.v1",
compatibleSchemaIds: ["schema-current"],
});
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "missing-catalog.msgpack");
await writeFile(
storePath,
encode([
"cultcache.store.v1",
[],
[
[
"record-1",
"sha256:stale-schema-id-from-cold-record",
"2026-06-25T12:00:00Z",
encode([
"tests.schema_stamped_entry.v1",
"schema-stamped",
"still readable",
]),
],
],
]),
);
const cache = CultCache.builder()
.withDocumentType(stampedDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.pullAllBackingStores();
assert.deepEqual(cache.getRequired(stampedDocument, "record-1"), [
"tests.schema_stamped_entry.v1",
"schema-stamped",
"still readable",
]);
assert.equal(
cache.getRequiredEnvelope(stampedDocument, "record-1").schemaId,
"sha256:stale-schema-id-from-cold-record",
);
});
test("SingleFileMessagePackBackingStore heals legacy envelopes whose payload was persisted as an object", async () => {
const noteDocument = defineDocumentType({
type: "note",
schema: z.object({
title: z.string(),
body: z.string(),
}),
});
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "legacy.msgpack");
const envelopePayload = {
title: "Hello",
body: "world",
};
await writeFile(
storePath,
encode([
{
key: "note:hello",
type: "note",
payload: envelopePayload,
storedAt: new Date().toISOString(),
},
]),
);
const cache = CultCache.builder()
.withDocumentType(noteDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.pullAllBackingStores();
assert.deepEqual(cache.getRequired(noteDocument, "note:hello"), envelopePayload);
const rewritten = decode(await readFile(storePath)) as unknown[];
assert.equal(rewritten[0], "cultcache.store.v1");
const records = rewritten[2] as unknown[][];
assert.equal(records.length, 1);
assert.ok(records[0]?.[3] instanceof Uint8Array);
});
test("CultCache rejects a second generic backing store", () => {
const cache = new CultCache();
cache.addGenericBackingStore(new SingleFileMessagePackBackingStore(join(tmpdir(), "unused-a.cc")));
assert.throws(
() => cache.addGenericBackingStore(new SingleFileMessagePackBackingStore(join(tmpdir(), "unused-b.cc"))),
/second generic store/u,
);
});
test("CultCache rejects a type registered to two stores", () => {
const cache = new CultCache();
cache.addBackingStore(new SingleFileMessagePackBackingStore(join(tmpdir(), "unused-a.cc")), "item");
assert.throws(
() => cache.addBackingStore(new SingleFileMessagePackBackingStore(join(tmpdir(), "unused-b.cc")), "settings", "item"),
/"item" is already routed/u,
);
// A refused registration claims nothing, so "settings" is still free.
cache.addBackingStore(new SingleFileMessagePackBackingStore(join(tmpdir(), "unused-c.cc")), "settings");
});
test("CultCache routes each type to its home store", async () => {
const itemDocument = defineDocumentType({
type: "item",
schema: z.object({ name: z.string() }),
});
const settingsDocument = defineDocumentType({
type: "settings",
schema: z.object({ theme: z.string() }),
});
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-routes-"));
const genericPath = join(tempDir, "generic.cc");
const settingsPath = join(tempDir, "settings.cc");
const build = () => CultCache.builder()
.withRegistry(defineDocumentRegistry(itemDocument, settingsDocument))
.withBackingStore(new SingleFileMessagePackBackingStore(settingsPath), settingsDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(genericPath))
.build();
const keysIn = async (path: string) =>
(await new SingleFileMessagePackBackingStore(path).pullAll()).map((entry) => `${entry.type}:${entry.key}`);
const cache = build();
await cache.put(itemDocument, "potion", { name: "Potion" });
await cache.put(settingsDocument, "app", { theme: "ash" });
assert.deepEqual(await keysIn(genericPath), ["item:potion"]);
assert.deepEqual(await keysIn(settingsPath), ["settings:app"]);
await cache.delete(settingsDocument, "app");
assert.deepEqual(await keysIn(settingsPath), []);
assert.deepEqual(await keysIn(genericPath), ["item:potion"]);
const reloaded = build();
await reloaded.pullAllBackingStores();
assert.deepEqual(reloaded.getRequired(itemDocument, "potion"), { name: "Potion" });
assert.equal(reloaded.get(settingsDocument, "app"), undefined);
});
test("CultCache v1 MessagePack stores are readable across TS, Rust, C#, and Python", async () => {
await buildInteropPeers();
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-interop-"));
const writers = [
{
name: "ts",
write: async (file: string) => writeTsInteropStore(file, "ts-writer"),
},
{
name: "rust",
write: async (file: string) => runJsonCommand("rust-write", rustInteropBinary, [
"write",
"--file", file,
"--runtime-id", "rust-writer",
], cultcacheRsRoot),
},
{
name: "csharp",
write: async (file: string) => runJsonCommand("csharp-write", dotnetCommand, [
csharpInteropDll,
"write",
"--file", file,
"--runtime-id", "csharp-writer",
], cultLibRoot),
},
{
name: "python",
write: async (file: string) => runJsonCommand("python-write", pythonCommand, [
"-m", "cultcache_py.interop",
"write",
"--file", file,
"--runtime-id", "python-writer",
], cultcachePyRoot, { PYTHONPATH: cultcachePySrc }),
},
];
const readers = [
{
name: "ts",
read: async (file: string) => readTsInteropStore(file),
},
{
name: "rust",
read: async (file: string) => runJsonCommand("rust-read", rustInteropBinary, [
"read",
"--file", file,
], cultcacheRsRoot),
},
{
name: "csharp",
read: async (file: string) => runJsonCommand("csharp-read", dotnetCommand, [
csharpInteropDll,
"read",
"--file", file,
], cultLibRoot),
},
{
name: "python",
read: async (file: string) => runJsonCommand("python-read", pythonCommand, [
"-m", "cultcache_py.interop",
"read",
"--file", file,
], cultcachePyRoot, { PYTHONPATH: cultcachePySrc }),
},
];
for (const writer of writers) {
const file = join(tempDir, `${writer.name}.msgpack`);
const written = await writer.write(file);
const decoded = decode(await readFile(file)) as unknown[];
assert.equal(decoded[0], "cultcache.store.v1");
assert.ok(Array.isArray(decoded[1]), `${writer.name} did not write a schema catalog`);
assert.ok(Array.isArray(decoded[2]), `${writer.name} did not write records`);
for (const reader of readers) {
const read = await reader.read(file);
assert.equal(read.documentId, written.documentId, `${reader.name} failed to read ${writer.name}`);
assert.equal(read.authorRuntimeId, written.authorRuntimeId);
assert.equal(read.body, "The v1 store format is the contract.");
assert.ok(read.tags.includes("interop"));
}
}
// C# write-routed attaches one home store per type: each file is a complete v1 snapshot holding only its own records.
const catalogFile = join(tempDir, "catalog.cc");
const runFile = join(tempDir, "run.cc");
const routed = await runJsonCommand("csharp-write-routed", dotnetCommand, [
csharpInteropDll,
"write-routed",
catalogFile,
runFile,
], cultLibRoot);
const recordsIn = async (file: string) => {
const decoded = decode(await readFile(file)) as unknown[];
assert.equal(decoded[0], "cultcache.store.v1");
assert.ok(Array.isArray(decoded[1]), `${file} has no schema catalog`);
return (decoded[2] as unknown[][]).map((record) => ({ schemaId: record[1], key: record[0] }));
};
const catalogRecords = await recordsIn(catalogFile);
const runRecords = await recordsIn(runFile);
assert.deepEqual(catalogRecords.map((record) => record.key), ["note:csharp-routed"]);
assert.deepEqual(runRecords.map((record) => record.key), ["run-note:csharp-routed"]);
assert.notEqual(catalogRecords[0]?.schemaId, runRecords[0]?.schemaId);
for (const reader of readers) {
const read = await reader.read(catalogFile);
assert.equal(read.documentId, routed.documentId, `${reader.name} failed to read the routed catalog store`);
assert.equal(read.body, "One home store per document type.");
}
});
test("CultCache interop reader accepts missing compatible trailing slots and rejects mismatched slots", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-interop-"));
const compatible = join(tempDir, "compatible.msgpack");
const mismatch = join(tempDir, "mismatch.msgpack");
await writeTsInteropStore(compatible, "legacy-writer", { legacyPayload: true });
assert.deepEqual(await readTsInteropStore(compatible), {
schemaVersion: "cultcache.interop_note.v1",
documentId: "note:legacy-writer",
authorRuntimeId: "legacy-writer",
title: "legacy-writer wrote a CultCache note",
body: "The v1 store format is the contract.",
tags: [],
});
await writeTsInteropStore(mismatch, "bad-writer", { mismatchedPayload: true });
await assert.rejects(
() => readTsInteropStore(mismatch),
/Expected string/u,
);
});
test("CultCache inspector decodes v1 store catalog and record payloads from bytes", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-inspector-"));
const file = join(tempDir, "cache.cc");
const written = await writeTsInteropStore(file, "inspector");
const inspection = inspectCultCacheBytes(file, await readFile(file));
assert.equal(inspection.format, "cultcache.store.v1");
assert.equal(inspection.catalog.length, 1);
assert.equal(inspection.catalog[0]?.schemaName, "cultcache.interop-note");
assert.equal(inspection.records.length, 1);
assert.equal(inspection.records[0]?.key, written.documentId);
assert.deepEqual(inspection.records[0]?.payloadPreview, [
"cultcache.interop_note.v1",
written.documentId,
written.authorRuntimeId,
written.title,
written.body,
written.tags,
]);
});
test("CultCache inspector recovers schema-stamped records missing catalog entries", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-inspector-"));
const file = join(tempDir, "missing-catalog.cc");
await writeFile(
file,
encode([
"cultcache.store.v1",
[],
[
[
"record-1",
"sha256:stale-schema-id-from-cold-record",
"2026-06-25T12:00:00Z",
encode([
"tests.schema_stamped_entry.v1",
"schema-stamped",
"still readable",
]),
],
],
]),
);
const inspection = inspectCultCacheBytes(file, await readFile(file));
assert.equal(inspection.catalog.length, 1);
assert.equal(inspection.catalog[0]?.schemaId, "sha256:stale-schema-id-from-cold-record");
assert.equal(inspection.catalog[0]?.schemaName, "tests.schema_stamped_entry");
assert.equal(inspection.records[0]?.schemaName, "tests.schema_stamped_entry");
assert.deepEqual(inspection.records[0]?.payloadPreview, [
"tests.schema_stamped_entry.v1",
"schema-stamped",
"still readable",
]);
});
interface InteropNote {
schemaVersion: "cultcache.interop_note.v1";
documentId: string;
authorRuntimeId: string;
title: string;
body: string;
tags: string[];
}
const interopNoteDocument = defineDocumentType({
type: "cultcache.interop-note",
schemaId: "cultcache.interop-note",
schemaName: "cultcache.interop-note",
schemaVersion: "cultcache.interop_note.v1",
contentHash: "cultcache.interop-note",
canonicalSchemaJson: "{\"schemaName\":\"cultcache.interop-note\",\"schemaVersion\":\"cultcache.interop_note.v1\",\"members\":[{\"slot\":0,\"name\":\"SchemaVersion\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":1,\"name\":\"DocumentId\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":true},{\"slot\":2,\"name\":\"AuthorRuntimeId\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":3,\"name\":\"Title\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":4,\"name\":\"Body\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":5,\"name\":\"Tags\",\"type\":\"System.String[]\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false}]}",
compatibleSchemaIds: ["cultcache.interop-note"],
members: [
{ slot: 0, memberName: "SchemaVersion", typeName: "System.String" },
{ slot: 1, memberName: "DocumentId", typeName: "System.String", isName: true },
{ slot: 2, memberName: "AuthorRuntimeId", typeName: "System.String" },
{ slot: 3, memberName: "Title", typeName: "System.String" },
{ slot: 4, memberName: "Body", typeName: "System.String" },
{ slot: 5, memberName: "Tags", typeName: "System.String[]" },
],
schema: z.object({
schemaVersion: z.literal("cultcache.interop_note.v1"),
documentId: z.string().min(1),
authorRuntimeId: z.string().min(1),
title: z.string().min(1),
body: z.string().min(1),
tags: z.array(z.string().min(1)),
}),
formatter: {
encode(value: InteropNote): Uint8Array {
return encode([
value.schemaVersion,
value.documentId,
value.authorRuntimeId,
value.title,
value.body,
value.tags,
]);
},
decode(payload: Uint8Array): InteropNote {
const decoded = decode(payload);
if (!Array.isArray(decoded) || decoded.length < 5) {
throw new Error("CultCache interop note payload must be a MessagePack slot array.");
}
const [schemaVersion, documentId, authorRuntimeId, title, body, tags] = decoded;
return interopNoteDocument.schema.parse({
schemaVersion,
documentId,
authorRuntimeId,
title,
body,
tags: Array.isArray(tags) ? tags : [],
});
},
},
});
async function writeTsInteropStore(
file: string,
runtimeId: string,
options: { legacyPayload?: boolean; mismatchedPayload?: boolean } = {},
): Promise<InteropNote> {
const cache = CultCache.builder()
.withDocumentType(interopNoteDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(file))
.build();
const note: InteropNote = {
schemaVersion: "cultcache.interop_note.v1",
documentId: `note:${runtimeId}`,
authorRuntimeId: runtimeId,
title: `${runtimeId} wrote a CultCache note`,
body: "The v1 store format is the contract.",
tags: [runtimeId, "ts", "interop"],
};
if (options.legacyPayload || options.mismatchedPayload) {
const payload = options.mismatchedPayload
? encode([note.schemaVersion, note.documentId, 42, note.title, note.body, note.tags])
: encode([note.schemaVersion, note.documentId, note.authorRuntimeId, note.title, note.body]);
await new SingleFileMessagePackBackingStore(file).push({
key: note.documentId,
type: interopNoteDocument.type,
schemaId: interopNoteDocument.schemaId,
catalogEntry: {
schemaId: interopNoteDocument.schemaId!,
schemaName: interopNoteDocument.schemaName!,
schemaVersion: interopNoteDocument.schemaVersion!,
contentHash: interopNoteDocument.contentHash!,
canonicalSchemaJson: interopNoteDocument.canonicalSchemaJson!,
compatibleSchemaIds: [interopNoteDocument.schemaId!],
members: interopNoteDocument.members,
},
storedAt: new Date().toISOString(),
payload,
});
return note;
}
await cache.put(interopNoteDocument, note.documentId, note);
return note;
}
async function readTsInteropStore(file: string): Promise<InteropNote> {
const cache = CultCache.builder()
.withDocumentType(interopNoteDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(file))
.build();
await cache.pullAllBackingStores();
const notes = cache.getAll(interopNoteDocument);
const note = notes[0];
if (!note) {
throw new Error("No cultcache.interop-note records found.");
}
return note;
}
async function buildInteropPeers(): Promise<void> {
if (!(await exists(rustInteropBinary))) {
await execAsync(`"${cargoCommand}" build --quiet --example cultcache_interop`, {
cwd: cultcacheRsRoot,
});
}
if (!(await exists(csharpInteropDll))) {
await execAsync(`"${dotnetCommand}" build "${csharpInteropProject}" -nologo`, {
cwd: cultLibRoot,
});
}
}
async function exists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}
async function runJsonCommand(
name: string,
command: string,
args: string[],
cwd: string,
env: NodeJS.ProcessEnv = {},
): Promise<any> {
const { stdout, stderr } = await execFileAsync(command, args, {
cwd,
env: { ...process.env, ...env },
timeout: 30_000,
});
const trimmed = stdout.trim();
if (!trimmed) {
throw new Error(`${name} produced no stdout.\n${stderr}`);
}
return JSON.parse(trimmed.split(/\r?\n/).at(-1) as string);
}