Files
bowenliang123 026ffca599 feat(context): fold the range off the step-start daily ledger
Context Insights now reads contextActivity for every range-scoped figure: a step's settlement, tool run, and wall time book on the local day its step/start fired, so a step crossing midnight stays on its start day. The KPI band, Token Stats, and session selection share the daily ledger; turns, tool calls, and step wall time join each day, and contextActivity's stateVersion moves to 5 so cached rows cold-refold. The Activity heatmap drops the Steps metric for daily token volume; Timing Stats stays session-lifetime and is labeled as such.

Refs #172
2026-10-20 08:42:51 +08:00

703 lines
36 KiB
TypeScript
Raw Permalink 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.
// OverviewPanel (src/client/components/overviewPanel.tsx) rendered in the layout's keyed `main` panel: mounting it IS
// opening it (the shell unmounts on panel switch), so no open flag exists.
import { act, createElement as h } from 'react'
import assert from 'node:assert/strict'
import { afterEach, beforeEach, describe, test, vi } from 'vitest'
import { makeOverviewPanel } from '../../../src/client/components/overviewPanel'
import { resetModelPrices, setModelPricesLoader } from '../../../src/client/modelPrices'
import { dayKeyOf } from '../../../src/shared/days'
import { TestClientCtx, asClientCtx } from '../helpers/harness'
import { click, flush, hover, makeKit, mount, query, queryAll, text, unhover, until, type Mounted } from '../helpers/kit'
const PROVIDERS = {
deepseek: { models: { 'deepseek-v4-flash': { cost: { input: 1, output: 2, cache_read: 0.1 } } } },
}
const NOW = Date.now()
const TODAY = dayKeyOf(NOW) ?? ''
const YESTERDAY = dayKeyOf(NOW - 86_400_000) ?? ''
/** Local midnight of NOW — the "today" range's floor, so today's session stays inside it whatever hour the spec runs. */
const MIDNIGHT = new Date(new Date(NOW).setHours(0, 0, 0, 0)).getTime()
/** A wire-shaped timeline head. */
function timeline(over: Record<string, unknown> = {}): Record<string, unknown> {
return {
ok: true,
current: { system: 100, tools: 50, user: 30, inject: 10, skill: 10, assistant: 200, tool: 100, total: 500 },
contextWindow: 1000,
requests: [],
events: [],
nodes: [],
droppedNodes: 0,
archive: [],
counts: { turns: 3, steps: 5, injects: 1, compactions: 0, prunes: 0 },
cost: { deepseek: { 'deepseek-v4-flash': { peak: { uncached: 1000, cacheRead: 500, cacheWrite: 0, output: 250 } } } },
timing: { wallMs: 90_000, ttftMs: 1_000, genMs: 30_000, calls: 4, toolsMs: 20_000, toolCalls: 7 },
...over,
}
}
/** The three-session snapshot: current + folded, older + cheap, stale + bare. */
function sessionsSnapshot(): { ids: string[]; byId: Record<string, Record<string, unknown>>; current: string; phase: string } {
return {
ids: ['a', 'b', 'c'],
byId: {
a: {
displayTitle: 'alpha session',
cwd: '/repo/alpha',
updatedAt: MIDNIGHT,
running: true,
projectionValues: {
contextTimeline: timeline(),
contextActivity: {
days: {
[TODAY]: {
tokens: 100,
requests: 2,
turns: 2,
toolCalls: 4,
toolsMs: 10_000,
wallMs: 45_000,
cost: { deepseek: { 'deepseek-v4-flash': { peak: { uncached: 1000, cacheRead: 500, cacheWrite: 0, output: 250 } } } },
},
[YESTERDAY]: { tokens: 50, requests: 1, turns: 1, toolCalls: 3, toolsMs: 10_000, wallMs: 45_000 },
},
},
},
},
b: {
displayTitle: 'beta session',
cwd: '/repo/beta',
updatedAt: NOW - 10 * 86_400_000,
projectionValues: {
contextTimeline: timeline({ current: { system: 10, tools: 0, user: 0, inject: 0, skill: 0, assistant: 0, tool: 0, total: 10 }, cost: undefined }),
contextActivity: { days: { [YESTERDAY]: { tokens: 25, requests: 1, turns: 1, toolCalls: 7, toolsMs: 20_000, wallMs: 90_000 } } },
},
},
c: {
title: 'gamma session',
updatedAt: NOW - 40 * 86_400_000,
},
},
current: 'a',
phase: 'ready',
}
}
const workspacesSnapshotValue = {
items: [{ workspaceId: 'w1', title: 'workspace-one', path: '/repo', sessionIds: ['a'] }],
archivedSessionIds: [],
state: 'idle',
phase: 'ready',
}
const useHookOf = (snapshot: unknown) => (<T>(sel: (s: unknown) => T): T => sel(snapshot))
function makeCtx(sessions: Record<string, unknown> = {}): TestClientCtx {
return new TestClientCtx({ services: { sessions } })
}
async function openPanel(
ctx: TestClientCtx,
props: Record<string, unknown> = {},
): Promise<{ m: Mounted; Panel: ReturnType<typeof makeOverviewPanel> }> {
const Panel = makeOverviewPanel(asClientCtx(ctx), makeKit())
const allProps = {
useSessions: useHookOf(sessionsSnapshot()),
useWorkspaces: useHookOf(workspacesSnapshotValue),
...props,
}
const m = await mount(h(Panel, allProps))
// The price book's first fetch resolves a microtask or two behind the mount; a second act window keeps its notify inside act.
await flush()
return { m, Panel }
}
const backfillPosts: string[] = []
/** One day cell of the page's portaled calendar picker, by its day key. */
function calendarDay(key: string): HTMLElement {
const el = queryAll(document.body, '.lc-dp-cell').find(d => d.getAttribute('aria-label') === key)
assert.ok(el !== undefined, `the calendar drew ${key}`)
return el
}
beforeEach(() => {
resetModelPrices()
setModelPricesLoader(() => Promise.resolve(PROVIDERS))
backfillPosts.length = 0
// The heading's balance capsule POSTs its own route on mount (client/balance.ts);
// only the warm-up trigger is this spec's subject.
vi.stubGlobal('fetch', async (url: string | URL) => {
const route = String(url)
if (route.endsWith('/backfill')) backfillPosts.push(route)
return { ok: true, json: async () => ({}) }
})
})
afterEach(async () => {
resetModelPrices()
vi.unstubAllGlobals()
await new Promise(resolve => setTimeout(resolve, 1))
})
describe('OverviewPanel', () => {
test('renders the KPI band, heatmap, and session cards; summons the warm-up and refreshes the list on mount', async () => {
let pulls = 0
const ctx = makeCtx({ refresh: () => { pulls++; return Promise.resolve() } })
const { m } = await openPanel(ctx)
assert.equal(pulls, 1, 'the baseline re-pull fires on mount')
assert.deepEqual(backfillPosts, ['api/dsh-context/backfill'], 'the warm-up trigger POST fires on mount')
assert.ok(text(m.container).includes('Context Insights'))
// The page chrome: the section's one scroll region carries the centered
// content column — the page heading, then the 1:1 first row (the KPI
// band beside the last-7-days usage chart), the aggregate stats pair, and the insight/session body.
const page = query(m.container, 'section.lc-ov-page')
assert.equal(page.getAttribute('aria-label'), 'Context Insights')
const scrollRows = [...query(m.container, '.lc-ov-pagecontent').children].map(el => el.className.split(' ')[0])
assert.deepEqual(scrollRows, ['lc-ov-pagehead', 'lc-ov-first', 'lc-ov-stats', 'lc-ov-body'])
assert.equal(query(m.container, '.lc-ov-pagehead h1.lc-ov-pagetitle').textContent, 'Context Insights')
assert.ok(query(m.container, '.lc-ov-pagehead').hasAttribute('data-window-drag'), 'the head is the desktop window drag area')
// The pair's halves: the KPI band's six cells.
assert.equal(queryAll(m.container, '.lc-ov-first > .lc-ov-kpis .lc-stat').length, 6)
assert.ok(query(m.container, '.lc-ov-first > .lc-card.lc-ov-usage') !== null, 'the usage chart rides the row')
// KPI band: 2 sessions in the 30d range, 1750 tokens billed, priced cost, cache hit.
const labels = queryAll(m.container, '.lc-stat-label').map(el => el.textContent)
assert.deepEqual(labels, ['Sessions', 'Tokens Used', 'Cost', 'Cache Hit', 'Tool Calls', 'Active Time'])
const values = queryAll(m.container, '.lc-stat-value').map(el => el.textContent)
assert.equal(values[0], '2')
assert.equal(values[1], '1.8k')
assert.ok(values[2].startsWith('$'), 'priced from the book')
assert.equal(values[3], '33.33%')
// Tools + active time: both timed sessions fold into the band (7 calls each).
assert.equal(values[4], '14')
assert.equal(values[5], '3m0s')
assert.ok(text(m.container).includes('tool runs 40.0s'), 'the tool calls cell qualifies with the summed run time')
assert.ok(text(m.container).includes('4 model calls'), 'the active-time cell qualifies with the settled model-call count')
// Cost and cache hit qualify with the session count each figure covers
// (only session a carries usage; b folds no cost at all).
const subs = queryAll(m.container, '.lc-stat-sub').map(el => el.textContent)
assert.deepEqual(subs.slice(2, 4), ['1 of 2 sessions priced', 'across 1 sessions'])
// The aggregate stats row: the range's Token Stats ring folded into the
// Context tab's OWN composition categories (every fixture category is
// billed > 0 here, output exact) beside the Timing Stats ring (the summed
// totals, un-split — no fixture carries a decode split).
const statsCards = queryAll(m.container, '.lc-ov-stats > .lc-card')
assert.equal(statsCards.length, 2)
assert.equal(query(statsCards[0], '.lc-donut-center b').textContent, '1.8k')
assert.deepEqual(queryAll(statsCards[0], '.lc-sl-label').map(el => el.textContent), [
'System Prompt', 'Tool Schemas', 'User Messages', 'Injected Context',
'Skill Injections', 'Assistant Messages', 'Tool Results', 'Output',
])
assert.equal(query(statsCards[1], '.lc-donut-center b').textContent, '3m0s')
assert.ok(text(statsCards[1]).includes('LLM Gen'))
assert.ok(queryAll(m.container, 'button.lc-heat-cell').length >= 2)
// Cards: a (current, running, grouped), b, and c is outside the 30d range.
const cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(cards.length, 2)
assert.ok(text(cards[0]).includes('alpha session'))
assert.ok(text(cards[0]).includes('workspace-one'), 'the breadcrumb group joined')
assert.ok(text(cards[0]).includes('alpha'), 'the project basename joined')
await m.unmount()
})
test('the coverage picker folds archived sessions in by default and its scopes split the page', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx, {
useWorkspaces: useHookOf({ ...workspacesSnapshotValue, archivedSessionIds: ['b'] }),
})
// Default 'all': the archived session folds back into every band, tagged on its card.
assert.deepEqual(queryAll(m.container, '.lc-stat-label')[0].textContent, 'Sessions')
assert.deepEqual(queryAll(m.container, '.lc-stat-sub')[0].textContent, 'incl. 1 archived')
let cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(cards.length, 2)
const betaCard = cards.find(c => text(c).includes('beta session'))
assert.ok(betaCard !== undefined && text(betaCard).includes('Archived'), 'the archived card carries the tag')
const select = query<HTMLSelectElement>(m.container, 'select.lc-ov-cover')
assert.deepEqual([...select.options].map(o => o.textContent), ['All (show archived)', 'Hide archived', 'Archived only'])
// 'active' restores the lighter list; the sub-line names what the full list holds.
await actSelect(select, 'active')
assert.deepEqual(queryAll(m.container, '.lc-stat-label')[0].textContent, 'Active Sessions')
assert.deepEqual(queryAll(m.container, '.lc-stat-sub')[0].textContent, 'of 3 total')
assert.equal(queryAll(m.container, '.lc-stat-value')[0].textContent, '1', 'the archived session leaves the KPI band')
cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(cards.length, 1)
assert.ok(text(cards[0]).includes('alpha session'))
// 'archived' flips the split: only the tagged session in range stays.
await actSelect(select, 'archived')
assert.deepEqual(queryAll(m.container, '.lc-stat-label')[0].textContent, 'Archived Sessions')
assert.deepEqual(queryAll(m.container, '.lc-stat-sub')[0].textContent, 'of 3 total')
assert.equal(queryAll(m.container, '.lc-stat-value')[0].textContent, '1')
const archCards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(archCards.length, 1)
assert.ok(text(archCards[0]).includes('beta session'))
await m.unmount()
})
test('an archived card click opens nothing (dsh refuses archived sessions)', async () => {
const opened: string[] = []
const ctx = new TestClientCtx({
services: {
uiWorkspace: { openSession: (id: string) => { opened.push(id) } },
sessions: {},
},
})
const { m } = await openPanel(ctx, {
useWorkspaces: useHookOf({ ...workspacesSnapshotValue, archivedSessionIds: ['b'] }),
})
await click(queryAll<HTMLButtonElement>(m.container, '.lc-ov-grid > .lc-ov-session').find(c => text(c).includes('beta session'))!)
assert.deepEqual(opened, [], 'the archived card never navigates')
await m.unmount()
})
test('range switching re-scopes the KPI band and the grid', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
const rangeButtons = queryAll<HTMLButtonElement>(m.container, '.lc-ov-range .lc-gran-btn')
assert.equal(rangeButtons.length, 4)
// The chip reads the LIVE window, so every preset prints its own days.
const span = () => queryAll(m.container, '.lc-dp-half').map(el => el.textContent)
const thirty = new Date(NOW - 30 * 86_400_000)
assert.deepEqual(span(), [dayKeyOf(thirty.getTime()), TODAY], '30d shows the thirty days it covers')
await click(rangeButtons[3]) // All
assert.deepEqual(span(), ['Start date', 'End date'], 'an unbounded scope has no span to print')
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 3, 'the stale session joins')
assert.equal(queryAll(m.container, '.lc-stat-value')[0].textContent, '3')
await click(rangeButtons[0]) // Today
assert.deepEqual(span(), [TODAY, TODAY], "today's own day on both ends")
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 1, "only today's session stays")
await click(rangeButtons[1]) // Last 7 days
assert.deepEqual(span(), [dayKeyOf(NOW - 7 * 86_400_000), TODAY])
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2, 'the yesterday-active session joins the prompt-fresh one')
await click(rangeButtons[2]) // back to 30d
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2)
await m.unmount()
})
test('a picked calendar range scopes the page and hands the presets back', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
const rangeButtons = queryAll<HTMLButtonElement>(m.container, '.lc-ov-range .lc-gran-btn')
await click(query(m.container, '.lc-dp-trigger'))
await click(calendarDay(YESTERDAY))
await click(calendarDay(TODAY))
assert.ok(query(m.container, '.lc-dp-trigger').className.includes('lc-dp-trigger-on'), 'the chip carries the picked range')
assert.deepEqual(
queryAll(m.container, '.lc-dp-trigger .lc-dp-half').map(el => el.textContent),
[YESTERDAY, TODAY],
'the head names both ends, start first',
)
// The window closes at both ends: the prompt-fresh session and the yesterday-active one stay.
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2)
assert.equal(queryAll(m.container, '.lc-stat-value')[0].textContent, '2', 'the KPI band narrowed with it')
assert.ok(
rangeButtons.every(b => !b.className.includes('lc-gran-on')),
'a picked range and a preset are never both lit',
)
await click(rangeButtons[3])
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 3, 'the stale session joins again')
assert.ok(!query(m.container, '.lc-dp-trigger').className.includes('lc-dp-trigger-on'))
await m.unmount()
})
test('a backwards pick still scopes the window the short way round', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
await click(query(m.container, '.lc-dp-trigger'))
// The end day first, then a start day BEFORE it: the pair swaps rather
// than leaving the page with a window that runs backwards.
await click(calendarDay(TODAY))
await click(calendarDay(YESTERDAY))
assert.deepEqual(
queryAll(m.container, '.lc-dp-trigger .lc-dp-half').map(el => el.textContent),
[YESTERDAY, TODAY],
'the picked pair is sorted, start day first',
)
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2, 'the same window a forward pick would have left')
await m.unmount()
})
test('the heatmap day pin filters the grid and the chip clears it', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
const cells = queryAll<HTMLButtonElement>(m.container, 'button.lc-heat-cell')
const todayCell = cells.find(c => c.getAttribute('aria-label')?.startsWith(TODAY))
assert.ok(todayCell !== undefined, 'today has a data cell')
await click(todayCell)
assert.ok(query(m.container, '.lc-ov-day-chip'), 'the day chip appears')
const cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(cards.length, 1)
assert.ok(text(cards[0]).includes('alpha session'), 'only the session active that day stays')
await click(query<HTMLButtonElement>(m.container, '.lc-ov-day-chip'))
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2, 'the chip clear restores')
// Pin and unpin via the cell itself.
await click(queryAll<HTMLButtonElement>(m.container, 'button.lc-heat-cell').find(c => c.getAttribute('aria-label')?.startsWith(YESTERDAY))!)
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2, 'both sessions were active yesterday')
await click(queryAll<HTMLButtonElement>(m.container, 'button.lc-heat-cell').find(c => c.getAttribute('aria-label')?.startsWith(YESTERDAY))!)
assert.equal(queryAll(m.container, '.lc-ov-day-chip').length, 0)
await m.unmount()
})
test('the skill card pins the session list to the skill’s loaders; the chip and the row release it', async () => {
const ctx = makeCtx()
// Session a loaded 'tdd' today; b's ledger carries no skills.
const snap = sessionsSnapshot()
const aDays = (snap.byId.a.projectionValues as { contextActivity: { days: Record<string, Record<string, unknown>> } }).contextActivity.days
aDays[TODAY] = { ...aDays[TODAY], skills: { tdd: { n: 2, last: NOW - 1000 } } }
const skillPosts: unknown[] = []
vi.stubGlobal('fetch', async (url: string | URL, init?: RequestInit) => {
const route = String(url)
if (route.endsWith('/skills')) {
skillPosts.push(init?.body === undefined ? undefined : JSON.parse(String(init.body)))
return {
ok: true,
json: async () => ({ ok: true, value: { skills: [{ name: 'tdd', description: 'Test-driven.', path: '/p/tdd/SKILL.md', source: 'user-agents' }] } }),
}
}
return { ok: true, json: async () => ({}) }
})
const { m } = await openPanel(ctx, { useSessions: useHookOf(snap) })
await flush() // the catalog read lands
assert.deepEqual(skillPosts, [{ cwd: '/repo/alpha', sessionId: 'a' }], 'the catalog read anchors on the current session')
const skillRow = query<HTMLButtonElement>(m.container, 'button.lc-ov-skill')
assert.ok(text(skillRow).includes('tdd'))
await hover(skillRow)
assert.ok(text(document.querySelector('#lc-skilltip-bubble') as HTMLElement).includes('Test-driven.'), 'the catalog’s description joins the hover card')
await unhover(skillRow)
await click(skillRow)
const cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(cards.length, 1)
assert.ok(text(cards[0]).includes('alpha session'))
const chip = query<HTMLButtonElement>(m.container, '.lc-ov-day-chip[title="Clear the skill filter"]')
assert.ok(text(chip).includes('Skill · tdd'))
assert.ok(text(query(m.container, '.lc-ov-skill-detail')).includes('/p/tdd/SKILL.md'))
await click(chip)
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2)
assert.equal(queryAll(m.container, '.lc-ov-skill-detail').length, 0, 'the detail block leaves with the pin')
// Re-pin and release via the row itself.
await click(query<HTMLButtonElement>(m.container, 'button.lc-ov-skill'))
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 1)
await click(query<HTMLButtonElement>(m.container, 'button.lc-ov-skill'))
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2)
await m.unmount()
})
test('the metric toggle re-prices the heatmap between tokens and sessions', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
const todayCellClass = (): string | undefined =>
queryAll<HTMLButtonElement>(m.container, 'button.lc-heat-cell')
.find(c => c.getAttribute('aria-label')?.startsWith(TODAY))?.className
assert.ok(todayCellClass()?.includes('lc-heat-4'), 'tokens mode (default): today holds the token peak')
const metricButtons = queryAll<HTMLButtonElement>(m.container, '.lc-heat-ctl .lc-gran-btn')
assert.deepEqual(metricButtons.map(b => b.textContent), ['Sessions', 'Tokens'])
assert.ok(metricButtons[1].className.includes('lc-gran-on'), 'tokens starts selected')
await click(metricButtons[0])
const after = queryAll<HTMLButtonElement>(m.container, '.lc-heat-ctl .lc-gran-btn')
assert.ok(after[0].className.includes('lc-gran-on'), 'the click selects sessions')
assert.ok(!after[1].className.includes('lc-gran-on'))
// Sessions: today had one of the window's two active sessions.
assert.ok(todayCellClass()?.includes('lc-heat-2'), 'sessions mode re-prices today down its own scale')
await m.unmount()
})
test('search and sort steer the grid', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx, {
useSessions: useHookOf({
...sessionsSnapshot(),
ids: ['a', 'b', 'c'],
byId: {
...sessionsSnapshot().byId,
c: { ...(sessionsSnapshot().byId.c as object), updatedAt: NOW - 86_400_000 },
},
}),
})
const input = query<HTMLInputElement>(m.container, 'input.lc-ov-search')
await actType(input, 'beta')
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 1)
assert.ok(text(m.container).includes('beta session'))
await actType(input, '/repo/a')
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 1)
await actType(input, 'zzz')
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 0)
assert.ok(text(m.container).includes('No sessions match'))
await actType(input, '')
// Sort: tokens puts the billed session first; context puts the largest current first.
const sortButtons = queryAll<HTMLButtonElement>(m.container, '.lc-ov-list-head .lc-gran-btn')
assert.equal(sortButtons.length, 3)
await click(sortButtons[1]) // Tokens
assert.ok(text(queryAll(m.container, '.lc-ov-grid > .lc-ov-session')[0]).includes('alpha session'))
await click(sortButtons[2]) // Context
assert.ok(text(queryAll(m.container, '.lc-ov-grid > .lc-ov-session')[0]).includes('alpha session'))
await click(sortButtons[0]) // Recent
await m.unmount()
})
test('the group chips filter the grid (All / workspace / Ungrouped), toggling on and off', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
const chips = queryAll<HTMLButtonElement>(m.container, '.lc-ov-chip')
assert.deepEqual(chips.map(c => c.textContent), ['All2', 'workspace-one1', 'Ungrouped1'])
await click(chips[1])
let cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(cards.length, 1)
assert.ok(text(cards[0]).includes('alpha session'))
assert.ok(queryAll<HTMLButtonElement>(m.container, '.lc-ov-chip')[1].className.includes('lc-ov-chip-on'))
await click(queryAll<HTMLButtonElement>(m.container, '.lc-ov-chip')[2])
cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(cards.length, 1)
assert.ok(text(cards[0]).includes('beta session'))
await click(queryAll<HTMLButtonElement>(m.container, '.lc-ov-chip')[2])
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2)
await click(queryAll<HTMLButtonElement>(m.container, '.lc-ov-chip')[1])
await click(queryAll<HTMLButtonElement>(m.container, '.lc-ov-chip')[0])
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 2)
await m.unmount()
})
test('a selection whose group fell out of scope keeps a phantom chip (one click out)', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
await click(queryAll<HTMLButtonElement>(m.container, '.lc-ov-chip')[1]) // workspace-one
// Filter everything away with a query: the selection's chip stays, lit, at 0.
const input = query<HTMLInputElement>(m.container, 'input.lc-ov-search')
await actType(input, 'zzz')
const chips = queryAll<HTMLButtonElement>(m.container, '.lc-ov-chip')
assert.deepEqual(chips.map(c => c.textContent), ['All0', 'workspace-one0'])
assert.ok(chips[1].className.includes('lc-ov-chip-on'))
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 0)
await m.unmount()
})
test('a card click opens the session through the harness verb', async () => {
const opened: string[] = []
const ctx = new TestClientCtx({ services: { uiWorkspace: { openSession: (id: string) => { opened.push(id) } } } })
const { m } = await openPanel(ctx)
await click(query<HTMLButtonElement>(m.container, '.lc-ov-grid > .lc-ov-session'))
// The verb's own navigation returns the center column to the conversation
// (selectPanel(null)); the shell unmounts the page — nothing here closes.
assert.deepEqual(opened, ['a'])
await m.unmount()
})
test('the settings chip beside the title runs the preferences jump', async () => {
// The harness chrome the jump drives (settingsJump.ts): the Plugins panel
// entry first, then the bundle card's open control (the one non-switch button inside the card), both click-tracked.
const clicks: string[] = []
const chrome = document.createElement('div')
chrome.innerHTML = `
<button aria-label="Plugins"></button>
<div data-plugin-package="dsh-context">
<button role="switch"></button>
<button>Context</button>
</div>`
for (const b of [...chrome.querySelectorAll('button')]) {
b.addEventListener('click', () => { clicks.push(b.getAttribute('aria-label') ?? (b.textContent ?? '')) })
}
document.body.appendChild(chrome)
try {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
const chip = query<HTMLButtonElement>(m.container, '.lc-ov-settings')
// The chip rides the heading, directly after the title (the long hint moved onto the hover title).
assert.equal(chip.textContent, 'Plugin Settings')
assert.equal(chip.title, 'Open plugin settings')
const head = query(m.container, '.lc-ov-pagehead')
assert.equal(head.children[0].className, 'lc-ov-pagetitle')
assert.equal(head.children[1], chip)
await click(chip)
assert.deepEqual(clicks, ['Plugins'], 'the jump clicked the Plugins panel entry synchronously')
await until(() => clicks.length > 1, 'the jump never reached the bundle card open control')
assert.equal(clicks[1], 'Context')
await m.unmount()
} finally {
chrome.remove()
}
})
test('hovering a heat cell portals its tip to <body>; leaving hides it', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
const cell = queryAll<HTMLButtonElement>(m.container, 'button.lc-heat-cell')
.find(c => c.getAttribute('aria-label')?.startsWith(TODAY))
assert.ok(cell !== undefined)
await act(async () => {
cell.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
})
const bubble = document.body.querySelector('#lc-hovertip-bubble')
assert.ok(bubble !== null, 'the portaled bubble mounted')
assert.ok(bubble.textContent.startsWith(TODAY), 'the cell label rides the bubble')
assert.equal(cell.getAttribute('aria-describedby'), 'lc-hovertip-bubble')
await act(async () => {
cell.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, relatedTarget: m.container }))
})
assert.equal(document.body.querySelector('#lc-hovertip-bubble'), null, 'leaving the anchor hides the bubble')
assert.equal(cell.getAttribute('aria-describedby'), null)
await m.unmount()
})
test('degraded states: unavailable list, empty list, no activity', async () => {
const ctx = makeCtx()
const bare = await openPanel(ctx, { useSessions: undefined, useWorkspaces: undefined })
assert.ok(text(bare.m.container).includes('The session list is unavailable'))
await bare.m.unmount()
const empty = await openPanel(ctx, {
useSessions: useHookOf({ ids: [], byId: {}, phase: 'ready' }),
useWorkspaces: useHookOf({ items: [] }),
})
assert.ok(text(empty.m.container).includes('No sessions yet'))
assert.ok(text(empty.m.container).includes('No activity yet'))
await empty.m.unmount()
const noTimeline = await openPanel(ctx, {
useSessions: useHookOf({
ids: ['x'],
byId: { x: { displayTitle: 'plain session', updatedAt: NOW - 1000 } },
phase: 'ready',
}),
})
assert.ok(text(noTimeline.m.container).includes('No context data yet'))
const kpiValues = queryAll(noTimeline.m.container, '.lc-stat-value').map(el => el.textContent)
assert.equal(kpiValues[1], '0')
assert.equal(kpiValues[2], '—', 'nothing priced without cost buckets')
assert.equal(kpiValues[3], '—', 'no cache hit without billed input')
assert.equal(kpiValues[4], '0', 'no tool calls without timing')
assert.equal(kpiValues[5], '—', 'no active time without timing')
assert.ok(text(noTimeline.m.container).includes('No billed tokens in this range'))
assert.ok(text(noTimeline.m.container).includes('No timing data yet'))
await noTimeline.m.unmount()
})
test('the zh locale renders translated chrome and CNY cost', async () => {
const ctx = new TestClientCtx({ locale: 'zh' })
const Panel = makeOverviewPanel(asClientCtx(ctx), makeKit('zh'))
const m = await mount(h(Panel, {
useSessions: useHookOf(sessionsSnapshot()),
useWorkspaces: useHookOf(workspacesSnapshotValue),
}))
await flush()
assert.ok(text(m.container).includes('上下文洞察'))
assert.ok(text(m.container).includes('全部会话'), 'the All default labels the sessions card and the coverage option')
assert.ok(text(m.container).includes('仅显示已归档'), 'the coverage options ride the zh dictionary too')
assert.ok(text(m.container).includes('Token 统计'), 'the aggregate stats row rides the zh dictionary too')
assert.ok(text(m.container).includes('系统提示词'), 'the Context tab\'s composition categories render in zh')
assert.equal(query<HTMLButtonElement>(m.container, '.lc-ov-settings').title, '打开插件设置', 'the settings chip rides the zh dictionary too')
await flush() // the price book lands
const values = queryAll(m.container, '.lc-stat-value').map(el => el.textContent)
assert.ok(values[2].startsWith('¥'), 'CNY under the zh locale')
await m.unmount()
})
test('a hostile session seat degrades to the unavailable note instead of an error card', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx, { useSessions: () => { throw new Error('boom') } })
assert.ok(text(m.container).includes('The session list is unavailable'))
await m.unmount()
})
test('an unpriceable model dashes the card cost', async () => {
const ctx = makeCtx()
const sparse = timeline({
current: { system: 10, tools: 0, user: 0, inject: 0, skill: 0, assistant: 0, tool: 0, total: 10 },
cost: { deepseek: { 'no-such-model': { peak: { uncached: 100, cacheRead: 0, cacheWrite: 0, output: 0 } } } },
})
const { m } = await openPanel(ctx, {
useSessions: useHookOf({
ids: ['s'],
byId: {
s: { displayTitle: 'sparse session', updatedAt: NOW - 1000, projectionValues: { contextTimeline: sparse } },
},
phase: 'ready',
}),
})
await flush() // the price book lands
// The card's cost: buckets exist but the book prices no such model → the dash.
const values = queryAll(m.container, '.lc-ov-mini-value').map(el => el.textContent)
assert.equal(values[2], '—')
await m.unmount()
})
test('a locale service without getLocale falls back to USD', async () => {
// A hand-rolled ctx: the locale face carries no getLocale at all.
const bare = {
locale: {
register: () => () => {},
bind: () => makeKit().t,
},
get: () => undefined,
}
const Panel = makeOverviewPanel(bare as never, makeKit())
const m = await mount(h(Panel, {
useSessions: useHookOf(sessionsSnapshot()),
useWorkspaces: useHookOf(workspacesSnapshotValue),
}))
await flush()
await flush() // the price book lands
const values = queryAll(m.container, '.lc-stat-value').map(el => el.textContent)
assert.ok(values[2].startsWith('$'), 'USD when the locale face cannot report an active locale')
await m.unmount()
})
test('the grid pages at 12 cards; the pager steps and a filter change re-anchors it', async () => {
const ctx = makeCtx()
const ids = Array.from({ length: 30 }, (_, i) => `s${i}`)
const byId: Record<string, Record<string, unknown>> = {}
ids.forEach((id, i) => { byId[id] = { displayTitle: `session ${i}`, updatedAt: NOW - i * 60_000 } })
const { m } = await openPanel(ctx, {
useSessions: useHookOf({ ids, byId, current: 's0', phase: 'ready' }),
useWorkspaces: useHookOf({ items: [] }),
})
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 12, 'the first page caps at 12 cards')
assert.ok(text(m.container).includes('Page 1 of 3'))
const btns = () => queryAll<HTMLButtonElement>(m.container, '.lc-ov-pager-btn')
assert.ok(btns()[0].disabled, 'prev sits out on the first page')
await click(btns()[1])
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 12, 'the middle page is full')
await click(btns()[1])
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 6, 'the tail page renders the rest')
assert.ok(text(m.container).includes('Page 3 of 3'))
assert.ok(btns()[1].disabled, 'next sits out on the last page')
await click(btns()[0])
assert.ok(text(m.container).includes('Page 2 of 3'), 'prev steps back')
// Typing while on the last page: the reset lands on the first page of the narrowed set.
await actType(query<HTMLInputElement>(m.container, 'input.lc-ov-search'), 'session 2')
assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 11, "'session 2' matches 2 and 20–29")
assert.ok(!text(m.container).includes('Page 2 of 3'), 'the pager drops once one page remains')
await actType(query<HTMLInputElement>(m.container, 'input.lc-ov-search'), '')
assert.ok(text(m.container).includes('Page 1 of 3'), 'clearing the query lands on the first page, not the stale one')
await m.unmount()
})
test('typing keeps the search input focused (re-renders never touch the active element)', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx)
const input = query<HTMLInputElement>(m.container, 'input.lc-ov-search')
input.focus()
await actType(input, 'beta')
await actType(input, 'bet')
assert.equal(document.activeElement, input, 'every re-render leaves the focused input alone')
await m.unmount()
})
})
/** Type into an input through the React change path (native setter + input event, act-wrapped). */
async function actType(input: HTMLInputElement, value: string): Promise<void> {
await act(async () => {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
setter?.call(input, value)
input.dispatchEvent(new Event('input', { bubbles: true }))
})
}
/** Pick a coverage option through the React change path (native setter + change event, act-wrapped). */
async function actSelect(select: HTMLSelectElement, value: string): Promise<void> {
await act(async () => {
const setter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value')?.set
setter?.call(select, value)
select.dispatchEvent(new Event('change', { bubbles: true }))
})
}