Files
openclaw/extensions/qa-lab/src/self-check.ts
Marcus Castro 000fc7f233 refactor(qa): add shared QA channel contract and harden worker startup (#64562)
* refactor(qa): add shared transport contract and suite migration

* refactor(qa): harden worker gateway startup

* fix(qa): scope waits and sanitize shutdown artifacts

* fix(qa): confine artifacts and redact preserved logs

* fix(qa): block symlink escapes in artifact paths

* fix(gateway): clear shutdown race timers

* fix(qa): harden shutdown cleanup paths

* fix(qa): sanitize gateway logs in thrown errors

* fix(qa): harden suite startup and artifact paths

* fix(qa): stage bundled plugins from mutated config

* fix(qa): broaden gateway log bearer redaction

* fix(qa-channel): restore runtime export

* fix(qa): stop failed gateway startups as a process tree

* fix(qa-channel): load runtime hook from api surface
2026-04-12 15:02:57 -03:00

102 lines
3.3 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { QaBusState } from "./bus-state.js";
import { createQaTransportAdapter, type QaTransportId } from "./qa-transport-registry.js";
import { renderQaMarkdownReport } from "./report.js";
import { runQaScenario, type QaScenarioResult } from "./scenario.js";
import { createQaSelfCheckScenario } from "./self-check-scenario.js";
export type QaSelfCheckResult = {
outputPath: string;
report: string;
checks: Array<{ name: string; status: "pass" | "fail"; details?: string }>;
scenarioResult: QaScenarioResult;
};
export function resolveQaSelfCheckOutputPath(params?: { outputPath?: string; repoRoot?: string }) {
if (params?.outputPath) {
return params.outputPath;
}
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
return path.join(repoRoot, ".artifacts", "qa-e2e", "self-check.md");
}
export async function runQaSelfCheckAgainstState(params: {
state: QaBusState;
cfg: OpenClawConfig;
transportId?: QaTransportId;
outputPath?: string;
repoRoot?: string;
notes?: string[];
}): Promise<QaSelfCheckResult> {
const startedAt = new Date();
const transport = createQaTransportAdapter({
id: params.transportId ?? "qa-channel",
state: params.state,
});
params.state.reset();
const scenarioResult = await runQaScenario(createQaSelfCheckScenario(), {
state: params.state,
performAction: async (action, args) =>
await transport.handleAction({
action,
args,
cfg: params.cfg,
accountId: transport.accountId,
}),
});
const checks = [
{
name: "QA self-check scenario",
status: scenarioResult.status,
details: `${scenarioResult.steps.filter((step) => step.status === "pass").length}/${scenarioResult.steps.length} steps passed`,
},
] satisfies Array<{ name: string; status: "pass" | "fail"; details?: string }>;
const finishedAt = new Date();
const snapshot = params.state.getSnapshot();
const timeline = snapshot.events.map((event) => {
switch (event.kind) {
case "thread-created":
return `${event.cursor}. ${event.kind} ${event.thread.conversationId}/${event.thread.id}`;
case "reaction-added":
return `${event.cursor}. ${event.kind} ${event.message.id} ${event.emoji}`;
default:
return `${event.cursor}. ${event.kind} ${"message" in event ? event.message.id : ""}`.trim();
}
});
const report = renderQaMarkdownReport({
title: "OpenClaw QA E2E Self-Check",
startedAt,
finishedAt,
checks,
scenarios: [
{
name: scenarioResult.name,
status: scenarioResult.status,
details: scenarioResult.details,
steps: scenarioResult.steps,
},
],
timeline,
notes: params.notes ?? [
"Vertical slice: qa-channel + qa-lab bus + private debugger surface.",
"Docker orchestration, matrix runs, and auto-fix loops remain follow-up work.",
],
});
const outputPath = resolveQaSelfCheckOutputPath({
outputPath: params.outputPath,
repoRoot: params.repoRoot,
});
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, report, "utf8");
return {
outputPath,
report,
checks,
scenarioResult,
};
}