Compare commits

...

4 Commits

13 changed files with 765 additions and 502 deletions
@@ -1,32 +1,5 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`Phase 1: CAS Core 1.3 schema get returns schema JSON (snapshot) 1`] = `
"{
"type": "object",
"required": [
"name"
],
"properties": {
"age": {
"type": "number"
},
"name": {
"type": "string"
}
},
"additionalProperties": false
}"
`;
exports[`Phase 1: CAS Core 1.4 schema list shows registered schemas 1`] = `
"8J5E9WCD54ZJZ (unnamed)
89ZQWMV9PTC7B (unnamed)
86QB0Q5VA7797 (unnamed)
AKEX4KYV98MGT (unnamed)
AGSJVKM01WNKZ (unnamed)
7XX5H51CVD9H0 (unnamed)"
`;
exports[`Phase 1: CAS Core 1.6 get returns node JSON (snapshot) 1`] = `
{
"payload": {
@@ -43,25 +16,6 @@ exports[`Phase 1: CAS Core 1.10 refs lists direct references (snapshot) 1`] = `"
exports[`Phase 1: CAS Core 1.11 walk shows traversal tree (snapshot) 1`] = `"ERARPP19YJT05"`;
exports[`Phase 1: CAS Core 1.13 cat returns full node (snapshot) 1`] = `
{
"payload": {
"age": 30,
"name": "Alice",
},
"type": "7XX5H51CVD9H0",
}
`;
exports[`Phase 1: CAS Core 1.14 cat --payload returns only payload (snapshot) 1`] = `
"{
"age": 30,
"name": "Alice"
}"
`;
exports[`Phase 2: Schema Validation 2.2 schema validate on valid node returns valid 1`] = `"valid"`;
exports[`Phase 2: Schema Validation 2.3 put against non-existent schema hash fails 1`] = `"Schema not found: AAAAAAAAAAAAA"`;
exports[`Phase 3: Variable System 3.1 var set creates variable 1`] = `
@@ -258,26 +212,20 @@ exports[`Phase 7: Edge Cases 7.3 var set empty name errors 1`] = `"Usage: json-c
exports[`Phase 7: Edge Cases 7.4 var set name with invalid chars errors 1`] = `"Error: Invalid variable name "invalid name!": Segment "invalid name!" contains invalid characters (only @, a-z, A-Z, 0-9, ., _, - allowed)"`;
exports[`Phase 7: Edge Cases 7.5 schema put invalid schema errors 1`] = `"Invalid schema: input does not conform to the json-cas JSON Schema meta-schema"`;
exports[`Phase 7: Edge Cases 7.6 no subcommand shows help text 1`] = `
exports[`Phase 7: Edge Cases 7.5 no subcommand shows help text 1`] = `
"Usage: json-cas [--store <path>] [--json] <command> [args]
Commands:
schema put <file.json> Register schema, print type hash
schema get <type-hash> Print schema JSON
schema list List all schemas (name + hash)
schema validate <hash> Validate node against its schema
put <type-hash> <file.json> Store node, print hash
get <hash> Print node as JSON
has <hash> Print true/false
verify <hash> Verify integrity, print ok/corrupted
verify <hash> Verify integrity + schema, print ok/corrupted/invalid
refs <hash> List direct cas_ref edges
walk <hash> [--format tree] Recursive traversal
hash <type-hash> <file.json> Compute hash without storing (dry run)
render <hash> [options] Render node as YAML with resolution decay
render --pipe/-p [options] Render { type, value } from stdin
cat <hash> [--payload] Output node (--payload for payload only)
list --type <hash-or-alias> List all hashes for a given type
var set <name> <hash> [--tag <tag>...] Create/update a variable
var get <name> --schema <hash> Get a variable by name + schema
var delete <name> [--schema <hash>] Delete variable(s)
+45 -290
View File
@@ -1,13 +1,36 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { bootstrap } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import type { JSONSchema } from "@uncaged/json-cas";
import { bootstrap, putSchema } from "@uncaged/json-cas";
import { createFsStore, openStore as openFsStore } from "@uncaged/json-cas-fs";
const pkgPath = resolve(import.meta.dir, "../package.json");
const entrypoint = resolve(import.meta.dir, "index.ts");
/**
* Register a schema directly via the library (CLI schema put was removed).
* Returns the type hash.
*/
async function putSchemaFile(
storePath: string,
schemaFilePath: string,
): Promise<string> {
const store = await openFsStore(storePath);
const schema = JSON.parse(
readFileSync(schemaFilePath, "utf-8"),
) as JSONSchema;
const hash = await putSchema(store, schema);
return hash;
}
async function runCli(
args: string[],
storePath?: string,
@@ -130,86 +153,6 @@ async function runCliAlias(...args: string[]): Promise<{
};
}
describe("@ Alias Resolution - schema get", () => {
test("ucas schema get @string should work", async () => {
await runCliAlias("init"); // Initialize store
const { stdout, stderr, exitCode } = await runCliAlias(
"schema",
"get",
"@string",
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "string" });
});
test("ucas schema get @number should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@number");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "number" });
});
test("ucas schema get @object should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@object");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "object" });
});
test("ucas schema get @array should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@array");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "array" });
});
test("ucas schema get @bool should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@bool");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "boolean" });
});
test("ucas schema get @schema should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@schema");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toHaveProperty("type", "object");
expect(schema).toHaveProperty(
"description",
"json-cas JSON Schema meta-schema",
);
});
test("ucas schema get @invalid should fail gracefully", async () => {
await runCliAlias("init");
const { stderr, exitCode } = await runCliAlias("schema", "get", "@invalid");
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Schema not found");
});
});
describe("@ Alias Resolution - put", () => {
test("ucas put @string <file> should resolve alias", async () => {
await runCliAlias("init");
@@ -357,10 +300,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Create valid payload
const payloadFile = join(tmpStore, "payload.json");
@@ -403,10 +343,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with only required properties
const payloadFile = join(tmpStore, "payload.json");
@@ -448,10 +385,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with nested structure
const payloadFile = join(tmpStore, "payload.json");
@@ -513,10 +447,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with name as number
const payloadFile = join(tmpStore, "payload.json");
@@ -560,10 +491,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Empty payload
const payloadFile = join(tmpStore, "payload.json");
@@ -596,10 +524,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with extra property
const payloadFile = join(tmpStore, "payload.json");
@@ -634,10 +559,7 @@ describe("Issue #50: Schema Validation in put", () => {
items: { type: "string" },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload is an object
const payloadFile = join(tmpStore, "payload.json");
@@ -676,10 +598,7 @@ describe("Issue #50: Schema Validation in put", () => {
},
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with wrong nested type
const payloadFile = join(tmpStore, "payload.json");
@@ -760,10 +679,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Invalid payload
const payloadFile = join(tmpStore, "payload.json");
@@ -798,10 +714,7 @@ describe("Issue #50: Schema Validation in put", () => {
},
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Valid cas_ref
const validFile = join(tmpStore, "valid.json");
@@ -843,11 +756,8 @@ describe("Issue #50: Schema Validation in put", () => {
}),
);
const { exitCode } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).toBe(0);
const hash = await putSchemaFile(tmpStore, schemaFile);
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
@@ -869,10 +779,7 @@ describe("Issue #50: Schema Validation in put", () => {
required: ["name"],
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const payloadFile = join(tmpStore, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ name: 123 }));
@@ -903,10 +810,7 @@ describe("Issue #50: Schema Validation in put", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const payloadFile = join(tmpStore, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ name: 123 }));
@@ -941,10 +845,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Create node
const nodeFile = join(tmpStore, "node.json");
@@ -1005,10 +906,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
},
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Create child node
const childFile = join(tmpStore, "child.json");
@@ -1077,10 +975,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Bob" }));
@@ -1144,10 +1039,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Charlie" }));
@@ -1183,10 +1075,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Test" }));
@@ -1309,140 +1198,6 @@ describe("Suite 6: CLI Integration with Templates", () => {
});
});
// ---- schema put - invalid schema error handling ----
describe("schema put - invalid schema error handling", () => {
test("invalid schema - unknown type value shows clean error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "invalid-schema.json");
writeFileSync(schemaFile, JSON.stringify({ type: "invalid" }));
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Invalid schema");
expect(stderr).not.toContain("at ");
expect(stdout).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("invalid schema - unknown key shows clean error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "invalid-schema.json");
writeFileSync(
schemaFile,
JSON.stringify({ type: "string", unknownKey: true }),
);
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Invalid schema");
expect(stderr).not.toContain("at ");
expect(stdout).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("invalid schema - invalid nested schema shows clean error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "invalid-schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: {
name: { type: "invalid" },
},
}),
);
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Invalid schema");
expect(stderr).not.toContain("at ");
expect(stdout).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("invalid schema - non-object root shows clean error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "invalid-schema.json");
writeFileSync(schemaFile, JSON.stringify(["type", "string"]));
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Invalid schema");
expect(stderr).not.toContain("at ");
expect(stdout).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("valid schema still works (regression)", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "valid-schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
},
required: ["name"],
}),
);
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
});
// Store validation tests removed - with auto-bootstrap in Phase 1a,
// stores are automatically created and bootstrapped when opened.
// Issue #55 validation is no longer applicable.
+20 -49
View File
@@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
@@ -57,7 +57,7 @@ function stripVolatile(json: string): unknown {
// ---- Phase 1: CAS Core ----
describe("Phase 1: CAS Core", () => {
test("1.1 schema put returns type hash (auto-bootstraps store)", async () => {
test("1.1 init + put with @object bootstraps store", async () => {
const schemaFile = join(tmpStore, "test-schema.json");
writeFileSync(
schemaFile,
@@ -71,23 +71,16 @@ describe("Phase 1: CAS Core", () => {
additionalProperties: false,
}),
);
const { stdout, exitCode } = await runCli(["schema", "put", schemaFile]);
expect(exitCode).toBe(0);
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
typeHash = stdout;
});
test("1.3 schema get returns schema JSON (snapshot)", async () => {
const { stdout, exitCode } = await runCli(["schema", "get", typeHash]);
expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});
test("1.4 schema list shows registered schemas", async () => {
const { stdout, exitCode } = await runCli(["schema", "list"]);
expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
expect(stdout).toContain(typeHash);
// Use putSchema via the library to register schema, since CLI schema put is removed
const { openStore: openFsStore } = await import("@uncaged/json-cas-fs");
const { putSchema } = await import("@uncaged/json-cas");
const store = await openFsStore(tmpStore);
const hash = await putSchema(
store,
JSON.parse(readFileSync(schemaFile, "utf-8")),
);
typeHash = hash;
expect(typeHash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("1.5 put returns node hash", async () => {
@@ -142,19 +135,10 @@ describe("Phase 1: CAS Core", () => {
expect(stdout).toBe(nodeHash);
});
test("1.13 cat returns full node (snapshot)", async () => {
const { stdout, exitCode } = await runCli(["cat", nodeHash]);
test("1.13 list --type returns nodes of that type", async () => {
const { stdout, exitCode } = await runCli(["list", "--type", typeHash]);
expect(exitCode).toBe(0);
expect(stripVolatile(stdout)).toMatchSnapshot();
});
test("1.14 cat --payload returns only payload (snapshot)", async () => {
const { stdout, exitCode } = await runCli(["cat", nodeHash, "--payload"]);
expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
const parsed = JSON.parse(stdout) as Record<string, unknown>;
expect(parsed).not.toHaveProperty("type");
expect(parsed).toHaveProperty("name", "Alice");
expect(stdout).toContain(nodeHash);
});
});
@@ -176,10 +160,10 @@ describe("Phase 2: Schema Validation", () => {
// Do NOT snapshot stderr — it embeds a machine-specific tmp path
});
test("2.2 schema validate on valid node returns valid", async () => {
const { stdout, exitCode } = await runCli(["schema", "validate", nodeHash]);
test("2.2 verify on valid node returns ok (hash + schema)", async () => {
const { stdout, exitCode } = await runCli(["verify", nodeHash]);
expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
expect(stdout).toBe("ok");
});
test("2.3 put against non-existent schema hash fails", async () => {
@@ -504,20 +488,7 @@ describe("Phase 7: Edge Cases", () => {
expect(stderr).toMatchSnapshot();
});
test("7.5 schema put invalid schema errors", async () => {
const badSchemaFile = join(tmpStore, "bad-schema.json");
writeFileSync(badSchemaFile, JSON.stringify({ type: "invalid" }));
const { stdout, stderr, exitCode } = await runCli([
"schema",
"put",
badSchemaFile,
]);
expect(exitCode).not.toBe(0);
expect(stdout).toBe("");
expect(stderr).toMatchSnapshot();
});
test("7.6 no subcommand shows help text", async () => {
test("7.5 no subcommand shows help text", async () => {
const { stdout, stderr, exitCode: _exitCode } = await runCli([]);
const combined = stdout + stderr;
expect(combined.length).toBeGreaterThan(0);
@@ -525,7 +496,7 @@ describe("Phase 7: Edge Cases", () => {
expect(combined.toLowerCase()).toContain("usage");
});
test("7.7 --store path is a file errors", async () => {
test("7.6 --store path is a file errors", async () => {
const fileAsStore = join(tmpStore, "not-a-directory");
writeFileSync(fileAsStore, "test");
const proc = Bun.spawn(
+22 -99
View File
@@ -17,7 +17,6 @@ import {
refs,
renderAsync,
renderDirect,
SchemaValidationError,
TagLabelConflictError,
VariableNotFoundError,
validate,
@@ -41,6 +40,7 @@ const VALUE_FLAGS = new Set([
"decay",
"epsilon",
"inline",
"type",
]);
function parseArgs(argv: string[]): { flags: Flags; positional: string[] } {
@@ -227,61 +227,6 @@ function parseTagsLabels(args: string[]): {
// ---- Commands ----
async function cmdSchemaPut(args: string[]): Promise<void> {
const file = args[0];
if (!file) die("Usage: json-cas schema put <file.json>");
const schema = readJsonFile(file) as JSONSchema;
const store = await openStore();
try {
const hash = await putSchema(store, schema);
console.log(hash);
} catch (e) {
if (e instanceof SchemaValidationError) {
die(e.message);
}
throw e;
}
}
async function cmdSchemaGet(args: string[]): Promise<void> {
const hashOrAlias = args[0];
if (!hashOrAlias) die("Usage: json-cas schema get <type-hash>");
const hash = await resolveTypeHash(hashOrAlias);
const store = await openStore();
const schema = getSchema(store, hash);
if (schema === null) die(`Schema not found: ${hashOrAlias}`);
out(schema);
}
async function cmdSchemaList(): Promise<void> {
const store = await openStore();
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"];
if (!metaHash) throw new Error("Meta-schema not found");
for (const hash of store.listByType(metaHash)) {
if (hash === metaHash) continue;
const node = store.get(hash);
if (node !== null) {
const schema = node.payload as JSONSchema;
const name =
(schema.title as string | undefined) ??
(schema.description as string | undefined) ??
"(unnamed)";
console.log(`${hash} ${name}`);
}
}
}
async function cmdSchemaValidate(args: string[]): Promise<void> {
const hash = args[0];
if (!hash) die("Usage: json-cas schema validate <hash>");
const store = await openStore();
const node = store.get(hash);
if (node === null) die(`Node not found: ${hash}`);
const valid = validate(store, node);
console.log(valid ? "valid" : "invalid");
}
async function cmdPut(args: string[]): Promise<void> {
const typeHashOrAlias = args[0];
const file = args[1];
@@ -334,7 +279,12 @@ async function cmdVerify(args: string[]): Promise<void> {
const node = store.get(hash);
if (node === null) die(`Node not found: ${hash}`);
const ok = await verify(hash, node);
console.log(ok ? "ok" : "corrupted");
if (!ok) {
console.log("corrupted");
} else {
const valid = validate(store, node);
console.log(valid ? "ok" : "invalid");
}
}
async function cmdRefs(args: string[]): Promise<void> {
@@ -509,19 +459,6 @@ async function cmdRender(args: string[]): Promise<void> {
}
}
async function cmdCat(args: string[]): Promise<void> {
const hash = args[0];
if (!hash) die("Usage: json-cas cat <hash>");
const store = await openStore();
const node = store.get(hash);
if (node === null) die(`Node not found: ${hash}`);
if (flags.payload === true) {
out(node.payload);
} else {
out(node);
}
}
async function cmdVarSet(args: string[]): Promise<void> {
const name = args[0];
const value = args[1];
@@ -883,25 +820,32 @@ async function cmdGc(_args: string[]): Promise<void> {
}
}
async function cmdList(_args: string[]): Promise<void> {
const typeFlag = flags.type;
if (typeof typeFlag !== "string")
die("Usage: json-cas list --type <hash-or-alias>");
const typeHash = await resolveTypeHash(typeFlag);
const store = await openStore();
for (const hash of store.listByType(typeHash)) {
console.log(hash);
}
}
function printUsage(): void {
console.log(`\
Usage: json-cas [--store <path>] [--json] <command> [args]
Commands:
schema put <file.json> Register schema, print type hash
schema get <type-hash> Print schema JSON
schema list List all schemas (name + hash)
schema validate <hash> Validate node against its schema
put <type-hash> <file.json> Store node, print hash
get <hash> Print node as JSON
has <hash> Print true/false
verify <hash> Verify integrity, print ok/corrupted
verify <hash> Verify integrity + schema, print ok/corrupted/invalid
refs <hash> List direct cas_ref edges
walk <hash> [--format tree] Recursive traversal
hash <type-hash> <file.json> Compute hash without storing (dry run)
render <hash> [options] Render node as YAML with resolution decay
render --pipe/-p [options] Render { type, value } from stdin
cat <hash> [--payload] Output node (--payload for payload only)
list --type <hash-or-alias> List all hashes for a given type
var set <name> <hash> [--tag <tag>...] Create/update a variable
var get <name> --schema <hash> Get a variable by name + schema
var delete <name> [--schema <hash>] Delete variable(s)
@@ -936,27 +880,6 @@ if (!cmd) {
}
switch (cmd) {
case "schema": {
const [sub, ...subRest] = rest;
switch (sub) {
case "put":
await cmdSchemaPut(subRest);
break;
case "get":
await cmdSchemaGet(subRest);
break;
case "list":
await cmdSchemaList();
break;
case "validate":
await cmdSchemaValidate(subRest);
break;
default:
die(`Unknown schema subcommand: ${sub ?? "(none)"}`);
}
break;
}
case "put":
await cmdPut(rest);
break;
@@ -989,8 +912,8 @@ switch (cmd) {
await cmdRender(rest);
break;
case "cat":
await cmdCat(rest);
case "list":
await cmdList(rest);
break;
case "var": {
+1 -1
View File
@@ -65,7 +65,7 @@ describe("createFsStore – init and bootstrap", () => {
const h2 = await bootstrap(store);
expect(h1).toEqual(h2);
expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(6);
expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(24);
});
});
+191 -2
View File
@@ -1,18 +1,40 @@
import { describe, expect, test } from "bun:test";
import { bootstrap } from "./bootstrap.js";
import type { JSONSchema } from "./schema.js";
import { getSchema } from "./schema.js";
import { createMemoryStore } from "./store.js";
const OUTPUT_ALIASES = [
"@output/put",
"@output/get",
"@output/has",
"@output/hash",
"@output/verify",
"@output/refs",
"@output/walk",
"@output/list",
"@output/var-set",
"@output/var-get",
"@output/var-delete",
"@output/var-tag",
"@output/var-list",
"@output/template-set",
"@output/template-get",
"@output/template-list",
"@output/template-delete",
"@output/gc",
] as const;
// ──────────────────────────────────────────────────────────────────────────────
// Built-in Schema Registration Tests
// ──────────────────────────────────────────────────────────────────────────────
describe("bootstrap - Built-in Schemas", () => {
test("should return map of built-in schema aliases to hashes", async () => {
test("should return map of 24 built-in schema aliases to hashes", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
// Should return object with 6 aliases
// Should return object with 6 primitive + 18 output aliases = 24
expect(builtinSchemas).toHaveProperty("@schema");
expect(builtinSchemas).toHaveProperty("@string");
expect(builtinSchemas).toHaveProperty("@number");
@@ -20,6 +42,12 @@ describe("bootstrap - Built-in Schemas", () => {
expect(builtinSchemas).toHaveProperty("@array");
expect(builtinSchemas).toHaveProperty("@bool");
for (const alias of OUTPUT_ALIASES) {
expect(builtinSchemas).toHaveProperty(alias);
}
expect(Object.keys(builtinSchemas)).toHaveLength(24);
// All values should be valid hashes
for (const [_alias, hash] of Object.entries(builtinSchemas)) {
expect(typeof hash).toBe("string");
@@ -127,3 +155,164 @@ describe("bootstrap - Built-in Schemas", () => {
}
});
});
// ──────────────────────────────────────────────────────────────────────────────
// @output/* Schema Registration Tests
// ──────────────────────────────────────────────────────────────────────────────
describe("bootstrap - @output/* Schemas", () => {
test("each @output/* schema has a title", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
for (const alias of OUTPUT_ALIASES) {
const hash = aliases[alias];
if (!hash) throw new Error(`${alias} not found`);
const schema = getSchema(store, hash) as JSONSchema;
expect(schema).not.toBeNull();
expect(typeof schema.title).toBe("string");
expect((schema.title as string).startsWith("ucas ")).toBe(true);
}
});
test("@output/put schema describes a cas_ref string", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const hash = aliases["@output/put"];
if (!hash) throw new Error("@output/put not found");
const schema = getSchema(store, hash);
expect(schema).toEqual({
type: "string",
format: "cas_ref",
title: "ucas put result",
});
});
test("@output/get schema describes object with type, payload, timestamp", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const hash = aliases["@output/get"];
if (!hash) throw new Error("@output/get not found");
const schema = getSchema(store, hash) as JSONSchema;
expect(schema.type).toBe("object");
expect(schema.title).toBe("ucas get result");
const props = schema.properties as Record<string, JSONSchema>;
expect(props.type).toEqual({ type: "string", format: "cas_ref" });
expect(props.payload).toEqual({});
expect(props.timestamp).toEqual({ type: "number" });
});
test("@output/has schema describes a boolean", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const hash = aliases["@output/has"];
if (!hash) throw new Error("@output/has not found");
expect(getSchema(store, hash)).toEqual({
type: "boolean",
title: "ucas has result",
});
});
test("@output/verify schema describes enum of ok|corrupted|invalid", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const hash = aliases["@output/verify"];
if (!hash) throw new Error("@output/verify not found");
const schema = getSchema(store, hash);
expect(schema).toEqual({
type: "string",
enum: ["ok", "corrupted", "invalid"],
title: "ucas verify result",
});
});
test("@output/refs schema describes array of cas_ref strings", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const hash = aliases["@output/refs"];
if (!hash) throw new Error("@output/refs not found");
expect(getSchema(store, hash)).toEqual({
type: "array",
items: { type: "string", format: "cas_ref" },
title: "ucas refs result",
});
});
test("@output/gc schema describes object with gc stats fields", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const hash = aliases["@output/gc"];
if (!hash) throw new Error("@output/gc not found");
const schema = getSchema(store, hash) as JSONSchema;
expect(schema.type).toBe("object");
expect(schema.title).toBe("ucas gc result");
const props = schema.properties as Record<string, JSONSchema>;
expect(props.total).toEqual({ type: "number" });
expect(props.reachable).toEqual({ type: "number" });
expect(props.collected).toEqual({ type: "number" });
expect(props.scanned).toEqual({ type: "number" });
});
test("@output/var-set schema describes a Variable object", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const hash = aliases["@output/var-set"];
if (!hash) throw new Error("@output/var-set not found");
const schema = getSchema(store, hash) as JSONSchema;
expect(schema.type).toBe("object");
expect(schema.title).toBe("ucas var set result");
const props = schema.properties as Record<string, JSONSchema>;
expect(props.name).toEqual({ type: "string" });
expect(props.schema).toEqual({ type: "string", format: "cas_ref" });
expect(props.value).toEqual({ type: "string", format: "cas_ref" });
});
test("@output/var-list schema describes array of Variable objects", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const hash = aliases["@output/var-list"];
if (!hash) throw new Error("@output/var-list not found");
const schema = getSchema(store, hash) as JSONSchema;
expect(schema.type).toBe("array");
expect(schema.title).toBe("ucas var list result");
const items = schema.items as JSONSchema;
expect(items.type).toBe("object");
const props = items.properties as Record<string, JSONSchema>;
expect(props.name).toEqual({ type: "string" });
});
test("@output/template-delete schema describes object with deleted boolean", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const hash = aliases["@output/template-delete"];
if (!hash) throw new Error("@output/template-delete not found");
expect(getSchema(store, hash)).toEqual({
type: "object",
properties: { deleted: { type: "boolean" } },
title: "ucas template delete result",
});
});
test("all @output/* schemas are distinct hashes", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const outputHashes = OUTPUT_ALIASES.map((alias) => aliases[alias]);
const uniqueHashes = new Set(outputHashes);
expect(uniqueHashes.size).toBe(OUTPUT_ALIASES.length);
});
});
+168 -3
View File
@@ -63,9 +63,168 @@ const BOOTSTRAP_PAYLOAD = {
},
} as const;
const VARIABLE_PROPERTIES = {
name: { type: "string" },
schema: { type: "string", format: "cas_ref" },
value: { type: "string", format: "cas_ref" },
created: { type: "number" },
updated: { type: "number" },
tags: { type: "object" },
labels: { type: "array", items: { type: "string" } },
} as const;
const OUTPUT_SCHEMAS: ReadonlyArray<
readonly [alias: string, schema: Record<string, unknown>]
> = [
[
"@output/put",
{ type: "string", format: "cas_ref", title: "ucas put result" },
],
[
"@output/get",
{
type: "object",
properties: {
type: { type: "string", format: "cas_ref" },
payload: {},
timestamp: { type: "number" },
},
title: "ucas get result",
},
],
["@output/has", { type: "boolean", title: "ucas has result" }],
[
"@output/hash",
{ type: "string", format: "cas_ref", title: "ucas hash result" },
],
[
"@output/verify",
{
type: "string",
enum: ["ok", "corrupted", "invalid"],
title: "ucas verify result",
},
],
[
"@output/refs",
{
type: "array",
items: { type: "string", format: "cas_ref" },
title: "ucas refs result",
},
],
[
"@output/walk",
{
type: "array",
items: { type: "string" },
title: "ucas walk result",
},
],
[
"@output/list",
{
type: "array",
items: { type: "string", format: "cas_ref" },
title: "ucas list result",
},
],
[
"@output/var-set",
{
type: "object",
properties: { ...VARIABLE_PROPERTIES },
title: "ucas var set result",
},
],
[
"@output/var-get",
{
type: "object",
properties: { ...VARIABLE_PROPERTIES },
title: "ucas var get result",
},
],
[
"@output/var-delete",
{
type: "object",
properties: { ...VARIABLE_PROPERTIES },
title: "ucas var delete result",
},
],
[
"@output/var-tag",
{
type: "object",
properties: { ...VARIABLE_PROPERTIES },
title: "ucas var tag result",
},
],
[
"@output/var-list",
{
type: "array",
items: { type: "object", properties: { ...VARIABLE_PROPERTIES } },
title: "ucas var list result",
},
],
[
"@output/template-set",
{
type: "object",
properties: {
schemaHash: { type: "string", format: "cas_ref" },
contentHash: { type: "string", format: "cas_ref" },
},
title: "ucas template set result",
},
],
[
"@output/template-get",
{ type: "string", title: "ucas template get result" },
],
[
"@output/template-list",
{
type: "array",
items: {
type: "object",
properties: {
schemaHash: { type: "string", format: "cas_ref" },
contentHash: { type: "string", format: "cas_ref" },
},
},
title: "ucas template list result",
},
],
[
"@output/template-delete",
{
type: "object",
properties: { deleted: { type: "boolean" } },
title: "ucas template delete result",
},
],
[
"@output/gc",
{
type: "object",
properties: {
total: { type: "number" },
reachable: { type: "number" },
collected: { type: "number" },
scanned: { type: "number" },
},
title: "ucas gc result",
},
],
];
/**
* Write the meta-schema seed node into the store and register built-in schemas.
* The returned object contains aliases for the meta-schema and 5 primitive schemas.
* The returned object contains aliases for the meta-schema, 5 primitive schemas,
* and 18 @output/* schemas (24 total).
* Idempotent: calling bootstrap multiple times returns the same hashes.
*/
export async function bootstrap(store: Store): Promise<Record<string, Hash>> {
@@ -83,8 +242,8 @@ export async function bootstrap(store: Store): Promise<Record<string, Hash>> {
const arrayHash = await store.put(metaHash, { type: "array" });
const boolHash = await store.put(metaHash, { type: "boolean" });
// 3. Return map of aliases to hashes
return {
// 3. Register @output/* schemas
const aliases: Record<string, Hash> = {
"@schema": metaHash,
"@string": stringHash,
"@number": numberHash,
@@ -92,4 +251,10 @@ export async function bootstrap(store: Store): Promise<Record<string, Hash>> {
"@array": arrayHash,
"@bool": boolHash,
};
for (const [alias, schema] of OUTPUT_SCHEMAS) {
aliases[alias] = await store.put(metaHash, schema);
}
return aliases;
}
+5 -3
View File
@@ -264,7 +264,7 @@ describe("bootstrap", () => {
);
});
test("returns a map with 6 built-in schema aliases", async () => {
test("returns a map with 24 built-in schema aliases", async () => {
const store = createMemoryStore();
const builtinSchemas = await bootstrap(store);
@@ -280,6 +280,8 @@ describe("bootstrap", () => {
expect(hash).toHaveLength(13);
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
}
expect(Object.keys(builtinSchemas)).toHaveLength(24);
});
test("meta-schema node is stored and retrievable", async () => {
@@ -316,7 +318,7 @@ describe("bootstrap", () => {
const h2 = await bootstrap(store);
expect(h1).toEqual(h2);
// All 6 built-in schemas should be typed by the meta-schema
expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(6);
// All 24 built-in schemas should be typed by the meta-schema
expect(store.listByType(h1["@schema"] ?? "")).toHaveLength(24);
});
});
+2
View File
@@ -5,6 +5,7 @@ export { cborEncode } from "./cbor.js";
export { type GcStats, gc } from "./gc.js";
export { computeHash, computeSelfHash } from "./hash.js";
export { renderWithTemplate } from "./liquid-render.js";
export { registerOutputTemplates } from "./output-templates.js";
export {
type RenderOptions,
render,
@@ -34,3 +35,4 @@ export {
VariableStore,
} from "./variable-store.js";
export { verify } from "./verify.js";
export { wrapEnvelope } from "./wrap-envelope.js";
@@ -0,0 +1,117 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootstrap } from "./bootstrap.js";
import { registerOutputTemplates } from "./output-templates.js";
import { createMemoryStore } from "./store.js";
import type { Store } from "./types.js";
import type { VariableStore } from "./variable-store.js";
import { createVariableStore } from "./variable-store.js";
const OUTPUT_ALIASES = [
"@output/put",
"@output/get",
"@output/has",
"@output/hash",
"@output/verify",
"@output/refs",
"@output/walk",
"@output/list",
"@output/var-set",
"@output/var-get",
"@output/var-delete",
"@output/var-tag",
"@output/var-list",
"@output/template-set",
"@output/template-get",
"@output/template-list",
"@output/template-delete",
"@output/gc",
] as const;
describe("registerOutputTemplates", () => {
let store: Store;
let varStore: VariableStore;
let tempDir: string;
afterEach(async () => {
varStore.close();
await rm(tempDir, { recursive: true });
});
test("registers a template for every @output/* schema", async () => {
tempDir = await mkdtemp(join(tmpdir(), "json-cas-tmpl-"));
store = createMemoryStore();
await bootstrap(store);
varStore = createVariableStore(join(tempDir, "vars.db"), store);
const registered = await registerOutputTemplates(store, varStore);
expect(Object.keys(registered)).toHaveLength(18);
for (const alias of OUTPUT_ALIASES) {
expect(registered).toHaveProperty(alias);
}
});
test("each template is retrievable via @ucas/template/text/<hash>", async () => {
tempDir = await mkdtemp(join(tmpdir(), "json-cas-tmpl-"));
store = createMemoryStore();
const aliases = await bootstrap(store);
varStore = createVariableStore(join(tempDir, "vars.db"), store);
await registerOutputTemplates(store, varStore);
const stringHash = aliases["@string"];
if (!stringHash) throw new Error("@string not found");
for (const alias of OUTPUT_ALIASES) {
const schemaHash = aliases[alias];
if (!schemaHash) throw new Error(`${alias} not found`);
const varName = `@ucas/template/text/${schemaHash}`;
const variable = varStore.get(varName, stringHash);
if (variable === null) throw new Error(`Variable ${varName} not found`);
const templateNode = store.get(variable.value);
if (templateNode === null)
throw new Error(`Template node ${variable.value} not found`);
expect(typeof templateNode.payload).toBe("string");
}
});
test("is idempotent — safe to call multiple times", async () => {
tempDir = await mkdtemp(join(tmpdir(), "json-cas-tmpl-"));
store = createMemoryStore();
await bootstrap(store);
varStore = createVariableStore(join(tempDir, "vars.db"), store);
const first = await registerOutputTemplates(store, varStore);
const second = await registerOutputTemplates(store, varStore);
expect(first).toEqual(second);
});
test("@output/put template contains payload reference", async () => {
tempDir = await mkdtemp(join(tmpdir(), "json-cas-tmpl-"));
store = createMemoryStore();
const aliases = await bootstrap(store);
varStore = createVariableStore(join(tempDir, "vars.db"), store);
await registerOutputTemplates(store, varStore);
const putHash = aliases["@output/put"];
if (!putHash) throw new Error("@output/put not found");
const stringHash = aliases["@string"];
if (!stringHash) throw new Error("@string not found");
const variable = varStore.get(`@ucas/template/text/${putHash}`, stringHash);
if (variable === null)
throw new Error("@output/put template variable not found");
const templateNode = store.get(variable.value);
if (templateNode === null) throw new Error("Template node not found");
expect(templateNode.payload).toBe("{{ payload }}");
});
});
+87
View File
@@ -0,0 +1,87 @@
import { bootstrap } from "./bootstrap.js";
import type { Hash, Store } from "./types.js";
import type { VariableStore } from "./variable-store.js";
const DEFAULT_TEMPLATES: ReadonlyArray<
readonly [alias: string, template: string]
> = [
["@output/put", "{{ payload }}"],
[
"@output/get",
"type: {{ payload.type }}\ntimestamp: {{ payload.timestamp }}",
],
["@output/has", "{{ payload }}"],
["@output/hash", "{{ payload }}"],
["@output/verify", "{{ payload }}"],
["@output/refs", "{% for ref in payload %}{{ ref }}\n{% endfor %}"],
["@output/walk", "{% for item in payload %}{{ item }}\n{% endfor %}"],
["@output/list", "{% for item in payload %}{{ item }}\n{% endfor %}"],
[
"@output/var-set",
"name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}",
],
[
"@output/var-get",
"name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}",
],
[
"@output/var-delete",
"name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}",
],
[
"@output/var-tag",
"name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}",
],
[
"@output/var-list",
"{% for v in payload %}name: {{ v.name }}\nschema: {{ v.schema }}\nvalue: {{ v.value }}\n{% endfor %}",
],
[
"@output/template-set",
"schemaHash: {{ payload.schemaHash }}\ncontentHash: {{ payload.contentHash }}",
],
["@output/template-get", "{{ payload }}"],
[
"@output/template-list",
"{% for t in payload %}schemaHash: {{ t.schemaHash }}\ncontentHash: {{ t.contentHash }}\n{% endfor %}",
],
["@output/template-delete", "deleted: {{ payload.deleted }}"],
[
"@output/gc",
"total: {{ payload.total }}\nreachable: {{ payload.reachable }}\ncollected: {{ payload.collected }}\nscanned: {{ payload.scanned }}",
],
];
/**
* Register default LiquidJS templates for all @output/* schemas.
* Each template is stored as a @string CAS node and bound to
* the variable `@ucas/template/text/<schema-hash>`.
*
* Idempotent: safe to call multiple times.
*/
export async function registerOutputTemplates(
store: Store,
varStore: VariableStore,
): Promise<Record<string, Hash>> {
const aliases = await bootstrap(store);
const stringHash = aliases["@string"];
if (stringHash === undefined) {
throw new Error("@string schema not found in bootstrap result");
}
const registered: Record<string, Hash> = {};
for (const [alias, template] of DEFAULT_TEMPLATES) {
const schemaHash = aliases[alias];
if (schemaHash === undefined) {
throw new Error(`Schema alias not found: ${alias}`);
}
const contentHash = await store.put(stringHash, template);
const varName = `@ucas/template/text/${schemaHash}`;
varStore.set(varName, contentHash);
registered[alias] = contentHash;
}
return registered;
}
@@ -0,0 +1,85 @@
import { describe, expect, test } from "bun:test";
import { bootstrap } from "./bootstrap.js";
import { createMemoryStore } from "./store.js";
import { wrapEnvelope } from "./wrap-envelope.js";
describe("wrapEnvelope", () => {
test("resolves @output/put alias and returns envelope", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const envelope = await wrapEnvelope(store, "@output/put", "AAAAAAAAAAAAA");
expect(envelope.type).toBe(aliases["@output/put"]);
expect(envelope.value).toBe("AAAAAAAAAAAAA");
});
test("resolves @output/has alias with boolean value", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const envelope = await wrapEnvelope(store, "@output/has", true);
expect(envelope.type).toBe(aliases["@output/has"]);
expect(envelope.value).toBe(true);
});
test("resolves @output/gc alias with object value", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const gcStats = { total: 100, reachable: 80, collected: 20, scanned: 5 };
const envelope = await wrapEnvelope(store, "@output/gc", gcStats);
expect(envelope.type).toBe(aliases["@output/gc"]);
expect(envelope.value).toEqual(gcStats);
});
test("resolves primitive alias @string", async () => {
const store = createMemoryStore();
const aliases = await bootstrap(store);
const envelope = await wrapEnvelope(store, "@string", "hello");
expect(envelope.type).toBe(aliases["@string"]);
expect(envelope.value).toBe("hello");
});
test("throws for unknown alias", async () => {
const store = createMemoryStore();
await bootstrap(store);
await expect(
wrapEnvelope(store, "@output/nonexistent", "value"),
).rejects.toThrow("Unknown schema alias: @output/nonexistent");
});
test("is idempotent — same alias returns same type hash", async () => {
const store = createMemoryStore();
const first = await wrapEnvelope(store, "@output/verify", "ok");
const second = await wrapEnvelope(store, "@output/verify", "corrupted");
expect(first.type).toBe(second.type);
expect(first.value).toBe("ok");
expect(second.value).toBe("corrupted");
});
test("preserves complex object values without mutation", async () => {
const store = createMemoryStore();
await bootstrap(store);
const original = {
name: "test",
schema: "AAAAAAAAAAAAA",
value: "BBBBBBBBBBBBB",
created: 1000,
updated: 2000,
tags: { env: "prod" },
labels: ["stable"],
};
const envelope = await wrapEnvelope(store, "@output/var-set", original);
expect(envelope.value).toEqual(original);
});
});
+19
View File
@@ -0,0 +1,19 @@
import { bootstrap } from "./bootstrap.js";
import type { Hash, Store } from "./types.js";
/**
* Resolve a schema alias (e.g. "@output/put") to its hash via bootstrap,
* then return a typed envelope ready for store.put() or direct rendering.
*/
export async function wrapEnvelope(
store: Store,
schemaAlias: string,
value: unknown,
): Promise<{ type: Hash; value: unknown }> {
const aliases = await bootstrap(store);
const typeHash = aliases[schemaAlias];
if (typeHash === undefined) {
throw new Error(`Unknown schema alias: ${schemaAlias}`);
}
return { type: typeHash, value };
}