mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(schedule): validate resumed cron rules live
This commit is contained in:
@@ -872,23 +872,44 @@ function previousCronInstant(
|
||||
return latestCronInstantThrough(rule, timeZone, baseline, acceptedAt)
|
||||
}
|
||||
|
||||
/** Validate one newly appended Cron record against the current parser, ICU, and calendar adapter. */
|
||||
function validateLiveCronRecord(record: CronScheduleRecord): void {
|
||||
/** Normalize a current calendar-validation failure for the package invariant. */
|
||||
function throwLiveCronValidationError(error: unknown): never {
|
||||
if (error instanceof ScheduleLogError) throw error
|
||||
/* v8 ignore next -- current parser and adapter failures are Error subclasses. */
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
throw new ScheduleLogError(`live cron record is invalid: ${detail}`)
|
||||
}
|
||||
|
||||
/** Validate one Cron rule and zone against current grammar, frequency, and ICU data. */
|
||||
function validateLiveCronRule(record: CronScheduleRecord): {
|
||||
readonly rule: ParsedCronRule
|
||||
readonly timeZone: string
|
||||
} {
|
||||
try {
|
||||
const rule = parseCronRule(record.cron)
|
||||
const timeZone = canonicalizeTimeZone(record.timeZone)
|
||||
if (timeZone !== record.timeZone) {
|
||||
throw new ScheduleLogError('live cron timeZone must use its current canonical IANA name')
|
||||
}
|
||||
if (!rule.hasMatchingDate) {
|
||||
throw new ScheduleLogError('live cron rule must have a matching Gregorian date')
|
||||
}
|
||||
return { rule, timeZone }
|
||||
} catch (error: unknown) {
|
||||
throwLiveCronValidationError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Validate one newly appended Cron record against the current calendar adapter. */
|
||||
function validateLiveCronRecord(record: CronScheduleRecord): void {
|
||||
const { rule, timeZone } = validateLiveCronRule(record)
|
||||
try {
|
||||
const target = Date.parse(record.scheduledAt)
|
||||
if (nextCronInstant(rule, timeZone, target - 60_000) !== target) {
|
||||
throw new ScheduleLogError('live cron scheduledAt must match its rule in the current time-zone data')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ScheduleLogError) throw error
|
||||
/* v8 ignore next -- current parser and adapter failures are Error subclasses. */
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
throw new ScheduleLogError(`live cron record is invalid: ${detail}`)
|
||||
throwLiveCronValidationError(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1310,6 +1331,7 @@ export function validateLiveScheduleChange(
|
||||
const record = foldScheduleEvents(events, seedLength).active.find(candidate => candidate.id === change.id)
|
||||
/* v8 ignore next -- the preceding candidate fold requires calendar fields to target an active Cron record. */
|
||||
if (record?.kind !== 'cron') return
|
||||
validateLiveCronRule(record)
|
||||
const expected = resolveCronOccurrence(record, Date.parse(change.acceptedAt))
|
||||
const nextScheduledAt = 'nextScheduledAt' in change ? change.nextScheduledAt : undefined
|
||||
if (change.occurrenceAt !== expected.occurrenceAt || nextScheduledAt !== expected.nextScheduledAt) {
|
||||
|
||||
@@ -143,6 +143,48 @@ describe('Schedule package invariant', () => {
|
||||
}, 0)],
|
||||
})
|
||||
const fiber = await ctx.plugin(scheduleInvariant)
|
||||
const invalidLiveRules = [
|
||||
{
|
||||
id: 'schedule-historical-fast-cron',
|
||||
cron: '* * * * *',
|
||||
scheduledAt: '2026-08-06T12:00:00.000Z',
|
||||
occurrenceAt: '2026-08-06T12:01:00.000Z',
|
||||
acceptedAt: '2026-08-06T12:01:00.000Z',
|
||||
nextScheduledAt: '2026-08-06T12:02:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'schedule-historical-impossible-cron',
|
||||
cron: '0 0 31 2 *',
|
||||
scheduledAt: '2026-02-01T00:00:00.000Z',
|
||||
occurrenceAt: '2026-02-01T00:00:00.000Z',
|
||||
acceptedAt: '2026-02-01T00:00:00.000Z',
|
||||
nextScheduledAt: undefined,
|
||||
},
|
||||
] as const
|
||||
for (const invalid of invalidLiveRules) {
|
||||
const replay = ctx.sessions.create(SessionId(invalid.id), {
|
||||
seed: [event({
|
||||
version: 1,
|
||||
operation: 'create',
|
||||
schedule: {
|
||||
id: invalid.id,
|
||||
kind: 'cron',
|
||||
prompt: 'historical rule',
|
||||
cron: invalid.cron,
|
||||
timeZone: 'UTC',
|
||||
scheduledAt: invalid.scheduledAt,
|
||||
},
|
||||
}, 0)],
|
||||
})
|
||||
expect(() => replay.append('schedule/change', {
|
||||
version: 1,
|
||||
operation: 'dispatch',
|
||||
id: ScheduleId(invalid.id),
|
||||
occurrenceAt: invalid.occurrenceAt,
|
||||
acceptedAt: invalid.acceptedAt,
|
||||
...(invalid.nextScheduledAt === undefined ? {} : { nextScheduledAt: invalid.nextScheduledAt }),
|
||||
})).toThrow(InvariantError)
|
||||
}
|
||||
await fiber.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user