From d595fb33e822420ae82051d9e64ec60b4d5c468f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 11 Sep 2026 23:26:08 +0200 Subject: [PATCH] docs(claude): record the core API tester's header-seeded context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .claude/skills/writing-unit-tests/SKILL.md | 2 +- .claude/skills/writing-unit-tests/server-and-core.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.claude/skills/writing-unit-tests/SKILL.md b/.claude/skills/writing-unit-tests/SKILL.md index c45509ce01..67191653c5 100644 --- a/.claude/skills/writing-unit-tests/SKILL.md +++ b/.claude/skills/writing-unit-tests/SKILL.md @@ -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 --root apps/client`, or `node_modules/.bin/vitest.CMD run --root apps/`. +> **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 --root apps/client`, or `node_modules/.bin/vitest.CMD run --root apps/`. ## Coverage config rules (Vitest 4) diff --git a/.claude/skills/writing-unit-tests/server-and-core.md b/.claude/skills/writing-unit-tests/server-and-core.md index df40f5fe7e..7f5aed2d56 100644 --- a/.claude/skills/writing-unit-tests/server-and-core.md +++ b/.claude/skills/writing-unit-tests/server-and-core.md @@ -37,6 +37,11 @@ describe("X API (core)", () => { - `api.(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.