Browse documentation

Test Specs

RankFlow AI — Job Queue & Workflow Engine Test Specification

- [ ] All 8 onboarding steps execute in correct order when triggered with valid input

docs/test-specs/TEST-job-queue-workflows.md
On this page

Version: 1.0.0
Date: 2026-06-13
Target Spec: docs/specs/job-queue-workflows.md
Systems Under Test: Inngest (durable workflows) + BullMQ (background jobs) + Redis (queue + cache)
Test Tooling: Vitest, msw, ioredis-mock, bullmq test helpers, inngest test utils


1. Inngest Workflow Engine Tests#

1.1 Unit Tests — Job Processor Logic#

Test Input Expected Output File
onboarding.step.validate-practice { practice_id: "prac_123" } Practice record with locations included, or null if missing src/__tests__/unit/inngest/onboarding.step.test.ts
onboarding.step.generate-profile practice_id: "prac_123" Generated site content object with html, seo fields src/__tests__/unit/inngest/onboarding.step.test.ts
onboarding.step.publish-profile practice_id + siteContent Deploy success with url and status: "DEPLOYED" src/__tests__/unit/inngest/onboarding.step.test.ts
onboarding.step.notify-gbp-auth practice with owner.email Email sent with GBP auth URL in body src/__tests__/unit/inngest/onboarding.step.test.ts
onboarding.step.create-first-post practice, location bullmq.queues.gbpPostPublish.add called with correct payload src/__tests__/unit/inngest/onboarding.step.test.ts
onboarding.step.schedule-recurring practice_id scheduleRecurringJobs(practice_id) called with correct jobs src/__tests__/unit/inngest/onboarding.step.test.ts
onboarding.step.send-welcome-report practice_id after sleep Email sent with report string in body src/__tests__/unit/inngest/onboarding.step.test.ts
citation.step.get-data { practice_id, location_id } Both practice and location records returned src/__tests__/unit/inngest/citation.step.test.ts
citation.step.get-directories None (reads DB) Array of active directories with submissionType field src/__tests__/unit/inngest/citation.step.test.ts
citation.step.generate-descriptions practice, location, directories Array of { directoryId, description } pairs, one per directory src/__tests__/unit/inngest/citation.step.test.ts
citation.step.submit-via-api dir (API type), practice, location, description { success: true, directoryId } src/__tests__/unit/inngest/citation.step.test.ts
citation.step.submit-via-browser dir (browser type), practice, location, description { success: true, directoryId } src/__tests__/unit/inngest/citation.step.test.ts
citation.step.submit-error Invalid directory config { success: false, error: "Error message" } src/__tests__/unit/inngest/citation.step.test.ts
citation.step.store-results results array db.citation.createMany called with correct mapped data src/__tests__/unit/inngest/citation.step.test.ts
citation.step.schedule-verification { practice_id, location_id } inngest.send called with skill/14-citation-verify-nap event src/__tests__/unit/inngest/citation.step.test.ts

1.2 Integration Tests — Inngest Step Functions#

Test Setup Action Assertion File
onboarding.full-flow Mock DB with practice + location records; mock profile.generate, profile.publish, email.send, gbpAuth.getAuthUrl, ai.generate Trigger skill/01-practice-onboard event with valid practice_id and user_id All 8 steps execute in order; final return has { practice_id, status: "onboarded" }; bullmq.queues.gbpPostPublish.add called once src/__tests__/integration/inngest/onboarding.flow.test.ts
onboarding.wait-for-oauth Same as above, but simulate OAuth callback arriving after 2 minutes Send gbp/oauth-callback event with matching practice_id Step 5 receives oauthResult with account_email; steps 6-8 continue without timeout src/__tests__/integration/inngest/onboarding.flow.test.ts
onboarding.oauth-timeout Same as above, but no OAuth callback sent Trigger event and wait for 7-day timeout simulation Step 5 times out; workflow returns with status: "onboarded-partial" or raises NonRetriableError as per spec src/__tests__/integration/inngest/onboarding.flow.test.ts
citation.full-flow Mock DB with practice, location, 3 active directories (2 API, 1 browser); mock ai.generate, submitViaApi, submitViaBrowser Trigger skill/13-citation-submit event All 6 steps execute; citation.createMany called with 3 records; 2 SUBMITTED, 1 FAILED (if browser mock fails) src/__tests__/integration/inngest/citation.flow.test.ts
citation.parallel-submission 10 active directories Trigger event Promise.all on step.run calls completes in < 15s mock time; no more than 3 concurrent step.run calls active per concurrency limit src/__tests__/integration/inngest/citation.flow.test.ts
content-generate.flow Mock DB with practice; mock ai.generate Trigger skill/29-content-generate event 3-step flow: generate → wait for approval → publish (if approved) or discard (if rejected) src/__tests__/integration/inngest/content.flow.test.ts
profile-refresh.cron-trigger Mock DB with active practices; mock profile.generate, profile.publish Simulate cron: 0 2 * * 1 trigger All 7 steps execute for each active practice; profile.publish called with updated content src/__tests__/integration/inngest/profile-refresh.flow.test.ts
monthly-report.cron-trigger Mock DB with report data; mock generateWelcomeReport, email.send Simulate cron: 0 3 1 * * trigger 4-step flow completes; email sent to practice owner with report body src/__tests__/integration/inngest/monthly-report.flow.test.ts
token-refresh.cron-trigger Mock DB with 5 OAuth tokens nearing expiry; mock token refresh APIs Simulate cron: 0 3 * * * trigger Batch step refreshes all 5 tokens; DB updated with new accessToken and expiresAt src/__tests__/integration/inngest/token-refresh.flow.test.ts

1.3 Failure/Recovery Tests — Inngest#

Test Setup Action Assertion File
onboarding.step-retry profile.generate throws transient error Trigger onboarding event step.run("generate-profile") retries up to 3 times (per Inngest config); on 3rd failure, workflow marked failed src/__tests__/integration/inngest/onboarding.retry.test.ts
onboarding.step-non-retriable practice_id does not exist in DB Trigger event step.run("validate-practice") returns null; workflow should raise NonRetriableError and fail immediately without retry src/__tests__/integration/inngest/onboarding.retry.test.ts
citation.partial-failure 3 directories; submitViaApi fails on 2nd directory with 500 Trigger event Results stored with 1 SUBMITTED, 1 FAILED, 1 SUBMITTED; workflow returns { submitted: 2, failed: 1 }; no retries because errors are caught inside step.run src/__tests__/integration/inngest/citation.retry.test.ts
citation.step-retry submitViaApi throws on first attempt, succeeds on second Trigger event Inngest retries step.run up to 3 times; final result is success: true src/__tests__/integration/inngest/citation.retry.test.ts
workflow.sleep-resume Workflow at step.sleep("1d") Simulate time advance + wake event Workflow resumes exactly at the sleep boundary; subsequent steps execute with correct context src/__tests__/integration/inngest/sleep-resume.test.ts

1.4 Concurrency Limit Tests — Inngest#

Test Setup Action Assertion File
onboarding.concurrency-5 10 onboarding events triggered simultaneously Trigger all 10 events No more than 5 onboarding workflows run concurrently; remaining 5 queue and start as slots free src/__tests__/integration/inngest/concurrency.test.ts
citation.concurrency-3 5 citation events triggered simultaneously Trigger all 5 events No more than 3 citation workflows run concurrently; rate-limiting respects directory site constraints src/__tests__/integration/inngest/concurrency.test.ts
mixed-queues.no-interference 5 onboarding + 5 citation events simultaneously Trigger all 10 Onboarding concurrency (5) and citation concurrency (3) are independent; total concurrent = 8 max src/__tests__/integration/inngest/concurrency.test.ts

1.5 Success Criteria (Binary) — Inngest#

  • All 8 onboarding steps execute in correct order when triggered with valid input
  • OAuth wait step resolves within 7-day timeout when callback is received
  • OAuth wait step fails gracefully (no infinite hang) when callback never arrives
  • Citation builder submits to all active directories and stores results with correct status mapping
  • Parallel directory submissions respect per-workflow concurrency limit (3 for citation)
  • Cron-triggered workflows (profile-refresh, monthly-report, token-refresh) execute on schedule
  • Inngest retries failed step.run up to the configured retry count (3) with exponential backoff
  • Non-retriable conditions (missing practice) fail immediately without retry waste
  • Workflow concurrency limits are enforced and excess flows queue correctly
  • Sleep steps resume accurately after simulated time advance

1.6 Agent Context (Pre-conditions) — Inngest#

  • Required DB state: practice, location, citationDirectory tables with at least 1 mock record each; citationDirectory.isActive = true for test directories
  • Required env vars: INNGEST_EVENT_KEY, INNGEST_API_URL (mocked in test)
  • Required external mocks: profile.generate, profile.publish, email.send, gbpAuth.getAuthUrl, ai.generate, submitViaApi, submitViaBrowser — all mocked via msw or vi.fn()
  • Mock Redis state: ioredis-mock instance initialized with empty queue state; no prior jobs lingering
  • Inngest test harness: inngest.test() or equivalent test runner configured to simulate events and step execution

1.7 Verification Commands — Inngest#

# Unit: step logic
pnpm test:unit -- src/__tests__/unit/inngest/

# Integration: full workflows
pnpm test:integration -- src/__tests__/integration/inngest/

# Retry & failure scenarios
pnpm test:integration -- src/__tests__/integration/inngest/onboarding.retry.test.ts
pnpm test:integration -- src/__tests__/integration/inngest/citation.retry.test.ts

# Concurrency
pnpm test:integration -- src/__tests__/integration/inngest/concurrency.test.ts

# Sleep/resume durability
pnpm test:integration -- src/__tests__/integration/inngest/sleep-resume.test.ts

2. BullMQ Job Queue Tests#

2.1 Unit Tests — Job Processor Logic#

Test Input Expected Output File
gbp-post.processor { practiceId: "prac_123", content: "Post body" } GBP API called with correct payload; post published; return { success: true, postId } src/__tests__/unit/bullmq/gbp-post.processor.test.ts
social-post.processor { practiceId: "prac_123", platform: "facebook", content: "..." } Platform API called; return { success: true, postId } src/__tests__/unit/bullmq/social-post.processor.test.ts
review-monitor.processor { practiceId: "prac_123" } Reviews fetched from GBP; new reviews stored in DB; return { newReviews: number } src/__tests__/unit/bullmq/review-monitor.processor.test.ts
nap-check.processor { practiceId: "prac_123", locationId: "loc_456" } NAP data scraped from directories; mismatches logged; return { mismatches: number } src/__tests__/unit/bullmq/nap-check.processor.test.ts
email-send.processor { to: "test@example.com", subject: "Hello", body: "..." } email.send called with exact payload; return { messageId } src/__tests__/unit/bullmq/email-send.processor.test.ts
token-refresh.processor { provider: "google", tokenId: "tok_789" } New token fetched; DB updated with accessToken and expiresAt src/__tests__/unit/bullmq/token-refresh.processor.test.ts
citation-submit.processor { practiceId, locationId, directoryId, description } Directory submission API/browser called; return { success, directoryId } src/__tests__/unit/bullmq/citation-submit.processor.test.ts
pdf-generate.processor { practiceId, reportType: "monthly" } PDF generated and stored; return { url, sizeBytes } src/__tests__/unit/bullmq/pdf-generate.processor.test.ts

2.2 Integration Tests — BullMQ Queue Operations#

Test Setup Action Assertion File
queue.add-job Redis mock running; Queue instance initialized queues.gbpPostPublish.add("weekly-post", payload) Job exists in Redis with correct name, data, and id; queue waiting count = 1 src/__tests__/integration/bullmq/queue.ops.test.ts
queue.job-processing Worker connected to mock Redis; 3 jobs added Start worker All 3 jobs processed sequentially or concurrently per worker config; completed count = 3; failed count = 0 src/__tests__/integration/bullmq/queue.ops.test.ts
queue.job-priority High-priority and low-priority jobs added Start worker High-priority jobs process before low-priority jobs (if priority configured) src/__tests__/integration/bullmq/queue.ops.test.ts
queue.delayed-job Job added with delay: 60000 Start worker immediately Job remains in delayed state for 60s; processes after delay expires src/__tests__/integration/bullmq/queue.ops.test.ts
queue.repeatable-job Job added with repeat: { pattern: "0 9 * * 1,3,5" } Query repeatable jobs Repeatable job exists with correct cron pattern; next execution time calculated correctly src/__tests__/integration/bullmq/queue.ops.test.ts
queue.remove-repeatable Repeatable job exists Call removeRepeatable Job removed from repeatable list; next execution does not occur src/__tests__/integration/bullmq/queue.ops.test.ts
worker.concurrency-respect Worker configured with concurrency: 3; 5 jobs added Start worker Never more than 3 jobs active simultaneously; worker internal concurrency respected src/__tests__/integration/bullmq/worker.concurrency.test.ts
worker.limiter-respect Worker configured with limiter: { max: 30, duration: 60000 } Add 40 jobs rapidly No more than 30 jobs processed in any 60-second window; excess jobs delayed or throttled src/__tests__/integration/bullmq/worker.limiter.test.ts
multi-queue.isolation Jobs added to gbpPostPublish and socialPostPublish simultaneously Start both workers Each worker processes only its own queue; no cross-queue pollution src/__tests__/integration/bullmq/multi-queue.test.ts

2.3 Failure/Recovery Tests — BullMQ#

Test Setup Action Assertion File
job.retry-exponential Processor throws on first 2 attempts; succeeds on 3rd Add job with attempts: 3, backoff: { type: "exponential", delay: 5000 } Job retried at 5s, then 10s, then 15s (or per config); succeeds on 3rd attempt; final status = completed src/__tests__/integration/bullmq/retry.test.ts
job.retry-exhaustion Processor throws on all attempts Add job with attempts: 3 After 3 attempts, job status = failed; attemptsMade = 3 src/__tests__/integration/bullmq/retry.test.ts
job.retry-exhaustion-dlq Same as above; DLQ handler wired Add job with attempts: 3 Job moved to dead-letter queue with originalJobId, error, failedAt, attemptsMade preserved src/__tests__/integration/bullmq/dlq.test.ts
dlq.admin-retry Job exists in DLQ with original payload Call retryDeadLetterJob(dlqJobId) Job re-added to original queue with same originalJobId; removed from DLQ; processes successfully src/__tests__/integration/bullmq/dlq.test.ts
dlq.admin-retry-invalid Invalid jobId passed Call retryDeadLetterJob("nonexistent") Throws Error("Job not found in dead letter queue") src/__tests__/integration/bullmq/dlq.test.ts
job.fatal-error Processor throws non-retriable error (e.g., invalid payload) Add job Job fails immediately; no retries; moved to DLQ if configured for immediate DLQ src/__tests__/integration/bullmq/retry.test.ts
graceful-shutdown 3 active jobs running; workers active Call gracefulShutdown(workers) Workers pause; active jobs complete (or timeout at 30s); workers close cleanly; no orphaned jobs src/__tests__/integration/bullmq/shutdown.test.ts
worker-crash-recovery Worker crashes mid-job (simulated SIGKILL) Restart worker Job remains in active state; on worker restart, stalled job detection moves it back to waiting; job re-processed src/__tests__/integration/bullmq/recovery.test.ts

2.4 Concurrency Limit Tests — BullMQ#

Test Setup Action Assertion File
gbp-post.concurrency-3 Worker concurrency: 3; 10 jobs added Start worker Never more than 3 gbp-post-publish jobs active at once src/__tests__/integration/bullmq/concurrency.test.ts
social-post.concurrency-5 Worker concurrency: 5; 10 jobs added Start worker Never more than 5 social-post-publish jobs active at once src/__tests__/integration/bullmq/concurrency.test.ts
token-refresh.concurrency-1 Worker concurrency: 1; 5 jobs added Start worker Jobs processed strictly sequentially; never 2 active simultaneously src/__tests__/integration/bullmq/concurrency.test.ts
citation-submit.concurrency-3 Worker concurrency: 3; 10 jobs added Start worker Never more than 3 citation-submit jobs active at once src/__tests__/integration/bullmq/concurrency.test.ts
review-monitor.concurrency-2 Worker concurrency: 2; 5 jobs added Start worker Never more than 2 review-monitor jobs active at once src/__tests__/integration/bullmq/concurrency.test.ts
nap-check.concurrency-2 Worker concurrency: 2; 5 jobs added Start worker Never more than 2 nap-check jobs active at once src/__tests__/integration/bullmq/concurrency.test.ts
rate-limiter.gbp-30-per-min Worker limiter: { max: 30, duration: 60000 }; 35 jobs added Start worker Exactly 30 jobs processed in first 60s; remaining 5 delayed to next minute window src/__tests__/integration/bullmq/limiter.test.ts
rate-limiter.social-25-per-min Worker limiter: { max: 25, duration: 60000 }; 30 jobs added Start worker Exactly 25 jobs processed in first 60s; remaining 5 delayed to next minute window src/__tests__/integration/bullmq/limiter.test.ts

2.5 Scheduling Tests — BullMQ Cron#

Test Setup Action Assertion File
schedule.gbp-post-mon-wed-fri-9am Cron "0 9 * * 1,3,5" Parse and evaluate next 5 run times Run times fall only on Monday, Wednesday, Friday at 09:00 IST; no other days src/__tests__/integration/bullmq/scheduler.test.ts
schedule.review-monitor-daily-8am Cron "0 8 * * *" Parse and evaluate next 5 run times Run times fall daily at 08:00 IST; interval = 24h src/__tests__/integration/bullmq/scheduler.test.ts
schedule.nap-check-1st-month-2am Cron "0 2 1 * *" Parse and evaluate next 5 run times Run times fall on 1st of each month at 02:00 IST src/__tests__/integration/bullmq/scheduler.test.ts
schedule.token-refresh-daily-3am Cron "0 3 * * *" Parse and evaluate next 5 run times Run times fall daily at 03:00 IST; interval = 24h src/__tests__/integration/bullmq/scheduler.test.ts
schedule.monthly-report-1st-4am Cron "0 4 1 * *" Parse and evaluate next 5 run times Run times fall on 1st of each month at 04:00 IST src/__tests__/integration/bullmq/scheduler.test.ts
schedule.profile-refresh-mon-2am Cron "0 2 * * 1" Parse and evaluate next 5 run times Run times fall on Mondays at 02:00 IST src/__tests__/integration/bullmq/scheduler.test.ts
schedule.timezone-correct All cron patterns Evaluate against IST and UTC IST schedule is correct; UTC equivalent is offset by -5:30 (or as configured) src/__tests__/integration/bullmq/scheduler.test.ts
schedule.overlap-prevention Long-running job + next cron tick Job exceeds 24h interval Next cron tick queues new job; does not skip or block; both jobs eventually process src/__tests__/integration/bullmq/scheduler.test.ts

2.6 Success Criteria (Binary) — BullMQ#

  • All 8 BullMQ queue types accept and store jobs with correct payload shape
  • Workers process jobs from their assigned queue with zero cross-queue leakage
  • Job processors execute the correct business logic and return expected result shapes
  • Exponential backoff retries function correctly (delay doubles per attempt)
  • Retry exhaustion moves job to Dead Letter Queue with full context preserved
  • Admin retry from DLQ re-queues job to original queue with same originalJobId
  • Worker concurrency limits are strictly enforced (3 GBP, 5 social, 1 token-refresh, etc.)
  • Rate limiters throttle job throughput within configured windows (30/min GBP, 25/min social)
  • All 6 cron schedules produce correct next-run times for their expressions
  • Cron jobs execute on time within ±1 minute tolerance under normal load
  • Graceful shutdown pauses workers, lets active jobs complete, and closes cleanly
  • Worker crash recovery detects stalled jobs and re-queues them without data loss
  • Delayed jobs remain in delayed state until expiration, then process correctly
  • Repeatable jobs can be added, queried, and removed correctly

2.7 Agent Context (Pre-conditions) — BullMQ#

  • Required DB state: practice, location, oauthToken, citation, review tables with mock records
  • Required env vars: REDIS_URL pointing to ioredis-mock instance; BULLMQ_PREFIX (optional)
  • Required external mocks: GBP API (msw), social platform APIs (msw), email provider (msw), Firecrawl/NAP scraper (msw), PDF generator (vi.fn())
  • Mock Redis state: Fresh ioredis-mock instance per test; no pre-existing jobs; flushall before each test suite
  • Queue state: All 8 queues initialized but empty; worker instances created but not started until test action
  • Time control: vi.useFakeTimers() or bullmq test helpers for advancing cron/delayed job time

2.8 Verification Commands — BullMQ#

# Unit: processor logic
pnpm test:unit -- src/__tests__/unit/bullmq/

# Integration: queue operations + scheduling
pnpm test:integration -- src/__tests__/integration/bullmq/queue.ops.test.ts
pnpm test:integration -- src/__tests__/integration/bullmq/scheduler.test.ts

# Retry, DLQ, recovery
pnpm test:integration -- src/__tests__/integration/bullmq/retry.test.ts
pnpm test:integration -- src/__tests__/integration/bullmq/dlq.test.ts
pnpm test:integration -- src/__tests__/integration/bullmq/recovery.test.ts

# Concurrency & rate limits
pnpm test:integration -- src/__tests__/integration/bullmq/concurrency.test.ts
pnpm test:integration -- src/__tests__/integration/bullmq/limiter.test.ts

# Graceful shutdown
pnpm test:integration -- src/__tests__/integration/bullmq/shutdown.test.ts

# Full BullMQ suite
pnpm test:integration -- src/__tests__/integration/bullmq/

3. Retry Policies & Dead Letter Queue Tests#

3.1 Unit Tests — Retry Calculator#

Test Input Expected Output File
retry.delay.exponential { attempt: 1, delay: 5000 } 5000 src/__tests__/unit/retry/calculator.test.ts
retry.delay.exponential-2nd { attempt: 2, delay: 5000 } 10000 src/__tests__/unit/retry/calculator.test.ts
retry.delay.exponential-3rd { attempt: 3, delay: 5000 } 20000 src/__tests__/unit/retry/calculator.test.ts
retry.delay.exponential-5th { attempt: 5, delay: 60000 } 960000 (60s * 2^4) src/__tests__/unit/retry/calculator.test.ts
retry.delay.max-cap Exponential delay exceeds 24h Clamped to maxDelay: 86400000 (24h) src/__tests__/unit/retry/calculator.test.ts
retry.config.lookup "review-monitor" { attempts: 5, delay: 60000 } src/__tests__/unit/retry/calculator.test.ts
retry.config.lookup-default "unknown-job-type" { attempts: 3, delay: 5000 } (DEFAULT_RETRY) src/__tests__/unit/retry/calculator.test.ts
retry.config.gbp-post "gbp-post-publish" { attempts: 3, delay: 5000 } src/__tests__/unit/retry/calculator.test.ts
retry.config.social-post "social-post-publish" { attempts: 3, delay: 10000 } src/__tests__/unit/retry/calculator.test.ts
retry.config.nap-check "nap-check" { attempts: 3, delay: 300000 } src/__tests__/unit/retry/calculator.test.ts
retry.config.token-refresh "token-refresh" { attempts: 5, delay: 60000 } src/__tests__/unit/retry/calculator.test.ts
retry.config.pdf-generate "pdf-generate" { attempts: 2, delay: 10000 } src/__tests__/unit/retry/calculator.test.ts
retry.should-retry.yes attemptsMade: 2, config.attempts: 3 true src/__tests__/unit/retry/calculator.test.ts
retry.should-retry.no attemptsMade: 3, config.attempts: 3 false src/__tests__/unit/retry/calculator.test.ts
retry.should-retry.exceeded attemptsMade: 4, config.attempts: 3 false src/__tests__/unit/retry/calculator.test.ts

3.2 Integration Tests — Retry → DLQ → Admin Retry Flow#

Test Setup Action Assertion File
retry-to-dlq.gbp-post gbp-post-publish processor always throws; attempts: 3 Add job and run worker After 3 retries, job status = failed; DLQ contains 1 job with originalJobId, error, failedAt, attemptsMade: 3 src/__tests__/integration/dlq/full-flow.test.ts
retry-to-dlq.review-monitor review-monitor processor always throws; attempts: 5 Add job and run worker After 5 retries, job in DLQ with attemptsMade: 5 src/__tests__/integration/dlq/full-flow.test.ts
retry-to-dlq.email-send email-send processor always throws; attempts: 3 Add job and run worker After 3 retries, job in DLQ; admin email notification sent to admin@rankflow.in src/__tests__/integration/dlq/full-flow.test.ts
dlq.notification-content Job in DLQ Inspect DLQ entry Notification email contains job.name, job.id, attemptsMade, and error.message src/__tests__/integration/dlq/notification.test.ts
admin-retry.success Job in DLQ with valid original payload Call retryDeadLetterJob(dlqJobId) Job re-added to original queue with same jobId; DLQ count decrements by 1; job processes successfully src/__tests__/integration/dlq/admin-retry.test.ts
admin-retry.missing-job Invalid jobId Call retryDeadLetterJob("fake-id") Throws Error("Job not found in dead letter queue") src/__tests__/integration/dlq/admin-retry.test.ts
admin-retry.unknown-queue Job in DLQ with name not matching any queue Call retryDeadLetterJob Throws Error("Unknown target queue: ...") or logs warning; job remains in DLQ src/__tests__/integration/dlq/admin-retry.test.ts
admin-retry.duplicate-prevention Same DLQ job retried twice Call retryDeadLetterJob twice Second call throws because job already removed from DLQ after first successful retry src/__tests__/integration/dlq/admin-retry.test.ts
dlq.batch-retry 5 jobs in DLQ Call batch retry function All 5 jobs re-queued to original queues; DLQ count = 0 src/__tests__/integration/dlq/admin-retry.test.ts
dlq.auto-retry Job in DLQ with autoRetry: true and transient error label Schedule auto-retry Job automatically re-queued after 1 hour; removed from DLQ src/__tests__/integration/dlq/auto-retry.test.ts

3.3 Success Criteria (Binary) — Retry & DLQ#

  • Exponential backoff delay is calculated correctly: delay * 2^(attempt-1)
  • Retry delay is capped at a maximum value (e.g., 24 hours)
  • Job-type-specific retry configs are looked up correctly; unknown types fall back to default
  • shouldRetry returns true when attemptsMade < config.attempts, false otherwise
  • After exhausting retries, job is automatically moved to Dead Letter Queue with all metadata preserved
  • DLQ notification email is sent to admin@rankflow.in with job name, ID, attempts, and error message
  • Admin retry function re-queues job to original queue with same originalJobId and removes from DLQ
  • Admin retry on missing job throws clear Job not found in dead letter queue error
  • Admin retry on unknown queue name fails safely without dropping the DLQ entry
  • Batch retry can re-queue multiple DLQ jobs atomically
  • Auto-retry (if configured) re-queues eligible DLQ jobs after a cooldown period
  • No job is retried more than its configured maximum attempts under any circumstance

3.4 Agent Context (Pre-conditions) — Retry & DLQ#

  • Required DB state: No specific DB state required for retry logic; DLQ state is Redis-only
  • Required env vars: ADMIN_EMAIL=admin@rankflow.in (or mocked in test); REDIS_URL for ioredis-mock
  • Required external mocks: email.send (mocked) to capture admin notification; logger (mocked) to verify error logs
  • Mock Redis state: dead-letter queue empty; all original queues empty; flushall before suite
  • DLQ state: deadLetterQueue instance initialized with connection: redisMock

3.5 Verification Commands — Retry & DLQ#

# Unit: retry calculator
pnpm test:unit -- src/__tests__/unit/retry/calculator.test.ts

# Integration: full retry → DLQ → admin retry flow
pnpm test:integration -- src/__tests__/integration/dlq/full-flow.test.ts
pnpm test:integration -- src/__tests__/integration/dlq/admin-retry.test.ts
pnpm test:integration -- src/__tests__/integration/dlq/notification.test.ts

# Full DLQ suite
pnpm test:integration -- src/__tests__/integration/dlq/

4. Observability & Monitoring Tests#

4.1 Unit Tests — Structured Logging#

Test Input Expected Output File
log.job-started Job object with id, name, data.practiceId, attemptsMade logger.info called with event: "job_started", jobId, jobName, practiceId, attempt, queue src/__tests__/unit/observability/logger.test.ts
log.job-completed Job object + startTime logger.info called with event: "job_completed", jobId, jobName, practiceId, durationMs > 0 src/__tests__/unit/observability/logger.test.ts
log.job-failed Job object + Error logger.error called with event: "job_failed", jobId, jobName, error, attempt src/__tests__/unit/observability/logger.test.ts
log.dlq-moved Failed job + err logger.error called with event: "job_dlq_moved", jobId, jobName, error, attempts src/__tests__/unit/observability/logger.test.ts

4.2 Integration Tests — Health Check & Metrics#

Test Setup Action Assertion File
health.queues.all-empty All queues empty GET /api/health/jobs Response 200; totalWaiting: 0, totalActive: 0, totalFailed: 0; each queue has waiting: 0, active: 0, completed: 0, failed: 0 src/__tests__/integration/observability/health.test.ts
health.queues.with-jobs 5 waiting, 2 active, 3 completed, 1 failed across mixed queues GET /api/health/jobs Response 200; counts match exact queue states; timestamp is valid ISO string src/__tests__/integration/observability/health.test.ts
health.queues.accuracy Add and process jobs; verify counts GET /api/health/jobs completed count increments by 1 per successful job; failed increments by 1 per failed job; waiting decrements as jobs process src/__tests__/integration/observability/health.test.ts
metric.queue-depth-alert 150 jobs in review-monitor queue Poll health endpoint totalWaiting > 100 triggers alert condition; response includes alert: "QUEUE_DEPTH_HIGH" src/__tests__/integration/observability/metrics.test.ts
metric.failure-rate-alert 6 failed out of 100 jobs in 1 hour Aggregate metrics failureRate > 0.05 (5%) triggers alert condition; response includes alert: "FAILURE_RATE_HIGH" src/__tests__/integration/observability/metrics.test.ts
metric.duration-alert Job baseline = 30s; current job takes 70s Compare durations duration > 2 * baseline triggers alert condition; response includes alert: "DURATION_HIGH" src/__tests__/integration/observability/metrics.test.ts
metric.dlq-count-alert 12 jobs in DLQ within 24h Count DLQ entries dlqCount > 10 triggers alert condition; response includes alert: "DLQ_HIGH" src/__tests__/integration/observability/metrics.test.ts
metric.inngest-latency Inngest step takes 6 minutes Measure step latency latency > 5 min triggers alert condition; response includes alert: "INNGEST_LATENCY_HIGH" src/__tests__/integration/observability/metrics.test.ts
metric.worker-cpu Worker CPU at 85% for 6 minutes Poll system metrics cpu > 80% for 5 min triggers alert condition; response includes alert: "WORKER_CPU_HIGH" src/__tests__/integration/observability/metrics.test.ts
dashboard.queue-list 3 queues with mixed states Render admin dashboard Dashboard shows all queue names, waiting/active/completed/failed counts, and health status color src/__tests__/integration/observability/dashboard.test.ts
dashboard.job-details Job with id exists Click job in dashboard Job detail panel shows payload, attempts, error stack, timeline, and retry/DLQ buttons src/__tests__/integration/observability/dashboard.test.ts

4.3 Success Criteria (Binary) — Observability#

  • Every job start emits a job_started log with jobId, jobName, practiceId, attempt, and queue
  • Every job completion emits a job_completed log with durationMs > 0
  • Every job failure emits a job_failed log with error message and attempt count
  • Every DLQ move emits a job_dlq_moved log with error and attempts
  • Health endpoint /api/health/jobs returns 200 with accurate queue counts for all 8 queues + DLQ
  • Health endpoint returns correct totalWaiting, totalActive, totalFailed aggregations
  • Queue depth alert triggers when any single queue has > 100 waiting jobs
  • Failure rate alert triggers when > 5% of jobs fail within any 1-hour window
  • Duration alert triggers when average job duration exceeds 2x the baseline
  • DLQ count alert triggers when > 10 jobs enter DLQ within 24 hours
  • Inngest latency alert triggers when any step takes > 5 minutes
  • Worker CPU alert triggers when CPU > 80% for > 5 minutes
  • Admin dashboard renders all queue states accurately with color-coded health status
  • Admin dashboard job detail view shows full job history, payload, error, and action buttons

4.4 Agent Context (Pre-conditions) — Observability#

  • Required DB state: No specific DB state; metrics are queue-state derived
  • Required env vars: REDIS_URL for ioredis-mock; ADMIN_API_KEY (if health endpoint is protected)
  • Required external mocks: logger (mocked Pino or equivalent) to capture and assert log objects; email.send for alert notifications
  • Mock Redis state: Queues populated with known job counts (waiting/active/completed/failed) for predictable health responses
  • Queue state: At least 3 queues have non-zero counts for dashboard rendering tests
  • Metrics baseline: Baseline durations stored in mock config for alert threshold calculations

4.5 Verification Commands — Observability#

# Unit: logger assertions
pnpm test:unit -- src/__tests__/unit/observability/logger.test.ts

# Integration: health endpoint + metrics
pnpm test:integration -- src/__tests__/integration/observability/health.test.ts
pnpm test:integration -- src/__tests__/integration/observability/metrics.test.ts

# Dashboard UI
pnpm test:integration -- src/__tests__/integration/observability/dashboard.test.ts

# Full observability suite
pnpm test:integration -- src/__tests__/integration/observability/

# Health endpoint direct curl (smoke test after deployment)
curl -s http://localhost:3000/api/health/jobs | jq '.queues | length'

5. Idempotency & Safety Tests#

5.1 Unit Tests — Idempotency Checks#

Test Input Expected Output File
idempotency.generateJobId practiceId="prac_123", jobType="gbp-post", date="2025-01-15" "prac_123:gbp-post:2025-01-15" src/__tests__/unit/idempotency/key.test.ts
idempotency.generateJobId-no-date practiceId="prac_123", jobType="gbp-post" "prac_123:gbp-post:2025-01-15" (uses today's date) src/__tests__/unit/idempotency/key.test.ts
idempotency.unique-per-practice Same jobType + date, different practiceId Different job IDs src/__tests__/unit/idempotency/key.test.ts
idempotency.unique-per-date Same practiceId + jobType, different date Different job IDs src/__tests__/unit/idempotency/key.test.ts
idempotency.unique-per-type Same practiceId + date, different jobType Different job IDs src/__tests__/unit/idempotency/key.test.ts
idempotency.gbp-post-dupe-prevention Same practiceId + date added twice Second queue.add returns existing job ID or rejects duplicate; queue depth = 1 src/__tests__/unit/idempotency/dupe.test.ts
idempotency.social-post-dupe-prevention Same practiceId + platform + date added twice Queue depth = 1; no duplicate job created src/__tests__/unit/idempotency/dupe.test.ts
idempotency.email-send-dupe-prevention Same to + subject + date added twice Queue depth = 1; no duplicate email queued src/__tests__/unit/idempotency/dupe.test.ts
idempotency.token-refresh-dupe-prevention Same tokenId + date added twice Queue depth = 1; no duplicate refresh queued src/__tests__/unit/idempotency/dupe.test.ts
idempotency.reentrant-processor Processor called twice with same jobId Second call returns cached result or no-op; no side effects duplicated src/__tests__/unit/idempotency/reentrant.test.ts

5.2 Integration Tests — Safety Checks#

Test Setup Action Assertion File
safety.gbp-suspension-guard practice already has 3 GBP posts this week Attempt to queue 4th GBP post Queue rejects or job processor returns { success: false, reason: "SUSPENSION_GUARD" }; no API call made src/__tests__/integration/safety/gbp-guard.test.ts
safety.gbp-suspension-guard-2-posts practice has 2 GBP posts this week Attempt to queue 1 more Job accepted and processed normally; count becomes 3 src/__tests__/integration/safety/gbp-guard.test.ts
safety.social-rate-limit Platform limiter set to 25/min; 30 jobs queued Run worker 25 jobs process in first minute; 5 jobs delayed to next minute; no platform rate limit hit src/__tests__/integration/safety/social-rate.test.ts
safety.token-refresh-24h Token expires in 48 hours Run token-refresh scheduler No refresh queued (not within 24h window) src/__tests__/integration/safety/token-refresh.test.ts
safety.token-refresh-12h Token expires in 12 hours Run token-refresh scheduler Refresh queued and executed; new token stored src/__tests__/integration/safety/token-refresh.test.ts
safety.cost-cap Practice has spent $49.50 of $50 monthly AI budget Attempt to run AI-generating job Job queued but processor checks cost cap first; if exceeded, returns { success: false, reason: "COST_CAP" } src/__tests__/integration/safety/cost-cap.test.ts
safety.cost-cap-under Practice has spent $40 of $50 budget Run AI-generating job Job processes normally; cost tracked and added to monthly spend src/__tests__/integration/safety/cost-cap.test.ts
safety.db-transaction Citation job writes to citation + directorySubmission tables Simulate DB failure mid-transaction Both writes rollback; no partial state; job fails and retries src/__tests__/integration/safety/transaction.test.ts
safety.graceful-shutdown 3 active jobs; workers running Trigger SIGTERM Workers pause; active jobs complete or timeout at 30s; no jobs lost; Redis state consistent src/__tests__/integration/safety/shutdown.test.ts
safety.idempotency-workflow Inngest event skill/01-practice-onboard sent twice with same practice_id Trigger duplicate event Second trigger is deduplicated or workflow returns same result; no duplicate onboarding side effects (no duplicate site deploy, no duplicate emails) src/__tests__/integration/safety/workflow-idempotency.test.ts

5.3 Success Criteria (Binary) — Idempotency & Safety#

  • generateJobId produces deterministic, unique keys in format practiceId:jobType:date
  • Duplicate job with same jobId is rejected or de-duplicated; queue depth does not increase
  • Re-entrant job processor detects duplicate execution and returns cached result without side effects
  • GBP suspension guard prevents > 3 posts per week per location
  • Social rate limiter prevents exceeding platform API limits per time window
  • Token refresh only runs when expiry is within 24 hours (not prematurely, not too late)
  • Cost cap prevents AI jobs when monthly budget is exceeded; returns clear COST_CAP reason
  • All multi-table DB writes in job processors use transactions; partial writes are impossible
  • Graceful shutdown completes without losing active jobs; Redis state remains consistent
  • Duplicate Inngest events with same payload do not trigger duplicate workflow side effects
  • Idempotency holds across Redis restarts (job IDs are deterministic, not session-based)

5.4 Agent Context (Pre-conditions) — Idempotency & Safety#

  • Required DB state: practice with monthlyAiSpend and monthlyAiBudget fields; location with weeklyPostCount field; oauthToken with expiresAt field; citation and directorySubmission tables
  • Required env vars: MONTHLY_AI_BUDGET_DEFAULT=50; GBP_MAX_POSTS_PER_WEEK=3; TOKEN_REFRESH_WINDOW_HOURS=24
  • Required external mocks: GBP API (msw), social APIs (msw), AI provider (msw), email provider (msw)
  • Mock Redis state: Fresh ioredis-mock; queue state must be inspectable for duplicate detection
  • Queue state: All queues empty before idempotency tests; flushall between test suites

5.5 Verification Commands — Idempotency & Safety#

# Unit: idempotency key generation + duplicate detection
pnpm test:unit -- src/__tests__/unit/idempotency/

# Integration: safety guards
pnpm test:integration -- src/__tests__/integration/safety/gbp-guard.test.ts
pnpm test:integration -- src/__tests__/integration/safety/social-rate.test.ts
pnpm test:integration -- src/__tests__/integration/safety/token-refresh.test.ts
pnpm test:integration -- src/__tests__/integration/safety/cost-cap.test.ts
pnpm test:integration -- src/__tests__/integration/safety/transaction.test.ts
pnpm test:integration -- src/__tests__/integration/safety/shutdown.test.ts
pnpm test:integration -- src/__tests__/integration/safety/workflow-idempotency.test.ts

# Full safety suite
pnpm test:integration -- src/__tests__/integration/safety/

6. Cross-Cutting Integration Tests#

6.1 End-to-End Flow Tests#

Flow Steps Expected End State File
e2e.onboarding-complete 1. Signup practice 2. Trigger onboarding 3. Mock site generate 4. Mock site deploy 5. Mock email sent 6. Mock OAuth callback 7. Mock first GBP post 8. Mock recurring schedule 9. Sleep 1 day 10. Mock welcome report email Practice status = ONBOARDED; site deployed; first GBP post queued; recurring jobs scheduled; welcome report email sent; all 8 steps completed e2e/onboarding.spec.ts
e2e.citation-submit-to-verify 1. Trigger citation submit 2. Submit to 3 directories 3. Store results 4. Sleep 7 days 5. Trigger NAP verify 6. Verify NAP consistency 3 citations in DB with SUBMITTED/FAILED status; NAP verification job queued after 7 days; verification results stored e2e/citation.spec.ts
e2e.gbp-post-cron 1. Schedule 0 9 * * 1,3,5 2. Advance time to next Monday 9 AM 3. Job processes 4. Verify no duplicate for same day GBP post published; queue has no duplicate for same practice + date; suspension guard respected e2e/gbp-post.spec.ts
e2e.monthly-report-cron 1. Schedule 0 3 1 * * 2. Advance time to 1st of month 3. Job processes 4. Email sent Monthly report generated; PDF attached/linked; email sent to practice owner; report data stored e2e/monthly-report.spec.ts

6.2 Mock Redis & Queue State#

Test Setup Action Assertion File
mock.redis.connection ioredis-mock with no persistence Connect BullMQ + Inngest Both systems connect without error; queue operations succeed src/__tests__/integration/mock/redis.test.ts
mock.redis.queue-state Add 5 jobs to gbp-post-publish Inspect Redis keys Redis contains keys with correct BullMQ prefix (bull:gbp-post-publish:id, bull:gbp-post-publish:wait, etc.) src/__tests__/integration/mock/redis.test.ts
mock.redis.flush 10 jobs across 3 queues Call flushall All queue keys removed; queue counts return 0 src/__tests__/integration/mock/redis.test.ts
mock.redis.multi Worker + queue connected Run multi/exec commands Redis transactions succeed; no race conditions in test state src/__tests__/integration/mock/redis.test.ts
mock.queue.snapshot 5 jobs with mixed states Capture queue snapshot Snapshot JSON accurately reflects waiting/active/completed/failed states src/__tests__/integration/mock/snapshot.test.ts
mock.queue.restore Saved snapshot JSON Restore to queue Queue state exactly matches snapshot; jobs resume processing src/__tests__/integration/mock/snapshot.test.ts

6.3 Success Criteria (Binary) — Cross-Cutting#

  • Full onboarding E2E flow completes all 8 steps and produces expected artifacts (site, post, email, schedule)
  • Citation submit-to-verify flow spans 7 days with durable sleep and correct event chaining
  • GBP cron flow triggers on correct days (Mon/Wed/Fri) and respects idempotency per day
  • Monthly report cron triggers on 1st of month and generates report + email
  • Mock Redis accurately simulates BullMQ key structures and queue operations
  • flushall reliably resets all queue state between test suites
  • Queue snapshots capture and restore state accurately for reproducible test scenarios
  • No test suite leaks queue state into subsequent suites (complete isolation)

6.4 Agent Context (Pre-conditions) — Cross-Cutting#

  • Required DB state: Full mock practice with location, owner email, and all related tables populated via factories
  • Required env vars: All production env vars set to mock values; NODE_ENV=test; INNGEST_TEST_MODE=true
  • Required external mocks: All external APIs mocked (msw handlers for Google, social platforms, email, AI, Firecrawl)
  • Mock Redis state: ioredis-mock with keepData: false; flushall in beforeEach and afterAll
  • Queue state: All 8 queues + DLQ initialized empty; workers created but not started until test action
  • Time control: vi.useFakeTimers() with Date.now() mockable for cron and sleep tests
  • Test factories: createMockPractice(), createMockLocation(), createMockDirectory() available from src/__tests__/factories/

6.5 Verification Commands — Cross-Cutting#

# E2E flows
pnpm test:e2e -- e2e/onboarding.spec.ts
pnpm test:e2e -- e2e/citation.spec.ts
pnpm test:e2e -- e2e/gbp-post.spec.ts
pnpm test:e2e -- e2e/monthly-report.spec.ts

# Mock Redis + queue state
pnpm test:integration -- src/__tests__/integration/mock/

# Full cross-cutting suite
pnpm test:e2e
pnpm test:integration -- src/__tests__/integration/mock/

7. Execution Plan & Agent Assignments#

Phase 1: Mock Infrastructure (Day 1)#

Agent Task Test Files Verification
Redis_Mock_Specialist Set up ioredis-mock with BullMQ-compatible key patterns src/__tests__/integration/mock/redis.test.ts pnpm test:integration -- mock/redis
Factory_Specialist Create mock factories for practice, location, directory, token, review src/__tests__/factories/*.ts pnpm test:unit -- factories
MSW_Specialist Set up msw handlers for all external APIs src/__tests__/mocks/handlers.ts pnpm test:contract -- smoke

Phase 2: Unit Tests (Days 2-3)#

Agent Task Test Files Verification
Inngest_Step_Specialist Unit tests for all Inngest step functions src/__tests__/unit/inngest/*.test.ts pnpm test:unit -- inngest
BullMQ_Processor_Specialist Unit tests for all 8 job processors src/__tests__/unit/bullmq/*.test.ts pnpm test:unit -- bullmq
Retry_Calculator_Specialist Unit tests for retry logic and idempotency keys src/__tests__/unit/retry/*.test.ts, src/__tests__/unit/idempotency/*.test.ts pnpm test:unit -- retry, pnpm test:unit -- idempotency
Observability_Logger_Specialist Unit tests for structured logging src/__tests__/unit/observability/*.test.ts pnpm test:unit -- observability

Phase 3: Integration Tests (Days 4-5)#

Agent Task Test Files Verification
Inngest_Integration_Specialist Full workflow + retry + concurrency + sleep tests src/__tests__/integration/inngest/*.test.ts pnpm test:integration -- inngest
BullMQ_Integration_Specialist Queue ops + scheduler + concurrency + limiter tests src/__tests__/integration/bullmq/*.test.ts pnpm test:integration -- bullmq
DLQ_Integration_Specialist Retry exhaustion + DLQ + admin retry + notification tests src/__tests__/integration/dlq/*.test.ts pnpm test:integration -- dlq
Safety_Integration_Specialist Idempotency + safety guards + transaction + shutdown tests src/__tests__/integration/safety/*.test.ts pnpm test:integration -- safety
Observability_Integration_Specialist Health endpoint + metrics + dashboard tests src/__tests__/integration/observability/*.test.ts pnpm test:integration -- observability

Phase 4: E2E & Cross-Cutting (Day 6)#

Agent Task Test Files Verification
E2E_Workflow_Specialist End-to-end flows for onboarding, citation, GBP cron, monthly report e2e/*.spec.ts pnpm test:e2e
Mock_Snapshot_Specialist Queue snapshot/restore for reproducible test scenarios src/__tests__/integration/mock/snapshot.test.ts pnpm test:integration -- mock/snapshot

Phase 5: Verification & Compliance (Day 7)#

Agent Task Verification
Test_Suite_Auditor Run full suite; verify all checkboxes pass pnpm test (runs unit + integration + e2e)
Coverage_Auditor Verify coverage thresholds: lines > 80%, functions > 85%, branches > 75% pnpm test:coverage
Smoke_Test_Agent Run health endpoint smoke test against local dev server curl http://localhost:3000/api/health/jobs

Appendix A: Mock Redis Configuration#

// src/__tests__/mocks/redis.ts
import Redis from "ioredis-mock";

export const redisMock = new Redis({
  data: {}, // Empty initial state
  keyPrefix: "bull:", // Match BullMQ default
});

// BullMQ-compatible key helpers for assertions
export async function getQueueKeys(queueName: string): Promise<string[]> {
  const keys = await redisMock.keys(`bull:${queueName}:*`);
  return keys;
}

export async function getJobCount(queueName: string, state: string): Promise<number> {
  return redisMock.zcard(`bull:${queueName}:${state}`);
}

export async function flushAll(): Promise<void> {
  await redisMock.flushall();
}

Appendix B: Mock Factory Examples#

// src/__tests__/factories/job.ts
import { faker } from "@faker-js/faker";

export const createMockJobPayload = (overrides?: Record<string, unknown>) => ({
  practiceId: `prac_${faker.string.nanoid(8)}`,
  locationId: `loc_${faker.string.nanoid(8)}`,
  content: faker.lorem.paragraph(),
  ...overrides,
});

export const createMockRetryConfig = (overrides?: Partial<{ attempts: number; delay: number }>) => ({
  attempts: 3,
  delay: 5000,
  ...overrides,
});

Appendix C: Test Data Constants#

Constant Value Usage
MOCK_PRACTICE_ID prac_test_001 Deterministic practice ID for snapshot tests
MOCK_LOCATION_ID loc_test_001 Deterministic location ID
MOCK_USER_ID usr_test_001 Deterministic user ID
MOCK_CRON_IST Asia/Kolkata Default timezone for all cron tests
MAX_RETRY_DELAY_MS 86400000 24-hour cap for exponential backoff
GBP_MAX_POSTS_WEEK 3 Suspension guard threshold
AI_BUDGET_DEFAULT 50 Monthly AI cost cap in USD
GRACEFUL_SHUTDOWN_TIMEOUT_MS 30000 Worker close timeout

End of Job Queue & Workflow Engine Test Specification — RankFlow AI v1.0.0