| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- /**
- * Projection value store (push model; session-projection subsystem page:
- * docs/subsystems/session-projection.md): the single
- * higher-seq-wins rule on both paths (a stale baseline cannot overwrite a
- * newer push frame; a replayed frame cannot regress), capability absence as
- * undefined, generation invalidation, and the Session/manager wiring (tail-page
- * seeding, control-stream projection routing pre- and post-instantiation, the
- * list rows' title projection).
- */
- import { describe, expect } from 'vitest'
- import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
- import { SessionSeq } from '@deepseek-ai/dsh-session/types'
- import { ok, type RemoteMock } from '@deepseek-ai/dsh-remote-mock'
- import {
- createClientTest, type ClientTestFixtures, webApp,
- } from '@deepseek-ai/dsh-client-test-runtime/src/assembly/index.ts'
- import { ProjectionValueStore } from '../src/client/sessions/projection-store.ts'
- import { SessionManager } from '../src/client/sessions/manager.ts'
- import type { SessionRemotes } from '../src/client/sessions/remotes.ts'
- import { entries, plainTurn } from './event-script.client.ts'
- import { sessionBench } from './remote/bench.client.ts'
- import { FOLLOW, followScript, sessionWorld } from './remote/session.client.ts'
- // Test-domain keys merged into the projection map (the Service Definition package's
- // pure-type outlet), the same way domain host plugins merge theirs.
- declare module '@deepseek-ai/dsh-session-projection/types' {
- interface SessionProjectionMap {
- 'test/marks': { marks: string[] }
- }
- }
- const SID = 'fk-s1' as SessionId
- /** A Session talks through the Gateway client; its dependency cone is the Typert registry and the Connection. */
- const API_ROSTER = webApp.closure(['@deepseek-ai/dsh-api-gateway'])
- const it = createClientTest({ roster: API_ROSTER })
- /** The first client boot pays the cold module transform of the api cone. */
- const COLD_BOOT_TIMEOUT_MS = 60_000
- function makeManager(mock: RemoteMock, remote: ClientTestFixtures['remote']): SessionManager {
- mock.load(sessionWorld)
- // Manager-routing cases never open a Session, so they do not need the broader Client Remote's $stream member.
- return new SessionManager(remote as unknown as SessionRemotes)
- }
- describe('Session projection value semantics', () => {
- it('reads undefined until a value lands (capability absence)', () => {
- const store = new ProjectionValueStore()
- expect(store.get('test/marks')).toBeUndefined()
- expect(store.faceOf('test/marks').getSnapshot()).toBeUndefined()
- })
- it('applies frames last-wins by seq: replayed and stale frames drop', () => {
- const store = new ProjectionValueStore()
- store.apply('test/marks', { marks: ['a'] }, SessionSeq(5))
- store.apply('test/marks', { marks: ['a', 'b'] }, SessionSeq(9))
- expect(store.get('test/marks')).toEqual({ marks: ['a', 'b'] })
- store.apply('test/marks', { marks: ['stale'] }, SessionSeq(5))
- store.apply('test/marks', { marks: ['equal'] }, SessionSeq(9))
- expect(store.get('test/marks')).toEqual({ marks: ['a', 'b'] })
- })
- it('a stale baseline can neither overwrite nor clear a newer frame; a fresh one reseeds and clears', () => {
- const store = new ProjectionValueStore()
- store.apply('test/marks', { marks: ['frame-20'] }, SessionSeq(20))
- // Stale cut: carried key loses to the newer frame; omitted key survives.
- store.seed({ asOfSeq: SessionSeq(10), values: { 'test/marks': { marks: ['baseline-10'] } } })
- expect(store.get('test/marks')).toEqual({ marks: ['frame-20'] })
- store.seed({ asOfSeq: SessionSeq(15), values: {} })
- expect(store.get('test/marks')).toEqual({ marks: ['frame-20'] })
- // Fresh cut: carried key reseeds…
- store.seed({ asOfSeq: SessionSeq(30), values: { 'test/marks': { marks: ['baseline-30'] } } })
- expect(store.get('test/marks')).toEqual({ marks: ['baseline-30'] })
- // …and an omitting fresh cut clears (capability absent as of the cut).
- store.seed({ asOfSeq: SessionSeq(40), values: {} })
- expect(store.get('test/marks')).toBeUndefined()
- })
- it('clears all generation watermarks without replacing subscribed faces', async () => {
- const store = new ProjectionValueStore()
- const face = store.faceOf('test/marks')
- const observed: unknown[] = []
- const unsubscribe = face.subscribe(() => { observed.push(face.getSnapshot()) })
- try {
- store.apply('test/marks', { marks: ['lost-tail'] }, SessionSeq(20))
- store.apply('empty-session', 'old generation', -1)
- const previous = store.values()
- await Promise.resolve()
- store.clear()
- await Promise.resolve()
- expect(face.getSnapshot()).toBeUndefined()
- expect(store.get('empty-session')).toBeUndefined()
- expect(store.faceOf('test/marks')).toBe(face)
- expect(store.values()).toEqual({})
- expect(store.values()).not.toBe(previous)
- store.seed({ asOfSeq: SessionSeq(1), values: { 'test/marks': { marks: ['durable'] } } })
- await Promise.resolve()
- expect(observed).toEqual([{ marks: ['lost-tail'] }, undefined, { marks: ['durable'] }])
- } finally {
- unsubscribe()
- }
- })
- it('notifies the key face on change (batched) and not on dropped applications', async () => {
- const store = new ProjectionValueStore()
- let keyTicks = 0
- let anyTicks = 0
- store.faceOf('test/marks').subscribe(() => { keyTicks += 1 })
- store.subscribeAny(() => { anyTicks += 1 })
- store.apply('test/marks', { marks: ['a'] }, SessionSeq(5))
- await Promise.resolve()
- expect(keyTicks).toBe(1)
- expect(anyTicks).toBe(1)
- store.apply('test/marks', { marks: ['replay'] }, SessionSeq(3))
- await Promise.resolve()
- expect(keyTicks).toBe(1)
- expect(anyTicks).toBe(1)
- })
- it('faces are identity-stable per key (the React binding cache premise)', () => {
- const store = new ProjectionValueStore()
- expect(store.faceOf('test/marks')).toBe(store.faceOf('test/marks'))
- })
- it('publishes one reference-stable whole-value snapshot until a row changes', () => {
- const store = new ProjectionValueStore()
- const empty = store.values()
- expect(store.values()).toBe(empty)
- store.apply('test/marks', { marks: ['a'] }, SessionSeq(1))
- const populated = store.values()
- expect(populated).toEqual({ 'test/marks': { marks: ['a'] } })
- expect(populated).not.toBe(empty)
- expect(store.values()).toBe(populated)
- })
- })
- describe('Session tail-page seeding', () => {
- it('seeds the store from a history response carrying a projections block', async ({ mock, start }) => {
- const session = await sessionBench(mock, start, SID)
- mock.stream(FOLLOW, followScript(ok({
- records: entries(plainTurn(SessionSeq(0), 0, '问', '答')) as never[], hasMore: false,
- projections: { asOfSeq: 5, values: { 'test/marks': { marks: ['from-baseline'] } } },
- } as never)))
- await session.open()
- expect(session.projections.get('test/marks')).toEqual({ marks: ['from-baseline'] })
- }, COLD_BOOT_TIMEOUT_MS)
- it('a resync serving a stale block keeps the newer pushed value (seq rule end to end)', async ({ mock, start }) => {
- const session = await sessionBench(mock, start, SID)
- mock.stream(FOLLOW, followScript(ok({
- records: entries(plainTurn(SessionSeq(0), 0, 'a', 'b')) as never[], hasMore: false,
- projections: { asOfSeq: 5, values: { 'test/marks': { marks: ['baseline'] } } },
- } as never)))
- await session.open()
- session.projections.apply('test/marks', { marks: ['pushed-9'] }, SessionSeq(9))
- await session.resync()
- expect(session.projections.get('test/marks')).toEqual({ marks: ['pushed-9'] })
- })
- it('treats a blockless response as no reset: pushed values survive', async ({ mock, start }) => {
- const session = await sessionBench(mock, start, SID)
- mock.stream(FOLLOW, followScript(ok({ records: entries(plainTurn(SessionSeq(0), 0, 'a', 'b')) as never[], hasMore: false })))
- await session.open()
- session.projections.apply('test/marks', { marks: ['pushed'] }, SessionSeq(9))
- await session.resync()
- expect(session.projections.get('test/marks')).toEqual({ marks: ['pushed'] })
- })
- })
- describe('manager frame routing', () => {
- const sid = (s: string): SessionId => s as SessionId
- it('lands projection frames before instantiation and the Session adopts the same store', ({ mock, remote }) => {
- const manager = makeManager(mock, remote)
- manager.handleControlFrame({
- type: 'projection', sessionId: sid('s1'), key: 'test/marks', value: { marks: ['early'] }, seq: 7,
- })
- const session = manager.get(sid('s1'))
- expect(session.projections.get('test/marks')).toEqual({ marks: ['early'] })
- // Frames after instantiation land in the same store.
- manager.handleControlFrame({
- type: 'projection', sessionId: sid('s1'), key: 'test/marks', value: { marks: ['later'] }, seq: 9,
- })
- expect(session.projections.get('test/marks')).toEqual({ marks: ['later'] })
- })
- it('preserves a newer title when the control baseline omits it', async ({ mock, remote }) => {
- const manager = makeManager(mock, remote)
- remote.session.list.mockResolvedValue(ok({
- items: [{ sessionId: sid('s1'), updatedAt: 1, running: false, blank: false }],
- }))
- await manager.refreshList()
- manager.handleControlFrame({
- type: 'projection', sessionId: sid('s1'), key: 'title', value: 'Projected title', seq: 4,
- })
- await Promise.resolve()
- expect(manager.getListSnapshot().items[0]?.title).toBe('Projected title')
- manager.handleControlFrame({
- type: 'baseline',
- value: {
- jobs: {},
- projections: { [sid('s1')]: { asOfSeq: 2, values: {} } },
- },
- })
- await Promise.resolve()
- expect(manager.getListSnapshot().items[0]?.title).toBe('Projected title')
- })
- it('projects every retained value into list rows with stable snapshot identity', async ({ mock, remote }) => {
- const manager = makeManager(mock, remote)
- remote.session.list.mockResolvedValue(ok({
- items: [{
- sessionId: sid('s1'), updatedAt: 1, running: false, blank: false,
- projections: {
- asOfSeq: 2,
- values: { 'test/marks': { marks: ['baseline'] } },
- },
- }],
- }))
- await manager.refreshList()
- const baseline = manager.getListSnapshot().items[0]?.projectionValues
- expect(baseline).toEqual({ 'test/marks': { marks: ['baseline'] } })
- expect(manager.getListSnapshot().items[0]?.projectionValues).toBe(baseline)
- manager.handleControlFrame({
- type: 'projection', sessionId: sid('s1'), key: 'test/marks',
- value: { marks: ['live'] }, seq: 3,
- })
- await Promise.resolve()
- expect(manager.getListSnapshot().items[0]?.projectionValues)
- .toEqual({ 'test/marks': { marks: ['live'] } })
- expect(manager.getListSnapshot().items[0]?.projectionValues).not.toBe(baseline)
- })
- it('drops the projection store with the removed session', async ({ mock, remote }) => {
- const manager = makeManager(mock, remote)
- remote.session.list.mockResolvedValue(ok({
- items: [{ sessionId: sid('s1'), updatedAt: 1, running: false, blank: false }],
- }))
- await manager.refreshList()
- manager.handleControlFrame({
- type: 'projection', sessionId: sid('s1'), key: 'title', value: 'Doomed', seq: 4,
- })
- manager.handleSessionRemoved(sid('s1'))
- expect(manager.get(sid('s1')).projections.get('title')).toBeUndefined()
- })
- })
|