Files
deepseek-harness/examples/desktop/test/renderer-collisions.test.js
ZiyaZhang fc558c2db8 feat(desktop): context page deepening — window bar + compact Config + intervention marker + subagent drilldown
Four Context-page enhancements (lane-ctx-deep, F1-F4):

- F1 context window breakdown: replace percentage-only card header with
  a stacked-bar breakdown of input / cached / output token buckets,
  plus a right-side gauge showing the live window occupancy ratio.
- F2 compact Config tab: fold the sprawling profile Config editor into
  a Config tab on the Context page card, with the same yml-leaf
  ordering as the top-of-window profile picker.
- F3 intervention marker: on the intervention timeline, emit a marker
  glyph at each user-intervention row (turn-flow-glyph-style) so the
  card scans as a single stream instead of a header + separate list.
- F4 subagent drilldown: when a turn's tool trace hits a subagent, the
  Trace panel's Config + Output tabs get a second row of Subagent
  Config / Subagent Output tabs immediately below, driven by the same
  fold-in-place shape the parent panel already uses.

4 new renderer modules (compact-config-model / context-window-breakdown /
intervention-timeline / subagent-drilldown), 6 new test files (41 tests,
all node --test style), 4 QA shoot scripts for CDP-driven regression
screenshots.
2026-07-19 01:22:28 -07:00

327 lines
16 KiB
JavaScript

// Static analysis: guard against the historical "top-level `const api =`
// collision" regression. Every renderer script under src/renderer/*.js loads
// as a classic <script> tag into one shared global scope. A top-level
// `const NAME = ...` in two of them is a hard SyntaxError at load and the
// second script (and everything after) dies silently — that was the
// six-module `const api = { ... }` bug the IIFE fence fixed.
//
// This test runs `node --test`, so the guard fires as part of the same
// suite the rest of the shell already relies on. Cheaper and more reliable
// than a git pre-push hook because a developer never has to opt in.
//
// Rules enforced here:
// 1. All renderer scripts EXCEPT the ones listed in `NON_IIFE_ALLOWLIST`
// must be wrapped in an IIFE (`;(function () { ... })()`). New scripts
// that forget the wrapper trip this immediately.
// 2. Across the allow-listed non-IIFE files, no two top-level `const` /
// `let` / `var` / `function` identifiers may collide.
// 3. No allow-listed non-IIFE file may declare a top-level `const api`.
// That specific name is the historical repro — future files should
// pick a namespaced identifier or stay IIFE-wrapped.
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const RENDERER_DIR = path.join(__dirname, '..', 'src', 'renderer')
// Renderer files that legitimately skip the IIFE fence:
// - renderer.js — the shell entrypoint. Owns `state`, `streamEl`, `bootUi`,
// `onSessionEvent`; every other renderer file reads those globals by name.
// - Dual-export pure modules — files that both `module.exports` (for
// `node --test`) and assign to `window.__dshXxx` (for the renderer).
// Wrapping them in an IIFE breaks the CommonJS require path. Their public
// surface is namespaced under `window.__dshXxx`; only `function` names
// leak into the global scope, and the collision check below still catches
// any accidental duplicate across the allow-list.
const NON_IIFE_ALLOWLIST = new Set([
'renderer.js',
'context-meter.js',
'compact-badge.js',
// demo 批 2 (§1.7 tabs + §1.2 rail): dual-exported pure modules,
// same pattern as compact-badge.js.
'compact-card.js',
'context-rail.js',
// demo 批 3 (§1.4 subagent + §1.6 workflow 五族): dual-exported pure
// modules plus inlined fixture data (debug-fixtures.js).
'workflow-view.js',
'subagent-view.js',
'debug-fixtures.js',
'event-filter.js',
'panels-c.js',
'tool-cards.js',
'widgets.js',
'capabilities.js',
// Growth v2 (Ticket #140) pure model: dual-exported via module.exports +
// window.__dshGrowthV2Model. Follows the compact-badge.js pattern.
'growth-v2-model.js',
// §1.1 + §1.3 pure modules (task #136): same dual-export shape as
// compact-badge — CommonJS for node --test, `window.__dsh*` for the
// renderer. Not IIFE'd so preloadPure can require() them.
'inject-family.js',
'trace-aggregator.js',
// Task #158 cost-chip data source: dual-exported const table +
// side-effect assignment to window.__dshPriceTable. Same pattern as
// debug-fixtures.js — no functions to isolate, just static data.
'price-table.js',
// Task #162 rec 22 pure parser: dual-exported (module.exports for
// node --test, window.__dshParseJson for renderer). Same shape as
// event-filter.js / inject-family.js. No functions collide with
// other top-level identifiers.
'parse-incremental-json.js',
// Task #162 rec 21 reasoning block: dual-exported pure module +
// DOM builder. Follows the same pattern as inject-family.js.
'reasoning-block.js',
// Task #162 rec 23 turn footer: dual-exported pure module + DOM
// builder. Same shape as reasoning-block.js.
'turn-footer.js',
// Task #162 rec 22-bis assistant-turn container: dual-exported
// (module.exports for node --test, window.__dshAssistantTurn for
// renderer). Exposes a TurnBuilder class that assembles reasoning /
// text / tool-row / result / footer children in wire order.
'assistant-turn.js',
// Task #96 F-05: Debug popover mock helpers extracted from renderer.js.
// Non-IIFE by design so the top-level `function mock*` declarations land
// on the same global scope renderer.js's click-listener bindings resolve
// against (`addEventListener('click', mockApproval)` at renderer.js
// top-level). No exports; loads BEFORE renderer.js in index.html.
'mock-fixtures.js',
// Task #188 rubrics + #191 annotation pure models + inlined fixture
// seeds: dual-exported (module.exports for node --test, window.__dsh*
// for renderer). Same shape as compact-badge.js / debug-fixtures.js.
'rubrics-model.js',
'annotation-model.js',
'rubrics-seed.js',
// Hub page (#186 + #190) pure model: dual-exported (module.exports for
// node --test, globalThis.HubModel for the renderer). Follows the
// compact-badge.js / inject-family.js pattern — top-level KIND_ORDER,
// KIND_META, and helper functions are meant to be one shared shape.
'hub-model.js',
// Task #201 turn-flow glyph (trace-viz §4d): dual-exported pure module
// + SVG builder. Same shape as turn-footer.js — CommonJS require for
// node --test, window.__dshTurnFlowGlyph for the renderer.
'turn-flow-glyph.js',
// Task #187 Bench page: pure data model + inlined fixture batch.
// bench-model.js follows the growth-v2-model.js dual-export pattern
// (module.exports for node --test, window.__dshBenchModel for the
// renderer). bench-fixture.js follows the debug-fixtures.js pattern
// (static inlined JSON attached to window.__dshBenchFixture).
'bench-model.js',
'bench-fixture.js',
// Task #185 Context page pure projections: dual-exported
// (module.exports for node --test, window.__dshContextPageModel for
// renderer). Same shape as inject-family.js / context-rail.js. No
// top-level function names collide with the shared renderer scope.
'context-page-model.js',
// Task #225 Tracing index (reference tracing UI-style project runs table): pure
// per-session aggregator dual-exported (module.exports for node --test,
// window.__dshTracingIndexModel for renderer). Reads through
// trace-aggregator's usage/cost helpers so the numbers align with every
// other cost surface. Same shape as bench-model.js / context-page-model.js.
'tracing-index-model.js',
// Ticket #15 (2026-07-17) upstream-align pure modules: dual-exported
// (module.exports for node --test, window.__dsh* for renderer). Same
// shape as inject-family.js / subagent-view.js.
// subagent-lineage.js — live subagent event router (Part A)
// raw-inject.js — envelope:'raw' classifier (Part B)
'subagent-lineage.js',
'raw-inject.js',
// fix/expand-affordance (2026-07-18) universal aria-expanded reflector:
// dual-exported (module.exports for node --test, window.__dshDetailsAria
// for renderer). Same shape as inject-family.js / raw-inject.js — just
// one `wireDetailsAria(details, summary)` helper, no functions collide.
'details-aria.js',
// lane-ctx-deep (task #51, 2026-07-19) Context-page deepening. Four
// dual-exported pure modules — same shape as inject-family.js /
// context-page-model.js. CommonJS require for node --test,
// `window.__dsh*` handle for the renderer. No top-level function names
// collide with the shared renderer scope.
'context-window-breakdown.js',
'intervention-timeline.js',
'compact-config-model.js',
'subagent-drilldown.js',
])
function listRendererScripts() {
return fs.readdirSync(RENDERER_DIR)
.filter((f) => f.endsWith('.js'))
.sort()
}
function isIifeWrapped(source) {
// Walk lines top-down; skip blank / line-comment / 'use strict' / block-
// comment lines and stop at the first substantive statement. That
// statement must open the IIFE (`(function` or `;(function` or the
// `void function` variant). This mirrors how Chromium's script host
// parses the file — the check is intentionally forgiving about spacing
// and the leading semicolon, but strict about "the whole file body
// lives inside one call expression".
const lines = source.split('\n')
let inBlockComment = false
for (const rawLine of lines) {
const line = rawLine.trim()
if (!line) continue
if (inBlockComment) {
if (line.includes('*/')) inBlockComment = false
continue
}
if (line.startsWith('/*')) {
if (!line.includes('*/')) inBlockComment = true
continue
}
if (line.startsWith('//')) continue
if (/^['"]use strict['"];?$/.test(line)) continue
return /^;?\s*\(?\s*(?:void\s+)?function\s*\(/.test(line) ||
/^;?\s*\(\s*(?:async\s+)?function\s*\(/.test(line)
}
return false
}
// Grab every top-level `const|let NAME` declaration and every top-level
// `function NAME` in a source file. Only `const` and `let` collisions are
// FATAL at script-load time (they raise `SyntaxError: Identifier NAME has
// already been declared` and abort the second script, which is exactly what
// the six-module `const api` bug looked like). `function` declarations
// silently overwrite an earlier one, so their collision is a maintenance
// hazard rather than a load-time crash; the returned shape flags each type
// separately so the assertion can grade them differently.
//
// "Top-level" == column-0 declaration. This won't catch cursed cases like
// `\tconst api = ...` but every existing renderer file follows the
// convention, and the check is meant to catch human mistakes rather than
// arbitrary hostile input.
function topLevelBindings(source) {
const fatal = [] // const / let — load-time crash on collision
const soft = [] // function — silent overwrite on collision
const lines = source.split('\n')
for (const line of lines) {
let m = line.match(/^(?:const|let)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=/)
if (m) { fatal.push(m[1]); continue }
m = line.match(/^var\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=/)
// `var` at top-level of a classic script attaches to the global object
// and re-declaration is silently tolerated, so treat it like function.
if (m) { soft.push(m[1]); continue }
m = line.match(/^function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/)
if (m) { soft.push(m[1]); continue }
m = line.match(/^async\s+function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/)
if (m) { soft.push(m[1]); continue }
}
return { fatal, soft }
}
test('every renderer script is IIFE-wrapped except the entrypoint', () => {
const violations = []
for (const file of listRendererScripts()) {
if (NON_IIFE_ALLOWLIST.has(file)) continue
const src = fs.readFileSync(path.join(RENDERER_DIR, file), 'utf8')
if (!isIifeWrapped(src)) {
violations.push(file)
}
}
assert.deepEqual(violations, [],
'These renderer scripts leak top-level bindings into the global scope. ' +
'Wrap them in `;(function () { ... })()` or add to NON_IIFE_ALLOWLIST ' +
'with a comment explaining why.')
})
test('no allow-listed non-IIFE renderer script declares a top-level `const api`', () => {
// Historical repro: six renderer modules each ended with `const api = { ... }`
// to publish their public surface. All six landed in one shared global,
// and the second load was a hard SyntaxError. Even the intended entrypoint
// (renderer.js) must never use this exact name — it's the tripwire.
for (const file of NON_IIFE_ALLOWLIST) {
const src = fs.readFileSync(path.join(RENDERER_DIR, file), 'utf8')
const { fatal } = topLevelBindings(src)
assert.ok(!fatal.includes('api'),
`${file} declares top-level \`const api\` — that's the name that ` +
`collided in the six-module regression. Rename it or hide behind an ` +
`IIFE.`)
}
})
test('no two allow-listed non-IIFE renderer scripts share a top-level const/let', () => {
// Fatal-class check: `const` / `let` at column 0 of a classic script raises
// `SyntaxError: Identifier NAME has already been declared` when the second
// file loads, and every script tag after that never executes. This is the
// load-time crash the IIFE fence exists to prevent. `function` overwrites
// are silent and covered by the softer check below.
//
// The failure message names both culprits so the fix is obvious.
const byName = new Map() // name -> [files]
for (const file of NON_IIFE_ALLOWLIST) {
const src = fs.readFileSync(path.join(RENDERER_DIR, file), 'utf8')
const { fatal } = topLevelBindings(src)
for (const name of fatal) {
if (!byName.has(name)) byName.set(name, [])
byName.get(name).push(file)
}
}
const collisions = []
for (const [name, files] of byName.entries()) {
if (files.length > 1) collisions.push({ name, files })
}
assert.deepEqual(collisions, [],
'Top-level `const`/`let` collision(s) between allow-listed renderer ' +
'scripts. This is a load-time SyntaxError in the browser; the second ' +
'script tag will silently die. Rename or IIFE-wrap the loser.')
})
test('duplicate top-level `function` names between renderer.js and pure modules are known-and-safe', () => {
// The pure modules (event-filter.js, panels-c.js, tool-cards.js, widgets.js,
// context-meter.js) publish their exports via `window.__dshFoo` for the
// renderer AND `module.exports` for `node --test`. Their internal
// `function foo()` names may repeat names inside renderer.js — that's a
// silent overwrite at load, not a crash, and the shape of the pure-module
// wrappers means each caller reads through `window.__dshEventFilter.foo`
// rather than the bare global. This check ratchets the current known set
// so a NEW soft collision has to be reviewed and either accepted (added
// to the allow-list here) or fixed.
const known = new Set([
// event-filter.js's helpers were extracted from renderer.js; renderer.js
// still holds the historical copies for the direct-call sites that
// haven't been retargeted through `window.__dshEventFilter.*` yet. This
// is deliberately dead code on the renderer.js side — kept only so the
// extraction can happen without a lockstep multi-file diff.
'describeSource:renderer.js:event-filter.js',
'isDevOnlyEventType:renderer.js:event-filter.js',
// textFromContentBlocks: same extraction pattern — renderer.js keeps the
// callable copy (the switch-arm handlers call it directly), the pure
// module hosts the tested implementation. The two must stay in sync;
// event-filter.test.js has a source-level drift alarm on the renderer
// copy that guards against the `[${b.type}]` fallback returning.
'textFromContentBlocks:renderer.js:event-filter.js',
// CTX-merge lesson (2026-07-17): turn-flow-glyph (#201) and context-page
// (#185) each need to classify a compact event when projecting the trace
// slice they own. Two independent private helpers land at the same name;
// both keep their local copy because the shape check is intra-module
// (glyph steps vs context ledger rows). No caller reaches the bare
// `isCompactEvent` global — turn-flow-glyph attaches to
// window.__dshTurnFlowGlyph, context-page-model attaches to
// window.__dshContextPageModel. Silent overwrite is harmless here.
'isCompactEvent:turn-flow-glyph.js:context-page-model.js',
])
const byName = new Map()
for (const file of NON_IIFE_ALLOWLIST) {
const src = fs.readFileSync(path.join(RENDERER_DIR, file), 'utf8')
const { soft } = topLevelBindings(src)
for (const name of soft) {
if (!byName.has(name)) byName.set(name, [])
byName.get(name).push(file)
}
}
const unexpected = []
for (const [name, files] of byName.entries()) {
if (files.length < 2) continue
const key = [name, ...files].join(':')
if (known.has(key)) continue
unexpected.push({ name, files })
}
assert.deepEqual(unexpected, [],
'New top-level `function` name collision between renderer files. This ' +
'is a silent overwrite (not a crash), but the pattern was banned. ' +
'Rename or add to the `known` set in this test with a comment.')
})