fix(session-title): serialize provider request writes
This commit is contained in:
@@ -2,6 +2,7 @@ import { Context, type Fiber } from 'cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionTitleService, {
|
||||
appendSessionTitleOutOfBand,
|
||||
SessionTitleProviderId,
|
||||
type Config,
|
||||
type SessionTitleProvider,
|
||||
@@ -9,6 +10,16 @@ import SessionTitleService, {
|
||||
type SessionTitleProviderResult,
|
||||
} from '@deepseek-ai/dsh-session-title'
|
||||
|
||||
declare module '@deepseek-ai/dsh-session' {
|
||||
interface SessionEventMap {
|
||||
'test/title-provider-request': { revision: number }
|
||||
}
|
||||
|
||||
interface OutOfBandSessionEventMap {
|
||||
'test/title-provider-request': true
|
||||
}
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
fallbackMaxWords: 5,
|
||||
fallbackMaxBytes: 40,
|
||||
@@ -247,6 +258,78 @@ describe('SessionTitleService configuration and refresh boundaries', () => {
|
||||
expect(olderError.message).toMatch(/superseded/)
|
||||
})
|
||||
|
||||
it('serializes a newer provider write after the superseded write', async () => {
|
||||
const ctx = await setup()
|
||||
const session = startSession(ctx, 'refresh-provider-write-order')
|
||||
const source = appendPrompt(session, 'Serialize explicit provider writes')
|
||||
await settle()
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
const flushStarted = deferred<undefined>()
|
||||
const releaseFlush = deferred<undefined>()
|
||||
let flushCount = 0
|
||||
ctx.on('session/flush', async (subject) => {
|
||||
if (subject !== session || ++flushCount !== 1) return
|
||||
flushStarted.resolve(undefined)
|
||||
await releaseFlush.promise
|
||||
})
|
||||
let generation = 0
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('refresh-provider-write-order'),
|
||||
automatic: 'first-message',
|
||||
async generate(request) {
|
||||
generation += 1
|
||||
const revision = generation
|
||||
await appendSessionTitleOutOfBand(ctx, request.session, 'test/title-provider-request', {
|
||||
revision,
|
||||
}, request.signal)
|
||||
return {
|
||||
title: `Generated title ${revision}`,
|
||||
messageSeqs: [source.seq],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const older = ctx.sessionTitle.refresh(session)
|
||||
const olderOutcome = older.then(
|
||||
() => undefined,
|
||||
(error: unknown) => error,
|
||||
)
|
||||
await flushStarted.promise
|
||||
const middle = ctx.sessionTitle.refresh(session)
|
||||
const middleOutcome = middle.then(
|
||||
value => value,
|
||||
(error: unknown) => error,
|
||||
)
|
||||
await settle()
|
||||
|
||||
expect(generation).toBe(2)
|
||||
expect(session.events.filter(event => event.type === 'test/title-provider-request'))
|
||||
.toHaveLength(1)
|
||||
const newer = ctx.sessionTitle.refresh(session)
|
||||
const newerOutcome = newer.then(
|
||||
value => value,
|
||||
(error: unknown) => error,
|
||||
)
|
||||
await settle()
|
||||
expect(generation).toBe(3)
|
||||
expect(session.events.filter(event => event.type === 'test/title-provider-request'))
|
||||
.toHaveLength(1)
|
||||
|
||||
releaseFlush.resolve(undefined)
|
||||
const newerResult = await newerOutcome
|
||||
expect(newerResult).toMatchObject({ title: 'Generated title 3' })
|
||||
const olderError = await olderOutcome
|
||||
expect(olderError).toBeInstanceOf(Error)
|
||||
if (!(olderError instanceof Error)) throw new Error('expected older refresh to reject')
|
||||
expect(olderError.message).toMatch(/superseded/)
|
||||
const middleError = await middleOutcome
|
||||
expect(middleError).toBeInstanceOf(Error)
|
||||
if (!(middleError instanceof Error)) throw new Error('expected middle refresh to reject')
|
||||
expect(middleError.message).toMatch(/superseded/)
|
||||
expect(session.events.filter(event => event.type === 'test/title-provider-request').map(event => event.data.revision))
|
||||
.toEqual([1, 3])
|
||||
})
|
||||
|
||||
it('cancels a queued fallback when the session-title service unloads', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
|
||||
Reference in New Issue
Block a user