Files
deepseek-harness/packages/session/session-projection/tests/registration-reader.spec.ts
T
Magolorand_Kerman fcca540841 refactor(session): unify runtime projection reads (#7347)
* refactor(session): bind runtime projection reads to one session

* refactor(session): expose session-bound readers on projection handles

---------

Co-authored-by: _Kerman <[email protected]>
2026-10-27 09:50:22 +00:00

215 lines
9.8 KiB
TypeScript

/** Retained readers follow Session events independently of registry participation. */
import { Context } from '@deepseek-ai/cordis'
import SessionStore from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import { describe, expect, expectTypeOf, it, onTestFinished, vi } from 'vitest'
import { z } from 'zod'
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionMap {
'test/reader': number
}
}
const definition = {
key: 'test/reader',
stateSchema: z.number(),
stateVersion: 1,
init: () => 0,
apply: (state: number) => state + 1,
wire: { viewSchema: z.number(), view: (state: number) => state },
} satisfies ProjectionDefinition<'test/reader', number>
async function setup() {
const ctx = new Context()
onTestFinished(async () => { await ctx.fiber.dispose() })
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
const owner = await ctx.plugin(Object.assign(function registrationOwner() {}, { inject: ['sessionProjections'] }))
const session = ctx.sessions.create()
return { ctx, owner, session }
}
describe('retained projection readers', () => {
it('binds typed host states to the exact Session without reading unrelated units', async () => {
const { ctx, owner, session } = await setup()
const unrelatedInit = vi.fn(() => 0)
const unrelatedApply = vi.fn((state: number) => state)
await ctx.sessionProjections.register({
key: 'test/bound-unrelated', stateSchema: z.number(), stateVersion: 1,
init: unrelatedInit, apply: unrelatedApply,
})
const countInit = vi.fn(definition.init)
const countApply = vi.fn(definition.apply)
const count = await owner.ctx.sessionProjections.register({ ...definition, init: countInit, apply: countApply })
const hostInit = vi.fn((): { turn: number } | null => null)
const hostApply = vi.fn((state: { turn: number } | null, event: Parameters<ProjectionDefinition<'test/reader', number>['apply']>[1]) => (
event.type === 'turn/start' ? { turn: event.data.turn } : state
))
const host = await owner.ctx.sessionProjections.register({
key: 'test/bound-host', stateSchema: z.object({ turn: z.number() }).nullable(), stateVersion: 1,
init: hostInit, apply: hostApply,
})
const initialized = { count: countInit.mock.calls.length, host: hostInit.mock.calls.length, unrelated: unrelatedInit.mock.calls.length }
const applied = { count: countApply.mock.calls.length, host: hostApply.mock.calls.length, unrelated: unrelatedApply.mock.calls.length }
const reader = { count: count.readerFor(session), host: host.readerFor(session) }
expect({
count: countInit.mock.calls.length, host: hostInit.mock.calls.length, unrelated: unrelatedInit.mock.calls.length,
}).toEqual(initialized)
expect({
count: countApply.mock.calls.length, host: hostApply.mock.calls.length, unrelated: unrelatedApply.mock.calls.length,
}).toEqual(applied)
const initialCount = reader.count()
expectTypeOf(initialCount).toEqualTypeOf<number>()
expect(initialCount).toBe(0)
const initialHost = reader.host()
expectTypeOf(initialHost).toEqualTypeOf<{ turn: number } | null>()
expect(initialHost).toBeNull()
expect(unrelatedInit).toHaveBeenCalledTimes(initialized.unrelated)
expect(unrelatedApply).toHaveBeenCalledTimes(applied.unrelated)
const other = ctx.sessions.create()
const otherReader = count.readerFor(other)
other.append('turn/start', { turn: 1 })
expect(otherReader()).toBe(1)
expect(reader.count()).toBe(0)
session.append('turn/start', { turn: 2 })
expect(reader.count()).toBe(1)
expect(reader.host()).toEqual({ turn: 2 })
expect(reader.host()).toBe(host.stateOf(session))
const read = reader.host
expect(read()).toBe(host.stateOf(session))
})
it('retains the bound unit after owner disposal and version replacement', async () => {
const { ctx, owner, session } = await setup()
const original = await owner.ctx.sessionProjections.register(definition)
const reader = original.readerFor(session)
await owner.dispose()
expect(ctx.sessionProjections.checkpoint(session)).toEqual({})
const replacement = await ctx.sessionProjections.register({ ...definition, stateVersion: 2, apply: state => state + 10 })
const changed = vi.fn()
ctx.sessionProjections.onChanged(changed)
session.append('turn/start', { turn: 1 })
expect(reader()).toBe(1)
expect(original.readerFor(session)()).toBe(1)
expect(replacement.stateOf(session)).toBe(10)
expect(ctx.sessionProjections.snapshot(session).values).toEqual({ 'test/reader': 10 })
expect(changed).toHaveBeenCalledOnce()
expect(ctx.sessionProjections.checkpoint(session)['test/reader']?.ver).toBe(2)
})
it('does not read during binding and preserves the original synchronous failure', async () => {
const { owner, session } = await setup()
const failure = new Error('host state cannot initialize')
const init = vi.fn((): number => { throw failure })
const broken = await owner.ctx.sessionProjections.register({
key: 'test/bound-failure', stateSchema: z.number(), stateVersion: 1,
init, apply: state => state,
})
const initialized = init.mock.calls.length
const reader = broken.readerFor(session)
expect(init).toHaveBeenCalledTimes(initialized)
let caught: unknown
try {
reader()
} catch (error) {
caught = error
}
expect(caught).toBe(failure)
})
it('preserves undefined state without substituting a value', async () => {
const { owner, session } = await setup()
const handle = await owner.ctx.sessionProjections.register({
key: 'test/bound-undefined', stateSchema: z.number().optional(), stateVersion: 1,
init: () => undefined,
apply: (state, event) => event.type === 'turn/start' ? event.data.turn : state,
})
const read = handle.readerFor(session)
expectTypeOf(read()).toEqualTypeOf<number | undefined>()
expect(read()).toBeUndefined()
session.append('turn/start', { turn: 1 })
expect(read()).toBe(1)
})
it.each(['handle', 'fiber'] as const)('reads new events after %s disposal without rejoining snapshots or notifications', async (release) => {
const { ctx, owner, session } = await setup()
const apply = vi.fn(definition.apply)
const handle = await owner.ctx.sessionProjections.register({ ...definition, apply })
const read = handle.readerFor(session)
const changed = vi.fn()
ctx.sessionProjections.onChanged(changed)
expect(handle.stateOf(session)).toBe(0)
session.append('turn/start', { turn: 1 })
expect(handle.stateOf(session)).toBe(1)
expect(changed).toHaveBeenCalledOnce()
if (release === 'handle') await handle.dispose()
else await owner.dispose()
session.append('turn/start', { turn: 2 })
expect(apply).toHaveBeenCalledTimes(1)
expect(handle.stateOf(session)).toBe(2)
expect(read()).toBe(2)
expect(apply).toHaveBeenCalledTimes(2)
expect(changed).toHaveBeenCalledOnce()
expect(ctx.sessionProjections.snapshot(session).values).toEqual({})
expect(ctx.sessionProjections.checkpoint(session)).toEqual({})
const later = ctx.sessions.create()
later.append('turn/start', { turn: 1 })
expect(handle.stateOf(later)).toBe(1)
})
it('keeps the original unit when the same key is registered with a new version', async () => {
const { ctx, owner, session } = await setup()
const original = await owner.ctx.sessionProjections.register(definition)
expect(original.stateOf(session)).toBe(0)
await original.dispose()
const replacement = await ctx.sessionProjections.register({ ...definition, stateVersion: 2, apply: state => state + 10 })
session.append('turn/start', { turn: 1 })
expect(original.stateOf(session)).toBe(1)
expect(replacement.stateOf(session)).toBe(10)
expect(ctx.sessionProjections.snapshot(session).values).toEqual({ 'test/reader': 10 })
})
it('reads only the selected unit without materializing unrelated registrations', async () => {
const { ctx, owner, session } = await setup()
const unrelatedInit = vi.fn(() => 0)
await ctx.sessionProjections.register({
key: 'test/unrelated-reader', stateSchema: z.number(), stateVersion: 1,
init: unrelatedInit, apply: state => state,
})
const handle = await owner.ctx.sessionProjections.register(definition)
expect(handle.stateOf(session)).toBe(0)
expect(unrelatedInit).not.toHaveBeenCalled()
})
it('rejects initialization when the owner unloads before the handle is returned', async () => {
const { ctx, owner, session } = await setup()
const registration = owner.ctx.sessionProjections.register(definition)
const rejected = expect(Promise.resolve(registration)).rejects.toThrow()
await owner.dispose()
await rejected
expect(ctx.sessionProjections.checkpoint(session)).toEqual({})
})
it.each(['twice', 'owner-unload'] as const)('releases one reference once under %s disposal', async (release) => {
const { ctx, owner, session } = await setup()
const retained = await ctx.sessionProjections.register(definition)
const handle = await owner.ctx.sessionProjections.register(definition)
if (release === 'twice') {
await expect(handle.dispose()).resolves.toBeUndefined()
await expect(handle.dispose()).resolves.toBeUndefined()
} else {
await Promise.all([owner.dispose(), expect(handle.dispose()).resolves.toBeUndefined()])
await expect(handle.dispose()).resolves.toBeUndefined()
}
session.append('turn/start', { turn: 1 })
expect(ctx.sessionProjections.snapshot(session).values).toEqual({ 'test/reader': 1 })
expect(handle.stateOf(session)).toBe(1)
await retained.dispose()
expect(ctx.sessionProjections.snapshot(session).values).toEqual({})
})
})