feat: wrap put/get/has/hash/verify/list with {type,value} envelope (Phase 2)
Fixes #70
This commit is contained in:
@@ -2,15 +2,23 @@
|
|||||||
|
|
||||||
exports[`Phase 1: CAS Core 1.6 get returns node JSON (snapshot) 1`] = `
|
exports[`Phase 1: CAS Core 1.6 get returns node JSON (snapshot) 1`] = `
|
||||||
{
|
{
|
||||||
"payload": {
|
"type": "ASE7K6A0HG8W9",
|
||||||
"age": 30,
|
"value": {
|
||||||
"name": "Alice",
|
"payload": {
|
||||||
|
"age": 30,
|
||||||
|
"name": "Alice",
|
||||||
|
},
|
||||||
|
"type": "7XX5H51CVD9H0",
|
||||||
},
|
},
|
||||||
"type": "7XX5H51CVD9H0",
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Phase 1: CAS Core 1.9 verify returns ok for valid node 1`] = `"ok"`;
|
exports[`Phase 1: CAS Core 1.9 verify returns ok for valid node 1`] = `
|
||||||
|
{
|
||||||
|
"type": "8E2M8H30BHXS8",
|
||||||
|
"value": "ok",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Phase 1: CAS Core 1.10 refs lists direct references (snapshot) 1`] = `""`;
|
exports[`Phase 1: CAS Core 1.10 refs lists direct references (snapshot) 1`] = `""`;
|
||||||
|
|
||||||
@@ -216,16 +224,16 @@ exports[`Phase 7: Edge Cases 7.5 no subcommand shows help text 1`] = `
|
|||||||
"Usage: json-cas [--store <path>] [--json] <command> [args]
|
"Usage: json-cas [--store <path>] [--json] <command> [args]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
put <type-hash> <file.json> Store node, print hash
|
put <type-hash> <file.json> Store node, print { type, value } envelope (value=hash)
|
||||||
get <hash> Print node as JSON
|
get <hash> Print node as { type, value } envelope
|
||||||
has <hash> Print true/false
|
has <hash> Print { type, value } envelope (value=boolean)
|
||||||
verify <hash> Verify integrity + schema, print ok/corrupted/invalid
|
verify <hash> Verify integrity + schema → { type, value } (value=ok/corrupted/invalid)
|
||||||
refs <hash> List direct cas_ref edges
|
refs <hash> List direct cas_ref edges
|
||||||
walk <hash> [--format tree] Recursive traversal
|
walk <hash> [--format tree] Recursive traversal
|
||||||
hash <type-hash> <file.json> Compute hash without storing (dry run)
|
hash <type-hash> <file.json> Compute hash without storing → { type, value } envelope
|
||||||
render <hash> [options] Render node as YAML with resolution decay
|
render <hash> [options] Render node as YAML with resolution decay
|
||||||
render --pipe/-p [options] Render { type, value } from stdin
|
render --pipe/-p [options] Render { type, value } from stdin
|
||||||
list --type <hash-or-alias> List all hashes for a given type
|
list --type <hash-or-alias> List hashes for a type → { type, value } envelope (value=string[])
|
||||||
var set <name> <hash> [--tag <tag>...] Create/update a variable
|
var set <name> <hash> [--tag <tag>...] Create/update a variable
|
||||||
var get <name> --schema <hash> Get a variable by name + schema
|
var get <name> --schema <hash> Get a variable by name + schema
|
||||||
var delete <name> [--schema <hash>] Delete variable(s)
|
var delete <name> [--schema <hash>] Delete variable(s)
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import { createFsStore, openStore as openFsStore } from "@uncaged/json-cas-fs";
|
|||||||
const pkgPath = resolve(import.meta.dir, "../package.json");
|
const pkgPath = resolve(import.meta.dir, "../package.json");
|
||||||
const entrypoint = resolve(import.meta.dir, "index.ts");
|
const entrypoint = resolve(import.meta.dir, "index.ts");
|
||||||
|
|
||||||
|
/** Extract the `value` field from a { type, value } envelope JSON string. */
|
||||||
|
function envValue(json: string): unknown {
|
||||||
|
return (JSON.parse(json.trim()) as { value: unknown }).value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a schema directly via the library (CLI schema put was removed).
|
* Register a schema directly via the library (CLI schema put was removed).
|
||||||
* Returns the type hash.
|
* Returns the type hash.
|
||||||
@@ -168,8 +173,8 @@ describe("@ Alias Resolution - put", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stderr).toBe("");
|
expect(stderr).toBe("");
|
||||||
// Should output a valid hash (13 chars)
|
// Should output an envelope whose value is a valid hash (13 chars)
|
||||||
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ucas put @number <file> should resolve alias", async () => {
|
test("ucas put @number <file> should resolve alias", async () => {
|
||||||
@@ -185,7 +190,7 @@ describe("@ Alias Resolution - put", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ucas put @object <file> should resolve alias", async () => {
|
test("ucas put @object <file> should resolve alias", async () => {
|
||||||
@@ -201,7 +206,7 @@ describe("@ Alias Resolution - put", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ucas put @invalid <file> should fail", async () => {
|
test("ucas put @invalid <file> should fail", async () => {
|
||||||
@@ -236,7 +241,7 @@ describe("@ Alias Resolution - hash", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stderr).toBe("");
|
expect(stderr).toBe("");
|
||||||
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -313,10 +318,10 @@ describe("Issue #50: Schema Validation in put", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stderr).toBe("");
|
expect(stderr).toBe("");
|
||||||
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
|
|
||||||
// Verify node was stored
|
// Verify node was stored
|
||||||
const hash = stdout.trim();
|
const hash = envValue(stdout) as string;
|
||||||
const { exitCode: hasExitCode } = await runCli(["has", hash], tmpStore);
|
const { exitCode: hasExitCode } = await runCli(["has", hash], tmpStore);
|
||||||
expect(hasExitCode).toBe(0);
|
expect(hasExitCode).toBe(0);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -355,7 +360,7 @@ describe("Issue #50: Schema Validation in put", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(tmpStore, { recursive: true, force: true });
|
rmSync(tmpStore, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -403,7 +408,7 @@ describe("Issue #50: Schema Validation in put", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(tmpStore, { recursive: true, force: true });
|
rmSync(tmpStore, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -423,7 +428,7 @@ describe("Issue #50: Schema Validation in put", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(tmpStore, { recursive: true, force: true });
|
rmSync(tmpStore, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -469,7 +474,7 @@ describe("Issue #50: Schema Validation in put", () => {
|
|||||||
["has", "0000000000000"],
|
["has", "0000000000000"],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
expect(hasOutput.trim()).toBe("false");
|
expect(envValue(hasOutput)).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(tmpStore, { recursive: true, force: true });
|
rmSync(tmpStore, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -692,7 +697,7 @@ describe("Issue #50: Schema Validation in put", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(tmpStore, { recursive: true, force: true });
|
rmSync(tmpStore, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -850,33 +855,30 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
// Create node
|
// Create node
|
||||||
const nodeFile = join(tmpStore, "node.json");
|
const nodeFile = join(tmpStore, "node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice" }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice" }));
|
||||||
const { stdout: nodeHash } = await runCli(
|
const { stdout: nodeOut } = await runCli(
|
||||||
["put", schemaHash.trim(), nodeFile],
|
["put", schemaHash.trim(), nodeFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const nodeHash = envValue(nodeOut) as string;
|
||||||
|
|
||||||
// Create template file (JSON-encoded string)
|
// Create template file (JSON-encoded string)
|
||||||
const templateFile = join(tmpStore, "template.json");
|
const templateFile = join(tmpStore, "template.json");
|
||||||
writeFileSync(templateFile, JSON.stringify("Hello {{ payload.name }}!"));
|
writeFileSync(templateFile, JSON.stringify("Hello {{ payload.name }}!"));
|
||||||
const { stdout: tmplHash } = await runCli(
|
const { stdout: tmplOut } = await runCli(
|
||||||
["put", "@string", templateFile],
|
["put", "@string", templateFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const tmplHash = envValue(tmplOut) as string;
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
await runCli(
|
await runCli(
|
||||||
[
|
["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash],
|
||||||
"var",
|
|
||||||
"set",
|
|
||||||
`@ucas/template/text/${schemaHash.trim()}`,
|
|
||||||
tmplHash.trim(),
|
|
||||||
],
|
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render with template
|
// Render with template
|
||||||
const { stdout: output, exitCode } = await runCli(
|
const { stdout: output, exitCode } = await runCli(
|
||||||
["render", nodeHash.trim()],
|
["render", nodeHash],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -911,21 +913,23 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
// Create child node
|
// Create child node
|
||||||
const childFile = join(tmpStore, "child.json");
|
const childFile = join(tmpStore, "child.json");
|
||||||
writeFileSync(childFile, JSON.stringify({ value: "child", child: null }));
|
writeFileSync(childFile, JSON.stringify({ value: "child", child: null }));
|
||||||
const { stdout: childHash } = await runCli(
|
const { stdout: childOut } = await runCli(
|
||||||
["put", schemaHash.trim(), childFile],
|
["put", schemaHash.trim(), childFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const childHash = envValue(childOut) as string;
|
||||||
|
|
||||||
// Create parent node
|
// Create parent node
|
||||||
const parentFile = join(tmpStore, "parent.json");
|
const parentFile = join(tmpStore, "parent.json");
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
parentFile,
|
parentFile,
|
||||||
JSON.stringify({ value: "parent", child: childHash.trim() }),
|
JSON.stringify({ value: "parent", child: childHash }),
|
||||||
);
|
);
|
||||||
const { stdout: parentHash } = await runCli(
|
const { stdout: parentOut } = await runCli(
|
||||||
["put", schemaHash.trim(), parentFile],
|
["put", schemaHash.trim(), parentFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const parentHash = envValue(parentOut) as string;
|
||||||
|
|
||||||
// Create template showing resolution (JSON-encoded string)
|
// Create template showing resolution (JSON-encoded string)
|
||||||
const templateFile = join(tmpStore, "template.json");
|
const templateFile = join(tmpStore, "template.json");
|
||||||
@@ -933,25 +937,21 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
templateFile,
|
templateFile,
|
||||||
JSON.stringify("{{ payload.value }}(res={{ resolution }})"),
|
JSON.stringify("{{ payload.value }}(res={{ resolution }})"),
|
||||||
);
|
);
|
||||||
const { stdout: tmplHash } = await runCli(
|
const { stdout: tmplOut } = await runCli(
|
||||||
["put", "@string", templateFile],
|
["put", "@string", templateFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const tmplHash = envValue(tmplOut) as string;
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
await runCli(
|
await runCli(
|
||||||
[
|
["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash],
|
||||||
"var",
|
|
||||||
"set",
|
|
||||||
`@ucas/template/text/${schemaHash.trim()}`,
|
|
||||||
tmplHash.trim(),
|
|
||||||
],
|
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render with custom decay
|
// Render with custom decay
|
||||||
const { stdout: output, exitCode } = await runCli(
|
const { stdout: output, exitCode } = await runCli(
|
||||||
["render", parentHash.trim(), "--decay", "0.7"],
|
["render", parentHash, "--decay", "0.7"],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -979,10 +979,11 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
|
|
||||||
const nodeFile = join(tmpStore, "node.json");
|
const nodeFile = join(tmpStore, "node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Bob" }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Bob" }));
|
||||||
const { stdout: nodeHash } = await runCli(
|
const { stdout: nodeOut } = await runCli(
|
||||||
["put", schemaHash.trim(), nodeFile],
|
["put", schemaHash.trim(), nodeFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const nodeHash = envValue(nodeOut) as string;
|
||||||
|
|
||||||
// Create template (JSON-encoded string)
|
// Create template (JSON-encoded string)
|
||||||
const templateFile = join(tmpStore, "template.json");
|
const templateFile = join(tmpStore, "template.json");
|
||||||
@@ -990,25 +991,21 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
templateFile,
|
templateFile,
|
||||||
JSON.stringify("Greetings {{ payload.name }}!"),
|
JSON.stringify("Greetings {{ payload.name }}!"),
|
||||||
);
|
);
|
||||||
const { stdout: tmplHash } = await runCli(
|
const { stdout: tmplOut } = await runCli(
|
||||||
["put", "@string", templateFile],
|
["put", "@string", templateFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const tmplHash = envValue(tmplOut) as string;
|
||||||
|
|
||||||
await runCli(
|
await runCli(
|
||||||
[
|
["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash],
|
||||||
"var",
|
|
||||||
"set",
|
|
||||||
`@ucas/template/text/${schemaHash.trim()}`,
|
|
||||||
tmplHash.trim(),
|
|
||||||
],
|
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { stdout: output, exitCode } = await runCli(
|
const { stdout: output, exitCode } = await runCli(
|
||||||
[
|
[
|
||||||
"render",
|
"render",
|
||||||
nodeHash.trim(),
|
nodeHash,
|
||||||
"--resolution",
|
"--resolution",
|
||||||
"0.8",
|
"0.8",
|
||||||
"--decay",
|
"--decay",
|
||||||
@@ -1043,14 +1040,15 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
|
|
||||||
const nodeFile = join(tmpStore, "node.json");
|
const nodeFile = join(tmpStore, "node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Charlie" }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Charlie" }));
|
||||||
const { stdout: nodeHash } = await runCli(
|
const { stdout: nodeOut } = await runCli(
|
||||||
["put", schemaHash.trim(), nodeFile],
|
["put", schemaHash.trim(), nodeFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const nodeHash = envValue(nodeOut) as string;
|
||||||
|
|
||||||
// No template registered - should fall back to YAML
|
// No template registered - should fall back to YAML
|
||||||
const { stdout: output, exitCode } = await runCli(
|
const { stdout: output, exitCode } = await runCli(
|
||||||
["render", nodeHash.trim()],
|
["render", nodeHash],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1079,13 +1077,14 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
|
|
||||||
const nodeFile = join(tmpStore, "node.json");
|
const nodeFile = join(tmpStore, "node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Test" }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Test" }));
|
||||||
const { stdout: nodeHash } = await runCli(
|
const { stdout: nodeOut } = await runCli(
|
||||||
["put", schemaHash.trim(), nodeFile],
|
["put", schemaHash.trim(), nodeFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const nodeHash = envValue(nodeOut) as string;
|
||||||
|
|
||||||
const { exitCode, stderr } = await runCli(
|
const { exitCode, stderr } = await runCli(
|
||||||
["render", nodeHash.trim(), "--decay", "1.5"],
|
["render", nodeHash, "--decay", "1.5"],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1126,14 +1125,15 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
// Create and store a simple string node
|
// Create and store a simple string node
|
||||||
const nodeFile = join(tmpStore, "test.json");
|
const nodeFile = join(tmpStore, "test.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify("hello world"));
|
writeFileSync(nodeFile, JSON.stringify("hello world"));
|
||||||
const { stdout: nodeHash } = await runCli(
|
const { stdout: nodeOut } = await runCli(
|
||||||
["put", stringType, nodeFile],
|
["put", stringType, nodeFile],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
const nodeHash = envValue(nodeOut) as string;
|
||||||
|
|
||||||
// Render the valid hash
|
// Render the valid hash
|
||||||
const { exitCode, stdout, stderr } = await runCli(
|
const { exitCode, stdout, stderr } = await runCli(
|
||||||
["render", nodeHash.trim()],
|
["render", nodeHash],
|
||||||
tmpStore,
|
tmpStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ function stripVolatile(json: string): unknown {
|
|||||||
return strip(JSON.parse(json));
|
return strip(JSON.parse(json));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Extract the `value` field from a { type, value } envelope JSON string. */
|
||||||
|
function envValue(json: string): unknown {
|
||||||
|
return (JSON.parse(json) as { value: unknown }).value;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Phase 1: CAS Core ----
|
// ---- Phase 1: CAS Core ----
|
||||||
|
|
||||||
describe("Phase 1: CAS Core", () => {
|
describe("Phase 1: CAS Core", () => {
|
||||||
@@ -88,8 +93,8 @@ describe("Phase 1: CAS Core", () => {
|
|||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||||
const { stdout, exitCode } = await runCli(["put", typeHash, nodeFile]);
|
const { stdout, exitCode } = await runCli(["put", typeHash, nodeFile]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
nodeHash = stdout;
|
nodeHash = envValue(stdout) as string;
|
||||||
});
|
});
|
||||||
|
|
||||||
test("1.6 get returns node JSON (snapshot)", async () => {
|
test("1.6 get returns node JSON (snapshot)", async () => {
|
||||||
@@ -101,19 +106,19 @@ describe("Phase 1: CAS Core", () => {
|
|||||||
test("1.7 has returns true for existing node", async () => {
|
test("1.7 has returns true for existing node", async () => {
|
||||||
const { stdout, exitCode } = await runCli(["has", nodeHash]);
|
const { stdout, exitCode } = await runCli(["has", nodeHash]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toBe("true");
|
expect(envValue(stdout)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("1.8 has returns false for non-existing hash", async () => {
|
test("1.8 has returns false for non-existing hash", async () => {
|
||||||
const { stdout, exitCode } = await runCli(["has", "AAAAAAAAAAAAA"]);
|
const { stdout, exitCode } = await runCli(["has", "AAAAAAAAAAAAA"]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toBe("false");
|
expect(envValue(stdout)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("1.9 verify returns ok for valid node", async () => {
|
test("1.9 verify returns ok for valid node", async () => {
|
||||||
const { stdout, exitCode } = await runCli(["verify", nodeHash]);
|
const { stdout, exitCode } = await runCli(["verify", nodeHash]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toMatchSnapshot();
|
expect(stripVolatile(stdout)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("1.10 refs lists direct references (snapshot)", async () => {
|
test("1.10 refs lists direct references (snapshot)", async () => {
|
||||||
@@ -132,13 +137,13 @@ describe("Phase 1: CAS Core", () => {
|
|||||||
const nodeFile = join(tmpStore, "test-node.json");
|
const nodeFile = join(tmpStore, "test-node.json");
|
||||||
const { stdout, exitCode } = await runCli(["hash", typeHash, nodeFile]);
|
const { stdout, exitCode } = await runCli(["hash", typeHash, nodeFile]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toBe(nodeHash);
|
expect(envValue(stdout)).toBe(nodeHash);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("1.13 list --type returns nodes of that type", async () => {
|
test("1.13 list --type returns nodes of that type", async () => {
|
||||||
const { stdout, exitCode } = await runCli(["list", "--type", typeHash]);
|
const { stdout, exitCode } = await runCli(["list", "--type", typeHash]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toContain(nodeHash);
|
expect(envValue(stdout)).toContain(nodeHash);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,7 +168,7 @@ describe("Phase 2: Schema Validation", () => {
|
|||||||
test("2.2 verify on valid node returns ok (hash + schema)", async () => {
|
test("2.2 verify on valid node returns ok (hash + schema)", async () => {
|
||||||
const { stdout, exitCode } = await runCli(["verify", nodeHash]);
|
const { stdout, exitCode } = await runCli(["verify", nodeHash]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toBe("ok");
|
expect(envValue(stdout)).toBe("ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("2.3 put against non-existent schema hash fails", async () => {
|
test("2.3 put against non-existent schema hash fails", async () => {
|
||||||
@@ -222,12 +227,13 @@ describe("Phase 3: Variable System", () => {
|
|||||||
test("3.5 var set upsert updates existing variable", async () => {
|
test("3.5 var set upsert updates existing variable", async () => {
|
||||||
const node2File = join(tmpStore, "node2.json");
|
const node2File = join(tmpStore, "node2.json");
|
||||||
writeFileSync(node2File, JSON.stringify({ name: "Bob", age: 25 }));
|
writeFileSync(node2File, JSON.stringify({ name: "Bob", age: 25 }));
|
||||||
const { stdout: node2Hash } = await runCli(["put", typeHash, node2File]);
|
const { stdout: node2Out } = await runCli(["put", typeHash, node2File]);
|
||||||
|
const node2Hash = envValue(node2Out) as string;
|
||||||
const { exitCode, stdout } = await runCli([
|
const { exitCode, stdout } = await runCli([
|
||||||
"var",
|
"var",
|
||||||
"set",
|
"set",
|
||||||
"myapp/config",
|
"myapp/config",
|
||||||
node2Hash.trim(),
|
node2Hash,
|
||||||
]);
|
]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stripVolatile(stdout)).toMatchSnapshot();
|
expect(stripVolatile(stdout)).toMatchSnapshot();
|
||||||
@@ -405,7 +411,7 @@ describe("Phase 6: GC", () => {
|
|||||||
const gcNodeFile = join(tmpStore, "gc-node.json");
|
const gcNodeFile = join(tmpStore, "gc-node.json");
|
||||||
writeFileSync(gcNodeFile, JSON.stringify({ name: "GcAlice", age: 30 }));
|
writeFileSync(gcNodeFile, JSON.stringify({ name: "GcAlice", age: 30 }));
|
||||||
const { stdout } = await runCli(["put", typeHash, gcNodeFile]);
|
const { stdout } = await runCli(["put", typeHash, gcNodeFile]);
|
||||||
gcNodeHash = stdout.trim();
|
gcNodeHash = envValue(stdout) as string;
|
||||||
// Set a var referencing this node so it survives GC during Phase 6
|
// Set a var referencing this node so it survives GC during Phase 6
|
||||||
await runCli(["var", "set", "gc-test/ref", gcNodeHash]);
|
await runCli(["var", "set", "gc-test/ref", gcNodeHash]);
|
||||||
});
|
});
|
||||||
@@ -428,25 +434,21 @@ describe("Phase 6: GC", () => {
|
|||||||
const { exitCode } = await runCli(["gc"]);
|
const { exitCode } = await runCli(["gc"]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
const { stdout } = await runCli(["has", gcNodeHash]);
|
const { stdout } = await runCli(["has", gcNodeHash]);
|
||||||
expect(stdout).toBe("true");
|
expect(envValue(stdout)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("6.3 gc reclaims orphan node", async () => {
|
test("6.3 gc reclaims orphan node", async () => {
|
||||||
const orphanFile = join(tmpStore, "orphan.json");
|
const orphanFile = join(tmpStore, "orphan.json");
|
||||||
writeFileSync(orphanFile, JSON.stringify({ name: "Orphan", age: 99 }));
|
writeFileSync(orphanFile, JSON.stringify({ name: "Orphan", age: 99 }));
|
||||||
const { stdout: orphanHashRaw } = await runCli([
|
const { stdout: orphanOut } = await runCli(["put", typeHash, orphanFile]);
|
||||||
"put",
|
const orphanHash = envValue(orphanOut) as string;
|
||||||
typeHash,
|
|
||||||
orphanFile,
|
|
||||||
]);
|
|
||||||
const orphanHash = orphanHashRaw.trim();
|
|
||||||
|
|
||||||
const { stdout: beforeGc } = await runCli(["has", orphanHash]);
|
const { stdout: beforeGc } = await runCli(["has", orphanHash]);
|
||||||
expect(beforeGc).toBe("true");
|
expect(envValue(beforeGc)).toBe(true);
|
||||||
|
|
||||||
await runCli(["gc"]);
|
await runCli(["gc"]);
|
||||||
const { stdout: afterGc } = await runCli(["has", orphanHash]);
|
const { stdout: afterGc } = await runCli(["has", orphanHash]);
|
||||||
expect(afterGc).toBe("false");
|
expect(envValue(afterGc)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
validate,
|
validate,
|
||||||
verify,
|
verify,
|
||||||
walk,
|
walk,
|
||||||
|
wrapEnvelope,
|
||||||
} from "@uncaged/json-cas";
|
} from "@uncaged/json-cas";
|
||||||
import { openStore as openFsStore } from "@uncaged/json-cas-fs";
|
import { openStore as openFsStore } from "@uncaged/json-cas-fs";
|
||||||
|
|
||||||
@@ -253,7 +254,7 @@ async function cmdPut(args: string[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hash = await store.put(typeHash, payload);
|
const hash = await store.put(typeHash, payload);
|
||||||
console.log(hash);
|
out(await wrapEnvelope(store, "@output/put", hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdGet(args: string[]): Promise<void> {
|
async function cmdGet(args: string[]): Promise<void> {
|
||||||
@@ -262,14 +263,14 @@ async function cmdGet(args: string[]): Promise<void> {
|
|||||||
const store = await openStore();
|
const store = await openStore();
|
||||||
const node = store.get(hash);
|
const node = store.get(hash);
|
||||||
if (node === null) die(`Node not found: ${hash}`);
|
if (node === null) die(`Node not found: ${hash}`);
|
||||||
out(node);
|
out(await wrapEnvelope(store, "@output/get", node));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdHas(args: string[]): Promise<void> {
|
async function cmdHas(args: string[]): Promise<void> {
|
||||||
const hash = args[0];
|
const hash = args[0];
|
||||||
if (!hash) die("Usage: json-cas has <hash>");
|
if (!hash) die("Usage: json-cas has <hash>");
|
||||||
const store = await openStore();
|
const store = await openStore();
|
||||||
console.log(String(store.has(hash)));
|
out(await wrapEnvelope(store, "@output/has", store.has(hash)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdVerify(args: string[]): Promise<void> {
|
async function cmdVerify(args: string[]): Promise<void> {
|
||||||
@@ -279,12 +280,13 @@ async function cmdVerify(args: string[]): Promise<void> {
|
|||||||
const node = store.get(hash);
|
const node = store.get(hash);
|
||||||
if (node === null) die(`Node not found: ${hash}`);
|
if (node === null) die(`Node not found: ${hash}`);
|
||||||
const ok = await verify(hash, node);
|
const ok = await verify(hash, node);
|
||||||
|
let status: string;
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
console.log("corrupted");
|
status = "corrupted";
|
||||||
} else {
|
} else {
|
||||||
const valid = validate(store, node);
|
status = validate(store, node) ? "ok" : "invalid";
|
||||||
console.log(valid ? "ok" : "invalid");
|
|
||||||
}
|
}
|
||||||
|
out(await wrapEnvelope(store, "@output/verify", status));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdRefs(args: string[]): Promise<void> {
|
async function cmdRefs(args: string[]): Promise<void> {
|
||||||
@@ -346,7 +348,8 @@ async function cmdHash(args: string[]): Promise<void> {
|
|||||||
const typeHash = await resolveTypeHash(typeHashOrAlias);
|
const typeHash = await resolveTypeHash(typeHashOrAlias);
|
||||||
const payload = readJsonFile(file);
|
const payload = readJsonFile(file);
|
||||||
const hash = await computeHash(typeHash, payload);
|
const hash = await computeHash(typeHash, payload);
|
||||||
console.log(hash);
|
const store = await openStore();
|
||||||
|
out(await wrapEnvelope(store, "@output/hash", hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdRender(args: string[]): Promise<void> {
|
async function cmdRender(args: string[]): Promise<void> {
|
||||||
@@ -826,9 +829,8 @@ async function cmdList(_args: string[]): Promise<void> {
|
|||||||
die("Usage: json-cas list --type <hash-or-alias>");
|
die("Usage: json-cas list --type <hash-or-alias>");
|
||||||
const typeHash = await resolveTypeHash(typeFlag);
|
const typeHash = await resolveTypeHash(typeFlag);
|
||||||
const store = await openStore();
|
const store = await openStore();
|
||||||
for (const hash of store.listByType(typeHash)) {
|
const hashes = Array.from(store.listByType(typeHash));
|
||||||
console.log(hash);
|
out(await wrapEnvelope(store, "@output/list", hashes));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function printUsage(): void {
|
function printUsage(): void {
|
||||||
@@ -836,16 +838,16 @@ function printUsage(): void {
|
|||||||
Usage: json-cas [--store <path>] [--json] <command> [args]
|
Usage: json-cas [--store <path>] [--json] <command> [args]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
put <type-hash> <file.json> Store node, print hash
|
put <type-hash> <file.json> Store node, print { type, value } envelope (value=hash)
|
||||||
get <hash> Print node as JSON
|
get <hash> Print node as { type, value } envelope
|
||||||
has <hash> Print true/false
|
has <hash> Print { type, value } envelope (value=boolean)
|
||||||
verify <hash> Verify integrity + schema, print ok/corrupted/invalid
|
verify <hash> Verify integrity + schema → { type, value } (value=ok/corrupted/invalid)
|
||||||
refs <hash> List direct cas_ref edges
|
refs <hash> List direct cas_ref edges
|
||||||
walk <hash> [--format tree] Recursive traversal
|
walk <hash> [--format tree] Recursive traversal
|
||||||
hash <type-hash> <file.json> Compute hash without storing (dry run)
|
hash <type-hash> <file.json> Compute hash without storing → { type, value } envelope
|
||||||
render <hash> [options] Render node as YAML with resolution decay
|
render <hash> [options] Render node as YAML with resolution decay
|
||||||
render --pipe/-p [options] Render { type, value } from stdin
|
render --pipe/-p [options] Render { type, value } from stdin
|
||||||
list --type <hash-or-alias> List all hashes for a given type
|
list --type <hash-or-alias> List hashes for a type → { type, value } envelope (value=string[])
|
||||||
var set <name> <hash> [--tag <tag>...] Create/update a variable
|
var set <name> <hash> [--tag <tag>...] Create/update a variable
|
||||||
var get <name> --schema <hash> Get a variable by name + schema
|
var get <name> --schema <hash> Get a variable by name + schema
|
||||||
var delete <name> [--schema <hash>] Delete variable(s)
|
var delete <name> [--schema <hash>] Delete variable(s)
|
||||||
|
|||||||
Reference in New Issue
Block a user