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
127 lines
5.6 KiB
TypeScript
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('层级止于章节')
|
|
})
|
|
})
|