From d2240a9476c74220584bd7919e493997e1e3ff09 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Apr 2026 12:08:02 +0100 Subject: [PATCH] test: harden qa-lab concurrent web scenarios --- extensions/qa-lab/src/suite.test.ts | 10 +++++++ extensions/qa-lab/src/suite.ts | 27 ++++++++++++++----- extensions/qa-lab/src/web-runtime.test.ts | 16 +++++++++++ extensions/qa-lab/src/web-runtime.ts | 18 ++++++++++--- .../active-memory-preprompt-recall.md | 2 +- 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/extensions/qa-lab/src/suite.test.ts b/extensions/qa-lab/src/suite.test.ts index 54fc388d31f..81e4caf1693 100644 --- a/extensions/qa-lab/src/suite.test.ts +++ b/extensions/qa-lab/src/suite.test.ts @@ -214,6 +214,16 @@ describe("qa suite failure reply handling", () => { }); }); + it("enables Control UI only for Control UI scenario workers", () => { + expect( + qaSuiteTesting.scenarioRequiresControlUi({ + ...makeScenario("control-ui"), + surface: "control-ui", + }), + ).toBe(true); + expect(qaSuiteTesting.scenarioRequiresControlUi(makeScenario("plain"))).toBe(false); + }); + it("filters provider-specific scenarios from an implicit live lane", () => { const scenarios = [ makeScenario("generic"), diff --git a/extensions/qa-lab/src/suite.ts b/extensions/qa-lab/src/suite.ts index 77a327f6698..0528c25f0da 100644 --- a/extensions/qa-lab/src/suite.ts +++ b/extensions/qa-lab/src/suite.ts @@ -68,7 +68,7 @@ import { readQaBootstrapScenarioCatalog } from "./scenario-catalog.js"; import { runScenarioFlow } from "./scenario-flow-runner.js"; import { createQaScenarioRuntimeApi } from "./scenario-runtime-api.js"; import { - closeAllQaWebSessions, + closeQaWebSessions, qaWebEvaluate, qaWebOpenPage, qaWebSnapshot, @@ -98,6 +98,7 @@ type QaSuiteEnvironment = { providerMode: "mock-openai" | "live-frontier"; primaryModel: string; alternateModel: string; + webSessionIds: Set; }; export type QaSuiteStartLabFn = (params?: QaLabServerStartParams) => Promise; @@ -340,6 +341,12 @@ function collectQaSuiteGatewayRuntimeOptions( return forwardHostHome ? { forwardHostHome: true } : undefined; } +function scenarioRequiresControlUi( + scenario: ReturnType["scenarios"][number], +) { + return normalizeLowercaseStringOrEmpty(scenario.surface) === "control-ui"; +} + function liveTurnTimeoutMs(env: QaSuiteEnvironment, fallbackMs: number) { return resolveQaLiveTurnTimeoutMs(env, fallbackMs); } @@ -1268,7 +1275,11 @@ function createScenarioFlowApi( browserOpenTab: qaBrowserOpenTab, browserSnapshot: qaBrowserSnapshot, browserAct: qaBrowserAct, - webOpenPage: qaWebOpenPage, + webOpenPage: async (params: Parameters[0]) => { + const opened = await qaWebOpenPage(params); + env.webSessionIds.add(opened.pageId); + return opened; + }, webWait: qaWebWait, webType: qaWebType, webSnapshot: qaWebSnapshot, @@ -1330,6 +1341,7 @@ export const qaSuiteTesting = { mapQaSuiteWithConcurrency, normalizeQaSuiteConcurrency, scenarioMatchesLiveLane, + scenarioRequiresControlUi, selectQaSuiteScenarios, readTransportTranscript, formatTransportTranscript, @@ -1575,10 +1587,10 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise ({ import { closeAllQaWebSessions, + closeQaWebSessions, qaWebEvaluate, qaWebOpenPage, qaWebSnapshot, @@ -114,4 +115,19 @@ describe("qa web runtime", () => { expect(contextClose).toHaveBeenCalledTimes(1); expect(browserClose).toHaveBeenCalledTimes(1); }); + + it("can close only selected page sessions", async () => { + const first = await qaWebOpenPage({ url: "http://127.0.0.1:3000/one" }); + const second = await qaWebOpenPage({ url: "http://127.0.0.1:3000/two" }); + + await closeQaWebSessions([first.pageId]); + + await expect(qaWebSnapshot({ pageId: first.pageId })).rejects.toThrow( + `unknown web session: ${first.pageId}`, + ); + await expect(qaWebSnapshot({ pageId: second.pageId })).resolves.toMatchObject({ + text: "hello from body", + }); + await closeAllQaWebSessions(); + }); }); diff --git a/extensions/qa-lab/src/web-runtime.ts b/extensions/qa-lab/src/web-runtime.ts index b894760e9ae..fa4d507d0ee 100644 --- a/extensions/qa-lab/src/web-runtime.ts +++ b/extensions/qa-lab/src/web-runtime.ts @@ -144,11 +144,23 @@ export async function qaWebEvaluate(params: QaWebEvaluateParams): P ])) as T; } -export async function closeAllQaWebSessions(): Promise { - const active = [...sessions.values()]; - sessions.clear(); +export async function closeQaWebSessions(pageIds?: Iterable): Promise { + const active = pageIds + ? [...pageIds].flatMap((pageId) => { + const session = sessions.get(pageId); + sessions.delete(pageId); + return session ? [session] : []; + }) + : [...sessions.values()]; + if (!pageIds) { + sessions.clear(); + } for (const session of active) { await session.context.close().catch(() => {}); await session.browser.close().catch(() => {}); } } + +export async function closeAllQaWebSessions(): Promise { + await closeQaWebSessions(); +} diff --git a/qa/scenarios/active-memory-preprompt-recall.md b/qa/scenarios/active-memory-preprompt-recall.md index d0d2270d6dd..02ca35fbb92 100644 --- a/qa/scenarios/active-memory-preprompt-recall.md +++ b/qa/scenarios/active-memory-preprompt-recall.md @@ -207,7 +207,7 @@ steps: args: - lambda: async: true - expr: "await (async () => { const store = await readRawQaSessionStore(env); const entry = store[activeSessionKey]; if (!entry || !Array.isArray(entry.pluginDebugEntries)) return undefined; return entry.pluginDebugEntries.some((pluginEntry) => pluginEntry?.pluginId === 'active-memory' && Array.isArray(pluginEntry.lines) && pluginEntry.lines.some((line) => line.includes('Active Memory: ok'))) ? entry : undefined; })()" + expr: "await (async () => { const store = await readRawQaSessionStore(env); const entry = store[activeSessionKey]; if (!entry || !Array.isArray(entry.pluginDebugEntries)) return undefined; return entry.pluginDebugEntries.some((pluginEntry) => pluginEntry?.pluginId === 'active-memory' && Array.isArray(pluginEntry.lines) && pluginEntry.lines.some((line) => line.includes('Active Memory: status=ok'))) ? entry : undefined; })()" - 10000 - if: expr: "Boolean(env.mock)"