From c23ca101f1536c7a875eccb408541bc41e35e0f3 Mon Sep 17 00:00:00 2001 From: claude_code Date: Tue, 23 Jun 2026 00:26:18 +0300 Subject: [PATCH] docs(qa): add forgotten-case pass (Section V) to the manual QA plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Append Section V — ~75 additional manual/integration cases surfaced by a code-grounded gap audit (8 read-only zone audits) of this plan, and correct two now-stale cases: - TC-TRASH-01: no confirm dialog / "30-day note" anymore — delete is immediate with an 8s Undo toast (page-query.ts:132-144). - TC-SPACE-03: server slugExists does not exclude self (bug to verify), see new TC-SPACE-11. New cases cover the fork's recently shipped, uncovered behavior (AI-chat message queue / stopped-notice / partial-answer persistence, streaming dictation via Silero VAD, trash undo-toast, MCP write-only headers) and code-grounded server branches (notification CASL count leak, 3s restriction-cache realtime leak, MovePageDto bound vs fractional-index keys, to_tsquery 500, import zip-bomb / HTML-XSS, attachment download authZ). Cases tagged [BUG?] double as candidate defects. Full rationale in docs/qa-plan-gaps-pr136.md. Docs-only. Co-Authored-By: Claude Opus 4.8 --- docs/manual-qa-test-plan.md | 128 +++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/docs/manual-qa-test-plan.md b/docs/manual-qa-test-plan.md index 5a6e9c32..6aefe893 100644 --- a/docs/manual-qa-test-plan.md +++ b/docs/manual-qa-test-plan.md @@ -60,7 +60,7 @@ Conventions: - **TC-SPACE-01** Create space: name (2-100), slug (alphanumeric, auto-generated from name as you type, 2-100), description (≤500). Watch slug live-generation quirks (single-char words, spaces, unicode). - **TC-SPACE-02** Duplicate slug → "Space slug exists…" red toast. -- **TC-SPACE-03** Edit space: change name/slug/desc; only dirty fields sent; save disabled when clean; slug uniqueness excludes self. +- **TC-SPACE-03** Edit space: change name/slug/desc; only dirty fields sent; save disabled when clean. NOTE (corrected): server `slugExists` does NOT exclude self (`space.repo.ts:60-74`) — saving with the slug field present but unchanged can wrongly 400 "Space slug exists". The client only avoids it by sending dirty fields. Treat self-exclusion as a BUG to verify, not expected behavior. See TC-SPACE-11. - **TC-SPACE-04** Delete space: type-name-to-confirm (case-insensitive); destructive; redirect to /home. Try wrong name → confirm disabled. - **TC-SPACE-05** Space members & roles (ADMIN/WRITER/READER): change role, remove; infinite scroll; search. - **TC-SPACE-06** Space grid / switcher: favorite a space, default space; only member spaces shown; create-space entry. @@ -82,7 +82,7 @@ Conventions: ## G. Trash, History, Favorites, Labels -- **TC-TRASH-01** Soft-delete page ("Move to trash", 30-day note); children remain; current page deleted → redirect. +- **TC-TRASH-01** Soft-delete page via "Move to trash". NOTE (corrected): there is NO confirm dialog anymore — delete fires immediately and shows an 8-second **Undo toast** "Page moved to trash" (`page-query.ts:132-144`; menus call `handleDelete(id)` directly). The 30-day retention is real but lives only in `TrashCleanupService` and is NEVER surfaced at delete time. Children remain; current page deleted → redirect. Undo-specific scenarios in TC-TRASH-14..17. - **TC-TRASH-02** Trash list: view content modal, deleted-by/at, restore, delete-forever (admin only), empty state, pagination (50). - **TC-TRASH-03** Restore page whose parent is still deleted → where does it land? (orphan behavior). - **TC-TRASH-04** Permanent delete cascades to children; non-admin blocked. @@ -309,3 +309,127 @@ Conventions: - **TC-MED-07** Notion ZIP import + async task lifecycle: import modal → pick Notion source (distinct from generic ZIP), upload Notion export zip. Verify persistent "Importing pages / Please don't close this tab" toast → 3s-poll lifecycle: success → "Import complete" + tree refetch + ws refetchRootTreeNode; failed → "Page import failed: {reason}"; backend error → "Import failed". Generic-zip path uses same poller. - **TC-MED-08** Multi-file md/html import partial-failure + per-file size: select MULTIPLE .md/.html at once; 30MB per-single-file limit ("File exceeds the 30mb import limit", distinct from 200MB zip); failing files SILENTLY swallowed (console.log only, no per-file error toast), summary counts only successes ("Successfully imported N pages"); zero success → "Failed to import pages". Mixed valid+invalid → count + no per-file error (sloppy-feedback candidate). + +--- + +## V. Forgotten cases — second code-grounded pass (added 2026-06-23) + +Additional cases from a code-grounded gap audit of this plan (8 read-only audits across product zones; full rationale in `docs/qa-plan-gaps-pr136.md`). Same convention: steps → expected, `file:line` cited for grounding. **[BUG?]** = the case also surfaces a candidate defect. + +### V.0 Corrections to existing cases (text now stale vs code) + +- **TC-DICT-02 (expand)** — transcription throttle is **20 req/60s** (`ai-chat.controller.ts:213`), a SEPARATE bucket from the 25/60s AI-chat throttle. Streaming sends one request per VAD pause, so this bucket is the real limit. +- **TC-MED-08 / TC-MED-05** — single md/html import limit is **30MB** (`import.controller.ts:56`) but the 413 message wrongly says **"Exceeds the 10mb import limit"** (`:67`). [BUG?] wording. +- **TC-NOTIF-03** — the ≤4-immediate-emails/24h budget is **global per user**, NOT per page (`page-update-email-rate-limiter.ts:18-23`). The 7h cooldown IS per-(user,page). +- **TC-SRCH-02** — on the default Postgres FTS build there is **no "Attachments" content-type filter** (hidden; `contentType` never sent to server; attachment search is Typesense/EE only) — `search-spotlight-filters.tsx:50`, `use-unified-search.ts:23`. Don't file a bug; it's EE-only. + +### V.1 AI chat & MCP (mostly new code, no manual coverage) + +- **TC-AI-09** Queue a message while a turn streams: text becomes a pending row (clock icon), composer clears; on clean finish the FIRST queued msg auto-sends (FIFO), then the next (`chat-thread.tsx:189-259`, `chat-input.tsx:39-104`). +- **TC-AI-10** [BUG?] Queue preserved on Stop AND on stream error (must NOT auto-flush); removing a queued item via its X works (`chat-thread.tsx:249-259, 364-372`). +- **TC-AI-11** Queue lifecycle: cleared when switching to another chat (remount by key); survives new-chat→server-id adoption without remount (`ai-chat-window.tsx:299-316`). +- **TC-AI-12** "Stopped" notice: manual Stop → gray "Response stopped."; dropped connection → gray "Connection lost — the answer was interrupted."; clears on next turn (`chat-thread.tsx:249-290`, `chat-stopped-notice.tsx`). +- **TC-AI-13** Reopen a chat with an interrupted turn from history → partial answer present + combined-wording gray notice (`message-item.tsx:129-144`, `ai-chat.service.ts:456-471`). +- **TC-AI-14** [BUG?] Provider drops mid-answer → partial answer + tool-call cards + classified error banner persist and survive reload (`ai-chat.service.ts:435-455`). +- **TC-AI-15** [BUG?] "Copy chat" while streaming includes the in-progress turn ("still being generated…"); switching chat mid-stream must not leak the previous chat's tail (`ai-chat-window.tsx:268-295`, `chat-thread.tsx:297-303`). +- **TC-AI-16** [BUG?] Chat-history row shows relative creation time + origin page title; restrict the origin page from the user → title must NOT leak ("No document") (`conversation-list.tsx:29-44`, `ai-chat.service.ts:201-243`). +- **TC-AI-17** Tool-call card states (running spinner → green done → red error+errorText) and clickable `/p/{uuid}` citations; generic "Ran tool {{name}}" fallback (`tool-call-card.tsx:41-81`, `tool-parts.tsx:55-136`). +- **TC-AI-18** Provider ECONNRESET: turn recovers without a user-visible error before first byte; after partial bytes → classified "Lost connection" banner + partial retained (`ai-http.ts:55-84`, `error-message.ts:104-114`). +- **TC-AI-19** "Ask AI" toolbar button: present in edit mode with AI enabled, absent when AI/generative off, expected behavior in read-only (`ask-ai-group.tsx:8-23`). +- **TC-MCP-01** [BUG?] MCP auth headers are write-only: list never returns `headersEnc` (only `hasHeaders`); update with header omitted = unchanged, `{}` = clear, non-empty = replace (`mcp-servers.service.ts:80-101, 160-171`). +- **TC-MCP-02** MCP transport http vs sse both connect; editing the URL re-runs SSRF check (blocked → 400), editing other fields without URL change does not (`mcp-clients.service.ts:306-323`, `mcp-servers.service.ts:76-78`). + +### V.2 Dictation / STT (fork feature, heavily changed) + +- **TC-DICT-03** [BUG?] Dictate with frequent pauses so >20 VAD segments cut in 60s → graceful degradation after the 21st 429s (no toast flood); earlier text preserved (`ai-chat.controller.ts:213`, `use-streaming-dictation.ts:185-227`). +- **TC-DICT-04** Streaming is hardcoded ON in both surfaces (editor toolbar + chat composer); no batch-mode toggle exists; VAD assets (~13–26MB) download for every dictation user (`dictation-group.tsx:73`, `chat-input.tsx:74`). +- **TC-DICT-05** Block one of `/vad/*.onnx|*.wasm|*.worklet` (or it serves as text/html) then click mic → status returns to idle, mic re-clickable, single red toast, no silent hang (`use-streaming-dictation.ts:348-367`, `copy-vad-assets.mjs`). +- **TC-DICT-06** Fresh load, uncached model, ONE mic click → "Preparing…" then recording starts on the first click (AudioContext-in-gesture fix) (`use-streaming-dictation.ts:256-274`). +- **TC-DICT-07** [BUG?] Click Stop while still talking (no pause) → the un-ended segment is silently dropped (data loss); compare to Stop after a pause (`use-streaming-dictation.ts:421-425`). +- **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`). + +### V.3 Editor & collaboration + +- **TC-ED-07** [BUG class] Type a long title fast in two tabs while a PAGE_UPDATED echo arrives → no dropped chars (focused-field guard); blur then apply an external title change (`title-editor.tsx:143-183`). +- **TC-RT-02** Collab JWT expires/revoked with editor open → reads latest token via ref, refetches, reconnects; cover the `exp === undefined` / unparseable-token / no-token branches (no reconnect thrash) (`page-editor.tsx:176-204`). +- **TC-RT-03** Live security window: trash / demote-to-reader / deactivate a user who has the page open → next reconnect is read-only or rejected; verify the mid-session window before reconnect (`authentication.extension.ts:78-102`). +- **TC-MED-09** Paste a block with an image/pdf/excalidraw/drawio copied from page A into page B → only foreign-owned attachments re-upload to B; 404 fetch keeps original; node moved mid-fetch re-validates; dedup of repeats (`editor-paste-handler.tsx:75-217`). +- **TC-LINK-02** Paste internal page URL matrix: empty selection → mention (with `#anchor`); non-empty selection → plain link; cross-host → link; unresolved/deleted page → link mark fallback, no crash (`editor-paste-handler.tsx:33-58`, `internal-link-paste.ts:20-76`). +- **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`). + +### V.4 Pages, tree, watchers, backlinks + +- **TC-PAGE-17** [BUG?] Reorder a node between adjacent siblings in a deep tree → `generateJitteredKeyBetween` can yield <5 or >12 chars, which `MovePageDto` (`@MinLength(5)@MaxLength(12)`) rejects with a generic 400 and the tree reverts (`move-page.dto.ts:14-16`). +- **TC-PAGE-18** [BUG?] Watcher `page.updated` fires ONLY on collab body saves with a prior history version — NOT on REST rename/icon and NOT on the first edit (`history.processor.ts:105-118`). +- **TC-PAGE-19** Force the breadcrumb lazy-ancestry fetch to fail (offline/500) → graceful fallback; today there is no try/catch so ancestor expansion aborts with an uncaught error (`space-tree.tsx:129-139`). +- **TC-PAGE-20** `/pages/created-by-user` accepts a `userId` the dashboard never sends → verify access is scoped to the requester's spaces and a non-UUID returns a clean 400 (`page.controller.ts:453-473`). +- **TC-PAGE-21** Move-to-space with an accessible grandchild under an INACCESSIBLE child → confirm where the grandchild lands (orphan to source root vs dangling) (`page.service.ts:437-573, 1251`). +- **TC-PAGE-22** [BUG?] Duplicate a deep tree where one attachment copy fails → silent broken image (log only); duplicating 3× yields three "Copy of X" siblings (`page.service.ts:837-885`). +- **TC-BLK-11 (expand)** [BUG?] Subpages block on a parent with more children than the sidebar page-size shows only the first page (no `fetchNextPage`); verify live-refresh on child add/rename/move/trash (`subpages-view.tsx:28-48`). +- **TC-WATCH-01** Mute a page (Stop watching) while watching its space → no `page.updated` for that page but still for other pages in the space; re-watch clears mute; favoriting has no notification effect (`watcher.repo.ts:66-102`). +- **TC-PAGE-24** Backlinks edges: self-link (count/double-count), deleted target (count drops), incoming link from a space the viewer can't read (excluded) (`backlink.repo.ts:83-111`, `backlink.service.ts:40-55`). +- **TC-DASH-02 (expand)** "New note" picker excludes READER-only spaces and caps at 100 spaces; a failed create still shows the error and re-enables (`new-note-button.tsx:17-44`). + +### V.5 Trash (undo toast), history, favorites, labels + +- **TC-TRASH-14** Trash a child from the tree menu → its node unmounts → ~3s later click Undo → page restored at the correct parent with children intact (restore reads tree via store, not a render closure) (`page-query.ts:120-279`). +- **TC-TRASH-15** Undo after the 8s window expires → toast gone, no other undo affordance; recovery only via Trash → Restore; current-page trash redirects to space root (`page-query.ts:134`). +- **TC-TRASH-16** Trash 3–4 sibling pages rapidly → stacked per-page toasts, each Undo restores the CORRECT page (closure capture), no id collision (`page-query.ts:132-142`). +- **TC-TRASH-17** Two-tab consistency: trash in A (B drops node) → Undo in A → no duplicate node in either tab (addTreeNode vs refetchRoot reconciliation) (`page.repo.ts:358-447`, `page-query.ts:241-258`). +- **TC-TRASH-18** Restore a child whose parent is still trashed → child detaches to root, subtree stays attached; restore the parent later → child does NOT re-parent (`page.repo.ts:380-448`). +- **TC-TRASH-19** WRITER opens Trash and clicks "Delete" (item NOT hidden, unlike the banner) → server 403 "Only space admins can permanently delete pages" renders cleanly (`trash.tsx:173`, `page.controller.ts:335`). +- **TC-HIST-02** AI-agent version badge: violet sparkles + tooltip, human author still shown; click/Enter opens that chat, CLEARS the chat draft, closes history, does not select the row; agent version without `aiChatId` is non-clickable (`history-item.tsx:37-108`). +- **TC-HIST-03** Diff highlight on a version with text edits + added image/table + deleted callout: toggle on/off, counts match, deleted special-node renders as a ghost widget; oldest version → no diff; malformed → plain fallback (`history-editor.tsx:23-203`). +- **TC-HIST-04** Restore is Manage-gated (WRITER sees no Restore button) and client-optimistic — un-saved restore must not persist on reload (`use-history-restore.tsx:38-69`, `history-list.tsx:151`). +- **TC-FAV-02** Rapid favorite/unfavorite from the page/tree MENU (no pending guard, unlike the list StarButton) → final state correct, no duplicate row, no toast mismatch (`space-tree-node-menu.tsx:181-189`). +- **TC-FAV-03** Trashed favorite still renders (click → deleted banner); permanently-deleted favorite row is null-skipped → verify pagination pages don't visibly shrink and empty state only when truly empty (`favorites-pages.tsx:52-126`). +- **TC-LABEL-03** Label name regex `/^[a-z0-9_-][a-z0-9_~-]*$/`: `~tag` (tilde first) rejected; `a~b` ok; uppercase/space/unicode handled (client lowercases first) (`label.dto.ts:31`). +- **TC-LABEL-05** `/labels/:name` for a label mostly on inaccessible pages → results filtered AFTER pagination → under-filled pages but "load more" terminates; nonexistent label returns uniform `usageCount:0` (no leak) (`label.service.ts:87-138`). +- **TC-LABEL-06** Apply a label to pages X+Y, remove from each → label auto-deletes workspace-wide only on removal from its LAST page; re-adding creates a fresh row (`label.service.ts:40-67`). + +### V.6 Auth, members, groups, spaces, workspace + +- **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-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`). +- **TC-WS-12** aiSearch toggle OFF (schedules 24h embedding-purge job) then back ON within the window → the pending purge job is removed so it can't wipe rebuilt embeddings; one dedup reindex runs (`workspace.service.ts:578-609`). +- **TC-USER-08** Notification preferences: per-type email opt-outs (page.updated, page/comment user-mention, comment.created/resolved) toggle + persist + suppress email (`update-user-settings` path). +- **GAP** Space/group `description` has no server MaxLength (≤500 is client-only) → 10k-char description via API is stored (`create-space.dto.ts:17`, `create-group.dto.ts`). + +### V.7 Import / export / attachments / storage + +- **TC-ATT-DL-01** Non-member (or restricted-page reader) fetches `/api/files//` directly → 403/404; download uses `validateCanView`, upload uses `validateCanEdit`; READER who can view CAN download (`attachment.controller.ts:166-211`). +- **TC-ATT-PUB-01** [BUG?] Public attachment JWT survives un-share / page move (check is only `jwt.pageId === attachment.pageId`, no live share check) → token-outlives-revocation leak; also expiry and cross-attachment reuse → 404 (`attachment.controller.ts:213-259`). +- **TC-ATT-CHAT-01** AI-chat attachment is creator-only: another user (even admin) gets 404; `files/info` rejects chat attachments (`attachment.controller.ts:185-191, 391-415`). +- **TC-STOR-RANGE-01** Seek a large inline video/audio (Range header) → 206 with correct Content-Range; past-EOF → 416; malformed Range → full stream; test on both local and S3 (`attachment.controller.ts:490-533`). +- **TC-IMP-ZIPBOMB-01** [BUG?] Upload a <200MB zip that decompresses to many GB / hundreds of thousands of files → no decompression-ratio or entry-count cap → disk-fill/OOM stalls the import queue (`file.utils.ts:146-228`). +- **TC-IMP-XSS-01** [BUG?] Import md/html with `