Files
openclaw/test/scripts/bench-agent-concurrency.test.ts
T
Peter Steinberger 8616c0c374 refactor: finish shared test helper migrations (#120996)
* test: finish shared helper migrations

* test: fix helper migration CI

* style: fix test import ordering

* test(acpx): restore deferred void types

* test: fix helper migrations after rebase
2026-08-09 06:00:06 -07:00

345 lines
11 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import { testing as workerTesting } from "../../scripts/bench-agent-concurrency-worker.ts";
import {
testing,
WORKER_RESULT_SENTINEL,
type WorkerResult,
type WorkerScenario,
} from "../../scripts/bench-agent-concurrency.ts";
import {
resetGatewayWorkAdmission,
runWithGatewayIndependentRootWorkAdmission,
} from "../../src/process/gateway-work-admission.js";
import { createDeferred } from "../helpers/promise.js";
function workerResult(scenario: WorkerScenario, size: number, timingsMs = [1, 2, 3]): WorkerResult {
const invariant: Record<string, number | boolean> =
scenario === "spawnPipelineInMemory" || scenario === "spawnPipelineDurable"
? {
ok: true,
registeredRuns: size,
reservationsReleased: size,
blockedWaits: size,
settledRuns: size,
settledTasks: size,
outstandingWaits: 0,
durableSubagentRows: scenario === "spawnPipelineDurable" ? size : 0,
durableTaskRows: scenario === "spawnPipelineDurable" ? size : 0,
durableStateFile: scenario === "spawnPipelineDurable",
postTeardownRegistryRows: 0,
postTeardownTaskRows: 0,
postTeardownDurableSubagentRows: 0,
postTeardownDurableTaskRows: 0,
postTeardownActiveRootWork: 0,
}
: scenario === "admission"
? { ok: true, admissionCap: size, overflowRejected: true, released: true }
: scenario === "recoverySweep"
? {
ok: true,
seededRows: size * 3,
removedRows: size * 2,
retainedCurrent: size,
sessionEffects: 0,
recoveryProjections: size,
lostContextCompletions: 0,
}
: {
ok: true,
inputRowsPerOrdering: size * 3,
newestFirstSelectedRows: size,
oldestFirstSelectedRows: size,
newestFirstSelectedNewest: true,
oldestFirstSelectedNewest: true,
};
return {
scenario,
size,
timingsMs,
memory: {
rssStartBytes: 100,
rssEndBytes: 120,
processMaxRssBytes: 150,
},
invariant,
};
}
function workerStdout(result: WorkerResult): string {
return `${WORKER_RESULT_SENTINEL}${JSON.stringify(result)}\n`;
}
describe("agent concurrency benchmark", () => {
it("parses bounded options and rejects ambiguous arguments", () => {
expect(
testing.parseOptions([
"--runs",
"2",
"--warmup",
"0",
"--fanout",
"1,4",
"--sweep-rows",
"8,16",
"--output",
"bench.json",
"--json",
]),
).toMatchObject({
runs: 2,
warmup: 0,
fanout: [1, 4],
sweepRows: [8, 16],
output: "bench.json",
json: true,
});
expect(() => testing.parseOptions(["--runs", "101"])).toThrow("--runs must be at most 100");
expect(() => testing.parseOptions(["--fanout", "1,1"])).toThrow(
"--fanout contains duplicate values",
);
expect(() => testing.parseOptions(["--runs", "1", "--runs", "2"])).toThrow(
"--runs was provided more than once",
);
expect(() => testing.parseOptions(["--wat"])).toThrow("Unknown argument: --wat");
});
it("emits tail percentiles only when the sample supports them", () => {
expect(testing.summarizeTimings([100, 1, 4, 2, 3])).toEqual({
count: 5,
min: 1,
p50: 3,
max: 100,
});
const summary = testing.summarizeTimings(Array.from({ length: 20 }, (_, index) => index + 1));
expect(summary).toMatchObject({ count: 20, p50: 10, p95: 19, p99: 20, max: 20 });
});
it("drains detached gateway root work before the next spawn sample", async () => {
resetGatewayWorkAdmission();
const deferred = createDeferred();
const rootWork = runWithGatewayIndependentRootWorkAdmission(() => deferred.promise);
try {
const drain = workerTesting.drainSpawnSampleRootWork();
await expect(
Promise.race([
drain.then(() => "drained"),
new Promise<string>((resolve) => {
setImmediate(() => resolve("pending"));
}),
]),
).resolves.toBe("pending");
deferred.resolve();
await expect(drain).resolves.toBeUndefined();
} finally {
deferred.resolve();
await rootWork;
resetGatewayWorkAdmission();
}
});
it("rejects a spawn sample when detached gateway root work does not drain", async () => {
await expect(
workerTesting.drainSpawnSampleRootWork(async (timeoutMs) => {
expect(timeoutMs).toBe(30_000);
return { drained: false, active: 2 };
}),
).rejects.toThrow("spawn sample left 2 active gateway root work items");
});
it("aggregates synthetic worker results into schema version 2", () => {
const options = testing.parseOptions([
"--runs",
"3",
"--warmup",
"1",
"--fanout",
"2",
"--sweep-rows",
"4",
]);
const report = testing.aggregateWorkerResults(
options,
[
workerResult("spawnPipelineInMemory", 2),
workerResult("spawnPipelineDurable", 2),
workerResult("admission", 2),
workerResult("recoverySweep", 4),
workerResult("duplicateSuppression", 4),
],
{ rssStartBytes: 10, rssEndBytes: 20 },
);
expect(report).toMatchObject({
schemaVersion: 2,
options: { runs: 3, warmup: 1, fanout: [2], sweepRows: [4] },
memory: {
rssStartBytes: 10,
rssEndBytes: 20,
workerProcessMaxRssBytes: 150,
},
invariants: {
ok: true,
failures: [],
spawnPipelineInMemory: true,
spawnPipelineDurable: true,
admissionCapOverflowRelease: true,
sweepRecoveryRowsWithoutSessionEffects: true,
dedupeNewestPerChild: true,
},
});
expect(report.scenarios.spawnPipelineDurable[0]?.timingsMs).toEqual({
count: 3,
min: 1,
p50: 2,
max: 3,
});
expect(report.generatedAt).toEqual(expect.any(String));
});
it("reports deterministic parent progress around every worker", () => {
const options = testing.parseOptions([
"--runs",
"3",
"--warmup",
"0",
"--fanout",
"2",
"--sweep-rows",
"4",
]);
const progress: string[] = [];
let now = 0;
const report = testing.benchmark(options, {
runWorker: (_options, scenario, size) => workerResult(scenario, size),
writeProgress: (line) => progress.push(line),
now: () => {
const value = now;
now += 250;
return value;
},
});
expect(report.invariants.ok).toBe(true);
expect(progress).toHaveLength(10);
expect(progress[0]).toBe(
"[bench-agent-concurrency] worker 1/5 start scenario=spawnPipelineInMemory size=2",
);
expect(progress[1]).toBe(
"[bench-agent-concurrency] worker 1/5 complete scenario=spawnPipelineInMemory size=2 elapsed=0.250s",
);
expect(progress.at(-2)).toBe(
"[bench-agent-concurrency] worker 5/5 start scenario=duplicateSuppression size=4",
);
expect(progress.at(-1)).toBe(
"[bench-agent-concurrency] worker 5/5 complete scenario=duplicateSuppression size=4 elapsed=0.250s",
);
});
it("rejects incomplete synthetic worker sets", () => {
const options = testing.parseOptions(["--fanout", "1", "--sweep-rows", "1"]);
expect(() =>
testing.aggregateWorkerResults(options, [workerResult("spawnPipelineInMemory", 1)]),
).toThrow("worker result mismatch");
});
it("rejects malformed, duplicate, mismatched, partial, and timed-out worker results", () => {
const expected = { scenario: "admission" as const, size: 2, runs: 3 };
const valid = workerResult("admission", 2);
expect(
testing.parseWorkerProcessResult(
{ status: 0, stdout: workerStdout(valid), stderr: "" },
expected,
),
).toEqual(valid);
expect(() =>
testing.parseWorkerProcessResult(
{ status: 0, stdout: `${WORKER_RESULT_SENTINEL}{\n`, stderr: "" },
expected,
),
).toThrow("returned invalid JSON");
expect(() =>
testing.parseWorkerProcessResult(
{ status: 0, stdout: `${workerStdout(valid)}${workerStdout(valid)}`, stderr: "" },
expected,
),
).toThrow("returned 2 result payloads");
expect(() =>
testing.parseWorkerProcessResult(
{ status: 0, stdout: workerStdout(workerResult("admission", 3)), stderr: "" },
expected,
),
).toThrow("mismatched identity");
expect(() =>
testing.parseWorkerProcessResult(
{
status: 0,
stdout: `${WORKER_RESULT_SENTINEL}${JSON.stringify({ ...valid, memory: {} })}\n`,
stderr: "",
},
expected,
),
).toThrow("memory.rssStartBytes");
expect(() =>
testing.parseWorkerProcessResult(
{
status: 0,
stdout: workerStdout({ ...valid, timingsMs: [1, 2] }),
stderr: "",
},
expected,
),
).toThrow("returned 2 samples; expected 3");
const missingInvariant = structuredClone(valid);
delete missingInvariant.invariant.admissionCap;
expect(() =>
testing.parseWorkerProcessResult(
{ status: 0, stdout: workerStdout(missingInvariant), stderr: "" },
expected,
),
).toThrow("invariant.admissionCap is missing or invalid");
expect(() =>
testing.parseWorkerProcessResult(
{ status: 7, stdout: "", stderr: "worker exploded" },
expected,
),
).toThrow("failed (7): worker exploded");
expect(() =>
testing.parseWorkerProcessResult(
{
status: null,
stdout: "",
stderr: "",
error: Object.assign(new Error("spawnSync timed out"), { code: "ETIMEDOUT" }),
},
expected,
),
).toThrow("timed out after 300000ms");
});
it("supports help and ends failures with the marker", () => {
const help = spawnSync(
process.execPath,
["--import", "tsx", "scripts/bench-agent-concurrency.ts", "--help"],
{ cwd: process.cwd(), encoding: "utf8", env: { ...process.env, NODE_NO_WARNINGS: "1" } },
);
expect(help.status).toBe(0);
expect(help.stdout).toContain("OpenClaw agent concurrency benchmark");
expect(help.stdout).toContain("--sweep-rows <list>");
expect(help.stderr).toBe("");
const failure = spawnSync(
process.execPath,
["--import", "tsx", "scripts/bench-agent-concurrency.ts", "--wat"],
{ cwd: process.cwd(), encoding: "utf8", env: { ...process.env, NODE_NO_WARNINGS: "1" } },
);
expect(failure.status).toBe(1);
expect(failure.stdout).toBe("");
expect(failure.stderr.trim().split("\n").at(-1)).toBe(
"[bench-agent-concurrency] FAILED (exit 1)",
);
});
});