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:
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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" } } };
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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([]);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 };
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user