fix: validate schema exists in json-cas put command

Add schema existence check before storing nodes to prevent data
corruption from invalid schema references. The put command now
properly rejects non-existent schema hashes and @aliases with
clear error messages.

Changes:
- Add store.has(typeHash) check in cmdPut before store.put()
- Return exit code 1 with "Schema not found" error message
- Add 11 comprehensive tests covering error and regression cases
- All 392 tests pass, no regressions

Fixes #51

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 08:31:59 +00:00
parent 2932aa5980
commit 0aa9074de6
2 changed files with 207 additions and 0 deletions
+201
View File
@@ -315,6 +315,207 @@ describe("ucas render command", () => {
});
});
describe("Schema Hash Validation in put command", () => {
test("put with non-existent literal hash should fail", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ test: "data" }));
const { stderr, exitCode } = await runCliAlias(
"put",
"AAAAAAAAAAAAA",
payloadFile,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Schema not found: AAAAAAAAAAAAA");
});
test("put with different non-existent literal hash should fail", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ test: "data" }));
const { stderr, exitCode } = await runCliAlias(
"put",
"ZZZZZZZZZZZZZ",
payloadFile,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Schema not found: ZZZZZZZZZZZZZ");
});
test("put with malformed hash should fail", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ test: "data" }));
const { stderr, exitCode } = await runCliAlias(
"put",
"INVALID_HASH",
payloadFile,
);
expect(exitCode).not.toBe(0);
expect(stderr.length).toBeGreaterThan(0);
});
test("put with non-existent @alias should fail with clear message", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ test: "data" }));
const { stderr, exitCode } = await runCliAlias(
"put",
"@nonexistent",
payloadFile,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Schema not found");
expect(stderr).toContain("@nonexistent");
});
test("put with valid @string alias should succeed (regression)", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify("hello"));
const { stdout, stderr, exitCode } = await runCliAlias(
"put",
"@string",
payloadFile,
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("put with valid @number alias should succeed (regression)", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, "42");
const { stdout, stderr, exitCode } = await runCliAlias(
"put",
"@number",
payloadFile,
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("put with valid @object alias should succeed (regression)", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ key: "value" }));
const { stdout, stderr, exitCode } = await runCliAlias(
"put",
"@object",
payloadFile,
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("put with explicit valid schema hash should succeed (regression)", async () => {
await runCliAlias("init");
// First, create a custom schema
const schemaFile = join(testDir, "schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCliAlias(
"schema",
"put",
schemaFile,
);
// Now create a node with that schema
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ name: "test" }));
const { stdout, stderr, exitCode } = await runCliAlias(
"put",
schemaHash.trim(),
payloadFile,
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("all bootstrap schema aliases should work (regression)", async () => {
await runCliAlias("init");
const aliases = ["@string", "@number", "@object", "@array", "@bool"];
const payloads = [
JSON.stringify("test"),
"123",
JSON.stringify({}),
JSON.stringify([]),
"true",
];
for (let i = 0; i < aliases.length; i++) {
const payloadFile = join(testDir, `payload-${i}.json`);
writeFileSync(payloadFile, payloads[i] ?? "");
const { exitCode } = await runCliAlias(
"put",
aliases[i] ?? "",
payloadFile,
);
expect(exitCode).toBe(0);
}
});
test("error message should preserve original input (hash)", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ test: "data" }));
const { stderr } = await runCliAlias("put", "AAAAAAAAAAAAA", payloadFile);
// Error should show the original hash, not a resolved version
expect(stderr).toContain("AAAAAAAAAAAAA");
});
test("error message should preserve original input (alias)", async () => {
await runCliAlias("init");
const payloadFile = join(testDir, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ test: "data" }));
const { stderr } = await runCliAlias("put", "@invalid", payloadFile);
// Error should show the original alias, not a resolved hash
expect(stderr).toContain("@invalid");
});
});
describe("Suite 6: CLI Integration with Templates", () => {
test("6.1 CLI with Template (Default Parameters)", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
+6
View File
@@ -294,6 +294,12 @@ async function cmdPut(args: string[]): Promise<void> {
const typeHash = await resolveTypeHash(typeHashOrAlias);
const payload = readJsonFile(file);
const store = openStore();
// Check if schema exists before storing
if (!store.has(typeHash)) {
die(`Schema not found: ${typeHashOrAlias}`);
}
const hash = await store.put(typeHash, payload);
console.log(hash);
}