mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 12:56:09 +00:00
* chore(types): add declaration files for scripts/lib and scripts/e2e modules
* chore(types): add declaration files for top-level script modules (a-m)
* chore(types): add declaration files for top-level script modules (n-z)
* test: use a non-secret-shaped gateway token fixture
* test: type ci workflow guard helpers for the root test lane
* chore(tooling): typecheck root test/** with a dedicated tsgo lane
- test/tsconfig/tsconfig.test.root.json: root-test program (strict unused checks,
fixtures excluded; two Docker E2E clients that import built dist/** stay out,
same rationale as the scripts/e2e exclusion in tsconfig.scripts.json)
- tsgo:test:root wired into tsgo:test, check:test-types, scripts/check.mjs, and
the ci.yml test-types shard, mirroring the tsgo:scripts lane (#104348)
- changed-lane routing: test/**/*.ts (excluding fixtures) and the lane tsconfig
now trigger 'typecheck test root' in check:changed; previously test/ paths ran
lint only, so harness type errors surfaced first in CI (#104287 envDir case)
- burn down all 1071 latent type errors in the program: precise param/local
types across test/scripts, test/vitest, test/e2e, and transitive scripts/e2e
program members; 205 sibling .d.mts declaration files for imported .mjs
modules (committed separately); zero any, zero ts-expect-error
- resolve the pre-existing testing star-export ambiguity in
scripts/e2e/parallels/common.ts with an explicit re-export
Closes #104388
* chore(types): correct declaration fidelity per structured review
- re-derive 51 .d.mts files from implementation data flow instead of
initializers: fix a wrong never return (runTestProjectsDelegation returns
the child), add encoding-sensitive exec/spawn overloads (plain-gh), restore
the full release profile union, make parsed paths string | null, add missing
parseArgs fields via help/non-help unions, add a missing sibling declaration
(budget-number-args), drop 15 unused lint directives
- precise install-record/tuple typing removes the type-aware oxlint
regressions the first declarations caused in scripts/e2e implementations
- route .mts declaration edits under test/ to the testRoot lane and reference
the test-root project from tsconfig.projects.json so tsgo:all covers it
(closes both review findings against the lane wiring)
* chore(scripts): keep telegram runner dist typing structural for the boundary guard
* chore(types): declare runtime pack and gateway readiness exports added on main
* test: pin the importTargetPlan form of the plugin-contract plan import
The guard expectation still referenced the raw await import( form that
7ae5996bb3 (#103975) replaced with the importTargetPlan fallback helper;
the assertion fails on current main.
568 lines
18 KiB
TypeScript
568 lines
18 KiB
TypeScript
// Check Gateway Watch Regression tests cover check gateway watch regression script behavior.
|
|
import { EventEmitter } from "node:events";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
appendBoundedWatchLog,
|
|
buildTimedWatchCommand,
|
|
collectGatewayWatchFindings,
|
|
hasGatewayReadyLog,
|
|
parseArgs,
|
|
resolveTimedWatchShell,
|
|
runTimedWatch,
|
|
readNonNegativeInteger,
|
|
shouldReportDuplicateDistRuntimeRegression,
|
|
shouldRefreshBuildStampForRestoredArtifacts,
|
|
stopTimedWatchChild,
|
|
updateWatchBuildDetection,
|
|
WATCH_LOG_CAPTURE_MAX_CHARS,
|
|
writeBuildAndRuntimePostBuildStamps,
|
|
} from "../../scripts/check-gateway-watch-regression.mjs";
|
|
import {
|
|
BUILD_STAMP_FILE,
|
|
RUNTIME_POSTBUILD_STAMP_FILE,
|
|
} from "../../scripts/lib/local-build-metadata-paths.mjs";
|
|
|
|
describe("check-gateway-watch-regression", () => {
|
|
it("accepts package-manager argument separators before script options", () => {
|
|
expect(parseArgs(["--", "--window-ms", "1500", "--skip-build"])).toMatchObject({
|
|
skipBuild: true,
|
|
windowMs: 1500,
|
|
});
|
|
});
|
|
|
|
it("parses timing and growth limits as strict non-negative integers", () => {
|
|
expect(readNonNegativeInteger("0", "limit")).toBe(0);
|
|
expect(readNonNegativeInteger(" 42 ", "limit")).toBe(42);
|
|
expect(
|
|
parseArgs([
|
|
"--window-ms",
|
|
"0",
|
|
"--ready-timeout-ms",
|
|
"1",
|
|
"--ready-settle-ms",
|
|
"2",
|
|
"--sigkill-grace-ms",
|
|
"3",
|
|
"--sigkill-exit-grace-ms",
|
|
"4",
|
|
"--cpu-warn-ms",
|
|
"5",
|
|
"--cpu-fail-ms",
|
|
"6",
|
|
"--dist-runtime-file-growth-max",
|
|
"7",
|
|
"--dist-runtime-byte-growth-max",
|
|
"8",
|
|
]),
|
|
).toMatchObject({
|
|
cpuFailMs: 6,
|
|
cpuWarnMs: 5,
|
|
distRuntimeByteGrowthMax: 8,
|
|
distRuntimeFileGrowthMax: 7,
|
|
readySettleMs: 2,
|
|
readyTimeoutMs: 1,
|
|
sigkillExitGraceMs: 4,
|
|
sigkillGraceMs: 3,
|
|
windowMs: 0,
|
|
});
|
|
|
|
expect(() => readNonNegativeInteger("1.5", "limit")).toThrow(
|
|
"limit must be a non-negative integer",
|
|
);
|
|
expect(() => readNonNegativeInteger("1e3", "limit")).toThrow(
|
|
"limit must be a non-negative integer",
|
|
);
|
|
expect(() => readNonNegativeInteger("-1", "limit")).toThrow(
|
|
"limit must be a non-negative integer",
|
|
);
|
|
expect(() => readNonNegativeInteger("9007199254740992", "limit")).toThrow(
|
|
"limit must be a safe integer",
|
|
);
|
|
expect(() => parseArgs(["--window-ms", "soon"])).toThrow(
|
|
"--window-ms must be a non-negative integer",
|
|
);
|
|
});
|
|
|
|
it("recognizes current and legacy gateway ready logs", () => {
|
|
expect(hasGatewayReadyLog("[gateway] http server listening (0 plugins, 0.8s)")).toBe(true);
|
|
expect(hasGatewayReadyLog("[gateway] ready (0 plugins, 0.8s)")).toBe(true);
|
|
expect(hasGatewayReadyLog("\u001B[36m[gateway]\u001B[39m \u001B[36mready\u001B[39m")).toBe(
|
|
true,
|
|
);
|
|
expect(hasGatewayReadyLog("[gateway] starting HTTP server...")).toBe(false);
|
|
});
|
|
|
|
it("bounds in-memory watch output capture while keeping the newest logs", () => {
|
|
const first = appendBoundedWatchLog("abc", "def", 8);
|
|
expect(first).toEqual({ text: "abcdef", truncated: false });
|
|
|
|
const second = appendBoundedWatchLog(first.text, "ghijkl", 8);
|
|
expect(second).toEqual({ text: "efghijkl", truncated: true });
|
|
expect(second.text).toHaveLength(8);
|
|
expect(WATCH_LOG_CAPTURE_MAX_CHARS).toBeGreaterThan(1024);
|
|
});
|
|
|
|
it("keeps build-regression detection after diagnostic logs truncate", () => {
|
|
const detected = updateWatchBuildDetection(
|
|
{ buffer: "", triggered: false, reason: null },
|
|
"Building TypeScript (dist is stale: source_mtime_newer)\n",
|
|
);
|
|
const afterNoise = updateWatchBuildDetection(detected, "x".repeat(10_000));
|
|
|
|
expect(afterNoise.triggered).toBe(true);
|
|
expect(afterNoise.reason).toBe("source_mtime_newer");
|
|
|
|
const coalesced = updateWatchBuildDetection(
|
|
{ buffer: "", triggered: false, reason: null },
|
|
`Building TypeScript (dist is stale: config_newer)\n${"x".repeat(10_000)}`,
|
|
);
|
|
expect(coalesced.triggered).toBe(true);
|
|
expect(coalesced.reason).toBe("config_newer");
|
|
});
|
|
|
|
it("uses bash for timed watch commands when available", () => {
|
|
expect(
|
|
resolveTimedWatchShell({
|
|
existsSync: (candidate: string) => candidate === "/usr/bin/bash",
|
|
}),
|
|
).toBe("/usr/bin/bash");
|
|
|
|
const command = buildTimedWatchCommand(
|
|
"watch.pid",
|
|
"watch.time",
|
|
"/tmp/openclaw-watch",
|
|
19042,
|
|
{
|
|
existsSync: (candidate: string) =>
|
|
candidate === "/usr/bin/bash" || candidate === "/usr/bin/time",
|
|
},
|
|
);
|
|
const shellIndex = command.args.indexOf("/usr/bin/bash");
|
|
|
|
if (shellIndex >= 0) {
|
|
expect(command.args[shellIndex + 1]).toBe("-lc");
|
|
} else {
|
|
expect(command.command).toBe("/usr/bin/bash");
|
|
expect(command.args[0]).toBe("-lc");
|
|
}
|
|
});
|
|
|
|
it("runs gateway watch through the parent Node binary", () => {
|
|
const nodeExecPath = "/opt/hostedtoolcache/node/24.11.1/x64/bin/node";
|
|
const command = buildTimedWatchCommand(
|
|
"watch.pid",
|
|
"watch.time",
|
|
"/tmp/openclaw-watch",
|
|
19042,
|
|
{
|
|
existsSync: (candidate: string) =>
|
|
candidate === "/usr/bin/bash" || candidate === "/usr/bin/time",
|
|
nodeExecPath,
|
|
},
|
|
);
|
|
const shellSource = command.args.at(-1);
|
|
|
|
expect(shellSource).toContain(`exec '${nodeExecPath}' scripts/watch-node.mjs gateway --force`);
|
|
expect(command.env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(nodeExecPath));
|
|
});
|
|
|
|
it("fails the regression gate when gateway watch never becomes ready", () => {
|
|
const findings = collectGatewayWatchFindings({
|
|
cpuMs: 0,
|
|
distRuntimeByteGrowth: 0,
|
|
distRuntimeFileGrowth: 0,
|
|
options: {
|
|
cpuFailMs: 8000,
|
|
cpuWarnMs: 1000,
|
|
distRuntimeByteGrowthMax: 2 * 1024 * 1024,
|
|
distRuntimeFileGrowthMax: 200,
|
|
windowMs: 10_000,
|
|
},
|
|
watchBuildReason: null,
|
|
watchResult: {
|
|
idleCpuMs: 0,
|
|
readyBeforeWindow: false,
|
|
spawnError: null,
|
|
timingFileMissing: false,
|
|
},
|
|
watchTriggeredBuild: false,
|
|
});
|
|
|
|
expect(findings.failures).toContain(
|
|
"gateway:watch did not report ready before the idle CPU window",
|
|
);
|
|
expect(findings.warnings).toEqual([]);
|
|
});
|
|
|
|
it("reports early gateway watch exit before readiness distinctly", () => {
|
|
const findings = collectGatewayWatchFindings({
|
|
cpuMs: 0,
|
|
distRuntimeByteGrowth: 0,
|
|
distRuntimeFileGrowth: 0,
|
|
options: {
|
|
cpuFailMs: 8000,
|
|
cpuWarnMs: 1000,
|
|
distRuntimeByteGrowthMax: 2 * 1024 * 1024,
|
|
distRuntimeFileGrowthMax: 200,
|
|
windowMs: 10_000,
|
|
},
|
|
watchBuildReason: null,
|
|
watchResult: {
|
|
exit: { code: 1, signal: null },
|
|
exitedBeforeReady: true,
|
|
idleCpuMs: 0,
|
|
readyBeforeWindow: false,
|
|
spawnError: null,
|
|
timingFileMissing: false,
|
|
},
|
|
watchTriggeredBuild: false,
|
|
});
|
|
|
|
expect(findings.failures).toContain("gateway:watch exited before ready (code 1, signal null)");
|
|
expect(findings.failures).not.toContain(
|
|
"gateway:watch did not report ready before the idle CPU window",
|
|
);
|
|
expect(findings.warnings).toEqual([]);
|
|
});
|
|
|
|
it("reports gateway watch exit after readiness before the idle window completes", () => {
|
|
const findings = collectGatewayWatchFindings({
|
|
cpuMs: 0,
|
|
distRuntimeByteGrowth: 0,
|
|
distRuntimeFileGrowth: 0,
|
|
options: {
|
|
cpuFailMs: 8000,
|
|
cpuWarnMs: 1000,
|
|
distRuntimeByteGrowthMax: 2 * 1024 * 1024,
|
|
distRuntimeFileGrowthMax: 200,
|
|
windowMs: 10_000,
|
|
},
|
|
watchBuildReason: null,
|
|
watchResult: {
|
|
exit: { code: 0, signal: null },
|
|
exitedBeforeReady: false,
|
|
exitedBeforeStop: true,
|
|
idleCpuMs: 0,
|
|
readyBeforeWindow: true,
|
|
spawnError: null,
|
|
timingFileMissing: false,
|
|
},
|
|
watchTriggeredBuild: false,
|
|
});
|
|
|
|
expect(findings.failures).toContain(
|
|
"gateway:watch exited before the idle CPU window completed (code 0, signal null)",
|
|
);
|
|
expect(findings.warnings).toEqual([]);
|
|
});
|
|
|
|
it("reports duplicate dist-runtime regression only for dist-runtime growth failures", () => {
|
|
expect(
|
|
shouldReportDuplicateDistRuntimeRegression([
|
|
"gateway:watch did not report ready before the idle CPU window",
|
|
]),
|
|
).toBe(false);
|
|
expect(
|
|
shouldReportDuplicateDistRuntimeRegression(["dist-runtime file growth 201 exceeded max 200"]),
|
|
).toBe(true);
|
|
expect(
|
|
shouldReportDuplicateDistRuntimeRegression([
|
|
"dist-runtime apparent byte growth 2097153 exceeded max 2097152",
|
|
]),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("refreshes restored build stamps only for skip-build config mtime drift", () => {
|
|
expect(
|
|
shouldRefreshBuildStampForRestoredArtifacts({
|
|
skipBuild: true,
|
|
buildRequirement: { shouldBuild: true, reason: "config_newer" },
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldRefreshBuildStampForRestoredArtifacts({
|
|
skipBuild: false,
|
|
buildRequirement: { shouldBuild: true, reason: "config_newer" },
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
shouldRefreshBuildStampForRestoredArtifacts({
|
|
skipBuild: true,
|
|
buildRequirement: { shouldBuild: true, reason: "source_mtime_newer" },
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("refreshes runtime postbuild stamps after build stamps", () => {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-gateway-watch-stamps-"));
|
|
try {
|
|
fs.mkdirSync(path.join(rootDir, ".git"), { recursive: true });
|
|
writeBuildAndRuntimePostBuildStamps({ cwd: rootDir });
|
|
|
|
const buildStampPath = path.join(rootDir, "dist", BUILD_STAMP_FILE);
|
|
const runtimeStampPath = path.join(rootDir, "dist", RUNTIME_POSTBUILD_STAMP_FILE);
|
|
expect(fs.existsSync(buildStampPath)).toBe(true);
|
|
expect(fs.existsSync(runtimeStampPath)).toBe(true);
|
|
expect(fs.statSync(runtimeStampPath).mtimeMs).toBeGreaterThanOrEqual(
|
|
fs.statSync(buildStampPath).mtimeMs,
|
|
);
|
|
} finally {
|
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("bounds teardown when the watch process ignores termination signals", async () => {
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
exitCode: number | null;
|
|
signalCode: NodeJS.Signals | null;
|
|
stderr: { destroy: ReturnType<typeof vi.fn> };
|
|
stdin: { destroy: ReturnType<typeof vi.fn> };
|
|
stdout: { destroy: ReturnType<typeof vi.fn> };
|
|
unref: ReturnType<typeof vi.fn>;
|
|
};
|
|
child.exitCode = null;
|
|
child.signalCode = null;
|
|
child.stderr = { destroy: vi.fn() };
|
|
child.stdin = { destroy: vi.fn() };
|
|
child.stdout = { destroy: vi.fn() };
|
|
child.unref = vi.fn();
|
|
const killProcess = vi.fn();
|
|
|
|
await expect(
|
|
stopTimedWatchChild(
|
|
child,
|
|
1234,
|
|
{ sigkillExitGraceMs: 1, sigkillGraceMs: 1 },
|
|
{ killProcess },
|
|
),
|
|
).resolves.toEqual({ code: null, signal: "SIGKILL" });
|
|
|
|
expect(killProcess).toHaveBeenNthCalledWith(1, 1234, "SIGTERM");
|
|
expect(killProcess).toHaveBeenNthCalledWith(2, 1234, "SIGKILL");
|
|
expect(child.stdin.destroy).toHaveBeenCalledOnce();
|
|
expect(child.stdout.destroy).toHaveBeenCalledOnce();
|
|
expect(child.stderr.destroy).toHaveBeenCalledOnce();
|
|
expect(child.unref).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("removes the isolated watch home after spawn failures", async () => {
|
|
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-gateway-watch-output-"));
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
stderr: EventEmitter;
|
|
stdout: EventEmitter;
|
|
};
|
|
child.stderr = new EventEmitter();
|
|
child.stdout = new EventEmitter();
|
|
const sleep = vi.fn(() => new Promise<never>(() => {}));
|
|
const stopChild = vi.fn(
|
|
() =>
|
|
new Promise<never>(() => {
|
|
// Spawn failures must win before cleanup waits for a child that never started.
|
|
}),
|
|
);
|
|
const waitForGatewayReady = vi.fn(async () => false);
|
|
const spawn = vi.fn(
|
|
(_command: string, _args: string[], _options: { env: NodeJS.ProcessEnv }) => {
|
|
process.nextTick(() => {
|
|
child.emit("error", new Error("spawn failed"));
|
|
});
|
|
return child;
|
|
},
|
|
);
|
|
|
|
try {
|
|
const result = await runTimedWatch(
|
|
{
|
|
readySettleMs: 0,
|
|
readyTimeoutMs: 0,
|
|
sigkillGraceMs: 1,
|
|
windowMs: 0,
|
|
},
|
|
outputDir,
|
|
{
|
|
allocateLoopbackPort: async () => 19042,
|
|
spawn,
|
|
sleep,
|
|
stopTimedWatchChild: stopChild,
|
|
waitForGatewayReady,
|
|
},
|
|
);
|
|
|
|
const isolatedHomeDir = fs
|
|
.readFileSync(path.join(outputDir, "watch.home.txt"), "utf8")
|
|
.trim();
|
|
expect(result.spawnError).toBe("spawn failed");
|
|
expect(fs.existsSync(isolatedHomeDir)).toBe(false);
|
|
expect(fs.existsSync(path.join(outputDir, "watch.home.txt"))).toBe(true);
|
|
expect(spawn.mock.calls[0]?.[2]?.env?.OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS).toBe("0");
|
|
expect(waitForGatewayReady).not.toHaveBeenCalled();
|
|
expect(stopChild).not.toHaveBeenCalled();
|
|
} finally {
|
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("stops waiting for readiness when the watch process exits early", async () => {
|
|
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-gateway-watch-output-"));
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
stderr: EventEmitter;
|
|
stdout: EventEmitter;
|
|
};
|
|
child.stderr = new EventEmitter();
|
|
child.stdout = new EventEmitter();
|
|
const sleep = vi.fn(() => new Promise<never>(() => {}));
|
|
const stopChild = vi.fn(async () => ({ code: null, signal: "SIGTERM" }));
|
|
const waitForGatewayReady = vi.fn(
|
|
() =>
|
|
new Promise<never>(() => {
|
|
// Child exit must win before readiness timeout.
|
|
}),
|
|
);
|
|
const spawn = vi.fn(() => {
|
|
process.nextTick(() => {
|
|
child.stderr.emit("data", "gateway startup failed\n");
|
|
child.emit("exit", 1, null);
|
|
});
|
|
return child;
|
|
});
|
|
|
|
try {
|
|
const result = await runTimedWatch(
|
|
{
|
|
readySettleMs: 0,
|
|
readyTimeoutMs: 30_000,
|
|
sigkillGraceMs: 1,
|
|
windowMs: 10_000,
|
|
},
|
|
outputDir,
|
|
{
|
|
allocateLoopbackPort: async () => 19042,
|
|
spawn,
|
|
sleep,
|
|
stopTimedWatchChild: stopChild,
|
|
waitForGatewayReady,
|
|
},
|
|
);
|
|
|
|
expect(result.exit).toEqual({ code: 1, signal: null });
|
|
expect(result.exitedBeforeReady).toBe(true);
|
|
expect(result.exitedBeforeStop).toBe(true);
|
|
expect(result.readyBeforeWindow).toBe(false);
|
|
expect(result.spawnError).toBeNull();
|
|
expect(fs.readFileSync(result.stderrPath, "utf8")).toContain("gateway startup failed");
|
|
expect(waitForGatewayReady).not.toHaveBeenCalled();
|
|
expect(stopChild).not.toHaveBeenCalled();
|
|
} finally {
|
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("records a ready gateway watch exit during the settle window as unplanned", async () => {
|
|
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-gateway-watch-output-"));
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
stderr: EventEmitter;
|
|
stdout: EventEmitter;
|
|
};
|
|
child.stderr = new EventEmitter();
|
|
child.stdout = new EventEmitter();
|
|
const stopChild = vi.fn(async () => ({ code: null, signal: "SIGTERM" }));
|
|
const sleep = vi.fn(
|
|
() =>
|
|
new Promise<never>(() => {
|
|
process.nextTick(() => {
|
|
child.emit("exit", 0, null);
|
|
});
|
|
}),
|
|
);
|
|
const spawn = vi.fn(() => {
|
|
fs.writeFileSync(path.join(outputDir, "watch.pid"), "1234\n", "utf8");
|
|
return child;
|
|
});
|
|
|
|
try {
|
|
const result = await runTimedWatch(
|
|
{
|
|
readySettleMs: 10_000,
|
|
readyTimeoutMs: 30_000,
|
|
sigkillGraceMs: 1,
|
|
windowMs: 10_000,
|
|
},
|
|
outputDir,
|
|
{
|
|
allocateLoopbackPort: async () => 19042,
|
|
spawn,
|
|
sleep,
|
|
stopTimedWatchChild: stopChild,
|
|
waitForGatewayReady: async () => true,
|
|
},
|
|
);
|
|
|
|
expect(result.exit).toEqual({ code: 0, signal: null });
|
|
expect(result.exitedBeforeReady).toBe(false);
|
|
expect(result.exitedBeforeStop).toBe(true);
|
|
expect(result.readyBeforeWindow).toBe(true);
|
|
expect(result.idleCpuMs).toBeNull();
|
|
expect(stopChild).not.toHaveBeenCalled();
|
|
} finally {
|
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("records a ready gateway watch exit during the idle window as unplanned", async () => {
|
|
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-gateway-watch-output-"));
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
stderr: EventEmitter;
|
|
stdout: EventEmitter;
|
|
};
|
|
child.stderr = new EventEmitter();
|
|
child.stdout = new EventEmitter();
|
|
const stopChild = vi.fn(async () => ({ code: null, signal: "SIGTERM" }));
|
|
const sleep = vi.fn(
|
|
() =>
|
|
new Promise<never>(() => {
|
|
process.nextTick(() => {
|
|
child.emit("exit", 0, null);
|
|
});
|
|
}),
|
|
);
|
|
const readProcessTreeCpuMs = vi.fn(() => 12);
|
|
const spawn = vi.fn(() => {
|
|
fs.writeFileSync(path.join(outputDir, "watch.pid"), "1234\n", "utf8");
|
|
return child;
|
|
});
|
|
|
|
try {
|
|
const result = await runTimedWatch(
|
|
{
|
|
readySettleMs: 0,
|
|
readyTimeoutMs: 30_000,
|
|
sigkillGraceMs: 1,
|
|
windowMs: 10_000,
|
|
},
|
|
outputDir,
|
|
{
|
|
allocateLoopbackPort: async () => 19042,
|
|
readProcessTreeCpuMs,
|
|
spawn,
|
|
sleep,
|
|
stopTimedWatchChild: stopChild,
|
|
waitForGatewayReady: async () => true,
|
|
},
|
|
);
|
|
|
|
expect(result.exit).toEqual({ code: 0, signal: null });
|
|
expect(result.exitedBeforeReady).toBe(false);
|
|
expect(result.exitedBeforeStop).toBe(true);
|
|
expect(result.readyBeforeWindow).toBe(true);
|
|
expect(result.idleCpuMs).toBeNull();
|
|
expect(readProcessTreeCpuMs).toHaveBeenCalledOnce();
|
|
expect(stopChild).not.toHaveBeenCalled();
|
|
} finally {
|
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|