Mock global.fetch in the test-connection test so it no longer hits the
real network. The route has a 10s fetch timeout, but the test used the
default 5s vitest timeout, causing intermittent failures when DNS/network
was slow. Add afterEach(vi.restoreAllMocks) to clean up between tests,
and reconcile the misleading "returns 400" test title to match the 200
assertion the route actually produces.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
UAT_PLAYBOOK.md §3 — new test case TC-API-3.30:
trigger reset-demo-data CronJob manually, confirm job succeeds,
public tables/enums and drizzle schema survive, demo data reseeded,
/api/readyz stays 200.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
GRO-2722: packages/db/src/reset.ts and apps/api/src/db/reset.ts no longer
emit any DROP TABLE / DROP TYPE / DROP SCHEMA / DROP DATABASE DDL. The four
destructive DO-blocks are replaced with a single:
TRUNCATE <all public tables> RESTART IDENTITY CASCADE
Table names are enumerated dynamically via pg_tables WHERE schemaname='public'
so new tables are picked up automatically. The drizzle schema and
__drizzle_migrations table are untouched (different schema), keeping
drizzle-kit migrate a no-op on an already-migrated DB.
Preserves: production guard (NODE_ENV=production && ALLOW_RESET!='true'),
advisory lock, migrations (drizzle-kit migrate), and seed (runSeedBody).
Fixes the GRO-2678 prod outage root cause.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
/api/readyz is a public monitoring endpoint like /api/health.
app.basePath("/api") + api.use("*", authMiddleware) applies to all
/api/* paths regardless of route registration order (GRO-2692 UAT
failure: Hono basePath middleware bypass).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
/api/readyz was returning 401 Unauthorized in UAT (GRO-2692 Shedward
regression). The authMiddleware for /api/* did not whitelist /api/readyz,
causing every unauthenticated probe request to be rejected.
Add /api/readyz to the bypass condition alongside /api/auth/* and
/api/health. Add readyz-auth-bypass.test.ts confirming the route passes
through the middleware without auth and that non-whitelisted paths still
block (503 when auth not configured).
Closes GRO-2692 regression (UAT fail).
CTO architectural ruling (PR #235 review): K8s readiness/liveness probes
must remain DB-less to prevent transient DB blips from cycling pods. The
/api/readyz endpoint (GRO-2687) is the canonical DB-health signal for the
monitoring layer and satisfies the GRO-2678 detection-gap requirement.
Removes:
- GET /health/ready route from src/index.ts
- src/__tests__/health-ready.test.ts
Refs GRO-2689, GRO-2687, GRO-2678
Register GET /health/ready before the /api/* auth middleware so it is
public and reachable by K8s readinessProbe on port 3000 without auth.
On success → 200 {"status":"ready"}; on any DB/schema failure → 503
{"status":"degraded"}. Logs the pg error code; never leaks SQL in body.
A dropped schema (42P01) surfaces as non-200, closing the /health mask
that allowed the GRO-2678 incident to go undetected for ~43h.
Add health-ready.test.ts covering the 200 success path, 503 on schema
drop (42P01), and 503 on connection error; all assert no SQL leakage.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Register GET /api/readyz after /api/health in src/index.ts (before authMiddleware)
- Runs `getDb().select({id: staff.id}).from(staff).limit(1)` inside try/catch
- Success → 200 {"status":"ready"}; failure → 503 {"status":"degraded","check":"db"}
- Raw driver error is console.error'd but never leaked to the response body
- /health and /api/health are unchanged (K8s probes remain DB-less per CTO decision)
- New unit tests: mock getDb to resolve → assert 200/ready; throw → assert 503/degraded
- Updated UAT_PLAYBOOK.md §4.0 with TC-API-0.2 and TC-API-0.3
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Root cause: drizzle-orm's HWM-based migrate() marked migrations as applied in the ledger but rolled back schema changes when it detected a dirty state. Replacing with execSync("pnpm exec drizzle-kit migrate") matches the K8s migrate-schema Job, ensuring reset leaves a fully-migrated schema before seeding. Fixes reset-demo-data CronJob failures and resolves seed-test-data relation-does-not-exist reconcile loop.
QA approved: gb_lint (PR #229, head f7f90a7)
drizzle-orm's migrate() has a high-water-mark (HWM) bug: on a fresh DB,
migration 0000 sets the watermark to 2026-03-17. Migrations 0001, 0003,
0010, 0011 have stale 2025-era `when` timestamps and are silently
skipped. Migration 0003 (recurring_series) is the blocker — its skip
leaves `recurring_series`, `appointments.series_id`, and
`appointments.series_index` missing. A downstream migration inside
migrate()'s single Postgres transaction then fails, rolling back
everything including 0000's `staff` and `services` tables.
Replace the drizzle-orm migrate() call with `pnpm exec drizzle-kit
migrate` (hash-based). drizzle-kit applies every unhashed migration
regardless of `when` ordering, matching the K8s migrate Job exactly.
Merge origin/main into uat to resolve divergence (main was 27 commits
ahead). Only real conflict: UAT_PLAYBOOK.md — kept uat's §4.19 Boot
Resilience and §4.20 OOBE clients-from-auth test cases; excluded
.mcp.json and trigger-uat-1779751324.txt from merge result (CTO review
5042 — these must not land on main).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Merge pull request 'GRO-2342: portal/appointments — symmetric service {id, name} on both card paths' (#211) from release/main-GRO-2342-api into main
GRO-2342: GET /portal/appointments populates service: {id, name} on the synthetic waitlist card (was {id} only) and on the appointment card (consistent shape). TC-API-8.20 in UAT_PLAYBOOK.md.
Approved CTO. Squashed from release/main-GRO-2342-api @ c737bfe.
Refs: GRO-2342, GRO-2344, GRO-2345, GRO-2346, PR #211.
Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Promote GRO-2172 from uat to main. Pins src/routes/pets.ts to its exact
content at uat merge commit c4385617 (PR #200), adding the extended pet
profile fields to createPetSchema/updatePetSchema and wiring medicalAlerts
into POST/PATCH /pets:
- temperamentScore: int 1–5
- temperamentFlags: string[] (≤20, each ≤100 chars)
- medicalAlerts: {type,description,severity}[] (≤50)
- preferredCuts: string[] (≤20, each ≤200 chars)
- coatType already present on main; schema now references all 5 fields
Based on main HEAD (03f79a37) so the PR diff is limited to src/routes/pets.ts.
GRO-2311 (uat HEAD 807ccb45) is intentionally excluded.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Duplicate 'provenance: false' in each docker/build-push-action step caused
Gitea to reject the workflow file, breaking push CI and workflow_dispatch.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
| TC-API-0.3 | DB-touching readiness check — response body safe | GET /api/readyz and inspect body | Body contains only `status` and (on error) `check` fields — no raw SQL, driver messages, or stack traces |
> **Note (GRO-1544):** Health endpoint registered on `api` basePath before auth middleware at `/api/health`. The old path `/health` was incorrect (routed to web pod via HTTPRoute `/*` rule).
> **Note (GRO-2678):** `/api/readyz` is a separate DB-touching endpoint for monitoring. It is intentionally NOT used for K8s liveness/readiness probes — those remain DB-less to avoid pod cycling on transient DB blips.
### 4.1 Authentication
@@ -193,6 +196,7 @@ Geocoding turns a client's street address into `latitude`/`longitude` + `geocode
| TC-API-3.27 | Verify coat_type enum has all seed values | After UAT seed completes, inspect the coat_type enum on the UAT DB — it must contain: short, medium, long, double, wire, silky, curly, hairless | UAT seed jobs (`reset-demo-data`, `seed-test-data`) complete 1/1 with no `enum_in` error; coat_type includes all 8 values used by seed.ts `coatTypePool` |
| TC-API-3.28 | Verify pet_size_category enum has all seed values | After UAT seed completes, inspect the pet_size_category enum on the UAT DB — it must contain: small, medium, large, extra_large | UAT seed jobs (`reset-demo-data`, `seed-test-data`) complete 1/1 with no `enum_in` error; pet_size_category includes all 4 values used by seed.ts `petSizeCategoryPool` (regression for GRO-1999, mirrors TC-API-3.27) |
| TC-API-3.29 | Verify `reset-demo-data` CronJob does not fail with FK 23503 on `invoice_tip_splits` (GRO-2123) | Trigger the CronJob manually: `kubectl create job --from=cronjob/reset-demo-data verify-gro2123 -n groombook-uat`. Wait for pod to terminate. Inspect logs: `kubectl logs -n groombook-uat -l job-name=verify-gro2123` | Pod reaches `Completed` state; logs show `✓ Acquired seed advisory lock` and `✓ Released seed advisory lock` from `seed.ts`; no `PostgresError: … violates foreign key constraint "invoice_tip_splits_invoice_id_invoices_id_fk"` (code 23503); final counts unchanged (500 clients, ~4000 invoices) |
| TC-API-3.30 | Verify `reset-demo-data` CronJob is schema-safe — TRUNCATE only, no DDL drops (GRO-2722) | 1. Trigger: `kubectl -n groombook-uat create job --from=cronjob/reset-demo-data reset-verify-$(date +%s) --dry-run=client -o name \| xargs kubectl -n groombook-uat apply -f -` or simply `kubectl -n groombook-uat create job --from=cronjob/reset-demo-data reset-verify-$(date +%s)`. 2. Wait for job completion: `kubectl -n groombook-uat wait job/reset-verify-<ts> --for=condition=complete --timeout=300s`. 3. Check logs: `kubectl -n groombook-uat logs job/reset-verify-<ts>`. 4. Verify schema survival: `kubectl -n groombook-uat exec deploy/api -- psql $DATABASE_URL -c "\dt public.*"` and `kubectl -n groombook-uat exec deploy/api -- psql $DATABASE_URL -c "\dt drizzle.*"`. 5. Verify readyz: `curl -s https://uat.groombook.dev/api/readyz` | Job reaches `Completed` (exit 0); logs show `✓ All public tables truncated, sequences reset` — no `DROP TABLE`/`DROP TYPE`/`DROP SCHEMA` in output; all `public.*` tables still present after reset; `drizzle.__drizzle_migrations` still populated (row count unchanged); `https://uat.groombook.dev/api/readyz` returns HTTP 200; demo data reseeded (500 clients visible via GET /api/clients) |
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.