feat(core): MemoryStore returns OcasStore (cas + var + tag)

Rewrite createMemoryStore() to return an OcasStore with three sub-stores:
cas, var, tag. The cas sub-store keeps the existing Map-based logic, but
its put is now synchronous; the in-memory VarStore and TagStore are new
Map-based implementations of the types defined in #39.

The legacy Store.put signature is widened to Hash | Promise<Hash> so that
the new sync cas can still be passed to helpers (bootstrap, gc, render,
schema, …) that have not yet been migrated to the unified surface.

Tests in packages/core were mechanically updated from store.* to
store.cas.* and new test suites for VarStore (var-store.test.ts) and
TagStore (tag-store.test.ts) cover the spec from issue #40.

Closes #40

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 06:30:01 +00:00
parent 7ff3e438be
commit 56e3253829
20 changed files with 1215 additions and 313 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import type { Hash, Store } from "./types.js";
export const BOOTSTRAP_STORE = Symbol.for("@ocas/core/bootstrap-store"); export const BOOTSTRAP_STORE = Symbol.for("@ocas/core/bootstrap-store");
export type BootstrapCapableStore = Store & { export type BootstrapCapableStore = Store & {
[BOOTSTRAP_STORE](payload: unknown): Promise<Hash>; [BOOTSTRAP_STORE](payload: unknown): Hash | Promise<Hash>;
}; };
export function isBootstrapCapableStore( export function isBootstrapCapableStore(
+22 -22
View File
@@ -34,7 +34,7 @@ const OUTPUT_ALIASES = [
describe("bootstrap - Built-in Schemas", () => { describe("bootstrap - Built-in Schemas", () => {
test("should return map of 30 built-in schema aliases to hashes", async () => { 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); const builtinSchemas = await bootstrap(store);
// Should return object with 9 primitive + 21 output aliases = 30 // 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 () => { test("should register @ocas/schema as meta-schema alias", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"]; const metaHash = builtinSchemas["@ocas/schema"];
@@ -75,7 +75,7 @@ describe("bootstrap - Built-in Schemas", () => {
}); });
test("should register @ocas/string schema correctly", async () => { test("should register @ocas/string schema correctly", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const stringHash = builtinSchemas["@ocas/string"]; const stringHash = builtinSchemas["@ocas/string"];
@@ -86,7 +86,7 @@ describe("bootstrap - Built-in Schemas", () => {
}); });
test("should register @ocas/number schema correctly", async () => { test("should register @ocas/number schema correctly", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const numberHash = builtinSchemas["@ocas/number"]; const numberHash = builtinSchemas["@ocas/number"];
@@ -97,7 +97,7 @@ describe("bootstrap - Built-in Schemas", () => {
}); });
test("should register @ocas/object schema correctly", async () => { test("should register @ocas/object schema correctly", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const objectHash = builtinSchemas["@ocas/object"]; const objectHash = builtinSchemas["@ocas/object"];
@@ -108,7 +108,7 @@ describe("bootstrap - Built-in Schemas", () => {
}); });
test("should register @ocas/array schema correctly", async () => { test("should register @ocas/array schema correctly", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const arrayHash = builtinSchemas["@ocas/array"]; const arrayHash = builtinSchemas["@ocas/array"];
@@ -119,7 +119,7 @@ describe("bootstrap - Built-in Schemas", () => {
}); });
test("should register @ocas/bool schema correctly", async () => { test("should register @ocas/bool schema correctly", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const boolHash = builtinSchemas["@ocas/bool"]; const boolHash = builtinSchemas["@ocas/bool"];
@@ -130,7 +130,7 @@ describe("bootstrap - Built-in Schemas", () => {
}); });
test("should return same hashes on repeated bootstrap calls", async () => { test("should return same hashes on repeated bootstrap calls", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const first = await bootstrap(store); const first = await bootstrap(store);
const second = 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 () => { 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 builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"]; const metaHash = builtinSchemas["@ocas/schema"];
@@ -168,7 +168,7 @@ describe("bootstrap - Built-in Schemas", () => {
describe("bootstrap - @ocas/output/* Schemas", () => { describe("bootstrap - @ocas/output/* Schemas", () => {
test("each @ocas/output/* schema has a title", async () => { test("each @ocas/output/* schema has a title", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
for (const alias of OUTPUT_ALIASES) { 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 () => { test("@ocas/output/put schema describes a ocas_ref string", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const hash = aliases["@ocas/output/put"]; const hash = aliases["@ocas/output/put"];
if (!hash) throw new Error("@ocas/output/put not found"); 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 () => { 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 aliases = await bootstrap(store);
const hash = aliases["@ocas/output/get"]; const hash = aliases["@ocas/output/get"];
if (!hash) throw new Error("@ocas/output/get not found"); 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 () => { test("@ocas/output/has schema describes a boolean", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const hash = aliases["@ocas/output/has"]; const hash = aliases["@ocas/output/has"];
if (!hash) throw new Error("@ocas/output/has not found"); 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 () => { 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 aliases = await bootstrap(store);
const hash = aliases["@ocas/output/verify"]; const hash = aliases["@ocas/output/verify"];
if (!hash) throw new Error("@ocas/output/verify not found"); 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 () => { 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 aliases = await bootstrap(store);
const hash = aliases["@ocas/output/refs"]; const hash = aliases["@ocas/output/refs"];
if (!hash) throw new Error("@ocas/output/refs not found"); 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 () => { 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 aliases = await bootstrap(store);
const hash = aliases["@ocas/output/gc"]; const hash = aliases["@ocas/output/gc"];
if (!hash) throw new Error("@ocas/output/gc not found"); 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 () => { test("@ocas/output/var-set schema describes a Variable object", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const hash = aliases["@ocas/output/var-set"]; const hash = aliases["@ocas/output/var-set"];
if (!hash) throw new Error("@ocas/output/var-set not found"); 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 () => { 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 aliases = await bootstrap(store);
const hash = aliases["@ocas/output/var-list"]; const hash = aliases["@ocas/output/var-list"];
if (!hash) throw new Error("@ocas/output/var-list not found"); 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 () => { 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 aliases = await bootstrap(store);
const hash = aliases["@ocas/output/template-delete"]; const hash = aliases["@ocas/output/template-delete"];
if (!hash) throw new Error("@ocas/output/template-delete not found"); 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 () => { test("all @ocas/output/* schemas are distinct hashes", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const outputHashes = OUTPUT_ALIASES.map((alias) => aliases[alias]); const outputHashes = OUTPUT_ALIASES.map((alias) => aliases[alias]);
@@ -325,14 +325,14 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
describe("bootstrap - meta and schemas indexes (D1)", () => { describe("bootstrap - meta and schemas indexes (D1)", () => {
test("listMeta contains the bootstrap meta-schema hash", async () => { test("listMeta contains the bootstrap meta-schema hash", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const metaHash = aliases["@ocas/schema"]; const metaHash = aliases["@ocas/schema"];
expect(store.listMeta().map((e) => e.hash)).toContain(metaHash as string); expect(store.listMeta().map((e) => e.hash)).toContain(metaHash as string);
}); });
test("listSchemas contains meta-schema and all built-in schemas", async () => { test("listSchemas contains meta-schema and all built-in schemas", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const schemas = store.listSchemas().map((e) => e.hash); const schemas = store.listSchemas().map((e) => e.hash);
+5 -5
View File
@@ -28,7 +28,7 @@ describe("GC - Variable Model Refactoring", () => {
}); });
test("GC preserves variable-referenced nodes", async () => { test("GC preserves variable-referenced nodes", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = { type: "object", properties: { name: { type: "string" } } }; const schema = { type: "object", properties: { name: { type: "string" } } };
const schemaHash = await putSchema(store, schema); 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 () => { test("GC preserves nodes from variables with same name, different schemas", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = { type: "object", properties: { x: { type: "number" } } }; const schemaA = { type: "object", properties: { x: { type: "number" } } };
const schemaB = { type: "object", properties: { y: { type: "string" } } }; 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 () => { test("GC removes nodes after variable deletion", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = { type: "object", properties: { name: { type: "string" } } }; const schema = { type: "object", properties: { name: { type: "string" } } };
const schemaHash = await putSchema(store, schema); const schemaHash = await putSchema(store, schema);
@@ -102,7 +102,7 @@ describe("GC - Variable Model Refactoring", () => {
}); });
test("GC is global across all variables", async () => { test("GC is global across all variables", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = { type: "object", properties: { x: { type: "number" } } }; const schemaA = { type: "object", properties: { x: { type: "number" } } };
const schemaB = { type: "object", properties: { y: { type: "string" } } }; 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 () => { test("GC integration with refactored variable store", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = { type: "object", properties: { x: { type: "number" } } }; const schemaA = { type: "object", properties: { x: { type: "number" } } };
+32
View File
@@ -48,6 +48,38 @@ async function getInstance(): Promise<XXHashAPI> {
return _pending; 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<void> {
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)) * hash = XXH64(utf8(typeHash) ++ CBOR_deterministic(payload))
* Used for all normal nodes. * Used for all normal nodes.
+20 -20
View File
@@ -72,7 +72,7 @@ describe("computeHash", () => {
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("createMemoryStore – put and get", () => { describe("createMemoryStore – put and get", () => {
test("put returns a hash and get retrieves the node", async () => { 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 typeHash = await computeSelfHash({ name: "my-type" });
const hash = await store.put(typeHash, { greeting: "hello" }); const hash = await store.put(typeHash, { greeting: "hello" });
@@ -86,12 +86,12 @@ describe("createMemoryStore – put and get", () => {
}); });
test("get returns null for unknown hash", () => { test("get returns null for unknown hash", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
expect(store.get("0000000000000")).toBeNull(); expect(store.get("0000000000000")).toBeNull();
}); });
test("put is idempotent: same type+payload → same hash, no duplicate", async () => { 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 typeHash = await computeSelfHash({ name: "my-type" });
const h1 = await store.put(typeHash, { n: 42 }); 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 () => { test("put does not create self-referencing nodes", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const payload = { name: "type-descriptor" }; const payload = { name: "type-descriptor" };
const typeHash = await computeSelfHash(payload); const typeHash = await computeSelfHash(payload);
const hash = await store.put(typeHash, 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 () => { test("timestamp is preserved on second put (idempotency)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const typeHash = await computeSelfHash({ name: "my-type" }); const typeHash = await computeSelfHash({ name: "my-type" });
const h1 = await store.put(typeHash, { v: 1 }); const h1 = await store.put(typeHash, { v: 1 });
@@ -131,7 +131,7 @@ describe("createMemoryStore – put and get", () => {
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("createMemoryStore – has", () => { describe("createMemoryStore – has", () => {
test("has returns false before put, true after", async () => { test("has returns false before put, true after", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const typeHash = await computeSelfHash({ name: "t" }); const typeHash = await computeSelfHash({ name: "t" });
const hash = await computeHash(typeHash, { x: 1 }); const hash = await computeHash(typeHash, { x: 1 });
@@ -141,7 +141,7 @@ describe("createMemoryStore – has", () => {
}); });
test("listByType returns all stored hashes for a type", async () => { test("listByType returns all stored hashes for a type", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const typeHash = await computeSelfHash({ name: "t" }); const typeHash = await computeSelfHash({ name: "t" });
const h1 = await store.put(typeHash, { a: 1 }); const h1 = await store.put(typeHash, { a: 1 });
@@ -156,7 +156,7 @@ describe("createMemoryStore – has", () => {
}); });
test("listByType returns empty array on fresh store", () => { test("listByType returns empty array on fresh store", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
expect(store.listByType("0000000000000")).toEqual([]); expect(store.listByType("0000000000000")).toEqual([]);
}); });
}); });
@@ -166,12 +166,12 @@ describe("createMemoryStore – has", () => {
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("createMemoryStore – listByType", () => { describe("createMemoryStore – listByType", () => {
test("returns empty array for unknown type", () => { test("returns empty array for unknown type", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
expect(store.listByType("0000000000000")).toEqual([]); expect(store.listByType("0000000000000")).toEqual([]);
}); });
test("returns all hashes for the given type", async () => { test("returns all hashes for the given type", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const typeHash = await computeSelfHash({ name: "t" }); const typeHash = await computeSelfHash({ name: "t" });
const otherType = await computeSelfHash({ name: "other" }); const otherType = await computeSelfHash({ name: "other" });
@@ -186,7 +186,7 @@ describe("createMemoryStore – listByType", () => {
}); });
test("idempotent put does not duplicate in listByType", async () => { test("idempotent put does not duplicate in listByType", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const typeHash = await computeSelfHash({ name: "t" }); const typeHash = await computeSelfHash({ name: "t" });
const h1 = await store.put(typeHash, { n: 1 }); 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 () => { test("bootstrap node is listed under its self type", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const hash = builtinSchemas["@ocas/schema"] ?? ""; const hash = builtinSchemas["@ocas/schema"] ?? "";
@@ -216,7 +216,7 @@ describe("createMemoryStore – listByType", () => {
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("verify", () => { describe("verify", () => {
test("returns true for a correctly stored node", async () => { test("returns true for a correctly stored node", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const typeHash = await computeSelfHash({ name: "my-type" }); const typeHash = await computeSelfHash({ name: "my-type" });
const hash = await store.put(typeHash, { data: 123 }); const hash = await store.put(typeHash, { data: 123 });
const node = store.get(hash) as CasNode; const node = store.get(hash) as CasNode;
@@ -225,7 +225,7 @@ describe("verify", () => {
}); });
test("returns false when payload is tampered", async () => { test("returns false when payload is tampered", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const typeHash = await computeSelfHash({ name: "my-type" }); const typeHash = await computeSelfHash({ name: "my-type" });
const hash = await store.put(typeHash, { data: 123 }); const hash = await store.put(typeHash, { data: 123 });
@@ -238,7 +238,7 @@ describe("verify", () => {
}); });
test("returns false when type is tampered", async () => { test("returns false when type is tampered", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const typeHash = await computeSelfHash({ name: "my-type" }); const typeHash = await computeSelfHash({ name: "my-type" });
const hash = await store.put(typeHash, { data: 123 }); const hash = await store.put(typeHash, { data: 123 });
const node = store.get(hash) as CasNode; const node = store.get(hash) as CasNode;
@@ -265,7 +265,7 @@ describe("bootstrap", () => {
}); });
test("returns a map with 30 built-in schema aliases", async () => { test("returns a map with 30 built-in schema aliases", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
expect(builtinSchemas).toHaveProperty("@ocas/schema"); expect(builtinSchemas).toHaveProperty("@ocas/schema");
@@ -288,7 +288,7 @@ describe("bootstrap", () => {
}); });
test("meta-schema node is stored and retrievable", async () => { test("meta-schema node is stored and retrievable", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaHash = builtinSchemas["@ocas/schema"] ?? "";
@@ -298,7 +298,7 @@ describe("bootstrap", () => {
}); });
test("meta-schema node is self-referencing: type === hash", async () => { test("meta-schema node is self-referencing: type === hash", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaHash = builtinSchemas["@ocas/schema"] ?? "";
const node = store.get(metaHash) as CasNode; const node = store.get(metaHash) as CasNode;
@@ -307,7 +307,7 @@ describe("bootstrap", () => {
}); });
test("bootstrap node passes verify()", async () => { test("bootstrap node passes verify()", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaHash = builtinSchemas["@ocas/schema"] ?? "";
const node = store.get(metaHash) as CasNode; const node = store.get(metaHash) as CasNode;
@@ -316,7 +316,7 @@ describe("bootstrap", () => {
}); });
test("bootstrap is idempotent: same hashes on repeated calls", async () => { test("bootstrap is idempotent: same hashes on repeated calls", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const h1 = await bootstrap(store); const h1 = await bootstrap(store);
const h2 = await bootstrap(store); const h2 = await bootstrap(store);
+1 -1
View File
@@ -13,7 +13,7 @@ import { createVariableStore } from "./variable-store.js";
async function createTempVarStore() { async function createTempVarStore() {
const tempDir = await mkdtemp(join(tmpdir(), "ocas-test-")); const tempDir = await mkdtemp(join(tmpdir(), "ocas-test-"));
const dbPath = join(tempDir, "vars.db"); const dbPath = join(tempDir, "vars.db");
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const varStore = createVariableStore(dbPath, store); const varStore = createVariableStore(dbPath, store);
return { return {
+16 -16
View File
@@ -22,7 +22,7 @@ async function putN(
describe("listByType - pagination + sort + timestamps", () => { describe("listByType - pagination + sort + timestamps", () => {
test("A1. returns objects with hash/created/updated", async () => { test("A1. returns objects with hash/created/updated", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 3, 0); await putN(store, m, 3, 0);
@@ -36,7 +36,7 @@ describe("listByType - pagination + sort + timestamps", () => {
}); });
test("A2. default sort is created ASC", async () => { test("A2. default sort is created ASC", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 4); await putN(store, m, 4);
@@ -49,7 +49,7 @@ describe("listByType - pagination + sort + timestamps", () => {
}); });
test("A3. desc:true reverses order", async () => { test("A3. desc:true reverses order", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 4); 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 () => { 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" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 4); await putN(store, m, 4);
@@ -72,14 +72,14 @@ describe("listByType - pagination + sort + timestamps", () => {
}); });
test("A5. limit truncates", async () => { test("A5. limit truncates", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 5, 0); await putN(store, m, 5, 0);
expect(store.listByType(m, { limit: 2 })).toHaveLength(2); expect(store.listByType(m, { limit: 2 })).toHaveLength(2);
}); });
test("A6. offset skips", async () => { test("A6. offset skips", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 5); await putN(store, m, 5);
@@ -90,21 +90,21 @@ describe("listByType - pagination + sort + timestamps", () => {
}); });
test("A7. limit:0 returns empty array", async () => { test("A7. limit:0 returns empty array", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 3, 0); await putN(store, m, 3, 0);
expect(store.listByType(m, { limit: 0 })).toEqual([]); expect(store.listByType(m, { limit: 0 })).toEqual([]);
}); });
test("A8. offset past end returns empty array", async () => { test("A8. offset past end returns empty array", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 3, 0); await putN(store, m, 3, 0);
expect(store.listByType(m, { offset: 100 })).toEqual([]); expect(store.listByType(m, { offset: 100 })).toEqual([]);
}); });
test("A9. core has no default limit (returns all)", async () => { 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" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 150, 0); await putN(store, m, 150, 0);
// No CLI-layer cap; with 150 nodes of type m (plus m itself which is // 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 () => { test("A10. desc + offset + limit combined", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 5, 15); await putN(store, m, 5, 15);
const all = store.listByType(m); const all = store.listByType(m);
@@ -128,7 +128,7 @@ describe("listByType - pagination + sort + timestamps", () => {
describe("listMeta / listSchemas - pagination", () => { describe("listMeta / listSchemas - pagination", () => {
test("B1. listMeta returns {hash,created,updated}", async () => { test("B1. listMeta returns {hash,created,updated}", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const h = await store[BOOTSTRAP_STORE]({ type: "object" }); const h = await store[BOOTSTRAP_STORE]({ type: "object" });
const list = store.listMeta(); const list = store.listMeta();
expect(list).toHaveLength(1); expect(list).toHaveLength(1);
@@ -139,7 +139,7 @@ describe("listMeta / listSchemas - pagination", () => {
}); });
test("B2. listMeta has no default limit (returns all)", async () => { test("B2. listMeta has no default limit (returns all)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
for (let i = 0; i < 150; i++) { for (let i = 0; i < 150; i++) {
await store[BOOTSTRAP_STORE]({ type: "object", i }); await store[BOOTSTRAP_STORE]({ type: "object", i });
} }
@@ -147,7 +147,7 @@ describe("listMeta / listSchemas - pagination", () => {
}); });
test("B3. listMeta limit/offset/desc", async () => { test("B3. listMeta limit/offset/desc", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
await store[BOOTSTRAP_STORE]({ type: "object", i }); await store[BOOTSTRAP_STORE]({ type: "object", i });
await new Promise((r) => setTimeout(r, 2)); await new Promise((r) => setTimeout(r, 2));
@@ -159,7 +159,7 @@ describe("listMeta / listSchemas - pagination", () => {
}); });
test("B4. listSchemas returns objects, supports limit", async () => { test("B4. listSchemas returns objects, supports limit", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await store.put(m, { type: "string" }); await store.put(m, { type: "string" });
await store.put(m, { type: "number" }); await store.put(m, { type: "number" });
@@ -175,7 +175,7 @@ describe("listMeta / listSchemas - pagination", () => {
describe("Determinism / edge cases", () => { describe("Determinism / edge cases", () => {
test("I1. same-ms timestamps yield deterministic ordering across calls", async () => { 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" }); const m = await store[BOOTSTRAP_STORE]({ type: "object" });
// No delay → likely same millisecond // No delay → likely same millisecond
await putN(store, m, 5, 0); await putN(store, m, 5, 0);
@@ -185,7 +185,7 @@ describe("Determinism / edge cases", () => {
}); });
test("I2. empty store returns []", () => { test("I2. empty store returns []", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
expect(store.listByType("0000000000000")).toEqual([]); expect(store.listByType("0000000000000")).toEqual([]);
expect(store.listMeta()).toEqual([]); expect(store.listMeta()).toEqual([]);
expect(store.listSchemas()).toEqual([]); expect(store.listSchemas()).toEqual([]);
+7 -5
View File
@@ -3,15 +3,17 @@ import { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
import { createMemoryStore } from "./store.js"; import { createMemoryStore } from "./store.js";
import type { CasNode, Hash, ListEntry, ListOptions } from "./types.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 { export class MemStore implements BootstrapCapableStore {
readonly #inner: BootstrapCapableStore; readonly #inner: ReturnType<typeof createMemoryStore>["cas"];
constructor() { constructor() {
this.#inner = createMemoryStore(); this.#inner = createMemoryStore().cas;
} }
put(typeHash: Hash, payload: unknown): Promise<Hash> { async put(typeHash: Hash, payload: unknown): Promise<Hash> {
return this.#inner.put(typeHash, payload); return this.#inner.put(typeHash, payload);
} }
@@ -43,7 +45,7 @@ export class MemStore implements BootstrapCapableStore {
this.#inner.delete(hash); this.#inner.delete(hash);
} }
[BOOTSTRAP_STORE](payload: unknown): Promise<Hash> { async [BOOTSTRAP_STORE](payload: unknown): Promise<Hash> {
return this.#inner[BOOTSTRAP_STORE](payload); return this.#inner[BOOTSTRAP_STORE](payload);
} }
} }
+4 -4
View File
@@ -43,7 +43,7 @@ describe("registerOutputTemplates", () => {
test("registers a template for every @ocas/output/* schema", async () => { test("registers a template for every @ocas/output/* schema", async () => {
tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-"));
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
varStore = createVariableStore(join(tempDir, "vars.db"), store); varStore = createVariableStore(join(tempDir, "vars.db"), store);
@@ -58,7 +58,7 @@ describe("registerOutputTemplates", () => {
test("each template is retrievable via @ocas/template/text/<hash>", async () => { test("each template is retrievable via @ocas/template/text/<hash>", async () => {
tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-"));
store = createMemoryStore(); store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
varStore = createVariableStore(join(tempDir, "vars.db"), store); varStore = createVariableStore(join(tempDir, "vars.db"), store);
@@ -84,7 +84,7 @@ describe("registerOutputTemplates", () => {
test("is idempotent — safe to call multiple times", async () => { test("is idempotent — safe to call multiple times", async () => {
tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-"));
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
varStore = createVariableStore(join(tempDir, "vars.db"), store); varStore = createVariableStore(join(tempDir, "vars.db"), store);
@@ -96,7 +96,7 @@ describe("registerOutputTemplates", () => {
test("@ocas/output/put template contains payload reference", async () => { test("@ocas/output/put template contains payload reference", async () => {
tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-")); tempDir = await mkdtemp(join(tmpdir(), "ocas-tmpl-"));
store = createMemoryStore(); store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
varStore = createVariableStore(join(tempDir, "vars.db"), store); varStore = createVariableStore(join(tempDir, "vars.db"), store);
+45 -45
View File
@@ -8,7 +8,7 @@ import { CasNodeNotFoundError } from "./variable-store.js";
describe("Suite 1: Basic Rendering (No Nesting)", () => { describe("Suite 1: Basic Rendering (No Nesting)", () => {
test("1.1 Render Simple Primitives", async () => { test("1.1 Render Simple Primitives", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" }); const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "hello"); 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 () => { test("1.2 Render Object Node (Flat)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const objSchema = await putSchema(store, { const objSchema = await putSchema(store, {
type: "object", type: "object",
@@ -40,7 +40,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => {
}); });
test("1.3 Render Array Node (Flat)", async () => { test("1.3 Render Array Node (Flat)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const arraySchema = await putSchema(store, { const arraySchema = await putSchema(store, {
type: "array", type: "array",
@@ -56,7 +56,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => {
}); });
test("1.4 Render with resolution=0 (Force Reference)", async () => { test("1.4 Render with resolution=0 (Force Reference)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" }); const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "hello"); 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", () => { test("1.5 Render Non-existent Hash Throws Error", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const fakeHash = "ZZZZZZZZZZZZZ" as Hash; const fakeHash = "ZZZZZZZZZZZZZ" as Hash;
// Non-existent root node should throw // Non-existent root node should throw
@@ -79,7 +79,7 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => {
describe("Suite 2: Resolution Decay Model", () => { describe("Suite 2: Resolution Decay Model", () => {
test("2.1 Single-level Nesting with Default Decay", async () => { test("2.1 Single-level Nesting with Default Decay", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const childSchema = await putSchema(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 () => { test("2.2 Multi-level Nesting Reaches Epsilon", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const leafSchema = await putSchema(store, { const leafSchema = await putSchema(store, {
@@ -151,7 +151,7 @@ describe("Suite 2: Resolution Decay Model", () => {
}); });
test("2.3 High Decay (Quick Cutoff)", async () => { test("2.3 High Decay (Quick Cutoff)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nodeSchema = await putSchema(store, { const nodeSchema = await putSchema(store, {
@@ -189,7 +189,7 @@ describe("Suite 2: Resolution Decay Model", () => {
}); });
test("2.4 Low Decay (Deep Expansion)", async () => { test("2.4 Low Decay (Deep Expansion)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nodeSchema = await putSchema(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 () => { test("2.5 Starting Resolution Below 1.0", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nodeSchema = await putSchema(store, { const nodeSchema = await putSchema(store, {
@@ -262,7 +262,7 @@ describe("Suite 2: Resolution Decay Model", () => {
describe("Suite 3: Complex Graph Structures", () => { describe("Suite 3: Complex Graph Structures", () => {
test("3.1 Multiple Child References", async () => { test("3.1 Multiple Child References", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const itemSchema = await putSchema(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 () => { test("3.2 Object with Multiple ocas_ref Fields", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const childSchema = await putSchema(store, { const childSchema = await putSchema(store, {
@@ -340,7 +340,7 @@ describe("Suite 3: Complex Graph Structures", () => {
}); });
test("3.3 Cycle Detection", async () => { test("3.3 Cycle Detection", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nodeSchema = await putSchema(store, { const nodeSchema = await putSchema(store, {
@@ -372,7 +372,7 @@ describe("Suite 3: Complex Graph Structures", () => {
}); });
test("3.4 DAG (Shared Descendant)", async () => { test("3.4 DAG (Shared Descendant)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const leafSchema = await putSchema(store, { const leafSchema = await putSchema(store, {
@@ -423,7 +423,7 @@ describe("Suite 3: Complex Graph Structures", () => {
}); });
test("3.5 Deep Tree", async () => { test("3.5 Deep Tree", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nodeSchema = await putSchema(store, { const nodeSchema = await putSchema(store, {
@@ -464,7 +464,7 @@ describe("Suite 3: Complex Graph Structures", () => {
describe("Suite 4: Epsilon Boundary Cases", () => { describe("Suite 4: Epsilon Boundary Cases", () => {
test("4.1 Resolution Exactly at Epsilon", async () => { test("4.1 Resolution Exactly at Epsilon", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" }); const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "test"); 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 () => { test("4.2 Resolution Just Above Epsilon", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" }); const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "test"); 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 () => { test("4.3 Very Small Epsilon (Deep Expansion)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nodeSchema = await putSchema(store, { const nodeSchema = await putSchema(store, {
@@ -529,7 +529,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => {
}); });
test("4.4 Zero Epsilon (Never Prune)", async () => { test("4.4 Zero Epsilon (Never Prune)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nodeSchema = await putSchema(store, { const nodeSchema = await putSchema(store, {
@@ -566,7 +566,7 @@ describe("Suite 4: Epsilon Boundary Cases", () => {
describe("Suite 5: YAML Output Format", () => { describe("Suite 5: YAML Output Format", () => {
test("5.1 Valid YAML Syntax", async () => { test("5.1 Valid YAML Syntax", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const objSchema = await putSchema(store, { const objSchema = await putSchema(store, {
type: "object", type: "object",
@@ -584,7 +584,7 @@ describe("Suite 5: YAML Output Format", () => {
}); });
test("5.2 Nested Object Indentation", async () => { test("5.2 Nested Object Indentation", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nestedSchema = await putSchema(store, { const nestedSchema = await putSchema(store, {
type: "object", type: "object",
@@ -610,7 +610,7 @@ describe("Suite 5: YAML Output Format", () => {
}); });
test("5.3 Array Rendering", async () => { test("5.3 Array Rendering", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const arraySchema = await putSchema(store, { const arraySchema = await putSchema(store, {
type: "array", type: "array",
@@ -625,7 +625,7 @@ describe("Suite 5: YAML Output Format", () => {
}); });
test("5.4 CAS Reference in YAML", async () => { test("5.4 CAS Reference in YAML", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const childSchema = await putSchema(store, { const childSchema = await putSchema(store, {
@@ -655,7 +655,7 @@ describe("Suite 5: YAML Output Format", () => {
}); });
test("5.5 Special Characters Escaping", async () => { test("5.5 Special Characters Escaping", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const textSchema = await putSchema(store, { type: "string" }); const textSchema = await putSchema(store, { type: "string" });
const hash = await store.put(textSchema, "line1\nline2: value"); 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 () => { test("5.6 Null Handling", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nullableSchema = await putSchema(store, { const nullableSchema = await putSchema(store, {
type: "object", type: "object",
@@ -687,7 +687,7 @@ describe("Suite 5: YAML Output Format", () => {
describe("Suite 6: Schema Integration", () => { describe("Suite 6: Schema Integration", () => {
test("6.1 Detect ocas_ref Fields via Schema", async () => { test("6.1 Detect ocas_ref Fields via Schema", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const childSchema = await putSchema(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 () => { test("6.2 Non-ocas_ref String Not Expanded", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const objSchema = await putSchema(store, { const objSchema = await putSchema(store, {
type: "object", type: "object",
@@ -734,7 +734,7 @@ describe("Suite 6: Schema Integration", () => {
}); });
test("6.3 Array of ocas_ref", async () => { test("6.3 Array of ocas_ref", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const itemSchema = await putSchema(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 () => { test("6.4 anyOf with ocas_ref (Nullable Reference)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const childSchema = await putSchema(store, { const childSchema = await putSchema(store, {
@@ -794,7 +794,7 @@ describe("Suite 6: Schema Integration", () => {
}); });
test("6.5 Schema-less Node (Bootstrap Node)", async () => { test("6.5 Schema-less Node (Bootstrap Node)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const types = await bootstrap(store); const types = await bootstrap(store);
const schemaHash = types["@ocas/schema"]; const schemaHash = types["@ocas/schema"];
@@ -807,7 +807,7 @@ describe("Suite 6: Schema Integration", () => {
describe("Suite 7: Error Handling", () => { describe("Suite 7: Error Handling", () => {
test("7.1 Missing Referenced Node", async () => { test("7.1 Missing Referenced Node", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const parentSchema = await putSchema(store, { const parentSchema = await putSchema(store, {
@@ -826,21 +826,21 @@ describe("Suite 7: Error Handling", () => {
}); });
test("7.3 Invalid Resolution Parameter", () => { test("7.3 Invalid Resolution Parameter", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const fakeHash = "AAAAAAAAAAAAA" as Hash; const fakeHash = "AAAAAAAAAAAAA" as Hash;
expect(() => render(store, fakeHash, { resolution: -1 })).toThrow(); expect(() => render(store, fakeHash, { resolution: -1 })).toThrow();
}); });
test("7.4 Invalid Decay Parameter", () => { test("7.4 Invalid Decay Parameter", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const fakeHash = "AAAAAAAAAAAAA" as Hash; const fakeHash = "AAAAAAAAAAAAA" as Hash;
expect(() => render(store, fakeHash, { decay: 1.5 })).toThrow(); expect(() => render(store, fakeHash, { decay: 1.5 })).toThrow();
}); });
test("7.5 Invalid Epsilon Parameter", () => { test("7.5 Invalid Epsilon Parameter", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const fakeHash = "AAAAAAAAAAAAA" as Hash; const fakeHash = "AAAAAAAAAAAAA" as Hash;
expect(() => render(store, fakeHash, { epsilon: -0.01 })).toThrow(); expect(() => render(store, fakeHash, { epsilon: -0.01 })).toThrow();
@@ -849,7 +849,7 @@ describe("Suite 7: Error Handling", () => {
describe("Suite 8: Performance & Edge Cases", () => { describe("Suite 8: Performance & Edge Cases", () => {
test("8.1 Large Payload", async () => { test("8.1 Large Payload", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const arraySchema = await putSchema(store, { const arraySchema = await putSchema(store, {
type: "array", type: "array",
@@ -877,7 +877,7 @@ describe("Suite 8: Performance & Edge Cases", () => {
}); });
test("8.2 Wide Fan-out", async () => { test("8.2 Wide Fan-out", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const itemSchema = await putSchema(store, { const itemSchema = await putSchema(store, {
@@ -909,7 +909,7 @@ describe("Suite 8: Performance & Edge Cases", () => {
}); });
test("8.3 Empty Payload", async () => { test("8.3 Empty Payload", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const emptySchema = await putSchema(store, { type: "object" }); const emptySchema = await putSchema(store, { type: "object" });
const hash = await store.put(emptySchema, {}); const hash = await store.put(emptySchema, {});
@@ -920,7 +920,7 @@ describe("Suite 8: Performance & Edge Cases", () => {
}); });
test("8.4 Unicode in Payload", async () => { test("8.4 Unicode in Payload", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const textSchema = await putSchema(store, { const textSchema = await putSchema(store, {
type: "object", type: "object",
@@ -985,7 +985,7 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => {
}); });
test("9.5 Render with store expands ocas_ref fields", async () => { test("9.5 Render with store expands ocas_ref fields", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
// Create a child node // 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 () => { test("9.10 store present but schema missing — renders without ref expansion", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const unknownType = "ZZZZZZZZZZZZ0" as Hash; const unknownType = "ZZZZZZZZZZZZ0" as Hash;
const output = renderDirect(unknownType, { key: "val" }, store, null); 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)", () => { describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => {
test("10.1 renderAsync() throws CasNodeNotFoundError for missing root hash", async () => { test("10.1 renderAsync() throws CasNodeNotFoundError for missing root hash", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const fakeHash = "AAAAAAAAAAAAA" as Hash; 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", () => { test("10.2 render() throws CasNodeNotFoundError for missing root hash", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const fakeHash = "ZZZZZZZZZZZZZ" as Hash; const fakeHash = "ZZZZZZZZZZZZZ" as Hash;
expect(() => render(store, fakeHash)).toThrow(CasNodeNotFoundError); 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", () => { 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 fakeTypeHash = "0000000000000" as Hash;
const output = renderDirect(fakeTypeHash, { key: "value" }, store, null); 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 () => { test("10.4 Missing nested node renders as cas: reference (no error)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const parentSchema = await putSchema(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 () => { test("10.5 Resolution below epsilon renders as cas: reference (no error)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const nodeSchema = await putSchema(store, { const nodeSchema = await putSchema(store, {
+48 -48
View File
@@ -10,14 +10,14 @@ import type { CasNode } from "./types.js";
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("putSchema", () => { describe("putSchema", () => {
test("returns a valid 13-char hash", async () => { test("returns a valid 13-char hash", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { type: "object", properties: {} }); const hash = await putSchema(store, { type: "object", properties: {} });
expect(hash).toHaveLength(13); expect(hash).toHaveLength(13);
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
}); });
test("schema node is stored in the store", async () => { 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 schema = { type: "object", properties: { name: { type: "string" } } };
const hash = await putSchema(store, schema); const hash = await putSchema(store, schema);
@@ -28,7 +28,7 @@ describe("putSchema", () => {
}); });
test("schema node type equals the meta-schema hash", async () => { test("schema node type equals the meta-schema hash", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaHash = builtinSchemas["@ocas/schema"] ?? "";
const schemaHash = await putSchema(store, { type: "string" }); const schemaHash = await putSchema(store, { type: "string" });
@@ -38,7 +38,7 @@ describe("putSchema", () => {
}); });
test("putSchema is idempotent: same schema → same hash", async () => { test("putSchema is idempotent: same schema → same hash", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schema = { type: "number" }; const schema = { type: "number" };
const h1 = await putSchema(store, schema); const h1 = await putSchema(store, schema);
const h2 = await putSchema(store, schema); const h2 = await putSchema(store, schema);
@@ -47,7 +47,7 @@ describe("putSchema", () => {
}); });
test("different schemas produce different hashes", async () => { test("different schemas produce different hashes", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const h1 = await putSchema(store, { type: "string" }); const h1 = await putSchema(store, { type: "string" });
const h2 = await putSchema(store, { type: "number" }); const h2 = await putSchema(store, { type: "number" });
@@ -60,7 +60,7 @@ describe("putSchema", () => {
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("getSchema", () => { describe("getSchema", () => {
test("returns the original schema object", async () => { test("returns the original schema object", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schema = { type: "object", properties: { age: { type: "number" } } }; const schema = { type: "object", properties: { age: { type: "number" } } };
const hash = await putSchema(store, schema); const hash = await putSchema(store, schema);
@@ -68,12 +68,12 @@ describe("getSchema", () => {
}); });
test("returns null for an unknown hash", () => { test("returns null for an unknown hash", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
expect(getSchema(store, "0000000000000")).toBeNull(); expect(getSchema(store, "0000000000000")).toBeNull();
}); });
test("roundtrip: put then get returns the same schema", async () => { test("roundtrip: put then get returns the same schema", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schema = { const schema = {
type: "object", type: "object",
required: ["id"], required: ["id"],
@@ -92,7 +92,7 @@ describe("getSchema", () => {
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("validate", () => { describe("validate", () => {
test("returns true when payload matches the schema", async () => { test("returns true when payload matches the schema", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { name: { type: "string" }, age: { type: "number" } }, properties: { name: { type: "string" }, age: { type: "number" } },
@@ -105,7 +105,7 @@ describe("validate", () => {
}); });
test("returns false when payload violates the schema", async () => { test("returns false when payload violates the schema", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { count: { type: "number" } }, properties: { count: { type: "number" } },
@@ -118,7 +118,7 @@ describe("validate", () => {
}); });
test("returns false when required field is missing", async () => { test("returns false when required field is missing", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
required: ["title"], required: ["title"],
@@ -131,7 +131,7 @@ describe("validate", () => {
}); });
test("returns false when schema cannot be found", async () => { test("returns false when schema cannot be found", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const fakeNode: CasNode = { const fakeNode: CasNode = {
type: "0000000000000", type: "0000000000000",
payload: { x: 1 }, payload: { x: 1 },
@@ -147,7 +147,7 @@ describe("validate", () => {
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("refs", () => { describe("refs", () => {
test("returns empty array when schema has no ocas_ref fields", async () => { test("returns empty array when schema has no ocas_ref fields", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { title: { type: "string" } }, properties: { title: { type: "string" } },
@@ -159,7 +159,7 @@ describe("refs", () => {
}); });
test("returns the ocas_ref hash values from payload", async () => { test("returns the ocas_ref hash values from payload", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { properties: {
@@ -179,7 +179,7 @@ describe("refs", () => {
}); });
test("collects multiple ocas_ref fields", async () => { test("collects multiple ocas_ref fields", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { properties: {
@@ -203,7 +203,7 @@ describe("refs", () => {
}); });
test("skips null/undefined ocas_ref values", async () => { test("skips null/undefined ocas_ref values", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { properties: {
@@ -219,7 +219,7 @@ describe("refs", () => {
}); });
test("returns empty array when schema is not found", () => { test("returns empty array when schema is not found", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const orphanNode: CasNode = { const orphanNode: CasNode = {
type: "0000000000000", type: "0000000000000",
payload: { x: 1 }, payload: { x: 1 },
@@ -235,7 +235,7 @@ describe("refs", () => {
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("walk", () => { describe("walk", () => {
test("visits a single node with no refs", async () => { test("visits a single node with no refs", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { val: { type: "number" } }, properties: { val: { type: "number" } },
@@ -249,7 +249,7 @@ describe("walk", () => {
}); });
test("visits all reachable nodes in a chain A → B → C", async () => { test("visits all reachable nodes in a chain A → B → C", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { properties: {
@@ -273,7 +273,7 @@ describe("walk", () => {
}); });
test("handles cycles without infinite loop", async () => { test("handles cycles without infinite loop", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { properties: {
@@ -316,7 +316,7 @@ describe("walk", () => {
}); });
test("skips missing hashes gracefully", async () => { test("skips missing hashes gracefully", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { ref: { type: "string", format: "ocas_ref" } }, properties: { ref: { type: "string", format: "ocas_ref" } },
@@ -331,7 +331,7 @@ describe("walk", () => {
}); });
test("visitor receives both hash and node", async () => { test("visitor receives both hash and node", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
properties: { x: { type: "number" } }, properties: { x: { type: "number" } },
@@ -355,7 +355,7 @@ describe("walk", () => {
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
describe("bootstrap meta-schema self-reference", () => { describe("bootstrap meta-schema self-reference", () => {
test("metaNode.type === metaHash (self-referencing)", async () => { test("metaNode.type === metaHash (self-referencing)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaHash = builtinSchemas["@ocas/schema"] ?? "";
const metaNode = store.get(metaHash) as CasNode; const metaNode = store.get(metaHash) as CasNode;
@@ -364,7 +364,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("schema nodes have type === metaHash", async () => { test("schema nodes have type === metaHash", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaHash = builtinSchemas["@ocas/schema"] ?? "";
const schemaHash = await putSchema(store, { type: "string" }); 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 () => { test("data nodes have type === schemaHash (not metaHash)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaHash = builtinSchemas["@ocas/schema"] ?? "";
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
@@ -391,7 +391,7 @@ describe("bootstrap meta-schema self-reference", () => {
// ── P1 leaf constraints ────────────────────────────────────────────────── // ── P1 leaf constraints ──────────────────────────────────────────────────
test("accepts schema with numeric constraints (minimum/maximum)", async () => { test("accepts schema with numeric constraints (minimum/maximum)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "number", type: "number",
minimum: 0, minimum: 0,
@@ -413,7 +413,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with string constraints (minLength/maxLength/pattern)", async () => { test("accepts schema with string constraints (minLength/maxLength/pattern)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "string", type: "string",
minLength: 1, minLength: 1,
@@ -430,7 +430,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with array constraints (minItems/maxItems/uniqueItems)", async () => { test("accepts schema with array constraints (minItems/maxItems/uniqueItems)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "array", type: "array",
items: { type: "number" }, items: { type: "number" },
@@ -451,7 +451,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("rejects schema with wrong constraint types", async () => { test("rejects schema with wrong constraint types", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await expect( await expect(
putSchema(store, { type: "number", minimum: "zero" } as never), putSchema(store, { type: "number", minimum: "zero" } as never),
).rejects.toThrow(); ).rejects.toThrow();
@@ -464,7 +464,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with nested property constraints", async () => { test("accepts schema with nested property constraints", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "object", type: "object",
properties: { properties: {
@@ -490,7 +490,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("bootstrap is idempotent across putSchema calls", async () => { test("bootstrap is idempotent across putSchema calls", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const builtinSchemas = await bootstrap(store); const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@ocas/schema"] ?? ""; const metaHash = builtinSchemas["@ocas/schema"] ?? "";
@@ -505,7 +505,7 @@ describe("bootstrap meta-schema self-reference", () => {
// ── P2 combinators, conditionals, and leaf constraints ────────────────── // ── P2 combinators, conditionals, and leaf constraints ──────────────────
test("accepts schema with allOf", async () => { test("accepts schema with allOf", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
allOf: [ allOf: [
{ type: "object", properties: { name: { type: "string" } } }, { type: "object", properties: { name: { type: "string" } } },
@@ -522,7 +522,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with if/then/else", async () => { test("accepts schema with if/then/else", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "object", type: "object",
properties: { properties: {
@@ -538,7 +538,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with patternProperties", async () => { test("accepts schema with patternProperties", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "object", type: "object",
patternProperties: { patternProperties: {
@@ -552,7 +552,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with prefixItems (tuple)", async () => { test("accepts schema with prefixItems (tuple)", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "array", type: "array",
prefixItems: [{ type: "string" }, { type: "number" }], prefixItems: [{ type: "string" }, { type: "number" }],
@@ -561,7 +561,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with multipleOf", async () => { test("accepts schema with multipleOf", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "number", type: "number",
multipleOf: 5, multipleOf: 5,
@@ -576,7 +576,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with minProperties/maxProperties", async () => { test("accepts schema with minProperties/maxProperties", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "object", type: "object",
minProperties: 1, minProperties: 1,
@@ -592,7 +592,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with default value", async () => { test("accepts schema with default value", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "string", type: "string",
default: "hello", default: "hello",
@@ -601,7 +601,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("rejects invalid P2 keyword types", async () => { test("rejects invalid P2 keyword types", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await expect( await expect(
putSchema(store, { allOf: "not-array" } as never), putSchema(store, { allOf: "not-array" } as never),
).rejects.toThrow(); ).rejects.toThrow();
@@ -614,7 +614,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("collectRefs traverses allOf sub-schemas", async () => { test("collectRefs traverses allOf sub-schemas", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const innerSchema = await putSchema(store, { type: "string" }); const innerSchema = await putSchema(store, { type: "string" });
const schema = await putSchema(store, { const schema = await putSchema(store, {
allOf: [ allOf: [
@@ -633,7 +633,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("collectRefs traverses patternProperties", async () => { test("collectRefs traverses patternProperties", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const innerSchema = await putSchema(store, { type: "string" }); const innerSchema = await putSchema(store, { type: "string" });
const schema = await putSchema(store, { const schema = await putSchema(store, {
type: "object", type: "object",
@@ -650,7 +650,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("collectRefs traverses prefixItems", async () => { test("collectRefs traverses prefixItems", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const innerSchema = await putSchema(store, { type: "string" }); const innerSchema = await putSchema(store, { type: "string" });
const schema = await putSchema(store, { const schema = await putSchema(store, {
type: "array", type: "array",
@@ -667,7 +667,7 @@ describe("bootstrap meta-schema self-reference", () => {
// ── P3 combinators, propertyNames, and metadata ────────────────────────── // ── P3 combinators, propertyNames, and metadata ──────────────────────────
test("accepts schema with not", async () => { test("accepts schema with not", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
not: { type: "string" }, not: { type: "string" },
}); });
@@ -681,7 +681,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with contains", async () => { test("accepts schema with contains", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "array", type: "array",
contains: { type: "number", minimum: 10 }, contains: { type: "number", minimum: 10 },
@@ -696,7 +696,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with propertyNames", async () => { test("accepts schema with propertyNames", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "object", type: "object",
propertyNames: { pattern: "^[a-z]+$" }, propertyNames: { pattern: "^[a-z]+$" },
@@ -711,7 +711,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("accepts schema with metadata keywords", async () => { test("accepts schema with metadata keywords", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const hash = await putSchema(store, { const hash = await putSchema(store, {
type: "string", type: "string",
examples: ["hello", "world"], examples: ["hello", "world"],
@@ -723,7 +723,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("rejects invalid P3 keyword types", async () => { test("rejects invalid P3 keyword types", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await expect( await expect(
putSchema(store, { not: "not-object" } as never), putSchema(store, { not: "not-object" } as never),
).rejects.toThrow(); ).rejects.toThrow();
@@ -737,7 +737,7 @@ describe("bootstrap meta-schema self-reference", () => {
}); });
test("collectRefs traverses contains", async () => { test("collectRefs traverses contains", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const innerSchema = await putSchema(store, { type: "string" }); const innerSchema = await putSchema(store, { type: "string" });
const schema = await putSchema(store, { const schema = await putSchema(store, {
type: "array", type: "array",
+1 -1
View File
@@ -262,7 +262,7 @@ export async function putSchema(
"Invalid schema: input does not conform to the ocas JSON Schema meta-schema", "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));
} }
/** /**
+114 -48
View File
@@ -2,88 +2,154 @@ import { describe, expect, test } from "bun:test";
import { BOOTSTRAP_STORE } from "./bootstrap-capable.js"; import { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
import { createMemoryStore } from "./store.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", () => { test("B1. listMeta and listSchemas are empty on a fresh store", () => {
const store = createMemoryStore(); const store = createMemoryStore();
expect(store.listMeta()).toEqual([]); expect(store.cas.listMeta()).toEqual([]);
expect(store.listSchemas()).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 store = createMemoryStore();
const hash = await store[BOOTSTRAP_STORE]({ type: "object" }); const hash = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string;
expect(store.cas.listMeta().map((e) => e.hash)).toContain(hash);
expect(store.listMeta().map((e) => e.hash)).toContain(hash); expect(store.cas.listSchemas().map((e) => e.hash)).toContain(hash);
expect(store.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 store = createMemoryStore();
const metaHash = await store[BOOTSTRAP_STORE]({ type: "object" }); const metaHash = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string;
const schemaHash = await store.put(metaHash, { type: "string" }); const schemaHash = store.cas.put(metaHash, { type: "string" });
expect(store.cas.listMeta().map((e) => e.hash)).not.toContain(schemaHash);
expect(store.listMeta().map((e) => e.hash)).not.toContain(schemaHash); expect(store.cas.listMeta().map((e) => e.hash)).toContain(metaHash);
expect(store.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 store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string;
const s = await store.put(m, { type: "string" }); const s = store.cas.put(m, { type: "string" });
const schemas = store.cas.listSchemas().map((e) => e.hash);
const schemas = store.listSchemas().map((e) => e.hash);
expect(schemas).toContain(m); expect(schemas).toContain(m);
expect(schemas).toContain(s); expect(schemas).toContain(s);
const meta = store.cas.listMeta().map((e) => e.hash);
const meta = store.listMeta().map((e) => e.hash);
expect(meta).toContain(m); expect(meta).toContain(m);
expect(meta).not.toContain(s); expect(meta).not.toContain(s);
}); });
test("B5. multiple meta-schemas (versioning)", async () => { test("B5. multiple meta-schemas (versioning)", () => {
const store = createMemoryStore(); const store = createMemoryStore();
const m1 = await store[BOOTSTRAP_STORE]({ type: "object", title: "v1" }); const m1 = store.cas[BOOTSTRAP_STORE]({
const m2 = await store[BOOTSTRAP_STORE]({ type: "object", title: "v2" }); type: "object",
const s1 = await store.put(m1, { type: "string" }); title: "v1",
const s2 = await store.put(m2, { type: "number" }); }) as string;
const m2 = store.cas[BOOTSTRAP_STORE]({
const meta = store.listMeta().map((e) => e.hash); 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(m1);
expect(meta).toContain(m2); expect(meta).toContain(m2);
expect(meta).toHaveLength(2); expect(meta).toHaveLength(2);
const schemas = store.cas.listSchemas().map((e) => e.hash);
const schemas = store.listSchemas().map((e) => e.hash);
expect(schemas).toContain(m1); expect(schemas).toContain(m1);
expect(schemas).toContain(m2); expect(schemas).toContain(m2);
expect(schemas).toContain(s1); expect(schemas).toContain(s1);
expect(schemas).toContain(s2); 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 store = createMemoryStore();
const payload = { type: "object", title: "dup" }; const payload = { type: "object", title: "dup" };
const h1 = await store[BOOTSTRAP_STORE](payload); const h1 = store.cas[BOOTSTRAP_STORE](payload) as string;
const h2 = await store[BOOTSTRAP_STORE](payload); const h2 = store.cas[BOOTSTRAP_STORE](payload) as string;
expect(h1).toBe(h2); expect(h1).toBe(h2);
const meta = store.cas.listMeta().map((e) => e.hash);
const meta = store.listMeta().map((e) => e.hash); expect(meta.filter((h) => h === h1)).toHaveLength(1);
const occurrences = meta.filter((h) => h === h1).length;
expect(occurrences).toBe(1);
}); });
test("B7. delete removes hash from metaSet and listSchemas", async () => { test("B7. delete removes hash from metaSet and listSchemas", () => {
const store = createMemoryStore(); const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" }); const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string;
const s = await store.put(m, { type: "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); test("B8. cas.put returns Hash synchronously (not a Promise)", () => {
expect(store.listSchemas().map((e) => e.hash)).toContain(s); 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); test("B9. has/get/delete reflect put state", () => {
const store = createMemoryStore();
expect(store.listMeta().map((e) => e.hash)).not.toContain(m); const m = store.cas[BOOTSTRAP_STORE]({ type: "object" }) as string;
// schemas typed by deleted meta no longer surface const h = store.cas.put(m, { type: "string" });
expect(store.listSchemas().map((e) => e.hash)).not.toContain(s); expect(store.cas.has(h)).toBe(true);
expect(store.listSchemas().map((e) => e.hash)).not.toContain(m); 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);
}); });
}); });
+490 -21
View File
@@ -2,11 +2,83 @@ import {
BOOTSTRAP_STORE, BOOTSTRAP_STORE,
type BootstrapCapableStore, type BootstrapCapableStore,
} from "./bootstrap-capable.js"; } 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 { 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<Hash, CasNode>(); const data = new Map<Hash, CasNode>();
const byType = new Map<Hash, Set<Hash>>(); const byType = new Map<Hash, Set<Hash>>();
const metaSet = new Set<Hash>(); const metaSet = new Set<Hash>();
@@ -20,8 +92,8 @@ export function createMemoryStore(): BootstrapCapableStore {
set.add(hash); set.add(hash);
} }
async function putSelfReferencing(payload: unknown): Promise<Hash> { function putSelfReferencing(payload: unknown): Hash {
const hash = await computeSelfHash(payload); const hash = computeSelfHashSync(payload);
if (!data.has(hash)) { if (!data.has(hash)) {
data.set(hash, { type: hash, payload, timestamp: Date.now() }); data.set(hash, { type: hash, payload, timestamp: Date.now() });
indexHash(hash, hash); indexHash(hash, hash);
@@ -39,15 +111,13 @@ export function createMemoryStore(): BootstrapCapableStore {
return result; return result;
} }
const store: BootstrapCapableStore = { const store: MemoryCasStore = {
async put(typeHash: Hash, payload: unknown): Promise<Hash> { put(typeHash: Hash, payload: unknown): Hash {
const hash = await computeHash(typeHash, payload); const hash = computeHashSync(typeHash, payload);
if (!data.has(hash)) { if (!data.has(hash)) {
data.set(hash, { type: typeHash, payload, timestamp: Date.now() }); data.set(hash, { type: typeHash, payload, timestamp: Date.now() });
indexHash(typeHash, hash); indexHash(typeHash, hash);
} }
return hash; return hash;
}, },
@@ -85,20 +155,19 @@ export function createMemoryStore(): BootstrapCapableStore {
return applyListOptions(entriesForHashes(result), options); return applyListOptions(entriesForHashes(result), options);
}, },
delete(hash: Hash): void { delete(hash: Hash): boolean {
const node = data.get(hash); const node = data.get(hash);
if (node) { if (!node) return false;
data.delete(hash); data.delete(hash);
// Remove from type index const set = byType.get(node.type);
const set = byType.get(node.type); if (set) {
if (set) { set.delete(hash);
set.delete(hash); if (set.size === 0) {
if (set.size === 0) { byType.delete(node.type);
byType.delete(node.type);
}
} }
metaSet.delete(hash);
} }
metaSet.delete(hash);
return true;
}, },
[BOOTSTRAP_STORE]: putSelfReferencing, [BOOTSTRAP_STORE]: putSelfReferencing,
@@ -106,3 +175,403 @@ export function createMemoryStore(): BootstrapCapableStore {
return store; return store;
} }
type VarRecord = {
name: string;
schema: Hash;
value: Hash;
created: number;
updated: number;
tags: Record<string, string>;
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<string, VarRecord>();
const byName = new Map<string, Set<string>>(); // 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<string, string>, 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<Hash, Map<string, Tag>>();
// key -> set of targets
const byKey = new Map<string, Set<Hash>>();
// per-target ordering (created)
const targetOrder = new Map<Hash, number>();
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 };
}
+107
View File
@@ -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);
});
});
+1 -1
View File
@@ -51,7 +51,7 @@ export type ListEntry = {
* Self-referencing nodes are created only via bootstrap(). * Self-referencing nodes are created only via bootstrap().
*/ */
export type Store = { export type Store = {
put(typeHash: Hash, payload: unknown): Promise<Hash>; put(typeHash: Hash, payload: unknown): Hash | Promise<Hash>;
get(hash: Hash): CasNode | null; get(hash: Hash): CasNode | null;
has(hash: Hash): boolean; has(hash: Hash): boolean;
listByType(typeHash: Hash, options?: ListOptions): ListEntry[]; listByType(typeHash: Hash, options?: ListOptions): ListEntry[];
+226
View File
@@ -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<typeof createMemoryStore>;
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);
});
});
@@ -16,7 +16,7 @@ let stringHash: Hash;
beforeEach(async () => { beforeEach(async () => {
dbDir = mkdtempSync(join(tmpdir(), "ocas-var-pagination-")); dbDir = mkdtempSync(join(tmpdir(), "ocas-var-pagination-"));
dbPath = join(dbDir, "vars.db"); dbPath = join(dbDir, "vars.db");
casStore = createMemoryStore(); casStore = createMemoryStore().cas;
const aliases = await bootstrap(casStore); const aliases = await bootstrap(casStore);
stringHash = aliases["@ocas/string"] as Hash; stringHash = aliases["@ocas/string"] as Hash;
varStore = createVariableStore(dbPath, casStore); varStore = createVariableStore(dbPath, casStore);
+67 -67
View File
@@ -25,7 +25,7 @@ const tmpDbPath = () =>
describe("VariableStore - Database Schema", () => { describe("VariableStore - Database Schema", () => {
test("Database schema has (name, schema) composite primary key", () => { test("Database schema has (name, schema) composite primary key", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const dbPath = tmpDbPath(); const dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store); const varStore = new VariableStore(dbPath, store);
@@ -61,7 +61,7 @@ describe("VariableStore - Database Schema", () => {
}); });
test("Database indexes reference name instead of id/scope", () => { test("Database indexes reference name instead of id/scope", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const dbPath = tmpDbPath(); const dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store); const varStore = new VariableStore(dbPath, store);
@@ -92,7 +92,7 @@ describe("VariableStore - Database Schema", () => {
}); });
test("variable_tags table has composite foreign key", () => { test("variable_tags table has composite foreign key", () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const dbPath = tmpDbPath(); const dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store); 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 () => { test("set() creates new variable when (name, schema) doesn't exist", async () => {
// Setup: store with schema and data node // Setup: store with schema and data node
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
@@ -161,7 +161,7 @@ describe("VariableStore - set() Upsert Method", () => {
}); });
test("set() updates value when (name, schema) already exists", async () => { test("set() updates value when (name, schema) already exists", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
@@ -197,7 +197,7 @@ describe("VariableStore - set() Upsert Method", () => {
}); });
test("set() creates variable with tags and labels", async () => { test("set() creates variable with tags and labels", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); 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 () => { test("set() preserves tags/labels when updating without options", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", type: "object",
@@ -247,7 +247,7 @@ describe("VariableStore - set() Upsert Method", () => {
}); });
test("set() allows same name with different schemas", async () => { test("set() allows same name with different schemas", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { const schemaA = await putSchema(store, {
type: "object", type: "object",
@@ -285,7 +285,7 @@ describe("VariableStore - set() Upsert Method", () => {
}); });
test("set() validates variable name", async () => { test("set() validates variable name", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); const dataHash = await store.put(schemaHash, {});
@@ -320,7 +320,7 @@ describe("VariableStore - set() Upsert Method", () => {
test("set() extracts schema from value hash internally", async () => { test("set() extracts schema from value hash internally", async () => {
// Given: Two different schemas // Given: Two different schemas
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { type: "number" }); const schemaA = await putSchema(store, { type: "number" });
const schemaB = await putSchema(store, { type: "string" }); const schemaB = await putSchema(store, { type: "string" });
@@ -349,7 +349,7 @@ describe("VariableStore - set() Upsert Method", () => {
test("set() upserts based on extracted schema", async () => { test("set() upserts based on extracted schema", async () => {
// Given: Existing variable with schemaA // Given: Existing variable with schemaA
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { type: "number" }); const schemaA = await putSchema(store, { type: "number" });
const value1 = await store.put(schemaA, 42); const value1 = await store.put(schemaA, 42);
@@ -371,7 +371,7 @@ describe("VariableStore - set() Upsert Method", () => {
}); });
test("set() throws CasNodeNotFoundError for invalid hash", async () => { test("set() throws CasNodeNotFoundError for invalid hash", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
dbPath = tmpDbPath(); dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store); 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 () => { test("set() throws TagLabelConflictError when updating with tag key that matches new label", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = { type: "object", properties: { x: { type: "number" } } }; const schema = { type: "object", properties: { x: { type: "number" } } };
const schemaHash = await putSchema(store, schema); 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 () => { test("set() throws TagLabelConflictError when updating with label that matches new tag key", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = { type: "object", properties: { x: { type: "number" } } }; const schema = { type: "object", properties: { x: { type: "number" } } };
const schemaHash = await putSchema(store, schema); 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 () => { test("set() allows updating tags/labels when no conflicts", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = { type: "object", properties: { x: { type: "number" } } }; const schema = { type: "object", properties: { x: { type: "number" } } };
const schemaHash = await putSchema(store, schema); 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 () => { test("get(name, schema) returns Variable when exists", async () => {
// Given: Variable with (name, schema) // Given: Variable with (name, schema)
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); const schema = await putSchema(store, { type: "number" });
const value = await store.put(schema, 42); 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 () => { test("get(name, schema) returns null when name doesn't exist", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); 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 () => { test("get(name, schema) returns null when schema doesn't match", async () => {
// Given: Variable with schemaA // Given: Variable with schemaA
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { type: "number" }); const schemaA = await putSchema(store, { type: "number" });
const schemaB = await putSchema(store, { type: "string" }); 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 () => { test("get(name, schema) returns correct variant when multiple schemas exist", async () => {
// Given: Same name with two different schemas // Given: Same name with two different schemas
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { type: "number" }); const schemaA = await putSchema(store, { type: "number" });
const schemaB = await putSchema(store, { type: "string" }); 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 () => { test("get(name, schema) includes tags and labels", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "object" }); const schema = await putSchema(store, { type: "object" });
const value = await store.put(schema, {}); const value = await store.put(schema, {});
@@ -598,7 +598,7 @@ describe("VariableStore - get() with Optional Schema", () => {
}); });
test("get(name, schema) returns exact match", async () => { test("get(name, schema) returns exact match", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { const schemaA = await putSchema(store, {
type: "object", type: "object",
@@ -635,7 +635,7 @@ describe("VariableStore - get() with Optional Schema", () => {
}); });
test("get(name, schema) returns null when combination doesn't exist", async () => { test("get(name, schema) returns null when combination doesn't exist", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { const schemaA = await putSchema(store, {
type: "object", type: "object",
@@ -674,7 +674,7 @@ describe("VariableStore - remove() with Optional Schema", () => {
}); });
test("remove(name) deletes all schema variants", async () => { test("remove(name) deletes all schema variants", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { const schemaA = await putSchema(store, {
type: "object", type: "object",
@@ -712,7 +712,7 @@ describe("VariableStore - remove() with Optional Schema", () => {
}); });
test("remove(name) returns empty array when variable doesn't exist", async () => { test("remove(name) returns empty array when variable doesn't exist", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
dbPath = tmpDbPath(); dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store); 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 () => { test("remove(name, schema) deletes only specified variant", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { const schemaA = await putSchema(store, {
type: "object", type: "object",
@@ -762,7 +762,7 @@ describe("VariableStore - remove() with Optional Schema", () => {
}); });
test("remove(name, schema) throws VariableNotFoundError when not found", async () => { test("remove(name, schema) throws VariableNotFoundError when not found", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); 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 () => { test("remove() cascades deletion to tags and labels", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); 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 () => { test("remove(name) returns array even with single variant", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); const dataHash = await store.put(schemaHash, {});
@@ -852,7 +852,7 @@ describe("VariableStore - Name Validation", () => {
}); });
test("validateName accepts valid variable names", async () => { test("validateName accepts valid variable names", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); const dataHash = await store.put(schemaHash, {});
@@ -878,7 +878,7 @@ describe("VariableStore - Name Validation", () => {
}); });
test("validateName rejects empty name", async () => { test("validateName rejects empty name", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); const dataHash = await store.put(schemaHash, {});
@@ -893,7 +893,7 @@ describe("VariableStore - Name Validation", () => {
}); });
test("validateName rejects invalid characters", async () => { test("validateName rejects invalid characters", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); const dataHash = await store.put(schemaHash, {});
@@ -933,7 +933,7 @@ describe("VariableStore - Name Validation", () => {
}); });
test("validateName rejects empty segments", async () => { test("validateName rejects empty segments", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); const dataHash = await store.put(schemaHash, {});
@@ -958,7 +958,7 @@ describe("VariableStore - Name Validation", () => {
}); });
test("validateName rejects leading or trailing slashes", async () => { test("validateName rejects leading or trailing slashes", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); 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 () => { test("validateName error message mentions 'empty' for empty string", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
schemaHash = await putSchema(store, { type: "object" }); schemaHash = await putSchema(store, { type: "object" });
dataHash = await store.put(schemaHash, {}); dataHash = await store.put(schemaHash, {});
@@ -1053,7 +1053,7 @@ describe("VariableStore - validateName() Error Messages", () => {
}); });
test("validateName error message identifies specific invalid segment", async () => { test("validateName error message identifies specific invalid segment", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
schemaHash = await putSchema(store, { type: "object" }); schemaHash = await putSchema(store, { type: "object" });
dataHash = await store.put(schemaHash, {}); dataHash = await store.put(schemaHash, {});
@@ -1073,7 +1073,7 @@ describe("VariableStore - validateName() Error Messages", () => {
}); });
test("validateName error message explains consecutive slashes", async () => { test("validateName error message explains consecutive slashes", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
schemaHash = await putSchema(store, { type: "object" }); schemaHash = await putSchema(store, { type: "object" });
dataHash = await store.put(schemaHash, {}); dataHash = await store.put(schemaHash, {});
@@ -1092,7 +1092,7 @@ describe("VariableStore - validateName() Error Messages", () => {
}); });
test("validateName error message distinguishes leading vs trailing slash", async () => { test("validateName error message distinguishes leading vs trailing slash", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
schemaHash = await putSchema(store, { type: "object" }); schemaHash = await putSchema(store, { type: "object" });
dataHash = await store.put(schemaHash, {}); dataHash = await store.put(schemaHash, {});
@@ -1126,7 +1126,7 @@ describe("VariableStore - validateName() Error Messages", () => {
}); });
test("validateName accepts valid names with dots, underscores, hyphens", async () => { test("validateName accepts valid names with dots, underscores, hyphens", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
schemaHash = await putSchema(store, { type: "object" }); schemaHash = await putSchema(store, { type: "object" });
dataHash = await store.put(schemaHash, {}); dataHash = await store.put(schemaHash, {});
@@ -1160,7 +1160,7 @@ describe("VariableStore - Integration Tests", () => {
}); });
test("Complete workflow: set, get, remove with multiple schemas", async () => { test("Complete workflow: set, get, remove with multiple schemas", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaConfig = await putSchema(store, { const schemaConfig = await putSchema(store, {
@@ -1231,7 +1231,7 @@ describe("VariableStore - Integration Tests", () => {
}); });
test("Upsert workflow preserves and updates tags", async () => { test("Upsert workflow preserves and updates tags", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { const schemaHash = await putSchema(store, {
type: "object", 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 () => { test("update() is distinct from set() and fails when variable doesn't exist", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); const dataHash = await store.put(schemaHash, {});
@@ -1305,7 +1305,7 @@ describe("VariableStore - Legacy Update Method", () => {
}); });
test("update() throws SchemaMismatchError when schema changes", async () => { test("update() throws SchemaMismatchError when schema changes", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { type: "object" }); const schemaA = await putSchema(store, { type: "object" });
const schemaB = await putSchema(store, { type: "string" }); const schemaB = await putSchema(store, { type: "string" });
@@ -1338,7 +1338,7 @@ describe("VariableStore - List Operation", () => {
}); });
test("list() returns all variables", async () => { test("list() returns all variables", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const data1 = await store.put(schemaHash, { a: 1 }); const data1 = await store.put(schemaHash, { a: 1 });
@@ -1362,7 +1362,7 @@ describe("VariableStore - List Operation", () => {
}); });
test("list() with namePrefix filters results", async () => { test("list() with namePrefix filters results", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const data = await store.put(schemaHash, {}); 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 () => { test("list({ exactName }) returns all schema variants for name", async () => {
// Given: Same name with multiple schemas // Given: Same name with multiple schemas
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { type: "number" }); const schemaA = await putSchema(store, { type: "number" });
const schemaB = await putSchema(store, { type: "string" }); 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 () => { test("list({ exactName }) returns empty array when name doesn't exist", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
dbPath = tmpDbPath(); dbPath = tmpDbPath();
@@ -1443,7 +1443,7 @@ describe("VariableStore - list() with exactName", () => {
test("list({ exactName, schema }) filters to specific variant", async () => { test("list({ exactName, schema }) filters to specific variant", async () => {
// Given: Same name with two schemas // Given: Same name with two schemas
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { type: "number" }); const schemaA = await putSchema(store, { type: "number" });
const schemaB = await putSchema(store, { type: "string" }); const schemaB = await putSchema(store, { type: "string" });
@@ -1470,7 +1470,7 @@ describe("VariableStore - list() with exactName", () => {
}); });
test("list({ exactName }) with tags filters variants", async () => { test("list({ exactName }) with tags filters variants", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { type: "number" }); const schemaA = await putSchema(store, { type: "number" });
const schemaB = await putSchema(store, { type: "string" }); const schemaB = await putSchema(store, { type: "string" });
@@ -1497,7 +1497,7 @@ describe("VariableStore - list() with exactName", () => {
}); });
test("exactName and namePrefix are mutually exclusive", async () => { test("exactName and namePrefix are mutually exclusive", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
dbPath = tmpDbPath(); dbPath = tmpDbPath();
@@ -1512,7 +1512,7 @@ describe("VariableStore - list() with exactName", () => {
}); });
test("list({ namePrefix }) does match partial exact names", async () => { test("list({ namePrefix }) does match partial exact names", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); const schema = await putSchema(store, { type: "number" });
const value = await store.put(schema, 42); const value = await store.put(schema, 42);
@@ -1542,7 +1542,7 @@ describe("VariableStore - list() with exactName", () => {
// This test demonstrates that list({ exactName }) provides // This test demonstrates that list({ exactName }) provides
// the functionality previously available via get(name) → Variable[] // the functionality previously available via get(name) → Variable[]
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaA = await putSchema(store, { type: "number" }); const schemaA = await putSchema(store, { type: "number" });
const schemaB = await putSchema(store, { type: "string" }); const schemaB = await putSchema(store, { type: "string" });
@@ -1579,7 +1579,7 @@ describe("VariableStore - Tag/Label Management", () => {
}); });
test("tag() adds tags to existing variable", async () => { test("tag() adds tags to existing variable", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); 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 () => { test("tag() throws error for conflicting tag/label names", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "object" }); const schemaHash = await putSchema(store, { type: "object" });
const dataHash = await store.put(schemaHash, {}); 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 () => { test("should accept variable name with @ prefix in first segment", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" }); const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test value"); 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 () => { test("should accept variable name starting with @", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" }); const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "config value"); const hash = await store.put(schemaHash, "config value");
@@ -1677,7 +1677,7 @@ describe("VariableStore - @ Prefix Variable Names", () => {
}); });
test("should accept complex @ prefix paths", async () => { test("should accept complex @ prefix paths", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" }); const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test"); const hash = await store.put(schemaHash, "test");
@@ -1704,7 +1704,7 @@ describe("VariableStore - @ Prefix Variable Names", () => {
}); });
test("should reject @ in non-first segment", async () => { test("should reject @ in non-first segment", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" }); const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test"); const hash = await store.put(schemaHash, "test");
@@ -1727,7 +1727,7 @@ describe("VariableStore - @ Prefix Variable Names", () => {
}); });
test("should reject @ followed by invalid characters", async () => { test("should reject @ followed by invalid characters", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" }); const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test"); 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 () => { test("should still accept all previously valid names", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" }); const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test"); const hash = await store.put(schemaHash, "test");
@@ -1777,7 +1777,7 @@ describe("VariableStore - @ Prefix Variable Names", () => {
}); });
test("should still reject previously invalid names", async () => { test("should still reject previously invalid names", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schemaHash = await putSchema(store, { type: "string" }); const schemaHash = await putSchema(store, { type: "string" });
const hash = await store.put(schemaHash, "test"); const hash = await store.put(schemaHash, "test");
@@ -1819,7 +1819,7 @@ describe("VariableStore - History (LRU)", () => {
}); });
test("history() initializes with single entry on create", async () => { test("history() initializes with single entry on create", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); const schema = await putSchema(store, { type: "number" });
const v1 = await store.put(schema, 1); const v1 = await store.put(schema, 1);
@@ -1834,7 +1834,7 @@ describe("VariableStore - History (LRU)", () => {
}); });
test("history() pushes new values to position 0", async () => { test("history() pushes new values to position 0", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); const schema = await putSchema(store, { type: "number" });
const v1 = await store.put(schema, 1); 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 () => { test("set() with same value as current is idempotent (no history change)", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); const schema = await putSchema(store, { type: "number" });
const v1 = await store.put(schema, 1); 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 () => { test("setting an existing-history value moves it to position 0 (no duplicates)", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); const schema = await putSchema(store, { type: "number" });
const v1 = await store.put(schema, 1); const v1 = await store.put(schema, 1);
@@ -1890,7 +1890,7 @@ describe("VariableStore - History (LRU)", () => {
}); });
test("history is bounded by MAX_HISTORY=10", async () => { test("history is bounded by MAX_HISTORY=10", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); const schema = await putSchema(store, { type: "number" });
const values: string[] = []; 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 () => { test("rollback semantics: re-setting an old value moves it to position 0", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); const schema = await putSchema(store, { type: "number" });
const v1 = await store.put(schema, 1); const v1 = await store.put(schema, 1);
@@ -1933,7 +1933,7 @@ describe("VariableStore - History (LRU)", () => {
}); });
test("history is cascade-deleted with the variable", async () => { test("history is cascade-deleted with the variable", async () => {
store = createMemoryStore(); store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const schema = await putSchema(store, { type: "number" }); const schema = await putSchema(store, { type: "number" });
const v1 = await store.put(schema, 1); const v1 = await store.put(schema, 1);
+7 -7
View File
@@ -5,7 +5,7 @@ import { wrapEnvelope } from "./wrap-envelope.js";
describe("wrapEnvelope", () => { describe("wrapEnvelope", () => {
test("resolves @ocas/output/put alias and returns envelope", async () => { test("resolves @ocas/output/put alias and returns envelope", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const envelope = await wrapEnvelope( const envelope = await wrapEnvelope(
@@ -19,7 +19,7 @@ describe("wrapEnvelope", () => {
}); });
test("resolves @ocas/output/has alias with boolean value", async () => { test("resolves @ocas/output/has alias with boolean value", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const envelope = await wrapEnvelope(store, "@ocas/output/has", true); 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 () => { test("resolves @ocas/output/gc alias with object value", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const gcStats = { total: 100, reachable: 80, collected: 20, scanned: 5 }; const gcStats = { total: 100, reachable: 80, collected: 20, scanned: 5 };
@@ -40,7 +40,7 @@ describe("wrapEnvelope", () => {
}); });
test("resolves primitive alias @ocas/string", async () => { test("resolves primitive alias @ocas/string", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
const aliases = await bootstrap(store); const aliases = await bootstrap(store);
const envelope = await wrapEnvelope(store, "@ocas/string", "hello"); const envelope = await wrapEnvelope(store, "@ocas/string", "hello");
@@ -50,7 +50,7 @@ describe("wrapEnvelope", () => {
}); });
test("throws for unknown alias", async () => { test("throws for unknown alias", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
await expect( await expect(
@@ -59,7 +59,7 @@ describe("wrapEnvelope", () => {
}); });
test("is idempotent — same alias returns same type hash", async () => { 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 first = await wrapEnvelope(store, "@ocas/output/verify", "ok");
const second = await wrapEnvelope( const second = await wrapEnvelope(
@@ -74,7 +74,7 @@ describe("wrapEnvelope", () => {
}); });
test("preserves complex object values without mutation", async () => { test("preserves complex object values without mutation", async () => {
const store = createMemoryStore(); const store = createMemoryStore().cas;
await bootstrap(store); await bootstrap(store);
const original = { const original = {