返回 DeepSeek-Reasonix
chat-transcript.mjs
根目录 / desktop / frontend / bench / chat-transcript.mjs
1 import assert from "node:assert/strict";
2 import { appendFile, mkdir, mkdtemp, writeFile, copyFile, rm } from "node:fs/promises";
3 import { createRequire } from "node:module";
4 import { tmpdir } from "node:os";
5 import path from "node:path";
6 import { fileURLToPath } from "node:url";
7 import { build, preview, loadConfigFromFile } from "vite";
8 import {
9 collectTranscriptPerformance,
10 decideTranscriptPerformance,
11 formatPerformanceSummary,
12 installTranscriptPerformanceObserver,
13 measureTranscriptPerformance,
14 percentile,
15 } from "./transcript-performance.mjs";
16 import { launchTranscriptRetryHost } from "./transcript-retry.mjs";
17
18 const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
19 process.env.PLAYWRIGHT_BROWSERS_PATH = !process.env.PLAYWRIGHT_BROWSERS_PATH || process.env.PLAYWRIGHT_BROWSERS_PATH === ".pw-browsers"
20 ? path.join(root, ".pw-browsers")
21 : process.env.PLAYWRIGHT_BROWSERS_PATH;
22 // Playwright reads PLAYWRIGHT_BROWSERS_PATH at module evaluation.
23 const { chromium, webkit, _electron } = await import("playwright");
24 const outDir = await mkdtemp(path.join(tmpdir(), "reasonix-chat-build-"));
25 const evidence = process.env.REASONIX_CHAT_EVIDENCE ?? process.env.REASONIX_LAYOUT_ARTIFACTS ?? path.join(tmpdir(), "reasonix-chat-evidence");
26 await mkdir(evidence, { recursive: true });
27 const loaded = await loadConfigFromFile({ command: "build", mode: "production" }, path.join(root, "vite.config.ts"));
28 const config = loaded.config;
29 await build({ ...config, configFile: false, root, logLevel: "error",
30 plugins: config.plugins.filter(plugin => !["archive-hidden-sourcemaps", "keep-dist-placeholder"].includes(plugin?.name)),
31 build: { ...config.build, outDir, sourcemap: false,
32 rolldownOptions: { ...config.build.rolldownOptions, input: path.join(root, "bench/chat-transcript.html") } } });
33 const server = await preview({ configFile: false, root, logLevel: "error", build: { outDir }, preview: { host: "127.0.0.1", port: 0 } });
34 const address = server.httpServer.address();
35 const reports = [];
36 const mode = process.env.REASONIX_TRANSCRIPT_MODE ?? (process.env.REASONIX_TRANSCRIPT_NATIVE_THUMB === "1" ? "native-scrollbar" : "headless-reader");
37 try {
38 for (const [name, engine] of Object.entries(process.env.CHAT_BROWSER === "electron" ? { electron: _electron } : process.env.CHAT_BROWSER === "webkit" ? { webkit } : process.env.CHAT_BROWSER === "chromium" ? { chromium } : { chromium, webkit })) {
39 let electronApp;
40 const url = `http://127.0.0.1:${address.port}/bench/chat-transcript.html?mock=1`;
41 if (name === "electron") {
42 const main = path.join(outDir, "main.cjs");
43 await copyFile(path.join(root, "bench/transcript-layout-electron.cjs"), main);
44 electronApp = await engine.launch({ executablePath: createRequire(path.join(root, "../electron/package.json"))("electron"), args: [main], env: { ...process.env, REASONIX_LAYOUT_URL: url } });
45 }
46 const nativeThumb = process.env.REASONIX_TRANSCRIPT_NATIVE_THUMB === "1";
47 const browser = electronApp ? undefined : await engine.launch({ headless: !nativeThumb });
48 const report = { browser: name, complete: false, samples: [], errors: [],
49 version: browser?.version() ?? await electronApp.evaluate(() => process.versions.electron),
50 platform: process.platform, arch: process.arch };
51 const writeAttempt = (scenario, attempt, decision = null) => writeFile(path.join(evidence, `${scenario}-attempt-${attempt.attempt}.json`), JSON.stringify({
52 browser: report.browser,
53 version: report.version,
54 platform: report.platform,
55 arch: report.arch,
56 mode,
57 ...attempt,
58 decision,
59 }, null, 2));
60 reports.push(report);
61 try {
62 const page = electronApp ? await electronApp.firstWindow() : await browser.newPage({ viewport: { width: 1280, height: 900 } });
63 const errors = report.errors;
64 await page.addInitScript(() => { window.chatWrites = []; window.__REASONIX_TRANSCRIPT_SCROLL_WRITE__ = write => { window.chatWrites.push(write); if (window.chatWrites.length > 100) window.chatWrites.shift(); }; });
65 await installTranscriptPerformanceObserver(page);
66 page.on("pageerror", error => { errors.push(error.message); console.error(error.stack); });
67 page.on("console", message => { if (/Maximum update depth|ResizeObserver loop/.test(message.text())) errors.push(message.text()); });
68 await page.goto(url);
69 await page.locator(".chat-column .md h3").last().waitFor();
70 await page.evaluate(() => document.fonts.ready);
71 const scroll = page.locator(".chat-flow-scroll");
72 const settleFrames = target => target.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
73 const frame = () => settleFrames(page);
74 const navigate = async key => {
75 const mark = page.locator(`[data-nav-turn="${key}"]`);
76 await mark.focus(); await mark.press("Enter");
77 };
78 const bottom = () => scroll.evaluate(el => el.scrollHeight - el.scrollTop - el.clientHeight);
79 await frame();
80 await page.waitForFunction(() => { const el = document.querySelector(".chat-flow-scroll"); return el.scrollHeight - el.scrollTop - el.clientHeight <= 24; }, null, { timeout: 5000 });
81 assert.ok(await bottom() <= 24, "initially follows latest");
82 const rail = page.locator('.dsh-TurnNavigator-frame');
83 await rail.waitFor();
84 assert.equal(await page.locator('[data-nav-turn]').count(), 20, 'rail lists loaded turns only');
85 const activeMark = page.locator('[data-nav-turn][aria-current="true"]');
86 assert.equal(await activeMark.evaluate(el => getComputedStyle(el, '::before').backgroundColor),
87 await page.locator('.chat-surface').evaluate(el => {
88 const probe = document.createElement('span');
89 probe.style.backgroundColor = 'var(--accent)'; el.append(probe);
90 const value = getComputedStyle(probe).backgroundColor; probe.remove(); return value;
91 }), 'active rail mark uses the v1.38.7 accent');
92 const railBox = await rail.boundingBox();
93 await page.mouse.move(railBox.x + 18, railBox.y + 196);
94 await page.locator('.dsh-TurnNavigator-preview').waitFor();
95 assert.match(await page.locator('.dsh-TurnNavigator-preview').innerText(), /Question 20/);
96 assert.equal(await page.locator('[data-nav-turn="u19"]').getAttribute('data-preview-distance'), '0', 'hovered mark owns the accent');
97 assert.equal(await page.locator('[data-nav-turn="u18"]').getAttribute('data-preview-distance'), '1', 'adjacent mark uses the first v1.38.7 fade');
98 assert.equal(await page.locator('[data-nav-turn="u17"]').getAttribute('data-preview-distance'), '2', 'second adjacent mark uses the second v1.38.7 fade');
99 await page.screenshot({ path: path.join(evidence, `${name}-turn-navigation.png`) });
100 await page.mouse.click(railBox.x + 18, railBox.y + 196);
101 await frame();
102 assert.equal(await scroll.getAttribute('data-scroll-mode'), 'reader', 'rail click enters reading intent');
103 await navigate('u0'); await frame();
104 assert.equal(await page.locator('[data-nav-turn="u0"]').getAttribute('aria-current'), 'true', 'keyboard navigation updates the active tick');
105 await page.locator('.chat-to-bottom').click(); await frame();
106 if (nativeThumb) {
107 await scroll.evaluate(el => {
108 window.chatThumbEvents = [];
109 for (const type of ["pointerdown", "pointerup", "scroll"]) el.addEventListener(type, event => window.chatThumbEvents.push({ type, top: el.scrollTop, target: event.target === el }));
110 });
111 const track = await scroll.evaluate(el => { const box = el.getBoundingClientRect();
112 const gutter = el.offsetWidth - el.clientWidth;
113 const arrow = gutter;
114 const trackHeight = Math.max(1, box.height - 2 * arrow);
115 const thumbHeight = Math.max(gutter, trackHeight * el.clientHeight / el.scrollHeight);
116 return {
117 x: box.right - gutter / 2,
118 top: box.top,
119 bottom: box.bottom,
120 before: el.scrollTop,
121 gutter,
122 thumbHeight,
123 thumbCenter: box.bottom - arrow - thumbHeight / 2,
124 }; });
125 report.nativeThumb = { track };
126 // This coordinate-based variant requires an exposed native gutter (for
127 // example headed Linux/Xvfb). A hidden macOS overlay can select text
128 // instead; that must never count as a successful scrollbar drag.
129 assert.ok(track.gutter > 0, "native scrollbar gutter unavailable; run the native-thumb variant in an isolated host with visible scrollbars");
130 // Chromium's Linux scrollbar reserves an arrow-button-sized region at
131 // each end. Start in the computed thumb center instead of the bottom
132 // arrow, then drag toward the middle of the track.
133 await page.mouse.move(track.x, track.thumbCenter); await page.mouse.down();
134 await page.mouse.move(track.x, (track.top + track.bottom) / 2, { steps: 20 }); await page.mouse.up();
135 report.nativeThumb = { track, after: await scroll.evaluate(el => ({ top: el.scrollTop, mode: el.dataset.scrollMode, events: window.chatThumbEvents })) };
136 await page.screenshot({ path: path.join(evidence, `${name}-native-thumb.png`) });
137 await page.waitForFunction(before => document.querySelector(".chat-flow-scroll").scrollTop < before - 100, track.before);
138 assert.equal(await scroll.getAttribute("data-scroll-mode"), "reader", "native scrollbar dragging releases follow");
139 await page.locator(".chat-to-bottom").click(); await frame();
140 }
141 await scroll.hover();
142 const beforeWheel = await scroll.evaluate(el => el.scrollTop);
143 await page.mouse.wheel(0, -600);
144 await page.waitForFunction(() => !document.querySelector(".chat-to-bottom").hidden);
145 // WebKit dispatches wheel input before its native scroll animation ends.
146 // Measure content-induced drift only after that user movement settles.
147 await page.waitForFunction(before => document.querySelector(".chat-flow-scroll").scrollTop < before - 1, beforeWheel);
148 await scroll.evaluate(el => new Promise(resolve => {
149 let previous = el.scrollTop, stable = 0;
150 const sample = () => {
151 const current = el.scrollTop;
152 stable = Math.abs(current - previous) < 0.1 ? stable + 1 : 0;
153 previous = current;
154 if (stable >= 6) resolve(); else requestAnimationFrame(sample);
155 };
156 requestAnimationFrame(sample);
157 }));
158 await frame();
159 const anchor = await page.evaluate(() => {
160 const el = document.querySelector(".chat-flow-scroll"), top = el.getBoundingClientRect().top;
161 const row = [...document.querySelectorAll("[data-chat-anchor-key]")].find(row => row.childNodes.length && row.getBoundingClientRect().bottom > top + 1);
162 return { key: row.dataset.chatAnchorKey, top: row.getBoundingClientRect().top };
163 });
164 for (let i = 0; i < 60; i++) { await page.evaluate(i => window.chatFixture.tick(i), i); await frame(); }
165 const topAfter = await page.locator(`[data-chat-anchor-key="${anchor.key}"]`).evaluate(el => el.getBoundingClientRect().top);
166 assert.ok(Math.abs(topAfter - anchor.top) <= 2, `stream anchor drift ${topAfter - anchor.top}`);
167 await page.evaluate(() => window.chatFixture.prepend()); await frame();
168 const topPrepended = await page.locator(`[data-chat-anchor-key="${anchor.key}"]`).evaluate(el => el.getBoundingClientRect().top);
169 assert.ok(Math.abs(topPrepended - anchor.top) <= 2, `prepend anchor drift ${topPrepended - anchor.top}`);
170 await page.locator(".chat-to-bottom").click(); await frame();
171 await page.waitForFunction(() => { const el = document.querySelector(".chat-flow-scroll"); return el.scrollHeight - el.scrollTop - el.clientHeight <= 24; }, null, { timeout: 5000 });
172 assert.ok(await bottom() <= 24, "return to latest resumes follow");
173 await page.locator('[data-chat-anchor-key="a19"] .md p').first().waitFor();
174 const selected = await page.locator('[data-chat-anchor-key="a19"] .md p').first().evaluate(el => {
175 window.selectedChatParagraph = el;
176 const range = document.createRange(); range.selectNodeContents(el);
177 getSelection().removeAllRanges(); getSelection().addRange(range); return getSelection().toString();
178 });
179 await page.evaluate(() => window.chatFixture.settle()); await frame();
180 assert.ok(await page.evaluate(() => window.selectedChatParagraph.isConnected), "settlement retains the selected paragraph host");
181 assert.equal(await page.evaluate(() => getSelection().toString()), selected, "native selection survives stream settlement");
182 await navigate("u19"); await frame();
183 await page.locator('[data-chat-kind="process"] button').last().click();
184 await page.locator(".chat-tool [data-disclosure-row]").last().click();
185 await page.locator(".dsh-ToolRow-inspectButton").last().click();
186 await page.locator('[role="dialog"]').waitFor();
187 await page.screenshot({ path: path.join(evidence, `${name}-details.png`) });
188 await page.keyboard.press("Escape");
189 assert.equal(await page.locator('[role="dialog"]').count(), 0);
190 assert.ok(await page.locator(".dsh-ToolRow-inspectButton").last().evaluate(el => el === document.activeElement), "drawer restores trigger focus after removing inert");
191 const input = page.locator("textarea.composer__input:not(.composer__input--measure)");
192 assert.deepEqual(errors, [], "browser errors before performance sampling");
193 report.performance = [];
194 for (const turns of [240, 1000]) {
195 const scenario = `${name}-${mode}-${turns}`;
196 let attempts = [];
197 let firstTraceActive = true;
198 let retryHost;
199 await page.context().tracing.start({ screenshots: true, snapshots: true });
200 try {
201 const collected = await collectTranscriptPerformance(async attempt => {
202 if (attempt === 1) {
203 const first = await measureTranscriptPerformance({ page, turns, attempt, frame: settleFrames, errors });
204 await writeAttempt(scenario, first);
205 return first;
206 }
207 if (attempt === 2) {
208 await page.screenshot({ path: path.join(evidence, `${scenario}-first-limit-exceedance.png`) });
209 await page.context().tracing.stop({ path: path.join(evidence, `${scenario}-first-limit-exceedance.zip`) });
210 firstTraceActive = false;
211 }
212 assert.ok(browser, "bounded transcript retry requires an isolated browser process");
213 const retryErrors = [];
214 // Keep the primary page alive for the functional checks below, but
215 // do not make retries compete with its fully mounted 1000-turn
216 // transcript. A browser context is storage isolation, not process
217 // or scheduler isolation, so retries need a separate browser.
218 retryHost ??= await launchTranscriptRetryHost(engine, {
219 headless: !nativeThumb,
220 viewport: { width: 1280, height: 900 },
221 });
222 return retryHost.run(async (retryPage, context) => {
223 await installTranscriptPerformanceObserver(retryPage);
224 retryPage.on("pageerror", error => { retryErrors.push(error.message); console.error(error.stack); });
225 retryPage.on("console", message => { if (/Maximum update depth|ResizeObserver loop/.test(message.text())) retryErrors.push(message.text()); });
226 await context.tracing.start({ screenshots: true, snapshots: true });
227 try {
228 await retryPage.goto(url);
229 await retryPage.locator(".chat-column .md h3").last().waitFor();
230 await retryPage.evaluate(() => document.fonts.ready);
231 const sample = await measureTranscriptPerformance({ page: retryPage, turns, attempt, frame: settleFrames, errors: retryErrors });
232 await writeAttempt(scenario, sample);
233 await context.tracing.stop();
234 return sample;
235 } catch (error) {
236 await context.tracing.stop({ path: path.join(evidence, `${scenario}-attempt-${attempt}-functional-failure.zip`) });
237 throw error;
238 }
239 });
240 });
241 attempts = collected.attempts;
242 if (firstTraceActive) {
243 await page.context().tracing.stop();
244 firstTraceActive = false;
245 }
246 } catch (error) {
247 if (firstTraceActive) await page.context().tracing.stop({ path: path.join(evidence, `${scenario}-functional-failure.zip`) });
248 await writeFile(path.join(evidence, `${scenario}-failure.json`), JSON.stringify({ error: String(error), attempts }, null, 2));
249 throw error;
250 } finally {
251 await retryHost?.close();
252 }
253 const decision = decideTranscriptPerformance(attempts);
254 for (const attempt of attempts) await writeAttempt(scenario, attempt, decision);
255 report.samples.push(...attempts);
256 report.performance.push({ turns, decision, attempts });
257 assert.ok(decision.passed, `${turns} turns ${decision.status}: ${JSON.stringify({
258 longTaskMedians: decision.medians,
259 inputP95Median: decision.inputP95Median,
260 })}`);
261 await page.evaluate(() => window.chatFixture.settle());
262 }
263 await frame();
264 await input.fill("");
265 await navigate("u998"); await frame();
266 await page.screenshot({ path: path.join(evidence, `${name}-chat.png`) });
267 let expanded;
268 if (process.env.CHAT_EXPANDED === "1") {
269 const started = Date.now();
270 await page.evaluate(() => {
271 document.querySelectorAll('[data-chat-kind="process"] button[aria-expanded="false"]').forEach(button => button.click());
272 }); await frame();
273 await page.evaluate(() => document.querySelectorAll('.chat-reasoning [data-disclosure-row][aria-expanded="false"]').forEach(button => button.click()));
274 await frame();
275 const beforeTasks = await page.evaluate(() => window.chatMetrics.tasks.length);
276 for (let turn = 0; turn < 1000; turn += 20) {
277 await navigate(`u${turn}`);
278 await page.waitForFunction(() => window.chatFixture.pending() === 0); await frame();
279 await page.evaluate(() => document.querySelectorAll('.chat-code-fold button[aria-expanded="false"]').forEach(button => button.click()));
280 }
281 await page.locator('[data-chat-anchor-key="tool980"] .chat-tool [data-disclosure-row]').click();
282 await page.locator('[data-chat-anchor-key="tool980"] .dsh-ToolRow-inspectButton').click();
283 await page.locator(".chat-details__body > .btn").first().click(); await frame();
284 expanded = { elapsedMs: Date.now() - started, dom: await page.locator("*").count(),
285 tasks: await page.evaluate(start => window.chatMetrics.tasks.slice(start), beforeTasks),
286 heap: await page.evaluate(() => performance.memory?.usedJSHeapSize) };
287 await page.keyboard.press("Escape");
288 }
289 const soakSeconds = Number(process.env.CHAT_SOAK_SECONDS ?? 0);
290 if (soakSeconds > 0) {
291 await page.evaluate(() => window.chatFixture.reset(240)); await frame();
292 for (let pageIndex = 0; pageIndex < 3; pageIndex++) { await page.evaluate(() => window.chatFixture.older()); await frame(); }
293 const started = Date.now(); let cycles = 0;
294 while (Date.now() - started < soakSeconds * 1000) {
295 await page.evaluate(index => window.chatFixture.tick(index % 40), cycles);
296 await input.press("s"); await input.press("Backspace");
297 if (cycles % 10 === 0) {
298 await navigate("u238");
299 await page.locator('[data-chat-kind="process"][data-chat-turn="u238"] > button').click();
300 }
301 if (cycles % 10 === 5) { await scroll.hover(); await page.mouse.wheel(0, -120); }
302 if (cycles % 20 === 19) { await page.evaluate(() => window.chatFixture.switchSession()); await frame(); }
303 cycles++;
304 }
305 await page.evaluate(() => window.chatFixture.settle()); await frame();
306 report.soak = { elapsedMs: Date.now() - started, cycles, dom: await page.locator("*").count() };
307 assert.equal(await input.inputValue(), "", "continuous streaming never loses or duplicates real input");
308 }
309 const switches = [];
310 await page.evaluate(() => window.chatFixture.reset(60)); await frame();
311 const cdp = name === "chromium" ? await page.context().newCDPSession(page) : undefined;
312 const heap = async () => { if (!cdp) return undefined; await cdp.send("HeapProfiler.collectGarbage"); return (await cdp.send("Runtime.getHeapUsage")).usedSize; };
313 const baseline = await heap();
314 for (let index = 0; index < 20; index++) {
315 const duration = await page.evaluate(() => new Promise(resolve => {
316 const start = performance.now(); window.chatFixture.switchSession(); requestAnimationFrame(() => requestAnimationFrame(() => resolve(performance.now() - start)));
317 })); switches.push(duration);
318 }
319 const released = await heap();
320 const heapGrowth = released === undefined ? undefined : released - baseline;
321 assert.ok(percentile(switches) <= 300, `session switch P95 ${percentile(switches)}`);
322 if (heapGrowth !== undefined) assert.ok(heapGrowth <= 20 * 1024 * 1024, `released heap growth ${heapGrowth}`);
323 await page.waitForFunction(() => window.chatFixture.pending() === 0);
324 await frame(); await frame();
325 // IntersectionObserver can admit the final visible parse after a transient
326 // zero-pending snapshot. Require bounded quiescence first; a layout loop
327 // can never satisfy this condition and fails the five-second deadline.
328 await page.waitForFunction(() => {
329 const last = window.chatWrites.at(-1);
330 const stamp = `${last?.generation}:${last?.transaction}`;
331 if (window.chatIdle?.stamp !== stamp || window.chatFixture.pending()) window.chatIdle = { stamp, at: performance.now() };
332 return performance.now() - window.chatIdle.at >= 250;
333 }, null, { timeout: 5000 });
334 const writesBeforeIdle = await page.evaluate(() => window.chatWrites.at(-1)?.transaction);
335 await page.waitForTimeout(1000);
336 const writesAfterIdle = await page.evaluate(() => window.chatWrites.at(-1)?.transaction);
337 assert.equal(writesAfterIdle, writesBeforeIdle, "settled layout queue converges");
338 await page.evaluate(() => window.chatFixture.authored());
339 await page.getByText("我是 Reasonix。", { exact: true }).waitFor();
340 const authoredText = await page.locator(".chat-column").innerText();
341 assert.match(authoredText, /你是谁/);
342 assert.match(authoredText, /旧会话问题/);
343 assert.match(authoredText, /<response-language>用户引用的 XML<\/response-language>/);
344 assert.doesNotMatch(authoredText, /private environment|internal policy|legacy internal route|session-context|capability-route/);
345 await page.screenshot({ path: path.join(evidence, `${name}-authored-chat.png`) });
346 await page.evaluate(() => window.chatFixture.weather());
347 await page.locator('[data-chat-anchor-key="weather-final"] table').waitFor();
348 const presented = page.locator('.presented-files');
349 await presented.waitFor();
350 assert.equal(await presented.locator('.presented-file').count(), 2, 'trusted present result renders one card per file');
351 assert.match(await presented.innerText(), /shanghai-weather\.html/);
352 assert.match(await presented.innerText(), /weather-notes\.md/);
353 await presented.scrollIntoViewIfNeeded();
354 await page.screenshot({ path: path.join(evidence, `${name}-presented-files.png`) });
355 await frame();
356 await scroll.hover();
357 await page.mouse.wheel(0, -500);
358 await frame();
359 const processToggle = page.locator('.chat-process');
360 assert.equal(await processToggle.getAttribute('aria-expanded'), 'false', 'recovered weather turn folds');
361 assert.equal(await page.locator('.chat-tool').count(), 0, 'collapsed process does not mount tool bodies');
362 assert.match(await processToggle.innerText(), /4/);
363 await page.screenshot({ path: path.join(evidence, `${name}-weather-collapsed.png`) });
364 await processToggle.click();
365 await page.locator('[data-chat-anchor-key="weather-search"]').scrollIntoViewIfNeeded();
366 const weatherRows = await page.locator('.chat-tool [data-disclosure-row]').evaluateAll(rows => rows.map(row => ({ height: row.getBoundingClientRect().height, text: row.textContent })));
367 assert.ok(weatherRows.every(row => row.height <= 32), 'tools use compact single-line rows');
368 assert.ok(weatherRows.some(row => row.text.includes('获取并核对上海天气')), 'description replaces line counts');
369 assert.equal(await page.locator('.dsh-ContextInjectionRow-body').count(), 0, 'permission record starts collapsed');
370 await page.screenshot({ path: path.join(evidence, `${name}-weather-expanded.png`) });
371 await page.locator('[data-chat-anchor-key="weather-bash"] [data-disclosure-row]').click();
372 await page.locator('[data-terminal]').waitFor();
373 await page.screenshot({ path: path.join(evidence, `${name}-weather-terminal.png`) });
374 await scroll.hover();
375 await page.mouse.wheel(0, -500);
376 await frame();
377 await page.locator('[data-chat-anchor-key="weather-search"] [data-disclosure-row]').click();
378 await page.locator('[data-web="search"]').waitFor();
379 assert.equal(await page.locator('[data-web="search"] a').count(), 1, 'web sources retain safe host links');
380 report.weatherRows = weatherRows;
381 assert.deepEqual(errors, []);
382 Object.assign(report, { complete: true, expanded, switches, switchP95: percentile(switches), heapGrowth, anchorDrift: topAfter - anchor.top, prependDrift: topPrepended - anchor.top });
383 console.log(JSON.stringify({
384 ...report,
385 samples: report.samples.map(sample => ({
386 ...sample,
387 phases: Object.fromEntries(Object.entries(sample.phases).map(([phase, value]) => [phase, { ...value, longTasks: undefined }])),
388 inputs: undefined,
389 })),
390 performance: report.performance.map(sample => ({ turns: sample.turns, decision: sample.decision, attempts: sample.attempts.length })),
391 }));
392 } catch (error) {
393 report.failure = String(error); throw error;
394 } finally { await browser?.close(); await electronApp?.close(); }
395 }
396 } finally {
397 await server.httpServer.close();
398 await writeFile(path.join(evidence, `${process.env.CHAT_BROWSER ?? "browsers"}-results.json`), JSON.stringify(reports, null, 2));
399 if (process.env.GITHUB_STEP_SUMMARY) {
400 const lines = reports.flatMap(report => (report.performance ?? []).map(sample => formatPerformanceSummary(report.browser, sample.turns, sample.decision, sample.attempts)));
401 if (lines.length) await appendFile(process.env.GITHUB_STEP_SUMMARY, `\n### Transcript performance (${mode})\n\n${lines.join("\n")}\n`);
402 }
403 await rm(outDir, { recursive: true, force: true });
404 }
405
405 lines Plain Text