Files
dsh-context/tests/client/components/contextView.splitDetail.spec.ts
T
bowenliang123 9aaa11591a test(client): split the oversized specs into siblings with shared harnesses
browser.spec.ts (2360), trendChart.spec.ts (1629), contextView.spec.ts (1504), and
overview.spec.ts (1065) exceeded the 1000-line limit. Each splits along its existing
describe seams into same-directory siblings, with the fixtures that more than one
sibling needs extracted once into a shared harness.

The move is verbatim: test titles, describe blocks, and every assertion line are
preserved exactly (2061 tests before and after, no duplicate titles, no skips), and
the suite reports the same coverage.
2026-10-10 01:21:15 +08:00

182 lines
8.4 KiB
TypeScript

import { createElement as h } from 'react'
import assert from 'node:assert/strict'
import { describe, test, vi } from 'vitest'
import { requestContextFocus, takeContextFocus } from '../../../src/client/viewFocus'
import type { ContextTimeline } from '../../../src/shared/types'
import { DICT_EN } from '../../../src/client/i18n'
import { TestClientCtx } from '../helpers/harness'
import { click, flush, mount, query, queryAll, text, until } from '../helpers/kit'
import { makeView, projectionsFor, richTimeline, T0, timeline } from './contextViewHarness'
describe('ContextView — the split generation (slim head + detail channel)', () => {
/** A slim wire head: the pushed value with the collections empty and the counters/markers on. */
function slimHead(over: Record<string, unknown> = {}): ContextTimeline {
return timeline({
model: 'deepseek-v4-flash',
provider: 'deepseek',
contextWindow: 128000,
toolCalls: 3,
images: 1,
counts: { turns: 1, steps: 3, injects: 1, compactions: 1, prunes: 0 },
last: { seq: 6, total: 420, prompt: 410 },
detailRev: 6,
...over,
})
}
/** The matching detail payload (richTimeline's collections). */
function slimDetail(rev = 6): Record<string, unknown> {
const rich = richTimeline()
return {
rev,
requests: rich.requests,
events: rich.events,
nodes: rich.nodes,
droppedNodes: rich.droppedNodes,
archive: rich.archive,
}
}
function slimCtx(serve: () => Promise<unknown>): TestClientCtx {
vi.stubGlobal('fetch', async () => ({ ok: true, status: 200, json: async () => await serve() }))
return new TestClientCtx()
}
test('the head paints the counters immediately; the detail collections land through the channel', async () => {
const ctx = slimCtx(async () => ({ ok: true, value: slimDetail() }))
const View = makeView(ctx)
const m = await mount(h(View, { sessionId: 'sv-slim', useProjection: projectionsFor(slimHead()) }))
// First paint: the counters are real (no detail needed), the detail cards name the pending read.
// The session node reads two figure rows off the head: turns/steps, then skill loads/answers.
const values = queryAll(m.container, '.lc-flow-kv b').map(el => text(el))
assert.deepEqual(values, ['1', '3', '0', '0'], 'turns/steps/skills/answers from the head counters')
assert.ok(text(m.container).includes(DICT_EN['detail.loading']))
assert.equal(queryAll(m.container, '.lc-bar').length, 0, 'the chart waits for the detail')
assert.equal(queryAll(m.container, '.lc-br-pick option').length, 1, 'the picker holds only the live row')
await until(() => queryAll(m.container, '.lc-bar').length === 3, 'the detail never landed')
assert.ok(!text(m.container).includes(DICT_EN['detail.loading']))
assert.ok(text(m.container).includes('heads-up'), 'the events list serves the detail')
assert.equal(queryAll(m.container, '.lc-br-pick option').length, 4, 'live + three steps')
assert.ok(text(m.container).includes('reply three'), 'the brief rows serve the detail')
assert.ok(text(m.container).includes(DICT_EN['files.empty']), 'no file ops in this detail')
await m.unmount()
})
test('the stats figures jump into the browser: skill loads open the skill section, answers open assistant filtered to Answer', async () => {
const rich = richTimeline()
const nodes = [...rich.nodes, { seq: 7, cat: 'skill', tokens: 5, skill: 'pdf', text: 'instructions', time: T0 + 6000 }]
const ctx = slimCtx(async () => ({
ok: true,
value: { rev: 6, requests: rich.requests, events: rich.events, nodes, droppedNodes: rich.droppedNodes, archive: rich.archive },
}))
const View = makeView(ctx)
const m = await mount(h(View, {
sessionId: 'sv-slim',
useProjection: projectionsFor(slimHead({
counts: { turns: 1, steps: 3, injects: 1, compactions: 1, prunes: 0, skills: 1 },
answers: 3,
})),
}))
await until(() => queryAll(m.container, '.lc-bar').length === 3, 'the detail never landed')
const openCat = (): HTMLElement => {
const on = queryAll(m.container, '.lc-br-cat-row.lc-br-cat-open')
assert.equal(on.length, 1)
const parent = on[0].parentElement
assert.ok(parent !== null)
return parent
}
const cells = queryAll(m.container, '.lc-flow-kv-btn')
assert.equal(cells.length, 2)
await click(cells[1])
await flush()
const assistantCat = openCat()
const answerChip = queryAll(assistantCat, '.lc-gran-btn').find(b => text(b).includes('Answer'))
assert.ok(answerChip !== undefined && answerChip.className.includes('lc-gran-on'), 'the Answer chip is picked')
assert.equal(queryAll(assistantCat, '.lc-br-elem').length, 3, 'all three textual replies survive the filter')
await click(cells[0])
await flush()
const skillCat = openCat()
assert.ok(text(skillCat).includes('pdf'), 'the loaded skill row shows')
assert.equal(queryAll(skillCat, '.lc-br-elem').length, 1)
await m.unmount()
})
test('a failed detail read arms the retry notes; one click refires and the cards land', async () => {
let online = false
const ctx = slimCtx(async () => {
if (!online) throw new Error('offline')
return { ok: true, value: slimDetail() }
})
const View = makeView(ctx)
const m = await mount(h(View, { sessionId: 'sv-slim-fail', useProjection: projectionsFor(slimHead()) }))
await until(() => text(m.container).includes(DICT_EN['detail.loadFailed']), 'the failure never surfaced')
assert.equal(queryAll(m.container, '.lc-bar').length, 0)
online = true
await click(query(m.container, '.lc-br-retry'))
await until(() => queryAll(m.container, '.lc-bar').length === 3, 'the retry never recovered the cards')
assert.ok(!text(m.container).includes(DICT_EN['detail.loadFailed']))
assert.ok(text(m.container).includes('heads-up'))
await m.unmount()
})
test('the jump relay survives the split: held while the detail reads, resolves when it lands', async () => {
const ctx = slimCtx(async () => ({ ok: true, value: slimDetail() }))
const View = makeView(ctx)
// The jump is armed BEFORE the view mounts (the chat action relay) — the
// detail read is still pending, so the pin must wait for it (not consume and clamp against an empty record list).
requestContextFocus('sv-slim-jump', 4)
const m = await mount(h(View, { sessionId: 'sv-slim-jump', useProjection: projectionsFor(slimHead()) }))
assert.equal(queryAll(m.container, '.lc-bar-selected').length, 0, 'nothing pins before the detail')
await until(
() => queryAll(m.container, '.lc-bar[data-seq="4"]')[0]?.className.includes('lc-bar-selected') === true,
'the jump never resolved',
)
assert.equal(takeContextFocus('sv-slim-jump'), null)
await m.unmount()
})
})
describe('ContextView — the op-log generation (fileOps on the detail payload)', () => {
test('the File Activity card renders the fold-derived ops, no conversation join needed', async () => {
vi.stubGlobal('fetch', async () => ({
ok: true,
status: 200,
json: async () => ({
ok: true,
value: {
rev: 1,
requests: [{ seq: 2, turn: 1, step: 1, time: T0, system: 1, tools: 2, user: 3, inject: 0, assistant: 4, tool: 5, total: 15 }],
events: [],
nodes: [],
droppedNodes: 0,
archive: [],
// The op log covers the full session — the conversation window join plays no role in this card on this generation.
fileOps: [
{ seq: 1, path: '/ws/README.md', kind: 'read', tool: 'read', err: false, added: 0, removed: 0, read: { start: 1, count: 12 } },
{ seq: 2, path: '/ws/src/a.ts', kind: 'write', tool: 'edit', err: false, added: 3, removed: 1 },
],
},
}),
}))
const ctx = new TestClientCtx()
const View = makeView(ctx)
const m = await mount(h(View, {
sessionId: 'sv-opslog',
useProjection: projectionsFor(timeline({
counts: { turns: 1, steps: 1, injects: 0, compactions: 0, prunes: 0 },
detailRev: 1,
})),
}))
await until(() => text(m.container).includes('README.md'), 'the detail never landed')
assert.ok(text(m.container).includes('a.ts'), 'both served ops row')
assert.ok(text(m.container).includes('+3'), 'the edit delta shows')
assert.ok(!text(m.container).includes('No file reads'), 'not the empty state')
await m.unmount()
})
})