docs(claude): record the core API tester's header-seeded context

CoreApiTester now seeds the execution context from the trilium-* request
headers, so Pattern 0 documents how to drive a hoisted request and warns that
a spec omitting trilium-hoisted-note-id exercises the unhoisted path. Anything
reading hoistedNoteService.getHoistedNoteId() — quick search, autocomplete,
SearchContext's implicit ancestorNoteId — sees "root" without it, so a passing
spec is not evidence that scoping works. A contributor's quick-search PR
asserted exactly that and proved nothing.

The Windows note covered only `pnpm --filter … exec vitest`; the auto-install
fires for a package's own test script too. It now names the symptom seen here:
EPERM / Access is denied on a node_modules directory VS Code holds open, which
rolls back cleanly but loses the run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Elian Doran 2026-09-11 23:26:08 +02:00
parent cd381e09fe
commit d595fb33e8
No known key found for this signature in database
2 changed files with 6 additions and 1 deletions

View File

@ -53,7 +53,7 @@ Some layers have a purpose-built spec harness documented in the skill that owns
- Coverage: append `--coverage`.
- Server tests run **sequentially** (shared DB, `pool: "forks"`, fork isolation is **per file**). Client/package tests run in parallel.
> **Windows/sandbox note:** `pnpm --filter … exec vitest` can trigger a pnpm auto-install that hits `EPERM`. If so, run the hoisted binary directly (it lives in the **repo-root** `node_modules`): `CI=true node node_modules/vitest/vitest.mjs run <spec> --root apps/client`, or `node_modules/.bin/vitest.CMD run <spec> --root apps/<app>`.
> **Windows/sandbox note:** any `pnpm --filter …` invocation — `exec vitest` and the package's own `test` script alike — can trigger a pnpm auto-install that hits `EPERM`/`Access is denied` on a `node_modules` directory VS Code holds open (it rolls back, but the run is lost). If so, run the hoisted binary directly (it lives in the **repo-root** `node_modules`): `CI=true node node_modules/vitest/vitest.mjs run <spec> --root apps/client`, or `node_modules/.bin/vitest.CMD run <spec> --root apps/<app>`.
## Coverage config rules (Vitest 4)

View File

@ -37,6 +37,11 @@ describe("X API (core)", () => {
- `api.<verb>(path, { body, query, headers, file })`. `createTextNote(api, {...})``{ noteId, branchId }`. Assert real state via `getSql()` / `becca`.
- Mutations are auto-wrapped in cls + a SQL transaction — **no `cls.init` needed** (unlike Pattern 3 direct service calls).
- Header-reading handlers (e.g. sync) work: pass `headers` and the handler's `req.get(name)` reads them case-insensitively.
- **`trilium-*` headers seed the execution context**, exactly as `route_api.ts` (Express) and `browser_routes.ts` (standalone) do: `trilium-hoisted-note-id``cls.getHoistedNoteId()` (defaults to `"root"`), plus `trilium-component-id` and `trilium-local-now-datetime`. This is the **only** way to test a hoist-dependent route — anything reading `hoistedNoteService.getHoistedNoteId()` (quick search, autocomplete, `SearchContext`'s implicit `ancestorNoteId`) sees `"root"` unless you pass the header:
```ts
api.get(`/api/quick-search/${token}`, { headers: { "trilium-hoisted-note-id": workspaceId } });
```
A spec that omits it is asserting the *unhoisted* path, so don't read a pass as proof that scoping works.
### It runs REAL services end to end — including streaming + multipart. Don't mock them.
Both test setups (`apps/server/spec/setup.ts`, `apps/standalone/src/test_setup.ts`) inject the **real platform providers** (zip = archiver/fflate, image = sharp/magic-bytes, backup = fs/OPFS), and **both vitest suites run on Node** (the standalone setup itself imports `node:fs`/`node:module`) — so `Buffer`, `node:stream`, `node:fs` are available in either runtime. The tester's mock `res` is a real Node `Writable` that also implements the Express surface (`set`/`setHeader`/`removeHeader`/`status`/`send`/`sendStatus`/`write`/`end`), so the **server** export path (`archiver.pipe(res)`, needs a real writable) and the **browser** path (`BrowserZipArchive.finalize()` → `res.send(bytes)`) both run. Match the ETAPI **zero-mock** convention: drive real inputs and assert real output.