236 lines
12 KiB
Markdown
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`
|