mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:40:49 +00:00
test(qa): add gateway CPU scenario pack
This commit is contained in:
@@ -22,6 +22,7 @@ import { formatQaGatewayLogsForError, redactQaGatewayDebugText } from "./gateway
|
||||
import { startQaGatewayRpcClient } from "./gateway-rpc-client.js";
|
||||
import { splitQaModelRef, type QaProviderMode } from "./model-selection.js";
|
||||
import { resolveQaNodeExecPath } from "./node-exec.js";
|
||||
import { readProcessTreeCpuMs } from "./process-tree-cpu.js";
|
||||
import {
|
||||
normalizeQaProviderModeEnv,
|
||||
QA_LIVE_PROVIDER_CONFIG_PATH_ENV,
|
||||
@@ -825,6 +826,7 @@ export async function startQaGatewayChild(params: {
|
||||
baseUrl,
|
||||
wsUrl,
|
||||
pid: child.pid ?? null,
|
||||
getProcessCpuMs: () => readProcessTreeCpuMs(activeChild.pid ?? null),
|
||||
token: gatewayToken,
|
||||
workspaceDir,
|
||||
tempRoot,
|
||||
|
||||
16
extensions/qa-lab/src/process-tree-cpu.test.ts
Normal file
16
extensions/qa-lab/src/process-tree-cpu.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parsePsCpuTimeMs } from "./process-tree-cpu.js";
|
||||
|
||||
describe("process tree CPU helpers", () => {
|
||||
it("parses ps CPU time strings", () => {
|
||||
expect(parsePsCpuTimeMs("00:01")).toBe(1_000);
|
||||
expect(parsePsCpuTimeMs("01:02")).toBe(62_000);
|
||||
expect(parsePsCpuTimeMs("01:02:03")).toBe(3_723_000);
|
||||
});
|
||||
|
||||
it("rejects malformed ps CPU time strings", () => {
|
||||
expect(parsePsCpuTimeMs("")).toBeNull();
|
||||
expect(parsePsCpuTimeMs("nope")).toBeNull();
|
||||
expect(parsePsCpuTimeMs("1:2:3:4")).toBeNull();
|
||||
});
|
||||
});
|
||||
72
extensions/qa-lab/src/process-tree-cpu.ts
Normal file
72
extensions/qa-lab/src/process-tree-cpu.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
export function parsePsCpuTimeMs(raw: string): number | null {
|
||||
const parts = raw.trim().split(":").map(Number);
|
||||
if (parts.some((part) => !Number.isFinite(part) || part < 0)) {
|
||||
return null;
|
||||
}
|
||||
if (parts.length === 2) {
|
||||
return Math.round((parts[0] * 60 + parts[1]) * 1000);
|
||||
}
|
||||
if (parts.length === 3) {
|
||||
return Math.round((parts[0] * 60 * 60 + parts[1] * 60 + parts[2]) * 1000);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function readProcessTreeCpuMs(rootPid: number | null | undefined): number | null {
|
||||
if (
|
||||
typeof rootPid !== "number" ||
|
||||
!Number.isInteger(rootPid) ||
|
||||
rootPid <= 0 ||
|
||||
process.platform === "win32"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const result = spawnSync("ps", ["-eo", "pid=,ppid=,time="], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const childrenByParent = new Map<number, number[]>();
|
||||
const cpuByPid = new Map<number, number>();
|
||||
for (const line of result.stdout.split("\n")) {
|
||||
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)$/u);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const [, pidRaw, ppidRaw, cpuRaw] = match;
|
||||
const pid = Number(pidRaw);
|
||||
const ppid = Number(ppidRaw);
|
||||
const cpuMs = parsePsCpuTimeMs(cpuRaw ?? "");
|
||||
if (!Number.isInteger(pid) || !Number.isInteger(ppid) || cpuMs === null) {
|
||||
continue;
|
||||
}
|
||||
cpuByPid.set(pid, cpuMs);
|
||||
const children = childrenByParent.get(ppid) ?? [];
|
||||
children.push(pid);
|
||||
childrenByParent.set(ppid, children);
|
||||
}
|
||||
if (!cpuByPid.has(rootPid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let totalCpuMs = 0;
|
||||
const seen = new Set<number>();
|
||||
const stack: number[] = [rootPid];
|
||||
while (stack.length > 0) {
|
||||
const pid = stack.pop();
|
||||
if (pid === undefined || seen.has(pid)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(pid);
|
||||
totalCpuMs += cpuByPid.get(pid) ?? 0;
|
||||
for (const childPid of childrenByParent.get(pid) ?? []) {
|
||||
stack.push(childPid);
|
||||
}
|
||||
}
|
||||
return totalCpuMs;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export type QaRuntimeGatewayClient = {
|
||||
tempRoot: string;
|
||||
workspaceDir: string;
|
||||
runtimeEnv: NodeJS.ProcessEnv;
|
||||
getProcessCpuMs?: () => number | null;
|
||||
restartAfterStateMutation?: (
|
||||
mutateState: (context: {
|
||||
configPath: string;
|
||||
|
||||
@@ -14,6 +14,11 @@ export type QaSuiteSummaryJson = {
|
||||
passed: number;
|
||||
failed: number;
|
||||
};
|
||||
metrics?: {
|
||||
wallMs: number;
|
||||
gatewayProcessCpuMs?: number | null;
|
||||
gatewayCpuCoreRatio?: number | null;
|
||||
};
|
||||
run: {
|
||||
startedAt: string;
|
||||
finishedAt: string;
|
||||
|
||||
@@ -98,4 +98,20 @@ describe("buildQaSuiteSummaryJson", () => {
|
||||
failed: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("records optional runtime metrics when provided", () => {
|
||||
const json = buildQaSuiteSummaryJson({
|
||||
...baseParams,
|
||||
metrics: {
|
||||
wallMs: 12_000,
|
||||
gatewayProcessCpuMs: 3_400,
|
||||
gatewayCpuCoreRatio: 0.283,
|
||||
},
|
||||
});
|
||||
expect(json.metrics).toEqual({
|
||||
wallMs: 12_000,
|
||||
gatewayProcessCpuMs: 3_400,
|
||||
gatewayCpuCoreRatio: 0.283,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -277,6 +277,7 @@ export type QaSuiteSummaryJsonParams = {
|
||||
scenarios: QaSuiteScenarioResult[];
|
||||
startedAt: Date;
|
||||
finishedAt: Date;
|
||||
metrics?: QaSuiteSummaryJson["metrics"];
|
||||
providerMode: QaProviderMode;
|
||||
primaryModel: string;
|
||||
alternateModel: string;
|
||||
@@ -317,6 +318,7 @@ export function buildQaSuiteSummaryJson(params: QaSuiteSummaryJsonParams): QaSui
|
||||
passed: params.scenarios.filter((scenario) => scenario.status === "pass").length,
|
||||
failed: countQaSuiteFailedScenarios(params.scenarios),
|
||||
},
|
||||
...(params.metrics ? { metrics: params.metrics } : {}),
|
||||
run: {
|
||||
startedAt: params.startedAt.toISOString(),
|
||||
finishedAt: params.finishedAt.toISOString(),
|
||||
@@ -340,6 +342,7 @@ async function writeQaSuiteArtifacts(params: {
|
||||
startedAt: Date;
|
||||
finishedAt: Date;
|
||||
scenarios: QaSuiteScenarioResult[];
|
||||
metrics?: QaSuiteSummaryJson["metrics"];
|
||||
transport: QaTransportAdapter;
|
||||
// Reuse the canonical QaProviderMode union instead of re-declaring it
|
||||
// inline. Loop 6 already unified `QaSuiteSummaryJsonParams.providerMode`
|
||||
@@ -376,6 +379,27 @@ async function writeQaSuiteArtifacts(params: {
|
||||
return { report, reportPath, summaryPath };
|
||||
}
|
||||
|
||||
function buildQaSuiteRuntimeMetrics(params: {
|
||||
startedAt: Date;
|
||||
finishedAt: Date;
|
||||
gatewayProcessCpuStartMs: number | null;
|
||||
gatewayProcessCpuEndMs: number | null;
|
||||
}): QaSuiteSummaryJson["metrics"] {
|
||||
const wallMs = Math.max(1, params.finishedAt.getTime() - params.startedAt.getTime());
|
||||
if (params.gatewayProcessCpuStartMs === null || params.gatewayProcessCpuEndMs === null) {
|
||||
return { wallMs };
|
||||
}
|
||||
const gatewayProcessCpuMs = Math.max(
|
||||
0,
|
||||
params.gatewayProcessCpuEndMs - params.gatewayProcessCpuStartMs,
|
||||
);
|
||||
return {
|
||||
wallMs,
|
||||
gatewayProcessCpuMs,
|
||||
gatewayCpuCoreRatio: Math.round((gatewayProcessCpuMs / wallMs) * 1000) / 1000,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResult> {
|
||||
const startedAt = new Date();
|
||||
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
|
||||
@@ -730,6 +754,7 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
|
||||
scenarios: liveScenarioOutcomes,
|
||||
});
|
||||
|
||||
const gatewayProcessCpuStartMs = gateway.getProcessCpuMs?.() ?? null;
|
||||
for (const [index, scenario] of selectedCatalogScenarios.entries()) {
|
||||
const scenarioIdForLog = sanitizeQaSuiteProgressValue(scenario.id);
|
||||
writeQaSuiteProgress(
|
||||
@@ -773,6 +798,12 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
|
||||
}
|
||||
|
||||
const finishedAt = new Date();
|
||||
const metrics = buildQaSuiteRuntimeMetrics({
|
||||
startedAt,
|
||||
finishedAt,
|
||||
gatewayProcessCpuStartMs,
|
||||
gatewayProcessCpuEndMs: gateway.getProcessCpuMs?.() ?? null,
|
||||
});
|
||||
const failedCount = scenarios.filter((scenario) => scenario.status === "fail").length;
|
||||
if (scenarios.some((scenario) => scenario.status === "fail")) {
|
||||
preserveGatewayRuntimeDir = path.join(outputDir, "artifacts", "gateway-runtime");
|
||||
@@ -789,6 +820,7 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
|
||||
startedAt,
|
||||
finishedAt,
|
||||
scenarios,
|
||||
metrics,
|
||||
transport,
|
||||
providerMode,
|
||||
primaryModel,
|
||||
|
||||
Reference in New Issue
Block a user