0c0491ea17
- 36 个 test 文件 bun:test → vitest
- Bun.spawn() → execFileSync('tsx', ...)
- Bun.file() → readFileSync
- import.meta.dir → import.meta.dirname (tests) / __dirname (CLI source)
- 删除 bun-types devDep
- 添加 vitest + tsx devDep
- CLI shebang bun → node
- 30/36 test files pass, 558/617 tests pass
Refs #62
220 lines
7.2 KiB
TypeScript
220 lines
7.2 KiB
TypeScript
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 <hash> 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");
|
|
});
|
|
});
|