Skip to content

test(service): add vitest coverage for ingest, routing, render, crons#4

Merged
mattdecrevel merged 1 commit into
mainfrom
claude/loop-service-tests
May 28, 2026
Merged

test(service): add vitest coverage for ingest, routing, render, crons#4
mattdecrevel merged 1 commit into
mainfrom
claude/loop-service-tests

Conversation

@mattdecrevel

Copy link
Copy Markdown
Owner

Expands the service-side vitest suite from 64 → 127 tests across 6 new files, covering the gaps in event validation, ingest pipeline, routing precedence, render house style, and cron handlers.

What changed

New tests (tests/)

File Tests Coverage
tests/ingest-validate.test.ts 19 Every event type (error, signup, subscription, feedback, cron, infra, booking, contact, seo_report, generic, raw) round-trips with the correct default category. Missing required fields, unknown types, unknown sub-categories, and missing category for generic/raw are rejected. Explicit category override beats the type default.
tests/ingest-pipeline.test.ts 5 ingestEvent posts to a channel destination, posts to a webhook destination, records skipped when no route resolves, records failed on transport error, and bypasses the transport entirely on digest:true info events.
tests/routing-pickroute.test.ts 5 Webhook URL targets resolve correctly. Unknown category falls through to a global rule. Most-specific match wins regardless of input order. null returned when only other-project rules exist.
tests/render-types.test.ts 19 Per-event-type baseline snapshots (text + key emoji + title) — one per type — locking the house style. infra severity override (info → 📡, warning → 📡, error → 🔴). links add URL buttons. footerNote renders as a context line. actions: ['todo']add_todo. actions: ['remind']remind_1h + remind_24h.
tests/digest-group.test.ts 8 groupPendingForDigest returns [] for empty input, creates one group per (projectId, category), treats null projectId as its own bucket. digestLineFor payload-field preference order: title → message → name → email → type.
tests/cron-digest.test.ts 3 GET /api/cron/digest returns 401 without bearer, { ok: true, groups: 0, posted: 0 } for empty pending, and groups multi-category events into 2 posts to the resolved channel.
tests/cron-reminders.test.ts 4 GET /api/cron/reminders returns 401 without bearer, { due: 0, sent: 0 } when nothing due, posts each due reminder (threading via messageTs when present) and flips status to sent, and does NOT flip status when the transport returns ok: false.

Production refactor (minimal, testability-only)

  • lib/digest/group.ts (new) — extracts the pure (projectId, category) grouping logic + digestLineFor from app/api/cron/digest/route.ts. The route now imports them. Behaviour unchanged.

CI

  • .github/workflows/test-service.yml (new) — sibling to test-client.yml. Triggers on PRs / main pushes touching app/**, lib/**, tests/**, vitest.config.ts, vercel.json, package.json, pnpm-lock.yaml. Runs pnpm test. Does not overlap with the client workflow's packages/client/** scope.

Mocking decisions

  • @/lib/db mocked at the vitest module boundary — each test stubs the select / insert / update chain it actually exercises. Where the route handler awaits a where(...) result, the stub returns a thenable.
  • @/lib/slack/transport (postToChannel / postToWebhook) mocked so no test hits the Slack API.
  • @/lib/routing.resolveDestination mocked from the ingest tests so route precedence is exercised separately in pure-function tests against pickRoute.
  • No Docker, no real Neon, no sqlite drizzle adapter. The existing vitest.config.ts DATABASE_URL dummy stays in place — postgres-js is lazy and the mocks intercept before any query is issued.

Verification

  • pnpm test → 16 files, 127 tests pass (0.7s).
  • npx tsc --noEmit clean.
  • pnpm lint clean.

Out of scope (intentionally)

  • packages/client/** — already covered by test-client.yml. Untouched.
  • Live DB / live Slack integration tests — out of scope per the test plan; the unit-level mocks are sufficient for the targeted invariants.

🤖 Generated with Claude Code

Expands the existing tests/ suite from 64 → 127 tests across 6 new files,
covering the gaps called out in the test plan:

- ingest-validate: every event type round-trips with the correct default
  category; missing/invalid required fields are rejected
- ingest-pipeline: channel + webhook destinations, skipped/failed status,
  digest path does not invoke any transport
- routing-pickroute: webhook URL targets, unknown category fall-through,
  order-independent most-specific-wins
- render-types: per-event-type baseline snapshots + severity overrides on
  infra + links/footerNote primitives + to-do/remind action_id identity
- digest-group: pure grouping helper extracted from app/api/cron/digest
  (lib/digest/group.ts) so the bucketing logic is testable in isolation
- cron-digest: route-level auth, empty-pending short-circuit, multi-category
  grouped fan-out
- cron-reminders: route-level auth, empty-due short-circuit, per-reminder
  post + mark-sent, no status flip on transport failure

Mocks @/lib/db and @/lib/slack/transport at the vitest module boundary —
no Postgres, no Slack API. Also adds .github/workflows/test-service.yml
(sibling to test-client.yml) scoped to app/**, lib/**, tests/**, and root
config files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 28, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

Hobby accounts are limited to daily cron jobs. This cron expression (*/5 * * * *) would run more than once per day. Upgrade to the Pro plan to unlock all Cron Jobs features on Vercel.

Learn More: https://vercel.link/3Fpeeb1

@mattdecrevel mattdecrevel merged commit 13ef4b1 into main May 28, 2026
1 of 2 checks passed
@mattdecrevel mattdecrevel deleted the claude/loop-service-tests branch May 28, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant