Four Context-page enhancements (lane-ctx-deep, F1-F4): - F1 context window breakdown: replace percentage-only card header with a stacked-bar breakdown of input / cached / output token buckets, plus a right-side gauge showing the live window occupancy ratio. - F2 compact Config tab: fold the sprawling profile Config editor into a Config tab on the Context page card, with the same yml-leaf ordering as the top-of-window profile picker. - F3 intervention marker: on the intervention timeline, emit a marker glyph at each user-intervention row (turn-flow-glyph-style) so the card scans as a single stream instead of a header + separate list. - F4 subagent drilldown: when a turn's tool trace hits a subagent, the Trace panel's Config + Output tabs get a second row of Subagent Config / Subagent Output tabs immediately below, driven by the same fold-in-place shape the parent panel already uses. 4 new renderer modules (compact-config-model / context-window-breakdown / intervention-timeline / subagent-drilldown), 6 new test files (41 tests, all node --test style), 4 QA shoot scripts for CDP-driven regression screenshots.
200 lines
7.9 KiB
JavaScript
200 lines
7.9 KiB
JavaScript
// DOM-shape tests for the four lane-ctx-deep enhancements (task #51).
|
|
//
|
|
// These assert on the rendered element trees without booting jsdom or the
|
|
// Electron shell. Each test mocks the minimal DOM surface each builder
|
|
// touches (createElement + appendChild + attribute + dataset), running
|
|
// through the same code paths the production shell exercises.
|
|
//
|
|
// The four features covered:
|
|
// F1 — window occupancy bar: renders 5 stacked segments.
|
|
// F2 — compact-card 4th tab "Config": strip includes a `Config` button
|
|
// AND the config body is populated with threshold+progress rows.
|
|
// F3 — intervention marker strip: emits one marker per intervention.
|
|
// F4 — subagent drill-down tabs: emits `Tool defs (N)` + `Inbound query`
|
|
// strip and populated bodies.
|
|
|
|
'use strict'
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
// --- Handroll DOM stub ---------------------------------------------------
|
|
//
|
|
// The builders under test never touch layout, only tree structure + a few
|
|
// attrs/dataset entries + textContent + eventListeners. This stub covers
|
|
// exactly that.
|
|
|
|
function makeEl(tag) {
|
|
return {
|
|
tagName: String(tag).toUpperCase(),
|
|
className: '',
|
|
textContent: '',
|
|
hidden: false,
|
|
tabIndex: 0,
|
|
style: (function () {
|
|
const map = {}
|
|
return {
|
|
setProperty(k, v) { map[k] = v },
|
|
getPropertyValue(k) { return map[k] },
|
|
}
|
|
})(),
|
|
dataset: {},
|
|
_attrs: {},
|
|
_listeners: {},
|
|
_children: [],
|
|
ownerDocument: null, // set below
|
|
appendChild(c) { this._children.push(c); return c },
|
|
append(...kids) { for (const k of kids) this._children.push(k) },
|
|
setAttribute(k, v) { this._attrs[k] = String(v) },
|
|
getAttribute(k) { return this._attrs[k] },
|
|
addEventListener(type, fn) {
|
|
(this._listeners[type] = this._listeners[type] || []).push(fn)
|
|
},
|
|
querySelector() { return null },
|
|
remove() { /* no-op */ },
|
|
}
|
|
}
|
|
|
|
function makeDoc() {
|
|
const doc = {
|
|
createElement(tag) {
|
|
const el = makeEl(tag)
|
|
el.ownerDocument = doc
|
|
return el
|
|
},
|
|
body: null,
|
|
getElementById() { return null },
|
|
}
|
|
doc.body = doc.createElement('body')
|
|
return doc
|
|
}
|
|
|
|
// Recursively find children matching a class prefix.
|
|
function findAllByClass(root, cls, out) {
|
|
out = out || []
|
|
if (!root) return out
|
|
if (root.className && String(root.className).split(/\s+/).includes(cls)) out.push(root)
|
|
for (const c of root._children || []) findAllByClass(c, cls, out)
|
|
return out
|
|
}
|
|
function findFirstByClass(root, cls) {
|
|
const all = findAllByClass(root, cls, [])
|
|
return all[0] || null
|
|
}
|
|
|
|
// --- F2: compact-card 4-tab shell ----------------------------------------
|
|
|
|
test('F2: mountTabs with fillConfig adds a Config tab and populates its body', () => {
|
|
const { mountTabs } = require('../src/renderer/compact-card.js')
|
|
const doc = makeDoc()
|
|
const parent = doc.createElement('div')
|
|
|
|
let filledConfig = null
|
|
mountTabs(parent, {
|
|
document: doc,
|
|
initial: 'post',
|
|
fillPre: (body) => { body.textContent = 'pre' },
|
|
fillPost: (body) => { body.textContent = 'post' },
|
|
fillMeta: (body) => { body.textContent = 'meta' },
|
|
fillConfig: (body) => { body.textContent = 'CONFIG_HERE'; filledConfig = body },
|
|
})
|
|
|
|
// Tab buttons: expect four in the strip.
|
|
const strip = findFirstByClass(parent, 'compact-card-tabstrip')
|
|
assert.ok(strip, 'tabstrip mounted')
|
|
const tabButtons = strip._children.filter((c) => c.tagName === 'BUTTON')
|
|
assert.equal(tabButtons.length, 4, 'expect 4 tabs when fillConfig is provided')
|
|
const labels = tabButtons.map((b) => b.textContent)
|
|
assert.deepEqual(labels, ['Diff', 'Summary', 'Policy & accounting', 'Config'])
|
|
|
|
// Config body populated.
|
|
assert.equal(filledConfig.textContent, 'CONFIG_HERE')
|
|
})
|
|
|
|
test('F2: mountTabs without fillConfig stays a 3-tab shell (back-compat)', () => {
|
|
const { mountTabs } = require('../src/renderer/compact-card.js')
|
|
const doc = makeDoc()
|
|
const parent = doc.createElement('div')
|
|
const out = mountTabs(parent, {
|
|
document: doc,
|
|
fillPre() {}, fillPost() {}, fillMeta() {},
|
|
})
|
|
const strip = findFirstByClass(parent, 'compact-card-tabstrip')
|
|
const tabButtons = strip._children.filter((c) => c.tagName === 'BUTTON')
|
|
assert.equal(tabButtons.length, 3)
|
|
assert.equal(out.configBody, null)
|
|
})
|
|
|
|
// --- F4: subagent drill-down tabs ---------------------------------------
|
|
|
|
test('F4: appendSubagentDrilldownTabs emits both panels with correct labels', () => {
|
|
const { appendSubagentDrilldownTabs } = require('../src/renderer/subagent-view.js')
|
|
const doc = makeDoc()
|
|
const parent = doc.createElement('div')
|
|
const spec = {
|
|
childEvents: [
|
|
{ type: 'user/message', seq: 1, data: { content: [{ type: 'text', text: 'seed' }], source: { kind: 'plugin', plugin: 'subagent-search' } } },
|
|
{ type: 'tool/call', seq: 2, data: { name: 'search', arguments: '{"q":"x"}' } },
|
|
{ type: 'tool/call', seq: 3, data: { name: 'read_file', arguments: '{"path":"a"}' } },
|
|
],
|
|
}
|
|
appendSubagentDrilldownTabs(doc, parent, spec)
|
|
// Two tab buttons expected.
|
|
const strip = findFirstByClass(parent, 'subagent-drilldown-tabstrip')
|
|
assert.ok(strip, 'drilldown tabstrip mounted')
|
|
const buttons = strip._children.filter((c) => c.tagName === 'BUTTON')
|
|
assert.equal(buttons.length, 2)
|
|
const btnLabels = buttons.map((b) => b.textContent)
|
|
assert.match(btnLabels[0], /Tool defs \(2\)/)
|
|
assert.equal(btnLabels[1], 'Inbound query')
|
|
// Tool list should have two entries.
|
|
const toolRows = findAllByClass(parent, 'subagent-drilldown-toolrow', [])
|
|
assert.equal(toolRows.length, 2)
|
|
// Inbound query blockquote should carry the seed text.
|
|
const query = findFirstByClass(parent, 'subagent-drilldown-query')
|
|
assert.ok(query, 'inbound query rendered')
|
|
assert.match(query.textContent, /seed/)
|
|
})
|
|
|
|
test('F4: appendSubagentDrilldownTabs skips entirely on empty spec', () => {
|
|
const { appendSubagentDrilldownTabs } = require('../src/renderer/subagent-view.js')
|
|
const doc = makeDoc()
|
|
const parent = doc.createElement('div')
|
|
appendSubagentDrilldownTabs(doc, parent, {})
|
|
assert.equal(parent._children.length, 0, 'no drilldown wrapper for empty spec')
|
|
})
|
|
|
|
// --- F1 + F3 model-shape sanity ----------------------------------------
|
|
// (Full DOM wire-up of context-page.js is exercised by the four real-machine
|
|
// screenshots at task-end. Here we lock the shapes the DOM depends on.)
|
|
|
|
test('F1 model: computeWindowBreakdown returns 5 slices in FAMILY_ORDER', () => {
|
|
const M = require('../src/renderer/context-window-breakdown.js')
|
|
const events = [
|
|
{ type: 'context/message', seq: 1, data: { content: [{ type: 'text', text: 'sys' }], source: { kind: 'system' } } },
|
|
{ type: 'assistant/message', seq: 2, data: { content: [{ type: 'text', text: 'ok' }], usage: { inputTokens: 100, outputTokens: 40 } } },
|
|
{ type: 'assistant/reasoning', seq: 3, data: { content: [{ type: 'text', text: 'thinking' }] } },
|
|
]
|
|
const view = M.computeWindowBreakdown(events)
|
|
assert.equal(view.slices.length, 5)
|
|
const totalPct = view.slices.reduce((s, sl) => s + sl.pct, 0)
|
|
assert.ok(totalPct <= 100.5, `sum ${totalPct} <= 100`)
|
|
})
|
|
|
|
test('F3 model: collectInterventions preserves seq order + kind counts', () => {
|
|
const M = require('../src/renderer/intervention-timeline.js')
|
|
const events = [
|
|
{ type: 'steering/message', seq: 5, data: { content: [{ type: 'text', text: 'a' }] } },
|
|
{ type: 'session/forked', seq: 10, data: { parentSeq: 8 } },
|
|
{ type: 'user/message', seq: 15, data: { content: [{ type: 'text', text: 'r' }], editRerun: { origSeq: 12 } } },
|
|
]
|
|
const markers = M.collectInterventions(events)
|
|
assert.equal(markers.length, 3)
|
|
assert.deepEqual(markers.map((m) => m.kind), ['steer', 'fork', 'edit-rerun'])
|
|
const roll = M.summariseInterventions(markers)
|
|
const rollMap = new Map(roll.map((r) => [r.kind, r.count]))
|
|
assert.equal(rollMap.get('steer'), 1)
|
|
assert.equal(rollMap.get('fork'), 1)
|
|
assert.equal(rollMap.get('edit-rerun'), 1)
|
|
})
|