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.mdOn this page
- 1. Inngest Workflow Engine Tests
- 1.1 Unit Tests — Job Processor Logic
- 1.2 Integration Tests — Inngest Step Functions
- 1.3 Failure/Recovery Tests — Inngest
- 1.4 Concurrency Limit Tests — Inngest
- 1.5 Success Criteria (Binary) — Inngest
- 1.6 Agent Context (Pre-conditions) — Inngest
- 1.7 Verification Commands — Inngest
- 2. BullMQ Job Queue Tests
- 2.1 Unit Tests — Job Processor Logic
- 2.2 Integration Tests — BullMQ Queue Operations
- 2.3 Failure/Recovery Tests — BullMQ
- 2.4 Concurrency Limit Tests — BullMQ
- 2.5 Scheduling Tests — BullMQ Cron
- 2.6 Success Criteria (Binary) — BullMQ
- 2.7 Agent Context (Pre-conditions) — BullMQ
- 2.8 Verification Commands — BullMQ
- 3. Retry Policies & Dead Letter Queue Tests
- 3.1 Unit Tests — Retry Calculator
- 3.2 Integration Tests — Retry → DLQ → Admin Retry Flow
- 3.3 Success Criteria (Binary) — Retry & DLQ
- 3.4 Agent Context (Pre-conditions) — Retry & DLQ
- 3.5 Verification Commands — Retry & DLQ
- 4. Observability & Monitoring Tests
- 4.1 Unit Tests — Structured Logging
- 4.2 Integration Tests — Health Check & Metrics
- 4.3 Success Criteria (Binary) — Observability
- 4.4 Agent Context (Pre-conditions) — Observability
- 4.5 Verification Commands — Observability
- 5. Idempotency & Safety Tests
- 5.1 Unit Tests — Idempotency Checks
- 5.2 Integration Tests — Safety Checks
- 5.3 Success Criteria (Binary) — Idempotency & Safety
- 5.4 Agent Context (Pre-conditions) — Idempotency & Safety
- 5.5 Verification Commands — Idempotency & Safety
- 6. Cross-Cutting Integration Tests
- 6.1 End-to-End Flow Tests
- 6.2 Mock Redis & Queue State
- 6.3 Success Criteria (Binary) — Cross-Cutting
- 6.4 Agent Context (Pre-conditions) — Cross-Cutting
- 6.5 Verification Commands — Cross-Cutting
- 7. Execution Plan & Agent Assignments
- Phase 1: Mock Infrastructure (Day 1)
- Phase 2: Unit Tests (Days 2-3)
- Phase 3: Integration Tests (Days 4-5)
- Phase 4: E2E & Cross-Cutting (Day 6)
- Phase 5: Verification & Compliance (Day 7)
- Appendix A: Mock Redis Configuration
- Appendix B: Mock Factory Examples
- Appendix C: Test Data Constants
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.runup 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,citationDirectorytables with at least 1 mock record each;citationDirectory.isActive = truefor 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 viamsworvi.fn() - Mock Redis state:
ioredis-mockinstance 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,reviewtables with mock records - Required env vars:
REDIS_URLpointing toioredis-mockinstance;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-mockinstance per test; no pre-existing jobs;flushallbefore each test suite - Queue state: All 8 queues initialized but empty; worker instances created but not started until test action
- Time control:
vi.useFakeTimers()orbullmqtest 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
-
shouldRetryreturnstruewhenattemptsMade < config.attempts,falseotherwise - After exhausting retries, job is automatically moved to Dead Letter Queue with all metadata preserved
- DLQ notification email is sent to
admin@rankflow.inwith job name, ID, attempts, and error message - Admin retry function re-queues job to original queue with same
originalJobIdand removes from DLQ - Admin retry on missing job throws clear
Job not found in dead letter queueerror - 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_URLforioredis-mock - Required external mocks:
email.send(mocked) to capture admin notification;logger(mocked) to verify error logs - Mock Redis state:
dead-letterqueue empty; all original queues empty;flushallbefore suite - DLQ state:
deadLetterQueueinstance initialized withconnection: 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_startedlog withjobId,jobName,practiceId,attempt, andqueue - Every job completion emits a
job_completedlog withdurationMs> 0 - Every job failure emits a
job_failedlog witherrormessage andattemptcount - Every DLQ move emits a
job_dlq_movedlog witherrorandattempts - Health endpoint
/api/health/jobsreturns200with accurate queue counts for all 8 queues + DLQ - Health endpoint returns correct
totalWaiting,totalActive,totalFailedaggregations - 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_URLforioredis-mock;ADMIN_API_KEY(if health endpoint is protected) - Required external mocks:
logger(mocked Pino or equivalent) to capture and assert log objects;email.sendfor 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#
-
generateJobIdproduces deterministic, unique keys in formatpracticeId:jobType:date - Duplicate job with same
jobIdis 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_CAPreason - 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:
practicewithmonthlyAiSpendandmonthlyAiBudgetfields;locationwithweeklyPostCountfield;oauthTokenwithexpiresAtfield;citationanddirectorySubmissiontables - 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;
flushallbetween 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
-
flushallreliably 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 (
mswhandlers for Google, social platforms, email, AI, Firecrawl) - Mock Redis state:
ioredis-mockwithkeepData: false;flushallinbeforeEachandafterAll - Queue state: All 8 queues + DLQ initialized empty; workers created but not started until test action
- Time control:
vi.useFakeTimers()withDate.now()mockable for cron and sleep tests - Test factories:
createMockPractice(),createMockLocation(),createMockDirectory()available fromsrc/__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