mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
7c70571683
* chore(lint): clean Android-Linux app assets batch * chore(lint): clean setup-launcher-plugin batch * chore(lint): clean changelog-updater batch * chore(lint): clean QA-runtime-helper batch * chore(lint): clean script-tests batch * fix(mxc): await sandbox spawn before bridge selection * fix(test): make fake plutil metacharacter escaping survive the template hop
172 lines
5.8 KiB
TypeScript
172 lines
5.8 KiB
TypeScript
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const launcherPath = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
"../src/mxc-spawn-launcher.mjs",
|
|
);
|
|
|
|
const loadLauncher = () =>
|
|
require(launcherPath) as {
|
|
decodePayload: (argv: string[]) => unknown;
|
|
exitOnChildProcessClose: (
|
|
child: {
|
|
on: (event: string, listener: (exitCode: number | null, signal?: string) => void) => void;
|
|
},
|
|
options?: { exit?: (code: number) => void },
|
|
) => void;
|
|
forwardSignals: (
|
|
spawned: { kill: (signal: string) => void },
|
|
options?: {
|
|
exit?: (code: number) => void;
|
|
exitGraceMs?: number;
|
|
setTimeout?: (callback: () => void, ms: number) => { unref?: () => void };
|
|
},
|
|
) => void;
|
|
launchSandbox: (
|
|
spawnSandboxFromConfig: (config: unknown, options: unknown) => unknown,
|
|
config: unknown,
|
|
options: unknown,
|
|
bridges?: {
|
|
child?: (spawned: unknown) => void;
|
|
pty?: (spawned: unknown) => void;
|
|
},
|
|
) => Promise<void>;
|
|
signalExitCode: (signal: number | string | undefined) => number;
|
|
};
|
|
|
|
describe("mxc-spawn-launcher", () => {
|
|
it("decodes a JSON --payload-file and removes it before spawning", () => {
|
|
const { decodePayload } = loadLauncher();
|
|
const dir = mkdtempSync(path.join(tmpdir(), "mxc-launcher-test-"));
|
|
const payloadFile = path.join(dir, "payload.json");
|
|
const body = { config: { process: { env: ["SECRET=value"] } }, options: {} };
|
|
try {
|
|
writeFileSync(payloadFile, JSON.stringify(body), "utf-8");
|
|
|
|
expect(decodePayload(["--payload-file", payloadFile])).toEqual(body);
|
|
expect(existsSync(payloadFile)).toBe(false);
|
|
expect(existsSync(dir)).toBe(false);
|
|
} finally {
|
|
rmSync(dir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("throws when --payload is missing", () => {
|
|
const { decodePayload } = loadLauncher();
|
|
|
|
expect(() => decodePayload([])).toThrow(/Missing --payload-file/);
|
|
});
|
|
|
|
it("maps PTY signal exits to process exit codes", () => {
|
|
const { signalExitCode } = loadLauncher();
|
|
|
|
expect(signalExitCode(15)).toBe(143);
|
|
expect(signalExitCode("SIGTERM")).toBe(143);
|
|
expect(signalExitCode("SIGINT")).toBe(130);
|
|
expect(signalExitCode("SIGUNKNOWN")).toBe(1);
|
|
expect(signalExitCode(undefined)).toBe(1);
|
|
});
|
|
|
|
it("resolves a promised PTY before selecting its bridge", async () => {
|
|
const { launchSandbox } = loadLauncher();
|
|
const pty = { onData: vi.fn() };
|
|
const spawnResult = Promise.resolve(pty);
|
|
const ptyBridge = vi.fn();
|
|
const childBridge = vi.fn();
|
|
|
|
await launchSandbox(() => spawnResult, { process: {} }, undefined, {
|
|
pty: ptyBridge,
|
|
child: childBridge,
|
|
});
|
|
|
|
expect(ptyBridge).toHaveBeenCalledWith(pty);
|
|
expect(ptyBridge).not.toHaveBeenCalledWith(spawnResult);
|
|
expect(childBridge).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resolves a promised child process before selecting its bridge", async () => {
|
|
const { launchSandbox } = loadLauncher();
|
|
const child = { on: vi.fn(), stdout: undefined, stderr: undefined };
|
|
const spawnResult = Promise.resolve(child);
|
|
const ptyBridge = vi.fn();
|
|
const childBridge = vi.fn();
|
|
|
|
await launchSandbox(
|
|
() => spawnResult,
|
|
{ process: {} },
|
|
{ usePty: false },
|
|
{
|
|
pty: ptyBridge,
|
|
child: childBridge,
|
|
},
|
|
);
|
|
|
|
expect(childBridge).toHaveBeenCalledWith(child);
|
|
expect(childBridge).not.toHaveBeenCalledWith(spawnResult);
|
|
expect(ptyBridge).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("forwards process termination signals to spawned sandbox children", () => {
|
|
const { forwardSignals } = loadLauncher();
|
|
const listeners = new Map<string, () => void>();
|
|
const processOn = vi.spyOn(process, "on").mockImplementation((event, listener) => {
|
|
if (typeof event === "string" && typeof listener === "function") {
|
|
listeners.set(event, listener as () => void);
|
|
}
|
|
return process;
|
|
});
|
|
const spawned = { kill: vi.fn() };
|
|
const exit = vi.fn();
|
|
const timers: Array<{ callback: () => void; ms: number; unref: ReturnType<typeof vi.fn> }> = [];
|
|
const setTimeoutMock = vi.fn((callback: () => void, ms: number) => {
|
|
const timer = { callback, ms, unref: vi.fn() };
|
|
timers.push(timer);
|
|
return timer;
|
|
});
|
|
try {
|
|
forwardSignals(spawned, { exit, exitGraceMs: 25, setTimeout: setTimeoutMock });
|
|
|
|
listeners.get("SIGTERM")?.();
|
|
listeners.get("SIGINT")?.();
|
|
|
|
expect(spawned.kill).toHaveBeenCalledWith("SIGTERM");
|
|
expect(spawned.kill).toHaveBeenCalledWith("SIGINT");
|
|
expect(setTimeoutMock).toHaveBeenCalledTimes(1);
|
|
expect(timers[0]?.ms).toBe(25);
|
|
expect(timers[0]?.unref).toHaveBeenCalledTimes(1);
|
|
|
|
timers[0]?.callback();
|
|
|
|
expect(exit).toHaveBeenCalledWith(143);
|
|
} finally {
|
|
processOn.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("exits non-PTY child processes after stdio close", () => {
|
|
const { exitOnChildProcessClose } = loadLauncher();
|
|
const listeners = new Map<string, (exitCode: number | null, signal?: string) => void>();
|
|
const child = {
|
|
on: vi.fn((event: string, listener: (exitCode: number | null, signal?: string) => void) => {
|
|
listeners.set(event, listener);
|
|
}),
|
|
};
|
|
const exit = vi.fn();
|
|
|
|
exitOnChildProcessClose(child, { exit });
|
|
|
|
expect(child.on).toHaveBeenCalledWith("close", expect.any(Function));
|
|
expect(child.on).not.toHaveBeenCalledWith("exit", expect.any(Function));
|
|
|
|
listeners.get("close")?.(null, "SIGTERM");
|
|
|
|
expect(exit).toHaveBeenCalledWith(143);
|
|
});
|
|
});
|