diff --git a/.claude/workflows/bughunt.js b/.claude/workflows/bughunt.js index 1f84fb77..c90df999 100644 --- a/.claude/workflows/bughunt.js +++ b/.claude/workflows/bughunt.js @@ -120,7 +120,7 @@ Method: structural reference is evidence of coupling, not of a bug; open the cited file and confirm. 2. For every candidate, grep for ALL callers before judging - a guard may already live upstream. 3. Check whether an existing test already locks the behavior you think is wrong. If a test asserts it, - it is intended behavior, not a bug. Test files are *_test.go and tests/unit/*.test.ts. + it is intended behavior, not a bug. Test files are *_test.go, tests/unit/*.test.ts and tests/contract/*.test.ts. 4. Report EVERY finding you can prove - there is no cap. The quality bar stays: zero findings is a valid, respectable answer, and each finding needs file, line, and a concrete repro. @@ -200,7 +200,7 @@ const SURFACE_LENSES = [ `an entity when events arrive out of order; read-state that can mark unread messages read, or lose an unread ` + `count, across a reconnect; an async handler whose await lets stale state be written after a newer update ` + `(last-write-wins race); a route guard bypassable by a rapid navigation sequence.\n` + - `Check tests/unit/ before reporting - much of this behavior is already test-locked.`, + `Check tests/unit/ and tests/contract/ before reporting - much of this behavior is already test-locked.`, }, ]; diff --git a/.superpowers/FINDINGS.md b/.superpowers/FINDINGS.md index c72d917f..b0c99e22 100644 --- a/.superpowers/FINDINGS.md +++ b/.superpowers/FINDINGS.md @@ -4742,7 +4742,7 @@ saveChannelPerms writes the quick "Can access" toggles first (PUT allow=0/deny=0 **Suggested fix:** In saveChannelPerms, record the role IDs the quick-toggle loop actually wrote and skip the matrix step when the selected target is one of them — one guard in the one function: collect `const touched=new Set()` in the loop (add role.role_id on each PUT/DELETE), then wrap the matrix block in `if(path && !(permTargetPath().indexOf('/permissions/')>-1 && touched.has(tid)))`. Cleanest variant: give the quick checkbox an onchange that patches the in-memory role.allow/role.deny in state.permChannel and calls renderPermMatrix(), so the matrix always reflects the pending toggle instead of the stale snapshot. -**Fixed:** `69258a51` · test `Client/tests/unit/admin-static-channel-perms.test.ts` · revert-proof pass +**Fixed:** `69258a51` · test `Client/tests/contract/server-admin-static-channel-perms.test.ts` · revert-proof pass ### OC-0155 — medium — Room-key offer pacing budget is per-call, so two back-to-back rotations blow the server's per-second offer cap and strand peers on a dead key diff --git a/.superpowers/findings-ledger.json b/.superpowers/findings-ledger.json index 3d2fa671..a50fb0c0 100644 --- a/.superpowers/findings-ledger.json +++ b/.superpowers/findings-ledger.json @@ -3775,7 +3775,7 @@ "confidence": "high", "fix": { "commit": "69258a51", - "test": "Client/tests/unit/admin-static-channel-perms.test.ts", + "test": "Client/tests/contract/server-admin-static-channel-perms.test.ts", "revertProof": "pass" }, "suggestedFix": "In saveChannelPerms, record the role IDs the quick-toggle loop actually wrote and skip the matrix step when the selected target is one of them — one guard in the one function: collect `const touched=new Set()` in the loop (add role.role_id on each PUT/DELETE), then wrap the matrix block in `if(path && !(permTargetPath().indexOf('/permissions/')>-1 && touched.has(tid)))`. Cleanest variant: give the quick checkbox an onchange that patches the in-memory role.allow/role.deny in state.permChannel and calls renderPermMatrix(), so the matrix always reflects the pending toggle instead of the stale snapshot.", diff --git a/Client/CLAUDE.md b/Client/CLAUDE.md index 21b48b2e..83e151cf 100644 --- a/Client/CLAUDE.md +++ b/Client/CLAUDE.md @@ -9,8 +9,13 @@ Rust backend in `src-tauri/` for native APIs only. LiveKit handles voice/video. `src/pages/`, `src/components/` UI - `src/lib/protocolTypes.ts` and `src/generated/` are generated — see the root CLAUDE.md -- `tests/unit`, `tests/integration` (vitest, jsdom) · `tests/e2e` (Playwright) · +- `tests/unit`, `tests/integration`, `tests/contract` (vitest, jsdom) · + `tests/e2e`, `tests/e2e/admin`, `tests/e2e/native` (Playwright) · `tests/browser` (vitest browser mode) +- A test whose assertions read, import or execute a **`Server/`-owned** + artifact belongs in `tests/contract`, not `tests/unit` — `src-tauri/` is + part of this component, so reading it is an ordinary unit test. The rule + is in [docs/contributing.md](../docs/contributing.md#testing) ## Gotchas diff --git a/Client/package.json b/Client/package.json index 2aa7d88d..18ecad58 100644 --- a/Client/package.json +++ b/Client/package.json @@ -15,6 +15,7 @@ "test": "vitest run", "test:unit": "vitest run tests/unit", "test:integration": "vitest run tests/integration", + "test:contract": "vitest run tests/contract", "test:e2e": "playwright test", "test:e2e:prod": "npm run build && playwright test --config playwright.config.prod.ts", "test:e2e:native": "playwright test --config playwright.config.native.ts", diff --git a/Client/tests/unit/admin-static-channel-perms.test.ts b/Client/tests/contract/server-admin-static-channel-perms.test.ts similarity index 91% rename from Client/tests/unit/admin-static-channel-perms.test.ts rename to Client/tests/contract/server-admin-static-channel-perms.test.ts index 455804fa..d15fa0a8 100644 --- a/Client/tests/unit/admin-static-channel-perms.test.ts +++ b/Client/tests/contract/server-admin-static-channel-perms.test.ts @@ -1,3 +1,8 @@ +// CONTRACT TEST. The artifact under test is owned by Server/admin; the runner +// lives here because placement follows capability, not ownership — the Go +// module carries no JavaScript engine, so nothing under Server/ can execute +// this SPA. See docs/contributing.md#testing for the membership rule. +// // Loads the real Server/admin/static/index.html (the Go admin panel's // single-file SPA) into a scripted jsdom window and drives its inline // channel-permissions logic directly, the same way a browser would. @@ -5,6 +10,9 @@ // There is no bundler or module system for this file — it is one inline //