import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { execFileSync } from "node:child_process"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { envValue } from "./helpers"; const entrypoint = resolve(import.meta.dirname, "../src/index.ts"); let tmpStore: string; let typeHash: string; let _nodeHash: string; beforeAll(async () => { tmpStore = mkdtempSync(join(tmpdir(), "ocas-e2e-")); const schemaFile = join(tmpStore, "test-schema.json"); writeFileSync( schemaFile, JSON.stringify({ type: "object", properties: { name: { type: "string" }, age: { type: "number" } }, required: ["name"], additionalProperties: false, }), ); const { openStore: openFsStore } = await import("@ocas/fs"); const { putSchema } = await import("@ocas/core"); const store = await openFsStore(tmpStore); typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8"))); const nodeFile = join(tmpStore, "test-node.json"); writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 })); const { stdout } = await runCli(["put", typeHash, nodeFile]); _nodeHash = envValue(stdout) as string; // Set up template for render tests const tmplFile = join(tmpStore, "render-template.liquid"); writeFileSync(tmplFile, "Hello {{ payload.name }}!"); await runCli(["template", "set", typeHash, tmplFile]); }); afterAll(() => { rmSync(tmpStore, { recursive: true, force: true }); }); function runCli(...args: string[]): { stdout: string; stderr: string; exitCode: number } { try { const stdout = execFileSync("tsx", [entrypoint, "--home", tmpStore, ...args], { encoding: "utf-8", timeout: 10000, }); return { stdout: stdout.trim(), stderr: "", exitCode: 0 }; } catch (e: unknown) { const err = e as { stdout?: string; stderr?: string; status?: number }; return { stdout: (err.stdout ?? "").trim(), stderr: (err.stderr ?? "").trim(), exitCode: err.status ?? 1 }; } } function runCliWithStdin( args: string[], stdin: string, ): { stdout: string; stderr: string; exitCode: number } { try { const stdout = execFileSync("tsx", [entrypoint, "--home", tmpStore, ...args], { input: stdin, encoding: "utf-8", timeout: 10000, }); return { stdout: stdout.trim(), stderr: "", exitCode: 0 }; } catch (e: unknown) { const err = e as { stdout?: string; stderr?: string; status?: number }; return { stdout: (err.stdout ?? "").trim(), stderr: (err.stderr ?? "").trim(), exitCode: err.status ?? 1 }; } } // ---- Phase 8: Pipe Composition ---- describe("Phase 8: Pipe Composition", () => { test("8.1 put | render -p expands the stored hash to its content", async () => { const nodeFile = join(tmpStore, "pipe-node.json"); writeFileSync(nodeFile, JSON.stringify({ name: "Bob", age: 42 })); const { stdout: putOut, exitCode: putExit } = await runCli([ "put", typeHash, nodeFile, ]); expect(putExit).toBe(0); // The put envelope value is a ocas_ref hash; render -p dereferences it and // renders the stored node's payload. const { stdout, exitCode } = await runCliWithStdin( ["render", "--pipe"], putOut, ); expect(exitCode).toBe(0); expect(stdout).toContain("Bob"); }); test("8.3 list --type @ocas/schema emits a parseable envelope of hashes", async () => { const { stdout, exitCode } = await runCli([ "list", "--type", "@ocas/schema", ]); expect(exitCode).toBe(0); // Downstream consumers (jq, etc.) read the `value` array of {hash,...}. const value = envValue(stdout) as Array<{ hash: string }>; expect(Array.isArray(value)).toBe(true); for (const entry of value) { expect(entry.hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); } }); test("8.4 list --type @ocas/schema | render -p expands the schema list", async () => { const { stdout: listOut } = await runCli([ "list", "--type", "@ocas/schema", ]); // list result items are ocas_ref hashes; render -p dereferences each one // and renders the schema contents. const { stdout, exitCode } = await runCliWithStdin( ["render", "--pipe"], listOut, ); expect(exitCode).toBe(0); expect(stdout.length).toBeGreaterThan(0); }); test("8.5 render uses a registered template", async () => { // Register a template for the schema, then render a fresh node by hash. const tmplFile = join(tmpStore, "pipe-render.liquid"); writeFileSync(tmplFile, "Person: {{ payload.name }} ({{ payload.age }})"); const { exitCode: setExit } = await runCli([ "template", "set", typeHash, tmplFile, ]); expect(setExit).toBe(0); const nodeFile = join(tmpStore, "pipe-render-node.json"); writeFileSync(nodeFile, JSON.stringify({ name: "Carol", age: 25 })); const { stdout: putOut } = await runCli(["put", typeHash, nodeFile]); const freshHash = envValue(putOut) as string; const { stdout, exitCode } = await runCli(["render", freshHash]); expect(exitCode).toBe(0); expect(stdout).toBe("Person: Carol (25)"); }); }); // ---- Phase 9: Put/Hash Pipe Input ---- describe("Phase 9: Put/Hash Pipe Input", () => { test("9.1 put -p reads JSON from stdin and stores node", async () => { const payload = JSON.stringify({ name: "PipeAlice", age: 99 }); const { stdout, exitCode } = await runCliWithStdin( ["put", typeHash, "-p"], payload, ); expect(exitCode).toBe(0); const hash = envValue(stdout); expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); // Verify stored correctly const { stdout: getOut } = await runCli(["get", hash as string]); expect(getOut).toContain("PipeAlice"); }); test("9.2 hash -p reads JSON from stdin and computes hash without storing", async () => { const payload = JSON.stringify({ name: "PipeBob", age: 55 }); const { stdout, exitCode } = await runCliWithStdin( ["hash", typeHash, "-p"], payload, ); expect(exitCode).toBe(0); const hash = envValue(stdout); expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); // Should NOT be stored const { exitCode: hasExit, stdout: hasOut } = await runCli([ "has", hash as string, ]); expect(hasExit).toBe(0); expect(envValue(hasOut)).toBe(false); }); test("9.3 put -p with file arg errors", async () => { const { stderr, exitCode } = await runCliWithStdin( ["put", typeHash, "some-file.json", "-p"], "{}", ); expect(exitCode).not.toBe(0); expect(stderr).toContain("Cannot use --pipe/-p with a file argument"); }); test("9.4 put -p with empty stdin errors", async () => { const { stderr, exitCode } = await runCliWithStdin( ["put", typeHash, "-p"], "", ); expect(exitCode).not.toBe(0); expect(stderr).toContain("No input on stdin"); }); test("9.5 put -p with invalid JSON errors", async () => { const { stderr, exitCode } = await runCliWithStdin( ["put", typeHash, "-p"], "not json", ); expect(exitCode).not.toBe(0); expect(stderr).toContain("Invalid JSON on stdin"); }); });