Files
dsh-context/tests/client/components/contextModal.spec.ts
T
Bowen Liang da394d7e0a refactor(context): drop the 'Top Tool Schemas' chip row from the overview
The chip row lived in CurrentComposition, was fed the fold's per-tool
toolList summary, and the chips focused the Context browser on a tool
via a one-shot toolFocus callback. The browser already exposes the
tool schemas through its headers section, so the chip row was a
duplicate entry point that no longer pulled its weight.

Removes the lc-tools DOM block, the toolFocus/onToolFocusHandled
prop wiring across ContextView/ContextModal/Browser, the
'tools.top'/'tools.more' translations, the .lc-tools* CSS, and the
now-unused toolList summary across the wire + state schemas
(stateVersion 9 -> 10). estimateToolSchema stays because headers.ts
still prices individual tools for the browser's tool-schema cards.
2026-08-26 03:45:04 +08:00

355 lines
14 KiB
TypeScript

// ContextModal (src/client/components/contextModal.tsx) — the /context
// dialog rendered through the REGISTERED composition (makeContextModal over
// a TestClientCtx), with the per-session modal store driving open/close,
// real sessions faces for the consume-token bail, and real projections.
import assert from 'node:assert/strict'
import { act } from 'react'
import { afterEach, describe, test, vi } from 'vitest'
import { React, h } from '../../../src/client/react'
import { makeContextModal } from '../../../src/client/components/contextModal'
import { modalStoreOf, setPendingConsume, takePendingConsume } from '../../../src/client/modalStore'
import type { ContextTimeline } from '../../../src/shared/types'
import { DICT_EN } from '../../../src/client/i18n'
import { TestClientCtx, TestSessions, asClientCtx } from '../helpers/harness'
import { click, hover, keydown, makeKit, mount, query, queryAll, text, unhover } from '../helpers/kit'
const kit = makeKit()
function timeline(over: Record<string, unknown> = {}): ContextTimeline {
return {
ok: true,
current: { system: 1, tools: 2, user: 3, inject: 4, assistant: 5, tool: 6, total: 21 },
requests: [],
events: [],
nodes: [],
droppedNodes: 0,
archive: [],
...over,
} as ContextTimeline
}
/** A useContextModal hook really bound to the per-session store. */
function boundModalHook(sessionId: string) {
const store = modalStoreOf(sessionId)
return (sel: (open: boolean) => boolean): boolean =>
React.useSyncExternalStore(store.subscribe, () => sel(store.getSnapshot()))
}
const OPEN = (): boolean => true
afterEach(() => {
vi.restoreAllMocks()
})
describe('ContextModal', () => {
test('renders null without a useContextModal hook or while closed', async () => {
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const m1 = await mount(h(ContextModal, { sessionId: 'sm-none', useProjection: () => timeline() }))
assert.equal(m1.container.childElementCount, 0)
await m1.unmount()
const m2 = await mount(h(ContextModal, {
sessionId: 'sm-closed',
useContextModal: () => false,
useProjection: () => timeline(),
}))
assert.equal(m2.container.childElementCount, 0)
await m2.unmount()
})
test('open with no projection hook (or an empty one) shows the loading view', async () => {
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const m1 = await mount(h(ContextModal, { sessionId: 'sm-load1', useContextModal: OPEN }))
assert.ok(text(m1.container).includes(DICT_EN.loading))
assert.ok(m1.container.querySelector('.lc-modal-card') !== null)
await m1.unmount()
const m2 = await mount(h(ContextModal, {
sessionId: 'sm-load2',
useContextModal: OPEN,
useProjection: () => undefined,
}))
assert.ok(text(m2.container).includes(DICT_EN.loading))
await m2.unmount()
})
test('a corrupt timeline projection is sanitized and still renders the composition', async () => {
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const m = await mount(h(ContextModal, {
sessionId: 'sm-garbage',
useContextModal: OPEN,
useProjection: (key: string) =>
key === 'contextTimeline' ? { current: 'junk', requests: 'nope', nodes: 7, archive: null } : undefined,
}))
assert.ok(!text(m.container).includes(DICT_EN.loading))
assert.ok(text(m.container).includes(DICT_EN['overview.title']))
assert.ok(m.container.querySelector('.lc-br-cats') !== null)
await m.unmount()
})
test('full render: subtitle variants and hover link through the composed browser', async () => {
const sessions = new TestSessions()
const ctx = new TestClientCtx({ services: { sessions } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const projections: Record<string, unknown> = {
contextTimeline: timeline({
model: 'deepseek-v4-flash',
provider: 'deepseek',
}),
contextPressure: { projectedTokens: 100, contextWindow: 128000 },
contextBreakdown: { systemTokens: 1, toolsTokens: 2, messageTokens: 18 },
contextHeaders: {
headers: [{ seq: 1, time: 0, system: 'SYS', tools: [{ name: 'bash', tokens: 12, description: 'run' }] }],
},
}
const m = await mount(h(ContextModal, {
sessionId: 'sm-full',
useContextModal: OPEN,
useProjection: (key: string) => projections[key],
}))
assert.ok(text(m.container).includes('deepseek-v4-flash · deepseek'))
// Hovering a composition segment flows into hoverCat (dim + legend chip).
const compCard = query(m.container, '.lc-modal-card .lc-card')
const seg = query(compCard, '.lc-stacked-seg')
await hover(seg)
assert.ok(query(compCard, '.lc-stacked').className.includes('lc-stacked-dim'))
assert.ok(queryAll(compCard, '.lc-chip').some(c => c.className.includes('lc-chip-on')))
await unhover(query(compCard, '.lc-stacked'))
assert.ok(!query(compCard, '.lc-stacked').className.includes('lc-stacked-dim'))
await m.unmount()
})
test('subtitle degrades to the model alone, then to nothing', async () => {
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const m1 = await mount(h(ContextModal, {
sessionId: 'sm-sub1',
useContextModal: OPEN,
useProjection: (key: string) => key === 'contextTimeline' ? timeline({ model: 'only-model' }) : undefined,
}))
assert.ok(text(m1.container).includes('only-model'))
assert.ok(!text(m1.container).includes('only-model ·'))
await m1.unmount()
const m2 = await mount(h(ContextModal, {
sessionId: 'sm-sub2',
useContextModal: OPEN,
useProjection: (key: string) => key === 'contextTimeline' ? timeline() : undefined,
}))
const card = query(m2.container, '.lc-modal-card .lc-card')
assert.equal(card.querySelector('.lc-card-sub'), null)
await m2.unmount()
})
test('backdrop click without a sessionId is a no-op', async () => {
const sessions = new TestSessions()
const ctx = new TestClientCtx({ services: { sessions } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const m = await mount(h(ContextModal, { useContextModal: OPEN, useProjection: () => undefined }))
await click(query(m.container, '.lc-modal-backdrop'))
assert.ok(m.container.querySelector('.lc-modal-backdrop') !== null)
assert.equal(sessions.bails.length, 0)
await m.unmount()
})
test('clicking the card itself does not close (stopPropagation)', async () => {
const sessions = new TestSessions()
const ctx = new TestClientCtx({ services: { sessions } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const m = await mount(h(ContextModal, { sessionId: 'sm-card', useContextModal: OPEN, useProjection: () => undefined }))
await click(query(m.container, '.lc-modal-card'))
assert.ok(m.container.querySelector('.lc-modal-backdrop') !== null)
assert.equal(sessions.bails.length, 0)
await m.unmount()
})
test('close flips the store and bails a pending consume through the session scope', async () => {
const sessions = new TestSessions()
const ctx = new TestClientCtx({ services: { sessions } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const sid = 'sm-close'
modalStoreOf(sid).set(true)
setPendingConsume(sid, { kind: 'bare-token', token: '/context' })
const m = await mount(h(ContextModal, {
sessionId: sid,
useContextModal: boundModalHook(sid),
useProjection: () => undefined,
}))
assert.ok(m.container.querySelector('.lc-modal-backdrop') !== null)
await click(query(m.container, '.lc-modal-close'))
assert.equal(modalStoreOf(sid).getSnapshot(), false)
assert.equal(m.container.querySelector('.lc-modal-backdrop'), null)
assert.equal(sessions.bails.length, 1)
assert.equal(sessions.bails[0].event, 'slash/input-consume-token')
assert.deepEqual(sessions.bails[0].payload, { guard: { kind: 'bare-token', token: '/context' } })
// The guard was taken, not left behind.
assert.equal(takePendingConsume(sid), undefined)
await m.unmount()
})
test('close without a pending consume sets the store but bails nothing', async () => {
const sessions = new TestSessions()
const ctx = new TestClientCtx({ services: { sessions } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const sid = 'sm-noguard'
modalStoreOf(sid).set(true)
const m = await mount(h(ContextModal, {
sessionId: sid,
useContextModal: boundModalHook(sid),
useProjection: () => undefined,
}))
await click(query(m.container, '.lc-modal-backdrop'))
assert.equal(modalStoreOf(sid).getSnapshot(), false)
assert.equal(sessions.bails.length, 0)
await m.unmount()
})
test('a pending consume without the sessions service closes quietly', async () => {
const ctx = new TestClientCtx()
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const sid = 'sm-nosvc'
modalStoreOf(sid).set(true)
setPendingConsume(sid, { kind: 'bare-token', token: '/context' })
const m = await mount(h(ContextModal, {
sessionId: sid,
useContextModal: boundModalHook(sid),
useProjection: () => undefined,
}))
await click(query(m.container, '.lc-modal-backdrop'))
assert.equal(modalStoreOf(sid).getSnapshot(), false)
assert.equal(m.container.querySelector('.lc-modal-backdrop'), null)
await m.unmount()
})
test('a pending consume whose scope is gone closes quietly', async () => {
const scoped: string[] = []
const sessions = {
scope: (id: string) => {
scoped.push(id)
return undefined
},
}
const ctx = new TestClientCtx({ services: { sessions } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const sid = 'sm-noscope'
modalStoreOf(sid).set(true)
setPendingConsume(sid, { kind: 'span', span: { start: 0, end: 8, draftRev: 3 } })
const m = await mount(h(ContextModal, {
sessionId: sid,
useContextModal: boundModalHook(sid),
useProjection: () => undefined,
}))
await click(query(m.container, '.lc-modal-backdrop'))
assert.equal(modalStoreOf(sid).getSnapshot(), false)
assert.deepEqual(scoped, [sid])
await m.unmount()
})
test('Escape closes; other keys do not; the listener is removed on close', async () => {
const sessions = new TestSessions()
const ctx = new TestClientCtx({ services: { sessions } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const sid = 'sm-esc'
modalStoreOf(sid).set(true)
const m = await mount(h(ContextModal, {
sessionId: sid,
useContextModal: boundModalHook(sid),
useProjection: () => undefined,
}))
await keydown('a')
assert.ok(m.container.querySelector('.lc-modal-backdrop') !== null)
await keydown('Escape')
assert.equal(modalStoreOf(sid).getSnapshot(), false)
assert.equal(m.container.querySelector('.lc-modal-backdrop'), null)
// Listener removed on close: another Escape must not run close() again —
// with a pending consume armed, a live listener would bail.
setPendingConsume(sid, { kind: 'bare-token', token: '/context' })
await keydown('Escape')
assert.equal(sessions.bails.length, 0)
assert.notEqual(takePendingConsume(sid), undefined)
await m.unmount()
})
test('closing restores focus to the previously focused element', async () => {
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const sid = 'sm-focus'
const btn = document.createElement('button')
document.body.appendChild(btn)
btn.focus()
assert.equal(document.activeElement, btn)
const m = await mount(h(ContextModal, {
sessionId: sid,
useContextModal: boundModalHook(sid),
useProjection: () => undefined,
}))
await act(async () => {
modalStoreOf(sid).set(true)
})
const closeBtn = query(m.container, '.lc-modal-close')
closeBtn.focus()
assert.equal(document.activeElement, closeBtn)
await click(closeBtn)
assert.equal(document.activeElement, btn)
btn.remove()
await m.unmount()
})
test('closing skips the focus restore when the previous element left the document', async () => {
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const sid = 'sm-focus-gone'
const btn = document.createElement('button')
document.body.appendChild(btn)
btn.focus()
const m = await mount(h(ContextModal, {
sessionId: sid,
useContextModal: boundModalHook(sid),
useProjection: () => undefined,
}))
await act(async () => {
modalStoreOf(sid).set(true)
})
btn.remove()
await click(query(m.container, '.lc-modal-close'))
assert.equal(modalStoreOf(sid).getSnapshot(), false)
assert.ok(!document.contains(btn))
await m.unmount()
})
test('no focus bookkeeping when nothing was focused (activeElement not an element)', async () => {
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
const sid = 'sm-focus-null'
modalStoreOf(sid).set(true)
// Environment control (not a plugin fake): jsdom always reports body;
// the harness browser can report null before the body exists.
vi.spyOn(document, 'activeElement', 'get').mockReturnValue(null)
const m = await mount(h(ContextModal, {
sessionId: sid,
useContextModal: boundModalHook(sid),
useProjection: () => undefined,
}))
vi.restoreAllMocks()
await click(query(m.container, '.lc-modal-close'))
assert.equal(modalStoreOf(sid).getSnapshot(), false)
assert.equal(document.activeElement, document.body)
await m.unmount()
})
})