diff --git a/src/lib/jobs/__tests__/apple-health-import-reconcile.test.ts b/src/lib/jobs/__tests__/apple-health-import-reconcile.test.ts index 13e240628..219978aae 100644 --- a/src/lib/jobs/__tests__/apple-health-import-reconcile.test.ts +++ b/src/lib/jobs/__tests__/apple-health-import-reconcile.test.ts @@ -73,12 +73,16 @@ describe("periodic reconcile — wiring", () => { /const schedules[\s\S]*?=\s*\[([\s\S]*?)\];/, ); expect(schedules).not.toBeNull(); + // The third element pins the retry policy. It is load-bearing now that + // the handler reports a failed outcome instead of swallowing: without + // `cronIsTheRetry` a database outage would run the same doomed sweep + // three times per tick, and the next tick is already the retry. expect(schedules![1]).toMatch( - /\[IMPORT_JOB_RECONCILE_QUEUE,\s*IMPORT_JOB_RECONCILE_CRON\]/, + /\[IMPORT_JOB_RECONCILE_QUEUE,\s*IMPORT_JOB_RECONCILE_CRON,\s*cronIsTheRetry\]/, ); expect(maintenanceSource).toMatch( - /boss\.work\(\s*IMPORT_JOB_RECONCILE_QUEUE[\s\S]{0,120}handleImportJobReconcileTick/, + /createAndWork\(\s*boss,\s*IMPORT_JOB_RECONCILE_QUEUE[\s\S]{0,120}handleImportJobReconcileTick/, ); }); }); @@ -163,24 +167,30 @@ describe("reconcileOrphanImportJobs", () => { }); describe("handleImportJobReconcileTick", () => { - it("delegates to reconcileOrphanImportJobs and resolves on success", async () => { + it("delegates to reconcileOrphanImportJobs and reports a done outcome", async () => { mocks.findMany.mockResolvedValue([]); - await expect( - handleImportJobReconcileTick([] as never), - ).resolves.toBeUndefined(); + await expect(handleImportJobReconcileTick([] as never)).resolves.toEqual({ + ok: true, + did: {}, + }); expect(mocks.findMany).toHaveBeenCalledTimes(1); }); - it("swallows a reconcile failure instead of throwing", async () => { - mocks.findMany.mockRejectedValue(new Error("db unavailable")); + it("reports a failed outcome when the reconcile pass throws", async () => { + const cause = new Error("db unavailable"); + mocks.findMany.mockRejectedValue(cause); - // A background sweep this cheap must never spam pg-boss's - // retry/backoff machinery — it just re-runs on the next 15-minute - // tick. The handler must resolve even when the underlying reconcile - // pass throws. - await expect( - handleImportJobReconcileTick([] as never), - ).resolves.toBeUndefined(); + // The handler used to swallow this and resolve, which left a sweep + // that had not run for hours indistinguishable from one that found + // nothing to do. It now names the failure, `runJob` rethrows it, and + // pg-boss records a failed job. The queue carries `cronIsTheRetry` + // (retryLimit 0) because the next 15-minute tick is the retry — a + // database that is down stays down for an immediate re-run. + await expect(handleImportJobReconcileTick([] as never)).resolves.toEqual({ + ok: false, + reason: "apple health import reconcile failed", + cause, + }); }); }); diff --git a/src/lib/jobs/__tests__/apple-health-import-send-policy.test.ts b/src/lib/jobs/__tests__/apple-health-import-send-policy.test.ts index 5e5de5c6c..d28594276 100644 --- a/src/lib/jobs/__tests__/apple-health-import-send-policy.test.ts +++ b/src/lib/jobs/__tests__/apple-health-import-send-policy.test.ts @@ -87,11 +87,14 @@ describe("apple health import — parser revision boundary", () => { expect(allQueues).not.toBeNull(); expect(allQueues![1]).toMatch(/\bAPPLE_HEALTH_IMPORT_V2_QUEUE\b/); expect(allQueues![1]).toMatch(/\bAPPLE_HEALTH_IMPORT_LEGACY_QUEUE\b/); + // The v2 window is the wider of the two: its binding wraps the batch in + // a loop that keeps the first failed outcome, so the handler call sits + // further from the queue name than the legacy bridge's does. expect(maintenanceSource).toMatch( - /boss\.work[\s\S]{0,200}APPLE_HEALTH_IMPORT_V2_QUEUE[\s\S]{0,400}handleAppleHealthImport/, + /createAndWork[\s\S]{0,200}APPLE_HEALTH_IMPORT_V2_QUEUE[\s\S]{0,600}handleAppleHealthImport/, ); expect(maintenanceSource).toMatch( - /boss\.work[\s\S]{0,200}APPLE_HEALTH_IMPORT_LEGACY_QUEUE[\s\S]{0,400}migrateLegacyAppleHealthImport/, + /createAndWork[\s\S]{0,200}APPLE_HEALTH_IMPORT_LEGACY_QUEUE[\s\S]{0,400}migrateLegacyAppleHealthImport/, ); }); diff --git a/src/lib/jobs/__tests__/coach-memory-refresh-queue.test.ts b/src/lib/jobs/__tests__/coach-memory-refresh-queue.test.ts index a8ad973da..bb6147eab 100644 --- a/src/lib/jobs/__tests__/coach-memory-refresh-queue.test.ts +++ b/src/lib/jobs/__tests__/coach-memory-refresh-queue.test.ts @@ -4,7 +4,7 @@ * * Same source-text-grep approach as the period-narrative / stress-strain * guards: assert the queue is imported, registered in `allQueues`, and wired - * to a `boss.work` handler that runs the refresh pipeline — without booting + * to a `createAndWork` handler that runs the refresh pipeline — without booting * pg-boss + Prisma. An unregistered queue silently never runs (the recurring * past bug this guards against; v1.4.37 W10 caught exactly this class). */ @@ -41,9 +41,9 @@ describe("reminder-worker — coach-memory-refresh wiring", () => { expect(allQueuesBlock()).toMatch(/\bCOACH_MEMORY_REFRESH_QUEUE\b/); }); - it("registers a boss.work handler that runs the refresh pipeline", () => { + it("registers a createAndWork handler that runs the refresh pipeline", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}COACH_MEMORY_REFRESH_QUEUE[\s\S]{0,400}runCoachMemoryRefresh/, + /createAndWork[\s\S]{0,200}COACH_MEMORY_REFRESH_QUEUE[\s\S]{0,600}runCoachMemoryRefresh/, ); }); }); diff --git a/src/lib/jobs/__tests__/coach-message-cleanup-queue.test.ts b/src/lib/jobs/__tests__/coach-message-cleanup-queue.test.ts index e00d24206..0567b4e40 100644 --- a/src/lib/jobs/__tests__/coach-message-cleanup-queue.test.ts +++ b/src/lib/jobs/__tests__/coach-message-cleanup-queue.test.ts @@ -2,7 +2,7 @@ * v1.18.7 — coach-message-cleanup queue registration guard. * * Same source-text-grep approach as the other queue-wiring guards: assert the - * queue is registered in `allQueues`, scheduled, and wired to a `boss.work` + * queue is registered in `allQueues`, scheduled, and wired to a `createAndWork` * handler — without booting pg-boss + Prisma. An unregistered queue silently * never drains (the recurring v1.4.37 dead-queue bug), which would leave the * encrypted Coach history growing unbounded with the retention job dark. @@ -41,13 +41,13 @@ describe("reminder-worker — coach-message-cleanup wiring", () => { it("schedules the coach-message-cleanup cron", () => { expect(workerSource).toMatch( - /\[COACH_MESSAGE_CLEANUP_QUEUE,\s*COACH_MESSAGE_CLEANUP_CRON\]/, + /\[COACH_MESSAGE_CLEANUP_QUEUE,\s*COACH_MESSAGE_CLEANUP_CRON,\s*cronIsTheRetry\]/, ); }); - it("registers a boss.work handler for the queue", () => { + it("registers a createAndWork handler for the queue", () => { expect(workerSource).toMatch( - /boss\.work[\s\S]{0,200}COACH_MESSAGE_CLEANUP_QUEUE[\s\S]{0,200}handleCoachMessageCleanup/, + /createAndWork[\s\S]{0,200}COACH_MESSAGE_CLEANUP_QUEUE[\s\S]{0,200}handleCoachMessageCleanup/, ); }); diff --git a/src/lib/jobs/__tests__/cumulative-pr-rederive-queue.test.ts b/src/lib/jobs/__tests__/cumulative-pr-rederive-queue.test.ts index 61aa8c699..18a28299b 100644 --- a/src/lib/jobs/__tests__/cumulative-pr-rederive-queue.test.ts +++ b/src/lib/jobs/__tests__/cumulative-pr-rederive-queue.test.ts @@ -3,7 +3,7 @@ * registration guard. * * Same source-text-grep approach as the step-consolidation-repair guard: - * assert the queue is registered in `allQueues`, a `boss.work` handler is + * assert the queue is registered in `allQueues`, a `createAndWork` handler is * wired against it, and the boot-discovery enqueue helper fires — without * booting pg-boss + Prisma. An unregistered queue silently never drains * (the v1.4.37 dead-queue class). @@ -34,9 +34,9 @@ describe("reminder-worker — cumulative-pr-rederive wiring", () => { expect(allQueuesMatch![1]).toMatch(/\bCUMULATIVE_PR_REDERIVE_QUEUE\b/); }); - it("registers a boss.work handler against the queue", () => { + it("registers a createAndWork handler against the queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}CUMULATIVE_PR_REDERIVE_QUEUE[\s\S]{0,400}runCumulativePrRederivationForUser/, + /createAndWork[\s\S]{0,200}CUMULATIVE_PR_REDERIVE_QUEUE[\s\S]{0,400}runCumulativePrRederivationForUser/, ); }); diff --git a/src/lib/jobs/__tests__/cycle-reminder-queue.test.ts b/src/lib/jobs/__tests__/cycle-reminder-queue.test.ts index e060711e6..8c9158af2 100644 --- a/src/lib/jobs/__tests__/cycle-reminder-queue.test.ts +++ b/src/lib/jobs/__tests__/cycle-reminder-queue.test.ts @@ -5,7 +5,7 @@ * consolidation registration guards: assert the cycle-reminder queue is * declared, listed in `allQueues` (the v1.4.37 dead-queue lesson — an * unregistered queue is never provisioned and the schedule silently - * no-ops), scheduled on its cron, and wired to a `boss.work` handler that + * no-ops), scheduled on its cron, and wired to a `createAndWork` handler that * delegates to `runCycleReminderTick`. No pg-boss / Prisma boot. */ import { readFileSync } from "node:fs"; @@ -57,9 +57,9 @@ describe("reminder-worker — cycle-reminder wiring", () => { expect(source).toMatch(/\[CYCLE_REMINDER_QUEUE,\s*CYCLE_REMINDER_CRON\]/); }); - it("registers a boss.work handler that runs the cycle-reminder tick", () => { + it("registers a createAndWork handler that runs the cycle-reminder tick", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}CYCLE_REMINDER_QUEUE[\s\S]{0,200}handleCycleReminderCheck/, + /createAndWork[\s\S]{0,200}CYCLE_REMINDER_QUEUE[\s\S]{0,200}handleCycleReminderCheck/, ); expect(source).toMatch( /handleCycleReminderCheck[\s\S]{0,400}runCycleReminderTick/, diff --git a/src/lib/jobs/__tests__/daily-briefing.test.ts b/src/lib/jobs/__tests__/daily-briefing.test.ts index 8ae057493..ed34641db 100644 --- a/src/lib/jobs/__tests__/daily-briefing.test.ts +++ b/src/lib/jobs/__tests__/daily-briefing.test.ts @@ -98,9 +98,9 @@ describe("reminder-worker — daily-briefing wiring", () => { ); }); - it("wires a boss.work handler that runs the tick", () => { + it("wires a createAndWork handler that runs the tick", () => { expect(registrar).toMatch( - /boss\.work[\s\S]{0,200}DAILY_BRIEFING_QUEUE[\s\S]{0,400}runDailyBriefingTick/, + /createAndWork[\s\S]{0,200}DAILY_BRIEFING_QUEUE[\s\S]{0,400}runDailyBriefingTick/, ); }); diff --git a/src/lib/jobs/__tests__/data-arrival-queue.test.ts b/src/lib/jobs/__tests__/data-arrival-queue.test.ts index 9deeb634e..a86f8057f 100644 --- a/src/lib/jobs/__tests__/data-arrival-queue.test.ts +++ b/src/lib/jobs/__tests__/data-arrival-queue.test.ts @@ -39,9 +39,9 @@ describe("data-arrival queue wiring", () => { expect(allQueues![1]).toMatch(/\bDATA_ARRIVAL_QUEUE\b/); }); - it("binds a boss.work handler that drains it", () => { + it("binds a createAndWork handler that drains it", () => { expect(statusRegistrar).toMatch( - /boss\.work[\s\S]{0,200}DATA_ARRIVAL_QUEUE[\s\S]{0,200}handleDataArrival/, + /createAndWork[\s\S]{0,200}DATA_ARRIVAL_QUEUE[\s\S]{0,200}handleDataArrival/, ); }); @@ -61,7 +61,11 @@ describe("data-arrival queue wiring", () => { }); it("has no cron schedule — it is send-only, driven by ingest", () => { - expect(statusRegistrar).not.toMatch(/\[DATA_ARRIVAL_QUEUE,\s*\w+_CRON\]/); + // Tolerates the optional third schedule element, so a cron sneaking in + // with send options still trips this. + expect(statusRegistrar).not.toMatch( + /\[\s*DATA_ARRIVAL_QUEUE,\s*\w+_CRON\s*[,\]]/, + ); }); it("registers the retention job that prunes the markers", () => { @@ -71,10 +75,10 @@ describe("data-arrival queue wiring", () => { expect(allQueues).not.toBeNull(); expect(allQueues![1]).toMatch(/\bARRIVAL_REACTION_CLEANUP_QUEUE\b/); expect(maintenanceSource).toMatch( - /\[ARRIVAL_REACTION_CLEANUP_QUEUE,\s*ARRIVAL_REACTION_CLEANUP_CRON\]/, + /\[\s*ARRIVAL_REACTION_CLEANUP_QUEUE,\s*ARRIVAL_REACTION_CLEANUP_CRON,\s*cronIsTheRetry,?\s*\]/, ); expect(maintenanceSource).toMatch( - /boss\.work[\s\S]{0,200}ARRIVAL_REACTION_CLEANUP_QUEUE[\s\S]{0,200}handleArrivalReactionCleanup/, + /createAndWork[\s\S]{0,200}ARRIVAL_REACTION_CLEANUP_QUEUE[\s\S]{0,200}handleArrivalReactionCleanup/, ); }); diff --git a/src/lib/jobs/__tests__/drain-cumulative-queue.test.ts b/src/lib/jobs/__tests__/drain-cumulative-queue.test.ts index 4bfc499cb..b904a30dd 100644 --- a/src/lib/jobs/__tests__/drain-cumulative-queue.test.ts +++ b/src/lib/jobs/__tests__/drain-cumulative-queue.test.ts @@ -50,15 +50,15 @@ describe("reminder-worker — drainPerSampleCumulative nightly schedule", () => expect(source).toMatch(/\bDRAIN_CUMULATIVE_CUTOFF_HOURS\b/); }); - it("registers a boss.work handler against the drain queue", () => { + it("registers a createAndWork handler against the drain queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}DRAIN_CUMULATIVE_QUEUE[\s\S]{0,400}drainPerSampleCumulative/, + /createAndWork[\s\S]{0,200}DRAIN_CUMULATIVE_QUEUE[\s\S]{0,900}drainPerSampleCumulative/, ); }); it("schedules the drain cron via boss.schedule (allQueues + schedules)", () => { expect(source).toMatch( - /\[DRAIN_CUMULATIVE_QUEUE,\s*DRAIN_CUMULATIVE_CRON\]/, + /\[DRAIN_CUMULATIVE_QUEUE,\s*DRAIN_CUMULATIVE_CRON,\s*cronIsTheRetry\]/, ); }); diff --git a/src/lib/jobs/__tests__/fitbit-queues.test.ts b/src/lib/jobs/__tests__/fitbit-queues.test.ts index 3e6145540..a9e9fbbc6 100644 --- a/src/lib/jobs/__tests__/fitbit-queues.test.ts +++ b/src/lib/jobs/__tests__/fitbit-queues.test.ts @@ -58,22 +58,22 @@ describe("reminder-worker — Fitbit sync queues", () => { it("schedules the hourly poll cron + the daily oauth-state sweep", () => { expect(source).toMatch(/\[FITBIT_SYNC_QUEUE,\s*FITBIT_SYNC_CRON\]/); expect(source).toMatch( - /\[FITBIT_OAUTH_STATE_CLEANUP_QUEUE,\s*FITBIT_OAUTH_STATE_CLEANUP_CRON\]/, + /\[\s*FITBIT_OAUTH_STATE_CLEANUP_QUEUE,\s*FITBIT_OAUTH_STATE_CLEANUP_CRON,\s*cronIsTheRetry,?\s*\]/, ); }); - it("registers a boss.work handler for every Fitbit queue", () => { + it("registers a createAndWork handler for every Fitbit queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,160}FITBIT_SYNC_QUEUE[\s\S]{0,160}handleFitbitSync/, + /createAndWork[\s\S]{0,160}FITBIT_SYNC_QUEUE[\s\S]{0,160}handleFitbitSync/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,200}FITBIT_BACKFILL_QUEUE[\s\S]{0,260}enqueueIntegrationBackfillAdmission/, + /createAndWork[\s\S]{0,200}FITBIT_BACKFILL_QUEUE[\s\S]{0,260}enqueueIntegrationBackfillAdmission/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,200}FITBIT_SLEEP_REPAIR_QUEUE[\s\S]{0,260}enqueueIntegrationBackfillAdmission/, + /createAndWork[\s\S]{0,200}FITBIT_SLEEP_REPAIR_QUEUE[\s\S]{0,260}enqueueIntegrationBackfillAdmission/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,160}FITBIT_OAUTH_STATE_CLEANUP_QUEUE[\s\S]{0,160}handleFitbitOAuthStateCleanup/, + /createAndWork[\s\S]{0,160}FITBIT_OAUTH_STATE_CLEANUP_QUEUE[\s\S]{0,160}handleFitbitOAuthStateCleanup/, ); }); diff --git a/src/lib/jobs/__tests__/google-health-queues.test.ts b/src/lib/jobs/__tests__/google-health-queues.test.ts index 179091be9..783593da4 100644 --- a/src/lib/jobs/__tests__/google-health-queues.test.ts +++ b/src/lib/jobs/__tests__/google-health-queues.test.ts @@ -61,19 +61,19 @@ describe("reminder-worker — Google Health sync queues", () => { /\[GOOGLE_HEALTH_SYNC_QUEUE,\s*GOOGLE_HEALTH_SYNC_CRON\]/, ); expect(source).toMatch( - /\[\s*GOOGLE_HEALTH_OAUTH_STATE_CLEANUP_QUEUE,\s*GOOGLE_HEALTH_OAUTH_STATE_CLEANUP_CRON,?\s*\]/, + /\[\s*GOOGLE_HEALTH_OAUTH_STATE_CLEANUP_QUEUE,\s*GOOGLE_HEALTH_OAUTH_STATE_CLEANUP_CRON,\s*cronIsTheRetry,?\s*\]/, ); }); - it("registers a boss.work handler for every Google Health queue", () => { + it("registers a createAndWork handler for every Google Health queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}GOOGLE_HEALTH_SYNC_QUEUE[\s\S]{0,200}handleGoogleHealthSync/, + /createAndWork[\s\S]{0,200}GOOGLE_HEALTH_SYNC_QUEUE[\s\S]{0,200}handleGoogleHealthSync/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,320}GOOGLE_HEALTH_BACKFILL_QUEUE[\s\S]{0,320}enqueueIntegrationBackfillAdmission/, + /createAndWork[\s\S]{0,320}GOOGLE_HEALTH_BACKFILL_QUEUE[\s\S]{0,320}enqueueIntegrationBackfillAdmission/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,200}GOOGLE_HEALTH_OAUTH_STATE_CLEANUP_QUEUE[\s\S]{0,200}handleGoogleHealthOAuthStateCleanup/, + /createAndWork[\s\S]{0,200}GOOGLE_HEALTH_OAUTH_STATE_CLEANUP_QUEUE[\s\S]{0,200}handleGoogleHealthOAuthStateCleanup/, ); }); diff --git a/src/lib/jobs/__tests__/insight-pregenerate.test.ts b/src/lib/jobs/__tests__/insight-pregenerate.test.ts index e8bb06131..38c92003e 100644 --- a/src/lib/jobs/__tests__/insight-pregenerate.test.ts +++ b/src/lib/jobs/__tests__/insight-pregenerate.test.ts @@ -1181,9 +1181,9 @@ describe("queue registration", () => { ); }); - it("registers a boss.work handler for the queue", () => { + it("registers a createAndWork handler for the queue", () => { expect(workerSrc).toMatch( - /boss\.work[\s\S]{0,80}INSIGHT_PREGENERATE_QUEUE/, + /createAndWork[\s\S]{0,80}INSIGHT_PREGENERATE_QUEUE/, ); }); diff --git a/src/lib/jobs/__tests__/insight-status-generate.test.ts b/src/lib/jobs/__tests__/insight-status-generate.test.ts index fe4a9b116..06ab5ed4e 100644 --- a/src/lib/jobs/__tests__/insight-status-generate.test.ts +++ b/src/lib/jobs/__tests__/insight-status-generate.test.ts @@ -6,7 +6,7 @@ * - an unknown metric is annotated and skipped, not thrown; * - the enqueue helper no-ops without a global boss instance and de-dupes * with a singletonKey when one is present; - * - the queue is registered in `allQueues` AND wired to a `boss.work` + * - the queue is registered in `allQueues` AND wired to a `createAndWork` * handler in reminder-worker.ts (the v1.4.37 W10 unregistered-queue * catch — a queue built but never registered silently never drains). */ @@ -171,9 +171,9 @@ describe("queue registration", () => { expect(match![1]).toMatch(/\bINSIGHT_STATUS_GENERATE_QUEUE\b/); }); - it("registers a boss.work handler for the queue", () => { + it("registers a createAndWork handler for the queue", () => { expect(workerSrc).toMatch( - /boss\.work[\s\S]{0,120}INSIGHT_STATUS_GENERATE_QUEUE/, + /createAndWork[\s\S]{0,120}INSIGHT_STATUS_GENERATE_QUEUE/, ); }); diff --git a/src/lib/jobs/__tests__/intake-slot-dedup-queue.test.ts b/src/lib/jobs/__tests__/intake-slot-dedup-queue.test.ts index 16e1ae0ad..a99672474 100644 --- a/src/lib/jobs/__tests__/intake-slot-dedup-queue.test.ts +++ b/src/lib/jobs/__tests__/intake-slot-dedup-queue.test.ts @@ -3,7 +3,7 @@ * * Same source-text-grep approach as the drain + step-consolidation + * mean-consolidation regression guards: assert the queue is registered - * in `allQueues`, a `boss.work` handler is wired against it, and the + * in `allQueues`, a `createAndWork` handler is wired against it, and the * boot-discovery enqueue helper is called — without booting pg-boss + * Prisma. An unregistered queue silently never drains (the past incident * this guard exists to prevent). @@ -42,12 +42,12 @@ describe("reminder-worker — intake-slot-dedup wiring", () => { expect(allQueuesMatch![1]).toMatch(/\bINTAKE_SLOT_DEDUP_QUEUE\b/); }); - it("registers a boss.work handler against the intake-slot-dedup queue", () => { + it("registers a createAndWork handler against the intake-slot-dedup queue", () => { // v1.15.19 — span widened from {0,400}: the handler gained the daily // cron-tick dispatch branch (no `userId` → discovery fan-out) between // the queue name and the per-user dedup call. expect(source).toMatch( - /boss\.work[\s\S]{0,200}INTAKE_SLOT_DEDUP_QUEUE[\s\S]{0,1200}dedupeUserIntakeSlots/, + /createAndWork[\s\S]{0,200}INTAKE_SLOT_DEDUP_QUEUE[\s\S]{0,1400}dedupeUserIntakeSlots/, ); }); diff --git a/src/lib/jobs/__tests__/mcp-token-cleanup-queue.test.ts b/src/lib/jobs/__tests__/mcp-token-cleanup-queue.test.ts index 6215ec22f..7f42958a7 100644 --- a/src/lib/jobs/__tests__/mcp-token-cleanup-queue.test.ts +++ b/src/lib/jobs/__tests__/mcp-token-cleanup-queue.test.ts @@ -2,7 +2,7 @@ * MCP Phase 3 (M2) — mcp-token-cleanup queue registration guard. * * Source-text-grep guard like the other queue-wiring checks: assert the queue - * is in `allQueues`, scheduled, and wired to a `boss.work` handler. An + * is in `allQueues`, scheduled, and wired to a `createAndWork` handler. An * unregistered queue silently never drains (the recurring v1.4.37 dead-queue * bug), which would leave expired MCP connector access tokens growing unbounded. */ @@ -40,13 +40,13 @@ describe("reminder-worker — mcp-token-cleanup wiring", () => { it("schedules the mcp-token-cleanup cron", () => { expect(workerSource).toMatch( - /\[MCP_TOKEN_CLEANUP_QUEUE,\s*MCP_TOKEN_CLEANUP_CRON\]/, + /\[MCP_TOKEN_CLEANUP_QUEUE,\s*MCP_TOKEN_CLEANUP_CRON,\s*cronIsTheRetry\]/, ); }); - it("registers a boss.work handler for the queue", () => { + it("registers a createAndWork handler for the queue", () => { expect(workerSource).toMatch( - /boss\.work[\s\S]{0,200}MCP_TOKEN_CLEANUP_QUEUE[\s\S]{0,200}handleMcpTokenCleanup/, + /createAndWork[\s\S]{0,200}MCP_TOKEN_CLEANUP_QUEUE[\s\S]{0,200}handleMcpTokenCleanup/, ); }); diff --git a/src/lib/jobs/__tests__/mean-consolidation-queue.test.ts b/src/lib/jobs/__tests__/mean-consolidation-queue.test.ts index 3b5956ec6..4dbfd6d11 100644 --- a/src/lib/jobs/__tests__/mean-consolidation-queue.test.ts +++ b/src/lib/jobs/__tests__/mean-consolidation-queue.test.ts @@ -3,7 +3,7 @@ * * Same source-text-grep approach as the drain + step-consolidation * regression guards: assert the queue is registered in `allQueues`, a - * `boss.work` handler is wired against it, and the boot-discovery + * `createAndWork` handler is wired against it, and the boot-discovery * enqueue helper is called — without booting pg-boss + Prisma. An * unregistered queue silently never drains. */ @@ -34,9 +34,9 @@ describe("reminder-worker — mean-consolidation wiring", () => { expect(allQueuesMatch![1]).toMatch(/\bMEAN_CONSOLIDATION_QUEUE\b/); }); - it("registers a boss.work handler against the mean-consolidation queue", () => { + it("registers a createAndWork handler against the mean-consolidation queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}MEAN_CONSOLIDATION_QUEUE[\s\S]{0,400}runMeanConsolidationForUser/, + /createAndWork[\s\S]{0,200}MEAN_CONSOLIDATION_QUEUE[\s\S]{0,400}runMeanConsolidationForUser/, ); }); diff --git a/src/lib/jobs/__tests__/measurement-reminder-queue.test.ts b/src/lib/jobs/__tests__/measurement-reminder-queue.test.ts index 9c36703d4..564b9cb95 100644 --- a/src/lib/jobs/__tests__/measurement-reminder-queue.test.ts +++ b/src/lib/jobs/__tests__/measurement-reminder-queue.test.ts @@ -5,7 +5,7 @@ * guards: assert the Vorsorge-reminder queue is declared, listed in * `allQueues` (the v1.4.37 dead-queue lesson — an unregistered queue is * never provisioned and the schedule silently no-ops), scheduled on its - * cron, and wired to a `boss.work` handler that delegates to + * cron, and wired to a `createAndWork` handler that delegates to * `runMeasurementReminderTick`. No pg-boss / Prisma boot. */ import { readFileSync } from "node:fs"; @@ -56,9 +56,9 @@ describe("reminder-worker — measurement-reminder wiring", () => { ); }); - it("registers a boss.work handler that runs the tick", () => { + it("registers a createAndWork handler that runs the tick", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}MEASUREMENT_REMINDER_QUEUE[\s\S]{0,200}handleMeasurementReminderCheck/, + /createAndWork[\s\S]{0,200}MEASUREMENT_REMINDER_QUEUE[\s\S]{0,200}handleMeasurementReminderCheck/, ); expect(source).toMatch( /handleMeasurementReminderCheck[\s\S]{0,400}runMeasurementReminderTick/, @@ -80,12 +80,12 @@ describe("reminder-worker — eventful reminder-satisfy wiring (v1.18.1)", () => expect(allQueuesMatch![1]).toMatch(/\bREMINDER_SATISFY_QUEUE\b/); }); - it("binds a boss.work handler for the satisfy queue", () => { + it("binds a createAndWork handler for the satisfy queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}REMINDER_SATISFY_QUEUE[\s\S]{0,200}handleReminderSatisfy/, + /createAndWork[\s\S]{0,200}REMINDER_SATISFY_QUEUE[\s\S]{0,200}handleReminderSatisfy/, ); expect(source).toMatch( - /handleReminderSatisfy[\s\S]{0,600}runReminderSatisfyForUser/, + /handleReminderSatisfy[\s\S]{0,800}runReminderSatisfyForUser/, ); }); }); diff --git a/src/lib/jobs/__tests__/measurement-tombstone-cleanup.test.ts b/src/lib/jobs/__tests__/measurement-tombstone-cleanup.test.ts index ed96efe98..fa68a9100 100644 --- a/src/lib/jobs/__tests__/measurement-tombstone-cleanup.test.ts +++ b/src/lib/jobs/__tests__/measurement-tombstone-cleanup.test.ts @@ -7,7 +7,7 @@ * recently-tombstoned rows untouched. * 2. Source-grep wiring (same approach as the drain-cumulative guard): * the queue is registered in `allQueues`, scheduled, and bound to a - * `boss.work` handler — a missing `allQueues` entry silently no-ops + * `createAndWork` handler — a missing `allQueues` entry silently no-ops * the schedule under pg-boss v12. */ import { readFileSync } from "node:fs"; @@ -114,9 +114,9 @@ describe("reminder-worker — measurement-tombstone-cleanup schedule", () => { ); }); - it("binds a boss.work handler to the queue", () => { + it("binds a createAndWork handler to the queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}MEASUREMENT_TOMBSTONE_CLEANUP_QUEUE[\s\S]{0,200}handleMeasurementTombstoneCleanup/, + /createAndWork[\s\S]{0,200}MEASUREMENT_TOMBSTONE_CLEANUP_QUEUE[\s\S]{0,200}handleMeasurementTombstoneCleanup/, ); }); }); diff --git a/src/lib/jobs/__tests__/nightscout-queues.test.ts b/src/lib/jobs/__tests__/nightscout-queues.test.ts index 5873b8382..6c994d55a 100644 --- a/src/lib/jobs/__tests__/nightscout-queues.test.ts +++ b/src/lib/jobs/__tests__/nightscout-queues.test.ts @@ -50,9 +50,9 @@ describe("reminder-worker — Nightscout sync queue", () => { expect(source).toMatch(/\[NIGHTSCOUT_SYNC_QUEUE,\s*NIGHTSCOUT_SYNC_CRON\]/); }); - it("registers a boss.work handler for the queue", () => { + it("registers a createAndWork handler for the queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,160}NIGHTSCOUT_SYNC_QUEUE[\s\S]{0,160}handleNightscoutSync/, + /createAndWork[\s\S]{0,160}NIGHTSCOUT_SYNC_QUEUE[\s\S]{0,160}handleNightscoutSync/, ); }); }); @@ -85,12 +85,12 @@ describe("reminder-worker — Polar / Oura sync queues", () => { expect(source).toMatch(/\[OURA_SYNC_QUEUE,\s*OURA_SYNC_CRON\]/); }); - it("registers a boss.work handler for each queue", () => { + it("registers a createAndWork handler for each queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,160}POLAR_SYNC_QUEUE[\s\S]{0,160}handlePolarSync/, + /createAndWork[\s\S]{0,160}POLAR_SYNC_QUEUE[\s\S]{0,160}handlePolarSync/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,160}OURA_SYNC_QUEUE[\s\S]{0,160}handleOuraSync/, + /createAndWork[\s\S]{0,160}OURA_SYNC_QUEUE[\s\S]{0,160}handleOuraSync/, ); }); }); diff --git a/src/lib/jobs/__tests__/period-narrative-warm.test.ts b/src/lib/jobs/__tests__/period-narrative-warm.test.ts index 45838182e..5c9e6ea83 100644 --- a/src/lib/jobs/__tests__/period-narrative-warm.test.ts +++ b/src/lib/jobs/__tests__/period-narrative-warm.test.ts @@ -196,8 +196,10 @@ describe("queue registration", () => { ); }); - it("registers a boss.work handler for the queue", () => { - expect(workerSrc).toMatch(/boss\.work[\s\S]{0,120}PERIOD_NARRATIVE_QUEUE/); + it("registers a createAndWork handler for the queue", () => { + expect(workerSrc).toMatch( + /createAndWork[\s\S]{0,120}PERIOD_NARRATIVE_QUEUE/, + ); }); it("exposes a sane queue name + nightly cron", () => { diff --git a/src/lib/jobs/__tests__/recovery-score-queue.test.ts b/src/lib/jobs/__tests__/recovery-score-queue.test.ts index 5e7a15866..65628b9e9 100644 --- a/src/lib/jobs/__tests__/recovery-score-queue.test.ts +++ b/src/lib/jobs/__tests__/recovery-score-queue.test.ts @@ -3,7 +3,7 @@ * * Same source-text-grep approach as the other queue-wiring guards: assert * the queue is imported, registered in `allQueues`, scheduled, and wired to - * a `boss.work` handler — without booting pg-boss + Prisma. An unregistered + * a `createAndWork` handler — without booting pg-boss + Prisma. An unregistered * queue silently never drains (the recurring past bug this guards against). * * Also pins the iOS-write rejection of the server-owned COMPUTED source on @@ -57,9 +57,9 @@ describe("reminder-worker — recovery-score wiring", () => { ); }); - it("registers a boss.work handler that runs the recovery-score pass", () => { + it("registers a createAndWork handler that runs the recovery-score pass", () => { expect(workerSource).toMatch( - /boss\.work[\s\S]{0,200}RECOVERY_SCORE_QUEUE[\s\S]{0,400}runRecoveryScore/, + /createAndWork[\s\S]{0,200}RECOVERY_SCORE_QUEUE[\s\S]{0,400}runRecoveryScore/, ); }); }); diff --git a/src/lib/jobs/__tests__/sleep-timeline-queues.test.ts b/src/lib/jobs/__tests__/sleep-timeline-queues.test.ts index c6375c871..cc803ecc5 100644 --- a/src/lib/jobs/__tests__/sleep-timeline-queues.test.ts +++ b/src/lib/jobs/__tests__/sleep-timeline-queues.test.ts @@ -3,7 +3,7 @@ * * The reminder-worker imports heavy infrastructure, so we read the source as * text and assert the queue constant is declared, registered in `allQueues`, - * wired to a `boss.work` handler, and enqueued at boot — the fast guard that + * wired to a `createAndWork` handler, and enqueued at boot — the fast guard that * catches the v1.4.37 dead-queue class (a queue declared but never registered, * which silently never drains). */ @@ -30,9 +30,9 @@ describe("reminder-worker — sleep-timeline backfill queue", () => { expect(block![1]!).toContain("SLEEP_TIMELINE_BACKFILL_QUEUE"); }); - it("wires a boss.work handler through the shared admission queue", () => { + it("wires a createAndWork handler through the shared admission queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,260}SLEEP_TIMELINE_BACKFILL_QUEUE[\s\S]{0,300}enqueueIntegrationBackfillAdmission/, + /createAndWork[\s\S]{0,260}SLEEP_TIMELINE_BACKFILL_QUEUE[\s\S]{0,300}enqueueIntegrationBackfillAdmission/, ); }); diff --git a/src/lib/jobs/__tests__/status-cron-candidates.test.ts b/src/lib/jobs/__tests__/status-cron-candidates.test.ts index 15c13baa8..4f2690a1f 100644 --- a/src/lib/jobs/__tests__/status-cron-candidates.test.ts +++ b/src/lib/jobs/__tests__/status-cron-candidates.test.ts @@ -196,8 +196,8 @@ describe("mood-status queue registration (dead-queue guard)", () => { expect(workerSrc).toMatch(/MOOD_STATUS_CRON\s*=\s*"30 2 \* \* \*"/); }); - it("registers a boss.work handler for the queue", () => { - expect(workerSrc).toMatch(/boss\.work[\s\S]{0,120}MOOD_STATUS_QUEUE/); + it("registers a createAndWork handler for the queue", () => { + expect(workerSrc).toMatch(/createAndWork[\s\S]{0,120}MOOD_STATUS_QUEUE/); }); it("drives all seven status crons through the shared discovery", () => { diff --git a/src/lib/jobs/__tests__/step-consolidation-repair-queue.test.ts b/src/lib/jobs/__tests__/step-consolidation-repair-queue.test.ts index 8e0eeab75..e34c295cd 100644 --- a/src/lib/jobs/__tests__/step-consolidation-repair-queue.test.ts +++ b/src/lib/jobs/__tests__/step-consolidation-repair-queue.test.ts @@ -4,7 +4,7 @@ * * Same source-text-grep approach as the drain + step-consolidation + * mean-consolidation guards: assert the repair queue is registered in - * `allQueues`, a `boss.work` handler is wired against it, and the + * `allQueues`, a `createAndWork` handler is wired against it, and the * boot-discovery enqueue helper fires — without booting pg-boss + Prisma. * An unregistered queue silently never drains (the dead-queue class). */ @@ -36,9 +36,9 @@ describe("reminder-worker — step-consolidation-repair wiring", () => { expect(allQueuesMatch![1]).toMatch(/\bSTEP_CONSOLIDATION_REPAIR_QUEUE\b/); }); - it("registers a boss.work handler against the repair queue", () => { + it("registers a createAndWork handler against the repair queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}STEP_CONSOLIDATION_REPAIR_QUEUE[\s\S]{0,400}runStepConsolidationRepairForUser/, + /createAndWork[\s\S]{0,200}STEP_CONSOLIDATION_REPAIR_QUEUE[\s\S]{0,400}runStepConsolidationRepairForUser/, ); }); diff --git a/src/lib/jobs/__tests__/stress-strain-retention-queue.test.ts b/src/lib/jobs/__tests__/stress-strain-retention-queue.test.ts index 839c2b575..969d80fba 100644 --- a/src/lib/jobs/__tests__/stress-strain-retention-queue.test.ts +++ b/src/lib/jobs/__tests__/stress-strain-retention-queue.test.ts @@ -5,7 +5,7 @@ * * Same source-text-grep approach as the WX-C recovery-score guard: assert * each queue is imported, registered in `allQueues`, scheduled (the two - * cron-driven score jobs), and wired to a `boss.work` handler — without + * cron-driven score jobs), and wired to a `createAndWork` handler — without * booting pg-boss + Prisma. An unregistered queue silently never drains * (the recurring past bug this guards against). */ @@ -67,9 +67,9 @@ describe("reminder-worker — stress-score wiring", () => { expect(source).toMatch(/\[STRESS_SCORE_QUEUE,\s*STRESS_SCORE_CRON\]/); }); - it("registers a boss.work handler that runs the stress-score pass", () => { + it("registers a createAndWork handler that runs the stress-score pass", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}STRESS_SCORE_QUEUE[\s\S]{0,400}runStressScore/, + /createAndWork[\s\S]{0,200}STRESS_SCORE_QUEUE[\s\S]{0,400}runStressScore/, ); }); }); @@ -90,9 +90,9 @@ describe("reminder-worker — strain-score wiring", () => { expect(source).toMatch(/\[STRAIN_SCORE_QUEUE,\s*STRAIN_SCORE_CRON\]/); }); - it("registers a boss.work handler that runs the strain-score pass", () => { + it("registers a createAndWork handler that runs the strain-score pass", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,200}STRAIN_SCORE_QUEUE[\s\S]{0,400}runStrainScore/, + /createAndWork[\s\S]{0,200}STRAIN_SCORE_QUEUE[\s\S]{0,400}runStrainScore/, ); }); }); @@ -111,9 +111,9 @@ describe("reminder-worker — dense intra-day retention wiring", () => { expect(allQueuesBlock()).toMatch(/\bDENSE_INTRADAY_RETENTION_QUEUE\b/); }); - it("registers a boss.work handler against the dense-intraday-retention queue", () => { + it("registers a createAndWork handler against the dense-intraday-retention queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,250}DENSE_INTRADAY_RETENTION_QUEUE[\s\S]{0,500}runDenseIntradayRetentionForUser/, + /createAndWork[\s\S]{0,250}DENSE_INTRADAY_RETENTION_QUEUE[\s\S]{0,500}runDenseIntradayRetentionForUser/, ); }); @@ -163,9 +163,9 @@ describe("reminder-worker — dense hourly-rebuild wiring", () => { expect(allQueuesBlock()).toMatch(/\bDENSE_INTRADAY_HOURLY_REBUILD_QUEUE\b/); }); - it("registers a boss.work handler against the hourly-rebuild queue", () => { + it("registers a createAndWork handler against the hourly-rebuild queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,250}DENSE_INTRADAY_HOURLY_REBUILD_QUEUE[\s\S]{0,500}runDenseIntradayHourlyRebuildForUser/, + /createAndWork[\s\S]{0,250}DENSE_INTRADAY_HOURLY_REBUILD_QUEUE[\s\S]{0,500}runDenseIntradayHourlyRebuildForUser/, ); }); diff --git a/src/lib/jobs/__tests__/tls-pin-monitor-queue.test.ts b/src/lib/jobs/__tests__/tls-pin-monitor-queue.test.ts index 68764d932..9fe67d0b0 100644 --- a/src/lib/jobs/__tests__/tls-pin-monitor-queue.test.ts +++ b/src/lib/jobs/__tests__/tls-pin-monitor-queue.test.ts @@ -3,7 +3,7 @@ * * Same source-text-grep approach as the other queue-wiring guards: assert * the queue is imported, registered in `allQueues`, scheduled, and wired to - * a `boss.work` handler — without booting pg-boss + Prisma. An unregistered + * a `createAndWork` handler — without booting pg-boss + Prisma. An unregistered * queue silently never drains (the recurring v1.4.37 dead-queue bug), which * would leave the pinned-leaf-rotation alarm dark. */ @@ -50,9 +50,9 @@ describe("reminder-worker — tls-pin-monitor wiring", () => { ); }); - it("registers a boss.work handler for the queue", () => { + it("registers a createAndWork handler for the queue", () => { expect(workerSource).toMatch( - /boss\.work[\s\S]{0,200}TLS_PIN_MONITOR_QUEUE[\s\S]{0,200}handleTlsPinMonitor/, + /createAndWork[\s\S]{0,200}TLS_PIN_MONITOR_QUEUE[\s\S]{0,200}handleTlsPinMonitor/, ); }); diff --git a/src/lib/jobs/__tests__/whoop-queues.test.ts b/src/lib/jobs/__tests__/whoop-queues.test.ts index 7d2c514c1..a1ad468b4 100644 --- a/src/lib/jobs/__tests__/whoop-queues.test.ts +++ b/src/lib/jobs/__tests__/whoop-queues.test.ts @@ -77,25 +77,25 @@ describe("reminder-worker — WHOOP sync queues", () => { /\[WHOOP_CYCLE_SYNC_QUEUE,\s*WHOOP_CYCLE_SYNC_CRON\]/, ); expect(source).toMatch( - /\[WHOOP_OAUTH_STATE_CLEANUP_QUEUE,\s*WHOOP_OAUTH_STATE_CLEANUP_CRON\]/, + /\[\s*WHOOP_OAUTH_STATE_CLEANUP_QUEUE,\s*WHOOP_OAUTH_STATE_CLEANUP_CRON,\s*cronIsTheRetry,?\s*\]/, ); }); - it("registers a boss.work handler for every WHOOP sync queue", () => { + it("registers a createAndWork handler for every WHOOP sync queue", () => { expect(source).toMatch( - /boss\.work[\s\S]{0,160}WHOOP_RECOVERY_SYNC_QUEUE[\s\S]{0,160}handleWhoopRecoverySync/, + /createAndWork[\s\S]{0,160}WHOOP_RECOVERY_SYNC_QUEUE[\s\S]{0,160}handleWhoopRecoverySync/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,160}WHOOP_SLEEP_SYNC_QUEUE[\s\S]{0,160}handleWhoopSleepSync/, + /createAndWork[\s\S]{0,160}WHOOP_SLEEP_SYNC_QUEUE[\s\S]{0,160}handleWhoopSleepSync/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,160}WHOOP_WORKOUT_SYNC_QUEUE[\s\S]{0,160}handleWhoopWorkoutSync/, + /createAndWork[\s\S]{0,160}WHOOP_WORKOUT_SYNC_QUEUE[\s\S]{0,160}handleWhoopWorkoutSync/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,160}WHOOP_CYCLE_SYNC_QUEUE[\s\S]{0,160}handleWhoopCycleSync/, + /createAndWork[\s\S]{0,160}WHOOP_CYCLE_SYNC_QUEUE[\s\S]{0,160}handleWhoopCycleSync/, ); expect(source).toMatch( - /boss\.work[\s\S]{0,200}WHOOP_BACKFILL_QUEUE[\s\S]{0,260}enqueueIntegrationBackfillAdmission/, + /createAndWork[\s\S]{0,200}WHOOP_BACKFILL_QUEUE[\s\S]{0,260}enqueueIntegrationBackfillAdmission/, ); }); diff --git a/src/lib/jobs/__tests__/whoop-resource-sync.test.ts b/src/lib/jobs/__tests__/whoop-resource-sync.test.ts index 6359d2ecb..cbe181b6d 100644 --- a/src/lib/jobs/__tests__/whoop-resource-sync.test.ts +++ b/src/lib/jobs/__tests__/whoop-resource-sync.test.ts @@ -128,7 +128,18 @@ describe("runWhoopResourceSync dispatch", () => { findManyMock.mockResolvedValue([{ userId: "a" }, { userId: "b" }]); syncUserWorkout.mockRejectedValueOnce(new Error("boom for a")); - await expect(handleWhoopWorkoutSync([job({})])).resolves.toBeUndefined(); + // The pass is what the outcome judges: the sweep ran, so the job is + // done. The revoked-grant user rides out as `users_failed` rather than + // failing the queue and retrying the whole cohort over one account. + await expect(handleWhoopWorkoutSync([job({})])).resolves.toEqual({ + ok: true, + did: { + total: 2, + users_synced: 1, + users_failed: 1, + measurements_imported: 0, + }, + }); // Both users were attempted despite the first throwing. expect(syncUserWorkout).toHaveBeenCalledTimes(2); }); diff --git a/src/lib/jobs/__tests__/withings-ecg-worker.test.ts b/src/lib/jobs/__tests__/withings-ecg-worker.test.ts index cb11bde9e..e40bf868c 100644 --- a/src/lib/jobs/__tests__/withings-ecg-worker.test.ts +++ b/src/lib/jobs/__tests__/withings-ecg-worker.test.ts @@ -93,7 +93,15 @@ describe("handleWithingsEcgSync", () => { enddate: 1715000060, }); - await expect(handleWithingsEcgSync([queued])).resolves.toBeUndefined(); + await expect(handleWithingsEcgSync([queued])).resolves.toEqual({ + ok: true, + did: { + total: 1, + users_synced: 1, + users_failed: 0, + recordings_imported: 1, + }, + }); expect(ecgFindMany).not.toHaveBeenCalled(); expect(syncUserEcg).toHaveBeenCalledTimes(1); @@ -120,14 +128,24 @@ describe("handleWithingsEcgSync", () => { it("one user's failure never starves the rest of the cohort", async () => { ecgFindMany.mockResolvedValue([{ userId: "a" }, { userId: "b" }]); + syncUserEcg.mockResolvedValue(0); syncUserEcg.mockRejectedValueOnce(new Error("boom for a")); // Per-user catch-and-warn (mirrors the activity / sleep handlers): the - // handler resolves, both users attempted, the leaf records its own ledger - // failure so honesty is preserved without rejecting the whole batch. + // pass ran, so the job is done and the failing user rides out as a + // `users_failed` count. The leaf records its own ledger failure, so + // honesty is preserved without rejecting the whole batch. await expect( handleWithingsEcgSync([job({} as WithingsEcgSyncPayload)]), - ).resolves.toBeUndefined(); + ).resolves.toEqual({ + ok: true, + did: { + total: 2, + users_synced: 1, + users_failed: 1, + recordings_imported: 0, + }, + }); expect(syncUserEcg).toHaveBeenCalledTimes(2); }); }); @@ -143,7 +161,7 @@ describe("withings ECG queue registration", () => { /\[WITHINGS_ECG_SYNC_QUEUE\]:\s*\{[\s\S]*?policy:\s*"short"/, ); expect(registrarSource).toMatch( - /boss\.work\([\s\S]*?WITHINGS_ECG_SYNC_QUEUE[\s\S]*?handleWithingsEcgSync/, + /createAndWork\(\s*boss,\s*WITHINGS_ECG_SYNC_QUEUE[\s\S]{0,120}handleWithingsEcgSync/, ); }); }); diff --git a/src/lib/jobs/__tests__/withings-queues.test.ts b/src/lib/jobs/__tests__/withings-queues.test.ts index 5dc7dac08..9861c41a8 100644 --- a/src/lib/jobs/__tests__/withings-queues.test.ts +++ b/src/lib/jobs/__tests__/withings-queues.test.ts @@ -50,14 +50,14 @@ describe("reminder-worker — Withings activity + sleep v2 queues", () => { it("registers the activity-sync handler against pg-boss", () => { expect(source).toMatch(/handleWithingsActivitySync\b/); expect(source).toMatch( - /boss\.work[\s\S]{0,200}WITHINGS_ACTIVITY_QUEUE[\s\S]{0,200}handleWithingsActivitySync/, + /createAndWork[\s\S]{0,200}WITHINGS_ACTIVITY_QUEUE[\s\S]{0,200}handleWithingsActivitySync/, ); }); it("registers the sleep-sync handler against pg-boss", () => { expect(source).toMatch(/handleWithingsSleepSync\b/); expect(source).toMatch( - /boss\.work[\s\S]{0,200}WITHINGS_SLEEP_QUEUE[\s\S]{0,200}handleWithingsSleepSync/, + /createAndWork[\s\S]{0,200}WITHINGS_SLEEP_QUEUE[\s\S]{0,200}handleWithingsSleepSync/, ); }); @@ -78,7 +78,7 @@ describe("reminder-worker — Withings activity + sleep v2 queues", () => { }); it("schedules the ECG catch-net cron at :41 so watch-only accounts pull strips", () => { - // The queue is already in allQueues + boss.work-bound; W-1 adds the missing + // The queue is already in allQueues + createAndWork-bound; W-1 adds the missing // schedule entry so a payload-less cron tick walks the connection cohort. expect(source).toMatch( /WITHINGS_ECG_SYNC_CRON\s*=\s*["']41 \* \* \* \*["']/, diff --git a/src/lib/jobs/__tests__/withings-subscription-retry.test.ts b/src/lib/jobs/__tests__/withings-subscription-retry.test.ts index e873c9ca5..be6de5b0e 100644 --- a/src/lib/jobs/__tests__/withings-subscription-retry.test.ts +++ b/src/lib/jobs/__tests__/withings-subscription-retry.test.ts @@ -72,7 +72,16 @@ describe("handleWithingsFallbackSync subscription repair", () => { data: { triggeredAt: "2026-07-21T10:00:00.000Z" }, } as Job; - await handleWithingsFallbackSync([queued]); + await expect(handleWithingsFallbackSync([queued])).resolves.toEqual({ + ok: true, + did: { + total: 0, + users_synced: 0, + users_failed: 0, + measurements_imported: 0, + subscription_repair_failed: false, + }, + }); expect(retryDueWithingsWebhookSubscriptions).toHaveBeenCalledTimes(1); expect(syncUserMeasurements).not.toHaveBeenCalled(); @@ -89,7 +98,19 @@ describe("handleWithingsFallbackSync subscription repair", () => { data: { triggeredAt: "2026-07-21T11:00:00.000Z" }, } as Job; - await expect(handleWithingsFallbackSync([queued])).resolves.toBeUndefined(); + // The repair is an auxiliary leg, not the pass this queue is named for: + // the fallback sync still ran for everyone, so the job is done and the + // repair failure rides out as a fact on the outcome. + await expect(handleWithingsFallbackSync([queued])).resolves.toEqual({ + ok: true, + did: { + total: 2, + users_synced: 2, + users_failed: 0, + measurements_imported: 0, + subscription_repair_failed: true, + }, + }); expect(findMany).toHaveBeenCalledWith({ select: { userId: true } }); expect(syncUserMeasurements).toHaveBeenNthCalledWith( diff --git a/src/lib/jobs/__tests__/workout-insight-queue.test.ts b/src/lib/jobs/__tests__/workout-insight-queue.test.ts index 4fdc3ca15..7af32f112 100644 --- a/src/lib/jobs/__tests__/workout-insight-queue.test.ts +++ b/src/lib/jobs/__tests__/workout-insight-queue.test.ts @@ -32,9 +32,9 @@ describe("workout-insight-generate queue wiring", () => { expect(allQueues![1]).toMatch(/\bWORKOUT_INSIGHT_GENERATE_QUEUE\b/); }); - it("binds a boss.work handler that drains it", () => { + it("binds a createAndWork handler that drains it", () => { expect(statusRegistrar).toMatch( - /boss\.work[\s\S]{0,300}WORKOUT_INSIGHT_GENERATE_QUEUE[\s\S]{0,300}handleWorkoutInsightGenerate/, + /createAndWork[\s\S]{0,300}WORKOUT_INSIGHT_GENERATE_QUEUE[\s\S]{0,300}handleWorkoutInsightGenerate/, ); }); @@ -50,8 +50,10 @@ describe("workout-insight-generate queue wiring", () => { it("has no cron schedule — it is dispatched by arrival only", () => { // A cron here would be a standing invitation to regenerate, which is the // saturation surface the design refuses. + // Tolerates the optional third schedule element, so a cron sneaking in + // with send options still trips this. expect(statusRegistrar).not.toMatch( - /\[WORKOUT_INSIGHT_GENERATE_QUEUE,\s*\w+_CRON\]/, + /\[\s*WORKOUT_INSIGHT_GENERATE_QUEUE,\s*\w+_CRON\s*[,\]]/, ); }); diff --git a/src/lib/jobs/reminder/__tests__/poll-cohort.test.ts b/src/lib/jobs/reminder/__tests__/poll-cohort.test.ts index f0dc9123d..111c255ab 100644 --- a/src/lib/jobs/reminder/__tests__/poll-cohort.test.ts +++ b/src/lib/jobs/reminder/__tests__/poll-cohort.test.ts @@ -107,7 +107,17 @@ describe("makePollCohortHandler — marker-aware recordFailure boundary", () => recordFailure, }); - await expect(handler([job()])).resolves.toBeUndefined(); + // The recorder rejection is warned, not propagated: the pass ran, so the + // job is done and the failing user is a `users_failed` count. + await expect(handler([job()])).resolves.toEqual({ + ok: true, + did: { + total: 2, + users_synced: 1, + users_failed: 1, + measurements_imported: 1, + }, + }); // The next user still synced despite the recorder throwing. expect(syncUser).toHaveBeenCalledTimes(2); expect(syncUser).toHaveBeenLastCalledWith("next");