fix(schedule): close concurrent durability gaps

This commit is contained in:
pku-xht
2026-08-07 19:06:34 +08:00
committed by Tianyi Cui
parent 76da9e3f9f
commit 9d73b527de
12 changed files with 160 additions and 71 deletions

View File

@@ -16,7 +16,7 @@ const contexts: Context[] = []
interface ToolHarness {
readonly ctx: Context
readonly agent: Agent
readonly flushes: { count: number; outcomes: Array<'resolve' | 'reject'> }
readonly flushes: { count: number; outcomes: Array<'resolve' | 'reject' | Promise<'resolve' | 'reject'>> }
readonly changes: { count: number }
readonly disposeTools: () => void
}
@@ -50,11 +50,12 @@ async function harness(withPersistence = true): Promise<ToolHarness> {
await ctx.plugin(ToolRegistry)
const agent = stubAgent(ctx, `schedule-tools-${Math.random()}`)
ctx.agents.register(agent)
const flushes = { count: 0, outcomes: [] as Array<'resolve' | 'reject'> }
const flushes = { count: 0, outcomes: [] as Array<'resolve' | 'reject' | Promise<'resolve' | 'reject'>> }
if (withPersistence) {
ctx.on('session/flush', async () => {
flushes.count += 1
if (flushes.outcomes.shift() === 'reject') return Promise.reject(new Error('disk unavailable'))
const outcome = await (flushes.outcomes.shift() ?? 'resolve')
if (outcome === 'reject') return Promise.reject(new Error('disk unavailable'))
return true as const
})
}
@@ -278,6 +279,31 @@ describe('Schedule persistence failure boundaries', () => {
expect(test.changes.count).toBe(2)
})
it('serializes concurrent management transactions across both persistence barriers', async () => {
const test = await harness()
let releaseCreatePreflight: (() => void) | undefined
const createPreflight = new Promise<'resolve'>((resolve) => {
releaseCreatePreflight = () => { resolve('resolve') }
})
test.flushes.outcomes.push(createPreflight, 'reject', 'resolve')
const creating = execute(test, 'schedule_create', { prompt: 'persist me', after_seconds: 10 })
await vi.waitFor(() => { expect(test.flushes.count).toBe(1) })
const listing = execute(test, 'schedule_list', {})
await Promise.resolve()
expect(test.flushes.count).toBe(1)
if (releaseCreatePreflight === undefined) throw new Error('missing create preflight release')
releaseCreatePreflight()
expect(value(await creating)).toMatchObject({
code: 'persistence_uncertain', operation: 'create', id: 'schedule-1',
})
expect(value(await listing)).toEqual([
expect.objectContaining({ id: 'schedule-1', prompt: 'persist me' }),
])
expect(test.flushes.count).toBe(3)
})
it('returns uncertainty before create or delete reads when their preflight rejects', async () => {
const createTest = await harness()
createTest.flushes.outcomes.push('reject')