From d79373fa142eb056709725cee7d450fb4708d76e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 12 Jul 2026 10:15:33 +0100 Subject: [PATCH] ci: bound legacy QA smoke compatibility (#105213) --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++- test/scripts/ci-workflow-guards.test.ts | 9 +++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1dbc2d9a4781..539b2c258e83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1198,13 +1198,58 @@ jobs: if (typeof smokePlan.createQaSmokeCiPart === "function") { runs = smokePlan.createQaSmokeCiPart(partId).runs; } else if (typeof smokePlan.createQaSmokeCiMatrix === "function") { + // Legacy planners select the entire profile and can mix long-lived + // execution kinds. Reuse the current bounded smoke contract and + // isolate each kind so frozen targets cannot pin a profile part. + const compatibilityScenarioIds = new Set([ + "control-ui-chat-flow-playwright", + "crestodian-ring-zero-setup", + "dreaming-shadow-trial-report", + "gateway-smoke", + "group-visible-reply-tool", + "long-running-release-audit", + "luna-thinking-visibility-switch", + "matrix-restart-resume", + "personal-task-followthrough-status", + "plugin-lifecycle-hot-reload", + "subagent-completion-direct-fallback", + "telegram-commands-command", + ]); const partIndex = partId === "profile-1" ? 0 : partId === "profile-2" ? 1 : -1; if (partIndex < 0) { throw new Error(`unknown QA smoke CI profile part: ${partId}`); } - runs = smokePlan + const scenarioCatalog = await import("./extensions/qa-lab/src/scenario-catalog.ts"); + const scenarioKindById = new Map( + scenarioCatalog + .readQaScenarioPack() + .scenarios.map((scenario) => [scenario.id, scenario.execution.kind]), + ); + const legacyRuns = smokePlan .createQaSmokeCiMatrix() .include.filter((_, index) => index % 2 === partIndex); + runs = legacyRuns.flatMap((run) => { + const scenarioIdsByKind = new Map(); + for (const scenarioId of run.scenario_ids) { + if (!compatibilityScenarioIds.has(scenarioId)) { + continue; + } + const kind = scenarioKindById.get(scenarioId); + if (!kind) { + throw new Error(`legacy QA smoke scenario not found: ${scenarioId}`); + } + const scenarioIds = scenarioIdsByKind.get(kind) ?? []; + scenarioIds.push(scenarioId); + scenarioIdsByKind.set(kind, scenarioIds); + } + return [...scenarioIdsByKind.entries()] + .toSorted(([left], [right]) => left.localeCompare(right)) + .map(([kind, scenarioIds]) => ({ + ...run, + slug: `${run.slug}-${kind}`, + scenario_ids: scenarioIds, + })); + }); } else { throw new Error("QA smoke plan does not expose a supported CI planner."); } diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 524adf1e8aae..05240f7a2b55 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -2675,6 +2675,15 @@ describe("ci workflow guards", () => { expect(smokeProfileJob["runs-on"]).toContain("blacksmith-16vcpu-ubuntu-2404"); expect(smokeRunStep.run).toContain("createQaSmokeCiPart"); expect(smokeRunStep.run).toContain("createQaSmokeCiMatrix"); + expect(smokeRunStep.run).toContain("readQaScenarioPack"); + expect(smokeRunStep.run).toContain("scenarioIdsByKind"); + const compatibilityScenarioBlock = smokeRunStep.run.match( + /const compatibilityScenarioIds = new Set\(\[([\s\S]*?)\]\);/u, + )?.[1]; + expect(compatibilityScenarioBlock?.match(/^\s+"[^"]+",$/gmu)).toHaveLength(12); + expect(compatibilityScenarioBlock).toContain('"control-ui-chat-flow-playwright"'); + expect(compatibilityScenarioBlock).toContain('"gateway-smoke"'); + expect(compatibilityScenarioBlock).toContain('"matrix-restart-resume"'); expect(smokeRunStep.run).toContain("No QA smoke runs assigned"); expect(smokeRunStep.run).toContain("node openclaw.mjs qa run"); expect(smokeRunStep.run).not.toContain("pnpm openclaw qa run");