diff --git a/packages/core/src/bootstrap-capable.ts b/packages/core/src/bootstrap-capable.ts index feef49e..0b09e02 100644 --- a/packages/core/src/bootstrap-capable.ts +++ b/packages/core/src/bootstrap-capable.ts @@ -4,7 +4,7 @@ import type { Hash, Store } from "./types.js"; export const BOOTSTRAP_STORE = Symbol.for("@ocas/core/bootstrap-store"); export type BootstrapCapableStore = Store & { - [BOOTSTRAP_STORE](payload: unknown): Promise; + [BOOTSTRAP_STORE](payload: unknown): Hash | Promise; }; export function isBootstrapCapableStore( diff --git a/packages/core/src/bootstrap.test.ts b/packages/core/src/bootstrap.test.ts index 23f1d33..042ee28 100644 --- a/packages/core/src/bootstrap.test.ts +++ b/packages/core/src/bootstrap.test.ts @@ -34,7 +34,7 @@ const OUTPUT_ALIASES = [ describe("bootstrap - Built-in Schemas", () => { test("should return map of 30 built-in schema aliases to hashes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); // Should return object with 9 primitive + 21 output aliases = 30 @@ -62,7 +62,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/schema as meta-schema alias", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"]; @@ -75,7 +75,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/string schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const stringHash = builtinSchemas["@ocas/string"]; @@ -86,7 +86,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/number schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const numberHash = builtinSchemas["@ocas/number"]; @@ -97,7 +97,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/object schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const objectHash = builtinSchemas["@ocas/object"]; @@ -108,7 +108,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/array schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const arrayHash = builtinSchemas["@ocas/array"]; @@ -119,7 +119,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should register @ocas/bool schema correctly", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const boolHash = builtinSchemas["@ocas/bool"]; @@ -130,7 +130,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("should return same hashes on repeated bootstrap calls", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const first = await bootstrap(store); const second = await bootstrap(store); @@ -146,7 +146,7 @@ describe("bootstrap - Built-in Schemas", () => { }); test("all built-in schemas should be typed by meta-schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"]; @@ -168,7 +168,7 @@ describe("bootstrap - Built-in Schemas", () => { describe("bootstrap - @ocas/output/* Schemas", () => { test("each @ocas/output/* schema has a title", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); for (const alias of OUTPUT_ALIASES) { @@ -183,7 +183,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/put schema describes a ocas_ref string", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/put"]; if (!hash) throw new Error("@ocas/output/put not found"); @@ -197,7 +197,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/get schema describes object with type, payload, timestamp", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/get"]; if (!hash) throw new Error("@ocas/output/get not found"); @@ -213,7 +213,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/has schema describes a boolean", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/has"]; if (!hash) throw new Error("@ocas/output/has not found"); @@ -225,7 +225,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/verify schema describes enum of ok|corrupted|invalid", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/verify"]; if (!hash) throw new Error("@ocas/output/verify not found"); @@ -239,7 +239,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/refs schema describes array of ocas_ref strings", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/refs"]; if (!hash) throw new Error("@ocas/output/refs not found"); @@ -252,7 +252,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/gc schema describes object with gc stats fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/gc"]; if (!hash) throw new Error("@ocas/output/gc not found"); @@ -269,7 +269,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/var-set schema describes a Variable object", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/var-set"]; if (!hash) throw new Error("@ocas/output/var-set not found"); @@ -285,7 +285,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/var-list schema describes array of Variable objects", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/var-list"]; if (!hash) throw new Error("@ocas/output/var-list not found"); @@ -301,7 +301,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("@ocas/output/template-delete schema describes object with deleted boolean", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const hash = aliases["@ocas/output/template-delete"]; if (!hash) throw new Error("@ocas/output/template-delete not found"); @@ -314,7 +314,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => { }); test("all @ocas/output/* schemas are distinct hashes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const outputHashes = OUTPUT_ALIASES.map((alias) => aliases[alias]); @@ -325,14 +325,14 @@ describe("bootstrap - @ocas/output/* Schemas", () => { describe("bootstrap - meta and schemas indexes (D1)", () => { test("listMeta contains the bootstrap meta-schema hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const metaHash = aliases["@ocas/schema"]; expect(store.listMeta().map((e) => e.hash)).toContain(metaHash as string); }); test("listSchemas contains meta-schema and all built-in schemas", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const schemas = store.listSchemas().map((e) => e.hash); diff --git a/packages/core/src/gc.test.ts b/packages/core/src/gc.test.ts index eb8d80f..7c95129 100644 --- a/packages/core/src/gc.test.ts +++ b/packages/core/src/gc.test.ts @@ -28,7 +28,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC preserves variable-referenced nodes", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { name: { type: "string" } } }; const schemaHash = await putSchema(store, schema); @@ -52,7 +52,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC preserves nodes from variables with same name, different schemas", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = { type: "object", properties: { x: { type: "number" } } }; const schemaB = { type: "object", properties: { y: { type: "string" } } }; @@ -80,7 +80,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC removes nodes after variable deletion", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { name: { type: "string" } } }; const schemaHash = await putSchema(store, schema); @@ -102,7 +102,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC is global across all variables", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = { type: "object", properties: { x: { type: "number" } } }; const schemaB = { type: "object", properties: { y: { type: "string" } } }; @@ -133,7 +133,7 @@ describe("GC - Variable Model Refactoring", () => { }); test("GC integration with refactored variable store", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = { type: "object", properties: { x: { type: "number" } } }; diff --git a/packages/core/src/hash.ts b/packages/core/src/hash.ts index 559d4ae..8e9c247 100644 --- a/packages/core/src/hash.ts +++ b/packages/core/src/hash.ts @@ -48,6 +48,38 @@ async function getInstance(): Promise { return _pending; } +/** + * Initialize the xxhash WASM instance. After this resolves, the synchronous + * hashing functions {@link computeHashSync} and {@link computeSelfHashSync} + * may be called. + */ +export async function initHasher(): Promise { + await getInstance(); +} + +/** + * Synchronous variant of {@link computeHash}. Must only be called after + * {@link initHasher} has resolved at least once; throws otherwise. + */ +export function computeHashSync(typeHash: Hash, payload: unknown): Hash { + if (_instance === null) { + throw new Error("Hasher not initialised — call initHasher() first"); + } + const input = concatBytes(asciiToBytes(typeHash), cborEncode(payload)); + return u64ToCrockford(_instance.h64Raw(input)); +} + +/** + * Synchronous variant of {@link computeSelfHash}. Must only be called after + * {@link initHasher} has resolved at least once; throws otherwise. + */ +export function computeSelfHashSync(payload: unknown): Hash { + if (_instance === null) { + throw new Error("Hasher not initialised — call initHasher() first"); + } + return u64ToCrockford(_instance.h64Raw(cborEncode(payload))); +} + /** * hash = XXH64(utf8(typeHash) ++ CBOR_deterministic(payload)) * Used for all normal nodes. diff --git a/packages/core/src/index.test.ts b/packages/core/src/index.test.ts index 319d9fd..df664bb 100644 --- a/packages/core/src/index.test.ts +++ b/packages/core/src/index.test.ts @@ -72,7 +72,7 @@ describe("computeHash", () => { // ────────────────────────────────────────────────────────────────────────────── describe("createMemoryStore – put and get", () => { test("put returns a hash and get retrieves the node", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const hash = await store.put(typeHash, { greeting: "hello" }); @@ -86,12 +86,12 @@ describe("createMemoryStore – put and get", () => { }); test("get returns null for unknown hash", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(store.get("0000000000000")).toBeNull(); }); test("put is idempotent: same type+payload → same hash, no duplicate", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const h1 = await store.put(typeHash, { n: 42 }); @@ -101,7 +101,7 @@ describe("createMemoryStore – put and get", () => { }); test("put does not create self-referencing nodes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const payload = { name: "type-descriptor" }; const typeHash = await computeSelfHash(payload); const hash = await store.put(typeHash, payload); @@ -112,7 +112,7 @@ describe("createMemoryStore – put and get", () => { }); test("timestamp is preserved on second put (idempotency)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const h1 = await store.put(typeHash, { v: 1 }); @@ -131,7 +131,7 @@ describe("createMemoryStore – put and get", () => { // ────────────────────────────────────────────────────────────────────────────── describe("createMemoryStore – has", () => { test("has returns false before put, true after", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "t" }); const hash = await computeHash(typeHash, { x: 1 }); @@ -141,7 +141,7 @@ describe("createMemoryStore – has", () => { }); test("listByType returns all stored hashes for a type", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "t" }); const h1 = await store.put(typeHash, { a: 1 }); @@ -156,7 +156,7 @@ describe("createMemoryStore – has", () => { }); test("listByType returns empty array on fresh store", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(store.listByType("0000000000000")).toEqual([]); }); }); @@ -166,12 +166,12 @@ describe("createMemoryStore – has", () => { // ────────────────────────────────────────────────────────────────────────────── describe("createMemoryStore – listByType", () => { test("returns empty array for unknown type", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(store.listByType("0000000000000")).toEqual([]); }); test("returns all hashes for the given type", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "t" }); const otherType = await computeSelfHash({ name: "other" }); @@ -186,7 +186,7 @@ describe("createMemoryStore – listByType", () => { }); test("idempotent put does not duplicate in listByType", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "t" }); const h1 = await store.put(typeHash, { n: 1 }); @@ -196,7 +196,7 @@ describe("createMemoryStore – listByType", () => { }); test("bootstrap node is listed under its self type", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const hash = builtinSchemas["@ocas/schema"] ?? ""; @@ -216,7 +216,7 @@ describe("createMemoryStore – listByType", () => { // ────────────────────────────────────────────────────────────────────────────── describe("verify", () => { test("returns true for a correctly stored node", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const hash = await store.put(typeHash, { data: 123 }); const node = store.get(hash) as CasNode; @@ -225,7 +225,7 @@ describe("verify", () => { }); test("returns false when payload is tampered", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const hash = await store.put(typeHash, { data: 123 }); @@ -238,7 +238,7 @@ describe("verify", () => { }); test("returns false when type is tampered", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const typeHash = await computeSelfHash({ name: "my-type" }); const hash = await store.put(typeHash, { data: 123 }); const node = store.get(hash) as CasNode; @@ -265,7 +265,7 @@ describe("bootstrap", () => { }); test("returns a map with 30 built-in schema aliases", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); expect(builtinSchemas).toHaveProperty("@ocas/schema"); @@ -288,7 +288,7 @@ describe("bootstrap", () => { }); test("meta-schema node is stored and retrievable", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; @@ -298,7 +298,7 @@ describe("bootstrap", () => { }); test("meta-schema node is self-referencing: type === hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const node = store.get(metaHash) as CasNode; @@ -307,7 +307,7 @@ describe("bootstrap", () => { }); test("bootstrap node passes verify()", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const node = store.get(metaHash) as CasNode; @@ -316,7 +316,7 @@ describe("bootstrap", () => { }); test("bootstrap is idempotent: same hashes on repeated calls", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const h1 = await bootstrap(store); const h2 = await bootstrap(store); diff --git a/packages/core/src/liquid-render.test.ts b/packages/core/src/liquid-render.test.ts index 8fa4dbd..0a2ba55 100644 --- a/packages/core/src/liquid-render.test.ts +++ b/packages/core/src/liquid-render.test.ts @@ -13,7 +13,7 @@ import { createVariableStore } from "./variable-store.js"; async function createTempVarStore() { const tempDir = await mkdtemp(join(tmpdir(), "ocas-test-")); const dbPath = join(tempDir, "vars.db"); - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const varStore = createVariableStore(dbPath, store); return { diff --git a/packages/core/src/list-pagination.test.ts b/packages/core/src/list-pagination.test.ts index 943fb60..6b52fee 100644 --- a/packages/core/src/list-pagination.test.ts +++ b/packages/core/src/list-pagination.test.ts @@ -22,7 +22,7 @@ async function putN( describe("listByType - pagination + sort + timestamps", () => { test("A1. returns objects with hash/created/updated", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 3, 0); @@ -36,7 +36,7 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A2. default sort is created ASC", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 4); @@ -49,7 +49,7 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A3. desc:true reverses order", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 4); @@ -62,7 +62,7 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A4. sort: 'updated' is equivalent to 'created' for CAS nodes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 4); @@ -72,14 +72,14 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A5. limit truncates", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 5, 0); expect(store.listByType(m, { limit: 2 })).toHaveLength(2); }); test("A6. offset skips", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 5); @@ -90,21 +90,21 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A7. limit:0 returns empty array", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 3, 0); expect(store.listByType(m, { limit: 0 })).toEqual([]); }); test("A8. offset past end returns empty array", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 3, 0); expect(store.listByType(m, { offset: 100 })).toEqual([]); }); test("A9. core has no default limit (returns all)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 150, 0); // No CLI-layer cap; with 150 nodes of type m (plus m itself which is @@ -113,7 +113,7 @@ describe("listByType - pagination + sort + timestamps", () => { }); test("A10. desc + offset + limit combined", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 5, 15); const all = store.listByType(m); @@ -128,7 +128,7 @@ describe("listByType - pagination + sort + timestamps", () => { describe("listMeta / listSchemas - pagination", () => { test("B1. listMeta returns {hash,created,updated}", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const h = await store[BOOTSTRAP_STORE]({ type: "object" }); const list = store.listMeta(); expect(list).toHaveLength(1); @@ -139,7 +139,7 @@ describe("listMeta / listSchemas - pagination", () => { }); test("B2. listMeta has no default limit (returns all)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; for (let i = 0; i < 150; i++) { await store[BOOTSTRAP_STORE]({ type: "object", i }); } @@ -147,7 +147,7 @@ describe("listMeta / listSchemas - pagination", () => { }); test("B3. listMeta limit/offset/desc", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; for (let i = 0; i < 5; i++) { await store[BOOTSTRAP_STORE]({ type: "object", i }); await new Promise((r) => setTimeout(r, 2)); @@ -159,7 +159,7 @@ describe("listMeta / listSchemas - pagination", () => { }); test("B4. listSchemas returns objects, supports limit", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await store.put(m, { type: "string" }); await store.put(m, { type: "number" }); @@ -175,7 +175,7 @@ describe("listMeta / listSchemas - pagination", () => { describe("Determinism / edge cases", () => { test("I1. same-ms timestamps yield deterministic ordering across calls", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const m = await store[BOOTSTRAP_STORE]({ type: "object" }); // No delay → likely same millisecond await putN(store, m, 5, 0); @@ -185,7 +185,7 @@ describe("Determinism / edge cases", () => { }); test("I2. empty store returns []", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(store.listByType("0000000000000")).toEqual([]); expect(store.listMeta()).toEqual([]); expect(store.listSchemas()).toEqual([]); diff --git a/packages/core/src/mem-store.ts b/packages/core/src/mem-store.ts index 51806c2..75ac1f3 100644 --- a/packages/core/src/mem-store.ts +++ b/packages/core/src/mem-store.ts @@ -3,15 +3,17 @@ import { BOOTSTRAP_STORE } from "./bootstrap-capable.js"; import { createMemoryStore } from "./store.js"; import type { CasNode, Hash, ListEntry, ListOptions } from "./types.js"; -/** In-memory store wrapper used by schema validation tests. */ +/** In-memory store wrapper used by schema validation tests. Wraps the + * `cas` sub-store of an `OcasStore` and exposes the legacy + * `BootstrapCapableStore` interface (async `put`, etc.). */ export class MemStore implements BootstrapCapableStore { - readonly #inner: BootstrapCapableStore; + readonly #inner: ReturnType["cas"]; constructor() { - this.#inner = createMemoryStore(); + this.#inner = createMemoryStore().cas; } - put(typeHash: Hash, payload: unknown): Promise { + async put(typeHash: Hash, payload: unknown): Promise { return this.#inner.put(typeHash, payload); } @@ -43,7 +45,7 @@ export class MemStore implements BootstrapCapableStore { this.#inner.delete(hash); } - [BOOTSTRAP_STORE](payload: unknown): Promise { + async [BOOTSTRAP_STORE](payload: unknown): Promise { return this.#inner[BOOTSTRAP_STORE](payload); } } diff --git a/packages/core/src/output-templates.test.ts b/packages/core/src/output-templates.test.ts index 2d5f009..469ef9d 100644 --- a/packages/core/src/output-templates.test.ts +++ b/packages/core/src/output-templates.test.ts @@ -43,7 +43,7 @@ describe("registerOutputTemplates", () => { test("registers a template for every @ocas/output/* schema", async () => { tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); varStore = createVariableStore(join(tempDir, "vars.db"), store); @@ -58,7 +58,7 @@ describe("registerOutputTemplates", () => { test("each template is retrievable via @ocas/template/text/", async () => { tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); - store = createMemoryStore(); + store = createMemoryStore().cas; const aliases = await bootstrap(store); varStore = createVariableStore(join(tempDir, "vars.db"), store); @@ -84,7 +84,7 @@ describe("registerOutputTemplates", () => { test("is idempotent — safe to call multiple times", async () => { tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); varStore = createVariableStore(join(tempDir, "vars.db"), store); @@ -96,7 +96,7 @@ describe("registerOutputTemplates", () => { test("@ocas/output/put template contains payload reference", async () => { tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); - store = createMemoryStore(); + store = createMemoryStore().cas; const aliases = await bootstrap(store); varStore = createVariableStore(join(tempDir, "vars.db"), store); diff --git a/packages/core/src/render.test.ts b/packages/core/src/render.test.ts index 8076d80..8e0210e 100644 --- a/packages/core/src/render.test.ts +++ b/packages/core/src/render.test.ts @@ -8,7 +8,7 @@ import { CasNodeNotFoundError } from "./variable-store.js"; describe("Suite 1: Basic Rendering (No Nesting)", () => { test("1.1 Render Simple Primitives", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "hello"); @@ -20,7 +20,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { }); test("1.2 Render Object Node (Flat)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const objSchema = await putSchema(store, { type: "object", @@ -40,7 +40,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { }); test("1.3 Render Array Node (Flat)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const arraySchema = await putSchema(store, { type: "array", @@ -56,7 +56,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { }); test("1.4 Render with resolution=0 (Force Reference)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "hello"); @@ -67,7 +67,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { }); test("1.5 Render Non-existent Hash Throws Error", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "ZZZZZZZZZZZZZ" as Hash; // Non-existent root node should throw @@ -79,7 +79,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => { describe("Suite 2: Resolution Decay Model", () => { test("2.1 Single-level Nesting with Default Decay", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -115,7 +115,7 @@ describe("Suite 2: Resolution Decay Model", () => { }); test("2.2 Multi-level Nesting Reaches Epsilon", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const leafSchema = await putSchema(store, { @@ -151,7 +151,7 @@ describe("Suite 2: Resolution Decay Model", () => { }); test("2.3 High Decay (Quick Cutoff)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -189,7 +189,7 @@ describe("Suite 2: Resolution Decay Model", () => { }); test("2.4 Low Decay (Deep Expansion)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -224,7 +224,7 @@ describe("Suite 2: Resolution Decay Model", () => { }); test("2.5 Starting Resolution Below 1.0", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -262,7 +262,7 @@ describe("Suite 2: Resolution Decay Model", () => { describe("Suite 3: Complex Graph Structures", () => { test("3.1 Multiple Child References", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const itemSchema = await putSchema(store, { @@ -301,7 +301,7 @@ describe("Suite 3: Complex Graph Structures", () => { }); test("3.2 Object with Multiple ocas_ref Fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -340,7 +340,7 @@ describe("Suite 3: Complex Graph Structures", () => { }); test("3.3 Cycle Detection", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -372,7 +372,7 @@ describe("Suite 3: Complex Graph Structures", () => { }); test("3.4 DAG (Shared Descendant)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const leafSchema = await putSchema(store, { @@ -423,7 +423,7 @@ describe("Suite 3: Complex Graph Structures", () => { }); test("3.5 Deep Tree", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -464,7 +464,7 @@ describe("Suite 3: Complex Graph Structures", () => { describe("Suite 4: Epsilon Boundary Cases", () => { test("4.1 Resolution Exactly at Epsilon", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "test"); @@ -479,7 +479,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => { }); test("4.2 Resolution Just Above Epsilon", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "test"); @@ -494,7 +494,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => { }); test("4.3 Very Small Epsilon (Deep Expansion)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -529,7 +529,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => { }); test("4.4 Zero Epsilon (Never Prune)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { @@ -566,7 +566,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => { describe("Suite 5: YAML Output Format", () => { test("5.1 Valid YAML Syntax", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const objSchema = await putSchema(store, { type: "object", @@ -584,7 +584,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.2 Nested Object Indentation", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nestedSchema = await putSchema(store, { type: "object", @@ -610,7 +610,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.3 Array Rendering", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const arraySchema = await putSchema(store, { type: "array", @@ -625,7 +625,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.4 CAS Reference in YAML", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -655,7 +655,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.5 Special Characters Escaping", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "string" }); const hash = await store.put(textSchema, "line1\nline2: value"); @@ -667,7 +667,7 @@ describe("Suite 5: YAML Output Format", () => { }); test("5.6 Null Handling", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nullableSchema = await putSchema(store, { type: "object", @@ -687,7 +687,7 @@ describe("Suite 5: YAML Output Format", () => { describe("Suite 6: Schema Integration", () => { test("6.1 Detect ocas_ref Fields via Schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -716,7 +716,7 @@ describe("Suite 6: Schema Integration", () => { }); test("6.2 Non-ocas_ref String Not Expanded", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const objSchema = await putSchema(store, { type: "object", @@ -734,7 +734,7 @@ describe("Suite 6: Schema Integration", () => { }); test("6.3 Array of ocas_ref", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const itemSchema = await putSchema(store, { @@ -763,7 +763,7 @@ describe("Suite 6: Schema Integration", () => { }); test("6.4 anyOf with ocas_ref (Nullable Reference)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const childSchema = await putSchema(store, { @@ -794,7 +794,7 @@ describe("Suite 6: Schema Integration", () => { }); test("6.5 Schema-less Node (Bootstrap Node)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const types = await bootstrap(store); const schemaHash = types["@ocas/schema"]; @@ -807,7 +807,7 @@ describe("Suite 6: Schema Integration", () => { describe("Suite 7: Error Handling", () => { test("7.1 Missing Referenced Node", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const parentSchema = await putSchema(store, { @@ -826,21 +826,21 @@ describe("Suite 7: Error Handling", () => { }); test("7.3 Invalid Resolution Parameter", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "AAAAAAAAAAAAA" as Hash; expect(() => render(store, fakeHash, { resolution: -1 })).toThrow(); }); test("7.4 Invalid Decay Parameter", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "AAAAAAAAAAAAA" as Hash; expect(() => render(store, fakeHash, { decay: 1.5 })).toThrow(); }); test("7.5 Invalid Epsilon Parameter", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "AAAAAAAAAAAAA" as Hash; expect(() => render(store, fakeHash, { epsilon: -0.01 })).toThrow(); @@ -849,7 +849,7 @@ describe("Suite 7: Error Handling", () => { describe("Suite 8: Performance & Edge Cases", () => { test("8.1 Large Payload", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const arraySchema = await putSchema(store, { type: "array", @@ -877,7 +877,7 @@ describe("Suite 8: Performance & Edge Cases", () => { }); test("8.2 Wide Fan-out", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const itemSchema = await putSchema(store, { @@ -909,7 +909,7 @@ describe("Suite 8: Performance & Edge Cases", () => { }); test("8.3 Empty Payload", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const emptySchema = await putSchema(store, { type: "object" }); const hash = await store.put(emptySchema, {}); @@ -920,7 +920,7 @@ describe("Suite 8: Performance & Edge Cases", () => { }); test("8.4 Unicode in Payload", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const textSchema = await putSchema(store, { type: "object", @@ -985,7 +985,7 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => { }); test("9.5 Render with store expands ocas_ref fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); // Create a child node @@ -1051,7 +1051,7 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => { }); test("9.10 store present but schema missing — renders without ref expansion", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const unknownType = "ZZZZZZZZZZZZ0" as Hash; const output = renderDirect(unknownType, { key: "val" }, store, null); @@ -1061,7 +1061,7 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => { describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { test("10.1 renderAsync() throws CasNodeNotFoundError for missing root hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const fakeHash = "AAAAAAAAAAAAA" as Hash; @@ -1075,7 +1075,7 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { }); test("10.2 render() throws CasNodeNotFoundError for missing root hash", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeHash = "ZZZZZZZZZZZZZ" as Hash; expect(() => render(store, fakeHash)).toThrow(CasNodeNotFoundError); @@ -1084,7 +1084,7 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { }); test("10.3 renderDirect() does NOT throw for non-existent type hash", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeTypeHash = "0000000000000" as Hash; const output = renderDirect(fakeTypeHash, { key: "value" }, store, null); @@ -1092,7 +1092,7 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { }); test("10.4 Missing nested node renders as cas: reference (no error)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const parentSchema = await putSchema(store, { @@ -1116,7 +1116,7 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => { }); test("10.5 Resolution below epsilon renders as cas: reference (no error)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const nodeSchema = await putSchema(store, { diff --git a/packages/core/src/schema.test.ts b/packages/core/src/schema.test.ts index 53134b9..87a8dc5 100644 --- a/packages/core/src/schema.test.ts +++ b/packages/core/src/schema.test.ts @@ -10,14 +10,14 @@ import type { CasNode } from "./types.js"; // ────────────────────────────────────────────────────────────────────────────── describe("putSchema", () => { test("returns a valid 13-char hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", properties: {} }); expect(hash).toHaveLength(13); expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); }); test("schema node is stored in the store", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schema = { type: "object", properties: { name: { type: "string" } } }; const hash = await putSchema(store, schema); @@ -28,7 +28,7 @@ describe("putSchema", () => { }); test("schema node type equals the meta-schema hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const schemaHash = await putSchema(store, { type: "string" }); @@ -38,7 +38,7 @@ describe("putSchema", () => { }); test("putSchema is idempotent: same schema → same hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schema = { type: "number" }; const h1 = await putSchema(store, schema); const h2 = await putSchema(store, schema); @@ -47,7 +47,7 @@ describe("putSchema", () => { }); test("different schemas produce different hashes", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const h1 = await putSchema(store, { type: "string" }); const h2 = await putSchema(store, { type: "number" }); @@ -60,7 +60,7 @@ describe("putSchema", () => { // ────────────────────────────────────────────────────────────────────────────── describe("getSchema", () => { test("returns the original schema object", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schema = { type: "object", properties: { age: { type: "number" } } }; const hash = await putSchema(store, schema); @@ -68,12 +68,12 @@ describe("getSchema", () => { }); test("returns null for an unknown hash", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; expect(getSchema(store, "0000000000000")).toBeNull(); }); test("roundtrip: put then get returns the same schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schema = { type: "object", required: ["id"], @@ -92,7 +92,7 @@ describe("getSchema", () => { // ────────────────────────────────────────────────────────────────────────────── describe("validate", () => { test("returns true when payload matches the schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { name: { type: "string" }, age: { type: "number" } }, @@ -105,7 +105,7 @@ describe("validate", () => { }); test("returns false when payload violates the schema", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { count: { type: "number" } }, @@ -118,7 +118,7 @@ describe("validate", () => { }); test("returns false when required field is missing", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", required: ["title"], @@ -131,7 +131,7 @@ describe("validate", () => { }); test("returns false when schema cannot be found", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const fakeNode: CasNode = { type: "0000000000000", payload: { x: 1 }, @@ -147,7 +147,7 @@ describe("validate", () => { // ────────────────────────────────────────────────────────────────────────────── describe("refs", () => { test("returns empty array when schema has no ocas_ref fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { title: { type: "string" } }, @@ -159,7 +159,7 @@ describe("refs", () => { }); test("returns the ocas_ref hash values from payload", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -179,7 +179,7 @@ describe("refs", () => { }); test("collects multiple ocas_ref fields", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -203,7 +203,7 @@ describe("refs", () => { }); test("skips null/undefined ocas_ref values", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -219,7 +219,7 @@ describe("refs", () => { }); test("returns empty array when schema is not found", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const orphanNode: CasNode = { type: "0000000000000", payload: { x: 1 }, @@ -235,7 +235,7 @@ describe("refs", () => { // ────────────────────────────────────────────────────────────────────────────── describe("walk", () => { test("visits a single node with no refs", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { val: { type: "number" } }, @@ -249,7 +249,7 @@ describe("walk", () => { }); test("visits all reachable nodes in a chain A → B → C", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -273,7 +273,7 @@ describe("walk", () => { }); test("handles cycles without infinite loop", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { @@ -316,7 +316,7 @@ describe("walk", () => { }); test("skips missing hashes gracefully", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { ref: { type: "string", format: "ocas_ref" } }, @@ -331,7 +331,7 @@ describe("walk", () => { }); test("visitor receives both hash and node", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const schemaHash = await putSchema(store, { type: "object", properties: { x: { type: "number" } }, @@ -355,7 +355,7 @@ describe("walk", () => { // ────────────────────────────────────────────────────────────────────────────── describe("bootstrap meta-schema self-reference", () => { test("metaNode.type === metaHash (self-referencing)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaNode = store.get(metaHash) as CasNode; @@ -364,7 +364,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("schema nodes have type === metaHash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const schemaHash = await putSchema(store, { type: "string" }); @@ -374,7 +374,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("data nodes have type === schemaHash (not metaHash)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const schemaHash = await putSchema(store, { @@ -391,7 +391,7 @@ describe("bootstrap meta-schema self-reference", () => { // ── P1 leaf constraints ────────────────────────────────────────────────── test("accepts schema with numeric constraints (minimum/maximum)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "number", minimum: 0, @@ -413,7 +413,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with string constraints (minLength/maxLength/pattern)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "string", minLength: 1, @@ -430,7 +430,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with array constraints (minItems/maxItems/uniqueItems)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "array", items: { type: "number" }, @@ -451,7 +451,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("rejects schema with wrong constraint types", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await expect( putSchema(store, { type: "number", minimum: "zero" } as never), ).rejects.toThrow(); @@ -464,7 +464,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with nested property constraints", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", properties: { @@ -490,7 +490,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("bootstrap is idempotent across putSchema calls", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@ocas/schema"] ?? ""; @@ -505,7 +505,7 @@ describe("bootstrap meta-schema self-reference", () => { // ── P2 combinators, conditionals, and leaf constraints ────────────────── test("accepts schema with allOf", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { allOf: [ { type: "object", properties: { name: { type: "string" } } }, @@ -522,7 +522,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with if/then/else", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", properties: { @@ -538,7 +538,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with patternProperties", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", patternProperties: { @@ -552,7 +552,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with prefixItems (tuple)", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "array", prefixItems: [{ type: "string" }, { type: "number" }], @@ -561,7 +561,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with multipleOf", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "number", multipleOf: 5, @@ -576,7 +576,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with minProperties/maxProperties", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", minProperties: 1, @@ -592,7 +592,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with default value", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "string", default: "hello", @@ -601,7 +601,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("rejects invalid P2 keyword types", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await expect( putSchema(store, { allOf: "not-array" } as never), ).rejects.toThrow(); @@ -614,7 +614,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("collectRefs traverses allOf sub-schemas", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const innerSchema = await putSchema(store, { type: "string" }); const schema = await putSchema(store, { allOf: [ @@ -633,7 +633,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("collectRefs traverses patternProperties", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const innerSchema = await putSchema(store, { type: "string" }); const schema = await putSchema(store, { type: "object", @@ -650,7 +650,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("collectRefs traverses prefixItems", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const innerSchema = await putSchema(store, { type: "string" }); const schema = await putSchema(store, { type: "array", @@ -667,7 +667,7 @@ describe("bootstrap meta-schema self-reference", () => { // ── P3 combinators, propertyNames, and metadata ────────────────────────── test("accepts schema with not", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { not: { type: "string" }, }); @@ -681,7 +681,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with contains", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "array", contains: { type: "number", minimum: 10 }, @@ -696,7 +696,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with propertyNames", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "object", propertyNames: { pattern: "^[a-z]+$" }, @@ -711,7 +711,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("accepts schema with metadata keywords", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const hash = await putSchema(store, { type: "string", examples: ["hello", "world"], @@ -723,7 +723,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("rejects invalid P3 keyword types", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await expect( putSchema(store, { not: "not-object" } as never), ).rejects.toThrow(); @@ -737,7 +737,7 @@ describe("bootstrap meta-schema self-reference", () => { }); test("collectRefs traverses contains", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const innerSchema = await putSchema(store, { type: "string" }); const schema = await putSchema(store, { type: "array", diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index cfd370b..251499e 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -262,7 +262,7 @@ export async function putSchema( "Invalid schema: input does not conform to the ocas JSON Schema meta-schema", ); } - return store.put(metaHash, jsonSchema); + return Promise.resolve(store.put(metaHash, jsonSchema)); } /** diff --git a/packages/core/src/store.test.ts b/packages/core/src/store.test.ts index f6a4b1e..1f9d79b 100644 --- a/packages/core/src/store.test.ts +++ b/packages/core/src/store.test.ts @@ -2,88 +2,154 @@ import { describe, expect, test } from "bun:test"; import { BOOTSTRAP_STORE } from "./bootstrap-capable.js"; import { createMemoryStore } from "./store.js"; -describe("createMemoryStore – meta and schema indexes", () => { +describe("A. createMemoryStore – shape", () => { + test("A1. returns an object with cas, var, tag sub-stores", () => { + const store = createMemoryStore(); + expect(typeof store.cas).toBe("object"); + expect(store.cas).not.toBeNull(); + expect(typeof store.var).toBe("object"); + expect(store.var).not.toBeNull(); + expect(typeof store.tag).toBe("object"); + expect(store.tag).not.toBeNull(); + }); + + test("A2. cas/var/tag are independent objects", () => { + const store = createMemoryStore(); + expect(store.cas).not.toBe(store.var as unknown as typeof store.cas); + expect(store.cas).not.toBe(store.tag as unknown as typeof store.cas); + expect(store.var).not.toBe(store.tag as unknown as typeof store.var); + }); +}); + +describe("B. cas sub-store – meta and schema indexes", () => { test("B1. listMeta and listSchemas are empty on a fresh store", () => { const store = createMemoryStore(); - expect(store.listMeta()).toEqual([]); - expect(store.listSchemas()).toEqual([]); + expect(store.cas.listMeta()).toEqual([]); + expect(store.cas.listSchemas()).toEqual([]); }); - test("B2. self-referencing put adds hash to metaSet and listSchemas", async () => { + test("B2. self-referencing put adds hash to metaSet and listSchemas", () => { const store = createMemoryStore(); - const hash = await store[BOOTSTRAP_STORE]({ type: "object" }); - - expect(store.listMeta().map((e) => e.hash)).toContain(hash); - expect(store.listSchemas().map((e) => e.hash)).toContain(hash); + const hash = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + expect(store.cas.listMeta().map((e) => e.hash)).toContain(hash); + expect(store.cas.listSchemas().map((e) => e.hash)).toContain(hash); }); - test("B3. regular put does not add hash to metaSet", async () => { + test("B3. regular put does not add hash to metaSet", () => { const store = createMemoryStore(); - const metaHash = await store[BOOTSTRAP_STORE]({ type: "object" }); - const schemaHash = await store.put(metaHash, { type: "string" }); - - expect(store.listMeta().map((e) => e.hash)).not.toContain(schemaHash); - expect(store.listMeta().map((e) => e.hash)).toContain(metaHash); + const metaHash = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const schemaHash = store.cas.put(metaHash, { type: "string" }); + expect(store.cas.listMeta().map((e) => e.hash)).not.toContain(schemaHash); + expect(store.cas.listMeta().map((e) => e.hash)).toContain(metaHash); }); - test("B4. schema typed by meta-schema appears in listSchemas", async () => { + test("B4. schema typed by meta-schema appears in listSchemas", () => { const store = createMemoryStore(); - const m = await store[BOOTSTRAP_STORE]({ type: "object" }); - const s = await store.put(m, { type: "string" }); - - const schemas = store.listSchemas().map((e) => e.hash); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const s = store.cas.put(m, { type: "string" }); + const schemas = store.cas.listSchemas().map((e) => e.hash); expect(schemas).toContain(m); expect(schemas).toContain(s); - - const meta = store.listMeta().map((e) => e.hash); + const meta = store.cas.listMeta().map((e) => e.hash); expect(meta).toContain(m); expect(meta).not.toContain(s); }); - test("B5. multiple meta-schemas (versioning)", async () => { + test("B5. multiple meta-schemas (versioning)", () => { const store = createMemoryStore(); - const m1 = await store[BOOTSTRAP_STORE]({ type: "object", title: "v1" }); - const m2 = await store[BOOTSTRAP_STORE]({ type: "object", title: "v2" }); - const s1 = await store.put(m1, { type: "string" }); - const s2 = await store.put(m2, { type: "number" }); - - const meta = store.listMeta().map((e) => e.hash); + const m1 = store.cas[BOOTSTRAP_STORE]({ + type: "object", + title: "v1", + }) as string; + const m2 = store.cas[BOOTSTRAP_STORE]({ + type: "object", + title: "v2", + }) as string; + const s1 = store.cas.put(m1, { type: "string" }); + const s2 = store.cas.put(m2, { type: "number" }); + const meta = store.cas.listMeta().map((e) => e.hash); expect(meta).toContain(m1); expect(meta).toContain(m2); expect(meta).toHaveLength(2); - - const schemas = store.listSchemas().map((e) => e.hash); + const schemas = store.cas.listSchemas().map((e) => e.hash); expect(schemas).toContain(m1); expect(schemas).toContain(m2); expect(schemas).toContain(s1); expect(schemas).toContain(s2); }); - test("B6. idempotent self-referencing put does not duplicate", async () => { + test("B6. idempotent self-referencing put does not duplicate", () => { const store = createMemoryStore(); const payload = { type: "object", title: "dup" }; - const h1 = await store[BOOTSTRAP_STORE](payload); - const h2 = await store[BOOTSTRAP_STORE](payload); + const h1 = store.cas[BOOTSTRAP_STORE](payload) as string; + const h2 = store.cas[BOOTSTRAP_STORE](payload) as string; expect(h1).toBe(h2); - - const meta = store.listMeta().map((e) => e.hash); - const occurrences = meta.filter((h) => h === h1).length; - expect(occurrences).toBe(1); + const meta = store.cas.listMeta().map((e) => e.hash); + expect(meta.filter((h) => h === h1)).toHaveLength(1); }); - test("B7. delete removes hash from metaSet and listSchemas", async () => { + test("B7. delete removes hash from metaSet and listSchemas", () => { const store = createMemoryStore(); - const m = await store[BOOTSTRAP_STORE]({ type: "object" }); - const s = await store.put(m, { type: "string" }); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const s = store.cas.put(m, { type: "string" }); + expect(store.cas.listMeta().map((e) => e.hash)).toContain(m); + expect(store.cas.listSchemas().map((e) => e.hash)).toContain(s); + store.cas.delete(m); + expect(store.cas.listMeta().map((e) => e.hash)).not.toContain(m); + expect(store.cas.listSchemas().map((e) => e.hash)).not.toContain(s); + expect(store.cas.listSchemas().map((e) => e.hash)).not.toContain(m); + }); - expect(store.listMeta().map((e) => e.hash)).toContain(m); - expect(store.listSchemas().map((e) => e.hash)).toContain(s); + test("B8. cas.put returns Hash synchronously (not a Promise)", () => { + const store = createMemoryStore(); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const result = store.cas.put(m, { type: "string" }); + expect(typeof result).toBe("string"); + expect(result).toHaveLength(13); + }); - store.delete(m); - - expect(store.listMeta().map((e) => e.hash)).not.toContain(m); - // schemas typed by deleted meta no longer surface - expect(store.listSchemas().map((e) => e.hash)).not.toContain(s); - expect(store.listSchemas().map((e) => e.hash)).not.toContain(m); + test("B9. has/get/delete reflect put state", () => { + const store = createMemoryStore(); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const h = store.cas.put(m, { type: "string" }); + expect(store.cas.has(h)).toBe(true); + expect(store.cas.get(h)?.payload).toEqual({ type: "string" }); + expect(store.cas.has("0000000000000")).toBe(false); + expect(store.cas.get("0000000000000")).toBeNull(); + expect(store.cas.delete(h)).toBe(true); + expect(store.cas.delete(h)).toBe(false); + expect(store.cas.has(h)).toBe(false); + }); +}); + +describe("E. cross-store independence", () => { + test("E1. tagging a hash does not surface it in cas.listByType", () => { + const store = createMemoryStore(); + const fakeTarget = "0000000000000"; + store.tag.tag(fakeTarget, [{ op: "set", key: "env", value: "prod" }]); + expect(store.cas.listByType(fakeTarget)).toEqual([]); + }); + + test("E2. setting a variable does not mutate the CAS node", () => { + const store = createMemoryStore(); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const s = store.cas.put(m, { type: "string" }); + const value = store.cas.put(s, "hello"); + const before = store.cas.get(value); + store.var.set("@app/x", value); + const after = store.cas.get(value); + expect(after).toEqual(before); + }); + + test("E3. cas.delete does not cascade-delete a variable referencing it", () => { + const store = createMemoryStore(); + const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string; + const s = store.cas.put(m, { type: "string" }); + const value = store.cas.put(s, "hello"); + store.var.set("@app/x", value); + store.cas.delete(value); + const got = store.var.get("@app/x", s); + expect(got).not.toBeNull(); + expect(got?.value).toBe(value); }); }); diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index f7ced1f..6a9bb66 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -2,11 +2,83 @@ import { BOOTSTRAP_STORE, type BootstrapCapableStore, } from "./bootstrap-capable.js"; -import { computeHash, computeSelfHash } from "./hash.js"; +import { computeHashSync, computeSelfHashSync, initHasher } from "./hash.js"; import { applyListOptions, casListEntry } from "./list-utils.js"; -import type { CasNode, Hash, ListEntry, ListOptions } from "./types.js"; +import type { + CasNode, + Hash, + HistoryEntry, + ListEntry, + ListOptions, + OcasStore, + Tag, + TagOp, + TagStore, + VarListOptions, + VarSetOptions, + VarStore, +} from "./types.js"; +import type { Variable } from "./variable.js"; +import { + CasNodeNotFoundError, + InvalidVariableNameError, + MAX_HISTORY, + SchemaMismatchError, + TagLabelConflictError, + VariableNotFoundError, +} from "./variable-store.js"; -export function createMemoryStore(): BootstrapCapableStore { +// Initialise the xxhash WASM instance once at module load. This allows the +// CAS sub-store's `put` method to be synchronous (per the new CasStore type). +await initHasher(); + +/** + * The cas sub-store of an in-memory `OcasStore` — also satisfies the legacy + * `BootstrapCapableStore` interface so that helpers that have not yet been + * refactored (e.g. bootstrap, gc, render) continue to work against + * `store.cas`. + */ +export type MemoryCasStore = BootstrapCapableStore & { + put(typeHash: Hash, payload: unknown): Hash; + delete(hash: Hash): boolean; +}; + +function validateName(name: string): void { + if (name === "") { + throw new InvalidVariableNameError(name, "Name cannot be empty"); + } + const match = name.match(/^@([a-zA-Z][a-zA-Z0-9]*)\/(.+)$/); + if (!match) { + throw new InvalidVariableNameError( + name, + "Name must follow @scope/name format (e.g. @myapp/config)", + ); + } + const rest = match[2] as string; + if (rest.endsWith("/")) { + throw new InvalidVariableNameError( + name, + "Name cannot end with trailing slash", + ); + } + const segments = rest.split("/"); + for (const segment of segments) { + if (segment === "") { + throw new InvalidVariableNameError( + name, + "Name contains empty segment (consecutive slashes //)", + ); + } + if (!/^[a-zA-Z0-9._-]+$/.test(segment)) { + throw new InvalidVariableNameError( + name, + `Segment "${segment}" contains invalid characters (only a-z, A-Z, 0-9, ., _, - allowed)`, + ); + } + } +} + +function createCasStore(): MemoryCasStore { const data = new Map(); const byType = new Map>(); const metaSet = new Set(); @@ -20,8 +92,8 @@ export function createMemoryStore(): BootstrapCapableStore { set.add(hash); } - async function putSelfReferencing(payload: unknown): Promise { - const hash = await computeSelfHash(payload); + function putSelfReferencing(payload: unknown): Hash { + const hash = computeSelfHashSync(payload); if (!data.has(hash)) { data.set(hash, { type: hash, payload, timestamp: Date.now() }); indexHash(hash, hash); @@ -39,15 +111,13 @@ export function createMemoryStore(): BootstrapCapableStore { return result; } - const store: BootstrapCapableStore = { - async put(typeHash: Hash, payload: unknown): Promise { - const hash = await computeHash(typeHash, payload); - + const store: MemoryCasStore = { + put(typeHash: Hash, payload: unknown): Hash { + const hash = computeHashSync(typeHash, payload); if (!data.has(hash)) { data.set(hash, { type: typeHash, payload, timestamp: Date.now() }); indexHash(typeHash, hash); } - return hash; }, @@ -85,20 +155,19 @@ export function createMemoryStore(): BootstrapCapableStore { return applyListOptions(entriesForHashes(result), options); }, - delete(hash: Hash): void { + delete(hash: Hash): boolean { const node = data.get(hash); - if (node) { - data.delete(hash); - // Remove from type index - const set = byType.get(node.type); - if (set) { - set.delete(hash); - if (set.size === 0) { - byType.delete(node.type); - } + if (!node) return false; + data.delete(hash); + const set = byType.get(node.type); + if (set) { + set.delete(hash); + if (set.size === 0) { + byType.delete(node.type); } - metaSet.delete(hash); } + metaSet.delete(hash); + return true; }, [BOOTSTRAP_STORE]: putSelfReferencing, @@ -106,3 +175,403 @@ export function createMemoryStore(): BootstrapCapableStore { return store; } + +type VarRecord = { + name: string; + schema: Hash; + value: Hash; + created: number; + updated: number; + tags: Record; + labels: string[]; + history: HistoryEntry[]; +}; + +function cloneVar(rec: VarRecord): Variable { + return { + name: rec.name, + schema: rec.schema, + value: rec.value, + created: rec.created, + updated: rec.updated, + tags: { ...rec.tags }, + labels: [...rec.labels], + }; +} + +function createMemoryVarStore(cas: MemoryCasStore): VarStore { + // composite key: `${name}\u0000${schema}` + const records = new Map(); + const byName = new Map>(); // name -> set of composite keys + + function key(name: string, schema: Hash): string { + return `${name}\u0000${schema}`; + } + + function addIndex(name: string, k: string): void { + let set = byName.get(name); + if (!set) { + set = new Set(); + byName.set(name, set); + } + set.add(k); + } + + function removeIndex(name: string, k: string): void { + const set = byName.get(name); + if (!set) return; + set.delete(k); + if (set.size === 0) byName.delete(name); + } + + function extractSchema(hash: Hash): Hash { + const node = cas.get(hash); + if (node === null) { + throw new CasNodeNotFoundError(hash); + } + return node.type; + } + + function checkConflict(tags: Record, labels: string[]): void { + for (const tk of Object.keys(tags)) { + if (labels.includes(tk)) { + throw new TagLabelConflictError(tk, "label", "tag"); + } + } + } + + function pushHistory(rec: VarRecord, value: Hash, now: number): boolean { + if (rec.history.length > 0 && rec.history[0]?.value === value) { + return false; + } + const existingIdx = rec.history.findIndex((e) => e.value === value); + if (existingIdx > 0) { + rec.history.splice(existingIdx, 1); + } + rec.history.unshift({ value, position: 0, setAt: now }); + if (rec.history.length > MAX_HISTORY) { + rec.history.length = MAX_HISTORY; + } + for (let i = 0; i < rec.history.length; i++) { + const entry = rec.history[i]; + if (entry !== undefined) entry.position = i; + } + return true; + } + + const varStore: VarStore = { + set(name: string, hash: Hash, options?: VarSetOptions): Variable { + validateName(name); + const schema = extractSchema(hash); + const k = key(name, schema); + const existing = records.get(k); + const now = Date.now(); + + if (existing) { + const tags = options?.tags ?? existing.tags; + const labels = options?.labels ?? existing.labels; + if (options !== undefined) checkConflict(tags, labels); + const changed = pushHistory(existing, hash, now); + if (changed) { + existing.value = hash; + existing.updated = now; + } + if (options !== undefined) { + existing.tags = { ...tags }; + existing.labels = [...labels]; + } + return cloneVar(existing); + } + + const tags = options?.tags ?? {}; + const labels = options?.labels ?? []; + checkConflict(tags, labels); + const rec: VarRecord = { + name, + schema, + value: hash, + created: now, + updated: now, + tags: { ...tags }, + labels: [...labels], + history: [{ value: hash, position: 0, setAt: now }], + }; + records.set(k, rec); + addIndex(name, k); + return cloneVar(rec); + }, + + get(name: string, schema?: Hash): Variable | null { + if (schema !== undefined) { + const rec = records.get(key(name, schema)); + return rec ? cloneVar(rec) : null; + } + // No schema: if exactly one variant, return it; otherwise null + const set = byName.get(name); + if (!set || set.size !== 1) return null; + const onlyKey = set.values().next().value; + if (onlyKey === undefined) return null; + const rec = records.get(onlyKey); + return rec ? cloneVar(rec) : null; + }, + + remove(name: string, schema?: Hash): Variable[] { + if (schema !== undefined) { + const k = key(name, schema); + const rec = records.get(k); + if (!rec) return []; + records.delete(k); + removeIndex(name, k); + return [cloneVar(rec)]; + } + const set = byName.get(name); + if (!set) return []; + const removed: Variable[] = []; + for (const k of [...set]) { + const rec = records.get(k); + if (rec) { + removed.push(cloneVar(rec)); + records.delete(k); + } + } + byName.delete(name); + return removed; + }, + + update(name: string, hash: Hash, options?: VarSetOptions): Variable { + validateName(name); + const newSchema = extractSchema(hash); + // Find existing record by name; require existing schema match new schema + const set = byName.get(name); + if (!set || set.size === 0) { + throw new VariableNotFoundError(name, newSchema); + } + // find a record matching newSchema + const k = key(name, newSchema); + const existing = records.get(k); + if (!existing) { + // Find any existing — schema mismatch + for (const ek of set) { + const erec = records.get(ek); + if (erec) { + throw new SchemaMismatchError(erec.schema, newSchema); + } + } + throw new VariableNotFoundError(name, newSchema); + } + const now = Date.now(); + const tags = options?.tags ?? existing.tags; + const labels = options?.labels ?? existing.labels; + if (options !== undefined) checkConflict(tags, labels); + const changed = pushHistory(existing, hash, now); + if (changed) { + existing.value = hash; + existing.updated = now; + } + if (options !== undefined) { + existing.tags = { ...tags }; + existing.labels = [...labels]; + } + return cloneVar(existing); + }, + + list(options?: VarListOptions): Variable[] { + if ( + options?.namePrefix !== undefined && + options?.exactName !== undefined + ) { + throw new Error( + "namePrefix and exactName are mutually exclusive - cannot specify both", + ); + } + const namePrefix = options?.namePrefix; + const exactName = options?.exactName; + const schema = options?.schema; + const filterTags = options?.tags ?? {}; + const filterLabels = options?.labels ?? []; + const sort = options?.sort ?? "created"; + const desc = options?.desc ?? false; + const limit = options?.limit; + const offset = options?.offset ?? 0; + + if (limit !== undefined && limit <= 0) return []; + + let results: VarRecord[] = []; + for (const rec of records.values()) { + if (exactName !== undefined && rec.name !== exactName) continue; + if (namePrefix !== undefined && !rec.name.startsWith(namePrefix)) + continue; + if (schema !== undefined && rec.schema !== schema) continue; + let ok = true; + for (const [tk, tv] of Object.entries(filterTags)) { + if (rec.tags[tk] !== tv) { + ok = false; + break; + } + } + if (!ok) continue; + for (const lb of filterLabels) { + if (!rec.labels.includes(lb)) { + ok = false; + break; + } + } + if (!ok) continue; + results.push(rec); + } + + results.sort((a, b) => { + const av = sort === "updated" ? a.updated : a.created; + const bv = sort === "updated" ? b.updated : b.created; + if (av !== bv) return desc ? bv - av : av - bv; + return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; + }); + + if (offset > 0) results = results.slice(offset); + if (limit !== undefined) results = results.slice(0, limit); + return results.map(cloneVar); + }, + + history(name: string, schema?: Hash): HistoryEntry[] { + if (schema !== undefined) { + const rec = records.get(key(name, schema)); + return rec ? rec.history.map((e) => ({ ...e })) : []; + } + const set = byName.get(name); + if (!set || set.size !== 1) return []; + const onlyKey = set.values().next().value; + if (onlyKey === undefined) return []; + const rec = records.get(onlyKey); + return rec ? rec.history.map((e) => ({ ...e })) : []; + }, + + close(): void { + // no-op for in-memory store + }, + }; + + return varStore; +} + +function createMemoryTagStore(): TagStore { + // target -> key -> Tag + const byTarget = new Map>(); + // key -> set of targets + const byKey = new Map>(); + // per-target ordering (created) + const targetOrder = new Map(); + + function addKeyIndex(key: string, target: Hash): void { + let set = byKey.get(key); + if (!set) { + set = new Set(); + byKey.set(key, set); + } + set.add(target); + } + + function removeKeyIndex(key: string, target: Hash): void { + const set = byKey.get(key); + if (!set) return; + // only remove if this target no longer has that key in any tag + const tmap = byTarget.get(target); + if (tmap && tmap.has(key)) return; + set.delete(target); + if (set.size === 0) byKey.delete(key); + } + + return { + tag(target: Hash, operations: TagOp[]): Tag[] { + let tmap = byTarget.get(target); + if (!tmap) { + tmap = new Map(); + byTarget.set(target, tmap); + } + const now = Date.now(); + for (const op of operations) { + if (op.op === "set") { + const tag: Tag = { + key: op.key, + value: op.value ?? null, + target, + created: tmap.get(op.key)?.created ?? now, + }; + tmap.set(op.key, tag); + addKeyIndex(op.key, target); + } else { + tmap.delete(op.key); + removeKeyIndex(op.key, target); + } + } + if (!targetOrder.has(target)) { + targetOrder.set(target, now); + } + // return the current tags + return [...tmap.values()].sort((a, b) => + a.key < b.key ? -1 : a.key > b.key ? 1 : 0, + ); + }, + + untag(target: Hash, keys: string[]): void { + const tmap = byTarget.get(target); + if (!tmap) return; + for (const k of keys) { + tmap.delete(k); + removeKeyIndex(k, target); + } + if (tmap.size === 0) { + byTarget.delete(target); + targetOrder.delete(target); + } + }, + + tags(target: Hash): Tag[] { + const tmap = byTarget.get(target); + if (!tmap) return []; + return [...tmap.values()].sort((a, b) => + a.key < b.key ? -1 : a.key > b.key ? 1 : 0, + ); + }, + + listByTag(tag: string, options?: ListOptions): Hash[] { + // accept "key" or "key=value" form + let key = tag; + let value: string | null | undefined; + const eqIdx = tag.indexOf("="); + if (eqIdx >= 0) { + key = tag.slice(0, eqIdx); + value = tag.slice(eqIdx + 1); + } + const targets = byKey.get(key); + if (!targets) return []; + let entries: ListEntry[] = []; + for (const t of targets) { + const tmap = byTarget.get(t); + if (!tmap) continue; + const tagEntry = tmap.get(key); + if (!tagEntry) continue; + if (value !== undefined && tagEntry.value !== value) continue; + entries.push(casListEntry(t, tagEntry.created)); + } + entries = applyListOptions(entries, options); + return entries.map((e) => e.hash); + }, + }; +} + +/** + * Create an in-memory `OcasStore` with three sub-stores: `cas`, `var`, `tag`. + * + * The `cas` sub-store also satisfies the legacy `BootstrapCapableStore` + * contract — it carries a `[BOOTSTRAP_STORE]` callable and a `listAll()` + * helper — so existing helpers (`bootstrap`, `gc`, `render`, …) can be + * called with `store.cas` until they are migrated to the unified surface. + */ +export function createMemoryStore(): OcasStore & { + cas: MemoryCasStore; +} { + const cas = createCasStore(); + const varStore = createMemoryVarStore(cas); + const tagStore = createMemoryTagStore(); + return { cas, var: varStore, tag: tagStore }; +} diff --git a/packages/core/src/tag-store.test.ts b/packages/core/src/tag-store.test.ts new file mode 100644 index 0000000..4f982d4 --- /dev/null +++ b/packages/core/src/tag-store.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from "bun:test"; +import { createMemoryStore } from "./store.js"; + +const T1 = "AAAAAAAAAAAAA"; +const T2 = "BBBBBBBBBBBBB"; + +describe("In-memory TagStore", () => { + test("D1. tag set with key/value round-trip", () => { + const { tag } = createMemoryStore(); + const result = tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + expect(result).toHaveLength(1); + expect(result[0]?.key).toBe("env"); + expect(result[0]?.value).toBe("prod"); + expect(result[0]?.target).toBe(T1); + expect(typeof result[0]?.created).toBe("number"); + expect(tag.tags(T1)).toEqual(result); + }); + + test("D2. label tag (value omitted) records value: null", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "pinned" }]); + const tags = tag.tags(T1); + expect(tags).toHaveLength(1); + expect(tags[0]?.key).toBe("pinned"); + expect(tags[0]?.value).toBeNull(); + }); + + test("D3. multiple ops in one call, sorted by key", () => { + const { tag } = createMemoryStore(); + const result = tag.tag(T1, [ + { op: "set", key: "b", value: "2" }, + { op: "set", key: "a", value: "1" }, + ]); + expect(result.map((t) => t.key)).toEqual(["a", "b"]); + expect(tag.tags(T1).map((t) => t.key)).toEqual(["a", "b"]); + }); + + test("D4. update existing key overwrites value", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + tag.tag(T1, [{ op: "set", key: "env", value: "dev" }]); + const tags = tag.tags(T1); + expect(tags).toHaveLength(1); + expect(tags[0]?.value).toBe("dev"); + }); + + test("D5. delete via tag op removes the entry", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + tag.tag(T1, [{ op: "delete", key: "env" }]); + expect(tag.tags(T1)).toEqual([]); + }); + + test("D6. untag removes listed keys; missing keys silently skipped", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [ + { op: "set", key: "a", value: "1" }, + { op: "set", key: "b", value: "2" }, + ]); + tag.untag(T1, ["a", "missing"]); + expect(tag.tags(T1).map((t) => t.key)).toEqual(["b"]); + }); + + test("D7. listByTag returns all targets with the bare key", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + tag.tag(T2, [{ op: "set", key: "env", value: "dev" }]); + const listed = tag.listByTag("env").sort(); + expect(listed).toEqual([T1, T2].sort()); + }); + + test("D8. listByTag with key=value form filters by exact value", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "env", value: "prod" }]); + tag.tag(T2, [{ op: "set", key: "env", value: "dev" }]); + expect(tag.listByTag("env=prod")).toEqual([T1]); + }); + + test("D9. ListOptions on listByTag (limit, offset, desc)", () => { + const { tag } = createMemoryStore(); + const targets: string[] = []; + for (let i = 0; i < 5; i++) { + const t = `${"C".repeat(12)}${i}`; + targets.push(t); + tag.tag(t, [{ op: "set", key: "k", value: String(i) }]); + } + expect(tag.listByTag("k", { limit: 2 })).toHaveLength(2); + expect(tag.listByTag("k", { offset: 3 })).toHaveLength(2); + const desc = tag.listByTag("k", { desc: true }); + expect(desc[0]).toBe(targets[targets.length - 1] as string); + }); + + test("D10. different targets are independent", () => { + const { tag } = createMemoryStore(); + tag.tag(T1, [{ op: "set", key: "k", value: "1" }]); + expect(tag.tags(T2)).toEqual([]); + }); + + test("D11. each Tag returned has a created timestamp", () => { + const { tag } = createMemoryStore(); + const before = Date.now(); + const result = tag.tag(T1, [{ op: "set", key: "k", value: "v" }]); + const after = Date.now(); + expect(result[0]?.created).toBeGreaterThanOrEqual(before); + expect(result[0]?.created).toBeLessThanOrEqual(after); + }); +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3450846..b344606 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -51,7 +51,7 @@ export type ListEntry = { * Self-referencing nodes are created only via bootstrap(). */ export type Store = { - put(typeHash: Hash, payload: unknown): Promise; + put(typeHash: Hash, payload: unknown): Hash | Promise; get(hash: Hash): CasNode | null; has(hash: Hash): boolean; listByType(typeHash: Hash, options?: ListOptions): ListEntry[]; diff --git a/packages/core/src/var-store.test.ts b/packages/core/src/var-store.test.ts new file mode 100644 index 0000000..30bcf93 --- /dev/null +++ b/packages/core/src/var-store.test.ts @@ -0,0 +1,226 @@ +import { describe, expect, test } from "bun:test"; +import { createMemoryStore } from "./store.js"; +import type { Hash } from "./types.js"; +import { + CasNodeNotFoundError, + InvalidVariableNameError, + MAX_HISTORY, + SchemaMismatchError, + TagLabelConflictError, + VariableNotFoundError, +} from "./variable-store.js"; + +function makeStoreWithSchema(): { + store: ReturnType; + schema: Hash; + meta: Hash; + put: (payload: unknown) => Hash; +} { + const store = createMemoryStore(); + const meta = store.cas[Symbol.for("@ocas/core/bootstrap-store")]({ + type: "object", + }) as Hash; + const schema = store.cas.put(meta, { type: "string" }); + return { + store, + schema, + meta, + put: (payload) => store.cas.put(schema, payload), + }; +} + +describe("In-memory VarStore", () => { + test("C1. set + get round-trip", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("hello"); + const v = store.var.set("@app/x", h); + expect(v.name).toBe("@app/x"); + expect(v.value).toBe(h); + expect(v.schema).toBe(schema); + expect(typeof v.created).toBe("number"); + expect(typeof v.updated).toBe("number"); + expect(v.tags).toEqual({}); + expect(v.labels).toEqual([]); + + const got = store.var.get("@app/x", schema); + expect(got).not.toBeNull(); + expect(got?.value).toBe(h); + expect(got?.schema).toBe(schema); + }); + + test("C2. name validation", () => { + const { store, put } = makeStoreWithSchema(); + const h = put("v"); + expect(() => store.var.set("x", h)).toThrow(InvalidVariableNameError); + expect(() => store.var.set("@/x", h)).toThrow(InvalidVariableNameError); + expect(() => store.var.set("@app/", h)).toThrow(InvalidVariableNameError); + expect(() => store.var.set("@app//x", h)).toThrow(InvalidVariableNameError); + expect(() => store.var.set("@app/x.y_z-1", h)).not.toThrow(); + }); + + test("C3. set throws CasNodeNotFoundError if hash not in cas", () => { + const { store } = makeStoreWithSchema(); + expect(() => store.var.set("@app/x", "ZZZZZZZZZZZZZ")).toThrow( + CasNodeNotFoundError, + ); + }); + + test("C4. idempotent same-value set", async () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("v"); + const v1 = store.var.set("@app/x", h); + await new Promise((r) => setTimeout(r, 5)); + const v2 = store.var.set("@app/x", h); + expect(v2.updated).toBe(v1.updated); + expect(store.var.history("@app/x", schema)).toHaveLength(1); + }); + + test("C5. update via re-set with new value bumps updated and history", async () => { + const { store, schema, put } = makeStoreWithSchema(); + const h1 = put("v1"); + const h2 = put("v2"); + const v1 = store.var.set("@app/x", h1); + await new Promise((r) => setTimeout(r, 5)); + const v2 = store.var.set("@app/x", h2); + expect(v2.updated).toBeGreaterThan(v1.updated); + const hist = store.var.history("@app/x", schema); + expect(hist.map((e) => e.value)).toEqual([h2, h1]); + expect(hist[0]?.position).toBe(0); + expect(hist[1]?.position).toBe(1); + }); + + test("C6. history bound to MAX_HISTORY (10)", () => { + const { store, schema, put } = makeStoreWithSchema(); + const hashes: Hash[] = []; + for (let i = 0; i < 12; i++) { + const h = put(`v${i}`); + hashes.push(h); + store.var.set("@app/x", h); + } + const hist = store.var.history("@app/x", schema); + expect(hist).toHaveLength(MAX_HISTORY); + expect(hist[0]?.value).toBe(hashes[11] as string); + }); + + test("C7. history rotation when re-setting an older value", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h1 = put("v1"); + const h2 = put("v2"); + const h3 = put("v3"); + store.var.set("@app/x", h1); + store.var.set("@app/x", h2); + store.var.set("@app/x", h3); + expect(store.var.history("@app/x", schema).map((e) => e.value)).toEqual([ + h3, + h2, + h1, + ]); + store.var.set("@app/x", h1); + expect(store.var.history("@app/x", schema).map((e) => e.value)).toEqual([ + h1, + h3, + h2, + ]); + }); + + test("C8. tags + labels round-trip", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("v"); + store.var.set("@app/x", h, { + tags: { env: "prod" }, + labels: ["pinned"], + }); + const got = store.var.get("@app/x", schema); + expect(got?.tags).toEqual({ env: "prod" }); + expect(got?.labels).toEqual(["pinned"]); + }); + + test("C9. tag/label conflict throws TagLabelConflictError", () => { + const { store, put } = makeStoreWithSchema(); + const h = put("v"); + expect(() => + store.var.set("@app/x", h, { + tags: { x: "y" }, + labels: ["x"], + }), + ).toThrow(TagLabelConflictError); + }); + + test("C10. update requires existing variable and matching schema", () => { + const { store, put } = makeStoreWithSchema(); + const h = put("v"); + expect(() => store.var.update("@app/x", h)).toThrow(VariableNotFoundError); + store.var.set("@app/x", h); + + // Different schema for new value + const meta = store.cas[Symbol.for("@ocas/core/bootstrap-store")]({ + type: "object", + }) as Hash; + const otherSchema = store.cas.put(meta, { type: "number" }); + const h2 = store.cas.put(otherSchema, 42); + expect(() => store.var.update("@app/x", h2)).toThrow(SchemaMismatchError); + }); + + test("C11. remove with and without schema", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("v"); + store.var.set("@app/x", h); + const removed = store.var.remove("@app/x", schema); + expect(removed).toHaveLength(1); + expect(removed[0]?.value).toBe(h); + expect(store.var.get("@app/x", schema)).toBeNull(); + + // remove(name) without schema returns array; empty if none + expect(store.var.remove("@app/none")).toEqual([]); + + store.var.set("@app/y", put("y1")); + const removedAll = store.var.remove("@app/y"); + expect(Array.isArray(removedAll)).toBe(true); + expect(removedAll).toHaveLength(1); + }); + + test("C12. list filters and ListOptions", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h1 = put("v1"); + const h2 = put("v2"); + store.var.set("@app/a", h1, { tags: { env: "prod" } }); + store.var.set("@app/b", h2, { labels: ["pinned"] }); + + expect(() => + store.var.list({ namePrefix: "@app", exactName: "@app/a" }), + ).toThrow(); + + const byPrefix = store.var.list({ namePrefix: "@app/" }); + expect(byPrefix.map((v) => v.name).sort()).toEqual(["@app/a", "@app/b"]); + + const exact = store.var.list({ exactName: "@app/a" }); + expect(exact).toHaveLength(1); + + const byTag = store.var.list({ tags: { env: "prod" } }); + expect(byTag.map((v) => v.name)).toEqual(["@app/a"]); + + const byLabel = store.var.list({ labels: ["pinned"] }); + expect(byLabel.map((v) => v.name)).toEqual(["@app/b"]); + + const bySchema = store.var.list({ schema }); + expect(bySchema).toHaveLength(2); + + const limited = store.var.list({ limit: 1 }); + expect(limited).toHaveLength(1); + }); + + test("C13. close() is a no-op", () => { + const { store } = makeStoreWithSchema(); + expect(() => store.var.close()).not.toThrow(); + }); + + test("C14. cas.delete does NOT cascade-delete the variable", () => { + const { store, schema, put } = makeStoreWithSchema(); + const h = put("v"); + store.var.set("@app/x", h); + store.cas.delete(h); + const got = store.var.get("@app/x", schema); + expect(got).not.toBeNull(); + expect(got?.value).toBe(h); + }); +}); diff --git a/packages/core/src/variable-list-pagination.test.ts b/packages/core/src/variable-list-pagination.test.ts index 93775c7..70f282e 100644 --- a/packages/core/src/variable-list-pagination.test.ts +++ b/packages/core/src/variable-list-pagination.test.ts @@ -16,7 +16,7 @@ let stringHash: Hash; beforeEach(async () => { dbDir = mkdtempSync(join(tmpdir(), "ocas-var-pagination-")); dbPath = join(dbDir, "vars.db"); - casStore = createMemoryStore(); + casStore = createMemoryStore().cas; const aliases = await bootstrap(casStore); stringHash = aliases["@ocas/string"] as Hash; varStore = createVariableStore(dbPath, casStore); diff --git a/packages/core/src/variable-store.test.ts b/packages/core/src/variable-store.test.ts index 7444c4f..c354bd4 100644 --- a/packages/core/src/variable-store.test.ts +++ b/packages/core/src/variable-store.test.ts @@ -25,7 +25,7 @@ const tmpDbPath = () => describe("VariableStore - Database Schema", () => { test("Database schema has (name, schema) composite primary key", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -61,7 +61,7 @@ describe("VariableStore - Database Schema", () => { }); test("Database indexes reference name instead of id/scope", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -92,7 +92,7 @@ describe("VariableStore - Database Schema", () => { }); test("variable_tags table has composite foreign key", () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -129,7 +129,7 @@ describe("VariableStore - set() Upsert Method", () => { test("set() creates new variable when (name, schema) doesn't exist", async () => { // Setup: store with schema and data node - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object", @@ -161,7 +161,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() updates value when (name, schema) already exists", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object", @@ -197,7 +197,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() creates variable with tags and labels", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -217,7 +217,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() preserves tags/labels when updating without options", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object", @@ -247,7 +247,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() allows same name with different schemas", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -285,7 +285,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() validates variable name", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -320,7 +320,7 @@ describe("VariableStore - set() Upsert Method", () => { test("set() extracts schema from value hash internally", async () => { // Given: Two different schemas - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -349,7 +349,7 @@ describe("VariableStore - set() Upsert Method", () => { test("set() upserts based on extracted schema", async () => { // Given: Existing variable with schemaA - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const value1 = await store.put(schemaA, 42); @@ -371,7 +371,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() throws CasNodeNotFoundError for invalid hash", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -388,7 +388,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() throws TagLabelConflictError when updating with tag key that matches new label", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { x: { type: "number" } } }; const schemaHash = await putSchema(store, schema); @@ -413,7 +413,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() throws TagLabelConflictError when updating with label that matches new tag key", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { x: { type: "number" } } }; const schemaHash = await putSchema(store, schema); @@ -438,7 +438,7 @@ describe("VariableStore - set() Upsert Method", () => { }); test("set() allows updating tags/labels when no conflicts", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = { type: "object", properties: { x: { type: "number" } } }; const schemaHash = await putSchema(store, schema); @@ -482,7 +482,7 @@ describe("VariableStore - get() with Optional Schema", () => { test("get(name, schema) returns Variable when exists", async () => { // Given: Variable with (name, schema) - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const value = await store.put(schema, 42); @@ -505,7 +505,7 @@ describe("VariableStore - get() with Optional Schema", () => { }); test("get(name, schema) returns null when name doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); @@ -523,7 +523,7 @@ describe("VariableStore - get() with Optional Schema", () => { test("get(name, schema) returns null when schema doesn't match", async () => { // Given: Variable with schemaA - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -545,7 +545,7 @@ describe("VariableStore - get() with Optional Schema", () => { test("get(name, schema) returns correct variant when multiple schemas exist", async () => { // Given: Same name with two different schemas - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -575,7 +575,7 @@ describe("VariableStore - get() with Optional Schema", () => { }); test("get(name, schema) includes tags and labels", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "object" }); const value = await store.put(schema, {}); @@ -598,7 +598,7 @@ describe("VariableStore - get() with Optional Schema", () => { }); test("get(name, schema) returns exact match", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -635,7 +635,7 @@ describe("VariableStore - get() with Optional Schema", () => { }); test("get(name, schema) returns null when combination doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -674,7 +674,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name) deletes all schema variants", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -712,7 +712,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name) returns empty array when variable doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; dbPath = tmpDbPath(); const varStore = new VariableStore(dbPath, store); @@ -725,7 +725,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name, schema) deletes only specified variant", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object", @@ -762,7 +762,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name, schema) throws VariableNotFoundError when not found", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); @@ -777,7 +777,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove() cascades deletion to tags and labels", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -817,7 +817,7 @@ describe("VariableStore - remove() with Optional Schema", () => { }); test("remove(name) returns array even with single variant", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -852,7 +852,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName accepts valid variable names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -878,7 +878,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName rejects empty name", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -893,7 +893,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName rejects invalid characters", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -933,7 +933,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName rejects empty segments", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -958,7 +958,7 @@ describe("VariableStore - Name Validation", () => { }); test("validateName rejects leading or trailing slashes", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -1034,7 +1034,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName error message mentions 'empty' for empty string", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1053,7 +1053,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName error message identifies specific invalid segment", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1073,7 +1073,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName error message explains consecutive slashes", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1092,7 +1092,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName error message distinguishes leading vs trailing slash", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1126,7 +1126,7 @@ describe("VariableStore - validateName() Error Messages", () => { }); test("validateName accepts valid names with dots, underscores, hyphens", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); schemaHash = await putSchema(store, { type: "object" }); dataHash = await store.put(schemaHash, {}); @@ -1160,7 +1160,7 @@ describe("VariableStore - Integration Tests", () => { }); test("Complete workflow: set, get, remove with multiple schemas", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaConfig = await putSchema(store, { @@ -1231,7 +1231,7 @@ describe("VariableStore - Integration Tests", () => { }); test("Upsert workflow preserves and updates tags", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object", @@ -1280,7 +1280,7 @@ describe("VariableStore - Legacy Update Method", () => { }); test("update() is distinct from set() and fails when variable doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -1305,7 +1305,7 @@ describe("VariableStore - Legacy Update Method", () => { }); test("update() throws SchemaMismatchError when schema changes", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "object" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1338,7 +1338,7 @@ describe("VariableStore - List Operation", () => { }); test("list() returns all variables", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const data1 = await store.put(schemaHash, { a: 1 }); @@ -1362,7 +1362,7 @@ describe("VariableStore - List Operation", () => { }); test("list() with namePrefix filters results", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const data = await store.put(schemaHash, {}); @@ -1397,7 +1397,7 @@ describe("VariableStore - list() with exactName", () => { test("list({ exactName }) returns all schema variants for name", async () => { // Given: Same name with multiple schemas - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1429,7 +1429,7 @@ describe("VariableStore - list() with exactName", () => { }); test("list({ exactName }) returns empty array when name doesn't exist", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); dbPath = tmpDbPath(); @@ -1443,7 +1443,7 @@ describe("VariableStore - list() with exactName", () => { test("list({ exactName, schema }) filters to specific variant", async () => { // Given: Same name with two schemas - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1470,7 +1470,7 @@ describe("VariableStore - list() with exactName", () => { }); test("list({ exactName }) with tags filters variants", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1497,7 +1497,7 @@ describe("VariableStore - list() with exactName", () => { }); test("exactName and namePrefix are mutually exclusive", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); dbPath = tmpDbPath(); @@ -1512,7 +1512,7 @@ describe("VariableStore - list() with exactName", () => { }); test("list({ namePrefix }) does match partial exact names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const value = await store.put(schema, 42); @@ -1542,7 +1542,7 @@ describe("VariableStore - list() with exactName", () => { // This test demonstrates that list({ exactName }) provides // the functionality previously available via get(name) → Variable[] - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaA = await putSchema(store, { type: "number" }); const schemaB = await putSchema(store, { type: "string" }); @@ -1579,7 +1579,7 @@ describe("VariableStore - Tag/Label Management", () => { }); test("tag() adds tags to existing variable", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -1599,7 +1599,7 @@ describe("VariableStore - Tag/Label Management", () => { }); test("tag() throws error for conflicting tag/label names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "object" }); const dataHash = await store.put(schemaHash, {}); @@ -1638,7 +1638,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should accept variable name with @ prefix in first segment", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test value"); @@ -1659,7 +1659,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should accept variable name starting with @", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "config value"); @@ -1677,7 +1677,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should accept complex @ prefix paths", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1704,7 +1704,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should reject @ in non-first segment", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1727,7 +1727,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should reject @ followed by invalid characters", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1751,7 +1751,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should still accept all previously valid names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1777,7 +1777,7 @@ describe("VariableStore - @ Prefix Variable Names", () => { }); test("should still reject previously invalid names", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schemaHash = await putSchema(store, { type: "string" }); const hash = await store.put(schemaHash, "test"); @@ -1819,7 +1819,7 @@ describe("VariableStore - History (LRU)", () => { }); test("history() initializes with single entry on create", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1834,7 +1834,7 @@ describe("VariableStore - History (LRU)", () => { }); test("history() pushes new values to position 0", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1852,7 +1852,7 @@ describe("VariableStore - History (LRU)", () => { }); test("set() with same value as current is idempotent (no history change)", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1870,7 +1870,7 @@ describe("VariableStore - History (LRU)", () => { }); test("setting an existing-history value moves it to position 0 (no duplicates)", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1890,7 +1890,7 @@ describe("VariableStore - History (LRU)", () => { }); test("history is bounded by MAX_HISTORY=10", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const values: string[] = []; @@ -1911,7 +1911,7 @@ describe("VariableStore - History (LRU)", () => { }); test("rollback semantics: re-setting an old value moves it to position 0", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); @@ -1933,7 +1933,7 @@ describe("VariableStore - History (LRU)", () => { }); test("history is cascade-deleted with the variable", async () => { - store = createMemoryStore(); + store = createMemoryStore().cas; await bootstrap(store); const schema = await putSchema(store, { type: "number" }); const v1 = await store.put(schema, 1); diff --git a/packages/core/src/wrap-envelope.test.ts b/packages/core/src/wrap-envelope.test.ts index 5f60348..400a0e1 100644 --- a/packages/core/src/wrap-envelope.test.ts +++ b/packages/core/src/wrap-envelope.test.ts @@ -5,7 +5,7 @@ import { wrapEnvelope } from "./wrap-envelope.js"; describe("wrapEnvelope", () => { test("resolves @ocas/output/put alias and returns envelope", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const envelope = await wrapEnvelope( @@ -19,7 +19,7 @@ describe("wrapEnvelope", () => { }); test("resolves @ocas/output/has alias with boolean value", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const envelope = await wrapEnvelope(store, "@ocas/output/has", true); @@ -29,7 +29,7 @@ describe("wrapEnvelope", () => { }); test("resolves @ocas/output/gc alias with object value", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const gcStats = { total: 100, reachable: 80, collected: 20, scanned: 5 }; @@ -40,7 +40,7 @@ describe("wrapEnvelope", () => { }); test("resolves primitive alias @ocas/string", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const aliases = await bootstrap(store); const envelope = await wrapEnvelope(store, "@ocas/string", "hello"); @@ -50,7 +50,7 @@ describe("wrapEnvelope", () => { }); test("throws for unknown alias", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); await expect( @@ -59,7 +59,7 @@ describe("wrapEnvelope", () => { }); test("is idempotent — same alias returns same type hash", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; const first = await wrapEnvelope(store, "@ocas/output/verify", "ok"); const second = await wrapEnvelope( @@ -74,7 +74,7 @@ describe("wrapEnvelope", () => { }); test("preserves complex object values without mutation", async () => { - const store = createMemoryStore(); + const store = createMemoryStore().cas; await bootstrap(store); const original = {