From 17351fae5c0b18c9a2b1b8bf6822df502e46fd61 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Fri, 13 Mar 2026 16:45:13 +0000 Subject: [PATCH] feat(run-tracking): store BullMQ jobId immediately after run creation --- src/agents/shared/runTracking.ts | 17 ++++++- tests/unit/agents/shared/runTracking.test.ts | 49 +++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/agents/shared/runTracking.ts b/src/agents/shared/runTracking.ts index d3a40797..8f05446e 100644 --- a/src/agents/shared/runTracking.ts +++ b/src/agents/shared/runTracking.ts @@ -5,6 +5,7 @@ import { completeRun, createRun, storeRunLogs, + updateRunJobId, } from '../../db/repositories/runsRepository.js'; import { logger } from '../../utils/logging.js'; import type { FileLogger } from './executionPipeline.js'; @@ -28,6 +29,7 @@ export interface RunTrackingInput { /** * Create a DB run record, suppressing errors so agent execution is unaffected. + * If JOB_ID env var is set (Docker mode), store it immediately after run creation. */ export async function tryCreateRun( input: RunTrackingInput, @@ -35,7 +37,7 @@ export async function tryCreateRun( maxIterations?: number, ): Promise { try { - return await createRun({ + const runId = await createRun({ projectId: input.projectId, workItemId: input.workItemId, prNumber: input.prNumber, @@ -45,6 +47,19 @@ export async function tryCreateRun( model, maxIterations, }); + + // Store BullMQ jobId if running in Docker (JOB_ID env var is set) + const jobId = process.env.JOB_ID; + if (jobId) { + try { + await updateRunJobId(runId, jobId); + } catch (err) { + logger.warn('Failed to store job ID for run', { runId, jobId, error: String(err) }); + // Continue - failure to store jobId should not block agent execution + } + } + + return runId; } catch (err) { logger.warn('Failed to create run record', { error: String(err) }); return undefined; diff --git a/tests/unit/agents/shared/runTracking.test.ts b/tests/unit/agents/shared/runTracking.test.ts index 2fae760d..ef6fd9c1 100644 --- a/tests/unit/agents/shared/runTracking.test.ts +++ b/tests/unit/agents/shared/runTracking.test.ts @@ -4,6 +4,7 @@ vi.mock('../../../../src/db/repositories/runsRepository.js', () => ({ createRun: vi.fn(), completeRun: vi.fn(), storeRunLogs: vi.fn(), + updateRunJobId: vi.fn(), })); vi.mock('../../../../src/utils/logging.js', () => ({ @@ -35,12 +36,14 @@ import { completeRun, createRun, storeRunLogs, + updateRunJobId, } from '../../../../src/db/repositories/runsRepository.js'; import { logger } from '../../../../src/utils/logging.js'; const mockCreateRun = vi.mocked(createRun); const mockCompleteRun = vi.mocked(completeRun); const mockStoreRunLogs = vi.mocked(storeRunLogs); +const mockUpdateRunJobId = vi.mocked(updateRunJobId); const mockExistsSync = vi.mocked(fs.existsSync); const mockReadFileSync = vi.mocked(fs.readFileSync); @@ -63,9 +66,18 @@ const baseInput: RunTrackingInput = { }; describe('tryCreateRun', () => { - it('creates a run and returns the run ID', async () => { + beforeEach(() => { mockCreateRun.mockResolvedValue('run-abc'); + mockUpdateRunJobId.mockResolvedValue(undefined); + mockUpdateRunJobId.mockClear(); + }); + afterEach(() => { + // biome-ignore lint/performance/noDelete: Clean up test environment + delete process.env.JOB_ID; + }); + + it('creates a run and returns the run ID', async () => { const runId = await tryCreateRun(baseInput, 'claude-3-5-sonnet-20241022', 25); expect(runId).toBe('run-abc'); expect(mockCreateRun).toHaveBeenCalledWith({ @@ -80,6 +92,41 @@ describe('tryCreateRun', () => { }); }); + it('stores the job ID when JOB_ID env var is set', async () => { + process.env.JOB_ID = 'job-12345'; + mockUpdateRunJobId.mockClear(); + + const runId = await tryCreateRun(baseInput); + expect(runId).toBe('run-abc'); + expect(mockUpdateRunJobId).toHaveBeenCalledWith('run-abc', 'job-12345'); + }); + + it('does not call updateRunJobId when JOB_ID env var is not set', async () => { + // biome-ignore lint/performance/noDelete: Clean environment before test + delete process.env.JOB_ID; + mockUpdateRunJobId.mockClear(); + + const runId = await tryCreateRun(baseInput); + expect(runId).toBe('run-abc'); + expect(mockUpdateRunJobId).not.toHaveBeenCalled(); + }); + + it('logs a warning but continues when updateRunJobId fails', async () => { + process.env.JOB_ID = 'job-12345'; + mockUpdateRunJobId.mockRejectedValue(new Error('Failed to update job ID')); + + const runId = await tryCreateRun(baseInput); + expect(runId).toBe('run-abc'); + expect(logger.warn).toHaveBeenCalledWith( + 'Failed to store job ID for run', + expect.objectContaining({ + runId: 'run-abc', + jobId: 'job-12345', + error: expect.stringContaining('Failed to update job ID'), + }), + ); + }); + it('returns undefined and logs a warning when createRun throws', async () => { mockCreateRun.mockRejectedValue(new Error('DB error'));