Files
openclaw/test/scripts/profile-extension-memory.test.ts
2026-05-28 14:27:59 +02:00

116 lines
4.1 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { runCase } from "../../scripts/profile-extension-memory.mjs";
const SCRIPT_PATH = path.resolve("scripts/profile-extension-memory.mjs");
function runProfileExtensionMemory(args: string[], cwd = process.cwd()) {
return spawnSync(process.execPath, [SCRIPT_PATH, ...args], {
cwd,
encoding: "utf8",
});
}
describe("scripts/profile-extension-memory", () => {
it("prints help without requiring built plugin artifacts", () => {
const result = runProfileExtensionMemory(["--help"]);
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
expect(result.stdout).toContain("Usage: node scripts/profile-extension-memory.mjs");
});
it("rejects loose numeric flags before scanning built plugin artifacts", () => {
const cases = [
["--concurrency", "2abc"],
["--timeout-ms", "1e3"],
["--combined-timeout-ms", "90000ms"],
["--top", "0x10"],
];
for (const [flag, value] of cases) {
const result = runProfileExtensionMemory([flag, value]);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toContain(`[extension-memory] ${flag} must be a positive integer`);
expect(result.stderr).not.toContain("dist/extensions");
expect(result.stderr).not.toContain("at ");
}
});
it("bounds noisy child output without losing RSS samples", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-extension-memory-test-"));
try {
const extensionDir = path.join(root, "dist", "extensions", "noisy");
const reportPath = path.join(root, "report.json");
mkdirSync(extensionDir, { recursive: true });
writeFileSync(
path.join(extensionDir, "index.js"),
[
`const fs = require("node:fs");`,
`fs.writeSync(2, "old stderr " + "x".repeat(160000) + "\\n");`,
`fs.writeSync(1, "old stdout " + "y".repeat(160000) + "\\n");`,
`process.on("exit", () => fs.writeSync(2, "exit tail\\n"));`,
].join("\n"),
"utf8",
);
const result = runProfileExtensionMemory(
["--extension", "noisy", "--skip-combined", "--concurrency", "1", "--json", reportPath],
root,
);
expect(result.status, result.stderr).toBe(0);
const report = JSON.parse(readFileSync(reportPath, "utf8"));
expect(report.results).toHaveLength(1);
expect(report.results[0].status).toBe("ok");
expect(report.results[0].maxRssMb).toEqual(expect.any(Number));
expect(report.results[0].stderrPreview).toContain("[output truncated");
expect(report.results[0].stderrPreview).toContain("[stderr preview truncated");
expect(report.results[0].stderrPreview).toContain("exit tail");
expect(report.results[0].stderrPreview).not.toContain("old stderr");
expect(report.results[0].stderrPreview.length).toBeLessThan(9_000);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("resolves spawn errors without waiting for the timeout", async () => {
const startedAt = Date.now();
const result = await runCase({
repoRoot: process.cwd(),
env: process.env,
hookPath: "missing-hook.mjs",
name: "spawn-error",
body: "",
timeoutMs: 30_000,
spawnImpl: () => {
const child = new EventEmitter() as EventEmitter & {
kill: () => boolean;
stderr: EventEmitter;
stdout: EventEmitter;
};
child.stderr = new EventEmitter();
child.stdout = new EventEmitter();
child.kill = () => true;
queueMicrotask(() => child.emit("error", new Error("spawn denied")));
return child;
},
});
expect(Date.now() - startedAt).toBeLessThan(1000);
expect(result).toMatchObject({
code: null,
error: "spawn denied",
name: "spawn-error",
signal: null,
timedOut: false,
});
});
});