docs(qa): restore the 8 cases trimmed from the first Section V pass

The first pass dropped 8 gap-audit findings "to keep it tight" — but those
ARE forgotten cases, so they belong in the plan. Add them with full context
(scenario → expected, file:line, defect caught):

- TC-DICT-12  encodeWavPcm16 WAV header/clipping (unit)
- TC-EMBED-05 getEmbedUrlAndProvider 11-provider URL parsing (unit+manual)
- TC-LINK-03  sanitizeUrl/isInternalFileUrl XSS gate (unit+manual, security)
- TC-SPACE-12 space slug @IsAlphanumeric rejects hyphen/underscore/unicode [BUG?]
- TC-ATT-DEDUP-01 diagram attachmentId overwrite authorization
- TC-STOR-DIV-01  local vs S3 missing-file behavior divergence
- TC-LIMIT-QUOTA-01 no per-workspace storage quota (verify-only)
- TC-CMT-09  realtime commentCreated appends only to last loaded page

Docs-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-23 00:48:54 +03:00
parent c23ca101f1
commit 1f5f2b60a8

View File

@@ -349,6 +349,7 @@ Additional cases from a code-grounded gap audit of this plan (8 read-only audits
- **TC-DICT-08** Stop then immediately restart with old responses in flight → stale-session transcripts must NOT leak into the new session; in-session order preserved despite out-of-order HTTP (`use-streaming-dictation.ts:148-227`).
- **TC-DICT-09** Multi-segment insertion: move the caret / delete content above mid-session → subsequent inserts stay contiguous and clamp into doc bounds, no crash on editor destroy (`dictation-group.tsx:13-68`).
- **TC-DICT-11** Three gate states: dictation toggle off (mic hidden, 403 if forced) / STT model blank (503 "Voice dictation is not configured") / endpoint 401/404 (verbatim server message) (`ai-chat.controller.ts:220-269`).
- **TC-DICT-12** [unit] `encodeWavPcm16` correctness — every streaming VAD segment is encoded here and POSTed as `audio/wav`, so a header/clipping bug silently degrades transcription across the whole feature with no visible error. Assert: the RIFF/WAVE/fmt/data chunk tags, channels=1, bitsPerSample=16, `byteRate = sampleRate*2`, `dataSize = samples.length*2`; sample-rate override (16000 default vs 8000/48000) written at offset 24; clipping `+1.0 → 32767`, `-1.0 → -32768`, out-of-range `±1.5` clamps to the rails with no wraparound at exactly −1.0; blob length `44 + samples.length*2` (`apps/client/src/features/dictation/utils/encode-wav.ts:3-32`; full spec in `docs/backlog/qa-plan-unit-test-candidates.md`).
### V.3 Editor & collaboration
@@ -360,6 +361,8 @@ Additional cases from a code-grounded gap audit of this plan (8 read-only audits
- **TC-MENT-02** Mention whitespace ladder: query starting with space never opens; >4 whitespace with only the create-page fallback present destroys; >7 destroys unconditionally; detect aside / comment-dialog / chat-input contexts (`mention-suggestion.ts:11-152`).
- **TC-BLK-19** Indent/outdent on paragraphs & headings: Tab increments and clamps at 8, Shift-Tab to 0; inside list/table/code-block Tab keeps native behavior; malformed `data-indent` clamps (`editor-ext/lib/indent.ts:36-83`).
- **TC-BLK-20** Slash menu does NOT open inside a code block; a no-match space-bearing query (`/todo abc`) auto-deactivates so stray text isn't left in the doc (`extensions/slash-command.ts:24-46`).
- **TC-EMBED-05** [unit+manual] Provider embed-URL parsing (`getEmbedUrlAndProvider`): for each of the 11 providers feed a canonical share URL, an already-`/embed/` URL, and a garbage URL → assert the rewritten `embedUrl` + provider (YouTube `watch?v=`/`youtu.be`/`m.`/`music.``youtube-nocookie.com/embed/<id>`; Vimeo channel/group/album/plain → `player.vimeo.com/video/<id>`; Loom/Airtable/Miro already-embed pass-through; Figma id-length `{22,128}`; unknown URL → `{provider:"iframe"}`). TC-EMBED-01 only does "insert + invalid URL"; a wrong capture group renders a broken iframe with NO error (`packages/editor-ext/src/lib/embed-provider.ts:8-142`).
- **TC-LINK-03** [unit+manual, security] URL sanitizer `sanitizeUrl` / `isInternalFileUrl` — the XSS boundary for every embed/link/attachment `href`. Assert `javascript:` / `data:` / `vbscript:``""` (the library's `about:blank` is mapped to empty by the fork wrapper); `https://`, relative `/api/files/…`, `mailto:` pass through; `isInternalFileUrl` true only for `/api/files/` and `/files/` after trim. TC-EMBED-02 only checks the iframe sandbox, NOT this sanitizer; a regression here is a stored-XSS vector through a crafted href (`packages/editor-ext/src/lib/utils.ts:385-398`).
### V.4 Pages, tree, watchers, backlinks
@@ -396,6 +399,7 @@ Additional cases from a code-grounded gap audit of this plan (8 read-only audits
- **TC-MEM-07** [BUG?] Plain MEMBER calls `POST /workspace/invites` and `/members` directly → returns pending invite emails+roles and the full roster (UI-only gating; CASL grants member `Read Member`) (`workspace.controller.ts:122-210`, `workspace-ability.factory.ts:69`).
- **TC-SPACE-10** [BUG?] Delete the workspace default space → no guard; verify it doesn't leave a dangling `defaultSpaceId` breaking home/new-note/onboarding (`space.service.ts:270-292`).
- **TC-SPACE-11** [BUG?] Save a space sending an unchanged slug (or two admins racing) → server `slugExists` counts the space's own row → wrong "Space slug exists" 400 (`space.repo.ts:60-74`).
- **TC-SPACE-12** [BUG?] Space slug DTO is `@IsAlphanumeric()` (ASCII only) → it rejects `my-space`, `my_space`, and any unicode-derived slug with a generic class-validator message. Create/update a space whose name auto-generates a hyphenated/unicode slug → verify the client generator never emits something the server then 400s, and that the rejection renders cleanly rather than as a raw validator error (`apps/server/src/core/space/dto/create-space.dto.ts:21-24`).
- **TC-AUTH-21** [BUG?] >70-char password on change-password and invite-accept (no MaxLength, unlike register/reset 8-70) → accepted and bcrypt silently truncates at 72 bytes (`change-password.dto.ts:10`, `invitation.dto.ts:51`).
- **TC-AUTH-22** Session table: >25 logins trim oldest-by-last-active (their cookies 401 next request); >7-day sessions purged; valid JWT + revoked DB session ⇒ 401; revoke-all keeps current, revoke-current → 400 (`session.service.ts:14-37`, `jwt.strategy.ts:64-72`, `session.controller.ts:41-79`).
- **TC-GRP-04** Delete a group (or remove a user) that is a space's only access path → user loses space access AND their favorites/watchers for that space are purged; users with another access path keep theirs; live sessions NOT killed (`group.service.ts:173-214`, `group-user.service.ts:106-172`).
@@ -417,6 +421,9 @@ Additional cases from a code-grounded gap audit of this plan (8 read-only audits
- **TC-EXP-ATTACH-FAIL-01** [BUG?] Export include-attachments with one unreadable attachment → file silently omitted, export reports "successful" (incomplete backup, no manifest) (`export.service.ts:358-380`).
- **TC-EXP-PERM-01** Export a subtree where a middle page is inaccessible → its accessible descendants are pruned (chain broken); root inaccessible → "Root page is not accessible" (`export.service.ts:563-609`).
- **TC-ATT-ORPHAN-01** Trash a page with attachments → blobs remain (restore works); permanent-delete/auto-purge → attachment-delete jobs run; partial storage-delete failure leaves orphaned blobs (log only) (`attachment.service.ts:382-426`).
- **TC-ATT-DEDUP-01** Diagram autosave (draw.io/excalidraw) reuses an `attachmentId`, and the only overwrite guard is a page+ext+workspace match. Save a diagram, then POST with that `attachmentId` but a different `pageId`/extension, and with a non-existent id → expect "File attachment does not match" / "Existing attachment to overwrite not found"; confirm a client CANNOT repoint an `attachmentId` to overwrite another page's file (overwrite primitive) (`attachment.service.ts:62-105`).
- **TC-STOR-DIV-01** Missing-file behavior differs by driver: local `readStream` throws "File not found" synchronously, while S3 returns a stream that errors only when consumed and `exists()` rethrows non-`NoSuchKey` errors. Delete an attachment's blob out-of-band then download → on local a clean 404 via the controller catch; on S3 verify it surfaces the same way (no hang/500), and that a transient S3 error during `copy`/`zipAttachments` isn't mistaken for a missing key (`local.driver.ts:73-79`, `s3.driver.ts:118-166`).
- **TC-LIMIT-QUOTA-01** [verify-only] There is NO per-workspace aggregate storage quota — only per-file caps (upload 50MB, import 200MB zip / 30MB single, avatar/logo 10MB). Confirm a workspace can upload unlimited TOTAL attachments up to disk capacity with no "over storage limit" error anywhere — record as a known-absent control (self-hosted disk-exhaustion risk) (`environment.service.ts:86-92`, `attachment.constants.ts:10`).
### V.8 Search, notifications, comments, realtime
@@ -430,6 +437,7 @@ Additional cases from a code-grounded gap audit of this plan (8 read-only audits
- **TC-RT-07** [BUG?] Same window: create/edit/resolve/delete a comment on a restricted page → comment body + selection text broadcast to the whole space room (`ws.service.ts:58-64`, `comment.service.ts:153-283`).
- **TC-CMT-08** [BUG?] Open the Add-comment dialog on a selection, move the cursor before Submit → stored selection text is captured at submit, not at open → empty/wrong anchor; "jump to selection" then shows wrong text (`comment-dialog.tsx:53-69`).
- **TC-CMT-10** Tab A resolves a comment; Tab B has the page open → list updates via WS but the inline decoration updates via Yjs → if one channel is mid-reconnect the panel and inline mark disagree (`use-query-subscription.ts:57-75`, `comment.service.ts:246-260`).
- **TC-CMT-09** Realtime `commentCreated` appends `data.comment` only to the LAST loaded page of the cursor-paginated comment list, and drops the event entirely if the panel was never opened (`pages.length === 0`). With a partially-scrolled list (not all pages loaded), a new comment arrives via WS → verify it lands in the right order relative to unloaded pages and that nothing duplicates or flips position on the next real fetch (`use-query-subscription.ts:34-55`).
- **TC-SRCH-05** [BUG?] Search operator-only / stopword-only inputs (`&`, `!`, `*`, `<->`, `\`, "the a of") → `to_tsquery` syntax error → unhandled 500; the `length<1` guard doesn't catch whitespace/operators (`search.service.ts:37, 50-60`).
- **TC-SRCH-07** Query whose top-N ranked hits include restricted pages → access-filtering AFTER limit/offset returns <N visible results and the removed ranks are unreachable (`search.service.ts:67-139`).
- **TC-SRCH-04** `/search/suggest` has no controller-level CASL check (unlike `pageSearch`) — pass a non-member `spaceId` and confirm the service filter alone blocks any page leak (`search.controller.ts:76-84`).