fix(schedule): close recurring gate edge cases

This commit is contained in:
pku-xht
2026-08-06 22:23:42 +08:00
committed by Tianyi Cui
parent 49a6e4ddb9
commit b96a9f2268
4 changed files with 79 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ import type { Agent, AgentCancelCause, InboxTarget } from '@deepseek-ai/dsh-agen
import type { UserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import {
MIN_RECURRING_INTERVAL_SECONDS,
ScheduleId,
createAfterScheduleRecord,
createEveryScheduleRecord,
@@ -379,6 +380,38 @@ describe('Schedule timer and admission runtime', () => {
await owner.dispose()
})
it('derives the 288-batch half-open-day bound from production gate spacing', async () => {
const test = await harness()
appendEvery(
test,
'schedule-budget',
MIN_RECURRING_INTERVAL_SECONDS,
Date.now() - MIN_RECURRING_INTERVAL_SECONDS * 1_000,
'budget',
)
const owner = ownerFor(test)
owner.start()
await settle()
const spacing = MIN_RECURRING_INTERVAL_SECONDS * 1_000
for (let index = 1; index <= 288; index += 1) {
await vi.advanceTimersByTimeAsync(spacing)
await settle()
}
const accepted = test.agent.session.events.flatMap((event) => {
if (event.type !== 'schedule/change' || event.data.operation !== 'dispatch'
|| !('acceptedAt' in event.data)) return []
return [Date.parse(event.data.acceptedAt)]
})
expect(accepted).toHaveLength(289)
const windowStart = accepted[0]!
const windowEnd = windowStart + 86_400_000
expect(accepted.slice(0, 288).every(value => value >= windowStart && value < windowEnd)).toBe(true)
expect(accepted[288]).toBe(windowEnd)
expect(accepted.every((value, index) => index === 0 || value - accepted[index - 1]! === spacing)).toBe(true)
await owner.dispose()
})
it('rechecks the wall clock after claiming maintenance before queuing', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)

View File

@@ -302,6 +302,37 @@ describe('Schedule tool protocol', () => {
expect(create?.data).not.toHaveProperty('anchorAt')
})
it('rejects Every creation after the shared gate exhausts despite a wall-clock rollback', async () => {
const test = await harness()
test.agent.session.append('schedule/change', {
version: 1,
operation: 'create',
schedule: {
id: 'schedule-final',
kind: 'every',
prompt: 'final batch',
everySeconds: 300,
scheduledAt: '9999-12-31T23:55:00.000Z',
},
} as never)
test.agent.session.append('schedule/change', {
version: 1,
operation: 'dispatch',
id: 'schedule-final',
acceptedAt: '9999-12-31T23:57:30.000Z',
} as never)
vi.setSystemTime(new Date('9999-12-31T23:50:00.000Z'))
expect(value(await execute(test, 'schedule_create', {
prompt: 'rolled back', every_seconds: 300,
}))).toEqual({
code: 'time_out_of_range',
message: 'The scheduled time must be representable as a four-digit-year RFC 3339 UTC instant.',
})
expect(test.agent.session.events.filter(event => event.type === 'schedule/change')).toHaveLength(2)
expect(value(await execute(test, 'schedule_list', {}))).toEqual([])
})
it('fails closed when local at lacks confirmed request-zone context', async () => {
const test = await harness()
expect(value(await execute(test, 'schedule_create', {