test(cli): e2e daemon start/stop/status lifecycle (#159) #169
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* E2E: daemon start / status / stop lifecycle against the in-process test harness.
|
||||
*
|
||||
* Does not invoke the `stop` CLI while the harness PID file points at the current process
|
||||
* (that would SIGTERM the test runner). After `status` shows running, we stop the kernel
|
||||
* and remove `nerve.pid`, then assert `status` reports not running; `afterEach` tears down
|
||||
* the temp HOME.
|
||||
*/
|
||||
|
||||
import { existsSync, unlinkSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { type TestDaemonHandle, runCli, startTestDaemon, stopTestDaemon } from "./e2e-harness.js";
|
||||
|
||||
describe("e2e daemon lifecycle", () => {
|
||||
let daemon: TestDaemonHandle | null = null;
|
||||
let kernelAlreadyStopped = false;
|
||||
|
||||
afterEach(async () => {
|
||||
const h = daemon;
|
||||
const skipKernel = kernelAlreadyStopped;
|
||||
daemon = null;
|
||||
kernelAlreadyStopped = false;
|
||||
if (h === null) return;
|
||||
await Promise.race([
|
||||
stopTestDaemon(h, skipKernel),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("stopTestDaemon timed out after 10s")), 10_000),
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it(
|
||||
"nerve.pid + kernel up, status running, then stopped + pid cleared, status not running",
|
||||
{ timeout: 30_000 },
|
||||
async () => {
|
||||
daemon = await startTestDaemon();
|
||||
|
||||
const pidPath = join(daemon.nerveRoot, "nerve.pid");
|
||||
expect(existsSync(pidPath)).toBe(true);
|
||||
|
||||
const health = daemon.kernel.getHealth();
|
||||
expect(health.activeGroups).toBeGreaterThan(0);
|
||||
expect(daemon.kernel.getWorkerPid("e2e")).not.toBeNull();
|
||||
expect(existsSync(daemon.socketPath)).toBe(true);
|
||||
|
||||
const statusUp = await runCli(daemon, ["daemon", "status"]);
|
||||
expect(statusUp.exitCode).toBe(0);
|
||||
expect(statusUp.stdout).toContain("running");
|
||||
expect(statusUp.stdout).toContain("✅ Nerve daemon is running.");
|
||||
|
||||
const statusTopLevel = await runCli(daemon, ["status"]);
|
||||
expect(statusTopLevel.exitCode).toBe(0);
|
||||
expect(statusTopLevel.stdout).toContain("running");
|
||||
|
||||
await daemon.kernel.stop();
|
||||
kernelAlreadyStopped = true;
|
||||
unlinkSync(pidPath);
|
||||
|
||||
expect(existsSync(pidPath)).toBe(false);
|
||||
expect(existsSync(daemon.socketPath)).toBe(false);
|
||||
|
||||
const statusDown = await runCli(daemon, ["daemon", "status"]);
|
||||
expect(statusDown.exitCode).toBe(0);
|
||||
expect(statusDown.stdout).toContain("not running");
|
||||
expect(statusDown.stdout).toContain("😴 Nerve daemon is not running.");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -47,7 +47,11 @@ import { createKernel } from "@uncaged/nerve-daemon";
|
||||
import type { Kernel } from "@uncaged/nerve-daemon";
|
||||
import { defineCommand, runCommand } from "citty";
|
||||
|
||||
import { daemonCommand } from "../commands/daemon.js";
|
||||
import { logsCommand } from "../commands/logs.js";
|
||||
import { senseCommand } from "../commands/sense.js";
|
||||
import { statusCommand } from "../commands/status.js";
|
||||
import { stopCommand } from "../commands/stop.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const nerveDaemonRoot = dirname(require.resolve("@uncaged/nerve-daemon/package.json"));
|
||||
@@ -90,6 +94,10 @@ const e2eRootCommand = defineCommand({
|
||||
meta: { name: "nerve", description: "e2e" },
|
||||
subCommands: {
|
||||
sense: senseCommand,
|
||||
logs: logsCommand,
|
||||
daemon: daemonCommand,
|
||||
status: statusCommand,
|
||||
stop: stopCommand,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -186,9 +194,19 @@ export async function startTestDaemon(
|
||||
return { fakeHome, nerveRoot, socketPath, kernel };
|
||||
}
|
||||
|
||||
/** Stops the kernel (workers + IPC) and removes the temp HOME tree. */
|
||||
export async function stopTestDaemon(handle: TestDaemonHandle): Promise<void> {
|
||||
await handle.kernel.stop();
|
||||
/**
|
||||
* Stops the kernel (workers + IPC) and removes the temp HOME tree.
|
||||
*
|
||||
* @param kernelAlreadyStopped — pass `true` when the test already called `kernel.stop()`
|
||||
* (e.g. daemon lifecycle e2e); only the temp directory is removed.
|
||||
*/
|
||||
export async function stopTestDaemon(
|
||||
handle: TestDaemonHandle,
|
||||
kernelAlreadyStopped = false,
|
||||
): Promise<void> {
|
||||
if (!kernelAlreadyStopped) {
|
||||
await handle.kernel.stop();
|
||||
}
|
||||
try {
|
||||
rmSync(handle.fakeHome, { recursive: true, force: true });
|
||||
} catch {
|
||||
@@ -235,10 +253,10 @@ function patchWriteStream(stream: NodeJS.WriteStream, sink: string[]): () => voi
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `nerve <args>` for the subset wired here (`sense` subcommands), with
|
||||
* `process.env.HOME` pointing at `handle.fakeHome` so `getNerveRoot()` resolves to the
|
||||
* test workspace. Captures stdout/stderr; sets `exitCode` when `process.exit` is invoked
|
||||
* or on thrown errors.
|
||||
* Runs `nerve <args>` for the subset wired in `e2eRootCommand` (`sense`, `logs`, `daemon`,
|
||||
* top-level `status` / `stop`), with `process.env.HOME` pointing at `handle.fakeHome` so
|
||||
* `getNerveRoot()` resolves to the test workspace. Captures stdout/stderr; sets `exitCode`
|
||||
* when `process.exit` is invoked or on thrown errors.
|
||||
*/
|
||||
export async function runCli(handle: TestDaemonHandle, args: string[]): Promise<CliRunResult> {
|
||||
const stdoutChunks: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user