From fa96c9731c15fa1f39f35eac309aa5918ca0e139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Birm=C3=A9?= Date: Tue, 7 Jul 2026 08:55:55 +0200 Subject: [PATCH] fix(core): check job.health field in waitForJobToComplete for pod-status health sources (#143) Services like eyevinn-ffmpeg-s3 signal completion via job.health === 'SuccessCriteriaMet' rather than job.status. The previous implementation only checked job.status, causing waitForJobToComplete to spin all 1000 iterations (~16 min) even after the job had finished. Now both job.status and job.health are checked against the terminal success and failure sets. Closes #143 Co-Authored-By: Claude --- packages/core/src/job.test.ts | 39 +++++++++++++++++++++++++++++++++++ packages/core/src/job.ts | 13 +++++++----- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/core/src/job.test.ts b/packages/core/src/job.test.ts index 6bd75b8..96ea248 100644 --- a/packages/core/src/job.test.ts +++ b/packages/core/src/job.test.ts @@ -64,6 +64,45 @@ describe('waitForJobToComplete', () => { expect(mockGetInstance).toHaveBeenCalledTimes(1); }); + test('resolves when job health is SuccessCriteriaMet (status field absent)', async () => { + const completedJob = { name: 'myjob', health: 'SuccessCriteriaMet' }; + mockGetInstance.mockResolvedValue(completedJob); + const result = await waitForJobToComplete( + ctx, + 'eyevinn-ffmpeg-s3', + 'myjob', + 'token' + ); + expect(result).toEqual(completedJob); + expect(mockGetInstance).toHaveBeenCalledTimes(1); + }); + + test('resolves when job health is Complete (status field absent)', async () => { + const completedJob = { name: 'myjob', health: 'Complete' }; + mockGetInstance.mockResolvedValue(completedJob); + const result = await waitForJobToComplete( + ctx, + 'eyevinn-ffmpeg-s3', + 'myjob', + 'token' + ); + expect(result).toEqual(completedJob); + }); + + test('throws when job health is Failed (status field absent)', async () => { + mockGetInstance.mockResolvedValue({ health: 'Failed' }); + await expect( + waitForJobToComplete(ctx, 'eyevinn-ffmpeg-s3', 'myjob', 'token') + ).rejects.toThrow("Job 'myjob' failed with status: Failed"); + }); + + test('throws when job health is FailureTarget (status field absent)', async () => { + mockGetInstance.mockResolvedValue({ health: 'FailureTarget' }); + await expect( + waitForJobToComplete(ctx, 'eyevinn-ffmpeg-s3', 'myjob', 'token') + ).rejects.toThrow("Job 'myjob' failed with status: FailureTarget"); + }); + test('polls until job reaches terminal status', async () => { const completedJob = { name: 'myjob', status: 'SuccessCriteriaMet' }; mockGetInstance diff --git a/packages/core/src/job.ts b/packages/core/src/job.ts index bfee377..6e43679 100644 --- a/packages/core/src/job.ts +++ b/packages/core/src/job.ts @@ -142,8 +142,8 @@ export interface WaitForJobOptions { * @throws {Error} If the job reaches a terminal failure state ('Failed' or 'FailureTarget') * @throws {Error} If timeoutMs is set and the job does not complete within the given time * - * Terminal success statuses: 'Complete', 'SuccessCriteriaMet' - * Terminal failure statuses: 'Failed', 'FailureTarget' + * Terminal success statuses (status or health field): 'Complete', 'SuccessCriteriaMet' + * Terminal failure statuses (status or health field): 'Failed', 'FailureTarget' */ export async function waitForJobToComplete( context: Context, @@ -162,11 +162,14 @@ export async function waitForJobToComplete( throw new Error(`Job '${name}' timed out after ${options!.timeoutMs}ms`); } const job = await getJob(context, serviceId, name, token); - if (job.status === 'Complete' || job.status === 'SuccessCriteriaMet') { + const terminalSuccess = new Set(['Complete', 'SuccessCriteriaMet']); + const terminalFailure = new Set(['Failed', 'FailureTarget']); + if (terminalSuccess.has(job.status) || terminalSuccess.has(job.health)) { return job; } - if (job.status === 'Failed' || job.status === 'FailureTarget') { - throw new Error(`Job '${name}' failed with status: ${job.status}`); + if (terminalFailure.has(job.status) || terminalFailure.has(job.health)) { + const signal = terminalFailure.has(job.status) ? job.status : job.health; + throw new Error(`Job '${name}' failed with status: ${signal}`); } await delay(1000); }