test(cli): add e2e validate and init test (#162)

This commit is contained in:
2026-04-27 06:41:12 +00:00
parent 39d2472a91
commit 991089a228
@@ -0,0 +1,236 @@
/**
* E2E tests for `nerve validate` and `nerve init` commands (#162).
* No running daemon needed — just temp dirs with HOME manipulation.
*/
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { defineCommand, runCommand } from "citty";
import { afterEach, describe, expect, it } from "vitest";
import { initCommand } from "../commands/init.js";
import { validateCommand } from "../commands/validate.js";
const testRootCommand = defineCommand({
meta: { name: "nerve", description: "e2e-validate-init" },
subCommands: {
validate: validateCommand,
init: initCommand,
},
});
type CliRunResult = {
stdout: string;
stderr: string;
exitCode: number;
};
class ProcessExitError extends Error {
readonly code: number;
constructor(code: number) {
super(`process.exit(${String(code)})`);
this.name = "ProcessExitError";
this.code = code;
}
}
function patchWriteStream(stream: NodeJS.WriteStream, sink: string[]): () => void {
const orig = stream.write.bind(stream) as (
chunk: string | Uint8Array,
encodingOrCb?: BufferEncoding | ((err: Error | null | undefined) => void),
cb?: (err: Error | null | undefined) => void,
) => boolean;
stream.write = ((
chunk: string | Uint8Array,
encodingOrCb?: BufferEncoding | ((err: Error | null | undefined) => void),
cb?: (err: Error | null | undefined) => void,
) => {
if (typeof chunk === "string") {
sink.push(chunk);
} else {
sink.push(Buffer.from(chunk).toString("utf8"));
}
if (typeof encodingOrCb === "function") {
encodingOrCb(null);
return true;
}
if (cb !== undefined) {
cb(null);
}
return true;
}) as typeof stream.write;
return () => {
stream.write = orig as typeof stream.write;
};
}
async function runTestCli(fakeHome: string, args: string[]): Promise<CliRunResult> {
const stdoutChunks: string[] = [];
const stderrChunks: string[] = [];
const restoreOut = patchWriteStream(process.stdout, stdoutChunks);
const restoreErr = patchWriteStream(process.stderr, stderrChunks);
const prevHome = process.env.HOME;
process.env.HOME = fakeHome;
let exitCode = 0;
const origExit = process.exit;
process.exit = ((code?: number) => {
exitCode = typeof code === "number" ? code : 0;
throw new ProcessExitError(exitCode);
}) as typeof process.exit;
try {
await runCommand(testRootCommand, { rawArgs: args });
} catch (e) {
if (e instanceof ProcessExitError) {
exitCode = e.code;
} else {
exitCode = 1;
stderrChunks.push(e instanceof Error ? e.message : String(e));
}
} finally {
process.exit = origExit;
if (prevHome === undefined) {
process.env.HOME = undefined;
} else {
process.env.HOME = prevHome;
}
restoreOut();
restoreErr();
}
return {
stdout: stdoutChunks.join(""),
stderr: stderrChunks.join(""),
exitCode,
};
}
const VALID_NERVE_YAML = `senses:
counter:
group: e2e
reflexes: []
workflows: {}
max_rounds: 10
api:
port: null
token: null
host: 127.0.0.1
`;
describe("e2e validate", () => {
let fakeHome: string | null = null;
afterEach(() => {
if (fakeHome !== null) {
rmSync(fakeHome, { recursive: true, force: true });
fakeHome = null;
}
});
it("exits 0 for valid nerve.yaml", { timeout: 10_000 }, async () => {
fakeHome = mkdtempSync(join(tmpdir(), "nerve-validate-e2e-"));
const nerveRoot = join(fakeHome, ".uncaged-nerve");
mkdirSync(nerveRoot, { recursive: true });
writeFileSync(join(nerveRoot, "nerve.yaml"), VALID_NERVE_YAML, "utf8");
const result = await runTestCli(fakeHome, ["validate"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("✅");
expect(result.stdout).toContain("valid");
});
it("exits 1 for invalid nerve.yaml", { timeout: 10_000 }, async () => {
fakeHome = mkdtempSync(join(tmpdir(), "nerve-validate-e2e-"));
const nerveRoot = join(fakeHome, ".uncaged-nerve");
mkdirSync(nerveRoot, { recursive: true });
writeFileSync(join(nerveRoot, "nerve.yaml"), "not: valid: yaml: {{{\n", "utf8");
const result = await runTestCli(fakeHome, ["validate"]);
expect(result.exitCode).toBe(1);
expect(result.stderr).toBeTruthy();
});
it("exits 1 for malformed config (missing required fields)", { timeout: 10_000 }, async () => {
fakeHome = mkdtempSync(join(tmpdir(), "nerve-validate-e2e-"));
const nerveRoot = join(fakeHome, ".uncaged-nerve");
mkdirSync(nerveRoot, { recursive: true });
writeFileSync(join(nerveRoot, "nerve.yaml"), "foo: bar\n", "utf8");
const result = await runTestCli(fakeHome, ["validate"]);
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("❌");
});
it("exits 1 when nerve.yaml is missing", { timeout: 10_000 }, async () => {
fakeHome = mkdtempSync(join(tmpdir(), "nerve-validate-e2e-"));
// Don't create .uncaged-nerve at all
const result = await runTestCli(fakeHome, ["validate"]);
expect(result.exitCode).toBe(1);
expect(result.stderr).toBeTruthy();
});
});
describe("e2e init", () => {
let fakeHome: string | null = null;
afterEach(() => {
if (fakeHome !== null) {
rmSync(fakeHome, { recursive: true, force: true });
fakeHome = null;
}
});
it("init --force creates workspace layout", { timeout: 60_000 }, async () => {
fakeHome = mkdtempSync(join(tmpdir(), "nerve-init-e2e-"));
const nerveRoot = join(fakeHome, ".uncaged-nerve");
const result = await runTestCli(fakeHome, ["init", "--force"]);
// init should exit 0 (install/git failures are warnings, not fatal)
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("✅");
// Verify key files exist
expect(existsSync(join(nerveRoot, "nerve.yaml"))).toBe(true);
expect(existsSync(join(nerveRoot, "package.json"))).toBe(true);
expect(existsSync(join(nerveRoot, "biome.json"))).toBe(true);
expect(existsSync(join(nerveRoot, ".gitignore"))).toBe(true);
expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "schema.ts"))).toBe(true);
expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "index.js"))).toBe(true);
expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "migrations", "0001_init.sql"))).toBe(
true,
);
});
it("generated nerve.yaml passes validate", { timeout: 60_000 }, async () => {
fakeHome = mkdtempSync(join(tmpdir(), "nerve-init-e2e-"));
await runTestCli(fakeHome, ["init", "--force"]);
const validateResult = await runTestCli(fakeHome, ["validate"]);
expect(validateResult.exitCode).toBe(0);
expect(validateResult.stdout).toContain("✅");
expect(validateResult.stdout).toContain("valid");
});
it("init without --force on existing dir exits 1", { timeout: 10_000 }, async () => {
fakeHome = mkdtempSync(join(tmpdir(), "nerve-init-e2e-"));
const nerveRoot = join(fakeHome, ".uncaged-nerve");
mkdirSync(nerveRoot, { recursive: true });
writeFileSync(join(nerveRoot, "marker"), "exists", "utf8");
const result = await runTestCli(fakeHome, ["init"]);
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("already exists");
});
});