diff --git a/packages/cli-json-cas/src/cli.test.ts b/packages/cli-json-cas/src/cli.test.ts index 90146d5..e4ce26a 100644 --- a/packages/cli-json-cas/src/cli.test.ts +++ b/packages/cli-json-cas/src/cli.test.ts @@ -597,3 +597,131 @@ describe("Suite 6: CLI Integration with Templates", () => { } }); }); + +describe("Store validation - non-existent paths (Issue #55)", () => { + test("E2E-55-1: get with non-existent store reports store not found", async () => { + const fakePath = "/nonexistent/path/to/store"; + const { exitCode, stderr } = await runCli( + ["get", "AAAAAAAAAAAAA"], + fakePath, + ); + + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Store not found"); + expect(stderr).toContain(fakePath); + expect(stderr).not.toContain("Node not found"); + }); + + test("E2E-55-2: has with non-existent store reports store not found", async () => { + const fakePath = `/tmp/nonexistent-store-${Date.now()}`; + const { exitCode, stderr } = await runCli( + ["has", "BBBBBBBBBBBBB"], + fakePath, + ); + + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Store not found"); + }); + + test("E2E-55-3: verify with non-existent store reports store not found", async () => { + const { exitCode, stderr } = await runCli( + ["verify", "CCCCCCCCCCCCC"], + "/does/not/exist", + ); + + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Store not found"); + }); + + test("E2E-55-4: schema get with non-existent store reports store not found", async () => { + const { exitCode, stderr } = await runCli( + ["schema", "get", "@string"], + "/fake/path", + ); + + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Store not found"); + }); + + test("E2E-55-5: schema list with non-existent store reports store not found", async () => { + const { exitCode, stderr } = await runCli( + ["schema", "list"], + "/nonexistent/store", + ); + + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Store not found"); + }); + + test("E2E-55-6: walk with non-existent store reports store not found", async () => { + const { exitCode, stderr } = await runCli( + ["walk", "DDDDDDDDDDDDD"], + "/tmp/missing-store", + ); + + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Store not found"); + }); + + test("E2E-55-7: cat with non-existent store reports store not found", async () => { + const { exitCode, stderr } = await runCli( + ["cat", "EEEEEEEEEEEEE"], + "/no/such/dir", + ); + + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Store not found"); + }); + + test("E2E-55-8: refs with non-existent store reports store not found", async () => { + const { exitCode, stderr } = await runCli( + ["refs", "FFFFFFFFFFFFF"], + "/nonexistent", + ); + + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Store not found"); + }); + + test("E2E-55-9: existing store works normally (regression)", async () => { + const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-")); + try { + await runCli(["init"], tmpStore); + + const payloadFile = join(tmpStore, "test.json"); + writeFileSync(payloadFile, JSON.stringify("test")); + + const { stdout: hash } = await runCli( + ["put", "@string", payloadFile], + tmpStore, + ); + const { exitCode, stdout, stderr } = await runCli( + ["get", hash.trim()], + tmpStore, + ); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + const node = JSON.parse(stdout); + expect(node).toHaveProperty("type"); + expect(node).toHaveProperty("payload", "test"); + } finally { + rmSync(tmpStore, { recursive: true, force: true }); + } + }); + + test("E2E-55-10: init creates directory (should not validate)", async () => { + const newStore = join(tmpdir(), `new-store-${Date.now()}`); + try { + const { exitCode, stdout, stderr } = await runCli(["init"], newStore); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + const { existsSync } = await import("node:fs"); + expect(existsSync(newStore)).toBe(true); + } finally { + rmSync(newStore, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/cli-json-cas/src/index.ts b/packages/cli-json-cas/src/index.ts index 51bdf4d..e40df7d 100644 --- a/packages/cli-json-cas/src/index.ts +++ b/packages/cli-json-cas/src/index.ts @@ -111,12 +111,26 @@ function readJsonFile(file: string): unknown { } } -function openStore(): Store { - return createFsStore(resolve(storePath)); +/** + * Validate that the store directory exists. + * Dies with a clear error message if not found. + */ +function validateStoreExists(path: string): void { + if (!existsSync(path)) { + die(`Store not found at ${path}`); + } +} + +function openStore(shouldCreate = false): Store { + const fullPath = resolve(storePath); + if (!shouldCreate) { + validateStoreExists(fullPath); + } + return createFsStore(fullPath); } function openVarStore(): VariableStore { - const store = openStore(); + const store = openStore(true); mkdirSync(resolve(storePath), { recursive: true }); return createVariableStore(resolve(varDbPath), store); } @@ -126,9 +140,12 @@ function openVarStore(): VariableStore { * If the input starts with @, resolve it via bootstrap * Otherwise, return the hash as-is */ -async function resolveTypeHash(typeHashOrAlias: string): Promise { +async function resolveTypeHash( + typeHashOrAlias: string, + shouldCreate = false, +): Promise { if (typeHashOrAlias.startsWith("@")) { - const store = openStore(); + const store = openStore(shouldCreate); const builtinSchemas = await bootstrap(store); const resolvedHash = builtinSchemas[typeHashOrAlias]; if (!resolvedHash) { @@ -232,7 +249,7 @@ async function cmdInit(): Promise { } async function cmdBootstrap(): Promise { - const store = openStore(); + const store = openStore(true); const builtinSchemas = await bootstrap(store); const metaHash = builtinSchemas["@schema"]; console.log(metaHash); @@ -242,7 +259,7 @@ async function cmdSchemaPut(args: string[]): Promise { const file = args[0]; if (!file) die("Usage: json-cas schema put "); const schema = readJsonFile(file) as JSONSchema; - const store = openStore(); + const store = openStore(true); const hash = await putSchema(store, schema); console.log(hash); } @@ -291,9 +308,9 @@ async function cmdPut(args: string[]): Promise { const file = args[1]; if (!typeHashOrAlias || !file) die("Usage: json-cas put "); - const typeHash = await resolveTypeHash(typeHashOrAlias); + const typeHash = await resolveTypeHash(typeHashOrAlias, true); const payload = readJsonFile(file); - const store = openStore(); + const store = openStore(true); const hash = await store.put(typeHash, payload); console.log(hash); } @@ -380,7 +397,7 @@ async function cmdHash(args: string[]): Promise { const file = args[1]; if (!typeHashOrAlias || !file) die("Usage: json-cas hash "); - const typeHash = await resolveTypeHash(typeHashOrAlias); + const typeHash = await resolveTypeHash(typeHashOrAlias, true); const payload = readJsonFile(file); const hash = await computeHash(typeHash, payload); console.log(hash);