Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/docs/architecture/auth-and-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,12 @@ add it to the `$GroupScopableSubjectName` exclusion.
_and_ a deny.
- `src/auth/__tests__/ability.utils.test.ts` — pins the `undefined` ability behavior and
`forcedAppSubject`.
- `src/auth/guards/__tests__/jwt-auth.guard.spec.ts` — the guard's own branches.
- `src/auth/guards/__tests__/jwt-auth.guard.test.ts` — the guard's own branches.

Service tests mock the Prisma layer, so they verify the arguments passed to the model, not the
database's answer. Assert the `where` clause the same way the existing specs assert `data`:
`model.<method>.mock.lastCall?.[0]` plus `toMatchObject` — see
`src/groups/__tests__/groups.service.spec.ts`.
`src/groups/__tests__/groups.service.test.ts`.

Row-level scoping is not observable in a unit test with a mocked model, which is why an end-to-end
test in `testing/` is required for anything that changes who can see what.
14 changes: 7 additions & 7 deletions .agents/docs/architecture/testing-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ of operations.

## Tiers

| Tier | Runner | Where | Command |
| ---------- | ---------- | -------------------------------------------------------- | -------------------- |
| Unit | vitest | `src/__tests__/` (or `test/`) in a participating package | `pnpm test:coverage` |
| End-to-end | Playwright | `testing/src/specs/` | `pnpm test:e2e` |
| Type-check | `tsc` | — | `pnpm lint` |
| Tier | Runner | Where | Command |
| ---------- | ---------- | --------------------------------------------------------------------- | -------------------- |
| Unit | vitest | `__tests__/<file>.test.ts` beside `<file>.ts`, or a package's `test/` | `pnpm test:coverage` |
| End-to-end | Playwright | `testing/src/specs/` | `pnpm test:e2e` |
| Type-check | `tsc` | — | `pnpm lint` |

There is no integration tier. `apps/api` unit tests mock the Prisma layer entirely; the only code
path exercised against a real database is Playwright.
Expand All @@ -33,7 +33,7 @@ package's config, which is **not** always the directory name.

| Project | Package | Notable config |
| ------------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api` | `apps/api` | libnest SWC plugin; `globals: true`; extra `include` for `src/**/*.spec.ts` |
| `api` | `apps/api` | libnest SWC plugin, confined to `apps/api` sources; `globals: true` |
| `web` | `apps/web` | `environment: 'happy-dom'`; redeclares the `@` alias |
| `gateway` | `apps/gateway` | node environment; redeclares the `@` alias. Request schemas, `resolveLanguage`, and the HTML `src/entry-server.tsx` renders — the routers and hydration are covered by `testing/src/specs/gateway-*.spec.ts`. A component test opts into happy-dom per file with a `// @vitest-environment happy-dom` docblock |
| `instrument-bundler` | `packages/instrument-bundler` | |
Expand Down Expand Up @@ -75,7 +75,7 @@ Read the canonical file before writing a test in that tier.

| Package | Canonical file | Shape |
| ----------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apps/api` | `src/groups/__tests__/groups.service.spec.ts` | `Test.createTestingModule` + `MockFactory.createForModelToken(getModelToken('Group'))` from `@douglasneuroinformatics/libnest/testing`; mocks typed `MockedInstance<Model<'Group'>>`; assert with `model.create.mock.lastCall?.[0]` and `toMatchObject`. A fresh module per test, so no mock state to reset. |
| `apps/api` | `src/groups/__tests__/groups.service.test.ts` | `Test.createTestingModule` + `MockFactory.createForModelToken(getModelToken('Group'))` from `@douglasneuroinformatics/libnest/testing`; mocks typed `MockedInstance<Model<'Group'>>`; assert with `model.create.mock.lastCall?.[0]` and `toMatchObject`. A fresh module per test, so no mock state to reset. |
| `apps/web` | `src/hooks/__tests__/useInstrumentBundle.test.ts` | `vi.hoisted` + `vi.mock` for `axios` and `@/store`; a **real** `QueryClient` per test with `retry: false`, wrapped in `QueryClientProvider`. |
| `packages/react-core` | `src/components/InstrumentRenderer/__tests__/SeriesInstrumentRenderer.test.tsx` | Renders a component against hand-written bundles — an async IIFE per instrument, evaluated by the real interpreter. `i18n.init({ translations: {} })` in `beforeAll`, `afterEach(cleanup)`, and `fireEvent` rather than `userEvent`, which is not installed. |
| `packages/schemas` | `src/instrument/__tests__/instrument.form.test.ts` | Parse fixtures imported from `@opendatacapture/instrument-stubs/*`, never hand-written literals, so schema and fixture cannot drift. |
Expand Down
2 changes: 1 addition & 1 deletion .agents/docs/packages/libnest.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ return { client } satisfies PrismaModuleOptions;

Everywhere else, inject a model: `@InjectModel('Group') private readonly groupModel: Model<'Group'>`.

### Unit tests (`apps/api/src/**/__tests__/*.spec.ts`)
### Unit tests (`apps/api/src/**/__tests__/*.test.ts`)

```ts
import { getModelToken } from '@douglasneuroinformatics/libnest';
Expand Down
4 changes: 2 additions & 2 deletions .agents/docs/playbooks/add-api-endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ AppSubject` in `apps/api/prisma/schema.prisma` and `$AppSubjectName` in
(`apps/api/src/auth/ability.factory.ts`); that table is typed over `$GroupScopableSubjectName`,
so `tsc` reports the omission.

9. **Service spec** at `apps/api/src/<feature>/__tests__/<feature>.service.spec.ts`. Copy the setup
from `apps/api/src/groups/__tests__/groups.service.spec.ts`:
9. **Service spec** at `apps/api/src/<feature>/__tests__/<feature>.service.test.ts`. Copy the setup
from `apps/api/src/groups/__tests__/groups.service.test.ts`:
`MockFactory.createForModelToken(getModelToken('X'))` per injected model, then assert with
`model.<method>.mock.lastCall?.[0]` and `toMatchObject`. Assert the `where` clause, not only
`data`.
Expand Down
12 changes: 5 additions & 7 deletions .agents/docs/playbooks/add-vitest-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,11 @@ of your file means the config is absent.
the projects directly and never reads this script; `vitest` is a root devDependency, so the package
declares nothing.

7. **Place the test file inside the inherited globs.** Those are
`**/*.{test,spec}.?(c|m)[jt]s?(x)` and `**/*/test.?(c|m)[jt]s?(x)`, both relative to the package
`root`; the house convention is `src/__tests__/<subject>.test.ts`. Only reach for a project-level
`include` when a file must live outside them — `mergeConfig` concatenates arrays, so a project
`include` _adds_ to the root globs rather than replacing them. `apps/api` is the proof: its own
`include` is `['src/**/*.spec.ts', 'test/**/*.test.ts']`, and
`src/auth/__tests__/ability.utils.test.ts` matches neither, yet is collected.
7. **Place the test file inside the inherited glob.** That is `**/*.{test,spec}.?(c|m)[jt]s?(x)`,
relative to the package `root`; the house convention is a `__tests__/` folder beside the subject,
with the spec named after it (`src/__tests__/index.test.ts` tests `src/index.ts`). Only reach for
a project-level `include` when a file must live outside that glob — `mergeConfig` concatenates
arrays, so a project `include` _adds_ to the root glob rather than replacing it.

8. **Prove the file is collected, then prove it can fail.** If the package imports
`instrument-library`, `runtime-core` or `runtime/v1`, run `pnpm build` once first — those three
Expand Down
10 changes: 6 additions & 4 deletions .agents/docs/playbooks/add-web-route.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ That error is the expected state of a correct new route, not a bug to fix. Never
right `ability.can(...)`. `NavItem.url` is typed `url?: string`, so a wrong URL there compiles and 404s
at runtime — copy it from your `createFileRoute` literal.

9. **Put nothing else in `src/routes/`.** Tests go in `apps/web/src/__tests__/`, helpers in
`src/components/` or `src/utils/`. Importing `vitest` or `@testing-library/*` from `src/routes/`
is an eslint error.
9. **Put nothing else in `src/routes/` but the route's `__tests__/` folder.** Helpers go in
`src/components/` or `src/utils/`. The generator skips `__tests__/` folders only because of
`routeFileIgnorePattern` in `apps/web/vite.config.ts`, and importing `vitest` or
`@testing-library/*` anywhere else under `src/routes/` is an eslint error.

10. **Ask the user to regenerate the route tree** (it happens when they run `pnpm dev` for `web`).
Steps 11 and 12, and `pnpm lint`, cannot pass before this.
Expand All @@ -75,7 +76,8 @@ That error is the expected state of a correct new route, not a bug to fix. Never
`testing/src/generated/route.d.ts`. The new route literal does not exist for `getPageModel` or
the `pageModels` map until this runs. You may run this one yourself.

12. **Add the tests.** A unit test under `apps/web/src/__tests__/`, and an e2e test —
12. **Add the tests.** A unit test named after the route file, in a `__tests__/` folder beside it
(`routes/_app/admin/__tests__/mail.test.tsx` tests `routes/_app/admin/mail.tsx`), and an e2e test —
`.agents/docs/playbooks/add-e2e-test.md`, which covers the page object and registering it in
`testing/src/support/fixtures.ts`.

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/odc-agent-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ is optional and holds what no step owns (`add-blog-post.md`).
- **The trap comes before the conventions**, under `## Traps` or a heading that states the trap as a
sentence (`## Nothing in this directory is checked by anything`, `vendor/AGENTS.md`).
- **Name the canonical file to read** for each recurring task, by path, the way `apps/api/AGENTS.md`
names `src/groups/__tests__/groups.service.spec.ts`.
names `src/groups/__tests__/groups.service.test.ts`.
- **Close on how the work is checked** — `## Tests` giving `pnpm exec vitest --project <name>`, or
stating there is no project and what covers the behaviour instead (`packages/react-core/AGENTS.md`).
Where vitest is not the answer the section keeps another name (`docs/AGENTS.md`,
Expand Down
6 changes: 2 additions & 4 deletions .agents/skills/odc-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ and the e2e test carries the change, or this change adds its `vitest.config.ts`.
| A type contract — inference, a discriminated union | The type-check, which is `tsc` inside lint | `pnpm lint` |

**There is no integration tier, and the middle row is why.** `apps/api` unit tests mock the Prisma
layer entirely (`apps/api/src/groups/__tests__/groups.service.spec.ts`), so the only code path
layer entirely (`apps/api/src/groups/__tests__/groups.service.test.ts`), so the only code path
exercised against a database is Playwright: a permissions or scoping change that a green unit suite
appears to cover has not been tested at all. The type-check tier has no file of its own — a type
that must hold is asserted by code that would not compile if it broke.
Expand Down Expand Up @@ -73,9 +73,7 @@ package. Two things it does not carry:
and `env-cmd` exits before vitest starts when `.env` is absent.
- **happy-dom, which `apps/web` runs under, computes no layout**: `getBoundingClientRect` and
`offsetWidth` return 0, so assert on rendered content, never on geometry or visibility. happy-dom
≥ 20 supplies its own no-op `ResizeObserver`, so the stub in
`apps/web/src/__tests__/data-table-server-mode.test.tsx` never assigns — copying it buys nothing,
whatever `apps/web/AGENTS.md` §Tests still says about needing one.
≥ 20 supplies its own no-op `ResizeObserver`, so no test needs to stub one.

Writing a component test under `apps/web`? That same §Tests adds the `@/services/i18n` side-effect
import libui controls need.
Expand Down
5 changes: 3 additions & 2 deletions .agents/skills/odc-web-route/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ component's hook passes.

Add a `data-testid` to whatever the e2e test will click or assert on, then open
`.agents/docs/playbooks/add-e2e-test.md` before writing the spec — it owns the selector contract and
the fixture registration. The unit test goes where the change is: `src/hooks/__tests__/` for a query
hook, which is the usual case for a route, otherwise `src/utils/__tests__/` or `src/__tests__/`.
the fixture registration. The unit test goes in a `__tests__/` folder beside the file that changed,
named after it: `src/hooks/__tests__/` for a query hook, `src/routes/**/__tests__/` for the route
itself.

The playbook's `## Verify` block is the check to run, with one ordering caveat: `pnpm test:e2e`
boots `apps/web` through `pnpm dev:test` (`testing/playwright.config.ts`), which regenerates
Expand Down
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,11 @@ explains it.
tooling rearrange it. Where ordering is load-bearing, the existing code carries an explicit
`eslint-disable` comment; follow that pattern rather than fighting the rule.

**Tests.** Keep test bodies short; each verifies one behavior. Descriptions are concise, grammatical
**Tests.** A unit test is `<file>.test.ts(x)` in a `__tests__/` folder beside the file it tests:
`src/index.ts` is tested by `src/__tests__/index.test.ts`. A suite that drives a whole package, like
`apps/api/test/`, lives in that package's `test/` folder instead. Two eslint rules (`odc/*` in
`eslint.config.js`) enforce the layout; `packages/instrument-library` is the one exception, for the
reason its `AGENTS.md` gives. Keep test bodies short; each verifies one behavior. Descriptions are concise, grammatical
sentences stating the behavioral reason, e.g. `'should key the cache on every parameter, so paging
does not serve a stale page'`.

Expand Down
8 changes: 4 additions & 4 deletions apps/api/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ src/<feature>/
<feature>.module.ts @Module({ controllers, exports: [Service], providers: [Service] })
<feature>.controller.ts @Controller(path); @RouteAccess on every handler
<feature>.service.ts @Injectable; @InjectModel(...) per model
__tests__/<feature>.service.spec.ts
__tests__/<feature>.service.test.ts
```

There is no repository layer and no `entity/` folder — `Model<'X'>` _is_ the repository.
Expand Down Expand Up @@ -170,7 +170,7 @@ which the client factory awaits after `$connect()`. MongoDB creates a collection
with only its `_id_` index, so without that step a deployed instance scans whole collections and
enforces none of the schema's unique constraints. Adding an index means editing **both**
`schema.prisma` and `DATABASE_INDEXES`, which
`src/core/__tests__/prisma.spec.ts` asserts agree — `db push` drops any index the schema
`src/core/__tests__/prisma.test.ts` asserts agree — `db push` drops any index the schema
does not declare, and a runtime-only index would not survive one.

**Do not point `prisma db push` at a database this application uses.** `InstrumentRecord.assignmentId`
Expand Down Expand Up @@ -214,8 +214,8 @@ Adding a variable touches several files that must agree — follow

`pnpm exec vitest --project api` runs two kinds of test, both in the `api` project.

**Unit tests** — `src/**/*.spec.ts`, the bulk of the suite — mock the Prisma layer.
`src/groups/__tests__/groups.service.spec.ts` is the canonical example. Build a testing module with
**Unit tests** — `src/**/__tests__/*.test.ts`, the bulk of the suite — mock the Prisma layer.
`src/groups/__tests__/groups.service.test.ts` is the canonical example. Build a testing module with
`MockFactory.createForModelToken(getModelToken('Group'))` from
`@douglasneuroinformatics/libnest/testing`, type mocks as `MockedInstance<Model<'Group'>>`, and
assert Prisma arguments with `model.create.mock.lastCall?.[0]` and `toMatchObject`. A fresh module is
Expand Down
1 change: 0 additions & 1 deletion apps/api/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export default mergeConfig(
root: import.meta.dirname,
test: {
globals: true,
include: ['src/**/*.spec.ts', 'test/**/*.test.ts'],
name: 'api'
}
})
Expand Down
Loading
Loading