This repository has been archived on 2026-08-23. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
pi-web/src/cli.test.ts
T

143 lines
5.1 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
agentCommandForChecks,
commandWithVersionCheck,
doctorExitCode,
isCliEntrypoint,
launchdRuntimeDetails,
regularFileExists,
serviceBackendForPlatform,
} from "./cli.js";
const originalShell = process.env["SHELL"];
const originalPiWebConfig = process.env["PI_WEB_CONFIG"];
const originalPiWebAgentCommand = process.env["PI_WEB_AGENT_COMMAND"];
afterEach(() => {
if (originalShell === undefined) {
delete process.env["SHELL"];
} else {
process.env["SHELL"] = originalShell;
}
if (originalPiWebConfig === undefined) {
delete process.env["PI_WEB_CONFIG"];
} else {
process.env["PI_WEB_CONFIG"] = originalPiWebConfig;
}
if (originalPiWebAgentCommand === undefined) {
delete process.env["PI_WEB_AGENT_COMMAND"];
} else {
process.env["PI_WEB_AGENT_COMMAND"] = originalPiWebAgentCommand;
}
});
describe("commandWithVersionCheck", () => {
it("emits a POSIX subshell group for bash", () => {
process.env["SHELL"] = "/bin/bash";
expect(commandWithVersionCheck("npm")).toBe("command -v 'npm' && ('npm' --version 2>&1 || true)");
});
it("emits a POSIX subshell group for zsh", () => {
process.env["SHELL"] = "/bin/zsh";
expect(commandWithVersionCheck("pi")).toBe("command -v 'pi' && ('pi' --version 2>&1 || true)");
});
it("uses fish begin/end grouping instead of a POSIX subshell", () => {
process.env["SHELL"] = "/usr/local/bin/fish";
const command = commandWithVersionCheck("npm");
expect(command).toBe("command -v 'npm' && begin; 'npm' --version 2>&1 || true; end");
expect(command).not.toContain("(");
});
it("shell-quotes command words", () => {
process.env["SHELL"] = "/bin/bash";
expect(commandWithVersionCheck("/tmp/agent's/acme-agent")).toBe("command -v '/tmp/agent'\\''s/acme-agent' && ('/tmp/agent'\\''s/acme-agent' --version 2>&1 || true)");
});
});
describe("agentCommandForChecks", () => {
it("reads the configured agent command for doctor checks", () => {
const dir = mkdtempSync(join(tmpdir(), "pi-web-cli-test-"));
try {
const configPath = join(dir, "config.json");
writeFileSync(configPath, `${JSON.stringify({ agent: { command: "acme-agent", dir: "/opt/acme-agent/state" } })}\n`);
process.env["PI_WEB_CONFIG"] = configPath;
delete process.env["PI_WEB_AGENT_COMMAND"];
expect(agentCommandForChecks()).toBe("acme-agent");
expect(agentCommandForChecks({
PI_WEB_CONFIG: configPath,
PI_WEB_AGENT_COMMAND: "environment-agent",
PI_WEB_AGENT_DIR: join(dir, "environment-agent-state"),
})).toBe("environment-agent");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
describe("native-service doctor CLI contracts", () => {
it("uses native services only on supported platforms", () => {
expect(serviceBackendForPlatform("linux")).toEqual({ kind: "systemd", label: "systemd user services" });
expect(serviceBackendForPlatform("darwin")).toEqual({ kind: "launchd", label: "LaunchAgents" });
expect(serviceBackendForPlatform("win32")).toBeUndefined();
});
it("fails doctor for general, native-plan, or node-pty failures", () => {
expect(doctorExitCode(true, true, true)).toBe(0);
expect(doctorExitCode(false, true, true)).toBe(1);
expect(doctorExitCode(true, false, true)).toBe(1);
expect(doctorExitCode(true, true, false)).toBe(1);
});
it("accepts only regular files as bundled entrypoints", () => {
const dir = mkdtempSync(join(tmpdir(), "pi-web-entrypoint-test-"));
try {
const file = join(dir, "entrypoint.js");
writeFileSync(file, "export {};\n");
expect(regularFileExists(file)).toBe(true);
expect(regularFileExists(dir)).toBe(false);
expect(regularFileExists(join(dir, "missing.js"))).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("surfaces launchd last exit code 127 in service status", () => {
expect(launchdRuntimeDetails("state = exited\nlast exit code = 127\n")).toEqual({
state: "exited",
detail: "exited (last exit code 127)",
pid: undefined,
});
});
});
describe("isCliEntrypoint", () => {
it("matches direct execution paths", () => {
expect(isCliEntrypoint("/tmp/pi-web-cli.js", "/tmp/pi-web-cli.js")).toBe(true);
});
it("matches npm-style symlinked bin entrypoints", () => {
const dir = mkdtempSync(join(tmpdir(), "pi-web-cli-test-"));
try {
const target = join(dir, "dist", "cli.js");
const symlink = join(dir, "bin", "pi-web");
mkdirSync(join(dir, "dist"));
mkdirSync(join(dir, "bin"));
writeFileSync(target, "#!/usr/bin/env node\n", { mode: 0o755 });
symlinkSync(target, symlink);
expect(isCliEntrypoint(symlink, target)).toBe(true);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("does not match unrelated paths", () => {
expect(isCliEntrypoint("/tmp/pi-web", "/tmp/other-pi-web")).toBe(false);
});
});