Files
openclaw/test/scripts/check-extension-package-tsc-boundary.test.ts
2026-04-07 12:11:17 +01:00

235 lines
7.4 KiB
TypeScript

import { EventEmitter } from "node:events";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
acquireBoundaryCheckLock,
cleanupCanaryArtifactsForExtensions,
formatBoundaryCheckSuccessSummary,
formatStepFailure,
installCanaryArtifactCleanup,
resolveBoundaryCheckLockPath,
resolveCanaryArtifactPaths,
runNodeStepAsync,
runNodeStepsWithConcurrency,
} from "../../scripts/check-extension-package-tsc-boundary.mjs";
const tempRoots = new Set<string>();
function createTempExtensionRoot(extensionId = "demo") {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-boundary-canary-"));
tempRoots.add(rootDir);
const extensionRoot = path.join(rootDir, "extensions", extensionId);
fs.mkdirSync(extensionRoot, { recursive: true });
return { rootDir, extensionRoot };
}
function writeCanaryArtifacts(rootDir: string, extensionId = "demo") {
const { canaryPath, tsconfigPath } = resolveCanaryArtifactPaths(extensionId, rootDir);
fs.writeFileSync(canaryPath, "export {};\n", "utf8");
fs.writeFileSync(tsconfigPath, '{ "extends": "./tsconfig.json" }\n', "utf8");
return { canaryPath, tsconfigPath };
}
afterEach(() => {
for (const rootDir of tempRoots) {
fs.rmSync(rootDir, { force: true, recursive: true });
}
tempRoots.clear();
});
describe("check-extension-package-tsc-boundary", () => {
it("removes stale canary artifacts across extensions", () => {
const { rootDir } = createTempExtensionRoot();
const { canaryPath, tsconfigPath } = writeCanaryArtifacts(rootDir);
cleanupCanaryArtifactsForExtensions(["demo"], rootDir);
expect(fs.existsSync(canaryPath)).toBe(false);
expect(fs.existsSync(tsconfigPath)).toBe(false);
});
it("cleans canary artifacts again on process exit", () => {
const { rootDir } = createTempExtensionRoot();
const { canaryPath, tsconfigPath } = writeCanaryArtifacts(rootDir);
const processObject = new EventEmitter();
const teardown = installCanaryArtifactCleanup(["demo"], { processObject, rootDir });
processObject.emit("exit");
teardown();
expect(fs.existsSync(canaryPath)).toBe(false);
expect(fs.existsSync(tsconfigPath)).toBe(false);
});
it("cleans stale artifacts for every extension id passed to the cleanup hook", () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-boundary-canary-"));
tempRoots.add(rootDir);
fs.mkdirSync(path.join(rootDir, "extensions", "demo-a"), { recursive: true });
fs.mkdirSync(path.join(rootDir, "extensions", "demo-b"), { recursive: true });
const demoA = writeCanaryArtifacts(rootDir, "demo-a");
const demoB = writeCanaryArtifacts(rootDir, "demo-b");
const processObject = new EventEmitter();
const teardown = installCanaryArtifactCleanup(["demo-a", "demo-b"], {
processObject,
rootDir,
});
processObject.emit("exit");
teardown();
expect(fs.existsSync(demoA.canaryPath)).toBe(false);
expect(fs.existsSync(demoA.tsconfigPath)).toBe(false);
expect(fs.existsSync(demoB.canaryPath)).toBe(false);
expect(fs.existsSync(demoB.tsconfigPath)).toBe(false);
});
it("blocks concurrent boundary checks in the same checkout", () => {
const { rootDir } = createTempExtensionRoot();
const processObject = new EventEmitter();
const release = acquireBoundaryCheckLock({ processObject, rootDir });
let thrownError = null;
try {
acquireBoundaryCheckLock({ rootDir });
} catch (error) {
thrownError = error;
}
expect(thrownError).toMatchObject({
message: expect.stringContaining("kind: lock-contention"),
fullOutput: expect.stringContaining(
"another extension package boundary check is already running",
),
kind: "lock-contention",
});
release();
const lockPath = resolveBoundaryCheckLockPath(rootDir);
expect(fs.existsSync(lockPath)).toBe(false);
});
it("summarizes long failure output with the useful tail", () => {
const stdout = Array.from({ length: 45 }, (_, index) => `stdout ${index + 1}`).join("\n");
const stderr = Array.from({ length: 3 }, (_, index) => `stderr ${index + 1}`).join("\n");
const message = formatStepFailure("demo-plugin", {
stdout,
stderr,
kind: "timeout",
elapsedMs: 4_321,
note: "demo-plugin timed out after 5000ms",
});
const messageLines = message.split("\n");
expect(message).toContain("demo-plugin");
expect(message).toContain("[... 5 earlier lines omitted ...]");
expect(message).toContain("kind: timeout");
expect(message).toContain("elapsed: 4321ms");
expect(message).toContain("stdout 45");
expect(messageLines).not.toContain("stdout 1");
expect(message).toContain("stderr:\nstderr 1\nstderr 2\nstderr 3");
expect(message).toContain("demo-plugin timed out after 5000ms");
});
it("formats a success summary with counts and elapsed time", () => {
expect(
formatBoundaryCheckSuccessSummary({
mode: "all",
compileCount: 97,
canaryCount: 12,
prepElapsedMs: 12_345,
compileElapsedMs: 54_321,
canaryElapsedMs: 6_789,
elapsedMs: 54_321,
}),
).toBe(
[
"extension package boundary check passed",
"mode: all",
"compiled plugins: 97",
"canary plugins: 12",
"prep elapsed: 12345ms",
"compile elapsed: 54321ms",
"canary elapsed: 6789ms",
"elapsed: 54321ms",
"",
].join("\n"),
);
});
it("omits phase timings that never ran", () => {
expect(
formatBoundaryCheckSuccessSummary({
mode: "compile",
compileCount: 97,
canaryCount: 0,
prepElapsedMs: 12_345,
compileElapsedMs: 54_321,
canaryElapsedMs: 0,
elapsedMs: 66_666,
}),
).toBe(
[
"extension package boundary check passed",
"mode: compile",
"compiled plugins: 97",
"canary plugins: 0",
"prep elapsed: 12345ms",
"compile elapsed: 54321ms",
"elapsed: 66666ms",
"",
].join("\n"),
);
});
it("keeps full failure output on the thrown error for canary detection", async () => {
await expect(
runNodeStepAsync(
"demo-plugin",
[
"--eval",
[
"console.log('src/cli/acp-cli.ts');",
"for (let index = 1; index <= 45; index += 1) console.log(`stdout ${index}`);",
"console.error('TS6059');",
"process.exit(2);",
].join(" "),
],
5_000,
),
).rejects.toMatchObject({
message: expect.stringContaining("[... 6 earlier lines omitted ...]"),
fullOutput: expect.stringContaining("src/cli/acp-cli.ts"),
kind: "nonzero-exit",
elapsedMs: expect.any(Number),
});
});
it("aborts concurrent sibling steps after the first failure", async () => {
const startedAt = Date.now();
await expect(
runNodeStepsWithConcurrency(
[
{
label: "fail-fast",
args: ["--eval", "setTimeout(() => process.exit(2), 10)"],
timeoutMs: 5_000,
},
{
label: "slow-step",
args: ["--eval", "setTimeout(() => {}, 10_000)"],
timeoutMs: 5_000,
},
],
2,
),
).rejects.toThrow("fail-fast");
expect(Date.now() - startedAt).toBeLessThan(2_000);
});
});