diff --git a/packages/api/src/schedules/service.spec.ts b/packages/api/src/schedules/service.spec.ts index 3d36547a4f..7b6945864c 100644 --- a/packages/api/src/schedules/service.spec.ts +++ b/packages/api/src/schedules/service.spec.ts @@ -950,6 +950,15 @@ describe('scheduled resume capacity', () => { }); expect(methods.markRunResumeClaimed).not.toHaveBeenCalled(); }); + + it('normalizes an exhausted slot-collision retry to public capacity conflict', async () => { + const { service, methods } = makeResumeService({ takenSlots: [], unslotted: 0 }); + methods.markRunResumeClaimed.mockResolvedValue({ conflict: 'slot-taken' }); + + await expect(service.claimScheduleResume('s1', '2026-08-17T12:00:00.000Z')).resolves.toEqual({ + conflict: 'capacity', + }); + }); }); describe('deployment-wide limits', () => { diff --git a/packages/api/src/schedules/service.ts b/packages/api/src/schedules/service.ts index 0d790c0d54..2e508d6f41 100644 --- a/packages/api/src/schedules/service.ts +++ b/packages/api/src/schedules/service.ts @@ -855,7 +855,12 @@ export function createSchedulesService( } const claimed = allocation.claimed; if ('conflict' in claimed) { - return { conflict: claimed.conflict }; + // withCapacitySlot consumes this internal collision sentinel by retrying the + // next free slot. Normalize defensively at the public boundary as well so a + // custom/test allocator can never leak an implementation-only conflict. + return { + conflict: claimed.conflict === 'slot-taken' ? 'capacity' : claimed.conflict, + }; } return { capacitySlot: claimed.capacitySlot }; });