test(e2e): sample kitchen sink RSS on Windows

This commit is contained in:
Vincent Koc
2026-05-23 08:51:23 +02:00
parent c298dfe013
commit 73c1e375e4
2 changed files with 110 additions and 7 deletions

View File

@@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";
import { pathToFileURL } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
const PLUGIN_SPEC =
process.env.OPENCLAW_KITCHEN_SINK_NPM_SPEC || "npm:@openclaw/kitchen-sink@latest";
@@ -517,12 +517,21 @@ function assertToolInvokeResult(payload) {
}
}
async function sampleProcess(pid) {
if (!pid || process.platform === "win32") {
export async function sampleProcess(pid, options = {}) {
const platform = options.platform ?? process.platform;
const run = options.runCommand ?? runCommand;
if (!pid) {
return null;
}
if (platform === "win32") {
return sampleWindowsProcess(pid, run);
}
return samplePosixProcess(pid, run);
}
async function samplePosixProcess(pid, run) {
try {
const { stdout } = await runCommand("ps", ["-o", "rss=,pcpu=", "-p", String(pid)], {
const { stdout } = await run("ps", ["-o", "rss=,pcpu=", "-p", String(pid)], {
timeoutMs: 5000,
});
const [rssKbRaw, cpuRaw] = stdout.trim().split(/\s+/u);
@@ -540,7 +549,44 @@ async function sampleProcess(pid) {
}
}
function assertResourceCeiling(sample) {
async function sampleWindowsProcess(pid, run) {
const safePid = Number(pid);
if (!Number.isInteger(safePid) || safePid <= 0) {
return null;
}
const command = [
"$ErrorActionPreference = 'Stop'",
`$process = Get-Process -Id ${safePid} -ErrorAction Stop`,
"$cpu = 0",
"if ($null -ne $process.CPU) { $cpu = $process.CPU }",
"[Console]::Out.Write(('{0} {1}' -f $process.WorkingSet64, $cpu))",
].join("; ");
for (const powershell of ["powershell.exe", "powershell"]) {
try {
const { stdout } = await run(
powershell,
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
{ timeoutMs: 5000 },
);
const [workingSetBytesRaw, cpuSecondsRaw] = stdout.trim().split(/\s+/u);
const workingSetBytes = Number.parseInt(workingSetBytesRaw ?? "", 10);
const cpuSeconds = Number.parseFloat(cpuSecondsRaw ?? "");
if (!Number.isFinite(workingSetBytes)) {
return null;
}
return {
rssMiB: Math.round((workingSetBytes / 1024 / 1024) * 10) / 10,
cpuPercent: null,
cpuSeconds: Number.isFinite(cpuSeconds) ? cpuSeconds : null,
};
} catch {
// Try the next Windows PowerShell command name.
}
}
return null;
}
export function assertResourceCeiling(sample) {
if (!sample) {
return;
}
@@ -590,7 +636,7 @@ function isNonEmptyString(value) {
return typeof value === "string" && value.trim().length > 0;
}
async function main() {
export async function main() {
const runner = resolveOpenClawRunner();
const port = readPositiveInt(process.env.OPENCLAW_KITCHEN_SINK_RPC_PORT, DEFAULT_PORT);
const { root, env } = makeEnv();
@@ -725,4 +771,6 @@ async function main() {
}
}
await main();
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main();
}

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { assertResourceCeiling, sampleProcess } from "../../scripts/e2e/kitchen-sink-rpc-walk.mjs";
describe("kitchen-sink RPC process sampling", () => {
it("samples RSS on Windows instead of silently disabling the resource guard", async () => {
const calls: Array<{ command: string; args: string[] }> = [];
const sample = await sampleProcess(1234, {
platform: "win32",
runCommand: async (command: string, args: string[]) => {
calls.push({ command, args });
return { stdout: `${256 * 1024 * 1024} 1.5`, stderr: "" };
},
});
expect(sample).toEqual({ cpuPercent: null, cpuSeconds: 1.5, rssMiB: 256 });
expect(calls[0]?.command).toBe("powershell.exe");
expect(calls[0]?.args.join(" ")).toContain("Get-Process -Id 1234");
});
it("falls back to the legacy powershell command name on Windows", async () => {
const commands: string[] = [];
const sample = await sampleProcess(1234, {
platform: "win32",
runCommand: async (command: string) => {
commands.push(command);
if (command === "powershell.exe") {
throw new Error("missing powershell.exe");
}
return { stdout: `${96 * 1024 * 1024} 0`, stderr: "" };
},
});
expect(commands).toEqual(["powershell.exe", "powershell"]);
expect(sample?.rssMiB).toBe(96);
});
it("samples RSS and CPU percent with ps on POSIX", async () => {
const sample = await sampleProcess(4321, {
platform: "linux",
runCommand: async (command: string, args: string[]) => {
expect(command).toBe("ps");
expect(args).toEqual(["-o", "rss=,pcpu=", "-p", "4321"]);
return { stdout: "262144 12.5\n", stderr: "" };
},
});
expect(sample).toEqual({ cpuPercent: 12.5, rssMiB: 256 });
});
it("fails when the sampled RSS exceeds the configured ceiling", () => {
expect(() => assertResourceCeiling({ rssMiB: 2049 })).toThrow(
"gateway RSS exceeded 2048 MiB: 2049 MiB",
);
});
});