Why: U4 read-only experience - surface.updated events project into a surfaces slice with a strict revision guard (stale/duplicate events never overwrite; server snapshot is the single owner), preview tabs gain a surface: kind that renders SurfaceView (doc-refs bridge into artifact previews), a new page auto-opens a tab without stealing the user's active non-surface tab, and opening a conversation reloads the authoritative surface list so refresh/断线 restores pages. Evidence: tsc clean; vitest 87/87 including new surface projection tests (stale/duplicate/multi-surface); UI SurfaceView suite green in its repo. Impact: Frontend-only; no backend or contract changes. Refs: Core PRs #27/#28; UI PRs #42/#43/#44; Dev task feature/generative-ui-u4
40 lines
1.6 KiB
TypeScript
40 lines
1.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { store } from './store'
|
||
import type { StreamEvent, SurfaceSnapshot } from './contracts'
|
||
|
||
// U4/R5:surface.updated 投影的 revision 守卫——旧修订/重复事件不得覆盖新修订;
|
||
// 服务端快照是唯一权威(KTD8 单一状态所有者)。
|
||
|
||
function surfEvent(seq: number, surfaceId: string, revision: number, title: string): StreamEvent<SurfaceSnapshot> {
|
||
return {
|
||
seq, conversationId: 'c1', ts: new Date().toISOString(), v: 1, type: 'surface.updated',
|
||
payload: {
|
||
identity: { surfaceId, owner: 'alice', pluginId: 'generative-ui', conversationId: 'c1' },
|
||
compat: { schemaVersion: 1, catalogVersion: 'ui-v1' },
|
||
revision, activationEpoch: 1,
|
||
spec: { title, nodes: [{ type: 'markdown', id: 'n1', markdown: 'x' }] },
|
||
},
|
||
}
|
||
}
|
||
|
||
describe('surface.updated projection', () => {
|
||
it('新事件入库;旧修订与重复事件不覆盖', () => {
|
||
store.applyEvent(surfEvent(1, 's1', 1, '初版'))
|
||
store.applyEvent(surfEvent(2, 's1', 2, '修订二'))
|
||
store.applyEvent(surfEvent(3, 's1', 1, '迟到的旧修订'))
|
||
store.applyEvent(surfEvent(4, 's1', 2, '重复事件'))
|
||
const list = store.getState().surfaces
|
||
expect(list).toHaveLength(1)
|
||
expect(list[0].revision).toBe(2)
|
||
expect(list[0].spec.title).toBe('修订二')
|
||
})
|
||
|
||
it('多页面并存,互不覆盖', () => {
|
||
store.applyEvent(surfEvent(5, 's2', 1, '页面二'))
|
||
const list = store.getState().surfaces
|
||
expect(list.filter(x => String(x.identity.surfaceId) === 's1')).toHaveLength(1)
|
||
expect(list.filter(x => String(x.identity.surfaceId) === 's2')).toHaveLength(1)
|
||
})
|
||
})
|
||
|