12 KiB
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 (configured in
vitest.config.ts). - Run everything:
npx vitest run(single pass) ornpx 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. (Nots-jeststep — 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 ofvi.hoisted+ a stateful PB mock so the test runs without a live PocketBase.src/lib/pb-helpers.test.tsand similar — client-side helpers.src/components/*.test.tsx— React component tests with@testing-library/react.src/app/(app)/**/page.test.tsxand*.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; seedocs/qa-review.mdfor 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:
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 tinywhereclause parser (field = valuewith&&) 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*_counterscollections so the race-handling path can be exercised.pb.collection(name).update(id, data)— handles thefield+syntax used by PB'slast_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(plusnpm run lintif you add it to your pipeline). Currently nots-jeststep.
Common test patterns in this codebase
- Service with side effects: mock
pocketbase-adminsogetAdminPocketBasereturns the stateful mock. Seed the store in the test body. Assert on the store after the call. - Component with hooks: use
@testing-library/react'srender+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
NextRequestmock. Auth is mocked via theagent-authmodule.
2. How do I know the app is working?
Three signals, ordered from cheapest to most thorough.
Signal 1 — Tests pass
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
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):
- Quote intake — POST a synthetic email to the n8n webhook
(
POST /webhook/wf-test-quote-email-monitor) with afrom_email,subject, andtextPlainbody. Expect aquotesrow +quote_itemsrows in PB within a few seconds. - Order creation — open an existing
approval_pendingquote in the UI, click "Convert to order". Expect a newordersrow withorder_numbermatchingORD-YYYY-MM-NNN. - PDF dispatch — on a
mail_failedquote, click "Retry". Expect status to flip tosending, thenemailed(ormail_failedagain if SMTP is down). - Counter atomicity — call the quote/order number generators twice in quick succession
(
/api/v1/quotes/next-numberand 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:
- 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. - PB
quotes.email_message_iduniqueness. Every new quote has a uniqueemail_message_id; duplicates mean the dedup path is broken. - PB
quotes.quote_numberformat. Every new row matches^Q\d{4}\d{2}\d{3}$(e.g.Q202607001). Drift means the counter is misbehaving. - PB
orders.order_numberformat. Every new row matches^ORD-\d{4}-\d{2}-\d{3}$. Drift means the order counter or the prefix-derivation logic is broken. - PB
quote_countersandorder_countersrows. Both should exist and have at least one row for the current (year, month). Missing collection =npm run setupwiped it = re-runscripts/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 devterminal). 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(ornpx vitestfor 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