|
1 | 1 | // Plugin Lifecycle Probe tests cover QA Lab plugin lifecycle evidence. |
| 2 | +import { EventEmitter } from "node:events"; |
2 | 3 | import { mkdirSync, writeFileSync } from "node:fs"; |
3 | 4 | import path from "node:path"; |
4 | | -import { afterEach, describe, expect, it } from "vitest"; |
| 5 | +import { afterEach, describe, expect, it, vi } from "vitest"; |
5 | 6 | import { createTempDirTracker } from "../../../helpers/temp-dir.js"; |
6 | 7 | import { |
7 | 8 | assertInspectLoaded, |
8 | 9 | assertUninstalled, |
9 | 10 | parseDurationMs, |
| 11 | +testing as probeTesting, |
10 | 12 | } from "./plugin-lifecycle-probe-runtime.js"; |
11 | 13 | |
12 | 14 | const tempDirs = createTempDirTracker(); |
@@ -15,6 +17,18 @@ function makeTempDir(): string {
|
15 | 17 | return tempDirs.make("openclaw-plugin-lifecycle-probe-"); |
16 | 18 | } |
17 | 19 | |
| 20 | +class FakeCommandChild extends EventEmitter { |
| 21 | +readonly signals: string[] = []; |
| 22 | + |
| 23 | +kill(signal?: NodeJS.Signals | number): boolean { |
| 24 | +this.signals.push(String(signal)); |
| 25 | +if (signal === "SIGTERM") { |
| 26 | +queueMicrotask(() => this.emit("exit", 0, null)); |
| 27 | +} |
| 28 | +return true; |
| 29 | +} |
| 30 | +} |
| 31 | + |
18 | 32 | afterEach(tempDirs.cleanup); |
19 | 33 | |
20 | 34 | describe("plugin lifecycle matrix probe", () => { |
@@ -70,4 +84,29 @@ describe("plugin lifecycle matrix probe", () => {
|
70 | 84 | it("preserves disabled npm install timeout semantics", () => { |
71 | 85 | expect(parseDurationMs("0", "600s")).toBeUndefined(); |
72 | 86 | }); |
| 87 | + |
| 88 | +it("rejects timed commands that exit cleanly during kill grace", async () => { |
| 89 | +vi.useFakeTimers(); |
| 90 | +try { |
| 91 | +const child = new FakeCommandChild(); |
| 92 | +const runPromise = probeTesting.runCommand("fake-command", ["install"], { |
| 93 | +spawnImpl: (() => child) as unknown as typeof import("node:child_process").spawn, |
| 94 | +timeoutKillGraceMs: 100, |
| 95 | +timeoutMs: 10, |
| 96 | +}); |
| 97 | +const runError = runPromise.catch((error: unknown) => error); |
| 98 | + |
| 99 | +await vi.advanceTimersByTimeAsync(10); |
| 100 | + |
| 101 | +const error = await runError; |
| 102 | +expect(error).toBeInstanceOf(Error); |
| 103 | +expect((error as Error).message).toBe("fake-command install timed out after 10ms"); |
| 104 | +expect(child.signals).toEqual(["SIGTERM"]); |
| 105 | + |
| 106 | +await vi.advanceTimersByTimeAsync(100); |
| 107 | +expect(child.signals).toEqual(["SIGTERM"]); |
| 108 | +} finally { |
| 109 | +vi.useRealTimers(); |
| 110 | +} |
| 111 | +}); |
73 | 112 | }); |