mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 12:11:23 +00:00
refactor(core): dedupe gateway runtime and config tests
This commit is contained in:
@@ -14,6 +14,62 @@ vi.mock("./install-source-utils.js", async (importOriginal) => {
|
||||
});
|
||||
|
||||
describe("installFromNpmSpecArchive", () => {
|
||||
const baseSpec = "@openclaw/test@1.0.0";
|
||||
const baseArchivePath = "/tmp/openclaw-test.tgz";
|
||||
|
||||
const mockPackedSuccess = (overrides?: {
|
||||
resolvedSpec?: string;
|
||||
integrity?: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
}) => {
|
||||
vi.mocked(packNpmSpecToArchive).mockResolvedValue({
|
||||
ok: true,
|
||||
archivePath: baseArchivePath,
|
||||
metadata: {
|
||||
resolvedSpec: overrides?.resolvedSpec ?? baseSpec,
|
||||
integrity: overrides?.integrity ?? "sha512-same",
|
||||
...(overrides?.name ? { name: overrides.name } : {}),
|
||||
...(overrides?.version ? { version: overrides.version } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const runInstall = async (overrides: {
|
||||
expectedIntegrity?: string;
|
||||
onIntegrityDrift?: (payload: {
|
||||
spec: string;
|
||||
expectedIntegrity: string;
|
||||
actualIntegrity: string;
|
||||
resolvedSpec: string;
|
||||
}) => boolean | Promise<boolean>;
|
||||
warn?: (message: string) => void;
|
||||
installFromArchive: (params: {
|
||||
archivePath: string;
|
||||
}) => Promise<{ ok: boolean; [k: string]: unknown }>;
|
||||
}) =>
|
||||
await installFromNpmSpecArchive({
|
||||
tempDirPrefix: "openclaw-test-",
|
||||
spec: baseSpec,
|
||||
timeoutMs: 1000,
|
||||
expectedIntegrity: overrides.expectedIntegrity,
|
||||
onIntegrityDrift: overrides.onIntegrityDrift,
|
||||
warn: overrides.warn,
|
||||
installFromArchive: overrides.installFromArchive,
|
||||
});
|
||||
|
||||
const expectWrappedOkResult = (
|
||||
result: Awaited<ReturnType<typeof runInstall>>,
|
||||
installResult: Record<string, unknown>,
|
||||
) => {
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
throw new Error("expected ok result");
|
||||
}
|
||||
expect(result.installResult).toEqual(installResult);
|
||||
return result;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(packNpmSpecToArchive).mockReset();
|
||||
vi.mocked(withTempDir).mockClear();
|
||||
@@ -36,52 +92,45 @@ describe("installFromNpmSpecArchive", () => {
|
||||
});
|
||||
|
||||
it("returns resolution metadata and installer result on success", async () => {
|
||||
vi.mocked(packNpmSpecToArchive).mockResolvedValue({
|
||||
ok: true,
|
||||
archivePath: "/tmp/openclaw-test.tgz",
|
||||
metadata: {
|
||||
name: "@openclaw/test",
|
||||
version: "1.0.0",
|
||||
resolvedSpec: "@openclaw/test@1.0.0",
|
||||
integrity: "sha512-same",
|
||||
},
|
||||
});
|
||||
mockPackedSuccess({ name: "@openclaw/test", version: "1.0.0" });
|
||||
const installFromArchive = vi.fn(async () => ({ ok: true as const, target: "done" }));
|
||||
|
||||
const result = await installFromNpmSpecArchive({
|
||||
tempDirPrefix: "openclaw-test-",
|
||||
spec: "@openclaw/test@1.0.0",
|
||||
timeoutMs: 1000,
|
||||
const result = await runInstall({
|
||||
expectedIntegrity: "sha512-same",
|
||||
installFromArchive,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.installResult).toEqual({ ok: true, target: "done" });
|
||||
expect(result.integrityDrift).toBeUndefined();
|
||||
expect(result.npmResolution.resolvedSpec).toBe("@openclaw/test@1.0.0");
|
||||
expect(result.npmResolution.resolvedAt).toBeTruthy();
|
||||
const okResult = expectWrappedOkResult(result, { ok: true, target: "done" });
|
||||
expect(okResult.integrityDrift).toBeUndefined();
|
||||
expect(okResult.npmResolution.resolvedSpec).toBe("@openclaw/test@1.0.0");
|
||||
expect(okResult.npmResolution.resolvedAt).toBeTruthy();
|
||||
expect(installFromArchive).toHaveBeenCalledWith({ archivePath: "/tmp/openclaw-test.tgz" });
|
||||
});
|
||||
|
||||
it("aborts when integrity drift callback rejects drift", async () => {
|
||||
vi.mocked(packNpmSpecToArchive).mockResolvedValue({
|
||||
ok: true,
|
||||
archivePath: "/tmp/openclaw-test.tgz",
|
||||
metadata: {
|
||||
resolvedSpec: "@openclaw/test@1.0.0",
|
||||
integrity: "sha512-new",
|
||||
},
|
||||
it("proceeds when integrity drift callback accepts drift", async () => {
|
||||
mockPackedSuccess({ integrity: "sha512-new" });
|
||||
const onIntegrityDrift = vi.fn(async () => true);
|
||||
const installFromArchive = vi.fn(async () => ({ ok: true as const, id: "plugin-accept" }));
|
||||
|
||||
const result = await runInstall({
|
||||
expectedIntegrity: "sha512-old",
|
||||
onIntegrityDrift,
|
||||
installFromArchive,
|
||||
});
|
||||
|
||||
const okResult = expectWrappedOkResult(result, { ok: true, id: "plugin-accept" });
|
||||
expect(okResult.integrityDrift).toEqual({
|
||||
expectedIntegrity: "sha512-old",
|
||||
actualIntegrity: "sha512-new",
|
||||
});
|
||||
expect(onIntegrityDrift).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("aborts when integrity drift callback rejects drift", async () => {
|
||||
mockPackedSuccess({ integrity: "sha512-new" });
|
||||
const installFromArchive = vi.fn(async () => ({ ok: true as const }));
|
||||
|
||||
const result = await installFromNpmSpecArchive({
|
||||
tempDirPrefix: "openclaw-test-",
|
||||
spec: "@openclaw/test@1.0.0",
|
||||
timeoutMs: 1000,
|
||||
const result = await runInstall({
|
||||
expectedIntegrity: "sha512-old",
|
||||
onIntegrityDrift: async () => false,
|
||||
installFromArchive,
|
||||
@@ -95,32 +144,18 @@ describe("installFromNpmSpecArchive", () => {
|
||||
});
|
||||
|
||||
it("warns and proceeds on drift when no callback is configured", async () => {
|
||||
vi.mocked(packNpmSpecToArchive).mockResolvedValue({
|
||||
ok: true,
|
||||
archivePath: "/tmp/openclaw-test.tgz",
|
||||
metadata: {
|
||||
resolvedSpec: "@openclaw/test@1.0.0",
|
||||
integrity: "sha512-new",
|
||||
},
|
||||
});
|
||||
mockPackedSuccess({ integrity: "sha512-new" });
|
||||
const warn = vi.fn();
|
||||
const installFromArchive = vi.fn(async () => ({ ok: true as const, id: "plugin-1" }));
|
||||
|
||||
const result = await installFromNpmSpecArchive({
|
||||
tempDirPrefix: "openclaw-test-",
|
||||
spec: "@openclaw/test@1.0.0",
|
||||
timeoutMs: 1000,
|
||||
const result = await runInstall({
|
||||
expectedIntegrity: "sha512-old",
|
||||
warn,
|
||||
installFromArchive,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.installResult).toEqual({ ok: true, id: "plugin-1" });
|
||||
expect(result.integrityDrift).toEqual({
|
||||
const okResult = expectWrappedOkResult(result, { ok: true, id: "plugin-1" });
|
||||
expect(okResult.integrityDrift).toEqual({
|
||||
expectedIntegrity: "sha512-old",
|
||||
actualIntegrity: "sha512-new",
|
||||
});
|
||||
@@ -130,26 +165,15 @@ describe("installFromNpmSpecArchive", () => {
|
||||
});
|
||||
|
||||
it("returns installer failures to callers for domain-specific handling", async () => {
|
||||
vi.mocked(packNpmSpecToArchive).mockResolvedValue({
|
||||
ok: true,
|
||||
archivePath: "/tmp/openclaw-test.tgz",
|
||||
metadata: { resolvedSpec: "@openclaw/test@1.0.0", integrity: "sha512-same" },
|
||||
});
|
||||
mockPackedSuccess({ integrity: "sha512-same" });
|
||||
const installFromArchive = vi.fn(async () => ({ ok: false as const, error: "install failed" }));
|
||||
|
||||
const result = await installFromNpmSpecArchive({
|
||||
tempDirPrefix: "openclaw-test-",
|
||||
spec: "@openclaw/test@1.0.0",
|
||||
timeoutMs: 1000,
|
||||
const result = await runInstall({
|
||||
expectedIntegrity: "sha512-same",
|
||||
installFromArchive,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.installResult).toEqual({ ok: false, error: "install failed" });
|
||||
expect(result.integrityDrift).toBeUndefined();
|
||||
const okResult = expectWrappedOkResult(result, { ok: false, error: "install failed" });
|
||||
expect(okResult.integrityDrift).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { retryAsync } from "./retry.js";
|
||||
|
||||
describe("retryAsync", () => {
|
||||
async function runRetryAfterCase(options: {
|
||||
maxDelayMs: number;
|
||||
retryAfterMs: number;
|
||||
expectedDelayMs: number;
|
||||
}) {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const fn = vi.fn().mockRejectedValueOnce(new Error("boom")).mockResolvedValueOnce("ok");
|
||||
const delays: number[] = [];
|
||||
const promise = retryAsync(fn, {
|
||||
attempts: 2,
|
||||
minDelayMs: 0,
|
||||
maxDelayMs: options.maxDelayMs,
|
||||
jitter: 0,
|
||||
retryAfterMs: () => options.retryAfterMs,
|
||||
onRetry: (info) => delays.push(info.delayMs),
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe("ok");
|
||||
expect(delays[0]).toBe(options.expectedDelayMs);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
async function runRetryAfterCase(params: {
|
||||
minDelayMs: number;
|
||||
maxDelayMs: number;
|
||||
retryAfterMs: number;
|
||||
}): Promise<number[]> {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const fn = vi.fn().mockRejectedValueOnce(new Error("boom")).mockResolvedValueOnce("ok");
|
||||
const delays: number[] = [];
|
||||
const promise = retryAsync(fn, {
|
||||
attempts: 2,
|
||||
minDelayMs: params.minDelayMs,
|
||||
maxDelayMs: params.maxDelayMs,
|
||||
jitter: 0,
|
||||
retryAfterMs: () => params.retryAfterMs,
|
||||
onRetry: (info) => delays.push(info.delayMs),
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe("ok");
|
||||
return delays;
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}
|
||||
|
||||
describe("retryAsync", () => {
|
||||
it("returns on first success", async () => {
|
||||
const fn = vi.fn().mockResolvedValue("ok");
|
||||
const result = await retryAsync(fn, 3, 10);
|
||||
@@ -74,20 +74,18 @@ describe("retryAsync", () => {
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "uses retryAfterMs when provided",
|
||||
maxDelayMs: 1000,
|
||||
retryAfterMs: 500,
|
||||
expectedDelayMs: 500,
|
||||
},
|
||||
{
|
||||
name: "clamps retryAfterMs to maxDelayMs",
|
||||
maxDelayMs: 100,
|
||||
retryAfterMs: 500,
|
||||
expectedDelayMs: 100,
|
||||
},
|
||||
])("$name", async ({ maxDelayMs, retryAfterMs, expectedDelayMs }) => {
|
||||
await runRetryAfterCase({ maxDelayMs, retryAfterMs, expectedDelayMs });
|
||||
it("uses retryAfterMs when provided", async () => {
|
||||
const delays = await runRetryAfterCase({ minDelayMs: 0, maxDelayMs: 1000, retryAfterMs: 500 });
|
||||
expect(delays[0]).toBe(500);
|
||||
});
|
||||
|
||||
it("clamps retryAfterMs to maxDelayMs", async () => {
|
||||
const delays = await runRetryAfterCase({ minDelayMs: 0, maxDelayMs: 100, retryAfterMs: 500 });
|
||||
expect(delays[0]).toBe(100);
|
||||
});
|
||||
|
||||
it("clamps retryAfterMs to minDelayMs", async () => {
|
||||
const delays = await runRetryAfterCase({ minDelayMs: 250, maxDelayMs: 1000, retryAfterMs: 50 });
|
||||
expect(delays[0]).toBe(250);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,16 @@ import {
|
||||
} from "./system-run-command.js";
|
||||
|
||||
describe("system run command helpers", () => {
|
||||
function expectRawCommandMismatch(params: { argv: string[]; rawCommand: string }) {
|
||||
const res = validateSystemRunCommandConsistency(params);
|
||||
expect(res.ok).toBe(false);
|
||||
if (res.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(res.message).toContain("rawCommand does not match command");
|
||||
expect(res.details?.code).toBe("RAW_COMMAND_MISMATCH");
|
||||
}
|
||||
|
||||
test("formatExecCommand quotes args with spaces", () => {
|
||||
expect(formatExecCommand(["echo", "hi there"])).toBe('echo "hi there"');
|
||||
});
|
||||
@@ -39,16 +49,10 @@ describe("system run command helpers", () => {
|
||||
});
|
||||
|
||||
test("validateSystemRunCommandConsistency rejects mismatched rawCommand vs direct argv", () => {
|
||||
const res = validateSystemRunCommandConsistency({
|
||||
expectRawCommandMismatch({
|
||||
argv: ["uname", "-a"],
|
||||
rawCommand: "echo hi",
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (res.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(res.message).toContain("rawCommand does not match command");
|
||||
expect(res.details?.code).toBe("RAW_COMMAND_MISMATCH");
|
||||
});
|
||||
|
||||
test("validateSystemRunCommandConsistency accepts rawCommand matching sh wrapper argv", () => {
|
||||
@@ -60,16 +64,17 @@ describe("system run command helpers", () => {
|
||||
});
|
||||
|
||||
test("validateSystemRunCommandConsistency rejects cmd.exe /c trailing-arg smuggling", () => {
|
||||
const res = validateSystemRunCommandConsistency({
|
||||
expectRawCommandMismatch({
|
||||
argv: ["cmd.exe", "/d", "/s", "/c", "echo", "SAFE&&whoami"],
|
||||
rawCommand: "echo",
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (res.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(res.message).toContain("rawCommand does not match command");
|
||||
expect(res.details?.code).toBe("RAW_COMMAND_MISMATCH");
|
||||
});
|
||||
|
||||
test("validateSystemRunCommandConsistency rejects mismatched rawCommand vs sh wrapper argv", () => {
|
||||
expectRawCommandMismatch({
|
||||
argv: ["/bin/sh", "-lc", "echo hi"],
|
||||
rawCommand: "echo bye",
|
||||
});
|
||||
});
|
||||
|
||||
test("resolveSystemRunCommand requires command when rawCommand is present", () => {
|
||||
|
||||
@@ -12,6 +12,16 @@ const {
|
||||
} = tailscale;
|
||||
const tailscaleBin = expect.stringMatching(/tailscale$/i);
|
||||
|
||||
function createRuntimeWithExitError() {
|
||||
return {
|
||||
error: vi.fn(),
|
||||
log: vi.fn(),
|
||||
exit: ((code: number) => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}) as (code: number) => never,
|
||||
};
|
||||
}
|
||||
|
||||
describe("tailscale helpers", () => {
|
||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||
|
||||
@@ -46,31 +56,47 @@ describe("tailscale helpers", () => {
|
||||
it("ensureGoInstalled installs when missing and user agrees", async () => {
|
||||
const exec = vi.fn().mockRejectedValueOnce(new Error("no go")).mockResolvedValue({}); // brew install go
|
||||
const prompt = vi.fn().mockResolvedValue(true);
|
||||
const runtime = {
|
||||
error: vi.fn(),
|
||||
log: vi.fn(),
|
||||
exit: ((code: number) => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}) as (code: number) => never,
|
||||
};
|
||||
const runtime = createRuntimeWithExitError();
|
||||
await ensureGoInstalled(exec as never, prompt, runtime);
|
||||
expect(exec).toHaveBeenCalledWith("brew", ["install", "go"]);
|
||||
});
|
||||
|
||||
it("ensureGoInstalled exits when missing and user declines install", async () => {
|
||||
const exec = vi.fn().mockRejectedValueOnce(new Error("no go"));
|
||||
const prompt = vi.fn().mockResolvedValue(false);
|
||||
const runtime = createRuntimeWithExitError();
|
||||
|
||||
await expect(ensureGoInstalled(exec as never, prompt, runtime)).rejects.toThrow("exit 1");
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"Go is required to build tailscaled from source. Aborting.",
|
||||
);
|
||||
expect(exec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("ensureTailscaledInstalled installs when missing and user agrees", async () => {
|
||||
const exec = vi.fn().mockRejectedValueOnce(new Error("missing")).mockResolvedValue({});
|
||||
const prompt = vi.fn().mockResolvedValue(true);
|
||||
const runtime = {
|
||||
error: vi.fn(),
|
||||
log: vi.fn(),
|
||||
exit: ((code: number) => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}) as (code: number) => never,
|
||||
};
|
||||
const runtime = createRuntimeWithExitError();
|
||||
await ensureTailscaledInstalled(exec as never, prompt, runtime);
|
||||
expect(exec).toHaveBeenCalledWith("brew", ["install", "tailscale"]);
|
||||
});
|
||||
|
||||
it("ensureTailscaledInstalled exits when missing and user declines install", async () => {
|
||||
const exec = vi.fn().mockRejectedValueOnce(new Error("missing"));
|
||||
const prompt = vi.fn().mockResolvedValue(false);
|
||||
const runtime = createRuntimeWithExitError();
|
||||
|
||||
await expect(ensureTailscaledInstalled(exec as never, prompt, runtime)).rejects.toThrow(
|
||||
"exit 1",
|
||||
);
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"tailscaled is required for user-space funnel. Aborting.",
|
||||
);
|
||||
expect(exec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("enableTailscaleServe attempts normal first, then sudo", async () => {
|
||||
// 1. First attempt fails
|
||||
// 2. Second attempt (sudo) succeeds
|
||||
|
||||
@@ -37,6 +37,16 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
|
||||
process.exit = originalExit;
|
||||
});
|
||||
|
||||
function emitUnhandled(reason: unknown): void {
|
||||
process.emit("unhandledRejection", reason, Promise.resolve());
|
||||
}
|
||||
|
||||
function expectExitCodeFromUnhandled(reason: unknown, expected: number[]): void {
|
||||
exitCalls = [];
|
||||
emitUnhandled(reason);
|
||||
expect(exitCalls).toEqual(expected);
|
||||
}
|
||||
|
||||
describe("fatal errors", () => {
|
||||
it("exits on fatal runtime codes", () => {
|
||||
const fatalCases = [
|
||||
@@ -46,10 +56,7 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
|
||||
] as const;
|
||||
|
||||
for (const { code, message } of fatalCases) {
|
||||
exitCalls = [];
|
||||
const err = Object.assign(new Error(message), { code });
|
||||
process.emit("unhandledRejection", err, Promise.resolve());
|
||||
expect(exitCalls).toEqual([1]);
|
||||
expectExitCodeFromUnhandled(Object.assign(new Error(message), { code }), [1]);
|
||||
}
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
@@ -67,10 +74,7 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
|
||||
] as const;
|
||||
|
||||
for (const { code, message } of configurationCases) {
|
||||
exitCalls = [];
|
||||
const err = Object.assign(new Error(message), { code });
|
||||
process.emit("unhandledRejection", err, Promise.resolve());
|
||||
expect(exitCalls).toEqual([1]);
|
||||
expectExitCodeFromUnhandled(Object.assign(new Error(message), { code }), [1]);
|
||||
}
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
@@ -92,9 +96,7 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
|
||||
];
|
||||
|
||||
for (const transientErr of transientCases) {
|
||||
exitCalls = [];
|
||||
process.emit("unhandledRejection", transientErr, Promise.resolve());
|
||||
expect(exitCalls).toEqual([]);
|
||||
expectExitCodeFromUnhandled(transientErr, []);
|
||||
}
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
@@ -106,13 +108,22 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
|
||||
it("exits on generic errors without code", () => {
|
||||
const genericErr = new Error("Something went wrong");
|
||||
|
||||
process.emit("unhandledRejection", genericErr, Promise.resolve());
|
||||
|
||||
expect(exitCalls).toEqual([1]);
|
||||
expectExitCodeFromUnhandled(genericErr, [1]);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"[openclaw] Unhandled promise rejection:",
|
||||
expect.stringContaining("Something went wrong"),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not exit on AbortError and logs suppression warning", () => {
|
||||
const abortErr = new Error("This operation was aborted");
|
||||
abortErr.name = "AbortError";
|
||||
|
||||
expectExitCodeFromUnhandled(abortErr, []);
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
"[openclaw] Suppressed AbortError:",
|
||||
expect.stringContaining("This operation was aborted"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,13 +9,18 @@ const createFakeProcess = () =>
|
||||
execPath: "/usr/local/bin/node",
|
||||
}) as unknown as NodeJS.Process;
|
||||
|
||||
const createWatchHarness = () => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
kill: vi.fn(),
|
||||
});
|
||||
const spawn = vi.fn(() => child);
|
||||
const fakeProcess = createFakeProcess();
|
||||
return { child, spawn, fakeProcess };
|
||||
};
|
||||
|
||||
describe("watch-node script", () => {
|
||||
it("wires node watch to run-node with watched source/config paths", async () => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
kill: vi.fn(),
|
||||
});
|
||||
const spawn = vi.fn(() => child);
|
||||
const fakeProcess = createFakeProcess();
|
||||
const { child, spawn, fakeProcess } = createWatchHarness();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
args: ["gateway", "--force"],
|
||||
@@ -54,11 +59,7 @@ describe("watch-node script", () => {
|
||||
});
|
||||
|
||||
it("terminates child on SIGINT and returns shell interrupt code", async () => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
kill: vi.fn(),
|
||||
});
|
||||
const spawn = vi.fn(() => child);
|
||||
const fakeProcess = createFakeProcess();
|
||||
const { child, spawn, fakeProcess } = createWatchHarness();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
args: ["gateway", "--force"],
|
||||
@@ -74,4 +75,22 @@ describe("watch-node script", () => {
|
||||
expect(fakeProcess.listenerCount("SIGINT")).toBe(0);
|
||||
expect(fakeProcess.listenerCount("SIGTERM")).toBe(0);
|
||||
});
|
||||
|
||||
it("terminates child on SIGTERM and returns shell terminate code", async () => {
|
||||
const { child, spawn, fakeProcess } = createWatchHarness();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
args: ["gateway", "--force"],
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
|
||||
fakeProcess.emit("SIGTERM");
|
||||
const exitCode = await runPromise;
|
||||
|
||||
expect(exitCode).toBe(143);
|
||||
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(fakeProcess.listenerCount("SIGINT")).toBe(0);
|
||||
expect(fakeProcess.listenerCount("SIGTERM")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user