WFDASH-WEBAPP/docs/dev-guide.md
SJCoetz cdaa2e2b25 fix, feat, style: standardize quote numbers, polish UI
- Standardize quote number format from Q-YYYY-MM-NNN to QYYYYMMNNN across all client, server, test and API code
- Add atomic backfill logic for quote counters to avoid sequence collisions with existing records
- Rearrange Delivery Notes and Repairs to the Operations sidebar section
- Add supplier count badge to pricelist catalogue and improve filter tab overflow styling
- Optimize font loading in root layout to skip preloading unused font weights
- Improve table styling for customer and company pages with better text wrapping and empty states
- Fix rep display logic in quotes page to show "Unassigned" when no rep is assigned
- Add supporting documentation, PocketBase counter schemas and setup migration scripts
- Remove embedded SupplierMap from suppliers page for cleaner layout
- Improve invoice card layout for company and customer info spacing
- Improve filter label styling and accessibility in collection map page
2026-07-10 22:21:50 +02:00

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) 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:

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

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):

  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