1d08c1bf4d
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).
192 lines
4.7 KiB
TypeScript
192 lines
4.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
import {
|
|
mkdirSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
// ---- @ Alias Resolution Tests ----
|
|
|
|
let testDir: string;
|
|
let storePath: string;
|
|
let cliPath: string;
|
|
|
|
beforeEach(() => {
|
|
// Create unique temp directory for each test
|
|
testDir = join(
|
|
tmpdir(),
|
|
`json-cas-cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
);
|
|
storePath = join(testDir, "store");
|
|
cliPath = join(import.meta.dir, "../src/index.ts");
|
|
|
|
mkdirSync(testDir, { recursive: true });
|
|
mkdirSync(storePath, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up test directory
|
|
try {
|
|
rmSync(testDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Run CLI command and return stdout, stderr, and exit code
|
|
*/
|
|
async function runCliAlias(...args: string[]): Promise<{
|
|
stdout: string;
|
|
stderr: string;
|
|
exitCode: number;
|
|
}> {
|
|
const proc = Bun.spawn(
|
|
["bun", "run", cliPath, "--store", storePath, ...args],
|
|
{
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
},
|
|
);
|
|
|
|
const [stdout, stderr] = await Promise.all([
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
]);
|
|
|
|
await proc.exited;
|
|
|
|
return {
|
|
stdout: stdout.trim(),
|
|
stderr: stderr.trim(),
|
|
exitCode: proc.exitCode ?? 0,
|
|
};
|
|
}
|
|
|
|
/** Extract the `value` field from a { type, value } envelope JSON string. */
|
|
function envValue(json: string): unknown {
|
|
return (JSON.parse(json.trim()) as { value: unknown }).value;
|
|
}
|
|
|
|
describe("@ Alias Resolution - put", () => {
|
|
test("ucas put @string <file> should resolve alias", async () => {
|
|
await runCliAlias("init");
|
|
|
|
const payloadFile = join(testDir, "payload.json");
|
|
writeFileSync(payloadFile, JSON.stringify("hello world"));
|
|
|
|
const { stdout, stderr, exitCode } = await runCliAlias(
|
|
"put",
|
|
"@string",
|
|
payloadFile,
|
|
);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(stderr).toBe("");
|
|
// Should output an envelope whose value is a valid hash (13 chars)
|
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
});
|
|
|
|
test("ucas put @number <file> should resolve alias", async () => {
|
|
await runCliAlias("init");
|
|
|
|
const payloadFile = join(testDir, "payload.json");
|
|
writeFileSync(payloadFile, "42");
|
|
|
|
const { stdout, exitCode } = await runCliAlias(
|
|
"put",
|
|
"@number",
|
|
payloadFile,
|
|
);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
});
|
|
|
|
test("ucas put @object <file> should resolve alias", async () => {
|
|
await runCliAlias("init");
|
|
|
|
const payloadFile = join(testDir, "payload.json");
|
|
writeFileSync(payloadFile, JSON.stringify({ foo: "bar" }));
|
|
|
|
const { stdout, exitCode } = await runCliAlias(
|
|
"put",
|
|
"@object",
|
|
payloadFile,
|
|
);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
});
|
|
|
|
test("ucas put @invalid <file> should fail", async () => {
|
|
await runCliAlias("init");
|
|
|
|
const payloadFile = join(testDir, "payload.json");
|
|
writeFileSync(payloadFile, "{}");
|
|
|
|
const { stderr, exitCode } = await runCliAlias(
|
|
"put",
|
|
"@invalid",
|
|
payloadFile,
|
|
);
|
|
|
|
expect(exitCode).not.toBe(0);
|
|
expect(stderr.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("ucas put @schema with nested type constraints should succeed", async () => {
|
|
await runCliAlias("init");
|
|
|
|
const schemaFile = join(testDir, "constrained-schema.json");
|
|
writeFileSync(
|
|
schemaFile,
|
|
JSON.stringify({
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string", minLength: 1, maxLength: 50 },
|
|
age: { type: "number", minimum: 0, maximum: 150 },
|
|
tags: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
minItems: 1,
|
|
uniqueItems: true,
|
|
},
|
|
},
|
|
required: ["name"],
|
|
}),
|
|
);
|
|
|
|
const { stdout, stderr, exitCode } = await runCliAlias(
|
|
"put",
|
|
"@schema",
|
|
schemaFile,
|
|
);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(stderr).toBe("");
|
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
});
|
|
});
|
|
|
|
describe("@ Alias Resolution - hash", () => {
|
|
test("ucas hash @string <file> should compute hash without storing", async () => {
|
|
await runCliAlias("init");
|
|
|
|
const payloadFile = join(testDir, "payload.json");
|
|
writeFileSync(payloadFile, JSON.stringify("test"));
|
|
|
|
const { stdout, stderr, exitCode } = await runCliAlias(
|
|
"hash",
|
|
"@string",
|
|
payloadFile,
|
|
);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(stderr).toBe("");
|
|
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
});
|
|
});
|