| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- import { describe, expect, it } from 'vitest'
- import { Context } from 'cordis'
- import { createScope, scopeOf } from '@deepseek-ai/dsh-scope'
- import type { Scope, ScopeKey } from '@deepseek-ai/dsh-scope'
- import SessionStore from '@deepseek-ai/dsh-session'
- import type { Session } from '@deepseek-ai/dsh-session'
- async function mount(): Promise<Context> {
- const ctx = new Context()
- await ctx.plugin(SessionStore)
- return ctx
- }
- async function mintScope(ctx: Context, name: string): Promise<Scope> {
- let scope!: Scope
- // The scoped context resolves services through the MINTING plugin's
- // dependency chain — the minter must inject what scope holders will reach.
- await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, { name }) },
- { inject: ['sessions'] }))
- return scope
- }
- /** The key a test scope was minted with. */
- function keyOf(scope: Scope): ScopeKey {
- return scopeOf(scope.ctx)!
- }
- describe('session dispatch carriers', () => {
- it('a session entered through a scoped context dispatches its events in that scope', async () => {
- const ctx = await mount()
- const scope = await mintScope(ctx, 'owner')
- const otherScope = await mintScope(ctx, 'other')
- const heard: string[] = []
- ctx.on('session/event', (_session, event) => void heard.push(`global:${event.type}`))
- scope.ctx.on('session/event', (_session, event) => void heard.push(`owner:${event.type}`))
- otherScope.ctx.on('session/event', (_session, event) => void heard.push(`other:${event.type}`))
- scope.ctx.on('session/created', session => void heard.push(`owner-created:${session.id}`))
- otherScope.ctx.on('session/created', session => void heard.push(`other-created:${session.id}`))
- const session = scope.ctx.sessions.create()
- session.append('turn/start', { turn: 1 })
- expect(heard).toEqual([
- `owner-created:${session.id}`,
- 'global:turn/start',
- 'owner:turn/start',
- ])
- })
- it('a bare session dispatches subject-less: scoped listeners never hear it', async () => {
- const ctx = await mount()
- const scope = await mintScope(ctx, 'owner')
- const heard: string[] = []
- ctx.on('session/event', (_s, event) => void heard.push(`global:${event.type}`))
- scope.ctx.on('session/event', (_s, event) => void heard.push(`owner:${event.type}`))
- const bare = ctx.sessions.create()
- bare.append('turn/start', { turn: 1 })
- expect(heard).toEqual(['global:turn/start'])
- })
- it('reuses the captured owner carrier for the paired disposal notification', async () => {
- const ctx = await mount()
- const owner = await mintScope(ctx, 'owner')
- const other = await mintScope(ctx, 'other')
- const heard: string[] = []
- ctx.on('session/disposed', (session) => { heard.push(`global:${session.id}`) })
- owner.ctx.on('session/disposed', (session) => { heard.push(`owner:${session.id}`) })
- other.ctx.on('session/disposed', (session) => { heard.push(`other:${session.id}`) })
- const session = owner.ctx.sessions.prepare()
- const detach = owner.ctx.sessions.enter(session)
- owner.ctx.sessions.announce(session)
- detach()
- expect(heard).toEqual([`global:${session.id}`, `owner:${session.id}`])
- })
- })
- describe('sessions.flush()', () => {
- it('allows an ordinary flush with no listeners', async () => {
- const ctx = await mount()
- const session = ctx.sessions.create()
- await expect(ctx.sessions.flush(session)).resolves.toBe(false)
- })
- it('reports a participating listener after it succeeds', async () => {
- const ctx = await mount()
- const session = ctx.sessions.create()
- const flushed: Session[] = []
- ctx.on('session/flush', current => void flushed.push(current))
- await expect(ctx.sessions.flush(session)).resolves.toBe(true)
- expect(flushed).toEqual([session])
- })
- it('dispatches session/flush with the owning carrier and awaits all listeners', async () => {
- const ctx = await mount()
- const scope = await mintScope(ctx, 'owner')
- const flushed: string[] = []
- ctx.on('session/flush', async (session: Session) => {
- await Promise.resolve()
- flushed.push(`global:${session.id}`)
- })
- scope.ctx.on('session/flush', (session: Session) => void flushed.push(`owner:${session.id}`))
- const owned = scope.ctx.sessions.create()
- const bare = ctx.sessions.create()
- await ctx.sessions.flush(owned)
- await ctx.sessions.flush(bare)
- // Parallel dispatch: listener completion order is unspecified (the global
- // listener awaits a microtask) — assert set membership per flush instead.
- expect(flushed.slice(0, 2).sort()).toEqual([`global:${owned.id}`, `owner:${owned.id}`])
- expect(flushed.slice(2)).toEqual([`global:${bare.id}`])
- })
- it('propagates a rejecting flush listener (the caller owns the failure policy)', async () => {
- const ctx = await mount()
- ctx.on('session/flush', () => Promise.reject(new Error('disk full')))
- const session = ctx.sessions.create()
- await expect(ctx.sessions.flush(session)).rejects.toThrow('disk full')
- })
- it('does not let a synchronous flush failure starve later listeners', async () => {
- const ctx = await mount()
- const flushed: Session[] = []
- ctx.on('session/flush', () => { throw new Error('disk full') })
- ctx.on('session/flush', (session) => { flushed.push(session) })
- const session = ctx.sessions.create()
- await expect(ctx.sessions.flush(session)).rejects.toThrow('disk full')
- expect(flushed).toEqual([session])
- })
- it('waits for slower flush listeners before reporting another listener failure', async () => {
- const ctx = await mount()
- const gate = Promise.withResolvers<undefined>()
- let slowStarted = false
- let settled = false
- ctx.on('session/flush', () => Promise.reject(new Error('disk full')))
- ctx.on('session/flush', () => {
- slowStarted = true
- return gate.promise
- })
- const session = ctx.sessions.create()
- const flushing = ctx.sessions.flush(session)
- void flushing.finally(() => { settled = true }).catch(() => undefined)
- await Promise.resolve()
- expect(slowStarted).toBe(true)
- expect(settled).toBe(false)
- gate.resolve(undefined)
- await expect(flushing).rejects.toThrow('disk full')
- expect(settled).toBe(true)
- })
- it('rejects a never-entered session instead of inventing a carrier', async () => {
- const ctx = await mount()
- const scope = await mintScope(ctx, 'owner')
- const flushed: string[] = []
- ctx.on('session/flush', (session: Session) => void flushed.push(`global:${session.id}`))
- scope.ctx.on('session/flush', (session: Session) => void flushed.push(`owner:${session.id}`))
- const prepared = ctx.sessions.prepare()
- await expect(ctx.sessions.flush(prepared)).rejects.toThrow(/not live/)
- expect(flushed).toEqual([])
- })
- it('clears a detached carrier and rejects stale flushes', async () => {
- const ctx = await mount()
- const scope = await mintScope(ctx, 'owner')
- const flushed: string[] = []
- ctx.on('session/flush', (session: Session) => void flushed.push(`global:${session.id}`))
- scope.ctx.on('session/flush', (session: Session) => void flushed.push(`owner:${session.id}`))
- const session = scope.ctx.sessions.prepare()
- const detach = scope.ctx.sessions.enter(session)
- await ctx.sessions.flush(session)
- expect(flushed.sort()).toEqual([`global:${session.id}`, `owner:${session.id}`])
- detach()
- await expect(ctx.sessions.flush(session)).rejects.toThrow(/not live/)
- expect(flushed).toHaveLength(2)
- })
- it('keyOf sanity: distinct scopes carry distinct keys', async () => {
- const ctx = await mount()
- const a = await mintScope(ctx, 'a')
- const b = await mintScope(ctx, 'b')
- expect(keyOf(a)).not.toBe(keyOf(b))
- })
- })
|