mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-24 08:21:39 +00:00
Merged via squash.
Prepared head SHA: edd8ed8254
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
219 lines
7.8 KiB
TypeScript
219 lines
7.8 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { runNodeWatchedPaths } from "../../scripts/run-node.mjs";
|
|
import { runWatchMain } from "../../scripts/watch-node.mjs";
|
|
|
|
const createFakeProcess = () =>
|
|
Object.assign(new EventEmitter(), {
|
|
pid: 4242,
|
|
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 watcher = Object.assign(new EventEmitter(), {
|
|
close: vi.fn(async () => {}),
|
|
});
|
|
const createWatcher = vi.fn(() => watcher);
|
|
const fakeProcess = createFakeProcess();
|
|
return { child, spawn, watcher, createWatcher, fakeProcess };
|
|
};
|
|
|
|
describe("watch-node script", () => {
|
|
it("wires chokidar watch to run-node with watched source/config paths", async () => {
|
|
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
|
|
|
const runPromise = runWatchMain({
|
|
args: ["gateway", "--force"],
|
|
cwd: "/tmp/openclaw",
|
|
createWatcher,
|
|
env: { PATH: "/usr/bin" },
|
|
now: () => 1700000000000,
|
|
process: fakeProcess,
|
|
spawn,
|
|
});
|
|
|
|
expect(createWatcher).toHaveBeenCalledTimes(1);
|
|
const firstWatcherCall = createWatcher.mock.calls[0];
|
|
expect(firstWatcherCall).toBeDefined();
|
|
const [watchPaths, watchOptions] = firstWatcherCall as unknown as [
|
|
string[],
|
|
{ ignoreInitial: boolean; ignored: (watchPath: string) => boolean },
|
|
];
|
|
expect(watchPaths).toEqual(runNodeWatchedPaths);
|
|
expect(watchPaths).toContain("extensions");
|
|
expect(watchPaths).toContain("tsdown.config.ts");
|
|
expect(watchOptions.ignoreInitial).toBe(true);
|
|
expect(watchOptions.ignored("src/infra/watch-node.test.ts")).toBe(true);
|
|
expect(watchOptions.ignored("src/infra/watch-node.test.tsx")).toBe(true);
|
|
expect(watchOptions.ignored("src/infra/watch-node-test-helpers.ts")).toBe(true);
|
|
expect(watchOptions.ignored("extensions/voice-call/README.md")).toBe(true);
|
|
expect(watchOptions.ignored("extensions/voice-call/openclaw.plugin.json")).toBe(false);
|
|
expect(watchOptions.ignored("extensions/voice-call/package.json")).toBe(false);
|
|
expect(watchOptions.ignored("extensions/voice-call/index.ts")).toBe(false);
|
|
expect(watchOptions.ignored("extensions/voice-call/src/runtime.ts")).toBe(false);
|
|
expect(watchOptions.ignored("src/infra/watch-node.ts")).toBe(false);
|
|
expect(watchOptions.ignored("tsconfig.json")).toBe(false);
|
|
|
|
expect(spawn).toHaveBeenCalledTimes(1);
|
|
expect(spawn).toHaveBeenCalledWith(
|
|
"/usr/local/bin/node",
|
|
["scripts/run-node.mjs", "gateway", "--force"],
|
|
expect.objectContaining({
|
|
cwd: "/tmp/openclaw",
|
|
stdio: "inherit",
|
|
env: expect.objectContaining({
|
|
PATH: "/usr/bin",
|
|
OPENCLAW_WATCH_MODE: "1",
|
|
OPENCLAW_WATCH_SESSION: "1700000000000-4242",
|
|
OPENCLAW_WATCH_COMMAND: "gateway --force",
|
|
}),
|
|
}),
|
|
);
|
|
fakeProcess.emit("SIGINT");
|
|
const exitCode = await runPromise;
|
|
expect(exitCode).toBe(130);
|
|
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
|
expect(watcher.close).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("terminates child on SIGINT and returns shell interrupt code", async () => {
|
|
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
|
|
|
const runPromise = runWatchMain({
|
|
args: ["gateway", "--force"],
|
|
createWatcher,
|
|
process: fakeProcess,
|
|
spawn,
|
|
});
|
|
|
|
fakeProcess.emit("SIGINT");
|
|
const exitCode = await runPromise;
|
|
|
|
expect(exitCode).toBe(130);
|
|
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
|
expect(watcher.close).toHaveBeenCalledTimes(1);
|
|
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, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
|
|
|
const runPromise = runWatchMain({
|
|
args: ["gateway", "--force"],
|
|
createWatcher,
|
|
process: fakeProcess,
|
|
spawn,
|
|
});
|
|
|
|
fakeProcess.emit("SIGTERM");
|
|
const exitCode = await runPromise;
|
|
|
|
expect(exitCode).toBe(143);
|
|
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
|
expect(watcher.close).toHaveBeenCalledTimes(1);
|
|
expect(fakeProcess.listenerCount("SIGINT")).toBe(0);
|
|
expect(fakeProcess.listenerCount("SIGTERM")).toBe(0);
|
|
});
|
|
|
|
it("ignores test-only changes and restarts on non-test source changes", async () => {
|
|
const childA = Object.assign(new EventEmitter(), {
|
|
kill: vi.fn(function () {
|
|
queueMicrotask(() => childA.emit("exit", 0, null));
|
|
}),
|
|
});
|
|
const childB = Object.assign(new EventEmitter(), {
|
|
kill: vi.fn(function () {
|
|
queueMicrotask(() => childB.emit("exit", 0, null));
|
|
}),
|
|
});
|
|
const childC = Object.assign(new EventEmitter(), {
|
|
kill: vi.fn(function () {
|
|
queueMicrotask(() => childC.emit("exit", 0, null));
|
|
}),
|
|
});
|
|
const childD = Object.assign(new EventEmitter(), {
|
|
kill: vi.fn(() => {}),
|
|
});
|
|
const spawn = vi
|
|
.fn()
|
|
.mockReturnValueOnce(childA)
|
|
.mockReturnValueOnce(childB)
|
|
.mockReturnValueOnce(childC)
|
|
.mockReturnValueOnce(childD);
|
|
const watcher = Object.assign(new EventEmitter(), {
|
|
close: vi.fn(async () => {}),
|
|
});
|
|
const createWatcher = vi.fn(() => watcher);
|
|
const fakeProcess = createFakeProcess();
|
|
|
|
const runPromise = runWatchMain({
|
|
args: ["gateway", "--force"],
|
|
createWatcher,
|
|
process: fakeProcess,
|
|
spawn,
|
|
});
|
|
|
|
watcher.emit("change", "src/infra/watch-node.test.ts");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(spawn).toHaveBeenCalledTimes(1);
|
|
expect(childA.kill).not.toHaveBeenCalled();
|
|
|
|
watcher.emit("change", "src/infra/watch-node.test.tsx");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(spawn).toHaveBeenCalledTimes(1);
|
|
expect(childA.kill).not.toHaveBeenCalled();
|
|
|
|
watcher.emit("change", "src/infra/watch-node-test-helpers.ts");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(spawn).toHaveBeenCalledTimes(1);
|
|
expect(childA.kill).not.toHaveBeenCalled();
|
|
|
|
watcher.emit("change", "extensions/voice-call/README.md");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(spawn).toHaveBeenCalledTimes(1);
|
|
expect(childA.kill).not.toHaveBeenCalled();
|
|
|
|
watcher.emit("change", "extensions/voice-call/openclaw.plugin.json");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(childA.kill).toHaveBeenCalledWith("SIGTERM");
|
|
expect(spawn).toHaveBeenCalledTimes(2);
|
|
|
|
watcher.emit("change", "extensions/voice-call/package.json");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(childB.kill).toHaveBeenCalledWith("SIGTERM");
|
|
expect(spawn).toHaveBeenCalledTimes(3);
|
|
|
|
watcher.emit("change", "src/infra/watch-node.ts");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(childC.kill).toHaveBeenCalledWith("SIGTERM");
|
|
expect(spawn).toHaveBeenCalledTimes(4);
|
|
|
|
fakeProcess.emit("SIGINT");
|
|
const exitCode = await runPromise;
|
|
expect(exitCode).toBe(130);
|
|
});
|
|
|
|
it("kills child and exits when watcher emits an error", async () => {
|
|
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
|
|
|
const runPromise = runWatchMain({
|
|
args: ["gateway", "--force"],
|
|
createWatcher,
|
|
process: fakeProcess,
|
|
spawn,
|
|
});
|
|
|
|
watcher.emit("error", new Error("watch failed"));
|
|
const exitCode = await runPromise;
|
|
|
|
expect(exitCode).toBe(1);
|
|
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
|
expect(watcher.close).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|