Files
dsh-context/tests/client.test.mjs
T
bowenliang123 021db5f2a2 build: migrate tsup to tsdown, mirroring the official client-bundle preset
- banner/intro/footer three-part closure-factory wrap (verbatim from
  packages/client/tsdown.client.ts), intro now owns var module/exports
- deps.neverBundle/alwaysBundle guards: host keeps production deps
  external, client keeps only module-table specifiers external
- bundle purity gate: any non-requested @deepseek-ai/* value import is
  a build error (INLINE_SAFE / vendored-library / generated-remote
  allowances mirrored)
- NODE_ENV + import.meta.env defines for inlined node-idiom deps
- client sourcemap on, matching the official artifact contract
- client smoke test now runs the bundle verbatim through the
  __ModuleLoader__ handoff instead of slicing the factory body by
  exact output strings (decoupled from bundler layout)
2026-08-22 02:14:44 +08:00

1638 lines
91 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Functional smoke test for the packaged client bundle: simulates the web
* boot handoff (window.__ModuleLoader__.load), the module-table require,
* and the client ctx (connection/locale/slots/effect + a fake DOM), then
* asserts the plugin registers its dictionaries, styles, and the
* conversation.view tab entry.
*/
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const bundle = await readFile(new URL('../lib/client.js', import.meta.url), 'utf8')
// ---- fake browser environment ----
const registered = new Map() // style tags keyed by data-plugin
const fakeDoc = {
createElement: (tag) => {
const el = { tagName: tag, attrs: {}, textContent: '', parentNode: null }
el.setAttribute = (k, v) => { el.attrs[k] = String(v) }
return el
},
head: {
appendChild(el) {
el.parentNode = { removeChild: () => { el.parentNode = null } }
registered.set(el.attrs['data-plugin'], el)
},
},
querySelectorAll: () => [],
}
const fakeReact = {
createElement: (...args) => ({ kind: 'element', args }),
useState: (init) => [init, () => {}],
useEffect: () => {},
// The ErrorBoundary extends React.Component; a minimal base keeps the
// class construction working (props assigned, state merge via setState).
Component: class Component {
constructor(props) { this.props = props }
setState(partial) { this.state = { ...this.state, ...partial } }
},
}
let handoff = null
globalThis.window = {
__ModuleLoader__: {
load(h) {
assert.ok(h.id === 'dsh-context', 'handoff id must be the package name')
assert.equal(typeof h.factory, 'function')
handoff = h
},
},
}
globalThis.document = fakeDoc
// The module table answers the platform seeds this bundle uses ('react' and
// the shared primitives singleton); everything else rides ctx or is inlined.
// Icons are stubbed as inert elements (glyph rendering is never asserted);
// MarkdownText resolves to a plain div carrying its source text, so the
// raw/markdown toggle assertions can find it by class and text.
const fakePrimitives = {
IconPlusOutline16: (p) => ({ kind: 'icon', name: 'IconPlusOutline16', props: p }),
IconBranchOutline16: (p) => ({ kind: 'icon', name: 'IconBranchOutline16', props: p }),
MarkdownText: (p) => ({ kind: 'element', args: ['div', { className: 'lc-md-stub' }, p.text] }),
}
const require = (spec) => {
assert.ok(spec === 'react' || spec === '@deepseek-ai/dsh-client-ui-primitives',
`bundle must only require platform modules (got "${spec}")`)
return spec === 'react' ? fakeReact : fakePrimitives
}
// ---- materialize the bundle the way the loader does ----
// Run the bundle verbatim: it registers itself through the boot handoff
// (window.__ModuleLoader__.load), and the loader materializes the plugin by
// invoking the captured factory with the module-table require. The factory
// body declares its own module/exports (the bundle intro), so no string
// surgery on the artifact — the test stays decoupled from bundler layout.
new Function(bundle)()
assert.ok(handoff !== null, 'bundle must register through __ModuleLoader__.load')
const pluginExports = handoff.factory(require)
assert.equal(pluginExports.name, 'dsh-context')
assert.deepEqual(pluginExports.inject, ['slots', 'locale'])
// ---- apply the client plugin ----
const localeRegistrations = []
const slotInjections = []
const effects = []
const fakeCtx = {
get: () => undefined,
effect(fn) { effects.push(fn); fn(); return () => {} }, // Cordis runs the effect body immediately
locale: {
register: (ns, dicts) => { localeRegistrations.push([ns, dicts]); return () => {} },
bind: (ns) => (key, vars) => {
const dict = localeRegistrations[0][1].zh
let s = dict[key] !== undefined ? dict[key] : key
if (vars) for (const k in vars) s = s.replace('{' + k + '}', String(vars[k]))
return s
},
},
slots: {
inject: (name, fn) => { slotInjections.push([name, fn]) },
register: (opts, component) => {
assert.equal(typeof component, 'function')
return opts
},
},
}
pluginExports.apply(fakeCtx)
assert.equal(effects.length, 4, 'dictionaries + styles + loadOlderHistory prop + /context command effects')
assert.deepEqual(localeRegistrations[0][0], 'dsh-context')
assert.ok(localeRegistrations[0][1].zh && localeRegistrations[0][1].en, 'bilingual dicts')
const styleTag = registered.get('dsh-context')
assert.ok(styleTag, 'plugin-owned <style data-plugin="dsh-context"> injected')
assert.ok(styleTag.textContent.includes('.lc-root'), 'styles content present')
assert.ok(styleTag.textContent.includes('transition: background-color 120ms ease'), 'row hovers ease in/out')
assert.ok(styleTag.textContent.includes('transition: filter 120ms ease, opacity 120ms ease'), 'composition bar hover eases in/out')
assert.ok(styleTag.textContent.includes('lc-bar-tip-on'), 'composition tooltip fades in and out')
assert.ok(styleTag.textContent.includes('lc-stat-tip'), 'stats cell tooltip bubble styles present')
assert.ok(styleTag.textContent.includes('lc-stat-tipped:hover .lc-stat-tip'), 'stats tooltip reveals on cell hover')
assert.ok(styleTag.textContent.includes('lc-occupied-box-on'), 'occupied frame fades in and out')
assert.equal(slotInjections.length, 2, 'view tab + input overlay injections')
assert.equal(slotInjections[0][0], 'conversation.view')
const registeredOpts = slotInjections[0][1]() // slots.inject callback returns the register result
assert.equal(registeredOpts.name, 'conversation.view')
assert.equal(registeredOpts.id, 'context')
assert.equal(registeredOpts.order, 20)
assert.equal(typeof registeredOpts.label, 'function')
assert.equal(registeredOpts.label(), '上下文', 'tab label localized')
assert.equal(slotInjections[1][0], 'conversation.input.overlay')
const overlayOpts = slotInjections[1][1]()
assert.equal(overlayOpts.name, 'conversation.input.overlay')
assert.equal(overlayOpts.id, 'context-modal')
assert.ok(typeof overlayOpts.inject === 'function', 'overlay injects the modal store hook')
const injected = overlayOpts.inject('s1')
assert.equal(typeof injected.hooks.contextModal.getSnapshot, 'function', 'hooks compartment carries the modal store')
assert.equal(injected.hooks.contextModal.getSnapshot(), false, 'modal closed initially')
// ---- /context command: the plugin's own '/' trigger source ----
// The first fake ctx has no inputTriggers service: the command effect must
// stay inert (soft dependency), which the apply above already proved by not
// throwing. Now apply again with the trigger service present.
{
const sources = []
let localeDict = null
const ctx3 = {
get: (name) => name === 'inputTriggers'
? { registerSource: (s) => { sources.push(s); return () => {} } }
: undefined,
effect: (fn) => { fn(); return () => {} },
locale: {
register: (ns, dicts) => { localeDict = dicts.zh; return () => {} },
bind: () => (key, vars) => {
let s = localeDict !== null && localeDict[key] !== undefined ? localeDict[key] : key
if (vars) for (const k in vars) s = s.replace('{' + k + '}', String(vars[k]))
return s
},
subscribe: () => () => {},
},
slots: { inject: () => {}, register: () => ({}) },
}
pluginExports.apply(ctx3)
assert.equal(sources.length, 1, 'one /context trigger source registered')
const src = sources[0]
assert.equal(src.trigger, '/')
assert.equal(src.name, 'context')
// Candidates: leading-only, prefix-filtered, description localized.
const req = (query, position = 'leading') => ({ query, position, signal: new AbortController().signal })
assert.deepEqual(await src.candidates({ sessionId: 's1' }, req('')), [{ name: 'context', description: '查看当前上下文构成,浏览各步骤组成' }])
assert.deepEqual((await src.candidates({ sessionId: 's1' }, req('cont'))).length, 1, 'prefix match')
assert.deepEqual(await src.candidates({ sessionId: 's1' }, req('xyz')), [], 'non-prefix miss')
assert.deepEqual(await src.candidates({ sessionId: 's1' }, req('', 'inline')), [], 'inline positions never offer the command')
// Menu pick: opens the session's modal and answers 'handled'; the token
// stays in the composer while the modal is open (consumed on close).
const outcome = src.onPick({ candidate: { name: 'context' }, session: { sessionId: 's1' }, position: 'leading', via: 'menu', span: { start: 0, end: 8, draftRev: 1 } })
assert.equal(outcome, 'handled', 'menu pick is handled internally')
assert.equal(injected.hooks.contextModal.getSnapshot(), true, 'menu pick opens the modal')
// Bare enter: also 'handled' (nothing is submitted, the draft keeps the
// token until the modal closes).
assert.equal(await src.matchEnter({ sessionId: 's1' }, '/context', new AbortController().signal), 'handled', 'bare enter opens the modal without submitting')
assert.equal(await src.matchEnter({ sessionId: 's1' }, '/context now', new AbortController().signal), undefined, 'argued lines miss')
assert.equal(await src.matchEnter({ sessionId: 's1' }, '/compact', new AbortController().signal), undefined, 'other commands miss')
}
console.log('✔ client bundle test passed (handoff, require table, dicts, styles, slot registration, /context command)')
// ---- chart render test: fixed-width bars, horizontal scroll, turn ranges ----
// Materialize the bundle a second time with a STATEFUL fake React so the
// view component can be driven from loading -> data, then walk the element
// tree the component produces. Hooks are tracked PER component function
// (like React fibers), so re-rendering re-reads the same slots.
const hookStates = new Map() // component fn -> [value, setter][] slots
let currentHooks = null
let hookCursor = 0
const statefulReact = {
createElement: (...args) => ({ kind: 'element', args }),
useCallback: (fn) => fn,
useState(init) {
const i = hookCursor++
const slots = currentHooks // captured by the setter: stable per fiber
if (slots[i] === undefined) {
const set = (v) => { slots[i][0] = typeof v === 'function' ? v(slots[i][0]) : v }
slots[i] = [typeof init === 'function' ? init() : init, set]
}
return slots[i]
},
useEffect(fn) {
// Store the callback in a hook slot (never auto-run) so tests can drive
// effects explicitly, with the LATEST render's closure — like React.
const i = hookCursor++
if (currentHooks[i] === undefined) currentHooks[i] = { effect: undefined }
currentHooks[i].effect = fn
return currentHooks[i]
},
// Memo values are recomputed every render in tests (no staleness assertions).
useMemo: (fn) => fn(),
useRef(init) {
// Like React: the SAME ref object is returned on every render (stored
// in the fiber slot), so tree ref props and effect closures agree.
const i = hookCursor++
if (currentHooks[i] === undefined) currentHooks[i] = { ref: { current: init } }
return currentHooks[i].ref
},
useLayoutEffect(fn) {
// Store the callback in a hook slot so tests can drive the layout effect.
// Always overwrite: real React runs the LATEST render's closure.
const i = hookCursor++
currentHooks[i] = { effect: fn }
return currentHooks[i]
},
Component: class Component {
constructor(props) { this.props = props }
setState(partial) { this.state = { ...this.state, ...partial } }
},
}
const requireStateful = (spec) => {
assert.ok(spec === 'react' || spec === '@deepseek-ai/dsh-client-ui-primitives',
`bundle must only require platform modules (got "${spec}")`)
return spec === 'react' ? statefulReact : fakePrimitives
}
const pluginExports2 = handoff.factory(requireStateful)
const DICT_FOR_TEST = { 'tab': 'Context', 'loading': '…', 'error': 'ERR:', 'error.retry': 'retry', 'detail.step': 'Turn {t} · Step {s}', 'gran.step': 'Step', 'gran.turn': 'Turn', 'detail.turn': 'Turn {t} · {n} steps', 'detail.lastStep': 'last step', 'overview.used': 'of context used', 'overview.free': 'Free window', 'events.at': 'Turn {t} · Step {s}', 'events.range': 'Turn {t} · Step {a}→{b}', 'events.rangeTo': 'Turn {a} · Step {as} → Turn {b} · Step {bs}', 'stats.recycleSub': '{c} compactions · {p} prunes', 'tip.step': 'Turn {t} · Step {s}', 'tip.turn': 'Turn {t} · {n} steps', 'tip.total': 'total ≈ {n}', 'tip.actual': ' (actual {n})', 'trend.title': 'History', 'trend.empty': 'empty trend', 'cmd.close': 'Close', 'cat.user': 'User', 'browser.live': 'Live (next request)', 'browser.liveNow': 'Live · next request', 'browser.items': '{n} items', 'browser.noContent': 'outside the loaded window', 'browser.loading': 'loading older history', 'browser.preview': 'preview', 'browser.noHeader': 'older plugin build',
'browser.deltaHint': 'vs previous turn', 'overview.compactReserve': 'compact reserve {pct}%', 'tool.desc': 'Description', 'tool.params': 'Parameters', 'tool.paramsEmpty': '(no parameters)', 'tool.jsonToggle': 'View Raw JSON', 'tool.jsonHide': 'Collapse', 'rich.raw': 'Raw', 'rich.md': 'Markdown', 'rich.toMd': 'View as Markdown', 'rich.toRaw': 'View Raw Text', 'attach.images': 'Images', 'attach.other': 'Other content', 'attach.image': 'Image', 'attach.open': 'Open full image', 'attach.loading': '…', 'attach.loadFailed': 'Load failed · click to retry', 'attach.orig': 'original {d}' }
let viewComponent = null
let modalComponent = null
let modalSource = null
const bailCalls = []
const provideDescriptors = []
// Conversation-service holder (drives resolveImage for image cards):
// undefined until the image-card test arms it, so every earlier render
// exercises the metadata-only degradation.
let conversationHolder = undefined
const fakeCtx2 = {
get: (name) => {
if (name === 'conversation') return conversationHolder
if (name === 'inputTriggers') return { registerSource: (s) => { modalSource = s; return () => {} } }
if (name === 'sessions') return {
scope: (id) => id === 's1' ? { bail: (...args) => { bailCalls.push(args); return true } } : undefined,
provide: (d) => { provideDescriptors.push(d); return () => {} },
}
return undefined
},
effect: (fn) => { fn(); return () => {} },
locale: {
register: () => () => {},
bind: () => (key, vars) => {
let s = DICT_FOR_TEST[key] !== undefined ? DICT_FOR_TEST[key] : key
if (vars) for (const k in vars) s = s.replace('{' + k + '}', String(vars[k]))
return s
},
},
slots: {
inject: (name, fn) => { fn() }, // the register call inside captures the component
register: (opts, component) => {
if (opts.name === 'conversation.view') viewComponent = component
if (opts.name === 'conversation.input.overlay') modalComponent = component
return opts
},
},
}
pluginExports2.apply(fakeCtx2)
assert.ok(viewComponent !== null, 'view component captured')
// The loadOlderHistory contribution rides the harness's sessions.provide
// channel: one declared prop, resolved per session to the session's own
// history-pagination verb.
assert.equal(provideDescriptors.length, 1, 'one sessions.provide contribution registered')
assert.deepEqual(provideDescriptors[0].props, ['loadOlderHistory'], 'the contributed prop is loadOlderHistory')
{
let olderPulled = 0
const contribution = provideDescriptors[0].resolve({ session: { loadOlder: async () => { olderPulled += 1 } } })
assert.equal(typeof contribution.props.loadOlderHistory, 'function', 'resolved prop is a function')
await contribution.props.loadOlderHistory()
assert.equal(olderPulled, 1, 'the prop delegates to session.loadOlder()')
}
// The framework standard kit delivers the context timeline as a push-fed
// projection (`useProjection('contextTimeline')`) and the official
// token-meter occupancy as another (`useProjection('contextPressure')`); the
// test drives renders by swapping the holders the stub reads, exactly like
// session/projection frames.
let dataValue = null
let pressureValue = undefined
let usageValue = undefined
let headersValue = undefined
// Optional session-snapshot hook + pagination verb holders: undefined until
// the auto-load test arms them, so every earlier render exercises the
// no-`useSession` degradation exactly as before.
let useSessionHolder = undefined
let loadOlderHolder = undefined
const renderView = () => evaluate(viewComponent({
sessionId: 's1',
useProjection: (key) => (key === 'contextTimeline' ? dataValue
: (key === 'contextPressure' ? pressureValue
: (key === 'contextHeaders' ? headersValue
: (key === 'tokenUsage' ? usageValue : undefined)))),
useSession: useSessionHolder,
loadOlderHistory: loadOlderHolder,
}))
/** Invoke function-typed elements so hooks run and the tree materializes.
* Hooks are keyed by the component's fiber path (e.g. root/ContextView#0/
* StackedBar#0), so distinct instances of the same component keep state.
* Class components (the ErrorBoundary) are simulated with a React-like
* lifecycle: an instance is created from the prototype with props + empty
* state, render errors are caught through the static getDerivedStateFromError
* (the boundary protocol) and the fallback re-renders, and instances are
* registered so tests can inspect the caught error. */
const classInstances = new Map()
function evaluate(node, path = '', fnIdx = 0) {
if (node === null || typeof node !== 'object') return node
if (node.kind === 'element') {
const [type, props, ...children] = node.args
if (typeof type === 'function' && type.prototype !== undefined && typeof type.prototype.render === 'function') {
const key = path + '/' + (type.name || 'anon') + '#' + fnIdx
// The fake createElement keeps children in args, not props; real React
// puts them on props.children (single child as-is, several as an
// array), which the class instance's render() reads. Constructing with
// `new` runs the real constructor, so props AND the initial state are
// exactly what React would set.
const childrenOf = children.length === 0 ? {} : children.length === 1 ? { children: children[0] } : { children }
const inst = new type(Object.assign({}, props || {}, childrenOf))
classInstances.set(key, inst)
try {
return evaluate(inst.render(), key)
} catch (err) {
if (typeof type.getDerivedStateFromError === 'function') {
inst.state = type.getDerivedStateFromError(err)
return evaluate(inst.render(), key)
}
throw err
}
}
if (typeof type === 'function') {
const key = path + '/' + (type.name || 'anon') + '#' + fnIdx
currentHooks = hookStates.get(key)
if (currentHooks === undefined) {
currentHooks = []
hookStates.set(key, currentHooks)
}
hookCursor = 0
return evaluate(type(props), key)
}
const kids = []
let f = 0
const walkChildren = (c) => {
if (Array.isArray(c)) { for (const x of c) walkChildren(x); return }
if (c !== null && typeof c === 'object' && c.kind === 'element' && typeof c.args[0] === 'function') {
kids.push(evaluate(c, path, f++))
} else {
kids.push(evaluate(c, path, f))
}
}
for (const c of children) walkChildren(c)
return { kind: 'element', args: [type, props, ...kids] }
}
if (Array.isArray(node)) return node.map(n => evaluate(n, path, fnIdx))
return node
}
/** Walk the h() element tree, returning every node whose className matches. */
function byClass(root, className) {
const found = []
const walk = (node) => {
if (node === null || node === undefined || typeof node !== 'object') return
if (node.kind === 'element') {
const props = node.args[1] || {}
if (typeof props.className === 'string' && props.className.split(' ').includes(className)) found.push(node)
for (let i = 2; i < node.args.length; i++) walk(node.args[i])
} else if (Array.isArray(node)) {
for (const child of node) walk(child)
}
}
walk(root)
return found
}
// Render 1 (no value yet): creates the ContextView hook slots and shows loading.
renderView()
const snapshot = {
ok: true, model: 'deepseek-v4', provider: 'deepseek', contextWindow: 128000,
current: { system: 10, tools: 20, user: 30, inject: 5, assistant: 15, tool: 20, total: 100 },
toolList: [], requests: [
{ seq: 1, turn: 1, step: 0, time: 1000, system: 10, tools: 20, user: 30, inject: 5, assistant: 15, tool: 20, total: 100, prompt: 95 },
{ seq: 2, turn: 1, step: 1, time: 2000, system: 10, tools: 20, user: 25, inject: 5, assistant: 10, tool: 20, total: 90 },
{ seq: 3, turn: 2, step: 0, time: 3000, system: 10, tools: 20, user: 40, inject: 5, assistant: 12, tool: 20, total: 107 },
{ seq: 4, turn: 3, step: 0, time: 4000, system: 10, tools: 20, user: 20, inject: 5, assistant: 8, tool: 20, total: 83, prompt: 83000 },
],
events: [], nodes: [
{ seq: 1, cat: 'user', tokens: 10, text: 'first message', time: 1000 },
{ seq: 2, cat: 'assistant', tokens: 20, text: 'second message', time: 65000 },
], droppedNodes: 0,
}
// Deliver the timeline like a session/projection frame: swap the holder the
// useProjection stub reads, then re-render the view.
// The ContextView fiber: the boundary wrapper owns an EMPTY hook slot array,
// so the data-driven body's fiber is the one carrying the view hooks.
const ctxKey = [...hookStates.keys()].find(k => k.includes('ContextView') && hookStates.get(k).length > 0)
assert.ok(ctxKey, 'ContextView fiber registered')
dataValue = snapshot
const tree = renderView()
const bars = byClass(tree, 'lc-bar')
assert.equal(bars.length, 4, 'one bar per request')
const turns = byClass(tree, 'lc-turn')
assert.equal(turns.length, 3, 'three turn ranges (T1 has two bars, T2/T3 one each)')
assert.deepEqual(turns.map(t => t.args[2]), ['T1', 'T2', 'T3'], 'turn labels in order')
const turnWidths = turns.map(t => t.args[1].style.width)
assert.equal(turnWidths[0], '30px', 'T1 tick spans two columns (2*16-2)')
assert.equal(turnWidths[1], '14px', 'T2 tick spans one column')
assert.ok(byClass(tree, 'lc-chart-scroll').length === 1, 'scroll container present')
assert.ok(byClass(tree, 'lc-turns').length === 1, 'turn tick row present')
// ---- context stats board: totals over the retained window ----
// fixture: 4 requests (turns 1,1,2,3), no events yet -> all event counters 0.
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'))
const statVals = byClass(tree, 'lc-stat-value').map(n => n.args[2])
assert.equal(statVals.length, 7, 'seven stats cells (turns / steps / injections / compactions / prunes / cache hit / cost)')
assert.equal(statVals[0], '3', 'turns counted by distinct turn')
assert.equal(statVals[1], '4', 'steps = request count')
assert.equal(statVals[2], '0', 'no injections yet')
assert.equal(statVals[3], '0', 'no compactions yet')
assert.equal(statVals[4], '0', 'no prunes yet')
assert.equal(statVals[5], '—', 'no tokenUsage projection yet -> cache-hit cell shows a dash')
assert.equal(statVals[6], '—', 'no cost totals yet -> cost cell shows a dash')
// ---- plugin info card: two full-width rows; every row is itself a link ----
function plainText(node) {
if (typeof node === 'string') return node
if (node === null || node === undefined || typeof node !== 'object') return ''
if (node.kind === 'element') return node.args.slice(2).map(plainText).join('')
if (Array.isArray(node)) return node.map(plainText).join('')
return ''
}
const piLabels = byClass(tree, 'lc-pi-label').map(n => n.args[2])
const piValues = byClass(tree, 'lc-pi-value').map(n => n.args[2])
const piGrid = byClass(tree, 'lc-pi-grid')
assert.equal(piGrid.length, 1, 'plugin info rendered as one grid')
assert.equal(piLabels.length, 2, 'plugin info: two rows (Plugin / GitHub)')
assert.equal(plainText(piValues[0]), 'dsh-context (v' + pkg.version + ')', 'Plugin row combines package id + version (update chip only after the npm check resolves)')
assert.equal(plainText(piValues[1]), 'bowenliang123/dsh-context', 'GitHub row shows the short owner/repo')
// Each row IS the link — Plugin goes to the repo's releases page, GitHub to
// the repo root.
const linkRows = byClass(tree, 'lc-pi-row')
assert.equal(linkRows.length, 2, 'every row is a whole-row link')
assert.equal(linkRows[0].args[1].href, 'https://github.com/bowenliang123/dsh-context/releases', 'Plugin → GitHub releases page')
assert.equal(linkRows[1].args[1].href, 'https://github.com/bowenliang123/dsh-context', 'GitHub → GitHub repo')
// Hover affordance is CSS-driven (row-level `:hover` underlines the value);
// no JS state needed, so no onMouseEnter/onMouseLeave handlers.
// ---- hover linking: hovering a trend bar updates the detail below ----
const ctxSlots = hookStates.get(ctxKey) // selected(0) hovered(1) hoverTurn(2) tick(3) gran(4) hoverCat(5)
function textOf(node) {
if (typeof node === 'string') return node
if (node === null || node === undefined || typeof node !== 'object') return ''
if (node.kind === 'element') return node.args.slice(2).map(textOf).join('')
if (Array.isArray(node)) return node.map(textOf).join('')
return ''
}
const detailStep = (tr) => {
const head = byClass(tr, 'lc-detail-head')[0]
return head === undefined ? '' : textOf(head).trim()
}
let tr = renderView()
assert.match(detailStep(tr), /Turn 3/, 'detail defaults to the newest request (Turn 3)')
assert.equal(byClass(tr, 'lc-bar-hovered').length, 0, 'no hovered bar initially')
ctxSlots[1][1](3) // setHoveredSeq(seq 3, turn 2)
tr = renderView()
assert.match(detailStep(tr), /Turn 2/, 'hovering a bar links the detail below to it')
const hovered = byClass(tr, 'lc-bar-hovered')
assert.equal(hovered.length, 1, 'exactly one hovered bar')
assert.equal(hovered[0].args[1].key, 3, 'hovered bar is seq 3')
const bar3 = byClass(tr, 'lc-bar').find(b => b.args[1].key === 3)
assert.equal(typeof bar3.args[1].onMouseEnter, 'function', 'bars carry onMouseEnter')
assert.equal(typeof bar3.args[1].onClick, 'function', 'bars carry onClick')
// the instant custom tooltip replaces the delayed native title
const chartTip = byClass(tr, 'lc-chart-tip')
assert.equal(chartTip.length, 1, 'hovering shows the custom tooltip immediately')
assert.match(textOf(chartTip[0]), /Turn 2 · Step 0/, 'tooltip names the hovered request')
assert.match(textOf(chartTip[0]), /total ≈ 107/, 'tooltip carries the estimated total')
assert.equal(typeof chartTip[0].args[1].style.left, 'string', 'tooltip is positioned at the bar column')
// turn-aware dimming: the chart is in dim mode while a turn is focused
assert.equal(byClass(tr, 'lc-chart-dim').length, 1, 'bar hover activates the turn-aware dim')
ctxSlots[1][1](null) // leave the plot
tr = renderView()
assert.match(detailStep(tr), /Turn 3/, 'leaving the plot reverts the detail to the newest request')
assert.equal(byClass(tr, 'lc-chart-tip').length, 0, 'tooltip clears with the hover')
assert.equal(byClass(tr, 'lc-chart-dim').length, 0, 'dim clears with the hover')
// ---- overview stacked bar: themed hover tooltip per segment ----
const overviewStack = byClass(tr, 'lc-stacked').find(s => s.args[1].style.height === '16px')
assert.ok(overviewStack, 'overview stacked bar present')
const segment = overviewStack.args.slice(2).flat().find(s => s !== null)
assert.ok(segment, 'overview has segments')
assert.equal(segment.args[1].title, undefined, 'native title replaced by the custom tooltip')
assert.equal(typeof segment.args[1].onMouseEnter, 'function', 'segments carry onMouseEnter')
segment.args[1].onMouseEnter({ clientX: 120 }) // fake pointer; ref is null in tests -> centered fallback
tr = renderView()
const tip = byClass(tr, 'lc-bar-tip-on')
assert.equal(tip.length, 1, 'hovering a segment shows the tooltip')
assert.match(textOf(tip[0]), /\(10%\)/, 'tooltip shows the segment share of the total')
// The occupied-region reference frame appears on hover (there is a free
// track here, so the legend % refers to the boxed part, not the bar width).
assert.equal(byClass(tr, 'lc-occupied-box-on').length, 1, 'hovered segment frames the occupied region')
const occBox = byClass(tr, 'lc-occupied-box-on')[0]
assert.equal(typeof occBox.args[1].style.width, 'string', 'frame width follows the used share')
assert.equal(typeof tip[0].args[1].style.left, 'string', 'tooltip is positioned along the pointer')
// ---- composition bar hover highlights the matching legend chip (and back) ----
// the tooltip test above left the first segment hovered -> its chip is on
let chipsOn = byClass(tr, 'lc-chip-on')
assert.equal(chipsOn.length, 1, 'hovered segment highlights its legend chip')
const chip0 = byClass(tr, 'lc-chip')[0]
assert.equal(typeof chip0.args[1].onMouseEnter, 'function', 'legend chips carry onMouseEnter')
chip0.args[1].onMouseEnter()
tr = renderView()
assert.equal(byClass(tr, 'lc-stacked-seg-on').length, 2, 'hovering a chip highlights its segment (overview + mirrored browser bar)')
assert.equal(byClass(tr, 'lc-bar-tip-on').length, 1, 'hovering a chip also shows the tooltip above its segment')
assert.match(textOf(byClass(tr, 'lc-bar-tip-on')[0]), /\(10%\)/, 'chip-driven tooltip carries the share')
chip0.args[1].onMouseLeave()
tr = renderView()
assert.equal(byClass(tr, 'lc-stacked-seg-on').length, 0, 'leaving the chip clears the segment highlight')
assert.equal(byClass(tr, 'lc-chip-on').length, 0, 'leaving the chip clears the chip highlight')
assert.equal(byClass(tr, 'lc-bar-tip-on').length, 0, 'leaving the chip fades the tooltip out')
// segment -> chip, on a different category
const seg1 = byClass(tr, 'lc-stacked').find(s => s.args[1].style.height === '16px')
.args.slice(2).flat().filter(s => s !== null)[1]
seg1.args[1].onMouseEnter({ clientX: 80 })
tr = renderView()
const chipsOn2 = byClass(tr, 'lc-chip-on')
assert.equal(chipsOn2.length, 1, 'hovering another segment moves the chip highlight')
// the label is now a styled span; compare its text (the highlighted chip's
// category name must equal the matching chip's)
assert.equal(textOf(chipsOn2[0].args[3]), textOf(byClass(tr, 'lc-chip')[1].args[3]), 'the matching chip is highlighted')
assert.equal(byClass(tr, 'lc-stacked-seg-on').length, 2, 'the hovered segment is marked (overview + mirrored browser bar)')
// ---- the free window space (blank track) is hoverable too ----
// fixture: window 128000 vs anchored occupancy 83017 -> 44983 free (35%)
const freeSeg = byClass(tr, 'lc-stacked-free')[0]
assert.ok(freeSeg, 'free window segment present when contextWindow > usage')
assert.equal(typeof freeSeg.args[1].onMouseEnter, 'function', 'free segment carries onMouseEnter')
freeSeg.args[1].onMouseEnter()
tr = renderView()
const freeTip = byClass(tr, 'lc-bar-tip-on')
assert.equal(freeTip.length, 1, 'hovering the blank space shows the tooltip')
assert.equal(byClass(tr, 'lc-occupied-box-on').length, 1, 'hovering the free track still frames the occupied region')
assert.match(textOf(freeTip[0]), /Free window 45\.0k \(35%\)/, 'tooltip names the free window and its share')
assert.equal(byClass(tr, 'lc-stacked-free-on').length, 1, 'free segment highlights on hover')
assert.equal(byClass(tr, 'lc-chip-on').length, 0, 'no legend chip matches the free space')
const stackEl = byClass(tr, 'lc-stacked').find(s => s.args[1].style.height === '16px')
stackEl.args[1].onMouseLeave()
tr = renderView()
assert.equal(byClass(tr, 'lc-bar-tip-on').length, 0, 'leaving the stack fades the free tooltip out')
assert.equal(byClass(tr, 'lc-occupied-box-on').length, 0, 'leaving the stack fades the occupied frame out')
// ---- auto-compaction reserve band: the rightmost (1−0.8) of the window is
// striped headroom; hovering it explains the area instead of the free track.
// fixture: max 128000 (window) > 83017 used -> scale = window -> 80%/20%.
const reserveEl = byClass(tr, 'lc-reserve')[0]
assert.ok(reserveEl, 'auto-compaction reserve band rendered when the window is known')
assert.equal(reserveEl.args[1].style.left, '80%', 'reserve starts at the 80% threshold')
assert.equal(reserveEl.args[1].style.width, '20%', 'reserve covers the rightmost 20% of the window')
assert.equal(typeof reserveEl.args[1].onMouseEnter, 'function', 'reserve band is hoverable')
reserveEl.args[1].onMouseEnter()
tr = renderView()
const reserveTip = byClass(tr, 'lc-bar-tip-on')
assert.equal(reserveTip.length, 1, 'hovering the reserve shows its explanation')
assert.match(textOf(reserveTip[0]), /compact reserve 80%/, 'reserve tooltip names the compaction threshold')
assert.equal(byClass(tr, 'lc-occupied-box-on').length, 0, 'reserve hover clears the segment reference frame')
reserveEl.args[1].onMouseLeave()
tr = renderView()
assert.equal(byClass(tr, 'lc-bar-tip-on').length, 0, 'leaving the reserve hides its tooltip')
// ---- turn strip: one color block per turn, aligned with the bars above ----
let turnBlocks = byClass(tr, 'lc-turn')
assert.equal(turnBlocks.length, 3, 'one turn block per turn')
assert.equal(typeof turnBlocks[0].args[1].onMouseEnter, 'function', 'turn blocks carry onMouseEnter')
const blockColors = turnBlocks.map(b => b.args[1].style.background)
assert.ok(blockColors.every(c => typeof c === 'string' && c.length > 0), 'turn blocks carry a color')
assert.notEqual(blockColors[0], blockColors[1], 'consecutive turns get distinct colors')
assert.equal(turnBlocks[0].args[1].title, 'T1', 'turn blocks carry a full-label tooltip')
turnBlocks[0].args[1].onMouseEnter() // T1 (covers seq 1 and 2)
tr = renderView()
const inTurn = byClass(tr, 'lc-bar-in-turn')
assert.equal(inTurn.length, 2, 'hovering T1 highlights its two bars')
assert.deepEqual(inTurn.map(b => b.args[1].key), [1, 2], 'highlighted bars are seq 1 and 2')
const onBlocks = byClass(tr, 'lc-turn-on')
assert.equal(onBlocks.length, 1, 'exactly one turn block highlighted')
assert.equal(onBlocks[0].args[2], 'T1', 'highlighted block is T1')
assert.equal(byClass(tr, 'lc-chart-dim').length, 1, 'strip hover also dims bars outside the turn')
// leaving the strip clears the turn highlight
const strip = byClass(tr, 'lc-turns')[0]
assert.equal(typeof strip.args[1].onMouseLeave, 'function', 'strip carries onMouseLeave')
strip.args[1].onMouseLeave()
tr = renderView()
assert.equal(byClass(tr, 'lc-bar-in-turn').length, 0, 'leaving the strip clears bar highlights')
assert.equal(byClass(tr, 'lc-chart-dim').length, 0, 'leaving the strip clears the dim')
// hovering a bar highlights its turn block (bidirectional)
ctxSlots[1][1](3) // hover seq 3 (turn 2)
tr = renderView()
const onBlocks2 = byClass(tr, 'lc-turn-on')
assert.equal(onBlocks2.length, 1, 'bar hover highlights exactly one turn block')
assert.equal(onBlocks2[0].args[2], 'T2', 'hovering a bar highlights its turn block')
ctxSlots[1][1](null)
// ---- granularity toggle: one bar per step vs one bar per turn ----
// the trend card scopes its own toggle row: the events card kind toggles
// reuse the pill-button classes, so address the row, not the buttons.
const granRow = () => byClass(tr, 'lc-gran')[0].args.slice(2)
const onBtns = (row) => row.filter(b => String(b.args[1].className || '').includes('lc-gran-on'))
let granBtns = granRow()
assert.equal(granBtns.length, 2, 'granularity toggle has two buttons')
assert.equal(granBtns[0].args[2], 'Step', 'first button is step granularity')
assert.equal(granBtns[1].args[2], 'Turn', 'second button is turn granularity')
assert.equal(onBtns(granRow()).length, 1, 'step is active by default')
assert.equal(byClass(tr, 'lc-bar').length, 4, 'step mode: one bar per step')
// switch to turn granularity: the 4 steps collapse into 3 turn bars
granBtns[1].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-bar').length, 3, 'turn mode: one bar per turn')
assert.equal(byClass(tr, 'lc-turn').length, 3, 'turn strip still has one block per turn')
const turnOn = onBtns(granRow())
assert.equal(turnOn[0].args[2], 'Turn', 'turn button is active after switching')
// turn bars keep the uniform column width and align with their strip blocks
const turnBars = byClass(tr, 'lc-bar')
const t1Bar = turnBars.find(b => b.args[1].key === 2)
assert.ok(t1Bar, 'T1 is aggregated into its last step (seq 2)')
for (const b of turnBars) assert.equal(b.args[1].style.width, '14px', 'every turn bar keeps the uniform column width')
const turnBlocks2 = byClass(tr, 'lc-turn')
for (const blk of turnBlocks2) assert.equal(blk.args[1].style.width, '14px', 'every turn block matches the bar width (aligned)')
assert.equal(turnBlocks2[0].args[1].style.width, t1Bar.args[1].style.width, 'T1 bar and block align 1:1')
// the turn detail is labeled with the step count and tagged as the last step
t1Bar.args[1].onMouseEnter()
tr = renderView()
assert.match(detailStep(tr), /Turn 1 · 2 steps/, 'turn detail shows the step count, not a bare step number')
assert.equal(byClass(tr, 'lc-detail-tag').length, 1, 'the last-step tag marks the shown breakdown')
assert.equal(byClass(tr, 'lc-detail-tag')[0].args[2], 'last step', 'tag text localized')
ctxSlots[1][1](null)
// back to step granularity
granBtns = granRow()
granBtns[0].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-bar').length, 4, 'back to one bar per step')
assert.equal(onBtns(granRow())[0].args[2], 'Step', 'step button active again')
// ---- edge fades signal reachable history beyond the viewport ----
let scroller = byClass(tr, 'lc-chart-scroll')[0]
const fakeScroller = { scrollLeft: 200, clientWidth: 120, scrollWidth: 800 }
scroller.args[1].onScroll({ currentTarget: fakeScroller })
tr = renderView()
assert.equal(byClass(tr, 'lc-chart-fade-l').length, 1, 'left fade shown while scrolled into history')
assert.equal(byClass(tr, 'lc-chart-fade-r').length, 1, 'right fade shown while more bars follow')
fakeScroller.scrollLeft = 680
scroller.args[1].onScroll({ currentTarget: fakeScroller })
tr = renderView()
assert.equal(byClass(tr, 'lc-chart-fade-r').length, 0, 'right fade gone at the right end')
assert.equal(byClass(tr, 'lc-chart-fade-l').length, 1, 'left fade stays at the right end')
fakeScroller.scrollLeft = 0
scroller.args[1].onScroll({ currentTarget: fakeScroller })
tr = renderView()
assert.equal(byClass(tr, 'lc-chart-fade-l').length, 0, 'left fade gone at the start')
// ---- no 80-bar cap: every request the host sends is rendered ----
const bigRequests = []
for (let i = 0; i < 120; i++) {
bigRequests.push({
seq: 1000 + i, turn: 1 + Math.floor(i / 4), step: i % 4, time: 1000 * i,
system: 10, tools: 20, user: 30, inject: 5, assistant: 15, tool: 20, total: 100,
})
}
dataValue = { ...snapshot, requests: bigRequests }
tr = renderView()
assert.equal(byClass(tr, 'lc-bar').length, 120, 'all requests render — earlier turns/steps stay reachable')
assert.equal(byClass(tr, 'lc-turn').length, 30, 'turn strip follows the full history')
dataValue = snapshot
tr = renderView()
assert.equal(byClass(tr, 'lc-bar').length, 4, 'snapshot restored')
// ---- default anchor: the newest bars sit at the right edge ----
const scrollNode = byClass(tr, 'lc-chart-scroll')[0]
const scrollEl = { scrollLeft: 0, clientWidth: 120, scrollWidth: 800 }
scrollNode.args[1].ref.current = scrollEl // attach a fake layout element
const trendKey = [...hookStates.keys()].find(k => k.includes('TrendChart'))
assert.ok(trendKey, 'TrendChart fiber registered')
const layoutEffectSlot = hookStates.get(trendKey).find(s => s && typeof s.effect === 'function')
assert.ok(layoutEffectSlot, 'chart layout effect captured')
layoutEffectSlot.effect()
assert.equal(scrollEl.scrollLeft, 800, 'initial layout anchors at the right (newest bars)')
scrollEl.scrollLeft = 200
layoutEffectSlot.effect()
assert.equal(scrollEl.scrollLeft, 200, 'scrolling away from the end is respected')
scrollEl.scrollLeft = 780
layoutEffectSlot.effect()
assert.equal(scrollEl.scrollLeft, 800, 'stays anchored at the end while near it')
tr = renderView()
assert.equal(byClass(tr, 'lc-chart-fade-l').length, 1, 'left fade shown once anchored at the newest bars')
assert.equal(byClass(tr, 'lc-chart-fade-r').length, 0, 'no right fade at the end')
// ---- granularity switches re-anchor at the newest bars ----
// (the turn->step report: returning to step must show the right edge)
const latestEffect = () => hookStates.get(trendKey).find(s => s && typeof s.effect === 'function')
scrollEl.scrollLeft = 0 // stale left edge from the narrow turn chart
const granTurnBtn = granRow()[1]
granTurnBtn.args[1].onClick() // step -> turn
tr = renderView()
latestEffect().effect()
assert.equal(scrollEl.scrollLeft, 800, 'switching to turn re-anchors at the newest bars')
scrollEl.scrollLeft = 0
const granStepBtn = granRow()[0]
granStepBtn.args[1].onClick() // turn -> step
tr = renderView()
latestEffect().effect()
assert.equal(scrollEl.scrollLeft, 800, 'switching back to step re-anchors at the newest bars')
// a plain re-render (poll) without a switch must NOT yank a scrolled-away view
scrollEl.scrollLeft = 200
latestEffect().effect()
assert.equal(scrollEl.scrollLeft, 200, 'plain re-renders still respect the scroll position')
// ---- message list: newest first, with timestamps when available ----
const nodeRows = byClass(tr, 'lc-node')
assert.equal(nodeRows.length, 2, 'message rows rendered')
assert.equal(nodeRows[0].args[1].key, 2, 'newest message on top')
assert.equal(nodeRows[1].args[1].key, 1, 'older message below')
const nodeTimes = byClass(tr, 'lc-node-time')
assert.equal(nodeTimes.length, 2, 'timestamps shown for every node')
// fmtTime renders LOCAL time; mirror the same formatting for expectations.
const fmtTimeLocal = (t) => {
const d = new Date(t)
const p = (x) => (x < 10 ? '0' : '') + x
return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
}
assert.equal(nodeTimes[0].args[2], fmtTimeLocal(65000), 'formatted time (65000ms)')
assert.equal(nodeTimes[1].args[2], fmtTimeLocal(1000), 'formatted time (1000ms)')
// ---- context events show where they sit in the request timeline ----
// The host sends events oldest->newest; the list reverses them. Boundary
// events (compaction/prune) show the GAP between the request before and the
// request after (same-turn "Step a→b", cross-turn "Turn a·s → Turn b·s");
// inject/model switches keep the single point of the request they belong
// to; events with no following request (in flight) stay bare. The same
// attachment drives the ✂ marker and the detail header chip.
dataValue = {
...snapshot,
events: [
{ seq: 3, kind: 'compaction', time: 1200, tokens: 900, count: 2, fromTurn: 1, fromStep: 0, turn: 2, step: 0 }, // attaches to request seq 3 (Turn 2 · Step 0)
{ seq: 6, kind: 'prune', time: 1500, tokens: 60, fromTurn: 1, fromStep: 0, turn: 1, step: 1 },
{ seq: 7, kind: 'prune', time: 2000, tokens: 100, fromTurn: 1, fromStep: 1, turn: 2, step: 0 },
{ seq: 8, kind: 'model', time: 3000, from: 'a', to: 'b', turn: 1, step: 1 },
{ seq: 9, kind: 'inject', time: 4000, tokens: 5, form: 'notice', turn: 2, step: 0 },
{ seq: 10, kind: 'compaction', time: 5000, tokens: 5000, count: 4, fromTurn: 2, fromStep: 0 }, // no request after -> unlabeled
],
}
tr = renderView()
const evRows = byClass(tr, 'lc-event')
assert.equal(evRows.length, 6, 'event rows rendered newest first')
const kindChips = byClass(tr, 'lc-kind')
assert.equal(kindChips.length, 6, 'every event row carries a kind chip (注入/压缩/剪枝/切换)')
const atLabels = byClass(tr, 'lc-event-at')
assert.equal(atLabels.length, 5, 'boundary + single-point events carry labels; in-flight stays bare')
assert.equal(atLabels[0].args[2], 'Turn 2 · Step 0', 'inject keeps the single point of the request it fed')
assert.equal(atLabels[1].args[2], 'Turn 1 · Step 1', 'model switch keeps the single point')
assert.equal(atLabels[2].args[2], 'Turn 1 · Step 1 → Turn 2 · Step 0', 'cross-turn boundary shows the gap')
assert.equal(atLabels[3].args[2], 'Turn 1 · Step 0→1', 'same-turn boundary compresses to a step range')
assert.equal(atLabels[4].args[2], 'Turn 1 · Step 0 → Turn 2 · Step 0', 'oldest boundary event shows its gap')
// the events card header carries the four kind buttons as a picker, all
// picked by default: clicking an unpicked kind adds it (A -> A+B -> ...),
// clicking a picked one removes it, clicking the last one resets to all
let kindsRow = byClass(tr, 'lc-kinds')
assert.equal(kindsRow.length, 1, 'events card shows the kind buttons inline')
const kindBtns = () => byClass(tr, 'lc-kinds')[0].args.slice(2)
assert.equal(kindBtns().length, 4, 'four kind buttons (注入/压缩/剪枝/切换)')
assert.equal(onBtns(kindBtns()).length, 4, 'all four kinds are picked by default')
assert.equal(byClass(tr, 'lc-event').length, 6, 'default shows every event')
const click = (i) => { kindBtns()[i].args[1].onClick(); tr = renderView() }
click(0) // pick-only 注入
assert.equal(byClass(tr, 'lc-event').length, 1, 'clicking 注入 among all shows only injections')
assert.ok(String(kindBtns()[0].args[1].className || '').includes('lc-gran-on'), 'picked button is highlighted')
assert.ok(String(kindBtns()[0].args[1].className || '').includes('lc-kind-inject'), 'highlight carries the kind color')
assert.equal(onBtns(kindBtns()).length, 1, 'the other three turned off')
click(1) // add 压缩
assert.equal(byClass(tr, 'lc-event').length, 3, 'adding 压缩 shows 注入 + 压缩')
click(2) // add 剪枝
assert.equal(byClass(tr, 'lc-event').length, 5, 'adding 剪枝 shows 注入 + 压缩 + 剪枝')
click(0) // remove 注入
assert.equal(byClass(tr, 'lc-event').length, 4, 'removing 注入 leaves 压缩 + 剪枝')
click(3) // add 切换
assert.equal(byClass(tr, 'lc-event').length, 5, 'adding 切换 shows 压缩 + 剪枝 + 切换')
click(1); click(2) // remove 压缩 and 剪枝
assert.equal(byClass(tr, 'lc-event').length, 1, 'only 切换 stays picked')
click(3) // remove the last one -> reset to all
assert.equal(byClass(tr, 'lc-event').length, 6, 'removing the last picked kind restores all')
assert.equal(onBtns(kindBtns()).length, 4, 'all four kinds picked again')
// the stats board picks up the event counters
const statVals2 = byClass(tr, 'lc-stat-value').map(n => n.args[2])
assert.equal(statVals2[2], '1', 'one injection counted')
assert.equal(statVals2[3], '2', 'two compactions counted')
assert.equal(statVals2[4], '2', 'two prunes counted')
assert.equal(statVals2[5], '—', 'cache-hit cell still a dash before a tokenUsage projection lands')
// the cache-hit cell reuses the OFFICIAL token-meter `tokenUsage` projection —
// the exact same data the chat stats line below the input box reads, same
// formula (cache reads over billed input = uncached + reads + writes).
usageValue = { uncachedInputTokens: 10, outputTokens: 40, cacheReadTokens: 80, cacheWriteTokens: 10 }
tr = renderView()
const statVals3 = byClass(tr, 'lc-stat-value').map(n => n.args[2])
assert.equal(statVals3.length, 7, 'seven stats cells with the cache-hit and cost cells')
assert.equal(statVals3[5], '80.00%', 'cache hit = cacheRead / (uncached + cacheRead + cacheWrite), two decimals')
assert.equal(statVals3[6], '—', 'cost cell stays a dash without cost totals')
assert.equal(statVals3[0], '3', 'turn count unchanged by the usage projection')
assert.equal(statVals3[3], '2', 'compaction count unchanged by the usage projection')
// truncation proof: 8334 / 25000 = 33.336% -> cut to 33.33%, never rounded up
usageValue = { uncachedInputTokens: 10000, outputTokens: 40, cacheReadTokens: 8334, cacheWriteTokens: 6666 }
tr = renderView()
const statVals4 = byClass(tr, 'lc-stat-value').map(n => n.args[2])
assert.equal(statVals4[5], '33.33%', 'two decimals are TRUNCATED, not rounded (33.336 -> 33.33)')
usageValue = undefined
// the cost cell prices the host-folded cumulative totals with the hardcoded
// DeepSeek V4 list prices (test host has no getLocale -> USD): 1M off-peak
// flash cache reads ($0.007) + 1M off-peak misses ($0.22) = $0.227 -> $0.23
const prevData = dataValue
dataValue = { ...prevData, cost: { flash: { off: { uncached: 1000000, cacheRead: 1000000, cacheWrite: 0, output: 0 } } } }
tr = renderView()
const statVals5 = byClass(tr, 'lc-stat-value').map(n => n.args[2])
assert.equal(statVals5[6], '$0.23', 'cost = (cacheRead x hit + uncached x miss) at the off-peak flash rate')
const costCell = byClass(tr, 'lc-stat')[6]
assert.equal(byClass(tr, 'lc-stat-q').length, 1, 'cost cell label shows the "?" affordance')
// The explanation is a styled DOM bubble (the native `title` attribute is
// invisible in the harness GUI): intro sentence + the price list built from
// cost.ts (test dict falls back to keys; no locale -> USD rates).
const costTipEls = byClass(tr, 'lc-stat-tip')
assert.equal(costTipEls.length, 1, 'cost cell carries a styled explanation bubble')
const tipText = plainText(costTipEls[0])
assert.match(tipText, /stats\.costTip/, 'bubble explains the whole-session estimate')
assert.match(tipText, /stats\.costPriceHead/, 'bubble sections the price list under its own head')
assert.match(tipText, /\$0\.014|\$0\.44/, 'bubble lists the peak flash input rate ($0.014 hit / $0.44 miss per 1M)')
assert.match(tipText, /\$0\.007|\$0\.22/, 'bubble lists the off-peak flash input rate (half price)')
dataValue = prevData
tr = renderView()
// the ✂ marker sits on the bar it attaches to and tooltips the event gap
const barMark = byClass(tr, 'lc-bar-marker')
assert.equal(barMark.length, 1, 'one ✂ marker on the attached bar')
assert.match(barMark[0].args[1].title, /Turn 1 · Step 0 → Turn 2 · Step 0/, '✂ tooltip carries the event gap')
// the detail header shows the same gap as a chip when that bar is active
ctxSlots[1][1](3) // hover the attached bar (seq 3, Turn 2 · Step 0)
tr = renderView()
const markerChip = byClass(tr, 'lc-detail-marker')
assert.equal(markerChip.length, 1, 'detail header shows the attached boundary event')
assert.equal(markerChip[0].args[2], '✂ Turn 1 · Step 0 → Turn 2 · Step 0', 'chip shows the event gap')
assert.equal(typeof markerChip[0].args[1].title, 'string', 'chip tooltips the event text')
ctxSlots[1][1](null) // leave the plot
tr = renderView()
assert.equal(byClass(tr, 'lc-detail-marker').length, 0, 'chip clears with the hover')
dataValue = snapshot
tr = renderView()
assert.equal(byClass(tr, 'lc-event').length, 0, 'event list restored to the empty state')
// ---- overview headline is the provider-based occupancy (like the chat
// ring); the composition is anchored to it, proportions stay heuristic ----
// fixture (no `contextPressure` projection -> derived fallback): last request
// prompt 83000, last total 83, current total 100, window 128000
// -> occupancy = 83000 + (100 - 83) = 83017 (65%), raw heuristic = 100.
const overviewNum = byClass(tr, 'lc-overview-num')[0]
assert.ok(overviewNum, 'overview number row present')
assert.match(textOf(overviewNum), /83\.0k/, 'headline shows the provider-based occupancy')
assert.match(textOf(overviewNum), /\/ 128\.0k tokens/, 'window shown next to the occupancy')
assert.match(textOf(overviewNum), /65%/, 'occupancy percent is the emphasized figure of the line')
assert.ok(!/~65%/.test(textOf(overviewNum)), 'no conflicting heuristic percentage next to the headline')
// ---- the OFFICIAL token-meter `contextPressure` projection wins over the
// derived fallback (the chat ring's own value, read as a second projection) ----
pressureValue = { pressureTokens: 90000, projectedTokens: 90010, contextWindow: 200000 }
tr = renderView()
const overviewNum2 = byClass(tr, 'lc-overview-num')[0]
assert.match(textOf(overviewNum2), /90\.0k/, 'official contextPressure projection is the headline when present')
assert.match(textOf(overviewNum2), /45%/, 'contextPressure window is the percent denominator')
pressureValue = undefined
dataValue = snapshot
tr = renderView()
console.log('✔ chart render test passed (context stats board, free window hover, fixed-width bars, scroll container, turn ranges, hover linking, overview tooltip, turn strip, granularity toggle, edge fades, full history, right-anchored default, message times, event range labels, detail marker chip, overview actual)')
// ---- Context browser card: step picker + category accordion + element
// content. Fixture: the snapshot's two live nodes (seq 1 user, seq 2
// assistant) plus one archived (removed) node that was still alive at the
// early steps; one header epoch with full prompt/schema content. ----
dataValue = {
...snapshot,
archive: [{ seq: 0, cat: 'user', tokens: 5, text: 'archived message', gone: 3, time: 500 }],
}
headersValue = {
headers: [{
seq: 1, time: 900, system: 'SYSTEM-PROMPT-TEXT',
// Listed in producer order (tiny BEFORE bash) — the tools section must
// re-rank them by token price, largest first. `bash` carries a real
// JSON-Schema parameter object so the parsed parameter table has rows
// to render; `tiny` ships an empty-parameter schema (table falls back
// to the "no parameters" line).
tools: [
{ name: 'tiny', tokens: 2, description: 'a tiny helper', schema: { name: 'tiny', parameters: { type: 'object' } } },
{
name: 'bash', tokens: 5, description: 'run a command',
schema: {
name: 'bash',
parameters: {
type: 'object',
properties: {
command: { type: 'string', description: 'shell command to run' },
cwd: { type: 'string', description: 'working directory' },
timeout: { type: 'number' },
flags: {
type: 'array',
items: { type: 'string' },
description: 'extra flags',
},
},
required: ['command'],
},
},
},
],
}],
}
tr = renderView()
const brKey = [...hookStates.keys()].find(k => k.includes('ContextBrowser'))
assert.ok(brKey, 'ContextBrowser fiber registered')
const brSlots = hookStates.get(brKey) // sel(0) openCat(1) openElem(2)
// Live view (default): six category rows; message counts follow the live nodes.
assert.equal(byClass(tr, 'lc-br-cat-row').length, 6, 'six category sections (system/tools + four message cats)')
assert.equal(byClass(tr, 'lc-br-pick').length, 1, 'step picker present')
const pickOptions = byClass(tr, 'lc-br-pick')[0].args.slice(2).flat()
assert.equal(pickOptions.length, 5, 'picker lists live + one option per retained step')
assert.equal(byClass(tr, 'lc-br-body').length, 0, 'all categories collapsed by default (no flat dump)')
// Open the user category -> one live element row; open the element -> content
// falls back to the preview + the window note (no useSession in this harness).
const catRowOf = (label) => byClass(tr, 'lc-br-cat-row').find(r => textOf(r).includes(label))
assert.ok(catRowOf('User'), 'user category row present')
brSlots[1][1]('user') // openCat('user')
tr = renderView()
assert.equal(byClass(tr, 'lc-br-body').length, 1, 'one category body open')
assert.equal(byClass(tr, 'lc-br-elem-row').length, 1, 'live view lists the live user node')
brSlots[2][1]('n1') // openElem(seq 1)
tr = renderView()
assert.equal(byClass(tr, 'lc-br-content').length, 1, 'element content area open')
assert.match(textOf(byClass(tr, 'lc-br-content')[0]), /first message/, 'content falls back to the node preview')
assert.match(textOf(byClass(tr, 'lc-br-content')[0]), /outside the loaded window/, 'window note follows the fallback preview')
// The message detail card carries the raw/markdown switch too (a segmented
// pill like the trend chart's Step/Turn), defaulting to MARKDOWN: the
// preview renders through MarkdownText while the window note stays.
const msgSeg = byClass(byClass(tr, 'lc-br-content')[0], 'lc-rich-seg')[0]
assert.ok(msgSeg, 'message detail card carries the raw/markdown switch')
const msgSegBtns = byClass(msgSeg, 'lc-rich-seg-btn')
assert.deepEqual(msgSegBtns.map(b => textOf(b)), ['Raw', 'Markdown'], 'one segment per view')
assert.match(msgSegBtns[1].args[1].className, /lc-rich-seg-on/, 'markdown segment is active by default')
assert.equal(msgSegBtns[0].args[1].title, 'View Raw Text', 'raw segment tooltip')
assert.equal(msgSegBtns[1].args[1].title, 'View as Markdown', 'markdown segment tooltip')
assert.equal(textOf(byClass(tr, 'lc-md-stub')[0]), 'first message', 'markdown view carries the message text')
assert.match(textOf(byClass(tr, 'lc-br-content')[0]), /outside the loaded window/, 'window note stays in markdown mode')
// The Raw segment restores the raw <pre>; picking Markdown again flips back.
msgSegBtns[0].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-md-stub').length, 0, 'raw mode drops the markdown renderer')
assert.match(textOf(byClass(tr, 'lc-br-pre')[0]), /first message/, 'raw view carries the message text')
assert.match(byClass(tr, 'lc-rich-seg-btn')[0].args[1].className, /lc-rich-seg-on/, 'raw segment lights up')
byClass(tr, 'lc-rich-seg-btn')[1].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-md-stub').length, 1, 'switching back restores the markdown view')
// Pick step seq 2 (Turn 1 · Step 1): the reconstruction includes the archived
// node (gone 3 > 2) — the accordion resets on picking, so reopen the category.
byClass(tr, 'lc-br-pick')[0].args[1].onChange({ target: { value: '2' } })
tr = renderView()
assert.equal(byClass(tr, 'lc-br-body').length, 0, 'picking a step collapses the accordion')
brSlots[1][1]('user')
tr = renderView()
assert.equal(byClass(tr, 'lc-br-elem-row').length, 2, 'a past step reconstructs archived + live nodes')
assert.match(textOf(byClass(tr, 'lc-br-body')[0]), /archived message/, 'the archived node appears in its step')
// The live view must NOT show the archived node.
byClass(tr, 'lc-br-pick')[0].args[1].onChange({ target: { value: 'live' } })
tr = renderView()
brSlots[1][1]('user')
tr = renderView()
assert.equal(byClass(tr, 'lc-br-elem-row').length, 1, 'live view excludes removed nodes')
// Header content sections: the system prompt and tool schemas ride the
// contextHeaders projection (full content, not just prices).
brSlots[1][1]('system')
tr = renderView()
brSlots[2][1]('sys')
tr = renderView()
assert.match(textOf(byClass(tr, 'lc-br-body')[0]), /SYSTEM-PROMPT-TEXT/, 'system section shows the full prompt')
// Raw/markdown view switch on the system-prompt detail card, defaulting to
// MARKDOWN; the Raw segment restores the raw <pre> and Markdown flips back.
assert.ok(byClass(tr, 'lc-rich-seg').length >= 1, 'system detail card carries the raw/markdown switch')
const sysSegBtns = () => byClass(byClass(tr, 'lc-rich-seg')[0], 'lc-rich-seg-btn')
assert.match(sysSegBtns()[1].args[1].className, /lc-rich-seg-on/, 'markdown segment is active by default')
const sysMd = byClass(tr, 'lc-md-stub')
assert.equal(sysMd.length, 1, 'markdown mode renders the prompt through MarkdownText')
assert.equal(textOf(sysMd[0]), 'SYSTEM-PROMPT-TEXT', 'markdown view carries the same source text')
assert.equal(byClass(tr, 'lc-br-body')[0] && byClass(byClass(tr, 'lc-br-body')[0], 'lc-br-pre').length, 0,
'markdown mode drops the raw <pre>')
sysSegBtns()[0].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-md-stub').length, 0, 'raw mode drops the markdown renderer')
assert.match(textOf(byClass(tr, 'lc-br-pre')[0]), /SYSTEM-PROMPT-TEXT/, 'raw view shows the prompt')
sysSegBtns()[1].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-md-stub').length, 1, 'switching back restores the markdown view')
brSlots[1][1]('tools')
tr = renderView()
assert.match(textOf(byClass(tr, 'lc-br-body')[0]), /bash/, 'tools section lists the schema rows')
const toolRowOrder = byClass(tr, 'lc-br-elem-row').map(r => textOf(r))
assert.equal(toolRowOrder.length, 2, 'both schemas listed')
assert.match(toolRowOrder[0], /bash/, 'tools ranked by tokens: largest first')
assert.match(toolRowOrder[1], /tiny/, 'tools ranked by tokens: smallest last')
brSlots[2][1]('tool:bash')
tr = renderView()
const toolContent = textOf(byClass(tr, 'lc-br-content')[0])
assert.match(toolContent, /run a command/, 'tool row expands to its description')
// The description sits inside its own titled card, with a "Description" head
// and a body carrying the prose — the same chrome the parameter table uses.
const descCards = byClass(tr, 'lc-ts-card').filter(c => {
const head = byClass(c, 'lc-ts-card-head')[0]
return head !== undefined && textOf(head).includes('Description')
})
assert.equal(descCards.length, 1, 'description is rendered inside a titled card')
// The description card head carries the same raw/markdown switch, also
// defaulting to MARKDOWN; the Raw segment restores the raw body (and back).
const descSeg = byClass(descCards[0], 'lc-rich-seg')[0]
assert.ok(descSeg, 'description card head carries the raw/markdown switch')
const descMd = byClass(descCards[0], 'lc-ts-desc-md')
assert.equal(descMd.length, 1, 'description defaults to the markdown view')
assert.equal(textOf(byClass(descMd[0], 'lc-md-stub')[0]), 'run a command', 'markdown view carries the description text')
byClass(descSeg, 'lc-rich-seg-btn')[0].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-ts-desc-md').length, 0, 'raw mode drops the markdown body')
assert.match(textOf(byClass(tr, 'lc-ts-desc-body')[0]), /run a command/, 'raw view carries the prose')
byClass(tr, 'lc-rich-seg-btn')[1].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-ts-desc-md').length, 1, 'switching back restores the markdown view')
// Parsed parameter table sits above the (still-collapsed) raw JSON: one row
// per declared property, type labels carry the JSON-Schema type, required
// ones marked with ✓, descriptions shown on a second line.
const paramRows = byClass(tr, 'lc-ts-param-row')
assert.equal(paramRows.length, 4, 'parameter table renders one row per property')
const bashRowText = paramRows.map(r => textOf(r))
assert.ok(bashRowText.some(s => /command/.test(s) && /string/.test(s) && /shell command to run/.test(s)),
'command row carries name + type + description')
assert.ok(bashRowText.some(s => /command/.test(s) && /✓/.test(s)),
'command is marked required')
assert.ok(bashRowText.some(s => /timeout/.test(s) && /number/.test(s) && !/✓/.test(s)),
'optional property shows type without the required mark')
assert.ok(bashRowText.some(s => /flags/.test(s) && /array<string>/.test(s)),
'array parameters render their element type')
// Raw JSON is collapsed by default — the toggle is visible but the schema
// string does NOT appear in the rendered text yet.
assert.equal(byClass(tr, 'lc-br-pre').filter(n => /"parameters"/.test(textOf(n))).length, 0,
'raw JSON stays collapsed behind the toggle by default')
const toggle = byClass(tr, 'lc-ts-json-toggle')[0]
assert.ok(toggle, 'JSON toggle button is rendered')
assert.match(textOf(toggle), /View Raw JSON|查看原始 JSON/, 'toggle shows the open label')
// Expanding the toggle reveals the schema; clicking again collapses it.
toggle.args[1].onClick()
tr = renderView()
assert.match(textOf(byClass(tr, 'lc-br-pre')[0]), /"parameters"/, 'expanding reveals the raw JSON')
const collapseToggle = byClass(tr, 'lc-ts-json-toggle')[0]
assert.match(textOf(collapseToggle), /Collapse|收起/, 'toggle label flips to the hide label')
collapseToggle.args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pre').length, 0, 'collapsing the toggle removes the JSON block')
// Without the contextHeaders key (older host), those sections degrade to a note.
headersValue = undefined
brSlots[1][1]('system')
brSlots[2][1](null)
tr = renderView()
assert.match(textOf(byClass(tr, 'lc-br-body')[0]), /older plugin build/, 'absent headers projection degrades gracefully')
brSlots[1][1](null)
dataValue = snapshot
tr = renderView()
// ---- Δ pills: count + token swings vs the PREVIOUS TURN's last request
// (one baseline whatever step/turn granularity; live = last request).
// The header caption states the baseline; each category row carries a count
// pill after "n items" and a token pill hugging the left of "≈X", each
// hidden while its figure held.
const deltaHint = byClass(tr, 'lc-br-hint')[0]
assert.ok(deltaHint, 'browser header carries the δ baseline caption')
assert.match(textOf(deltaHint), /previous turn/, 'caption names the previous-turn baseline')
headersValue = {
headers: [{
seq: 1, time: 900, system: 'SYSTEM-PROMPT-TEXT',
tools: [
{ name: 'tiny', tokens: 2, description: 'a tiny helper', schema: { name: 'tiny', parameters: { type: 'object' } } },
{ name: 'bash', tokens: 5, description: 'run a command', schema: { name: 'bash', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } } },
],
}],
}
dataValue = { ...snapshot, archive: [{ seq: 0, cat: 'user', tokens: 5, text: 'archived message', gone: 3, time: 500 }] }
tr = renderView()
// Pick step seq 3 (turn 2): baseline = turn 1's last request, seq 2. That
// step's assemble (seq 1 user + seq 2 assistant) vs seq 2's (archived seq 0
// + seq 1 user) reads user 1→2 (-1) and assistant 1→0 (+1); tokens ride the
// request records (user 40 vs 25, assistant 12 vs 10).
byClass(tr, 'lc-br-pick')[0].args[1].onChange({ target: { value: '3' } })
tr = renderView()
const countPills = byClass(tr, 'lc-br-delta')
assert.equal(countPills.length, 2, 'count pills: user -1, assistant +1')
assert.equal(textOf(countPills[0]), '-1', 'user lost one element vs the previous turn')
assert.equal(textOf(countPills[1]), '+1', 'assistant gained one element vs the previous turn')
assert.match(countPills[0].args[1].className, /down/, 'a shrunken category is red-tinted')
const tokenPills = byClass(tr, 'lc-br-tdelta')
assert.equal(tokenPills.length, 2, 'token pills: user +15, assistant +2')
assert.equal(textOf(tokenPills[0]), '+15', 'user tokens grew by 15 (40 vs 25)')
assert.equal(textOf(tokenPills[1]), '+2', 'assistant tokens grew by 2 (12 vs 10)')
// every category row still carries its count/tokens
assert.equal(byClass(tr, 'lc-br-tokens-grp').length, 6, 'token figure never leaves a row')
// ---- the baseline is the previous turn's last step in EITHER dimension:
// custom fixture, turn 2 has two steps and one user node joins per step, so
// viewing turn 2's last step (seq 4) reads +2 vs turn 1's last (seq 2, 1
// node) — stay identical after flipping the trend granularity to turn.
dataValue = {
ok: true, model: 'm', provider: 'p', contextWindow: 1000,
current: { system: 1, tools: 2, user: 99, inject: 0, assistant: 0, tool: 0, total: 102 },
toolList: [],
requests: [
{ seq: 1, turn: 1, step: 0, time: 1, system: 1, tools: 2, user: 10, inject: 0, assistant: 0, tool: 0, total: 13 },
{ seq: 2, turn: 1, step: 1, time: 2, system: 1, tools: 2, user: 20, inject: 0, assistant: 0, tool: 0, total: 23 },
{ seq: 3, turn: 2, step: 0, time: 3, system: 1, tools: 2, user: 30, inject: 0, assistant: 0, tool: 0, total: 33 },
{ seq: 4, turn: 2, step: 1, time: 4, system: 1, tools: 2, user: 40, inject: 0, assistant: 0, tool: 0, total: 43 },
{ seq: 5, turn: 3, step: 0, time: 5, system: 1, tools: 2, user: 50, inject: 0, assistant: 0, tool: 0, total: 53 },
],
events: [],
nodes: [
{ seq: 1, cat: 'user', tokens: 1, time: 1 },
{ seq: 2, cat: 'user', tokens: 2, time: 2 },
{ seq: 3, cat: 'user', tokens: 3, time: 3 },
{ seq: 4, cat: 'user', tokens: 4, time: 4 },
{ seq: 5, cat: 'user', tokens: 5, time: 5 },
],
droppedNodes: 0,
archive: [],
}
tr = renderView()
byClass(tr, 'lc-br-pick')[0].args[1].onChange({ target: { value: '4' } })
tr = renderView()
const readDeltas = () => ({ count: textOf(byClass(tr, 'lc-br-delta')[0]), token: textOf(byClass(tr, 'lc-br-tdelta')[0]) })
assert.deepEqual(readDeltas(), { count: '+2', token: '+20' }, 'step granularity: seq 4 vs turn 1 last (seq 2)')
ctxSlots[4][1]('turn') // context view -> turn granularity
tr = renderView()
assert.deepEqual(readDeltas(), { count: '+2', token: '+20' }, 'turn granularity keeps the SAME previous-turn baseline, not seq 3')
// Live view: baseline = last request (seq 5, turn 3) — user 5 vs 4 (+1).
byClass(tr, 'lc-br-pick')[0].args[1].onChange({ target: { value: 'live' } })
tr = renderView()
assert.deepEqual(readDeltas(), { count: '+1', token: '+49' }, 'live compares against the most recent request')
// restore state for the following tests
ctxSlots[4][1]('step')
dataValue = snapshot
headersValue = undefined
byClass(tr, 'lc-br-pick')[0].args[1].onChange({ target: { value: 'live' } })
tr = renderView()
console.log('✔ context browser test passed (picker, category accordion, per-step reconstruction, archived nodes, header content, graceful degradation)')
// ---- trend-chart hover linkage: the bar under the pointer transiently
// previews its step in the browser (picker value + meta follow); leaving
// the chart returns to the picker's own selection. Driven through
// ContextView's hoveredSeq state, exactly like TrendChart's onHover. ----
ctxSlots[1][1](2) // hover the seq-2 bar (Turn 1 · Step 1)
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, '2', 'hovered bar drives the browser picker')
assert.match(textOf(byClass(tr, 'lc-br-meta')[0]), /Turn 1 · Step 1/, 'meta shows the hovered step')
assert.match(textOf(byClass(tr, 'lc-br-meta')[0]), /preview/, 'hover preview is marked')
ctxSlots[1][1](3)
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, '3', 'hover moves across bars')
ctxSlots[1][1](9999)
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, 'live', 'an unknown (trimmed) preview seq is ignored')
ctxSlots[1][1](null)
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, 'live', 'leaving the chart returns to the picker selection')
console.log('✔ hover linkage test passed (bar hover previews its step, unknown seq ignored, picker resumes)')
// ---- trend-chart pin linkage: clicking a bar locks its step in the browser
// too (the picker follows the pin); unpinning (selectedSeq back to null)
// returns the browser to the live step. Driven through ContextView's
// selectedSeq state + the browser's pin-linkage effect (hook slot 8). ----
ctxSlots[0][1](2) // pin the seq-2 bar (Turn 1 · Step 1)
tr = renderView()
brSlots[8].effect() // the pin effect applies the new pinSeq
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, '2', 'pinned bar drives the browser picker')
assert.match(textOf(byClass(tr, 'lc-br-meta')[0]), /Turn 1 · Step 1/, 'meta shows the pinned step')
ctxSlots[1][1](3) // hovering another bar still previews transiently over the pin
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, '3', 'hover previews over the pin')
ctxSlots[1][1](null)
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, '2', 'leaving the chart returns to the pinned step')
ctxSlots[0][1](null) // unpin
tr = renderView()
brSlots[8].effect()
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, 'live', 'unpinning returns the browser to live')
console.log('✔ pin linkage test passed (pinned bar drives the browser picker, hover previews over the pin, unpin returns to live)')
// ---- current-composition hover link: while the browser shows the LIVE step,
// hover is shared bidirectionally with the Current Composition card. A
// browser category row or the browser's own composition bar lights the
// overview's segment + legend chip (and the browser echoes back); hovering
// the overview lights the browser's matching category row and bar segment.
// A pinned/previewed step has a different composition, so its hover never
// leaks into the overview and the overview's hover never highlights it. ----
const brWrap = () => byClass(tr, 'lc-br-bar')[0]
const brStack = () => byClass(brWrap(), 'lc-stacked')[0]
const ovrStack = () => byClass(tr, 'lc-stacked').find(s => s.args[1].style.height === '16px')
const segOf = (stack, key) => stack.args.slice(2).flat().filter(s => s !== null)
.find(s => s.args[1].key === key)
const barSegsOn = (stack) => stack.args.slice(2).flat().filter(s => s !== null)
.filter(s => String(s.args[1].className || '').includes('lc-stacked-seg-on'))
assert.ok(brStack(), 'browser composition bar present (height 10 wrapper hook)')
ctxSlots[1][1](null) // no trend hover: the browser is on the live step
brSlots[1][1](null)
brSlots[2][1](null)
tr = renderView()
assert.equal(byClass(tr, 'lc-br-cat-on').length, 0, 'no linked category row before any hover')
// browser category row -> overview (both bars, the legend, and the row echo)
const liveUserRow = catRowOf('User')
assert.equal(typeof liveUserRow.args[1].onMouseEnter, 'function', 'live browser rows carry the hover link')
liveUserRow.args[1].onMouseEnter()
tr = renderView()
assert.equal(ctxSlots[5][0], 'user', 'browser row hover updates the shared hover category')
assert.equal(byClass(tr, 'lc-br-cat-on').length, 1, 'browser row hover links its own row')
assert.equal(barSegsOn(brStack()).length, 1, 'browser row hover lights the browser bar segment')
assert.equal(barSegsOn(brStack())[0].args[1].key, 'user', 'the lit browser segment is user')
assert.equal(barSegsOn(ovrStack()).length, 1, 'browser row hover lights the overview bar segment')
assert.equal(byClass(tr, 'lc-chip-on').length, 1, 'browser row hover lights the overview legend chip')
assert.match(textOf(byClass(tr, 'lc-chip-on')[0]), /User/, 'the lit chip is the user category')
liveUserRow.args[1].onMouseLeave()
tr = renderView()
assert.equal(ctxSlots[5][0], null, 'leaving the browser row clears the shared hover')
assert.equal(byClass(tr, 'lc-br-cat-on').length, 0, 'the row echo clears with the leave')
assert.equal(byClass(tr, 'lc-stacked-seg-on').length, 0, 'both bars clear with the row leave')
assert.equal(byClass(tr, 'lc-chip-on').length, 0, 'the legend chip clears with the row leave')
// overview -> browser: the overview segment lights the browser's category
// row and bar segment, but only the overview floats its tooltip (the
// mirrored browser bar stays silent).
segOf(ovrStack(), 'assistant').args[1].onMouseEnter({ clientX: 80 })
tr = renderView()
assert.equal(byClass(tr, 'lc-br-cat-on').length, 1, 'overview hover lights the browser category row')
assert.match(textOf(byClass(tr, 'lc-br-cat-on')[0]), /assistant/, 'the echoed row is the assistant category')
assert.equal(barSegsOn(brStack()).length, 1, 'the browser bar mirrors the overview hover')
assert.equal(barSegsOn(brStack())[0].args[1].key, 'assistant', 'the mirrored segment is the assistant')
assert.equal(byClass(tr, 'lc-bar-tip-on').length, 1, 'exactly one tooltip floats (only over the overview bar)')
ovrStack().args[1].onMouseLeave()
tr = renderView()
assert.equal(byClass(tr, 'lc-br-cat-on').length, 0, 'overview leave clears the browser row echo')
assert.equal(byClass(tr, 'lc-stacked-seg-on').length, 0, 'overview leave clears both bars')
// browser composition bar -> overview (the browser's bart joins the link)
segOf(brStack(), 'user').args[1].onMouseEnter()
tr = renderView()
assert.equal(ctxSlots[5][0], 'user', 'browser bar hover updates the shared hover category')
assert.equal(barSegsOn(ovrStack()).length, 1, 'browser bar hover lights the overview segment')
assert.equal(byClass(tr, 'lc-chip-on').length, 1, 'browser bar hover lights the overview legend chip')
assert.equal(byClass(tr, 'lc-br-cat-on').length, 1, 'browser bar hover links the browser category row too')
brStack().args[1].onMouseLeave()
tr = renderView()
assert.equal(ctxSlots[5][0], null, 'leaving the browser bar clears the shared hover')
// pinned/previewed step: the compositions differ, so no linkage either way
ctxSlots[1][1](2) // trend hover previews step 2 in the browser
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, '2', 'browser is previewing a past step')
assert.equal(catRowOf('User').args[1].onMouseEnter, undefined, 'pinned browser rows carry no hover link')
segOf(brStack(), 'user').args[1].onMouseEnter() // handler exists; the live gate must make it a no-op
tr = renderView()
assert.equal(ctxSlots[5][0], null, 'pinned browser bar hover does not leak into the overview')
segOf(ovrStack(), 'assistant').args[1].onMouseEnter({ clientX: 80 })
tr = renderView()
assert.equal(byClass(tr, 'lc-br-cat-on').length, 0, 'overview hover does not echo into a pinned step')
assert.equal(barSegsOn(brStack()).length, 0, 'overview hover does not light the pinned composition bar')
assert.equal(barSegsOn(ovrStack()).length, 1, 'the overview itself still highlights normally')
ovrStack().args[1].onMouseLeave()
tr = renderView()
// back on the live step the link returns
ctxSlots[1][1](null)
tr = renderView()
assert.equal(typeof catRowOf('User').args[1].onMouseEnter, 'function', 'live browser rows carry the link again')
console.log('✔ current-composition hover link test passed (browser rows + composition bar <-> overview, gated on the live step)')
// ---- overview tool-chip bridge: the "工具定义 Top" label and each tool chip
// are clickable buttons that link into the Context browser — the label opens
// the tools category, a chip also expands that specific tool's row. The
// request is one-shot: applied by the browser's toolFocus effect (hook slot
// 9) and cleared back so the same chip can be clicked again. ----
dataValue = {
...snapshot,
toolList: [
{ name: 'bash', tokens: 5 },
{ name: 'rg', tokens: 3 },
],
}
headersValue = {
headers: [{
seq: 1, time: 900,
tools: [{ name: 'bash', tokens: 5, description: 'run a command', schema: { name: 'bash', parameters: { type: 'object' } } }],
}],
}
brSlots[2][1](null) // no element open from the previous test
tr = renderView()
const toolsLabel = byClass(tr, 'lc-tools-label')[0]
assert.ok(toolsLabel, '"工具定义 Top" label rendered as a button')
assert.equal(typeof toolsLabel.args[1].onClick, 'function', 'tools label is clickable')
let chips = byClass(tr, 'lc-tool-chip')
assert.equal(chips.length, 2, 'tool chips rendered (two tools)')
assert.ok(chips.every(c => typeof c.args[1].onClick === 'function'), 'every tool chip is clickable')
// Clicking the label opens the tools category only (no specific tool).
toolsLabel.args[1].onClick()
tr = renderView()
assert.deepEqual(ctxSlots[7][0], {}, 'label click records a category-only focus')
brSlots[9].effect() // the tool-bridge effect applies the one-shot request
tr = renderView()
assert.equal(ctxSlots[7][0], null, 'one-shot focus is cleared once applied')
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, 'live', 'focus switches the browser to the live step')
assert.equal(byClass(tr, 'lc-br-body').length, 1, 'tools category opens in the browser')
assert.match(textOf(byClass(tr, 'lc-br-body')[0]), /bash/, 'tools category lists the schema rows')
assert.equal(byClass(tr, 'lc-br-content').length, 0, 'category-only focus expands no specific tool')
// Clicking a specific tool also expands that tool's row.
chips = byClass(tr, 'lc-tool-chip')[0].args[1].onClick()
tr = renderView()
assert.deepEqual(ctxSlots[7][0], { tool: 'bash' }, 'chip click records a specific-tool focus')
brSlots[9].effect()
tr = renderView()
assert.equal(ctxSlots[7][0], null, 'tool focus cleared once applied')
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, 'live', 'stays on the live step')
const bridgeContent = byClass(tr, 'lc-br-content')
assert.equal(bridgeContent.length, 1, 'the clicked tool row expands open')
assert.match(textOf(bridgeContent[0]), /run a command/, 'the expanded body carries the clicked tool description')
// The "等 N 个" overflow link (more than five tools) also opens the category.
dataValue = {
...snapshot,
toolList: ['a', 'b', 'c', 'd', 'e', 'f'].map((name, i) => ({ name, tokens: 10 - i })),
}
brSlots[1][1](null) // retract the previously opened category
brSlots[2][1](null)
tr = renderView()
const moreBtn = byClass(tr, 'lc-tools-more')[0]
assert.ok(moreBtn, '"等 N 个" overflow link rendered as a button')
assert.equal(byClass(tr, 'lc-tool-chip').length, 5, 'still shows only the top five chips')
assert.equal(typeof moreBtn.args[1].onClick, 'function', 'the overflow link is clickable')
moreBtn.args[1].onClick()
tr = renderView()
assert.deepEqual(ctxSlots[7][0], {}, 'overflow link opens the tools category (no specific tool)')
brSlots[9].effect()
tr = renderView()
assert.equal(byClass(tr, 'lc-br-pick')[0].args[1].value, 'live', 'stays on the live step')
assert.equal(byClass(tr, 'lc-br-body').length, 1, 'tools category opens in the browser')
headersValue = undefined
dataValue = snapshot
tr = renderView()
console.log('✔ overview tool-chip bridge test passed (label opens the tools category, chip expands the tool row, overflow link opens the category too, one-shot focus clears)')
// ---- Context browser auto-load: expanding an element whose seq is outside
// the loaded conversation window pages older history in (via the plugin's
// own `sessions.provide` contribution) until the join hits. ----
let sessionSnap = { nodes: [], hasMore: true, loadingOlder: false }
let loadCalls = 0
useSessionHolder = (sel) => sel(sessionSnap)
loadOlderHolder = async () => {
loadCalls += 1
// The page lands: seq 1's full content enters the window, history ends.
sessionSnap = {
nodes: [{ kind: 'user', seq: 1, content: [{ type: 'text', text: 'FULL-MESSAGE-TEXT' }] }],
hasMore: false,
loadingOlder: false,
}
}
dataValue = snapshot
brSlots[1][1]('user') // reopen the user category
brSlots[2][1]('n1') // expand the out-of-window element
tr = renderView()
assert.match(textOf(byClass(tr, 'lc-br-content')[0]), /loading older history/, 'loading note while the page is being pulled')
// ContextBrowser hook slots: 0 sel, 1 openCat, 2 openElem, 3 exhausted,
// 4 pagesRef, 5 reset effect, 6 auto-load effect, 7 history-end effect,
// 8 pin-linkage effect.
// React runs effects in order post-render; drive them the same way.
brSlots[5].effect()
brSlots[6].effect()
assert.equal(loadCalls, 1, 'expanding pulls one older page')
tr = renderView()
const joined = textOf(byClass(tr, 'lc-br-content')[0])
assert.match(joined, /FULL-MESSAGE-TEXT/, 'joined content replaces the preview once the page lands')
assert.ok(!/outside the loaded window/.test(joined), 'window note gone after the join hits')
// No further pages once the seq joined.
brSlots[6].effect()
assert.equal(loadCalls, 1, 'no extra pages after the join hits')
useSessionHolder = undefined
loadOlderHolder = undefined
console.log('✔ context browser auto-load test passed (loading note, one page pulled, joined content, no over-paging)')
// ---- Image attachment cards: a user message carrying durable image refs
// renders the card layout — prose in the text card, images as thumbnail
// cards (name + dims + stored size), unrecognized blocks in a raw JSON
// card. Thumbnails load through the conversation service's resolveImage;
// without that service the cards degrade to metadata-only. ----
useSessionHolder = (sel) => sel({
nodes: [{
kind: 'user', seq: 1, content: [
{ type: 'text', text: 'WHAT-IS-THIS' },
{ type: 'image', attachment: { attachmentId: 'a1', mediaType: 'image/png', bytes: 153600, width: 2048, height: 1365, name: 'screenshot.png', originalDimensions: { width: 6000, height: 4000 } } },
{ type: 'image', attachment: { attachmentId: 'a2', mediaType: 'image/jpeg', bytes: 512000, width: 800, height: 600 } },
{ type: 'file', uri: 'file://x' },
],
}],
})
dataValue = snapshot
brSlots[1][1]('user') // openCat('user')
brSlots[2][1]('n1') // expand the user element
let trImg = renderView()
// Metadata-only degradation (no conversation service armed): two attachment
// cards (images + other), two image items, placeholder tiles, no <img>.
assert.equal(byClass(trImg, 'lc-ts-card').length, 2, 'images card + other-content card rendered')
assert.equal(byClass(trImg, 'lc-att-item').length, 2, 'one item per image ref')
assert.equal(byClass(trImg, 'lc-att-thumb').length, 2, 'one tile per image')
assert.equal(byClass(trImg, 'lc-att-ph').length, 2, 'placeholder tiles without the loader')
const imgCardText = textOf(byClass(trImg, 'lc-ts-card')[0])
assert.match(imgCardText, /screenshot\.png/, 'named image shows its display name')
assert.match(imgCardText, /2048×1365/, 'normalized dims shown')
assert.match(imgCardText, /original 6000×4000/, 'pre-normalization dims shown when present')
assert.match(imgCardText, /153\.6 kB/, 'stored byte size shown')
assert.match(imgCardText, /Image/, 'unnamed image falls back to the generic label')
assert.match(imgCardText, /800×600/, 'second image dims shown')
assert.match(textOf(byClass(trImg, 'lc-ts-card')[1]), /file:\/\/x/, 'unrecognized blocks keep the raw JSON card')
assert.match(textOf(byClass(trImg, 'lc-br-content')[0]), /WHAT-IS-THIS/, 'prose stays in the text card')
assert.equal(byClass(trImg, 'lc-att-thumb')[0].args.slice(2).filter(c => c !== null && typeof c === 'object' && c.args[0] === 'img').length, 0, 'no thumbnail img without the loader')
// Arm the conversation service: the loader resolves a display URL per ref.
conversationHolder = { resolveImage: async (sid, att) => 'blob:' + att.attachmentId }
trImg = renderView()
// Drive every ImageCard's load effect (slot 3 = its useEffect), then flush.
for (const [key, slots] of hookStates) {
if (key.includes('ImageCard') && slots[3] !== undefined) slots[3].effect()
}
await new Promise(r => setTimeout(r, 0))
await new Promise(r => setTimeout(r, 0))
trImg = renderView()
const thumbImgs = byClass(trImg, 'lc-att-thumb')
.flatMap(t => t.args.slice(2))
.filter(c => c !== null && typeof c === 'object' && c.args[0] === 'img')
assert.deepEqual(thumbImgs.map(i => i.args[1].src), ['blob:a1', 'blob:a2'], 'thumbnails resolve through resolveImage')
assert.equal(thumbImgs[0].args[1].alt, 'screenshot.png', 'thumbnail alt carries the display name')
conversationHolder = undefined
useSessionHolder = undefined
brSlots[1][1](null)
brSlots[2][1](null)
trImg = renderView()
console.log('✔ image attachment card test passed (card layout, metadata degradation, resolveImage thumbnails)')
// ---- /context modal render: centered dialog with the current-composition
// overview + the shared Context browser (which replaced the old last-10-turn
// trend), driven by the modal store hook ----
assert.ok(modalComponent !== null, 'overlay component captured')
const modalData = {
...snapshot,
current: { system: 10, tools: 20, user: 140, inject: 0, assistant: 15, tool: 20, total: 205 },
// No provider usage on the last request -> the headline falls back to the
// heuristic total (205), like the old trend-phase fixture.
requests: snapshot.requests.map(({ prompt, ...r }) => r),
events: [],
}
let modalOpen = false
const renderModal = () => evaluate(modalComponent({
sessionId: 's1',
useProjection: (key) => (key === 'contextTimeline' ? modalData : undefined),
useContextModal: (sel) => sel(modalOpen),
}))
assert.equal(renderModal(), null, 'modal renders nothing while closed')
modalOpen = true
const modalTree = renderModal()
assert.equal(byClass(modalTree, 'lc-modal-backdrop').length, 1, 'centered backdrop rendered')
const modalOverview = byClass(modalTree, 'lc-overview-num')[0]
assert.match(textOf(modalOverview), /205/, 'modal headline falls back to the heuristic total (no prompt on last request)')
assert.match(textOf(modalOverview), /0%/, 'percent against the 128k window')
// The trend chart is gone; the shared Context browser hosts the modal's step
// browsing instead (own picker + category accordion, opens on the live step).
assert.equal(byClass(modalTree, 'lc-modal-trend').length, 0, 'the last-10-turn trend section is gone')
assert.equal(byClass(modalTree, 'lc-br-cat-row').length, 6, 'context browser renders inside the modal')
assert.equal(byClass(modalTree, 'lc-br-pick').length, 1, 'browser step picker rides the modal')
assert.equal(byClass(modalTree, 'lc-br-pick')[0].args[1].value, 'live', 'browser opens on the live step')
const closeBtn = byClass(modalTree, 'lc-modal-close')[0]
assert.equal(closeBtn.args[1]['aria-label'], 'Close', 'close button localized')
// Deferred token consumption: the enter path left `/context` in the draft
// and recorded a bare-token guard; closing the modal dispatches the scoped
// consume-token event through the session scope.
assert.ok(modalSource !== null, 'trigger source registered on this instance')
await modalSource.matchEnter({ sessionId: 's1' }, '/context', new AbortController().signal)
const backdrop = byClass(modalTree, 'lc-modal-backdrop')[0]
backdrop.args[1].onClick()
assert.equal(bailCalls.length, 1, 'closing the modal dispatches consume-token')
assert.equal(bailCalls[0][1], 'slash/input-consume-token')
assert.deepEqual(bailCalls[0][2], { guard: { kind: 'bare-token', token: '/context' } }, 'enter-path guard consumed on close')
// A second close without a prior open records nothing.
backdrop.args[1].onClick()
assert.equal(bailCalls.length, 1, 'no pending guard -> no dispatch')
modalOpen = false
assert.equal(renderModal(), null, 'modal closes again')
console.log('✔ modal render test passed (open/close, current overview, embedded context browser, localized chrome, deferred token consume on close)')
// ---- backend-parse-failure resilience: a corrupt/foreign `contextTimeline`
// value must never white-screen the tab. The projection is sanitized into a
// render-safe shape — the tab shows the WHOLE UI with every usable piece —
// and any residual render error is caught by the error boundary (a styled
// error card, not an unmounted conversation view). ----
// A non-record value (capability absent, nothing delivered yet): loading.
dataValue = 'not-even-an-object'
tr = renderView()
assert.equal(textOf(byClass(tr, 'lc-empty')[0]), '…', 'a non-record value keeps the loading screen')
// A garbage payload: wrong-typed scalars drop, non-list collections degrade
// to [], non-object entries are dropped, and the breakdown zeroes out.
dataValue = {
ok: false,
model: 42,
current: null,
requests: ['junk', { seq: 1, turn: 1, time: 10, system: 1, tools: 2, user: 3, inject: 0, assistant: 4, tool: 0, total: 10 }],
events: [null, { kind: 'inject', seq: 1, time: 5 }],
nodes: [null, { seq: 1, cat: 'user', tokens: 10, text: 'talk', time: NaN }],
archive: { bogus: true },
toolList: [null, { name: 'bash', tokens: 5 }],
droppedNodes: -3,
cost: ['not-a-record'],
surfaceFloor: 'x',
}
tr = renderView() // must not throw
assert.equal(byClass(tr, 'lc-bar').length, 1, 'the usable request entry still renders a trend bar')
assert.equal(byClass(tr, 'lc-stat-value').length, 7, 'stats board still renders')
const statValsM = byClass(tr, 'lc-stat-value').map(n => n.args[2])
assert.equal(statValsM[0], '1', 'turns count survives (non-array requests degraded, entries filtered)')
assert.equal(statValsM[1], '1', 'steps count survives')
assert.equal(statValsM[5], '—', 'cache hit degrades to a dash')
assert.equal(statValsM[6], '—', 'cost degrades to a dash (non-record cost dropped)')
assert.equal(byClass(tr, 'lc-event').length, 1, 'the usable event entry still shows')
assert.equal(byClass(tr, 'lc-node').length, 1, 'the usable node entry still shows')
assert.equal(textOf(byClass(tr, 'lc-node-time')[0]), '—', 'an invalid timestamp shows a dash instead of throwing')
assert.equal(byClass(tr, 'lc-tool-chip').length, 1, 'the usable tool entry still shows')
assert.equal(byClass(tr, 'lc-br-cat-row').length, 6, 'the context browser still renders all six sections')
// The /context modal degrades identically: sanitized empty data renders the
// dialog shell with full sections instead of throwing.
const brokenModalTree = evaluate(modalComponent({
sessionId: 's1',
useProjection: (key) => (key === 'contextTimeline'
? { current: 'x', requests: 5, nodes: null, toolList: 7, events: {}, archive: 'z' }
: undefined),
useContextModal: (sel) => sel(true),
}))
assert.equal(byClass(brokenModalTree, 'lc-modal-card').length, 1, 'the modal shell stays intact')
assert.equal(byClass(brokenModalTree, 'lc-br-cat-row').length, 6, 'the modal degrades to full sections with sanitized data')
// A projection reader that THROWS (a broker/framework failure) lands in the
// error boundary's styled card — not a white screen — with the boundary
// protocol recording the offending error.
const boomTree = evaluate(viewComponent({
sessionId: 's1',
useProjection: () => { throw new Error('boom-parse') },
}))
assert.equal(byClass(boomTree, 'lc-error').length, 1, 'a throwing reader degrades to the error card')
assert.match(textOf(byClass(boomTree, 'lc-error')[0]), /ERR:.*boom-parse/, 'the card carries the offending message')
assert.equal(byClass(boomTree, 'lc-error-retry').length, 1, 'the card offers Retry')
const boundaryKey = [...classInstances.keys()].find(k => k.includes('ErrorBoundary'))
assert.ok(boundaryKey, 'boundary instance registered')
assert.match(classInstances.get(boundaryKey).state.error.message, /boom-parse/, 'the boundary protocol recorded the error')
// restore state for any later tests
dataValue = snapshot
tr = renderView()
assert.equal(byClass(tr, 'lc-bar').length, 4, 'snapshot restored after the resilience tests')
console.log('✔ backend-parse-failure resilience test passed (sanitized render of corrupt payloads, degraded modal, error boundary card)')