This repository has been archived on 2026-06-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
json-cas/packages/cli-json-cas/tests/render.test.ts
T
xiaoju 1d08c1bf4d refactor: reorganize CLI E2E tests into tests/ by scenario
Move 4 monolithic test files (cli.test.ts, e2e.test.ts, template.test.ts,
var.test.ts) from src/ into tests/ directory, split by scenario:

  tests/helpers.ts           - shared utilities (envValue, stripVolatile)
  tests/put-get-has.test.ts  - basic CAS storage operations
  tests/verify-refs-walk.test.ts - graph traversal
  tests/schema-validation.test.ts - Issue #50 schema validation
  tests/alias.test.ts        - @ alias resolution
  tests/variable.test.ts     - var set/get/delete/list/tag
  tests/template.test.ts     - template set/get/list/delete
  tests/render.test.ts       - render + render -p
  tests/pipe.test.ts         - pipe composition + stdin input
  tests/gc.test.ts           - garbage collection
  tests/edge-cases.test.ts   - help, --store, error paths

All 516 tests pass (159 CLI + 357 core/fs).
2026-06-01 03:38:35 +00:00

519 lines
16 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { bootstrap } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import { envValue, putSchemaFile, runCli, runCliWithStdin } from "./helpers";
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
// --- Standalone render tests from cli.test.ts ---
describe("ucas render command", () => {
test("R1: render requires hash argument", async () => {
const { exitCode, stderr } = await runCli(["render"]);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Usage");
});
test("R2: render with missing hash shows error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const { exitCode, stderr } = await runCli(
["render", "ZZZZZZZZZZZZZ"],
tmpStore,
);
// Missing hash should exit with error
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Node not found");
expect(stderr).toContain("ZZZZZZZZZZZZZ");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("R3: render with invalid numeric flag fails", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const { exitCode, stderr } = await runCli(
["render", "AAAAAAAAAAAAA", "--resolution", "invalid"],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("valid number");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
});
// --- e2e Phase 5: Render ---
describe("Phase 5: Render", () => {
let tmpStore: string;
let varDbPath: string;
let typeHash: string;
let nodeHash: string;
async function runCliE2e(
args: string[],
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(
["bun", entrypoint, "--store", tmpStore, "--var-db", varDbPath, ...args],
{ stdout: "pipe", stderr: "pipe" },
);
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
}
async function runCliE2eWithStdin(
args: string[],
stdin: string,
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(
["bun", entrypoint, "--store", tmpStore, "--var-db", varDbPath, ...args],
{ stdin: "pipe", stdout: "pipe", stderr: "pipe" },
);
proc.stdin.write(stdin);
proc.stdin.end();
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
}
beforeAll(async () => {
tmpStore = mkdtempSync(join(tmpdir(), "json-cas-e2e-"));
varDbPath = join(tmpStore, "variables.db");
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("@uncaged/json-cas-fs");
const { putSchema } = await import("@uncaged/json-cas");
const store = await openFsStore(tmpStore);
typeHash = await 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 runCliE2e(["put", typeHash, nodeFile]);
nodeHash = envValue(stdout) as string;
// Register template for render tests
const tmplFile = join(tmpStore, "render-template.liquid");
writeFileSync(tmplFile, "Hello {{ payload.name }}!");
await runCliE2e(["template", "set", typeHash, tmplFile]);
});
afterAll(() => {
rmSync(tmpStore, { recursive: true, force: true });
});
test("5.1 render fills payload variables", async () => {
const { stdout, exitCode } = await runCliE2e(["render", nodeHash]);
expect(exitCode).toBe(0);
expect(stdout).toBe("Hello Alice!");
expect(stdout).toMatchSnapshot();
});
test("5.2 render --resolution with different value", async () => {
const { stdout, exitCode } = await runCliE2e([
"render",
nodeHash,
"--resolution",
"0.5",
]);
expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});
test("5.3 render non-existent hash fails with error", async () => {
const { stderr, exitCode } = await runCliE2e(["render", "ZZZZZZZZZZZZZ"]);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Node not found");
expect(stderr).toContain("ZZZZZZZZZZZZZ");
});
});
// --- Suite 6: CLI Integration with Templates (from cli.test.ts) ---
describe("Suite 6: CLI Integration with Templates", () => {
test("6.1 CLI with Template (Default Parameters)", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
// Initialize store
await runCli(["init"], tmpStore);
// Create schema
const schemaFile = join(tmpStore, "schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: { name: { type: "string" } },
}),
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Create node
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Alice" }));
const { stdout: nodeOut } = await runCli(
["put", schemaHash.trim(), nodeFile],
tmpStore,
);
const nodeHash = envValue(nodeOut) as string;
// Create template file (JSON-encoded string)
const templateFile = join(tmpStore, "template.json");
writeFileSync(templateFile, JSON.stringify("Hello {{ payload.name }}!"));
const { stdout: tmplOut } = await runCli(
["put", "@string", templateFile],
tmpStore,
);
const tmplHash = envValue(tmplOut) as string;
// Register template
await runCli(
["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash],
tmpStore,
);
// Render with template
const { stdout: output, exitCode } = await runCli(
["render", nodeHash],
tmpStore,
);
expect(exitCode).toBe(0);
expect(output).toBe("Hello Alice!");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("6.2 CLI with Template + Custom Decay", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
// Create schema with child ref
const schemaFile = join(tmpStore, "schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: {
value: { type: "string" },
child: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
}),
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Create child node
const childFile = join(tmpStore, "child.json");
writeFileSync(childFile, JSON.stringify({ value: "child", child: null }));
const { stdout: childOut } = await runCli(
["put", schemaHash.trim(), childFile],
tmpStore,
);
const childHash = envValue(childOut) as string;
// Create parent node
const parentFile = join(tmpStore, "parent.json");
writeFileSync(
parentFile,
JSON.stringify({ value: "parent", child: childHash }),
);
const { stdout: parentOut } = await runCli(
["put", schemaHash.trim(), parentFile],
tmpStore,
);
const parentHash = envValue(parentOut) as string;
// Create template showing resolution (JSON-encoded string)
const templateFile = join(tmpStore, "template.json");
writeFileSync(
templateFile,
JSON.stringify("{{ payload.value }}(res={{ resolution }})"),
);
const { stdout: tmplOut } = await runCli(
["put", "@string", templateFile],
tmpStore,
);
const tmplHash = envValue(tmplOut) as string;
// Register template
await runCli(
["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash],
tmpStore,
);
// Render with custom decay
const { stdout: output, exitCode } = await runCli(
["render", parentHash, "--decay", "0.7"],
tmpStore,
);
expect(exitCode).toBe(0);
expect(output).toContain("parent(res=1)");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("6.3 CLI with Template + All Parameters", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: { name: { type: "string" } },
}),
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Bob" }));
const { stdout: nodeOut } = await runCli(
["put", schemaHash.trim(), nodeFile],
tmpStore,
);
const nodeHash = envValue(nodeOut) as string;
// Create template (JSON-encoded string)
const templateFile = join(tmpStore, "template.json");
writeFileSync(
templateFile,
JSON.stringify("Greetings {{ payload.name }}!"),
);
const { stdout: tmplOut } = await runCli(
["put", "@string", templateFile],
tmpStore,
);
const tmplHash = envValue(tmplOut) as string;
await runCli(
["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash],
tmpStore,
);
const { stdout: output, exitCode } = await runCli(
[
"render",
nodeHash,
"--resolution",
"0.8",
"--decay",
"0.6",
"--epsilon",
"0.005",
],
tmpStore,
);
expect(exitCode).toBe(0);
expect(output).toBe("Greetings Bob!");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("6.4 CLI with Non-templated Node (YAML Fallback)", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: { name: { type: "string" } },
}),
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Charlie" }));
const { stdout: nodeOut } = await runCli(
["put", schemaHash.trim(), nodeFile],
tmpStore,
);
const nodeHash = envValue(nodeOut) as string;
// No template registered - should fall back to YAML
const { stdout: output, exitCode } = await runCli(
["render", nodeHash],
tmpStore,
);
expect(exitCode).toBe(0);
expect(output).toContain("name:");
expect(output).toContain("Charlie");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("6.5 CLI Error: Invalid Decay Value", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: { name: { type: "string" } },
}),
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Test" }));
const { stdout: nodeOut } = await runCli(
["put", schemaHash.trim(), nodeFile],
tmpStore,
);
const nodeHash = envValue(nodeOut) as string;
const { exitCode, stderr } = await runCli(
["render", nodeHash, "--decay", "1.5"],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("decay");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("R8: render with non-existent hash exits with error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const { exitCode, stderr, stdout } = await runCli(
["render", "AAAAAAAAAAAAA"],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Node not found");
expect(stderr).toContain("AAAAAAAAAAAAA");
expect(stdout).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("R9: render with valid hash exits successfully", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
// Get @string type hash via bootstrap
const store = createFsStore(tmpStore);
const types = await bootstrap(store);
const stringType = types["@string"];
// Create and store a simple string node
const nodeFile = join(tmpStore, "test.json");
writeFileSync(nodeFile, JSON.stringify("hello world"));
const { stdout: nodeOut } = await runCli(
["put", stringType, nodeFile],
tmpStore,
);
const nodeHash = envValue(nodeOut) as string;
// Render the valid hash
const { exitCode, stdout, stderr } = await runCli(
["render", nodeHash],
tmpStore,
);
expect(exitCode).toBe(0);
expect(stdout).toContain("hello world");
expect(stderr).toBe("");
expect(stdout).not.toContain("Error");
expect(stdout).not.toContain("Node not found");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("R10: render --pipe with valid envelope succeeds", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
// Get @string type hash via bootstrap
const store = createFsStore(tmpStore);
const types = await bootstrap(store);
const stringType = types["@string"];
// Create envelope and pipe to render
const envelope = JSON.stringify({ type: stringType, value: "test" });
const { exitCode, stdout, stderr } = await runCliWithStdin(
["render", "--pipe"],
tmpStore,
envelope,
);
expect(exitCode).toBe(0);
expect(stdout).toContain("test");
expect(stderr).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("R11: render --pipe with invalid type hash still renders", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
// Use invalid type hash in envelope
const envelope = JSON.stringify({
type: "ZZZZZZZZZZZZZ",
value: "test",
});
const { exitCode, stdout, stderr } = await runCliWithStdin(
["render", "--pipe"],
tmpStore,
envelope,
);
expect(exitCode).toBe(0);
expect(stdout).toContain("test");
expect(stderr).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
});
// Store validation tests removed - with auto-bootstrap in Phase 1a,
// stores are automatically created and bootstrapped when opened.
// Issue #55 validation is no longer applicable.