| 1 | // bridgeBenchFixtures — deterministic heavyweight mock sessions for the |
| 2 | // real-DOM benchmark harness (desktop/frontend/bench), served by the dev mock |
| 3 | // when the page URL carries ?mock=bench. This module is imported lazily from |
| 4 | // bridge.ts so the generators never land in the eager production bundle. |
| 5 | |
| 6 | import type { HistoryMessage, HistoryToolCall } from "./types"; |
| 7 | |
| 8 | // Soak tests need async hydration, but not simulated backend latency per cycle. |
| 9 | // Geometry/browser fixtures keep their delayed expansion contract by default. |
| 10 | export function benchHydrationDelay(search = window.location.search): number { |
| 11 | const params = new URLSearchParams(search); |
| 12 | return params.get("mock") === "bench" && params.get("bench") === "1" |
| 13 | && params.get("app-lifecycle-probe") === "1" && params.get("bench-hydration") === "soak" ? 0 : 1_500; |
| 14 | } |
| 15 | |
| 16 | // ── Benchmark fixtures (?mock=bench, Phase F) ───────────────────────────── |
| 17 | // Fixed diagnostic sessions for the real-DOM performance harness |
| 18 | // (desktop/frontend/bench). Content is deterministic and generated once per |
| 19 | // page load. Shapes follow the plan's fixtures, mirrored from the Go-side |
| 20 | // history_slice tests: a tool-dense 38-turn session (~3.2k provider |
| 21 | // messages), a markdown-heavy 46-turn session (~600 messages incl. one |
| 22 | // ~500KiB answer with a big table and one oversized code block), a small |
| 23 | // 6-turn session, and a single turn with thousands of messages. |
| 24 | const benchFixtureCache = new Map<string, HistoryMessage[]>(); |
| 25 | const benchFixture = (key: string, build: () => HistoryMessage[]): HistoryMessage[] => { |
| 26 | const cached = benchFixtureCache.get(key); |
| 27 | if (cached) return cached; |
| 28 | const built = build(); |
| 29 | benchFixtureCache.set(key, built); |
| 30 | return built; |
| 31 | }; |
| 32 | const benchToolOutput = (turn: number, index: number): string => |
| 33 | [ |
| 34 | `ok turn=${turn} call=${index}`, |
| 35 | "status: success", |
| 36 | `duration_ms: ${(turn * 7 + index * 13) % 420}`, |
| 37 | `detail: ${"x".repeat(120)}`, |
| 38 | ].join("\n"); |
| 39 | const benchToolTurn = (turn: number, callCount: number, answer: string): HistoryMessage[] => { |
| 40 | const toolCalls: HistoryToolCall[] = []; |
| 41 | const results: HistoryMessage[] = []; |
| 42 | for (let k = 0; k < callCount; k += 1) { |
| 43 | const id = `bench-t${turn}-call-${k}`; |
| 44 | const readOnly = k % 3 !== 0; |
| 45 | toolCalls.push({ |
| 46 | id, |
| 47 | name: readOnly ? "read_file" : "bash", |
| 48 | arguments: JSON.stringify(readOnly ? { path: `internal/bench/pkg-${k}/mod.go` } : { command: `go test ./internal/bench/pkg-${k}` }), |
| 49 | resolvedReadOnly: readOnly, |
| 50 | subject: readOnly ? `internal/bench/pkg-${k}/mod.go` : `go test pkg-${k}`, |
| 51 | }); |
| 52 | results.push({ role: "tool", toolCallId: id, toolName: toolCalls[k].name, content: benchToolOutput(turn, k) }); |
| 53 | } |
| 54 | return [ |
| 55 | { role: "user", content: `bench turn ${turn}: run the verification batch and summarize per-package results.` }, |
| 56 | { role: "assistant", content: "", reasoning: `planning verification batch ${turn}: ${callCount} checks.`, toolCalls }, |
| 57 | ...results, |
| 58 | ...(answer ? [{ role: "assistant", content: answer, workDurationMs: 1200 }] : []), |
| 59 | ]; |
| 60 | }; |
| 61 | const benchToolDenseHistory = (): HistoryMessage[] => { |
| 62 | // 38 visible turns × 86 messages = 3268 provider messages (nominal 3255). |
| 63 | const messages: HistoryMessage[] = []; |
| 64 | for (let turn = 1; turn <= 38; turn += 1) messages.push(...benchToolTurn(turn, 42, turn % 4 === 0 ? `Batch ${turn} done: all checks green.` : "")); |
| 65 | return messages; |
| 66 | }; |
| 67 | const benchMarkdownSection = (turn: number): string => |
| 68 | [ |
| 69 | `## Turn ${turn} summary`, |
| 70 | "", |
| 71 | "The verification sweep completed with all packages green. Key observations:", |
| 72 | "", |
| 73 | "- display-index hits stayed high across paged reads", |
| 74 | "- long tasks remained under the main-thread budget", |
| 75 | "- cache weights stayed within their declared byte budgets", |
| 76 | "", |
| 77 | "```ts", |
| 78 | "export function digest(values: number[]): number {", |
| 79 | " return values.reduce((acc, v) => (acc * 31 + v) | 0, 7);", |
| 80 | "}", |
| 81 | "```", |
| 82 | "", |
| 83 | "| package | tests | duration ms |", |
| 84 | "| --- | ---: | ---: |", |
| 85 | ...Array.from({ length: 12 }, (_, k) => `| pkg-${k} | ${20 + k * 3} | ${40 + ((turn * 17 + k * 29) % 300)} |`), |
| 86 | "", |
| 87 | ].join("\n"); |
| 88 | const benchBigMarkdownAnswer = (): string => { |
| 89 | // ~500KiB answer: a giant table plus repeated prose/code sections. |
| 90 | const parts: string[] = ["# Full verification report", ""]; |
| 91 | parts.push("| row | package | tests | duration ms | status |", "| ---: | --- | ---: | ---: | --- |"); |
| 92 | for (let row = 0; row < 4000; row += 1) { |
| 93 | parts.push(`| ${row} | pkg-${row % 64} | ${(row * 7) % 90} | ${(row * 13) % 800} | ${row % 11 === 0 ? "flaky" : "green"} |`); |
| 94 | } |
| 95 | let body = parts.join("\n"); |
| 96 | let section = 0; |
| 97 | while (body.length < 500 * 1024) { |
| 98 | section += 1; |
| 99 | body += `\n\n${benchMarkdownSection(1000 + section)}`; |
| 100 | } |
| 101 | return body; |
| 102 | }; |
| 103 | const benchOversizedCodeBlock = (): string => { |
| 104 | // Single >64KiB code block (a content-ref candidate on the real backend). |
| 105 | const line = "const row = await db.query('select id, payload from bench where shard = $1', [shard]); // "; |
| 106 | const lines = Math.ceil((300 * 1024) / line.length); |
| 107 | return ["Here is the full generated migration:", "", "```sql", ...Array.from({ length: lines }, (_, k) => `-- ${k} ${line}`), "```"].join("\n"); |
| 108 | }; |
| 109 | const benchMarkdownHeavyHistory = (): HistoryMessage[] => { |
| 110 | // 46 visible turns; 13 messages per normal turn (10 tool pairs), plus the |
| 111 | // newest turn carrying the ~500KiB report and the oversized code block. |
| 112 | const messages: HistoryMessage[] = []; |
| 113 | for (let turn = 1; turn <= 45; turn += 1) messages.push(...benchToolTurn(turn, 5, benchMarkdownSection(turn))); |
| 114 | messages.push( |
| 115 | { role: "user", content: "bench final turn: produce the full verification report with the big table, then the migration SQL." }, |
| 116 | { role: "assistant", content: benchBigMarkdownAnswer(), workDurationMs: 2400 }, |
| 117 | { role: "assistant", content: benchOversizedCodeBlock(), workDurationMs: 800 }, |
| 118 | ); |
| 119 | return messages; |
| 120 | }; |
| 121 | const benchSmallHistory = (): HistoryMessage[] => { |
| 122 | // 6 visible turns × 78 messages = 468 provider messages (nominal 473). |
| 123 | const messages: HistoryMessage[] = []; |
| 124 | for (let turn = 1; turn <= 6; turn += 1) { |
| 125 | const answer = turn === 6 |
| 126 | ? [ |
| 127 | "# Asynchronously hydrated verification appendix", |
| 128 | ...Array.from({ length: 1_200 }, (_, row) => `- package-${row % 42}: verified row ${row} with stable virtual measurements`), |
| 129 | "ASYNC LAYOUT EXPANSION COMPLETE", |
| 130 | ].join("\n") |
| 131 | : `Batch ${turn} summary.`; |
| 132 | messages.push(...benchToolTurn(turn, 38, answer)); |
| 133 | } |
| 134 | return messages; |
| 135 | }; |
| 136 | const benchGiantTurnHistory = (): HistoryMessage[] => { |
| 137 | // A single turn with thousands of messages (1000 tool pairs). |
| 138 | return benchToolTurn(1, 1000, "Single-turn sweep complete."); |
| 139 | }; |
| 140 | |
| 141 | const benchWindowedHistory = (): HistoryMessage[] => { |
| 142 | const messages: HistoryMessage[] = []; |
| 143 | for (let turn = 1; turn <= 1_000; turn += 1) { |
| 144 | messages.push( |
| 145 | { role: "user", content: `windowed turn ${turn}: verify the stable block anchor.` }, |
| 146 | { |
| 147 | role: "assistant", |
| 148 | reasoning: turn === 950 ? Array.from({ length: 40 }, (_, line) => `Reasoning paragraph ${line + 1}: verify expanded cold history geometry.`).join("\n\n") : undefined, |
| 149 | content: turn % 25 === 0 |
| 150 | ? `## Windowed turn ${turn}\n\n中文 English emoji ✅\n\n| turn | status |\n| ---: | --- |\n| ${turn} | stable |\n\n\`\`\`ts\nconst turn = ${turn};\n\`\`\`` |
| 151 | : `Windowed result ${turn}: the block identity and native viewport geometry remain stable.`, |
| 152 | }, |
| 153 | ); |
| 154 | } |
| 155 | return messages; |
| 156 | }; |
| 157 | |
| 158 | const benchReportedLongTurnHistory = (): HistoryMessage[] => { |
| 159 | // Sanitized reproduction of the reported shape: one user turn, 70 tool |
| 160 | // results, and 44 separately measured assistant blocks. Keeping the height |
| 161 | // distribution matters; no user content or exported session data is used. |
| 162 | const messages: HistoryMessage[] = [ |
| 163 | { role: "user", content: "Inspect the workspace, apply the changes, and verify the result." }, |
| 164 | ]; |
| 165 | for (let block = 0; block < 44; block += 1) { |
| 166 | const callCount = block < 26 ? 2 : 1; |
| 167 | const toolCalls: HistoryToolCall[] = []; |
| 168 | for (let call = 0; call < callCount; call += 1) { |
| 169 | const id = `reported-block-${block}-call-${call}`; |
| 170 | toolCalls.push({ |
| 171 | id, |
| 172 | name: block % 3 === 0 ? "bash" : "read_file", |
| 173 | arguments: JSON.stringify(block % 3 === 0 |
| 174 | ? { command: `pnpm test --filter reported-${block}-${call}` } |
| 175 | : { path: `src/reported/section-${block}-${call}.ts` }), |
| 176 | resolvedReadOnly: block % 3 !== 0, |
| 177 | subject: `reported section ${block + 1}.${call + 1}`, |
| 178 | }); |
| 179 | } |
| 180 | messages.push({ |
| 181 | role: "assistant", |
| 182 | content: [ |
| 183 | `### Verification block ${block + 1}`, |
| 184 | "", |
| 185 | `Processed the deterministic fixture section ${block + 1}.`, |
| 186 | "", |
| 187 | ...Array.from({ length: 2 + (block % 5) }, (_, row) => `- check ${row + 1}: stable measurement ${block}-${row}`), |
| 188 | ].join("\n"), |
| 189 | reasoning: `Planning verification block ${block + 1}.`, |
| 190 | toolCalls, |
| 191 | }); |
| 192 | messages.push(...toolCalls.map((toolCall, call) => ({ |
| 193 | role: "tool" as const, |
| 194 | toolCallId: toolCall.id, |
| 195 | toolName: toolCall.name, |
| 196 | content: benchToolOutput(block + 1, call), |
| 197 | }))); |
| 198 | } |
| 199 | messages.push({ role: "assistant", content: "Reported long turn complete." }); |
| 200 | return messages; |
| 201 | }; |
| 202 | |
| 203 | const benchGeometryContractHistory = (): HistoryMessage[] => { |
| 204 | // One semantic turn with many heterogeneous rows. This verifies that row |
| 205 | // density does not change the block-level anchoring and mounting contract. |
| 206 | const messages: HistoryMessage[] = [ |
| 207 | { role: "user", content: "验证首次向上遍历未访问的折叠过程行,记录逐帧几何和滚动方向。" }, |
| 208 | ]; |
| 209 | for (let block = 0; block < 60; block += 1) { |
| 210 | const id = `geometry-contract-${block}`; |
| 211 | const readOnly = block % 3 !== 0; |
| 212 | // Start the reasoning tail earlier so the reduced fixture still carries |
| 213 | // the 31 folded reasoning rows required by the contract gate. |
| 214 | const reasoningOrdinal = block - 29; |
| 215 | const reasoning = reasoningOrdinal >= 0 |
| 216 | ? [ |
| 217 | `第 ${reasoningOrdinal + 1} 段分析:验证折叠状态不读取完整正文。`, |
| 218 | ...Array.from( |
| 219 | { length: 8 + reasoningOrdinal * 5 }, |
| 220 | (_, line) => `reasoning ${reasoningOrdinal + 1}.${line + 1} 中文 English ${"x".repeat(32 + (line % 4) * 16)}`, |
| 221 | ), |
| 222 | ].join("\n") |
| 223 | : undefined; |
| 224 | messages.push({ |
| 225 | role: "assistant", |
| 226 | reasoning, |
| 227 | content: [ |
| 228 | `### Geometry block ${block + 1}`, |
| 229 | "", |
| 230 | `折叠布局检查 ${block + 1} 完成,answer 保持内容感知估高。`, |
| 231 | "", |
| 232 | block % 5 === 0 ? "```ts\nconst stable = layoutVariant === 'reasoning-summary';\n```" : "- 中文换行\n- English wrapping", |
| 233 | ].join("\n"), |
| 234 | toolCalls: [ |
| 235 | { |
| 236 | id, |
| 237 | name: readOnly ? "read_file" : "bash", |
| 238 | arguments: JSON.stringify(readOnly |
| 239 | ? { path: `src/geometry/section-${block}.ts` } |
| 240 | : { command: `pnpm test --filter geometry-${block}` }), |
| 241 | resolvedReadOnly: readOnly, |
| 242 | subject: `geometry section ${block + 1}`, |
| 243 | }, |
| 244 | ...(block >= 31 && block < 55 ? [{ |
| 245 | id: `${id}-stopped`, |
| 246 | name: "bash", |
| 247 | arguments: JSON.stringify({ command: `pnpm typecheck --filter geometry-${block}` }), |
| 248 | resolvedReadOnly: false, |
| 249 | subject: `geometry stopped section ${block + 1}`, |
| 250 | }] : []), |
| 251 | ], |
| 252 | }); |
| 253 | messages.push({ |
| 254 | role: "tool", |
| 255 | toolCallId: id, |
| 256 | toolName: readOnly ? "read_file" : "bash", |
| 257 | content: `ok block=${block + 1}\nstatus: success\n${"measurement stable ".repeat(4)}`, |
| 258 | }); |
| 259 | } |
| 260 | messages.push({ role: "assistant", content: "Geometry contract fixture complete." }); |
| 261 | return messages; |
| 262 | }; |
| 263 | |
| 264 | // Ref-resolution storm fixture (#8657): the newest page of a long session |
| 265 | // carries many ref-replaced fields, so opening the session fires a paced |
| 266 | // stream of history_items_patch invalidations — the exact load that used to |
| 267 | // remount the virtual list on every scroll idle and strand the view at |
| 268 | // estimate-based restore landings. The marker sits past the 4KiB preview |
| 269 | // cut, so it only appears in the DOM once the ref has resolved. |
| 270 | export const BENCH_STORM_MARKER = "BENCH STORM HYDRATION RESOLVED"; |
| 271 | const benchStormAnswer = (turn: number): string => { |
| 272 | let body = [ |
| 273 | `## Storm turn ${turn} verification report`, |
| 274 | "", |
| 275 | "Inline preview summary: the batch completed and per-package details follow.", |
| 276 | "", |
| 277 | ].join("\n"); |
| 278 | let row = 0; |
| 279 | while (body.length < 9 * 1024) { |
| 280 | row += 1; |
| 281 | body += `\n- storm-${turn}-${row}: resolved payload ${"y".repeat(90)}`; |
| 282 | } |
| 283 | body += `\n- storm-${turn}-FINAL ${BENCH_STORM_MARKER}`; |
| 284 | return body; |
| 285 | }; |
| 286 | const benchStormReasoning = (turn: number): string => { |
| 287 | let body = `planning storm turn ${turn}: gather results, then tabulate.\n`; |
| 288 | while (body.length < 6 * 1024) body += `reasoning fragment ${turn} ${"z".repeat(90)}\n`; |
| 289 | return `${body}${BENCH_STORM_MARKER}`; |
| 290 | }; |
| 291 | const benchStormHistory = (): HistoryMessage[] => { |
| 292 | // 40 visible turns. The newest 12 turns (exactly the page a session opens |
| 293 | // with) carry ref-replaced answer+reasoning fields; turns 1-28 are small |
| 294 | // and eager. Opening the session resolves ~24 refs at a paced interval. |
| 295 | const messages: HistoryMessage[] = []; |
| 296 | for (let turn = 1; turn <= 40; turn += 1) { |
| 297 | if (turn <= 28) { |
| 298 | messages.push(...benchToolTurn(turn, 3, `Batch ${turn} summary.`)); |
| 299 | } else { |
| 300 | messages.push( |
| 301 | { role: "user", content: `storm turn ${turn}: produce the verification report.` }, |
| 302 | { role: "assistant", content: benchStormAnswer(turn), reasoning: benchStormReasoning(turn), workDurationMs: 900 }, |
| 303 | ); |
| 304 | } |
| 305 | } |
| 306 | return messages; |
| 307 | }; |
| 308 | |
| 309 | export const BENCH_SELECTION_TABLE_MARKER = "SELECTION REPAINT TARGET"; |
| 310 | |
| 311 | const benchSelectionTableHistory = (): HistoryMessage[] => { |
| 312 | const messages: HistoryMessage[] = []; |
| 313 | for (let turn = 1; turn <= 12; turn += 1) { |
| 314 | messages.push( |
| 315 | { role: "user", content: `selection fixture turn ${turn}: summarize the stable table inputs.` }, |
| 316 | { |
| 317 | role: "assistant", |
| 318 | content: [ |
| 319 | `## Selection fixture turn ${turn}`, |
| 320 | "", |
| 321 | "This deterministic paragraph keeps the virtual transcript away from its native top while all rows remain settled.", |
| 322 | "", |
| 323 | "- no streaming output", |
| 324 | "- no asynchronous content refs", |
| 325 | "- stable Markdown geometry", |
| 326 | ].join("\n"), |
| 327 | }, |
| 328 | ); |
| 329 | } |
| 330 | messages.push( |
| 331 | { role: "user", content: "Render the final selection regression table." }, |
| 332 | { |
| 333 | role: "assistant", |
| 334 | content: [ |
| 335 | "## WebView2 selection repaint regression", |
| 336 | "", |
| 337 | "| check | target | expected |", |
| 338 | "| --- | --- | --- |", |
| 339 | "| native multi-click | **SELECTION REPAINT TARGET** | transcript pixels remain stable |", |
| 340 | "| scroll geometry | fixed table row | no viewport movement |", |
| 341 | "| portal lifetime | one toolbar host | no mount churn |", |
| 342 | "", |
| 343 | "The table is the final settled row in this fixture.", |
| 344 | ].join("\n"), |
| 345 | }, |
| 346 | ); |
| 347 | return messages; |
| 348 | }; |
| 349 | |
| 350 | /** The bench session for a mock topic, or undefined for non-bench topics. */ |
| 351 | export function benchTopicHistory(topicId: string): HistoryMessage[] | undefined { |
| 352 | switch (topicId) { |
| 353 | case "topic_bench_tools": |
| 354 | return benchFixture("tools", benchToolDenseHistory); |
| 355 | case "topic_bench_markdown": |
| 356 | return benchFixture("markdown", benchMarkdownHeavyHistory); |
| 357 | case "topic_bench_small": |
| 358 | return benchFixture("small", benchSmallHistory); |
| 359 | case "topic_bench_giant_turn": |
| 360 | return benchFixture("giant", benchGiantTurnHistory); |
| 361 | case "topic_bench_windowed": |
| 362 | return benchFixture("windowed", benchWindowedHistory); |
| 363 | case "topic_bench_reported_long_turn": |
| 364 | return benchFixture("reported-long-turn", benchReportedLongTurnHistory); |
| 365 | case "topic_bench_geometry_contract": |
| 366 | return benchFixture("geometry-contract", benchGeometryContractHistory); |
| 367 | case "topic_bench_storm": |
| 368 | return benchFixture("storm", benchStormHistory); |
| 369 | case "topic_bench_selection_table": |
| 370 | return benchFixture("selection-table", benchSelectionTableHistory); |
| 371 | default: |
| 372 | return undefined; |
| 373 | } |
| 374 | } |
| 375 |