diff --git a/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts b/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts new file mode 100644 index 0000000..dc53769 --- /dev/null +++ b/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts @@ -0,0 +1,152 @@ +import { execSync } from "node:child_process"; +import { mkdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { cmdCasPutText } from "../commands/cas.js"; + +let storageRoot: string; +let uwfPath: string; + +beforeEach(async () => { + storageRoot = join( + tmpdir(), + `uwf-cas-exit-test-${Date.now()}-${Math.random().toString(36).slice(2)}`, + ); + await mkdir(storageRoot, { recursive: true }); + + // Find the uwf CLI path + uwfPath = join(__dirname, "../../src/cli.ts"); +}); + +afterEach(async () => { + await rm(storageRoot, { recursive: true, force: true }); +}); + +type ExecResult = { + stdout: string; + stderr: string; + exitCode: number; +}; + +function execUwf(args: string[]): ExecResult { + try { + const stdout = execSync(`bun ${uwfPath} ${args.join(" ")}`, { + env: { ...process.env, WORKFLOW_STORAGE_ROOT: storageRoot }, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }); + return { stdout, stderr: "", exitCode: 0 }; + } catch (error: unknown) { + if ( + error && + typeof error === "object" && + "stdout" in error && + "stderr" in error && + "status" in error + ) { + return { + stdout: (error.stdout as Buffer | string).toString(), + stderr: (error.stderr as Buffer | string).toString(), + exitCode: error.status as number, + }; + } + throw error; + } +} + +describe("uwf cas has CLI exit codes", () => { + test("exits 0 when hash exists", async () => { + // Setup: Create a temp storage root, put a text node, capture hash + const putResult = await cmdCasPutText(storageRoot, "test content"); + const hash = putResult.hash; + + // Execute: uwf cas has + const result = execUwf(["cas", "has", hash]); + + // Assert: stdout contains {"exists":true}, exit code === 0 + expect(result.stdout).toContain('"exists":true'); + expect(result.exitCode).toBe(0); + }); + + test("exits 1 when hash does not exist", () => { + // Setup: Create a temp storage root (empty CAS store) + // Execute: uwf cas has NOSUCHHASH123 + const result = execUwf(["cas", "has", "NOSUCHHASH123"]); + + // Assert: stdout contains {"exists":false}, exit code === 1 + expect(result.stdout).toContain('"exists":false'); + expect(result.exitCode).toBe(1); + }); + + test("JSON output format unchanged for exists=true", async () => { + // Setup: Create store, put node + const putResult = await cmdCasPutText(storageRoot, "test"); + const hash = putResult.hash; + + // Execute: uwf cas has + const result = execUwf(["cas", "has", hash]); + + // Assert: stdout JSON parses correctly to {exists: true} + const parsed = JSON.parse(result.stdout.trim()); + expect(parsed).toEqual({ exists: true }); + }); + + test("JSON output format unchanged for exists=false", () => { + // Setup: Create empty store + // Execute: uwf cas has INVALID + const result = execUwf(["cas", "has", "INVALID"]); + + // Assert: stdout JSON parses correctly to {exists: false} + const parsed = JSON.parse(result.stdout.trim()); + expect(parsed).toEqual({ exists: false }); + }); + + test("YAML output format preserves exit code behavior for exists=true", async () => { + // Setup: Create store with node + const putResult = await cmdCasPutText(storageRoot, "test"); + const hash = putResult.hash; + + // Execute: uwf --format yaml cas has + const result = execUwf(["--format", "yaml", "cas", "has", hash]); + + // Assert: exit code === 0, output is YAML format + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("exists:"); + expect(result.stdout).toContain("true"); + }); + + test("YAML output format preserves exit code behavior for exists=false", () => { + // Setup: Create empty store + // Execute: uwf --format yaml cas has INVALID + const result = execUwf(["--format", "yaml", "cas", "has", "INVALID"]); + + // Assert: exit code === 1, output is YAML format + expect(result.exitCode).toBe(1); + expect(result.stdout).toContain("exists:"); + expect(result.stdout).toContain("false"); + }); +}); + +describe("regression: other cas commands unaffected", () => { + test("uwf cas get still exits 1 on not-found with error message", () => { + // Execute: uwf cas get NOSUCHHASH + const result = execUwf(["cas", "get", "NOSUCHHASH"]); + + // Assert: exit code === 1, stderr contains "Node not found" + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("Node not found"); + }); + + test("uwf cas put-text behavior unchanged", () => { + // Execute: uwf cas put-text "hello" + const result = execUwf(["cas", "put-text", "hello"]); + + // Assert: exit code === 0, returns hash + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout.trim()); + expect(parsed).toHaveProperty("hash"); + expect(typeof parsed.hash).toBe("string"); + expect(parsed.hash.length).toBe(13); // Crockford Base32 XXH64 hash length + }); +}); diff --git a/packages/cli-workflow/src/__tests__/cas.test.ts b/packages/cli-workflow/src/__tests__/cas.test.ts new file mode 100644 index 0000000..a56e3ec --- /dev/null +++ b/packages/cli-workflow/src/__tests__/cas.test.ts @@ -0,0 +1,74 @@ +import { mkdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { cmdCasHas, cmdCasPutText } from "../commands/cas.js"; + +let storageRoot: string; + +beforeEach(async () => { + storageRoot = join(tmpdir(), `uwf-cas-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); + await mkdir(storageRoot, { recursive: true }); +}); + +afterEach(async () => { + await rm(storageRoot, { recursive: true, force: true }); +}); + +describe("cmdCasHas", () => { + test("returns {exists: true} for existing hash", async () => { + // Setup: Create a test store, put a node, get its hash + const putResult = await cmdCasPutText(storageRoot, "test content"); + const hash = putResult.hash; + + // Execute: Call cmdCasHas with the valid hash + const result = await cmdCasHas(storageRoot, hash); + + // Assert: Result equals {exists: true} + expect(result).toEqual({ exists: true }); + }); + + test("returns {exists: false} for non-existent hash", async () => { + // Setup: Create an empty test store + // (storageRoot already created in beforeEach) + + // Execute: Call cmdCasHas with an invalid hash + const result = await cmdCasHas(storageRoot, "INVALIDHASH12"); + + // Assert: Result equals {exists: false} + expect(result).toEqual({ exists: false }); + }); + + test("does not throw for non-existent hash", async () => { + // Setup: Create an empty test store + // Execute & Assert: Does not throw, returns {exists: false} + await expect(cmdCasHas(storageRoot, "NOSUCHHASH123")).resolves.toEqual({ + exists: false, + }); + }); + + test("handles malformed hash gracefully", async () => { + // Setup: Create a test store + // Execute: Call cmdCasHas with a too-short hash + const result = await cmdCasHas(storageRoot, "xyz"); + + // Assert: Returns {exists: false} (store.has() returns false) + expect(result).toEqual({ exists: false }); + }); + + test("handles empty hash string", async () => { + // Execute: Call cmdCasHas with an empty string + const result = await cmdCasHas(storageRoot, ""); + + // Assert: Returns {exists: false} + expect(result).toEqual({ exists: false }); + }); + + test("handles hash with special characters", async () => { + // Execute: Call cmdCasHas with special characters + const result = await cmdCasHas(storageRoot, "HASH!@#"); + + // Assert: Returns {exists: false} + expect(result).toEqual({ exists: false }); + }); +}); diff --git a/packages/cli-workflow/src/cli.ts b/packages/cli-workflow/src/cli.ts index 1ed04d6..ef1559c 100755 --- a/packages/cli-workflow/src/cli.ts +++ b/packages/cli-workflow/src/cli.ts @@ -549,7 +549,11 @@ cas .action((hash: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { - writeOutput(await cmdCasHas(storageRoot, hash)); + const result = await cmdCasHas(storageRoot, hash); + writeOutput(result); + if (!result.exists) { + process.exit(1); + } }); });