WFDASH-WEBAPP/docs/dev-guide.md
SJCoetz 7df7a8850f feat(item-history): add getItemHistory service with 5 PB queries
Co-authored-by: Orca <help@stably.ai>
2026-07-29 14:51:58 +02:00

236 lines
12 KiB
Markdown

# WFDASH Dev Guide — tests, "is it working?", debugging
Last updated: 2026-07-10. For project-wide docs see `docs/AGENT_INSTRUCTIONS.md`.
---
## 1. How tests work in this project
### What runs the tests
- **Framework:** [Vitest](https://vitest.dev/) (configured in `vitest.config.ts`).
- **Run everything:** `npx vitest run` (single pass) or `npx vitest` (watch mode).
- **Run one file:** `npx vitest run src/lib/server/quotes-service.test.ts`.
- **Run one test name:** `npx vitest run -t "starts the month at 001"`.
- **Type-check only:** `npx tsc --noEmit`. (No `ts-jest` step — TS errors surface here, not in Vitest.)
- **Lint:** `npm run lint` (ESLint flat config; not part of the test run, but the same files are scanned).
### Where the tests live
- `src/lib/server/*.test.ts` — server-side services (PocketBase, business logic). These are the
"real" tests. Heavy use of `vi.hoisted` + a stateful PB mock so the test runs without a live
PocketBase.
- `src/lib/pb-helpers.test.ts` and similar — client-side helpers.
- `src/components/*.test.tsx` — React component tests with `@testing-library/react`.
- `src/app/(app)/**/page.test.tsx` and `*.test.ts` — page-level tests.
- `src/app/api/v1/**/route.test.ts` — API route tests (mock the handler or the underlying service).
- `e2e/*.spec.ts` — Playwright end-to-end (only a handful; see `docs/qa-review.md` for the gap).
### How the PocketBase mock is built
The interesting part of this codebase's tests is the **stateful PB mock**. Look at
`src/lib/server/quotes-service.test.ts` for the canonical example.
The shape:
```ts
const { store, pbMock, clearStore } = vi.hoisted(() => {
// store: { [collectionName]: Map<id, row> }
// pbMock: object that quacks like a PocketBase instance
return { store, pbMock, clearStore };
});
vi.mock("pocketbase", () => ({
default: vi.fn().mockImplementation(() => pbMock),
}));
vi.mock("@/lib/server/pocketbase-admin", () => ({
getAdminPocketBase: vi.fn().mockResolvedValue(pbMock),
}));
beforeEach(() => clearStore());
```
Why the `vi.hoisted`? Because `vi.mock` factories get hoisted above all imports — if you reference
`pbMock` at the top level before declaring it, you get `Cannot access 'pbMock' before
initialization`. `vi.hoisted` lets you set up the mock state in a place that survives the hoisting.
The mock supports:
- `pb.collection(name).getList(page, perPage, { filter, sort })` — implements a tiny `where`
clause parser (`field = value` with `&&`) and a string/number sort. Good enough for most tests.
- `pb.collection(name).getFirstListItem(filter)` — throws 404 if nothing matches. Mirrors PB.
- `pb.collection(name).create(data)` — generates a mock id. Enforces unique `(year, month)` on the
`*_counters` collections so the race-handling path can be exercised.
- `pb.collection(name).update(id, data)` — handles the `field+` syntax used by PB's `last_seq+`
operator. Returns a clone so concurrent callers see the right value at their update moment.
When you're writing a new test, the most important rule: **seed the mock store with whatever the
service expects to find** (the right `quote` reference, the right `pool_status`, etc.). Forgetting
to seed a row that the service then tries to find is the #1 cause of mysterious test failures.
### What tests are NOT
- Tests don't open a real PocketBase. If you want to test against a live PB, do it via a Playwright
spec or a curl script — Vitest is the wrong tool.
- Tests don't open a real browser unless explicitly using `@testing-library/react`'s JSDOM env.
Page-level component tests do mount the component tree, but the network layer is mocked.
- The CI gate is `npx tsc --noEmit && npx vitest run` (plus `npm run lint` if you add it to your
pipeline). Currently no `ts-jest` step.
### Common test patterns in this codebase
- **Service with side effects:** mock `pocketbase-admin` so `getAdminPocketBase` returns the
stateful mock. Seed the store in the test body. Assert on the store after the call.
- **Component with hooks:** use `@testing-library/react`'s `render` + `screen.getByText(...)`. Mock
the data hooks (or the data fetching function) at the module boundary.
- **API route:** the test usually imports the route handler directly and calls it with a
`NextRequest` mock. Auth is mocked via the `agent-auth` module.
---
## 2. How do I know the app is working?
Three signals, ordered from cheapest to most thorough.
### Signal 1 — Tests pass
```sh
npx tsc --noEmit # 1 type error is the current pre-existing baseline
npx vitest run # 490/492 passing, 2 pre-existing failures
```
The 2 pre-existing failures are well-known and unrelated to recent work:
- `quotes-service.test.ts > createQuoteFromEmail > creates a new quote, customer, company, items,
and attachments` — the service was refactored to upload attachments in a separate API call, but
this test still asserts the old "1 attachment" inline behaviour. The service is correct; the
test is stale.
- `collection-pool/page.test.tsx > CollectionPoolPage sort control > defaults to operational sort
and reorders rows when nearby suppliers is selected` — UI test fixture mismatch with the
"nearby" sort comparator. Not a code bug.
Both are documented in `docs/qa-review.md`.
### Signal 2 — Local dev server boots and a smoke run succeeds
```sh
npm run dev
# in another terminal:
curl http://localhost:3000/api/health
# expect: {"status":"ok",...}
```
The `/api/health` route is a tiny liveness probe — if it returns 200, the app booted, the env vars
loaded, and the PB admin client authenticated.
For a fuller smoke, the four canonical end-to-end checks (each takes <30s):
1. **Quote intake** — POST a synthetic email to the n8n webhook
(`POST /webhook/wf-test-quote-email-monitor`) with a `from_email`, `subject`, and `textPlain`
body. Expect a `quotes` row + `quote_items` rows in PB within a few seconds.
2. **Order creation** — open an existing `approval_pending` quote in the UI, click "Convert to
order". Expect a new `orders` row with `order_number` matching `ORD-YYYY-MM-NNN`.
3. **PDF dispatch** — on a `mail_failed` quote, click "Retry". Expect status to flip to `sending`,
then `emailed` (or `mail_failed` again if SMTP is down).
4. **Counter atomicity** — call the quote/order number generators twice in quick succession
(`/api/v1/quotes/next-number` and the order service equivalent). Expect two distinct numbers.
### Signal 3 — Production signals
If `n8n` is the only thing that creates quotes, the end-to-end signal is:
1. **n8n execution success rate.** Workflow `eXSh623OjByIcip8` (the live "Quote Email Monitor")
should show 0 failures in the last 24h. The n8n UI's Executions tab is the source of truth.
2. **PB `quotes.email_message_id` uniqueness.** Every new quote has a unique `email_message_id`;
duplicates mean the dedup path is broken.
3. **PB `quotes.quote_number` format.** Every new row matches `^Q\d{4}\d{2}\d{3}$` (e.g.
`Q202607001`). Drift means the counter is misbehaving.
4. **PB `orders.order_number` format.** Every new row matches `^ORD-\d{4}-\d{2}-\d{3}$`. Drift
means the order counter or the prefix-derivation logic is broken.
5. **PB `quote_counters` and `order_counters` rows.** Both should exist and have at least one row
for the current (year, month). Missing collection = `npm run setup` wiped it = re-run
`scripts/migrations/2026-07-10-restore-counters.ts`.
If anything in signals 1-3 looks wrong, jump to section 3.
---
## 3. How do I find something that is broken?
A practical triage flow:
### Step 1 — Reproduce
If the user reported the bug, try to repro it. If you can't repro locally, check production logs
before going deeper.
### Step 2 — Look at the logs
- **n8n Executions tab** for the relevant workflow. Click the failed execution → "Error" tab.
Almost always a JSON dump with the failing node and the exception.
- **Next.js dev console** (the `npm run dev` terminal). Stack traces with file:line.
- **PocketBase logs.** PB UI → Settings → Logs. Includes auth failures, schema-validation
rejections, and crashed hooks.
- **Coolify container logs.** `coolify logs -f <container>` for the WFDASH service. Surfaces
build/runtime errors that don't make it to stdout.
### Step 3 — Open PocketBase and look at the data
`https://<your-pb-host>/_/` (admin login). The collections that drive 80% of the bugs:
| Collection | What to look for |
| --- | --- |
| `quotes` | `quote_number` format, `status`, `email_message_id`, `has_attachments` |
| `quote_items` | linked to a `quote`, `is_processed` flag, prices populated |
| `orders` | `order_number` format, `quote` reference, items linked |
| `order_items` | `order` reference, `supplier`, `item_status` |
| `quote_counters` | exists? at least one row for current month? |
| `order_counters` | exists? at least one row for current month? |
If a counter collection is missing — yes, this has happened before — re-run
`scripts/migrations/2026-07-10-restore-counters.ts` to restore it. The migration is idempotent.
### Step 4 — Run the test that should cover the broken path
If a service is misbehaving, the test for it is usually right next to it
(`src/lib/server/foo-service.test.ts`). Run just that file with
`npx vitest run src/lib/server/foo-service.test.ts`. If the test passes, the bug isn't in the
service — it's upstream (caller, schema, env) or downstream (UI, transport).
### Step 5 — Add a failing test first
Before fixing, write the test that would have caught the bug. If you can't write one in 10
minutes, the bug is probably in the data (PB), not the code. Run it — confirm it fails. Then fix
the code. Then re-run — confirm it passes. Commit both.
### Common broken things and their quick checks
| Symptom | First check |
| --- | --- |
| Quote number is `Q-2026-07-001` (dashed, not no-dash) | You're hitting the random fallback in `quote-form.tsx:189` — the API route or the counter path failed. Check `quote_counters` exists. |
| Quote number generator crashes with 404 | `quote_counters` collection is missing. Re-run the restore migration. |
| Order number is `ORD-2026-07-001` but the last order was `ORD-2026-06-030` | Code is using wall-clock month instead of latest-order's month. Shouldn't happen with current code — verify you're not on a stale checkout. |
| Two orders have the same `order_number` | Counter increment lost a race — should be impossible with `last_seq+`. If it happens, file a bug; the migration also enforces a unique sequence per (year, month). |
| Tests are flaky on CI but pass locally | The PB mock is stateful across tests. Make sure `beforeEach(() => clearStore())` is in place. Look for shared state between `describe` blocks. |
| `npm run setup` wiped everything | The destructive script (`scripts/setup-pocketbase.ts`) has no `--confirm-wipe` guard yet. Re-run `scripts/migrations/2026-07-10-restore-counters.ts` after each wipe. (P1 from the QA review — open ticket.) |
| TypeScript error: `'setBy' does not exist in type 'Pick<InvoiceDetails, "number" \| "date">'` | Pre-existing in `collection-confirm/view-model.test.ts:278`. Not blocking tests; tracked in the QA review. |
### Step 6 — Ask the agent
If you've gone through steps 1-5 and you're stuck, drop the symptom + the log line + the file/line
you suspect into the agent. The agent has a fuller view of the codebase, can grep across files,
and can run reproductions. Don't try to fix something you don't understand yet — the bug usually
moves once you prod it.
---
## Quick reference
- **Test runner:** `npx vitest run` (or `npx vitest` for watch)
- **Type check:** `npx tsc --noEmit`
- **Lint:** `npm run lint`
- **Dev server:** `npm run dev`
- **Health check:** `curl http://localhost:3000/api/health`
- **PB admin UI:** `https://<pb-host>/_/`
- **Restore counters (idempotent):** `npx tsx scripts/migrations/2026-07-10-restore-counters.ts`
- **QA review (read this if you're not sure if something is a bug):** `docs/qa-review.md`