test: harden qa-lab concurrent web scenarios

This commit is contained in:
Peter Steinberger
2026-04-14 12:08:02 +01:00
parent 10dbb21380
commit d2240a9476
5 changed files with 62 additions and 11 deletions

View File

@@ -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"),

View File

@@ -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<string>;
};
export type QaSuiteStartLabFn = (params?: QaLabServerStartParams) => Promise<QaLabServerHandle>;
@@ -340,6 +341,12 @@ function collectQaSuiteGatewayRuntimeOptions(
return forwardHostHome ? { forwardHostHome: true } : undefined;
}
function scenarioRequiresControlUi(
scenario: ReturnType<typeof readQaBootstrapScenarioCatalog>["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<typeof qaWebOpenPage>[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<QaSuiteResu
scenarioIds: [scenario.id],
concurrency: 1,
startLab,
// Isolated workers do not need their own Control UI proxy. The
// outer lab already owns the watch surface, so skip per-worker
// Control UI asset resolution and startup overhead.
controlUiEnabled: false,
// Most isolated workers do not need their own Control UI proxy.
// Control UI scenarios do, because they open the worker's
// gateway-backed app directly.
controlUiEnabled: scenarioRequiresControlUi(scenario),
});
const scenarioResult: QaSuiteScenarioResult =
result.scenarios[0] ??
@@ -1740,6 +1752,7 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
providerMode,
primaryModel,
alternateModel,
webSessionIds: new Set(),
};
let preserveGatewayRuntimeDir: string | undefined;
@@ -1849,7 +1862,7 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
preserveGatewayRuntimeDir = path.join(outputDir, "artifacts", "gateway-runtime");
throw error;
} finally {
await closeAllQaWebSessions();
await closeQaWebSessions(env.webSessionIds);
const keepTemp = process.env.OPENCLAW_QA_KEEP_TEMP === "1" || false;
await gateway.stop({
keepTemp,

View File

@@ -44,6 +44,7 @@ vi.mock("playwright-core", () => ({
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();
});
});

View File

@@ -144,11 +144,23 @@ export async function qaWebEvaluate<T = unknown>(params: QaWebEvaluateParams): P
])) as T;
}
export async function closeAllQaWebSessions(): Promise<void> {
const active = [...sessions.values()];
sessions.clear();
export async function closeQaWebSessions(pageIds?: Iterable<string>): Promise<void> {
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<void> {
await closeQaWebSessions();
}

View File

@@ -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)"