Files
Agent-System-Frontend/src/documentCockpit.test.ts
T
weichen 17996cf4dd Add document cockpit workspace consuming rdm.cockpit.* (I2)
Why: implement the reviewed I2 increment - statistics band, structure tree
with lazy section pages, panorama center (project overview + node summary),
shared selection/filter, snapshot-consistency banner and honest states, per
the frozen contract (Dev docs/superpowers 2026-10-16, rollout section 7).

Evidence: vitest 314/314 green (14 new pure-logic tests covering capability
gate, true-zero vs unavailable stats, filter toggle, snapshot equality and
update notice, context key, selection survival, honest badges); tsc -b and
vite production build green.

Impact: new workspace entry (rdm-cockpit, gated on the seven cockpit
commands); no changes to existing surfaces; reader view and right panel
land in I3.

Refs: rollout I2
2026-10-16 09:03:48 +00:00

127 lines
5.6 KiB
TypeScript

// 文档驾驶舱 I2 纯逻辑层测试:能力门控、筛选切换、统计呈现(真 0 vs 不可算)、
// 快照一致性/更新提示、上下文键、选择保留、树徽标。组件取数编排不在此覆盖。
import { describe, expect, it } from 'vitest'
import {
contextKey, nodeSummaryLine, sameSnapshot, selectionSurvives, statDisplay,
STAT_FILTERS, toggleFilter, treeBadges, updateNotice,
type CockpitMeta, type CockpitStats, type CockpitTreeNode,
} from './documentCockpit'
import { cockpitReady, COCKPIT_COMMANDS } from './workspaces'
const meta = (rev: string, seq: number): CockpitMeta => ({
schemaVersion: 'document-cockpit.v1', projectId: 'p1', snapshotRevision: rev,
asOfSeq: seq, generatedAt: '2026-10-16T00:00:00Z', truncated: false,
})
describe('cockpitReady', () => {
const all = COCKPIT_COMMANDS.map(id => ({ id, enabled: true }))
it('七命令齐备且 enabled 才就绪', () => {
expect(cockpitReady(null)).toBe(false)
expect(cockpitReady([])).toBe(false)
expect(cockpitReady(all.map(c => ({ ...c, enabled: false })))).toBe(false)
expect(cockpitReady(all.slice(0, 6))).toBe(false)
expect(cockpitReady(all)).toBe(true)
})
})
describe('统计呈现:真 0 与不可算不得互相冒充', () => {
const stats: CockpitStats = {
documentCount: 0,
completion: { known: 3, total: 5, state: 'known' },
emptyNodeCount: 0,
unverifiedSourceCount: 2,
conflictCount: null as unknown as number,
pendingChangeCount: null,
updatedAt: '',
statReasons: { conflictCount: 'upstream_error', pendingChangeCount: 'not_projected_v1' },
}
it('真 0 显示 0', () => {
expect(statDisplay(stats, 'empty').value).toBe('0')
expect(statDisplay(stats, 'documents').value).toBe('0')
})
it('null 显示 — 并带原因', () => {
const d = statDisplay(stats, 'conflict')
expect(d.value).toBe('—')
expect(d.hint).toBe('upstream_error')
})
it('未纳入投影的统计显式标注原因', () => {
const d = statDisplay(stats, 'pending')
expect(d.value).toBe('—')
expect(d.hint).toBe('not_projected_v1')
})
it('完成度三分态:known/partial/unavailable', () => {
expect(statDisplay(stats, 'completion').value).toBe('3/5')
expect(statDisplay({ ...stats, completion: { known: 1, total: 5, state: 'partial' } }, 'completion').hint).toBe('部分计算')
const u = statDisplay({ ...stats, completion: { known: 0, total: 0, state: 'unavailable' } }, 'completion')
expect(u.value).toBe('—')
})
})
describe('筛选切换', () => {
it('点击切换、再点清除;统计键与筛选值一一对应', () => {
expect(toggleFilter('', 'state:empty')).toBe('state:empty')
expect(toggleFilter('state:empty', 'state:empty')).toBe('')
expect(toggleFilter('state:empty', 'state:conflict')).toBe('state:conflict')
expect(STAT_FILTERS.map(s => s.filter)).toEqual(['state:empty', 'state:unverified', 'state:conflict'])
})
})
describe('快照一致性与更新提示', () => {
it('同 revision 同 seq 视为同快照', () => {
expect(sameSnapshot(meta('wm:5', 5), meta('wm:5', 5))).toBe(true)
expect(sameSnapshot(meta('wm:5', 5), meta('wm:6', 6))).toBe(false)
expect(sameSnapshot(undefined, meta('wm:5', 5))).toBe(false)
})
it('展示快照落后时提示刷新,一致时不提示', () => {
expect(updateNotice(meta('wm:5', 5), meta('wm:6', 6))).toContain('wm:6')
expect(updateNotice(meta('wm:6', 6), meta('wm:6', 6))).toBeNull()
expect(updateNotice(null, meta('wm:6', 6))).toBeNull()
})
})
describe('旧响应丢弃的上下文键', () => {
it('项目与筛选任一变化都改变键', () => {
expect(contextKey('p1', '')).not.toBe(contextKey('p2', ''))
expect(contextKey('p1', '')).not.toBe(contextKey('p1', 'state:empty'))
expect(contextKey('p1', 'state:empty')).toBe(contextKey('p1', 'state:empty'))
})
})
describe('刷新不重置选择', () => {
it('文档仍在根清单则保留', () => {
const kept = selectionSurvives({ kind: 'document', id: 'd1' }, ['d1', 'd2'])
expect(kept).toEqual({ kind: 'document', id: 'd1' })
})
it('文档被筛掉则清空', () => {
expect(selectionSurvives({ kind: 'document', id: 'd9' }, ['d1', 'd2'])).toBeNull()
})
it('章节选择随父文档存活', () => {
expect(selectionSurvives({ kind: 'section', id: 's1' }, ['d1'], 'd1')).toEqual({ kind: 'section', id: 's1' })
expect(selectionSurvives({ kind: 'section', id: 's1' }, ['d2'], 'd1')).toBeNull()
expect(selectionSurvives(null, ['d1'])).toBeNull()
})
})
describe('树徽标与状态行', () => {
const node = (over: Partial<CockpitTreeNode['state']> & { visibility?: string }): CockpitTreeNode => ({
ref: { kind: 'section', id: 's1' }, title: '章节', path: [], childCount: 0,
visibility: over.visibility || 'full',
state: {
content: 'complete', verification: 'not_applicable', sourceCount: 0,
conflictCount: 0, impactCount: 0, missingChildCount: 0, structuralGap: true, ...over,
},
} as CockpitTreeNode)
it('诚实徽标:空/受限/未核实/冲突计数', () => {
expect(treeBadges(node({}))).toEqual([])
expect(treeBadges(node({ content: 'empty' }))).toContain('空')
expect(treeBadges(node({ verification: 'unverified' }))).toContain('未核实')
expect(treeBadges(node({ conflictCount: 2 }))).toContain('冲突 2')
expect(treeBadges(node({ visibility: 'metadata_only' }))).toContain('仅元数据')
})
it('结构缺口显式标注(冻结事实:doc→section 两级)', () => {
const line = nodeSummaryLine(node({ content: 'empty' }))
expect(line).toContain('内容为空')
expect(line).toContain('层级止于章节')
})
})