Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import type { ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
expectRealExitWinsOverSigkillFallback,
expectWaitStaysPendingUntilSigkillFallback,
} from "./test-support.js";
const { spawnWithFallbackMock, killProcessTreeMock } = vi.hoisted(() => ({
spawnWithFallbackMock: vi.fn(),
killProcessTreeMock: vi.fn(),
}));
vi.mock("../../spawn-utils.js", () => ({
spawnWithFallback: spawnWithFallbackMock,
}));
vi.mock("../../kill-tree.js", () => ({
killProcessTree: killProcessTreeMock,
}));
let createChildAdapter: typeof import("./child.js").createChildAdapter;
function createStubChild(pid = 1234) {
const child = new EventEmitter() as ChildProcess;
child.stdin = new PassThrough() as ChildProcess["stdin"];
child.stdout = new PassThrough() as ChildProcess["stdout"];
child.stderr = new PassThrough() as ChildProcess["stderr"];
Object.defineProperty(child, "pid", { value: pid, configurable: true });
Object.defineProperty(child, "killed", { value: false, configurable: true, writable: true });
Object.defineProperty(child, "exitCode", { value: null, configurable: true, writable: true });
Object.defineProperty(child, "signalCode", { value: null, configurable: true, writable: true });
const killMock = vi.fn(() => true);
child.kill = killMock as ChildProcess["kill"];
const emitClose = (code: number | null, signal: NodeJS.Signals | null = null) => {
child.emit("close", code, signal);
};
const emitExit = (code: number | null, signal: NodeJS.Signals | null = null) => {
Object.defineProperty(child, "exitCode", { value: code, configurable: true, writable: true });
Object.defineProperty(child, "signalCode", {
value: signal,
configurable: true,
writable: true,
});
child.emit("exit", code, signal);
};
return { child, killMock, emitClose, emitExit };
}
async function createAdapterHarness(params?: {
pid?: number;
argv?: string[];
env?: NodeJS.ProcessEnv;
}) {
const { child, killMock } = createStubChild(params?.pid);
spawnWithFallbackMock.mockResolvedValue({
child,
usedFallback: false,
});
const adapter = await createChildAdapter({
argv: params?.argv ?? ["node", "-e", "setTimeout(() => {}, 1000)"],
env: params?.env,
stdinMode: "pipe-open",
});
return { adapter, killMock };
}
describe("createChildAdapter", () => {
const originalServiceMarker = process.env.OPENCLAW_SERVICE_MARKER;
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
const setPlatform = (platform: NodeJS.Platform) => {
Object.defineProperty(process, "platform", {
configurable: true,
value: platform,
});
};
beforeAll(async () => {
({ createChildAdapter } = await import("./child.js"));
});
beforeEach(() => {
spawnWithFallbackMock.mockClear();
killProcessTreeMock.mockClear();
delete process.env.OPENCLAW_SERVICE_MARKER;
vi.useRealTimers();
});
afterAll(() => {
if (originalServiceMarker === undefined) {
delete process.env.OPENCLAW_SERVICE_MARKER;
} else {
process.env.OPENCLAW_SERVICE_MARKER = originalServiceMarker;
}
});
afterEach(() => {
if (originalPlatformDescriptor) {
Object.defineProperty(process, "platform", originalPlatformDescriptor);
}
vi.useRealTimers();
});
it("uses process-tree kill for default SIGKILL", async () => {
const { adapter, killMock } = await createAdapterHarness({ pid: 4321 });
const spawnArgs = spawnWithFallbackMock.mock.calls[0]?.[0] as {
options?: { detached?: boolean };
fallbacks?: Array<{ options?: { detached?: boolean } }>;
};
// On Windows, detached defaults to false (headless Scheduled Task compat);
// on POSIX, detached is true with a no-detach fallback.
if (process.platform === "win32") {
expect(spawnArgs.options?.detached).toBe(false);
expect(spawnArgs.fallbacks).toEqual([]);
} else {
expect(spawnArgs.options?.detached).toBe(true);
expect(spawnArgs.fallbacks?.[0]?.options?.detached).toBe(false);
}
adapter.kill();
expect(killProcessTreeMock).toHaveBeenCalledWith(4321);
expect(killMock).toHaveBeenCalledWith("SIGKILL");
});
it("uses direct child.kill for non-SIGKILL signals", async () => {
const { adapter, killMock } = await createAdapterHarness({ pid: 7654 });
adapter.kill("SIGTERM");
expect(killProcessTreeMock).not.toHaveBeenCalled();
expect(killMock).toHaveBeenCalledWith("SIGTERM");
});
it("wait does not settle immediately on SIGKILL", async () => {
vi.useFakeTimers();
const { adapter } = await createAdapterHarness({ pid: 4567 });
await expectWaitStaysPendingUntilSigkillFallback(adapter.wait(), () => {
adapter.kill();
});
});
it("prefers real child close over the SIGKILL fallback settle", async () => {
vi.useFakeTimers();
const { adapter, emitClose, killMock } = await (async () => {
const stub = createStubChild(2468);
spawnWithFallbackMock.mockResolvedValue({
child: stub.child,
usedFallback: false,
});
const adapter = await createChildAdapter({
argv: ["node", "-e", "setTimeout(() => {}, 1000)"],
stdinMode: "pipe-open",
});
return { ...stub, adapter };
})();
await expectRealExitWinsOverSigkillFallback({
waitPromise: adapter.wait(),
triggerKill: () => {
adapter.kill();
},
emitExit: () => {
emitClose(0, "SIGKILL");
},
expected: { code: 0, signal: "SIGKILL" },
});
expect(killMock).toHaveBeenCalledWith("SIGKILL");
});
it("settles wait from exit state on Windows even when close never arrives", async () => {
vi.useFakeTimers();
setPlatform("win32");
const { adapter, emitExit, child } = await (async () => {
const stub = createStubChild(8642);
spawnWithFallbackMock.mockResolvedValue({
child: stub.child,
usedFallback: false,
});
const adapter = await createChildAdapter({
argv: ["openclaw", "version"],
stdinMode: "pipe-closed",
});
return { ...stub, adapter };
})();
const settled = vi.fn();
void adapter.wait().then((result) => {
settled(result);
});
emitExit(0, null);
child.stdout?.emit("end");
child.stderr?.emit("end");
await vi.advanceTimersByTimeAsync(300);
expect(settled).toHaveBeenCalledWith({ code: 0, signal: null });
});
it("disables detached mode in service-managed runtime", async () => {
process.env.OPENCLAW_SERVICE_MARKER = "openclaw";
await createAdapterHarness({ pid: 7777 });
const spawnArgs = spawnWithFallbackMock.mock.calls[0]?.[0] as {
options?: { detached?: boolean };
fallbacks?: Array<{ options?: { detached?: boolean } }>;
};
expect(spawnArgs.options?.detached).toBe(false);
expect(spawnArgs.fallbacks ?? []).toEqual([]);
});
it("keeps inherited env when no override env is provided on non-Linux", async () => {
setPlatform("darwin");
await createAdapterHarness({
pid: 3333,
argv: ["node", "-e", "process.exit(0)"],
});
const spawnArgs = spawnWithFallbackMock.mock.calls[0]?.[0] as {
argv?: string[];
options?: { env?: NodeJS.ProcessEnv };
};
expect(spawnArgs.argv).toEqual(["node", "-e", "process.exit(0)"]);
expect(spawnArgs.options?.env).toBeUndefined();
});
it("wraps Linux child spawns and strips shell-init env", async () => {
const originalBashEnv = process.env.BASH_ENV;
const originalEnv = process.env.ENV;
const originalCdpath = process.env.CDPATH;
setPlatform("linux");
process.env.BASH_ENV = "/tmp/bashenv";
process.env.ENV = "/tmp/env";
process.env.CDPATH = "/tmp";
try {
await createAdapterHarness({
pid: 3334,
argv: ["/usr/bin/node", "-e", "process.exit(0)"],
});
} finally {
if (originalBashEnv === undefined) {
delete process.env.BASH_ENV;
} else {
process.env.BASH_ENV = originalBashEnv;
}
if (originalEnv === undefined) {
delete process.env.ENV;
} else {
process.env.ENV = originalEnv;
}
if (originalCdpath === undefined) {
delete process.env.CDPATH;
} else {
process.env.CDPATH = originalCdpath;
}
}
const spawnArgs = spawnWithFallbackMock.mock.calls[0]?.[0] as {
argv?: string[];
options?: { env?: NodeJS.ProcessEnv };
};
expect(spawnArgs.argv?.slice(0, 4)).toEqual([
"/bin/sh",
"-c",
'echo 1000 > /proc/self/oom_score_adj 2>/dev/null; exec "$0" "$@"',
"/usr/bin/node",
]);
expect(spawnArgs.argv?.slice(4)).toEqual(["-e", "process.exit(0)"]);
expect(spawnArgs.options?.env).toBeDefined();
expect(spawnArgs.options?.env?.BASH_ENV).toBeUndefined();
expect(spawnArgs.options?.env?.ENV).toBeUndefined();
expect(spawnArgs.options?.env?.CDPATH).toBeUndefined();
});
it("passes explicit env overrides as strings", async () => {
await createAdapterHarness({
pid: 4444,
argv: ["node", "-e", "process.exit(0)"],
env: { FOO: "bar", COUNT: "12", DROP_ME: undefined },
});
const spawnArgs = spawnWithFallbackMock.mock.calls[0]?.[0] as {
options?: { env?: Record<string, string> };
};
expect(spawnArgs.options?.env).toEqual({ FOO: "bar", COUNT: "12" });
});
});
¤ Dauer der Verarbeitung: 0.21 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|