Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
1000 lines
36 KiB
TypeScript
1000 lines
36 KiB
TypeScript
// Qa Lab tests cover agentic parity report plugin behavior.
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildQaAgenticParityComparison,
|
|
buildQaRuntimeParityReport,
|
|
computeQaAgenticParityMetrics,
|
|
QaParityLabelMismatchError,
|
|
renderQaAgenticParityMarkdownReport,
|
|
renderQaRuntimeParityMarkdownReport,
|
|
type QaParityReportScenario,
|
|
type QaParitySuiteSummary,
|
|
type QaRuntimeParitySuiteSummary,
|
|
} from "./agentic-parity-report.js";
|
|
|
|
const FULL_PARITY_PASS_SCENARIOS: QaParityReportScenario[] = [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{ name: "Compaction retry after mutating tool", status: "pass" },
|
|
{ name: "Model switch with tool continuity", status: "pass" },
|
|
{ name: "Source and docs discovery report", status: "pass" },
|
|
{ name: "Image understanding from attachment", status: "pass" },
|
|
{ name: "Subagent handoff", status: "pass" },
|
|
{ name: "Subagent fanout synthesis", status: "pass" },
|
|
{ name: "Subagent stale child links", status: "pass" },
|
|
{ name: "Memory recall after context switch", status: "pass" },
|
|
{ name: "Thread memory isolation", status: "pass" },
|
|
{ name: "Config restart capability flip", status: "pass" },
|
|
{ name: "Instruction followthrough repo contract", status: "pass" },
|
|
];
|
|
|
|
function withScenarioOverride(name: string, override: Partial<QaParityReportScenario>) {
|
|
return FULL_PARITY_PASS_SCENARIOS.map((scenario) =>
|
|
scenario.name === name ? { ...scenario, ...override } : scenario,
|
|
);
|
|
}
|
|
|
|
function makeRuntimeParitySummary(): QaRuntimeParitySuiteSummary {
|
|
return {
|
|
scenarios: [
|
|
{
|
|
name: "Approval turn tool followthrough",
|
|
status: "pass",
|
|
steps: [],
|
|
runtimeParity: {
|
|
scenarioId: "approval-turn-tool-followthrough",
|
|
drift: "none",
|
|
cells: {
|
|
openclaw: {
|
|
runtime: "openclaw",
|
|
transcriptBytes: '{"role":"assistant"}\n',
|
|
toolCalls: [{ tool: "read_file", argsHash: "a", resultHash: "r" }],
|
|
finalText: "done",
|
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
|
|
wallClockMs: 20,
|
|
bootStateLines: [],
|
|
},
|
|
codex: {
|
|
runtime: "codex",
|
|
transcriptBytes: '{"role":"assistant"}\n',
|
|
toolCalls: [{ tool: "read_file", argsHash: "a", resultHash: "r" }],
|
|
finalText: "done",
|
|
usage: { inputTokens: 8, outputTokens: 4, totalTokens: 12 },
|
|
wallClockMs: 18,
|
|
bootStateLines: [],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Compaction retry after mutating tool",
|
|
status: "pass",
|
|
steps: [],
|
|
runtimeParity: {
|
|
scenarioId: "compaction-retry-after-mutating-tool",
|
|
drift: "tool-call-shape",
|
|
driftDetails: "tool call 1 differs",
|
|
cells: {
|
|
openclaw: {
|
|
runtime: "openclaw",
|
|
transcriptBytes: '{"role":"assistant"}\n',
|
|
toolCalls: [{ tool: "read_file", argsHash: "a", resultHash: "r" }],
|
|
finalText: "done",
|
|
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
|
|
wallClockMs: 20,
|
|
bootStateLines: [],
|
|
},
|
|
codex: {
|
|
runtime: "codex",
|
|
transcriptBytes: '{"role":"assistant"}\n',
|
|
toolCalls: [{ tool: "read_file", argsHash: "b", resultHash: "r" }],
|
|
finalText: "done",
|
|
usage: { inputTokens: 9, outputTokens: 4, totalTokens: 13 },
|
|
wallClockMs: 19,
|
|
bootStateLines: [],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
counts: {
|
|
total: 2,
|
|
passed: 2,
|
|
failed: 0,
|
|
},
|
|
run: {
|
|
providerMode: "mock-openai",
|
|
primaryModel: "openai/gpt-5.5",
|
|
runtimePair: ["openclaw", "codex"],
|
|
},
|
|
};
|
|
}
|
|
|
|
function firstRuntimeParityScenario() {
|
|
const scenario = makeRuntimeParitySummary().scenarios[0];
|
|
if (!scenario) {
|
|
throw new Error("missing runtime parity scenario fixture");
|
|
}
|
|
return scenario;
|
|
}
|
|
|
|
describe("qa agentic parity report", () => {
|
|
it("computes first-wave parity metrics from suite summaries", () => {
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{
|
|
name: "Compaction retry after mutating tool",
|
|
status: "fail",
|
|
details: "incomplete turn detected",
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(computeQaAgenticParityMetrics(summary)).toEqual({
|
|
totalScenarios: 2,
|
|
passedScenarios: 1,
|
|
failedScenarios: 1,
|
|
completionRate: 0.5,
|
|
unintendedStopCount: 1,
|
|
unintendedStopRate: 0.5,
|
|
validToolCallCount: 1,
|
|
validToolCallRate: 0.5,
|
|
fakeSuccessCount: 0,
|
|
});
|
|
});
|
|
|
|
it("uses scenario rows rather than stale summary counts for parity metrics", () => {
|
|
const summary: QaParitySuiteSummary = {
|
|
counts: {
|
|
total: 2,
|
|
passed: 2,
|
|
failed: 0,
|
|
},
|
|
scenarios: [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{ name: "Compaction retry after mutating tool", status: "fail" },
|
|
],
|
|
};
|
|
|
|
const metrics = computeQaAgenticParityMetrics(summary);
|
|
|
|
expect(metrics.totalScenarios).toBe(2);
|
|
expect(metrics.passedScenarios).toBe(1);
|
|
expect(metrics.failedScenarios).toBe(1);
|
|
expect(metrics.completionRate).toBe(0.5);
|
|
});
|
|
|
|
it("keeps non-tool scenarios out of the valid-tool-call metric", () => {
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{ name: "Memory recall after context switch", status: "pass" },
|
|
{ name: "Image understanding from attachment", status: "pass" },
|
|
],
|
|
};
|
|
|
|
const metrics = computeQaAgenticParityMetrics(summary);
|
|
expect(metrics.totalScenarios).toBe(3);
|
|
expect(metrics.passedScenarios).toBe(3);
|
|
expect(metrics.validToolCallCount).toBe(1);
|
|
expect(metrics.validToolCallRate).toBe(1);
|
|
});
|
|
|
|
it("does not count passing runtime parity scenarios without tool-call evidence", () => {
|
|
const summary: QaRuntimeParitySuiteSummary = {
|
|
scenarios: [
|
|
{
|
|
name: "Approval turn tool followthrough",
|
|
status: "pass",
|
|
steps: [],
|
|
runtimeParity: {
|
|
scenarioId: "approval-turn-tool-followthrough",
|
|
drift: "none",
|
|
cells: {
|
|
openclaw: {
|
|
runtime: "openclaw",
|
|
transcriptBytes: '{"role":"assistant"}\n',
|
|
toolCalls: [],
|
|
finalText: "done",
|
|
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
wallClockMs: 10,
|
|
bootStateLines: [],
|
|
},
|
|
codex: {
|
|
runtime: "codex",
|
|
transcriptBytes: '{"role":"assistant"}\n',
|
|
toolCalls: [],
|
|
finalText: "done",
|
|
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
wallClockMs: 10,
|
|
bootStateLines: [],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const metrics = computeQaAgenticParityMetrics(summary);
|
|
|
|
expect(metrics.passedScenarios).toBe(1);
|
|
expect(metrics.validToolCallCount).toBe(0);
|
|
expect(metrics.validToolCallRate).toBe(0);
|
|
});
|
|
|
|
it("counts passing runtime parity scenarios with tool calls in both runtimes", () => {
|
|
const metrics = computeQaAgenticParityMetrics({
|
|
scenarios: [firstRuntimeParityScenario()],
|
|
});
|
|
|
|
expect(metrics.validToolCallCount).toBe(1);
|
|
expect(metrics.validToolCallRate).toBe(1);
|
|
});
|
|
|
|
it("fails the parity gate when the candidate regresses against baseline", () => {
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{
|
|
name: "Compaction retry after mutating tool",
|
|
status: "fail",
|
|
details: "timed out before it continued",
|
|
},
|
|
{ name: "Model switch with tool continuity", status: "pass" },
|
|
{ name: "Source and docs discovery report", status: "pass" },
|
|
{ name: "Image understanding from attachment", status: "pass" },
|
|
],
|
|
},
|
|
baselineSummary: {
|
|
scenarios: [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{ name: "Compaction retry after mutating tool", status: "pass" },
|
|
{ name: "Model switch with tool continuity", status: "pass" },
|
|
{ name: "Source and docs discovery report", status: "pass" },
|
|
{ name: "Image understanding from attachment", status: "pass" },
|
|
],
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(false);
|
|
expect(comparison.failures).toContain(
|
|
"openai/gpt-5.5 completion rate 80.0% is below anthropic/claude-opus-4-8 100.0%.",
|
|
);
|
|
expect(comparison.failures).toContain(
|
|
"openai/gpt-5.5 unintended-stop rate 20.0% exceeds anthropic/claude-opus-4-8 0.0%.",
|
|
);
|
|
});
|
|
|
|
it("fails the parity gate when candidate and baseline cover different non-parity scenarios", () => {
|
|
const passScenario = (name: string): QaParityReportScenario => ({ name, status: "pass" });
|
|
const baselineScenarios = [
|
|
passScenario("Approval turn tool followthrough"),
|
|
passScenario("Compaction retry after mutating tool"),
|
|
passScenario("Model switch with tool continuity"),
|
|
passScenario("Source and docs discovery report"),
|
|
passScenario("Image understanding from attachment"),
|
|
passScenario("Extra non-parity lane"),
|
|
];
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: baselineScenarios.filter(
|
|
(scenario) => scenario.name !== "Extra non-parity lane",
|
|
),
|
|
},
|
|
baselineSummary: { scenarios: baselineScenarios },
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(false);
|
|
expect(comparison.failures).toContain(
|
|
"Scenario coverage mismatch for Extra non-parity lane: openai/gpt-5.5=missing, anthropic/claude-opus-4-8=pass.",
|
|
);
|
|
});
|
|
|
|
it("reports each missing required parity scenario exactly once (no double-counting)", () => {
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: [{ name: "Approval turn tool followthrough", status: "pass" }],
|
|
},
|
|
baselineSummary: {
|
|
scenarios: [{ name: "Approval turn tool followthrough", status: "pass" }],
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(false);
|
|
const missingScenario = "Image understanding from attachment";
|
|
const requiredLines = comparison.failures.filter((failure) =>
|
|
failure.includes(`Missing required parity scenario coverage for ${missingScenario}:`),
|
|
);
|
|
const mismatchLines = comparison.failures.filter((failure) =>
|
|
failure.includes(`Scenario coverage mismatch for ${missingScenario}:`),
|
|
);
|
|
expect(requiredLines).toHaveLength(1);
|
|
expect(mismatchLines).toHaveLength(0);
|
|
});
|
|
|
|
it("scopes parity metrics to declared parity scenarios even when extra lanes are present", () => {
|
|
const scopedSummary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{ name: "Compaction retry after mutating tool", status: "pass" },
|
|
{ name: "Model switch with tool continuity", status: "pass" },
|
|
{ name: "Source and docs discovery report", status: "pass" },
|
|
{ name: "Image understanding from attachment", status: "pass" },
|
|
],
|
|
};
|
|
const summaryWithExtras: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
...scopedSummary.scenarios,
|
|
{ name: "Extra lane A", status: "fail", details: "timed out" },
|
|
{ name: "Extra lane B", status: "fail", details: "timed out" },
|
|
],
|
|
};
|
|
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: summaryWithExtras,
|
|
baselineSummary: scopedSummary,
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
// Extra lanes must not drag the candidate's completion rate below baseline
|
|
// and must not generate unintended-stop or fake-success hits.
|
|
expect(comparison.candidateMetrics.totalScenarios).toBe(5);
|
|
expect(comparison.candidateMetrics.completionRate).toBe(1);
|
|
expect(comparison.candidateMetrics.unintendedStopRate).toBe(0);
|
|
expect(comparison.candidateMetrics.fakeSuccessCount).toBe(0);
|
|
// The pass/fail verdict here still depends only on the parity pack itself.
|
|
const regressionFailures = comparison.failures.filter((failure) =>
|
|
failure.includes("completion rate"),
|
|
);
|
|
expect(regressionFailures).toStrictEqual([]);
|
|
});
|
|
|
|
it("fails the parity gate when required parity scenarios are missing on both sides", () => {
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: [{ name: "Approval turn tool followthrough", status: "pass" }],
|
|
},
|
|
baselineSummary: {
|
|
scenarios: [{ name: "Approval turn tool followthrough", status: "pass" }],
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(false);
|
|
expect(comparison.failures).toContain(
|
|
"Missing required parity scenario coverage for Image understanding from attachment: openai/gpt-5.5=missing, anthropic/claude-opus-4-8=missing.",
|
|
);
|
|
});
|
|
|
|
it("fails the parity gate when required parity scenarios are skipped", () => {
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{ name: "Compaction retry after mutating tool", status: "skip" },
|
|
{ name: "Model switch with tool continuity", status: "pass" },
|
|
{ name: "Source and docs discovery report", status: "pass" },
|
|
{ name: "Image understanding from attachment", status: "pass" },
|
|
],
|
|
},
|
|
baselineSummary: {
|
|
scenarios: [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{ name: "Compaction retry after mutating tool", status: "skip" },
|
|
{ name: "Model switch with tool continuity", status: "pass" },
|
|
{ name: "Source and docs discovery report", status: "pass" },
|
|
{ name: "Image understanding from attachment", status: "pass" },
|
|
],
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(false);
|
|
expect(comparison.failures).toContain(
|
|
"Missing required parity scenario coverage for Compaction retry after mutating tool: openai/gpt-5.5=skip, anthropic/claude-opus-4-8=skip.",
|
|
);
|
|
});
|
|
|
|
it("fails the parity gate when a required parity scenario fails on both sides", () => {
|
|
// Regression for the loop-7 Codex-connector P1 finding: without this
|
|
// check, a required parity scenario that fails on both candidate and
|
|
// baseline still produces pass=true because the downstream metric
|
|
// comparisons are purely relative (candidate vs baseline). Cover the
|
|
// whole parity pack as pass on both sides except the one scenario we
|
|
// deliberately fail on both sides, so the assertion can pin the
|
|
// isolated gate failure under test.
|
|
const scenariosWithBothFail = withScenarioOverride("Approval turn tool followthrough", {
|
|
status: "fail",
|
|
});
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: { scenarios: scenariosWithBothFail },
|
|
baselineSummary: { scenarios: scenariosWithBothFail },
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(false);
|
|
expect(comparison.failures).toContain(
|
|
"Required parity scenario Approval turn tool followthrough failed: openai/gpt-5.5=fail, anthropic/claude-opus-4-8=fail.",
|
|
);
|
|
// Metric comparisons are relative, so a same-on-both-sides failure
|
|
// must not appear as a relative metric failure. The required-scenario
|
|
// failure line is the only thing keeping the gate honest here.
|
|
expect(comparison.failures.filter((failure) => failure.includes("completion rate"))).toEqual(
|
|
[],
|
|
);
|
|
});
|
|
|
|
it("fails the parity gate when a required parity scenario fails on the candidate only", () => {
|
|
// A candidate regression below a passing baseline is already caught
|
|
// by the relative completion-rate comparison, but surface it as a
|
|
// named required-scenario failure too so operators see a concrete
|
|
// scenario name alongside the rate differential.
|
|
const candidateWithOneFail = withScenarioOverride("Approval turn tool followthrough", {
|
|
status: "fail",
|
|
});
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: { scenarios: candidateWithOneFail },
|
|
baselineSummary: { scenarios: FULL_PARITY_PASS_SCENARIOS },
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(false);
|
|
expect(comparison.failures).toContain(
|
|
"Required parity scenario Approval turn tool followthrough failed: openai/gpt-5.5=fail, anthropic/claude-opus-4-8=pass.",
|
|
);
|
|
});
|
|
|
|
it("fails the parity gate when the baseline contains suspicious pass results", () => {
|
|
// Cover the full second-wave pack on both sides so the suspicious-pass assertion
|
|
// below is the isolated gate failure under test (no coverage-gap noise).
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
},
|
|
baselineSummary: {
|
|
scenarios: withScenarioOverride("Approval turn tool followthrough", {
|
|
details: "timed out before it continued",
|
|
}),
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(false);
|
|
expect(comparison.failures).toEqual([
|
|
"anthropic/claude-opus-4-8 produced 1 suspicious pass result(s); baseline fake-success count must also be 0.",
|
|
]);
|
|
});
|
|
|
|
it("ignores neutral Failed and Blocked headings in passing protocol reports", () => {
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{
|
|
name: "Source and docs discovery report",
|
|
status: "pass",
|
|
details: `Worked:
|
|
- Read the seeded QA material.
|
|
Failed:
|
|
- None observed.
|
|
Blocked:
|
|
- No live provider evidence in this lane.
|
|
Follow-up:
|
|
- Re-run with a real provider if needed.`,
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(computeQaAgenticParityMetrics(summary).fakeSuccessCount).toBe(0);
|
|
});
|
|
|
|
it("ignores neutral error-budget and no-errors-observed phrasing in passing reports", () => {
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{
|
|
name: "Source and docs discovery report",
|
|
status: "pass",
|
|
details: `Worked:
|
|
- Scenario finished with Error budget: 0.
|
|
- No errors found in the seeded material.
|
|
- Errors: none observed.`,
|
|
},
|
|
{
|
|
name: "Image understanding from attachment",
|
|
status: "pass",
|
|
details: "Error: none. The attached image analysis completed without incident.",
|
|
},
|
|
],
|
|
};
|
|
|
|
// Bare "error"/"Error" in narration is not a suspicious-pass signal on its own.
|
|
// Only phrases like "error occurred" or "an error was ..." should count.
|
|
expect(computeQaAgenticParityMetrics(summary).fakeSuccessCount).toBe(0);
|
|
});
|
|
|
|
it("still flags genuine error-narration suspicious passes", () => {
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{
|
|
name: "Approval turn tool followthrough",
|
|
status: "pass",
|
|
details: "Tool call completed, but an error occurred mid-turn and no retry happened.",
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(computeQaAgenticParityMetrics(summary).fakeSuccessCount).toBe(1);
|
|
});
|
|
|
|
it("does not flag positive-tone prose as fake success (positive-tone detection removed)", () => {
|
|
// Positive-tone detection was removed because for passing runs the
|
|
// `details` field is the model's prose, which never contains tool-call
|
|
// evidence. Criterion 2 is enforced by per-scenario tool-call assertions.
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{
|
|
name: "Subagent handoff",
|
|
status: "pass",
|
|
details: "Successfully completed the delegation. The subagent returned its result.",
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(computeQaAgenticParityMetrics(summary).fakeSuccessCount).toBe(0);
|
|
});
|
|
|
|
it("does not flag bare 'Done.' prose as fake success", () => {
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{
|
|
name: "Approval turn tool followthrough",
|
|
status: "pass",
|
|
details: "Done.",
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(computeQaAgenticParityMetrics(summary).fakeSuccessCount).toBe(0);
|
|
});
|
|
|
|
it("does not flag structured status lines that end in `done`", () => {
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{
|
|
name: "Compaction retry after mutating tool",
|
|
status: "pass",
|
|
details: `Confirmed, replay unsafe after write.
|
|
compactionCount=0
|
|
status=done`,
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(computeQaAgenticParityMetrics(summary).fakeSuccessCount).toBe(0);
|
|
});
|
|
|
|
it("does not flag positive-tone passes when the scenario shows real tool-call evidence", () => {
|
|
// A legitimate tool-mediated pass that happens to include
|
|
// "successfully" in its prose must not be flagged. The
|
|
// `plannedToolName` evidence (or any of the other tool-call
|
|
// evidence patterns) exempts the scenario from positive-tone
|
|
// detection. Without this exemption, real tool-backed passes with
|
|
// self-congratulatory prose would count as fake successes and break
|
|
// the gate.
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{
|
|
name: "Source and docs discovery report",
|
|
status: "pass",
|
|
details:
|
|
"Successfully completed the report. plannedToolName=read recorded via /debug/requests.",
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(computeQaAgenticParityMetrics(summary).fakeSuccessCount).toBe(0);
|
|
});
|
|
|
|
it("only flags failure-tone passes, not positive-tone", () => {
|
|
const summary: QaParitySuiteSummary = {
|
|
scenarios: [
|
|
{
|
|
name: "Approval turn tool followthrough",
|
|
status: "pass",
|
|
details: "Task executed successfully without errors.",
|
|
},
|
|
{
|
|
name: "Subagent handoff",
|
|
status: "pass",
|
|
details: "Tool call completed, but an error occurred mid-turn.",
|
|
},
|
|
],
|
|
};
|
|
|
|
// Only the failure-tone scenario ("error occurred") counts.
|
|
// The positive-tone one ("successfully") is not flagged.
|
|
expect(computeQaAgenticParityMetrics(summary).fakeSuccessCount).toBe(1);
|
|
});
|
|
|
|
it("throws QaParityLabelMismatchError when the candidate run.primaryProvider does not match the label", () => {
|
|
// Regression for the gate footgun: if an operator swaps the
|
|
// --candidate-summary and --baseline-summary paths, the gate would
|
|
// silently produce a reversed verdict. PR L #64789 ships the `run`
|
|
// block on every summary so the parity report can verify it against
|
|
// the caller-supplied label; this test pins the precondition check.
|
|
const parityPassScenarios: QaParityReportScenario[] = [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
{ name: "Compaction retry after mutating tool", status: "pass" },
|
|
{ name: "Model switch with tool continuity", status: "pass" },
|
|
{ name: "Source and docs discovery report", status: "pass" },
|
|
{ name: "Image understanding from attachment", status: "pass" },
|
|
];
|
|
|
|
expect(() =>
|
|
buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: parityPassScenarios,
|
|
run: { primaryProvider: "anthropic", primaryModel: "claude-opus-4-8" },
|
|
},
|
|
baselineSummary: {
|
|
scenarios: parityPassScenarios,
|
|
run: { primaryProvider: "anthropic", primaryModel: "claude-opus-4-8" },
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
}),
|
|
).toThrow(QaParityLabelMismatchError);
|
|
});
|
|
|
|
it("throws QaParityLabelMismatchError when the baseline run.primaryProvider does not match the label", () => {
|
|
const parityPassScenarios: QaParityReportScenario[] = [
|
|
{ name: "Approval turn tool followthrough", status: "pass" },
|
|
];
|
|
|
|
expect(() =>
|
|
buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: parityPassScenarios,
|
|
run: { primaryProvider: "openai" },
|
|
},
|
|
baselineSummary: {
|
|
scenarios: parityPassScenarios,
|
|
run: { primaryProvider: "openai", primaryModel: "gpt-5.5" },
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
}),
|
|
).toThrow(
|
|
/baseline summary run\.primaryProvider=openai and run\.primaryModel=gpt-5\.5 do not match --baseline-label/,
|
|
);
|
|
});
|
|
|
|
it("accepts matching run.primaryProvider labels without throwing", () => {
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "openai",
|
|
primaryModel: "openai/gpt-5.5",
|
|
primaryModelName: "gpt-5.5",
|
|
},
|
|
},
|
|
baselineSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "anthropic",
|
|
primaryModel: "anthropic/claude-opus-4-8",
|
|
primaryModelName: "claude-opus-4-8",
|
|
},
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
expect(comparison.pass).toBe(true);
|
|
});
|
|
|
|
it("skips run.primaryProvider verification when the summary is missing a run block (legacy summaries)", () => {
|
|
// Pre-PR-L summaries don't carry a `run` block. The gate must still
|
|
// work against those, trusting the caller-supplied label.
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: { scenarios: FULL_PARITY_PASS_SCENARIOS },
|
|
baselineSummary: { scenarios: FULL_PARITY_PASS_SCENARIOS },
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
expect(comparison.pass).toBe(true);
|
|
});
|
|
|
|
it("skips provider verification for arbitrary display labels when run metadata is present", () => {
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "GPT-5.5 candidate",
|
|
baselineLabel: "Opus 4.8 baseline",
|
|
candidateSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "openai",
|
|
primaryModel: "openai/gpt-5.5",
|
|
primaryModelName: "gpt-5.5",
|
|
},
|
|
},
|
|
baselineSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "anthropic",
|
|
primaryModel: "anthropic/claude-opus-4-8",
|
|
primaryModelName: "claude-opus-4-8",
|
|
},
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(true);
|
|
});
|
|
|
|
it("skips provider verification for mixed-case or decorated display labels", () => {
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "Candidate: GPT-5.5",
|
|
baselineLabel: "Opus 4.8 / baseline",
|
|
candidateSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "openai",
|
|
primaryModel: "openai/gpt-5.5",
|
|
primaryModelName: "gpt-5.5",
|
|
},
|
|
},
|
|
baselineSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "anthropic",
|
|
primaryModel: "anthropic/claude-opus-4-8",
|
|
primaryModelName: "claude-opus-4-8",
|
|
},
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(true);
|
|
});
|
|
|
|
it("throws when a structured label mismatches the recorded model even if the provider matches", () => {
|
|
expect(() =>
|
|
buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "openai",
|
|
primaryModel: "openai/gpt-5.5-alt",
|
|
primaryModelName: "gpt-5.5-alt",
|
|
},
|
|
},
|
|
baselineSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "anthropic",
|
|
primaryModel: "anthropic/claude-opus-4-8",
|
|
primaryModelName: "claude-opus-4-8",
|
|
},
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
}),
|
|
).toThrow(
|
|
/candidate summary run\.primaryProvider=openai and run\.primaryModel=openai\/gpt-5\.5-alt do not match --candidate-label=openai\/gpt-5\.5/,
|
|
);
|
|
});
|
|
|
|
it("accepts colon-delimited structured labels when provider and model both match", () => {
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai:gpt-5.5",
|
|
baselineLabel: "anthropic:claude-opus-4-8",
|
|
candidateSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "openai",
|
|
primaryModel: "openai/gpt-5.5",
|
|
primaryModelName: "gpt-5.5",
|
|
},
|
|
},
|
|
baselineSummary: {
|
|
scenarios: FULL_PARITY_PASS_SCENARIOS,
|
|
run: {
|
|
primaryProvider: "anthropic",
|
|
primaryModel: "anthropic/claude-opus-4-8",
|
|
primaryModelName: "claude-opus-4-8",
|
|
},
|
|
},
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
expect(comparison.pass).toBe(true);
|
|
});
|
|
|
|
it("renders a readable markdown parity report", () => {
|
|
// Cover the full parity pack on both sides so the pass
|
|
// verdict is not disrupted by required-scenario coverage failures
|
|
// added by the second-wave expansion.
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5",
|
|
baselineLabel: "anthropic/claude-opus-4-8",
|
|
candidateSummary: { scenarios: FULL_PARITY_PASS_SCENARIOS },
|
|
baselineSummary: { scenarios: FULL_PARITY_PASS_SCENARIOS },
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
|
|
const report = renderQaAgenticParityMarkdownReport(comparison);
|
|
|
|
expect(report).toContain(
|
|
"# OpenClaw Agentic Parity Report — openai/gpt-5.5 vs anthropic/claude-opus-4-8",
|
|
);
|
|
expect(report).toContain("| Completion rate | 100.0% | 100.0% |");
|
|
expect(report).toContain("### Approval turn tool followthrough");
|
|
expect(report).toContain("- Verdict: pass");
|
|
});
|
|
|
|
it("parametrizes the markdown header from the comparison labels", () => {
|
|
// Regression for the loop-7 Copilot finding: callers that configure
|
|
// non-gpt-5.5 / non-opus labels (for example an internal candidate vs
|
|
// another candidate) must see the labels in the rendered H1 instead of
|
|
// the hardcoded "GPT-5.5 / Opus 4.8" title that would otherwise confuse
|
|
// readers of saved reports.
|
|
const comparison = buildQaAgenticParityComparison({
|
|
candidateLabel: "openai/gpt-5.5-alt",
|
|
baselineLabel: "openai/gpt-5.5",
|
|
candidateSummary: { scenarios: [] },
|
|
baselineSummary: { scenarios: [] },
|
|
comparedAt: "2026-04-11T00:00:00.000Z",
|
|
});
|
|
const report = renderQaAgenticParityMarkdownReport(comparison);
|
|
expect(report).toContain(
|
|
"# OpenClaw Agentic Parity Report — openai/gpt-5.5-alt vs openai/gpt-5.5",
|
|
);
|
|
});
|
|
|
|
it("builds a runtime parity report from suite summaries", () => {
|
|
const report = buildQaRuntimeParityReport({
|
|
summary: makeRuntimeParitySummary(),
|
|
comparedAt: "2026-05-10T00:00:00.000Z",
|
|
});
|
|
|
|
expect(report.runtimePair).toEqual(["openclaw", "codex"]);
|
|
expect(report.pass).toBe(true);
|
|
expect(report.driftCounts.none).toBe(1);
|
|
expect(report.driftCounts["tool-call-shape"]).toBe(1);
|
|
expect(report.failures).toEqual([]);
|
|
});
|
|
|
|
it("fails runtime parity reports when a runtime cell has a hard failure", () => {
|
|
const summary = makeRuntimeParitySummary();
|
|
const scenario = summary.scenarios[1];
|
|
if (!scenario?.runtimeParity) {
|
|
throw new Error("runtime parity fixture missing");
|
|
}
|
|
scenario.status = "fail";
|
|
scenario.runtimeParity.cells.codex.runtimeErrorClass = "auth";
|
|
|
|
const report = buildQaRuntimeParityReport({
|
|
summary,
|
|
comparedAt: "2026-05-10T00:00:00.000Z",
|
|
});
|
|
|
|
expect(report.pass).toBe(false);
|
|
expect(report.failedScenarios).toBe(1);
|
|
expect(report.failures).toContain(
|
|
"Compaction retry after mutating tool drift=tool-call-shape (tool call 1 differs).",
|
|
);
|
|
});
|
|
|
|
it("passes runtime parity reports with controlled tool-error cells and advisory drift", () => {
|
|
const summary = makeRuntimeParitySummary();
|
|
const scenario = summary.scenarios[1];
|
|
if (!scenario?.runtimeParity) {
|
|
throw new Error("runtime parity fixture missing");
|
|
}
|
|
scenario.runtimeParity.cells.codex.runtimeErrorClass = "tool-error";
|
|
|
|
const report = buildQaRuntimeParityReport({
|
|
summary,
|
|
comparedAt: "2026-05-10T00:00:00.000Z",
|
|
});
|
|
|
|
expect(report.pass).toBe(true);
|
|
expect(report.failedScenarios).toBe(0);
|
|
expect(report.failures).toEqual([]);
|
|
});
|
|
|
|
it("fails live runtime parity reports when assistant-message usage is missing", () => {
|
|
const summary = makeRuntimeParitySummary();
|
|
summary.run = {
|
|
...summary.run,
|
|
providerMode: "live-frontier",
|
|
};
|
|
const scenario = summary.scenarios[0];
|
|
if (!scenario?.runtimeParity) {
|
|
throw new Error("runtime parity fixture missing");
|
|
}
|
|
scenario.runtimeParity.cells.openclaw.usage = {
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
totalTokens: 0,
|
|
};
|
|
scenario.runtimeParity.cells.codex.usage = {
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
totalTokens: 0,
|
|
};
|
|
|
|
const report = buildQaRuntimeParityReport({
|
|
summary,
|
|
comparedAt: "2026-05-10T00:00:00.000Z",
|
|
});
|
|
|
|
expect(report.pass).toBe(false);
|
|
expect(report.failedScenarios).toBe(1);
|
|
expect(report.failures).toContain(
|
|
"Approval turn tool followthrough missing live assistant-message usage (openclaw=0, codex=0).",
|
|
);
|
|
expect(report.scenarios[0]?.status).toBe("fail");
|
|
});
|
|
|
|
it("fails runtime parity reports with no executed scenarios", () => {
|
|
const report = buildQaRuntimeParityReport({
|
|
summary: {
|
|
scenarios: [],
|
|
counts: {
|
|
total: 0,
|
|
passed: 0,
|
|
failed: 0,
|
|
},
|
|
run: {
|
|
providerMode: "live-frontier",
|
|
runtimePair: ["openclaw", "codex"],
|
|
},
|
|
},
|
|
comparedAt: "2026-05-10T00:00:00.000Z",
|
|
});
|
|
|
|
expect(report.pass).toBe(false);
|
|
expect(report.failures).toContain("Runtime parity report has no executed scenarios.");
|
|
});
|
|
|
|
it("renders a readable runtime parity markdown report", () => {
|
|
const report = renderQaRuntimeParityMarkdownReport(
|
|
buildQaRuntimeParityReport({
|
|
summary: makeRuntimeParitySummary(),
|
|
comparedAt: "2026-05-10T00:00:00.000Z",
|
|
}),
|
|
);
|
|
|
|
expect(report).toContain("# OpenClaw Runtime Parity Report — openclaw vs codex");
|
|
expect(report).toContain("| Tool-call-shape drift | 1 |");
|
|
expect(report).toContain("### Compaction retry after mutating tool");
|
|
expect(report).toContain("- drift: tool-call-shape");
|
|
});
|
|
});
|