docs(agents): the doctrine — tests never run against a real server

Codify the standing rule at the top of the Tests section: the dev DB is
the test environment, and the invariant every test must uphold is the
testuser experience — its settings, scans, and reading data must be
exactly as the suite found them. The helpers exist to enforce this;
when a test needs protection the helpers don't provide, expand the
helpers rather than hand-rolling.

New known-gotchas from the 2026-10-21 incident cluster:
7. scanner re-extraction is write-after-success — audio_metadata is
   guarded on both writers; new scan-owned column writes need the same
   guard.
8. templ double-escapes style attribute values (sanitizer + EscapeString)
   — emit quote-free CSS via unquoted url() tokens through SafeCSS.
9. vp test runs the vite config — build-hook plugins fire in the test
   context too; outDir-mutating plugins need an "am I a build?" guard.
This commit is contained in:
John O'Keefe committed 2026-10-21 20:25:26 -04:00
1 parent 3ed852515b
commit 6508abc00f
1 file changed
+32
+32
View File
@@ -71,6 +71,16 @@ were name-only across ALL libraries. Fixed; do not regress.
## Tests
- **THE DOCTRINE — tests are never run against a real/production
server.** The dev DB is the test environment, and the invariant every
test must uphold is: **leave the testuser experience alone** — its
settings, scans, and reading data must be exactly as the suite found
them. The helpers exist to enforce this; if a test needs protection
the helpers don't provide, EXPAND THE HELPERS rather than hand-rolling
(2026-10-21: `setSystemSettingRestored` — snapshot-and-restore for
system_settings — exists because a hardcoded ev restore could clobber
a deliberate dev bump, and the live dev server obeys that setting on
its next poll).
- Use `setupTestServer()` from `cmd/server/tests/test_helpers_test.go`
(see the Conventions section below for the pool/cleanup rules).
- Integration tests run against the SHARED dev database: never write
@@ -223,3 +233,25 @@ of a server bug: stop and flag the user.
test pools use `max_conns=1`; never create a `pgxpool` per subtest
(connection exhaustion); use `t.Cleanup()`-based helpers; table-driven
subtests over duplicate functions.
7. **Scanner re-extraction is write-after-success** — a failed/empty
extraction must NEVER clear scan-derived columns. `updateMediaItem`
(generic full-row path) preserves `existing.AudioMetadata` when the
new extraction carries no audio payload, and `updateAudioBookItem`
refuses empty payloads over populated rows (both guarded 2026-10-21
after an ev-gated re-extract NULLed every multi-file audiobook's
audio_metadata — books displayed as 1 second long and lost their
track manifests). New scan-owned column writes need the same guard.
8. **templ double-escapes style attribute values** — the codegen wraps
style exprs in a sanitizer AND `templ.EscapeString`, so ANY quote
character (even inside `templ.SafeCSS`) reaches the DOM as literal
`'` text and the CSS never parses (2026-10-21: every book-detail
hero backdrop broken + Firefox fetched `/media/&` → 500 spam).
Emit quote-free CSS: unquoted `url(...)` tokens via `SafeCSS` +
`cssURL()` in book_detail.templ.
9. **`vp test` runs the vite config** — plugins with build hooks fire
in the test context too. `cleanStaleChunks` guards on
`produced.size === 0` precisely because a bare sweep deleted every
hashed chunk in `web/static` when run under vitest (2026-10-21:
`npm run test` gutted the build; every page 404'd its imports).
Any future outDir-mutating plugin needs the same "am I a build?"
guard.