Skip to content

Commit 591313e

Browse files
qa-lab: support script-backed evidence scenarios (#94276)
* qa: add script scenario execution kind * fix(qa-lab): carry suite profile into script producer evidence and simplify artifact path resolution * fix(qa-lab): keep out-of-repo producer artifacts absolute to avoid ../ traversal refs --------- Co-authored-by: Dallin Romney <[email protected]>
1 parent 8288b4d commit 591313e

10 files changed

Lines changed: 811 additions & 20 deletions

docs/concepts/qa-e2e-automation.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,12 @@ Every `qa suite` run writes top-level `qa-evidence.json`,
942942
`qa-suite-summary.json`, and `qa-suite-report.md` artifacts for the selected
943943
scenario set. Scenarios that declare `execution.kind: vitest` or
944944
`execution.kind: playwright` run the matching test path and also write
945-
per-scenario logs. When `qa suite` is reached through
945+
per-scenario logs. Scenarios that declare `execution.kind: script` run the
946+
evidence producer at `execution.path` through `node --import tsx` (with
947+
`${outputDir}` and `${scenarioId}` expanded in `execution.args`); the producer
948+
writes its own `qa-evidence.json`, whose entries are imported into the suite
949+
output and whose artifact paths are resolved relative to that producer
950+
`qa-evidence.json`. When `qa suite` is reached through
946951
`qa run --qa-profile`, the same `qa-evidence.json` also includes the profile
947952
scorecard summary for the selected taxonomy categories.
948953
Treat it as a discovery aid, not a gate replacement; the selected scenario still needs the right provider mode, live transport, Multipass, Testbox, or release lane for the behavior under test.

extensions/qa-lab/src/coverage-report.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,13 @@ function scenarioWithCoverage(params: {
6767
primary?: readonly string[];
6868
secondary?: readonly string[];
6969
sourcePath?: string;
70-
executionKind?: "flow" | "vitest" | "playwright";
70+
executionKind?: "flow" | "script" | "vitest" | "playwright";
7171
executionPath?: string;
7272
}): QaSeedScenarioWithSource {
7373
const execution =
74-
params.executionKind === "vitest" || params.executionKind === "playwright"
74+
params.executionKind === "script" ||
75+
params.executionKind === "vitest" ||
76+
params.executionKind === "playwright"
7577
? {
7678
kind: params.executionKind,
7779
path: params.executionPath ?? "src/test.test.ts",
@@ -328,6 +330,37 @@ describe("qa coverage report", () => {
328330
]);
329331
});
330332

333+
it("uses script producer evidence as coverage fulfillment", () => {
334+
const report = buildQaScorecardTaxonomyReport({
335+
taxonomy: testMaturityTaxonomy({
336+
categoryId: TEST_BROWSER_CATEGORY_ID,
337+
coverageIds: [TEST_BROWSER_COVERAGE_ID],
338+
}),
339+
repoRoot: process.cwd(),
340+
scenarios: [
341+
scenarioWithCoverage({
342+
primary: [TEST_BROWSER_COVERAGE_ID],
343+
sourcePath: "qa/scenarios/ui/script-evidence-producer.yaml",
344+
executionKind: "script",
345+
executionPath: "scripts/check-no-conflict-markers.mjs",
346+
}),
347+
],
348+
});
349+
350+
expect(report.validationIssues).toStrictEqual([]);
351+
expect(report.fulfilledCategoryCount).toBe(1);
352+
expect(report.fulfilledFeatureCount).toBe(1);
353+
expect(report.categories[0]?.evidence).toStrictEqual([
354+
{
355+
coverageId: TEST_BROWSER_COVERAGE_ID,
356+
kind: "script",
357+
path: "scripts/check-no-conflict-markers.mjs",
358+
role: "primary",
359+
scenarioRefs: ["qa/scenarios/ui/script-evidence-producer.yaml"],
360+
},
361+
]);
362+
});
363+
331364
it("reports profile membership refs missing from taxonomy categories", () => {
332365
const report = buildQaScorecardTaxonomyReport({
333366
taxonomy: testMaturityTaxonomy({

extensions/qa-lab/src/coverage-report.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,12 @@ function scenarioMatchCommandGroups(matches: readonly QaScenarioSearchMatch[]) {
465465
group.push(match);
466466
groups.set(match.executionKind, group);
467467
}
468-
const executionOrder: QaScenarioSearchMatch["executionKind"][] = ["flow", "vitest", "playwright"];
468+
const executionOrder: QaScenarioSearchMatch["executionKind"][] = [
469+
"flow",
470+
"script",
471+
"vitest",
472+
"playwright",
473+
];
469474
return executionOrder.flatMap((executionKind) => {
470475
const group = groups.get(executionKind);
471476
return group && group.length > 0 ? [{ executionKind, matches: group }] : [];

extensions/qa-lab/src/evidence-summary.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ function uniqueSortedStrings(values: readonly (string | undefined)[]) {
349349
);
350350
}
351351

352-
function resolveQaEvidenceProfile(params: {
352+
export function resolveQaEvidenceProfile(params: {
353353
env?: NodeJS.ProcessEnv;
354354
explicit?: QaEvidenceProfile;
355355
}) {
@@ -705,6 +705,20 @@ export function buildPlaywrightEvidenceSummary(
705705
});
706706
}
707707

708+
export function buildScriptEvidenceSummary(
709+
params: QaEvidenceBuildBase & {
710+
targets: readonly QaEvidenceTestTargetInput[];
711+
results: readonly QaEvidenceTestResultInput[];
712+
},
713+
): QaEvidenceSummaryJson {
714+
return buildTestRunnerEvidenceSummary({
715+
...params,
716+
defaultRunner: "script",
717+
testKind: "script-test",
718+
runner: params.runner ?? "script",
719+
});
720+
}
721+
708722
export function buildLiveTransportEvidenceSummary(
709723
params: QaEvidenceBuildBase & {
710724
checks: readonly QaEvidenceLiveTransportCheckInput[];

extensions/qa-lab/src/scenario-catalog.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ const qaTestFileScenarioExecutionBaseSchema = z.object({
7373
const qaTestFileScenarioExecutionSchema = z.discriminatedUnion("kind", [
7474
qaTestFileScenarioExecutionBaseSchema.extend({ kind: z.literal("vitest") }),
7575
qaTestFileScenarioExecutionBaseSchema.extend({ kind: z.literal("playwright") }),
76+
qaTestFileScenarioExecutionBaseSchema.extend({
77+
kind: z.literal("script"),
78+
args: z.array(z.string()).optional(),
79+
}),
7680
]);
7781

7882
const qaScenarioExecutionSchema = z.union([

extensions/qa-lab/src/scorecard-taxonomy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ const qaMaturityTaxonomySchema = z
9090
}
9191
});
9292

93-
export type QaNativeCoverageEvidenceKind = "vitest" | "playwright";
93+
export type QaNativeCoverageEvidenceKind = "script" | "vitest" | "playwright";
9494
export type QaScorecardEvidenceKind = QaNativeCoverageEvidenceKind | "qa-scenario";
9595
export type QaScorecardEvidenceMode = z.infer<typeof qaScorecardEvidenceModeSchema>;
9696
type QaCoverageEvidenceRole = z.infer<typeof qaCoverageEvidenceRoleSchema>;

extensions/qa-lab/src/suite-launch.runtime.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe("qa suite runtime launcher", () => {
7474
runQaTestFileScenarios.mockImplementation(
7575
async (params: {
7676
outputDir: string;
77-
scenarios: Array<{ id: string; execution: { kind: "vitest" | "playwright" } }>;
77+
scenarios: Array<{ id: string; execution: { kind: "script" | "vitest" | "playwright" } }>;
7878
}) => {
7979
const [scenario] = params.scenarios;
8080
if (!scenario) {

0 commit comments

Comments
 (0)