test(e2e): nerve sense trigger (closes #157) #167

Merged
xiaonuo merged 2 commits from test/157-sense-trigger into main 2026-04-27 07:05:00 +00:00
@@ -0,0 +1,122 @@
/**
* E2E-style tests for `nerve sense trigger` (issue #157): citty + workspace stubs
* and a mock daemon on a real Unix socket — no running nerve process required.
*/
import { mkdtempSync, rmSync } from "node:fs";
import { type Server, createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runCommand } from "citty";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { senseCommand } from "../commands/sense.js";
import * as workspace from "../workspace.js";
describe("nerve sense trigger (e2e mock daemon)", () => {
let sockDir: string;
let sockPath: string;
let server: Server;
let ipcReceived: unknown[];
let knownOkSenses: Set<string>;
let stdoutBuf: string;
let stderrBuf: string;
beforeEach(async () => {
ipcReceived = [];
knownOkSenses = new Set(["cpu-usage"]);
stdoutBuf = "";
stderrBuf = "";
sockDir = mkdtempSync(join(tmpdir(), "nerve-sense-trigger-e2e-"));
sockPath = join(sockDir, "nerve.sock");
server = createServer((socket) => {
socket.on("data", (chunk: Buffer) => {
const line = chunk.toString("utf8").trim();
let req: unknown;
try {
req = JSON.parse(line);
} catch {
return;
}
ipcReceived.push(req);
const r = req as { type?: string; sense?: string };
if (
r.type === "trigger-sense" &&
typeof r.sense === "string" &&
knownOkSenses.has(r.sense)
) {
socket.write(`${JSON.stringify({ ok: true })}\n`);
} else if (r.type === "trigger-sense" && typeof r.sense === "string") {
socket.write(`${JSON.stringify({ ok: false, error: `Unknown sense: "${r.sense}"` })}\n`);
}
});
});
await new Promise<void>((resolve) => {
server.listen(sockPath, resolve);
});
vi.spyOn(workspace, "isRunning").mockReturnValue(true);
vi.spyOn(workspace, "getSocketPath").mockReturnValue(sockPath);
vi.spyOn(process.stdout, "write").mockImplementation((chunk, encodingOrCb?, cb?) => {
stdoutBuf += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
if (typeof encodingOrCb === "function") {
encodingOrCb(null);
} else if (cb !== undefined) {
cb(null);
}
return true;
});
vi.spyOn(process.stderr, "write").mockImplementation((chunk, encodingOrCb?, cb?) => {
stderrBuf += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
if (typeof encodingOrCb === "function") {
encodingOrCb(null);
} else if (cb !== undefined) {
cb(null);
}
return true;
});
vi.spyOn(process, "exit").mockImplementation((code?: number | string | null) => {
const c = typeof code === "number" ? code : 1;
// Throw instead of actually exiting so the test runner stays alive;
// the test asserts on this message to verify the exit code.
throw new Error(`process.exit(${String(c)})`);
});
});
afterEach(async () => {
vi.restoreAllMocks();
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
rmSync(sockDir, { recursive: true, force: true });
});
it("trigger known sense exits normally and stdout contains Triggered", async () => {
await runCommand(senseCommand, { rawArgs: ["trigger", "cpu-usage"] });
expect(stdoutBuf).toContain("Triggered");
expect(stdoutBuf).toContain("✅");
expect(stderrBuf).toBe("");
});
it("trigger non-existent sense writes daemon error to stderr and exits 1", async () => {
await expect(
runCommand(senseCommand, { rawArgs: ["trigger", "no-such-sense"] }),
).rejects.toThrow("process.exit(1)");
expect(stdoutBuf).toBe("");
expect(stderrBuf).toContain("Daemon rejected trigger");
expect(stderrBuf).toContain("Unknown sense");
expect(stderrBuf).toContain("no-such-sense");
});
it("sends IPC { type: trigger-sense, sense: <name> } to the daemon", async () => {
knownOkSenses.add("custom-sense");
await runCommand(senseCommand, { rawArgs: ["trigger", "custom-sense"] });
expect(ipcReceived).toHaveLength(1);
expect(ipcReceived[0]).toEqual({ type: "trigger-sense", sense: "custom-sense" });
});
});