diff --git a/apps/api/src/trigger/evidence-export/export-organization-evidence.spec.ts b/apps/api/src/trigger/evidence-export/export-organization-evidence.spec.ts index 87da540128..a3db6f652a 100644 --- a/apps/api/src/trigger/evidence-export/export-organization-evidence.spec.ts +++ b/apps/api/src/trigger/evidence-export/export-organization-evidence.spec.ts @@ -66,7 +66,29 @@ jest.mock('@/tasks/evidence-export/evidence-attachment-streamer', () => ({ createFilenameTracker: jest.fn(), })); -import { streamArchiveToS3 } from './export-organization-evidence'; +import { + exportOrganizationEvidenceTask, + streamArchiveToS3, +} from './export-organization-evidence'; + +describe('exportOrganizationEvidenceTask config', () => { + // schemaTask is mocked to return its config, so the task IS its config object. + const config = exportOrganizationEvidenceTask as unknown as { + id: string; + maxDuration: number; + }; + + it('allows large orgs at least an hour before Trigger.dev kills the run', () => { + // Regression: orgs with high automation volume + large outputs exceeded the + // old 30-minute (60 * 30 = 1800s) budget, so Trigger.dev killed the run + // (retry maxAttempts: 0), leaving it in a non-COMPLETED terminal state that + // the browser surfaces as a failed export with no download link. The task + // must be allowed at least an hour to finish streaming the ZIP to S3. + expect(config.maxDuration).toBeGreaterThanOrEqual(60 * 60); + // maxDuration is in SECONDS — guard against the ms form (which would be days). + expect(config.maxDuration).toBeLessThan(24 * 60 * 60); + }); +}); describe('streamArchiveToS3', () => { const s3Client = {} as never; diff --git a/apps/api/src/trigger/evidence-export/export-organization-evidence.ts b/apps/api/src/trigger/evidence-export/export-organization-evidence.ts index 649dd019b3..98ded2448d 100644 --- a/apps/api/src/trigger/evidence-export/export-organization-evidence.ts +++ b/apps/api/src/trigger/evidence-export/export-organization-evidence.ts @@ -71,7 +71,12 @@ export const exportOrganizationEvidenceTask = schemaTask({ // concurrencyLimit 1 + a per-org concurrencyKey (passed at trigger time) means // at most one export runs per org at a time; different orgs still run in parallel. queue: { name: 'evidence-export', concurrencyLimit: 1 }, - maxDuration: 60 * 30, + // maxDuration is max compute time in SECONDS. Orgs with high automation + // volume + large outputs were exceeding the old 30-minute budget, so + // Trigger.dev killed the run (retry maxAttempts: 0) — a non-COMPLETED + // terminal state the browser reports as a failed export with no download + // link. Give the single-threaded stream-to-S3 pass up to an hour to finish. + maxDuration: 60 * 60, retry: { maxAttempts: 0 }, schema: z.object({ organizationId: z.string(),