mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:30:43 +00:00
* fix: forward launcher respawn signals * docs: explain respawn signal exit timer * fix: centralize launcher respawn supervision * fix: include respawn helper in duplicate scan * fix: keep launcher respawn bridge local
234 lines
7.9 KiB
TypeScript
234 lines
7.9 KiB
TypeScript
import type { ChildProcess } from "node:child_process";
|
|
import { EventEmitter } from "node:events";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { cleanupTempDirs, makeTempDir } from "../test/helpers/temp-dir.js";
|
|
import {
|
|
buildOpenClawCompileCacheRespawnPlan,
|
|
isSourceCheckoutInstallRoot,
|
|
resolveOpenClawCompileCacheDirectory,
|
|
resolveEntryInstallRoot,
|
|
runOpenClawCompileCacheRespawnPlan,
|
|
shouldEnableOpenClawCompileCache,
|
|
} from "./entry.compile-cache.js";
|
|
|
|
describe("entry compile cache", () => {
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
cleanupTempDirs(tempDirs);
|
|
});
|
|
|
|
it("resolves install roots from source and dist entry paths", () => {
|
|
expect(resolveEntryInstallRoot("/repo/openclaw/src/entry.ts")).toBe("/repo/openclaw");
|
|
expect(resolveEntryInstallRoot("/repo/openclaw/dist/entry.js")).toBe("/repo/openclaw");
|
|
expect(resolveEntryInstallRoot("/pkg/openclaw/entry.js")).toBe("/pkg/openclaw");
|
|
});
|
|
|
|
it("treats git and source entry markers as source checkouts", async () => {
|
|
const root = makeTempDir(tempDirs, "openclaw-compile-cache-source-");
|
|
await fs.writeFile(path.join(root, ".git"), "gitdir: .git/worktrees/openclaw\n", "utf8");
|
|
|
|
expect(isSourceCheckoutInstallRoot(root)).toBe(true);
|
|
});
|
|
|
|
it("disables compile cache for source-checkout installs", async () => {
|
|
const root = makeTempDir(tempDirs, "openclaw-compile-cache-src-entry-");
|
|
await fs.mkdir(path.join(root, "src"), { recursive: true });
|
|
await fs.writeFile(path.join(root, "src", "entry.ts"), "export {};\n", "utf8");
|
|
|
|
expect(
|
|
shouldEnableOpenClawCompileCache({
|
|
env: {},
|
|
installRoot: root,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("keeps compile cache enabled for packaged installs unless disabled by env", () => {
|
|
const root = makeTempDir(tempDirs, "openclaw-compile-cache-package-");
|
|
|
|
expect(shouldEnableOpenClawCompileCache({ env: {}, installRoot: root })).toBe(true);
|
|
expect(
|
|
shouldEnableOpenClawCompileCache({
|
|
env: { NODE_DISABLE_COMPILE_CACHE: "1" },
|
|
installRoot: root,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("scopes packaged compile cache by package install metadata", async () => {
|
|
const root = makeTempDir(tempDirs, "openclaw-compile-cache-package-key-");
|
|
const packageJsonPath = path.join(root, "package.json");
|
|
await fs.writeFile(packageJsonPath, '{"version":"2026.4.29"}\n', "utf8");
|
|
|
|
const directory = resolveOpenClawCompileCacheDirectory({
|
|
env: { NODE_COMPILE_CACHE: path.join(root, ".node-cache") },
|
|
installRoot: root,
|
|
});
|
|
|
|
expect(directory).toContain(path.join(".node-cache", "openclaw"));
|
|
expect(directory).toContain("2026.4.29");
|
|
expect(path.basename(directory)).toMatch(/^\d+-\d+$/);
|
|
});
|
|
|
|
it("builds a one-shot no-cache respawn plan when source checkout inherits NODE_COMPILE_CACHE", async () => {
|
|
const root = makeTempDir(tempDirs, "openclaw-compile-cache-respawn-");
|
|
await fs.mkdir(path.join(root, "src"), { recursive: true });
|
|
await fs.writeFile(path.join(root, "src", "entry.ts"), "export {};\n", "utf8");
|
|
|
|
const plan = buildOpenClawCompileCacheRespawnPlan({
|
|
currentFile: path.join(root, "dist", "entry.js"),
|
|
env: { NODE_COMPILE_CACHE: "/tmp/openclaw-cache" },
|
|
execArgv: ["--no-warnings"],
|
|
execPath: "/usr/bin/node",
|
|
installRoot: root,
|
|
argv: ["/usr/bin/node", path.join(root, "dist", "entry.js"), "status", "--json"],
|
|
});
|
|
|
|
expect(plan).toEqual({
|
|
command: "/usr/bin/node",
|
|
args: ["--no-warnings", path.join(root, "dist", "entry.js"), "status", "--json"],
|
|
env: {
|
|
NODE_DISABLE_COMPILE_CACHE: "1",
|
|
OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED: "1",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("does not respawn packaged installs when NODE_COMPILE_CACHE is configured", () => {
|
|
const root = makeTempDir(tempDirs, "openclaw-compile-cache-package-respawn-");
|
|
|
|
expect(
|
|
buildOpenClawCompileCacheRespawnPlan({
|
|
currentFile: path.join(root, "dist", "entry.js"),
|
|
env: { NODE_COMPILE_CACHE: "/tmp/openclaw-cache" },
|
|
installRoot: root,
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("does not respawn source checkouts twice", async () => {
|
|
const root = makeTempDir(tempDirs, "openclaw-compile-cache-respawn-once-");
|
|
await fs.mkdir(path.join(root, "src"), { recursive: true });
|
|
await fs.writeFile(path.join(root, "src", "entry.ts"), "export {};\n", "utf8");
|
|
|
|
expect(
|
|
buildOpenClawCompileCacheRespawnPlan({
|
|
currentFile: path.join(root, "dist", "entry.js"),
|
|
env: {
|
|
NODE_COMPILE_CACHE: "/tmp/openclaw-cache",
|
|
OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED: "1",
|
|
},
|
|
installRoot: root,
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("runs compile-cache respawn plans with the child-process bridge", () => {
|
|
const child = new EventEmitter() as ChildProcess;
|
|
const spawn = vi.fn(() => child);
|
|
const attachChildProcessBridge = vi.fn();
|
|
const exit = vi.fn();
|
|
const writeError = vi.fn();
|
|
|
|
runOpenClawCompileCacheRespawnPlan(
|
|
{
|
|
command: "/usr/bin/node",
|
|
args: ["/repo/openclaw/dist/entry.js", "status"],
|
|
env: { NODE_DISABLE_COMPILE_CACHE: "1" },
|
|
},
|
|
{
|
|
spawn: spawn as unknown as typeof import("node:child_process").spawn,
|
|
attachChildProcessBridge,
|
|
exit: exit as unknown as (code?: number) => never,
|
|
writeError,
|
|
},
|
|
);
|
|
|
|
expect(spawn).toHaveBeenCalledWith(
|
|
"/usr/bin/node",
|
|
["/repo/openclaw/dist/entry.js", "status"],
|
|
{
|
|
stdio: "inherit",
|
|
env: { NODE_DISABLE_COMPILE_CACHE: "1" },
|
|
},
|
|
);
|
|
expect(attachChildProcessBridge).toHaveBeenCalledWith(child, {
|
|
onSignal: expect.any(Function),
|
|
});
|
|
|
|
child.emit("exit", 0, null);
|
|
|
|
expect(exit).toHaveBeenCalledWith(0);
|
|
expect(writeError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("marks signal-terminated compile-cache respawn children as failed without forcing another exit", () => {
|
|
const child = new EventEmitter() as ChildProcess;
|
|
const spawn = vi.fn(() => child);
|
|
const exit = vi.fn();
|
|
|
|
runOpenClawCompileCacheRespawnPlan(
|
|
{
|
|
command: "/usr/bin/node",
|
|
args: ["/repo/openclaw/dist/entry.js"],
|
|
env: {},
|
|
},
|
|
{
|
|
spawn: spawn as unknown as typeof import("node:child_process").spawn,
|
|
attachChildProcessBridge: vi.fn(),
|
|
exit: exit as unknown as (code?: number) => never,
|
|
writeError: vi.fn(),
|
|
},
|
|
);
|
|
|
|
child.emit("exit", null, "SIGTERM");
|
|
|
|
expect(exit).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("terminates before force-killing a signaled compile-cache respawn child", () => {
|
|
vi.useFakeTimers();
|
|
const child = new EventEmitter() as ChildProcess;
|
|
const kill = vi.fn<(signal?: NodeJS.Signals) => boolean>(() => true);
|
|
child.kill = kill as ChildProcess["kill"];
|
|
const spawn = vi.fn(() => child);
|
|
const exit = vi.fn();
|
|
let onSignal: ((signal: NodeJS.Signals) => void) | undefined;
|
|
|
|
try {
|
|
runOpenClawCompileCacheRespawnPlan(
|
|
{
|
|
command: "/usr/bin/node",
|
|
args: ["/repo/openclaw/dist/entry.js"],
|
|
env: {},
|
|
},
|
|
{
|
|
spawn: spawn as unknown as typeof import("node:child_process").spawn,
|
|
attachChildProcessBridge: vi.fn((_child, options) => {
|
|
onSignal = options?.onSignal;
|
|
return { detach: vi.fn() };
|
|
}),
|
|
exit: exit as unknown as (code?: number) => never,
|
|
writeError: vi.fn(),
|
|
},
|
|
);
|
|
|
|
onSignal?.("SIGTERM");
|
|
vi.advanceTimersByTime(1_000);
|
|
|
|
expect(kill).toHaveBeenCalledWith("SIGTERM");
|
|
expect(exit).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(1_000);
|
|
|
|
expect(kill).toHaveBeenCalledWith(process.platform === "win32" ? "SIGTERM" : "SIGKILL");
|
|
expect(exit).toHaveBeenCalledWith(1);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
});
|