Conversation
chore: sync main to develop
PR #719 のレビュー(CodeRabbit)で「note deletion redirects to /notes、page deletion は /home」が不整合であると指摘されたが、 削除対象のスケール(ノート vs ページ)が異なるため遷移先が異なる のは設計通り。今後の同種レビューを防ぐため、その意図をコメントで 明示する。 PR #719 のサーバー / フロント側の本質的な指摘(byTitle invalidation, note-native title editability, MCP zedi_list_pages の説明矛盾, AI chat WikiLink scope, PUT content の title 永続化, useWikiLinkStatusSync のスコープ再評価)は既に PR #724 / #727 で対応済み。本 PR は レビュー対応サイクルを締めくくるためのドキュメンテーションのみの変更。 Refs #719 Made-with: Cursor
… Phase 1) (#733) * feat(api): accept link_type on /api/sync/pages (issue #725 Phase 1) サーバ同期エンドポイントに `link_type` を通す。GET レスポンスの `links` / `ghost_links` 各行に `link_type` を含め、POST ボディの両配列にも オプション項目として受け入れる。未指定は `'wiki'` にフォールバックし、 未知の値は 400 で拒否する。 DELETE は `(source_id, link_type)` ペア単位でスコープする。これにより、 タグ同期時に WikiLink エッジを(あるいはその逆を)巻き添え削除しなくなる。 body に現れなかった `link_type` のエッジには触れない。 Accepts `link_type` on `/api/sync/pages`. GET returns it on each row; POST accepts it (omitted → `'wiki'`, unknown values → 400). DELETE is scoped per `(source_id, link_type)` so tag sync cannot wipe wiki edges and vice versa. https://claude.ai/code/session_01VXcHb8Bt62oxSfRZ7PoZKz * feat(client): thread linkType through adapter/repo/syncWithApi (issue #725 Phase 1) IndexedDB スキーマを v3 に上げ、`my_links` / `my_ghost_links` のキーパスに `linkType` を含めて再生成する。既存行は `linkType: 'wiki'` で backfill する。 `by_source_type` 合成インデックスを追加し、`getLinks(pageId, linkType)` / `saveLinks(sourceId, links, linkType)` を種別スコープで動作させる。 `IPageRepository` / `StorageAdapterPageRepository` / pageStore の link 系 メソッドに `linkType` を追加(既定 `'wiki'`)。`syncWithApi` の push / pull は行ごとに `link_type` を乗せ、pull 側は `(source_id, link_type)` ペア単位で stale クリアを行う。 Bumps IndexedDB to v3: the `my_links` / `my_ghost_links` stores now include `linkType` in their key paths and existing rows are backfilled to `'wiki'`. Repository and store interfaces accept an optional `linkType` (defaults to `'wiki'`) so tag edges coexist with wiki edges end-to-end. https://claude.ai/code/session_01VXcHb8Bt62oxSfRZ7PoZKz * feat(editor): sync tag marks through syncLinksWithRepo pipeline (issue #725 Phase 1) `syncLinksWithRepo` に `linkType` オプションを追加(既定 `'wiki'`)し、`'tag'` 指定時はリポジトリの対応するメソッドに `'tag'` を伝搬させる。`useSyncWikiLinks` フックから新たに `syncTags` を露出し、エディタの auto-save 経路に配線する。 `useEditorAutoSave` は content から `extractTagsFromContent` でタグ Mark を抽出し、 `getUniqueTagNames` で重複除去したうえで `syncTags` を呼ぶ。WikiLink と同じく、 Mark が無いときは wire traffic を避けるためスキップする。`syncTags` が無い旧 呼び出し元では何も変わらない (backward compat)。 `syncLinksWithRepo` now accepts `linkType` (defaults to `'wiki'`). The `useSyncWikiLinks` hook exposes a parallel `syncTags` function, and `useEditorAutoSave` invokes it after save when the content carries tag marks, matching the existing WikiLink short-circuit on empty inputs. Legacy callers without `syncTags` keep their prior behavior. https://claude.ai/code/session_01VXcHb8Bt62oxSfRZ7PoZKz * feat(editor): refresh tag mark exists/referenced attributes (issue #725 Phase 1) タグ Mark の `exists` / `referenced` 属性をページグラフに合わせて同期する `useTagStatusSync` フックを追加し、`useEditorLifecycle` から `useWikiLinkStatusSync` と並行して呼ぶ。判定ロジックは WikiLink と同じ `useWikiLinkExistsChecker` を 流用し、`name` を `title` と同じ名前空間で解決する。 タグクリック時のページ遷移は既に `editorConfig.ts` で `onTagClick ?? onLinkClick` にフォールバックするよう配線済みのため、追加変更なしで WikiLink と同一の ナビゲーションフロー (`useWikiLinkNavigation`) が動く。 Adds `useTagStatusSync` that mirrors `useWikiLinkStatusSync` for `tag` marks, reusing the same existence checker and noteScope signature. Hooked into `useEditorLifecycle` alongside the WikiLink variant. Tag click navigation was already wired via the `onTagClick ?? onLinkClick` fallback in `editorConfig`, so it reuses `useWikiLinkNavigation` unchanged. https://claude.ai/code/session_01VXcHb8Bt62oxSfRZ7PoZKz * fix: address PR #733 review comments (issue #725 Phase 1) - pageStore.ts: bump zustand persist version to 3 and backfill `linkType: 'wiki'` on legacy links / ghostLinks, mirroring the IndexedDB v3 migration. Without this, duplicate-check `link.linkType === linkType` fails on pre-#725 persisted rows and `addLink` / `removeLink` silently misbehave (Devin / CodeRabbit / Codex P1). - syncWithApi.ts: only clear the `'tag'` bucket when the pull payload contains at least one row with an explicit `link_type`. Without this guard, a pre-#725 server or any cached legacy response (no `link_type` on the wire) would trigger empty `saveLinks(source, [], 'tag')` calls and erase local tag edges during mixed-version rollout (Codex P1). - useEditorAutoSave.ts: drop the `length > 0` short-circuit on both wiki and tag sync calls. `syncLinksWithRepo` uses an empty array as the delta signal to clear stale edges; skipping the call when the user removes the last mark left orphaned edges in the DB / server (Gemini / Codex P1). Mirror the same contract in the unmount flush path. - useTagStatusSync.ts: compare tag name signatures (sorted, normalized) instead of tag counts so `#foo` → `#bar` rewrites still trigger a status refresh, and drop the `pageTitles.size === 0` early return so a previously resolved tag can be flipped back to `exists: false` when its target page disappears from the scope (Gemini P2). - usePageEditorAutoSaveWithMutation.ts: replace the empty `/** */` blocks that lint-staged autofix injected with one meaningful bilingual TSDoc on the exported hook, and drop the noise blocks around internal consts (Devin / CodeRabbit cleanup). https://claude.ai/code/session_01VXcHb8Bt62oxSfRZ7PoZKz * fix: align pageStore read defaults to 'wiki' + bilingual cleanups (PR #733 review) - pageStore.ts: `getOutgoingLinks` / `getBacklinks` / `getGhostLinkSources` の未指定 `linkType` を `'wiki'` にフォールバックさせ、インターフェースの TSDoc で「既定 `'wiki'`」と謳っている契約に実装を揃える。これまでは undefined を「全種別」として扱っており、`addLink` / `removeLink` の既定 ('wiki') と挙動が食い違っていた (CodeRabbit potential issue)。 - pageStore.ts `promoteGhostLink`: 英語のみだったインラインコメントに日本語 訳を併記 (bilingual requirement)。 - useEditorAutoSave.test.ts: タグ dedup テストを強化し、同一タグ (`#tech`) を 2 回置いても `getUniqueTagNames` で 1 件に畳まれて `syncTags` に 渡ることを検証 (CodeRabbit test-hardening nitpick)。 - useEditorAutoSave.test.ts: 日本語のみだった `syncTags 未指定` ケースの コメントに英語を併記 (bilingual requirement)。 - syncWithApi.test.ts: `link_type='tag'` push のテストタイトルをバイリンガル にリネーム (bilingual requirement)。 https://claude.ai/code/session_01VXcHb8Bt62oxSfRZ7PoZKz * fix(sync): unify link_type detection across links / ghost_links (PR #733 Devin review) 独立に `hasExplicitLinkType(res.links)` と `hasExplicitLinkType(res.ghost_links)` を判定していた構造だと、`res.links` に `link_type: 'tag'` が含まれても `res.ghost_links` が空配列のとき、ghost バケット側で tag のクリアが発動せず、 ローカルに stale なタグゴーストが残ってしまう。レスポンス全体から「サーバが link_type を理解している」根拠を推定し、links / ghost_links の両方を同じ linkType セットで適用するよう統一した。 Previously `hasExplicitLinkType` was checked independently for links and ghost_links, so a response with `link_type: 'tag'` on `res.links` but an empty `res.ghost_links: []` would leave stale local tag ghost links intact. Derive `serverSpeaksLinkType` from the whole payload and use the same set for both buckets. https://claude.ai/code/session_01VXcHb8Bt62oxSfRZ7PoZKz * test: cover syncTags didSave=false + pageId constant (PR #733 nitpicks) - useEditorAutoSave.test.ts: ハードコードされた "page-1" を共通の `pageId` 定数に置き換え、fixture id が変わってもアサーションが追従するようにする。 - useEditorAutoSave.test.ts: WikiLink 版と対になる「保存が skipped (didSave false) でも syncTags が呼ばれる」契約テストを追加し、link-graph 同期が保存 成否に独立して走ることを明示する。 - Replace hard-coded "page-1" literals with the shared `pageId` constant in useEditorAutoSave test assertions so fixture renames stay cheap. - Mirror the existing WikiLink didSave=false test for `syncTags` to pin the contract that link-graph sync runs regardless of whether `onSave` reports a successful write. https://claude.ai/code/session_01VXcHb8Bt62oxSfRZ7PoZKz * docs(storage): replace empty StorageAdapter JSDoc with bilingual description (PR #733) PR #733 の `lint-staged` autofix が `jsdoc/require-jsdoc` ルールに反応して empty JSDoc を挿入してしまい、AGENTS.md の「export には TSDoc / JSDoc を付与」 「日英併記」両方に違反していた。意味のあるバイリンガルな説明に置き換える。 The lint-staged autofix on PR #733 dropped an empty `/** */` block onto the exported `StorageAdapter` interface in response to the `jsdoc/require-jsdoc` rule, which violated AGENTS.md's "export must have TSDoc / JSDoc" and "comments should be bilingual" requirements. Replace it with a meaningful ja/en description. https://claude.ai/code/session_01VXcHb8Bt62oxSfRZ7PoZKz --------- Co-authored-by: Claude <noreply@anthropic.com>
* chore: align config with develop as default branch (#734) - .coderabbit.yaml: develop をデフォルトブランチ前提に更新し base_branches を main へ差し替え - .github/workflows/claude-code-review.yml: OIDC 関連コメントを develop 前提に更新 - scripts/setup-develop-branch.sh: 初期化スクリプトとしての役割を終えたため削除 Update config docs to reflect switching the default branch from main to develop. External (non-code) ops for the switch itself are tracked in #734. * chore(coderabbit): keep develop in base_branches during migration Codex レビュー指摘(#735)に対応。デフォルトブランチ切替(#734)は 外部操作のためマージと非同期に行われる。その移行期間中、default が main のままだと develop 宛 PR が CodeRabbit の auto-review 対象から抜け落ちる ため、main / develop の両方を明示しておく。切替完了後に develop を外す フォローアップ PR を出す方針をコメントにも記載。 Address Codex feedback on #735: the default-branch migration (#734) is an external op and races the merge of this PR. Keep both main and develop in auto-review base_branches until the switch completes, then drop develop in a follow-up PR. * chore(ci): make OIDC note branch-agnostic in claude-code-review CodeRabbit 指摘(#735)に対応。コメントで `develop` をハードコードしていた ため、将来のデフォルトブランチ変更で陳腐化する。「現在のデフォルトブランチ」 という中立な表現に変更し、CLAUDE.md のバイリンガル方針に合わせて英語併記も追加。 Address CodeRabbit feedback on #735: the OIDC note hard-coded `develop` and would become stale if the default branch changes again. Switch to neutral wording ("the repository's current default branch") and add the English translation per CLAUDE.md's bilingual comment policy. --------- Co-authored-by: Claude <noreply@anthropic.com>
…s (issue #726 Phase 2) (#736) * feat(api): propagate page title renames to WikiLink / tag marks (issue #726 Phase 2) When a page title changes via PUT /api/pages/:id/content, traverse every source document referencing it and rewrite the matching `wikiLink` marks (`attrs.title`) and `tag` marks (`attrs.name`) inside the Y.Doc stored in `page_contents.ydoc_state`. Segment text is only replaced when it still matches the old title (preserving manually-edited link text), while the mark attribute is always refreshed. Ghost links whose text now matches the new title are promoted to real `links` rows. Design choices (see issue #726 open questions): - Background (fire-and-forget) propagation; the main PUT response is not blocked on the rewrite. - Row-locked per-source transactions serialize with Hocuspocus's `onStoreDocument` writes; the live Y.Doc cache is invalidated after each successful source-page rewrite so collaborators reload from DB. - Best-effort: per-source failures are logged and counted, never aborting the batch or the originating request. Also extracts `invalidateHocuspocusDocument` into `server/api/src/lib/hocuspocusInvalidation.ts` so both the rename flow and the existing snapshot-restore flow share the same helper. Closes #726. https://claude.ai/code/session_01BMij57Zvw2nvhf7tdKwQ3f * fix(api): address PR #736 review feedback on title-rename propagation - gemini: use `node.toArray()` in the Y.Doc walker to replace the O(N^2) `node.get(i)` index loop with a single O(N) pass. - coderabbit: `hocuspocusInvalidation` now logs a warn in every env when `HOCUSPOCUS_INTERNAL_URL` / `BETTER_AUTH_SECRET` is missing (previously silent in production), naming the missing variable(s) in the message. - chatgpt-codex P1: `promoteGhostLinks` now scopes promotion by the renamed page's `pages.note_id` / `pages.owner_id`. Without this, renaming a page in one tenant could consume ghost rows on unrelated users' / notes' pages and create cross-scope `links` edges. Note-native targets match by `note_id`; personal targets match by `note_id IS NULL AND owner_id = ...`. A new test covers the guard. - coderabbit: rewriting a source page's Y.Doc now re-derives the plain text via `extractTextFromYXml`, writes it into `page_contents.content_text` in the same transaction, and refreshes `pages.content_preview`. Search and listing metadata stay consistent with the rewritten document. - Adds `server/api/src/lib/extractPlainTextFromYXml.ts` duplicating the Hocuspocus-side helper (server/api and server/hocuspocus are separate Bun projects per CLAUDE.md; snapshot utilities already follow this mirror-with-note pattern). Tests updated to reflect the new query plans. Tracks #726 / PR #736. https://claude.ai/code/session_01BMij57Zvw2nvhf7tdKwQ3f --------- Co-authored-by: Claude <noreply@anthropic.com>
… (#738) * feat: rename propagation by targetId + shared tag char class (issue #737) Implements the two follow-ups tracked by issue #737: 1. Same-title rename ambiguity (approach A — `targetId` on marks) - Add `targetId` attribute to `wikiLink` and `tag` Tiptap marks. - Populate it during link resolution (`useWikiLinkStatusSync`, `useTagStatusSync`, `useBubbleMenuWikiLink`); `useWikiLinkExistsChecker` now returns a `pageTitleToId` map. - Switch `rewriteTitleRefsInDoc` to id-strict matching when a mark carries `targetId`; id-less marks still fall back to title matching for lazy migration of pre-existing Y.Docs. - Plumb `renamedPageId` through `propagateTitleRename` → `rewriteSourcePage` → `rewriteTitleRefsInDoc`. 2. Shared tag character class - New `@zedi/shared` workspace package owns `TAG_NAME_CHAR_CLASS` (single source of truth for client + admin). - Client `TagExtension` builds its regex from the shared constant. - `server/api` (intentionally outside the workspace, see AGENTS.md) keeps a duplicated `TAG_NAME_CHAR_CLASS_STRING`; a new client-side drift-check vitest (`src/lib/tagCharacterClassSync.test.ts`) reads the server file and asserts both literals match in CI. - AGENTS.md documents the cross-workspace sharing pattern and the server-side drift-check workaround. * chore: replace stub JSDocs with real descriptions on issue #737 exports The previous commit's lint-staged auto-fix added blank `/**\n *\n */` stubs to every variable declaration in `useBubbleMenuWikiLink.ts` and to `WikiLinkInfo` in `wikiLinkUtils.ts`. Replace those with real JSDoc descriptions on the public exports so `jsdoc/require-jsdoc` is satisfied without the stub noise polluting the file body. * refactor(hooks): single-pass scope resolution in useWikiLinkExistsChecker Address Gemini review on PR #738: collapse the duplicated note / personal branches into one walk that builds both `pageTitles` and `pageTitleToId` from a unified `sourcePages` list. The previous shape called `.map()` to seed the Set and a separate `for` loop to populate the Map for each branch, walking the candidate list twice. Behaviour is preserved (unloaded `notePages` in note scope still returns empty sets). * chore: address CodeRabbit nitpicks on PR #738 - packages/shared/src/tagCharacterClass.ts: clarify the hiragana / katakana Unicode comment so it no longer reads as "前半/後半" of a single block; list the two distinct blocks (U+3040..U+309F / U+30A0..U+30FF) and note that `-ヿ` covers them contiguously. - src/components/editor/TiptapEditor/useBubbleMenuWikiLink.ts: add an explicit `UseBubbleMenuWikiLinkResult` return type to the exported hook so the public contract is no longer inferred (matches project rule on explicit type annotations). - server/api/src/services/ydocRenameRewrite.ts: add a comment justifying why `VALID_TAG_NAME_REGEX` is ReDoS-safe (constant char class, single quantifier, no nested alternations) to preempt static-analysis false positives. - server/api/src/__tests__/services/ydocRenameRewrite.test.ts: add tag parity tests for the two `renamedPageId`-omitted fallback branches that previously only covered wikiLink marks (skip-when-targetId-present and rewrite-when-id-less). * fix(api): preserve legacy fragmentName form and correct renamedPageId TSDoc Address two CodeRabbit review comments on PR #738: 1. `rewriteTitleRefsInDoc` accepted a `fragmentName: string` as its 4th argument before issue #737 and now expects an options object. Any surviving caller passing a string would silently retarget the default fragment because `"foo".fragmentName === undefined`. Add a compatibility shim that normalizes a string 4th argument into `{ fragmentName }`. Two new tests lock in the legacy call form and guard against a regression that would silently rewrite the default fragment. 2. The TSDoc on `RewriteTitleRefsOptions.renamedPageId` claimed that omitting it falls back to title-only matching for *every* mark, but the implementation (and the issue #737 tests) skip marks that already carry a `targetId` because they cannot be safely verified. Update both the Japanese and English descriptions to match the real contract. --------- Co-authored-by: Claude <noreply@anthropic.com>
…bility (#739) * feat(search): clarify scope contracts and dedup shared results (Issue #718 Phase 5-4) - Document `useSearchPages` as personal-scope-only and `useSearchSharedNotes` as mixed-scope on the TSDoc/type level so the Phase 4 personal/note split is visible to callers. - Extract `buildGlobalSearchResults` as a pure helper from `useGlobalSearch` so the merge / dedup behavior can be tested directly. The hook now delegates to it. - Drop shared rows whose `note_id` is null (server still returns personal pages under `scope=shared`). Without this filter the same personal page surfaces twice — once via IDB `useSearchPages`, once via the API. - Apply the same dedup in `SearchResults` so the dedicated search page matches the global search dialog. - Add tests covering the dedup contract and personal/note-native split. https://claude.ai/code/session_0152ekY42SwWkwPw366D9A2h * fix(search): dedup global search by pageId, not by note_id Address review feedback on PR #739 (Codex / CodeRabbit / Gemini): - The previous `note_id !== null` filter dropped valid shared search hits. `scope=shared` returns linked personal pages (`note_id IS NULL`) reachable via note membership/ownership; those rows are NOT in IDB, so filtering by `note_id` removed search results for collaborators / note owners. It also hid every shared hit while the local IDB index was still hydrating. Replace with `dedupSharedRowsAgainstPersonal(rows, personalIds)` which only drops rows whose `id` overlaps with the personal IDB result set, and centralize that helper so `useGlobalSearch` and `SearchResults` share it. - Drop the redundant intermediate `.sort()` on `personal` inside `buildGlobalSearchResults` — the final merge sorts by score (Gemini). - Update tests: cover linked-personal-page survival and IDB-not-hydrated case, replace the old `note_id`-based dedup tests with `pageId`-based ones, and add unit tests for the new `dedupSharedRowsAgainstPersonal` helper. https://claude.ai/code/session_0152ekY42SwWkwPw366D9A2h --------- Co-authored-by: Claude <noreply@anthropic.com>
* test: strengthen assertions to kill surviving mutants (issue #471) Phase 2 of the mutation testing remediation epic: strengthen assertions in test files where coverage exists but mutants survive due to weak expectations. Targeted four files with the highest survivor density per exploratory analysis. Per-file mutation scores after changes (Stryker, --mutate on the four implementation files): - src/lib/aiChatConversationTitle.ts: 100.00% (16 killed, 0 survived) - src/lib/encryption.ts: 96.43% (27 killed, 1 survived) - src/lib/parseStdioArgs.ts: 66.67% (34 killed, 16 survived; remaining survivors are largely equivalent mutants constrained by the tokenizer regex) - src/hooks/useImageUpload.ts: 71.28% (67 killed, 19 survived, 6 errors); previously had a single happy-path assertion Highlights of the new assertions: - useImageUpload: 27 tests covering URL return, abort guards, storage-not- configured branch, non-image rejection (table-driven), JPEG/PNG → WebP conversion vs. GIF/WebP/SVG passthrough, error-state capture for both Error and non-Error throwables, post-resolve abort invariant, isConfigured truth table, clearError narrowness, and uploadImages batch filtering / order / failure paths. - parseStdioArgs: boundary cases for empty/whitespace input, escape unwinding (\\\" and \\\\) inside double quotes, asymmetric single-quote literal preservation, empty quoted segments, and adjacency concat. - aiChatConversationTitle: 50-char truncation boundary (49/50/51 + 100), multibyte semantics, first-user selection over subsequent user/leading assistant messages, empty-map and null-leaf branches. - encryption: IV positioning (prepend, not append), GCM tamper detection for both IV and payload bytes, stored-key reuse across calls, post- clearEncryptionKey regeneration breaking old ciphertext. Source code is unchanged; only tests are added/strengthened. * test: address review feedback on mutation-killing tests (#740) - useImageUpload: assert exact `file` reference (not `expect.any(File)`) to kill mutations that swap files (gemini-code-assist, codex) - useImageUpload: drive the captured `onProgress` callback after upload resolves and observe state.progress to prove the wiring, not just the callback presence (gemini-code-assist, codex, coderabbit) - useImageUpload: assert exact image-file references in the uploadImages filter test (gemini-code-assist) - useImageUpload: clearError test now snapshots isUploading/progress before the call and re-asserts equality after, matching the title (coderabbit) - encryption: extract `expectDecryptRejectsFromBytes` helper to centralize the tamper → reject pattern across IV/payload tests (coderabbit) --------- Co-authored-by: Claude <noreply@anthropic.com>
* test(P1): add TDD coverage for src/lib/aiService* (#742) `src/lib/aiService*` and `aiClient.ts` were under Stryker `mutate` but had near-zero coverage in 4 of 5 files. Add table-driven tests covering normal, boundary, error, and abort paths so callbacks and provider routing are verified against mutation. Coverage (statements): - aiClient.ts 31.4% → 99.0% - aiService.ts 41.3% → 95.7% - aiServiceDirectProviders.ts 95.5% → 100% - aiServiceModels.ts 9.3% → 100% - aiServiceServer.ts 10.3% → 100% Local mutation scores (per source file, threshold 70%): - aiClient.ts 90.20% - aiService.ts 77.78% - aiServiceDirectProviders.ts 80.90% - aiServiceModels.ts 76.52% - aiServiceServer.ts 81.58% Notes: - `aiServiceModels.test.ts` covers cache hit/expiry/forceRefresh, snake_case normalization, fetch error mapping (NO_BASE_URL / NETWORK / HTTP / INVALID_RESPONSE), and `fetchUsage` payload validation. - `aiServiceServer.test.ts` covers SSE chunking across boundaries, done detection, partial completion fallback, 401 → AUTH_REQUIRED mapping, abortSignal, and missing response body. - `aiServiceDirectProviders.test.ts` covers Anthropic system-message aggregation, Claude web-search auto-detection toggle, Google search default, default `max_tokens` / `temperature`, and abort propagation per provider. - `aiService.test.ts` adds claude-code dispatch (isAvailable / done / tool_use_start+complete / error / abort), `settings.modelId` override, and the `apiMode` undefined → `api_server` fallback. * test(aiClient): clarify corrupt-cache test name and comment (#742) Address reviewer feedback on PR #750: the previous "recovers" wording implied the corrupt cache was repaired, but the assertion confirms it is *not* fixed — saveCachedModels just exits via catch without overwriting. Rename and rewrite the bilingual comment to match actual behavior. * test(aiServiceModels): actually exercise the Number.isFinite guard (#742) Codex pointed out (PR #750) that `JSON.stringify(Infinity)` becomes `null`, so the previous "Infinity/NaN" test only validated the missing-field path via `typeof !== 'number'`, never the `Number.isFinite` branch in `toNum`. Use a raw JSON body containing `1e1000` so `JSON.parse` yields an actual Infinity, which now hits the finite-number guard. NaN is unrepresentable in JSON; the symmetric branch is covered by the same code path. * test(aiService): reset module cache around vi.doMock claude-code tests (#742) Address CodeRabbit review on PR #750: `vi.doUnmock` removes the mock registration but leaves cached dynamic-import modules intact, so a subsequent `vi.doMock` for the same path could return a stale module captured by the previous test. Add `vi.resetModules()` in beforeEach and afterEach to guarantee each test sees a fresh module instance. No behavior change; all 27 tests still pass. * test(aiService): guarantee stub cleanup via afterEach (#742) Address CodeRabbit review on PR #750: the API-server tests previously called vi.unstubAllEnvs / vi.unstubAllGlobals at the tail of each test body, which would be skipped if an earlier assertion threw, leaking fetch / env stubs into later tests. Hoist the cleanup to a describe- scoped afterEach so it runs unconditionally. * test(aiService): type Claude Code mock against real module (#742) Address CodeRabbit nitpick on PR #750: anchor `buildProviderModule`'s return type to `typeof import("@/lib/aiProviders/claudeCodeProvider")` so any change in `createClaudeCodeProvider` / `UnifiedAIProvider` shape (id, name, capabilities, isAvailable, abort, query) trips a TypeScript error here instead of silently drifting from the contract. No behavior change; tsc + 27 tests pass. --------- Co-authored-by: Claude <noreply@anthropic.com>
…751) * test(hooks): add coverage for 6 high-value src/hooks/ modules (#743) Add renderHook-based vitest suites for the hooks listed in #743 to lift mutation score on `src/hooks/**`: - useGeneralSettings: load/save lifecycle, theme/locale sync, font-size clamping, persistence error handling. - useAISettings: async load fallbacks, provider/model switching rules, connection-test side effects, save/reset flow. - useMermaidGenerator: state machine (idle → generating → completed/error), input validation, AI configuration check, reset. - useWorkflowDraft: step CRUD, template loading, save validation, JSON import/export, saved-definition selection. - useWorkflowRunSession: validation guards, run outcomes (completed/paused/stopped/error), pause/stop signal forwarding, unmount abort cleanup. - useAIChatPanelContentLogic: composition contract — page-title derivation forwarded to useAIChat, lifecycle/handler params wired through, branch prefill handlers exposed. All 88 new tests pass; full vitest run (1891 tests) green. * test(hooks): harden useWorkflowDraft.test.ts cleanup and timers (#743) Address Gemini Code Assist review on PR #751: - Add a file-wide afterEach that calls vi.restoreAllMocks() and vi.useRealTimers() so spies on URL/HTMLAnchorElement and any fake-timer setup are restored even when a test throws. - Replace the real `setTimeout(..., 5)` waiter in the "exportJson opens a download link" test with vi.useFakeTimers() + vi.runAllTimers() to deterministically flush the `window.setTimeout(..., 0)` revoke callback in the implementation, removing a CI-flakiness hazard. - Drop the now-redundant manual mockRestore() calls. All 17 useWorkflowDraft tests still pass. * test(hooks): address CodeRabbit review on PR #751 (#743) - useAISettings.test.ts: type the seven `vi.fn()` mocks with explicit function signatures to satisfy strict TS, and add a file-wide `afterEach(restoreAllMocks)` so the console.error spy is restored even on early failures. - useMermaidGenerator.test.ts: type both module mocks with explicit signatures (Promise<void> for generateMermaidDiagram, Promise<AISettings> for getAISettingsOrThrow) and add safety afterEach. - useGeneralSettings.test.ts: lock in the save() success-path contract by asserting `mockSaveGeneralSettings` was called once with the current settings. - useWorkflowDraft.test.ts: tighten the addStep updatedAt assertion to `toBeGreaterThan` so the test catches stale timestamps. Also mirror the real store's `sort by updatedAt desc` in the mocked upsertDefinition so order-dependent regressions surface. - useWorkflowRunSession.test.ts: replace the unsafe `as PageContext` cast with a valid `"home"` discriminant for the editorRequired guard test. All 88 hook tests + 1891-test full suite still green. * test(hooks): tighten useAISettings.test.ts per CodeRabbit nitpicks (#743) - Drop the two redundant `errorSpy.mockRestore()` calls now that the file-wide `afterEach(() => vi.restoreAllMocks())` handles cleanup on every test (including failing ones). - Switch the "errors thrown synchronously by testConnection" test from `mockRejectedValueOnce` (an async rejection) to `mockImplementationOnce(() => { throw ... })` so the mock matches the test title and actually exercises the synchronous-throw path through the hook's try/catch. All 17 useAISettings tests still pass. * test(hooks): drop stale comment in useAISettings test (#743) The comment "Pre-populate the modelId via save flow" misdescribed the test — it pre-populates state via `updateSettings`, not via the save flow. The test name already conveys the intent ("changing only the model also clears modelId unless explicit"), so per the project's "default to no comments" guideline, drop the misleading line rather than add a redundant one. Spotted in CodeRabbit nitpick on PR #751. * test(hooks): forward createConversation args in mock + assert snapshot (#743) Address Devin Review finding on PR #751: the `useAIChatConversations` mock factory dropped all arguments to `createConversation`, so the "handleSendMessage creates a conversation snapshot from pageContext on first send" test could not verify the snapshot was actually forwarded — a regression that removed the snapshot from `useAIChatPanelContentHandlers.handleSendMessage` would still pass the test. - Forward args in the mock: `(...args) => mockCreateConversation(...args)`. - Assert `mockCreateConversation` was called with exactly the snapshot shape `{ type, pageId, pageTitle }` derived from `editorContext`, excluding fields like `pageFullContent` that the handler intentionally omits. All 17 useAIChatPanelContentLogic tests still pass. --------- Co-authored-by: Claude <noreply@anthropic.com>
…752) * test(P3): cover agentSlashCommands and slashSuggestionPlugin (#744) Adds unit tests for 8 of 11 files in `src/lib/agentSlashCommands/` (plus the existing `parseAgentSlashQuery.test.ts`, totalling 9/11) and a ProseMirror-state-driven test for `slashSuggestionPlugin`. - Table-driven coverage of all 8 `buildAgentSlashPrompt` branches and the per-command tool/policy map. - Editor-mock based tests for `getEditorPlainText` / `getEditorSelectionText`, the `/explain` selection cache, the global hook registry, the localStorage insert-position helpers (incl. SSR fallback), and Markdown insertion fallbacks. - Mocked-collaborator integration tests for `executeAgentSlashCommand` covering hook short-circuit, hook errors, Claude success/failure, the cwd merge, and the `/explain` cache lifecycle. - Minimal-schema ProseMirror state harness for `slashSuggestionPlugin`: activation triggers, deactivation paths, decoration ranges, and explicit close meta. All assertions use exact-string / key-phrase checks (no snapshots) so mutations are caught. * test: address review feedback on agentSlashCommands tests (#752) - executeAgentSlashCommand.test.ts: replace `expect.any(Number)` with the deterministic literal `12` so the test fails if the post-delete cursor regresses (gemini-code-assist). - insertPosition.test.ts: also assert `localStorage.getItem` / `setItem` were not called in the no-`window` cases. This pins the SSR early-return even if `delete window` silently fails on a non-configurable property in some jsdom builds (gemini-code-assist). - buildAgentSlashPromptParts.test.ts: `/explain` falls back via `editor.state.doc.textBetween`, not `editor.getText` — fix the assertion accessor so a regression in the explain fallback is actually caught (coderabbitai). * test: drop unused @ts-expect-error in insertPosition SSR test (#752) The cast `(globalThis as { window?: unknown }).window` already makes the `delete` expression type-safe (window is typed as optional there), so the `@ts-expect-error` directive is unused. Removing it (per coderabbitai). --------- Co-authored-by: Claude <noreply@anthropic.com>
* test(P4): add coverage for src/stores/ and storage layer (#745) - pageStore: ゲスト CRUD / リンク / ゴーストリンク / 永続化 + v1→v2→v3 マイグレーション - aiChatStore: 全 action と partialize された永続キーの契約 - storage/index: factory 全分岐 + cloudflare-r2 → s3 レガシー migration / アップロード設定 - pageRepository: IPageRepository 契約テスト + onMutate コールバック Closes #745. * test(pageRepository): make in-memory mock soft-delete consistent (#745) gemini-code-assist のレビュー対応: - getPagesSummary / getPagesByIds / getPageByTitle / checkDuplicateTitle に isDeleted フィルタを追加し、read 系全体で soft-delete 挙動を統一。 - searchPages を pageStore.searchPages の契約に揃える(title+content 部分一致 / 空クエリは [] / 論理削除は除外)。 - soft-delete とサーチ挙動のリグレッションテストを追加。 Refs #753 review comments r3143213735 / r3143213736. * test(pageRepository, aiChatStore): address CodeRabbit review (#745) - pageRepository.test: 実プロダクション StorageAdapterPageRepository が IPageRepository を満たし、adapter / API へ委譲することを spy で検証する スモークスイートを追加(CodeRabbit 主指摘: contract が in-memory モック だけだと実装側のリグレッションを見逃す)。 - pageRepository.test: onMutate を CRUD に加え link / ghost link の mutation でも発火するように mock を拡張し、テストも 3 → 7 mutations に拡大。 - aiChatStore.test: rehydrate 後に揮発フィールド(activeConversationId / isStreaming / showConversationList)が初期値のまま蘇らないことを明示 assert。 Refs #753 review. * test(pageRepository, aiChatStore): address CodeRabbit follow-up review (#745) CodeRabbit のレビュー対応: - pageRepository.test: ゲスト経路の createPage 委譲テストに expect(api.createPage).not.toHaveBeenCalled() を追加し、誤って API を 叩くリグレッションをガード(Minor 指摘)。 - pageRepository.test: createMockApi の戻り型を Partial<ApiClient> に変更し 二重キャスト (as unknown as ApiClient) を解消(nitpick)。 - aiChatStore.test: rehydrate のテスト名を実際の assertion に合わせて 「unspecified volatile fields at defaults」へ変更(nitpick)。 Refs #753 review. ✅ Addressed in this commit --------- Co-authored-by: Claude <noreply@anthropic.com>
) * test(P5): add server/api security & core service unit tests (#746) Cover the six high-risk modules called out in the issue with focused unit tests, modeled on the existing mcpAuth / rateLimit / auditLog test patterns: - middleware/adminAuth: 401 unauth, 403 non-admin / missing-row, admin allow, DB short-circuit on missing userId - middleware/csrfOrigin: safe-method bypass, mutation enforcement (Origin / Referer / both / malformed), CORS_ORIGIN edge cases (unset, "*", multi), excluded paths (/api/webhooks/*, /api/ext/session, /api/ext/clip-and-create) and the deliberate inclusion of /api/ext/authorize-code (cookie-based) - middleware/errorHandler: HTTPException pass-through, statusMap mapping for every documented magic message, unknown-error 500 fallback, log emission - lib/userDelete: getUserImpact aggregation + null fallbacks; anonymizeUser PII redaction, suspension-field clearing, fixed cascade order (select → delete sessions → delete accounts → update users), missing-user short-circuit, and PII-free audit-log before-snapshot - services/subscriptionService: tier lookup (pro / free fallback), 30s TTL cache hit & expiry, per-userId cache isolation, getSubscription nullability - services/magicLinkService: outbound request shape (URL / method / JSON body), Accept-Language locale propagation (default ja, override), trailing-slash trimming, non-OK + empty-body responses, thrown-error absorption All six new test files (62 tests total) pass. Verified via: cd server/api && bunx vitest run \ src/__tests__/middleware/{adminAuth,csrfOrigin,errorHandler}.test.ts \ src/__tests__/lib/userDelete.test.ts \ src/__tests__/services/{subscriptionService,magicLinkService}.test.ts Pre-existing failures in unrelated suites (resend / @react-email module resolution, pageSnapshots, media routes) are not touched by this change. * test(P5): tighten userDelete & magicLinkService tests per review Address gemini-code-assist feedback on PR #754: - userDelete.test.ts: enhance the chain-recording mock to capture every chained call (`from`, `where`, `set`, `returning`, …) alongside the top-level op. This collapses the second nested-proxy spy used in the "anonymizes ... payload" test into a simple chain lookup. - userDelete.test.ts: strengthen the cascade-order assertion to verify the target table at each step (select.from(users), delete(session), delete(account), update(users)). Method-name-only checks couldn't catch an accidental session/account swap or a wrong update target. - userDelete.test.ts: also assert the four `getUserImpact` selects target the expected tables (notes, session, subscriptions, aiUsageLogs). - magicLinkService.test.ts: switch the `getEnv` mock to a `vi.fn()` with a mockImplementation default, so the trailing-slash test can use `mockReturnValueOnce` instead of `vi.resetModules()` + `vi.doMock()` + re-importing the SUT. All 16 tests in the two affected files still pass. #754 * test(magicLink): assert handler call count before indexing mock.calls Per coderabbitai nitpick on PR #754: indexing `mock.calls[0]?.[0]` swallows the failure mode where the mock wasn't called at all (you'd see a confusing "cannot read .url of undefined" instead of "expected 1 call, got 0"). Adding `expect(handlerMock).toHaveBeenCalledTimes(1)` first and dropping the optional chain makes diagnostics explicit and shifts the failure to the right line. #754 * test(magicLink): restore optional chain on mock.calls index for noUncheckedIndexedAccess The previous commit dropped `?.` from `handlerMock.mock.calls[0][0]` for diagnostic clarity, but the API tsconfig has `noUncheckedIndexedAccess: true` and the unguarded index trips TS2532. The diagnostic value is unchanged: the preceding `expect(handlerMock).toHaveBeenCalledTimes(1)` already fails first when the mock wasn't called, so the optional chain is effectively dead at runtime — it only exists to satisfy the type checker. Fixes the API Type Check job on PR #754. #754 --------- Co-authored-by: Claude <noreply@anthropic.com>
* fix(api): add missing 0018 migration for onboarding & pages.kind PR #728 added the userOnboardingStatus table and the pages.kind column to the Drizzle TS schema but never produced a matching migration under server/api/drizzle/. CI runs only `bunx drizzle-kit migrate`, so the legacy SQL placed in db/migrations/005_*.sql was never applied to either dev or prod, leaving production with `relation "user_onboarding_status" does not exist` and `column "kind" of relation "pages" does not exist` (500 on GET /api/onboarding/status and POST /api/pages). This commit: - Adds server/api/drizzle/0018_add_onboarding_and_page_kind.sql with the pages.kind column + CHECK, the partial unique index used by welcomePageService.onConflictDoNothing, the user_onboarding_status table, the retry-scan partial index, and a backfill that marks existing users as setup-complete so they are not pushed back through the wizard. - Mirrors the partial unique index idx_pages_unique_welcome_per_owner in the TS schema for documentation parity. - Removes the legacy db/migrations/ directory entirely; CI never touched it and leaving it around invites the same regression. README is updated to point at server/api/drizzle/ as the single source of truth. Re-fix-prevention: - New CI job drizzle-migration-check (PR-only) runs scripts/check-drizzle-migrations.mjs, which fails when a PR modifies any server/api/src/schema/**/*.ts file without adding a matching server/api/drizzle/NNNN_*.sql and updating _journal.json. PRs that truly do not need a DB migration can opt out with [skip drizzle-check]. - AGENTS.md / CLAUDE.md document the schema/migration pairing rule and the develop->dev / main->prod auto-apply pipeline. Made-with: Cursor * fix: address PR #755 review comments Tighten the Drizzle migration consistency checker by detecting deleted schema files, honoring the documented types/ exclusion, failing fast in CI when the diff base is unavailable, and aligning JSDoc with the required SQL-plus-journal policy. Made-with: Cursor
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds link-type ("wiki"/"tag") support across domain, storage, and sync; implements title-rename propagation with Yjs rewrites and Hocuspocus invalidation; extracts shared TAG_NAME_CHAR_CLASS into Changes
Sequence Diagram(s)sequenceDiagram
rect rgba(240,248,255,0.5)
participant Client as Client
participant API as Pages API
participant DB as Database
participant Service as TitleRenameService
participant Hocus as Hocuspocus
end
Client->>API: PUT /api/pages/:id/content (new title)
API->>DB: SELECT current title
DB-->>API: current title
API->>Service: propagateTitleRename(renamedPageId, oldTitle, newTitle) [fire-and-forget]
Service->>DB: SELECT source pages linking to renamed page
Service->>DB: SELECT ... FOR UPDATE page_contents per source
Service->>Service: rewriteTitleRefsInDoc (Y.Doc)
Service->>DB: UPDATE page_contents, pages.contentPreview
Service->>Hocus: POST /internal/invalidate/:pageId (x-internal-secret)
Hocus-->>Service: 2xx / non-2xx (logged)
API-->>Client: 200 OK (response not blocked)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Code Review
This pull request implements a robust system for propagating page title renames across WikiLinks and tags, introduces the @zedi/shared package for workspace-wide constants, and adds a CI check to ensure database schema changes are paired with migrations. It also updates the link graph to distinguish between WikiLinks and hashtags using a new link_type field. Feedback identifies a critical bug in the IndexedDB migration logic where asynchronous schema modifications will trigger runtime errors. Furthermore, there are performance concerns regarding the sequential execution of rename propagations and the use of database queries that bypass indexes, which could lead to scalability issues.
| function migrateLinkStoreToV3<TRow extends Record<string, unknown>>( | ||
| db: IDBDatabase, | ||
| tx: IDBTransaction, | ||
| storeName: string, | ||
| _oldKeyPath: string[], | ||
| mapRow: (row: TRow) => V3StoreSpec<TRow>, | ||
| ): void { | ||
| if (!db.objectStoreNames.contains(storeName)) return; | ||
| const oldStore = tx.objectStore(storeName); | ||
| const getAllReq = oldStore.getAll(); | ||
| getAllReq.onsuccess = () => { | ||
| const rows = (getAllReq.result as TRow[]) ?? []; | ||
| const firstRow = rows[0]; | ||
| const spec = firstRow !== undefined ? mapRow(firstRow) : undefined; | ||
|
|
||
| db.deleteObjectStore(storeName); | ||
| const newStore = db.createObjectStore( | ||
| storeName, | ||
| spec | ||
| ? { keyPath: spec.keyPath } | ||
| : // Empty store: use hard-coded v3 shape keyed per `storeName`. | ||
| // 行が 1 件も無い場合は mapRow を呼べないため、ストア名から v3 の | ||
| // keyPath を決め打ちで作る。 | ||
| { | ||
| keyPath: | ||
| storeName === "my_links" | ||
| ? ["sourceId", "targetId", "linkType"] | ||
| : ["linkText", "sourcePageId", "linkType"], | ||
| }, | ||
| ); | ||
| const indexSpecs: { name: string; keyPath: string | string[] }[] = | ||
| spec?.indexes ?? | ||
| (storeName === "my_links" | ||
| ? [ | ||
| { name: "by_source", keyPath: "sourceId" }, | ||
| { name: "by_target", keyPath: "targetId" }, | ||
| { name: "by_source_type", keyPath: ["sourceId", "linkType"] }, | ||
| ] | ||
| : [ | ||
| { name: "by_source", keyPath: "sourcePageId" }, | ||
| { name: "by_source_type", keyPath: ["sourcePageId", "linkType"] }, | ||
| ]); | ||
| for (const idx of indexSpecs) { | ||
| newStore.createIndex(idx.name, idx.keyPath, { unique: false }); | ||
| } | ||
| for (const row of rows) { | ||
| const migrated = mapRow(row); | ||
| newStore.put(migrated.value); | ||
| } | ||
| }; | ||
| } |
There was a problem hiding this comment.
この migrateLinkStoreToV3 関数内の実装には重大な問題があります。IndexedDB の createObjectStore および deleteObjectStore メソッドは、onupgradeneeded イベントハンドラの同期的な実行コンテキスト内でのみ呼び出すことができます。getAll() の onsuccess コールバックのような非同期コンテキストでこれらのメソッドを呼び出すと、InvalidStateError がスローされます。
IndexedDB では既存のストアの keyPath を変更できないため、データを保持したまま移行するには、onupgradeneeded 内で同期的に新しい名前のストアを作成し、そのトランザクション内でカーソル等を用いてデータをコピーする手法が一般的です。現状の実装では既存ユーザーの環境で DB アップグレードが失敗し、アプリが正常に起動できなくなるため、修正が必須です。
There was a problem hiding this comment.
確認しました。現実装は onupgradeneeded から取得した versionchange transaction 内の request callback で完結しており、deleteObjectStore / createObjectStore の呼び出し条件を満たしているため修正不要と判断しました。
| for (const sourceId of uniqueSourceIds) { | ||
| result.sourcePagesAttempted += 1; | ||
| try { | ||
| const { changed, rewrite } = await rewriteSourcePage( | ||
| db, | ||
| sourceId, | ||
| renamedPageId, | ||
| trimmedOld, | ||
| trimmedNew, | ||
| ); | ||
| result.sourcePagesSucceeded += 1; | ||
| result.wikiLinkMarksUpdated += rewrite.wikiLinkMarksUpdated; | ||
| result.wikiLinkTextUpdated += rewrite.wikiLinkTextUpdated; | ||
| result.tagMarksUpdated += rewrite.tagMarksUpdated; | ||
| result.tagTextUpdated += rewrite.tagTextUpdated; | ||
|
|
||
| if (changed) { | ||
| try { | ||
| await invalidate(sourceId); | ||
| } catch (error) { | ||
| console.warn( | ||
| `[RenamePropagation] Invalidation failed for source page ${sourceId}:`, | ||
| error, | ||
| ); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| result.sourcePagesFailed += 1; | ||
| console.error( | ||
| `[RenamePropagation] Failed to rewrite source page ${sourceId} ` + | ||
| `for rename ${renamedPageId} (${trimmedOld} → ${trimmedNew}):`, | ||
| error, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
参照元ページをループで1つずつ処理していますが、特定のページが大量のページから参照されている場合(例:数百〜数千ページ)、この処理が完了するまでサーバーのリソース(DB接続やCPU)を長時間占有し続ける可能性があります。
fire-and-forget で実行されているためユーザーのレスポンスは直接ブロックしませんが、システム全体の安定性のために、Promise.all を用いた並列実行(同時実行数の制限付き)や、一定数ずつのチャンク処理、あるいはコメントにある通りバックグラウンドジョブキューへの移行を検討してください。また、Hocuspocus への無効化リクエストもループ内で順次実行されているため、ネットワーク遅延の影響を強く受けます。
There was a problem hiding this comment.
ご指摘ありがとうございます。並列化や job queue 化は妥当な改善ですが、今回のリリースブロッカーではなく負荷制御設計を伴うため別 Issue で扱うのが適切と判断しました。
| .innerJoin(pages, eq(pages.id, ghostLinks.sourcePageId)) | ||
| .where( | ||
| and( | ||
| sql`LOWER(TRIM(${ghostLinks.linkText})) = LOWER(TRIM(${newTitle}))`, |
There was a problem hiding this comment.
There was a problem hiding this comment.
ご指摘ありがとうございます。Functional Index 追加は DB migration を伴う性能改善のため、このレビュー対応 PR ではスコープ外とし、別対応候補とします。
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/editor/TiptapEditor/useWikiLinkStatusSync.ts (1)
109-124:⚠️ Potential issue | 🟠 MajorGuard against stale async sync results before mutating the editor.
checkExistence()is awaited here, but the effect has no generation/cancellation check after the await. IfpageId,pageNoteId, orcontentchanges while the request is in flight, the old run can still callapplyWikiLinkUpdates()andonChange()with outdatedexists/referenced/targetIddata.Suggested fix
useEffect(() => { + let cancelled = false; + if (skipSync || !editor || !content || !pageId) { return; } const updateWikiLinkStatus = async () => { if (currentWikiLinks.length === 0) { + if (cancelled) return; lastCheckedRef.current = { pageId, wikiLinkCount: 0, pageScopeSignature }; return; } const titles = getUniqueWikiLinkTitles(currentWikiLinks); const { pageTitles, referencedTitles, pageTitleToId } = await checkExistence(titles, pageId); + if (cancelled) return; if (pageTitles.size === 0 && titles.length > 0) { return; } const updates = collectWikiLinkUpdates(editor, pageTitles, referencedTitles, pageTitleToId); + if (cancelled) return; lastCheckedRef.current = { pageId, wikiLinkCount: currentCount, pageScopeSignature }; if (updates.length > 0) { applyWikiLinkUpdates(editor, updates); + if (cancelled) return; const json = JSON.stringify(editor.getJSON()); onChange(json); } }; const timer = setTimeout(updateWikiLinkStatus, 150); - return () => clearTimeout(timer); + return () => { + cancelled = true; + clearTimeout(timer); + }; }, [skipSync, editor, content, checkExistence, pageId, onChange, pageScopeSignature]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/editor/TiptapEditor/useWikiLinkStatusSync.ts` around lines 109 - 124, After awaiting checkExistence(titles, pageId) guard against stale results by validating the run is still current before mutating the editor: capture the current run identity (e.g., pageId/pageNoteId/content or the computed pageScopeSignature/currentCount) into a local variable before the await and then compare it to the latest values (or lastCheckedRef/current refs) immediately after the await; if they differ, return early and do not call collectWikiLinkUpdates or applyWikiLinkUpdates. Ensure the same check is performed before calling applyWikiLinkUpdates(editor, updates) so outdated async responses never mutate the editor or update lastCheckedRef.current.src/components/editor/PageEditor/useEditorAutoSave.ts (1)
85-93:⚠️ Potential issue | 🟠 MajorSkip graph sync when unmount flush save throws
At Line 87, a thrown
saveAction()is logged, but Line 92 still runssyncGraphFromContent(...). That can write wiki/tag edges for content that did not persist, causing graph/content drift.💡 Suggested fix
void (async () => { + let saveCompletedWithoutThrow = false; try { await saveAction(); + saveCompletedWithoutThrow = true; } catch (e) { console.error("Auto-save flush on unmount failed:", e); } - try { - await syncGraphFromContent(pending.content); - } catch { - // Ignore sync errors during unmount flush + if (saveCompletedWithoutThrow) { + try { + await syncGraphFromContent(pending.content); + } catch { + // Ignore sync errors during unmount flush + } } })();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/editor/PageEditor/useEditorAutoSave.ts` around lines 85 - 93, The unmount auto-save IIFE currently calls syncGraphFromContent(pending.content) even if saveAction() failed; change the flow in useEditorAutoSave so syncGraphFromContent is only invoked when saveAction resolves successfully—e.g., introduce a success flag or return early after the catch in the async IIFE surrounding saveAction(), and reference saveAction, syncGraphFromContent, and pending.content when updating the logic so graph sync is skipped if the save failed.
🧹 Nitpick comments (12)
src/hooks/useMermaidGenerator.test.ts (1)
142-143: Test intent and mock behavior are mismatched (synchronous throwvs async rejection).The test name says synchronous throw, but
mockRejectedValueOnceexercises an async rejection path. Either rename the test or change the mock to a real sync throw.Suggested alignment (sync throw path)
- it("generate catches synchronous throws from generateMermaidDiagram and sets error", async () => { - mockGenerateMermaidDiagram.mockRejectedValueOnce(new Error("network")); + it("generate catches synchronous throws from generateMermaidDiagram and sets error", async () => { + mockGenerateMermaidDiagram.mockImplementationOnce(() => { + throw new Error("network"); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useMermaidGenerator.test.ts` around lines 142 - 143, The test "generate catches synchronous throws from generateMermaidDiagram and sets error" currently uses mockGenerateMermaidDiagram.mockRejectedValueOnce which simulates an async rejection; change the mock to throw synchronously instead (e.g., use mockImplementationOnce to immediately throw new Error("network")) so the test actually exercises the synchronous-throw path of generateMermaidDiagram; update the mockGenerateMermaidDiagram call in the same test (the it(...) block) to use the synchronous-throw implementation, or alternatively rename the test to reflect an async rejection if you prefer that behavior.src/hooks/useGeneralSettings.test.ts (1)
77-246: Consider extracting a sharedrenderReady()helper to remove repeated setup boilerplate.
renderHook + waitFor(isLoading === false)is repeated across many tests; a tiny helper will improve readability and reduce copy/paste drift.♻️ Example refactor
+const renderReady = async () => { + const hook = renderHook(() => useGeneralSettings()); + await waitFor(() => expect(hook.result.current.isLoading).toBe(false)); + return hook; +}; it("updateTheme persists, updates state, and syncs next-themes", async () => { - const { result } = renderHook(() => useGeneralSettings()); - await waitFor(() => expect(result.current.isLoading).toBe(false)); + const { result } = await renderReady(); mockSaveGeneralSettings.mockClear(); mockSetTheme.mockClear(); ... }); it("updateEditorFontSize persists the new preset", async () => { - const { result } = renderHook(() => useGeneralSettings()); - await waitFor(() => expect(result.current.isLoading).toBe(false)); + const { result } = await renderReady(); ... });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useGeneralSettings.test.ts` around lines 77 - 246, Extract a small helper (e.g. renderReady) to replace repeated renderHook + waitFor boilerplate: it should call renderHook(() => useGeneralSettings()), await waitFor(() => expect(result.current.isLoading).toBe(false)), and return the hook result (and optionally accept a list of vi mock functions to clear after ready). Replace each test's renderHook + waitFor sequence with a single call to renderReady(), and remove duplicated mockClear calls where you pass mocks into the helper; reference useGeneralSettings, renderHook, and waitFor when locating the repeated code to change.server/api/src/__tests__/services/subscriptionService.test.ts (1)
41-104: Consider adding an explicit cache-reset strategy per test.Current isolation depends on unique
userIdvalues. A test-only cache clear hook (or module reset per test block) would make this suite less brittle as more cases are added.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/src/__tests__/services/subscriptionService.test.ts` around lines 41 - 104, Add an explicit per-test cache reset: introduce and export a test-only clearUserTierCache() in the module that implements getUserTier (so tests can call it), then call clearUserTierCache() in the test suite afterEach hook (in the describe("getUserTier") block) to guarantee isolation; alternatively, if you prefer not to add an export, call vi.resetModules() in afterEach and re-import getUserTier in each test to ensure the module-level cache is cleared between tests.src/lib/aiChatConversationTitle.test.ts (1)
5-31: Hardenchain(...)against empty input.
leafIdis read fromnodes[nodes.length - 1].id; a zero-arg call would fail with an opaque runtime error. A small guard keeps future test failures explicit.Proposed patch
function chain( ...nodes: Array< Partial<TreeChatMessage> & { id: string; role: TreeChatMessage["role"]; content: string } > ): { map: Record<string, TreeChatMessage>; leafId: string; } { + if (nodes.length === 0) { + throw new Error("chain() requires at least one node"); + } + const map: Record<string, TreeChatMessage> = {}; let parentId: string | null = null; let ts = 1; for (const n of nodes) { map[n.id] = { @@ }; parentId = n.id; } - return { map, leafId: nodes[nodes.length - 1].id }; + return { map, leafId: nodes[nodes.length - 1]!.id }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/aiChatConversationTitle.test.ts` around lines 5 - 31, The chain(...) helper should guard against being called with zero arguments: detect if nodes.length === 0 and throw a clear Error (e.g. "chain requires at least one node") instead of reading nodes[nodes.length - 1].id; update the leafId assignment in the chain function to only read nodes[nodes.length - 1].id after that guard so empty calls fail with an explicit message and not an opaque runtime error.src/lib/agentSlashCommands/insertSlashAgentMarkdown.test.ts (1)
20-31: Capture and assertrun()to lock command-chain execution semantics.Right now the spy setup validates
insertContentAtarguments, but not whether the chain actually executes. Exposingrunin the helper and asserting it would harden this suite.🔧 Suggested hardening
function makeChainSpy(): { chain: () => { focus: () => { insertContentAt: ReturnType<typeof vi.fn> }; }; insertContentAt: ReturnType<typeof vi.fn>; + run: ReturnType<typeof vi.fn>; } { - const insertContentAt = vi.fn(() => ({ run: vi.fn() })); + const run = vi.fn(); + const insertContentAt = vi.fn(() => ({ run })); const chain = vi.fn(() => ({ focus: vi.fn(() => ({ insertContentAt })), })); - return { chain, insertContentAt }; + return { chain, insertContentAt, run }; }Also applies to: 37-49
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/agentSlashCommands/insertSlashAgentMarkdown.test.ts` around lines 20 - 31, The test helper makeChainSpy currently returns insertContentAt and chain mocks but doesn't expose the run() spy, so tests can't assert that the chain was executed; update makeChainSpy to have insertContentAt return an object with a run mock (e.g., const run = vi.fn()) and expose that run on the helper return value (return { chain, insertContentAt, run }); then update tests that use makeChainSpy to assert run was called (e.g., expect(run).toHaveBeenCalled()) after invoking the code that should execute the chain; reference the makeChainSpy, chain, insertContentAt, and run symbols when making changes.src/lib/agentSlashCommands/agentSlashEditorText.test.ts (1)
51-53: Use an explicit\uFFFCescape (or named constant) instead of the invisible literal.
""is visually ambiguous and can be accidentally altered by editors. Using"\uFFFC"keeps intent explicit in reviews and diffs.💡 Suggested tweak
- expect(textBetween).toHaveBeenCalledWith(2, 7, "\n", ""); + expect(textBetween).toHaveBeenCalledWith(2, 7, "\n", "\uFFFC");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/agentSlashCommands/agentSlashEditorText.test.ts` around lines 51 - 53, Replace the invisible U+FFFC literal in the assertion with an explicit escape or constant: update the expect(...toHaveBeenCalledWith(2, 7, "\n", "") call in the agentSlashEditorText.test.ts test to use "\uFFFC" (or a named constant like OBJECT_REPLACEMENT = "\uFFFC") instead of the invisible character so the test assertion on textBetween/toHaveBeenCalledWith is explicit and safe to edit in diffs.src/lib/agentSlashCommands/executeAgentSlashCommand.test.ts (1)
96-105: PrefermockReset()overmockClear()here to prevent future implementation leakage between tests.This setup otherwise resets implementations for most mocks, so aligning these two improves suite isolation consistency.
♻️ Suggested setup tweak
beforeEach(() => { vi.mocked(runClaudeQueryToCompletion).mockReset(); - vi.mocked(buildAgentSlashPrompt).mockClear(); - vi.mocked(getAgentSlashClaudeOptions).mockClear(); + vi.mocked(buildAgentSlashPrompt).mockReset().mockReturnValue("PROMPT"); + vi.mocked(getAgentSlashClaudeOptions) + .mockReset() + .mockReturnValue({ maxTurns: 5, allowedTools: ["Read"] }); vi.mocked(getEditorSelectionText).mockReset().mockReturnValue(""); vi.mocked(insertSlashAgentMarkdownAt).mockReset(); vi.mocked(readSlashAgentInsertPosition).mockReset().mockReturnValue("cursor"); vi.mocked(clearLastSlashAgentSelection).mockReset(); vi.mocked(getLastSlashAgentSelection).mockReset().mockReturnValue(""); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/agentSlashCommands/executeAgentSlashCommand.test.ts` around lines 96 - 105, The test setup mixes mockReset() and mockClear(), which risks leftover mock implementations leaking between tests; change the two calls using mockClear()—specifically vi.mocked(buildAgentSlashPrompt).mockClear() and vi.mocked(getAgentSlashClaudeOptions).mockClear()—to use mockReset() instead so all mocks are fully reset (consistent with the other mocks like runClaudeQueryToCompletion, getEditorSelectionText, insertSlashAgentMarkdownAt, readSlashAgentInsertPosition, clearLastSlashAgentSelection, and getLastSlashAgentSelection) and prevent future implementation leakage.src/hooks/useWorkflowDraft.test.ts (1)
205-231: Test title and assertions are slightly out of sync.The test name says “sanitized filename”, but the assertions currently validate URL lifecycle/click behavior only. Either assert the download filename too, or rename the test to match current assertions.
✏️ Minimal rename option
- it("exportJson opens a download link with sanitized filename", () => { + it("exportJson opens a download link and revokes object URL", () => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useWorkflowDraft.test.ts` around lines 205 - 231, The test title says it verifies a “sanitized filename” but the assertions only check URL lifecycle and click behavior; update the test to either (A) add an assertion that the created anchor's download attribute matches the expected sanitized filename (targeting the exportJson flow in useWorkflowDraft and using the same spies createObjectURLSpy/clickSpy/revokeObjectURLSpy and setDraft to set the name), or (B) more simply rename the test to reflect what it actually asserts (e.g., change the it description to something like "exportJson opens a download link and revokes the URL") so the title and assertions match.server/api/src/__tests__/middleware/csrfOrigin.test.ts (1)
61-64: AddHEADto the safe-method bypass test matrix.Line 61 currently tests only
GETandOPTIONS, but this file’s contract states safe methods includeHEADas well. Please includeHEADto keep tests/spec in sync.Suggested patch
- it.each(["GET", "OPTIONS"] as const)("%s does not require Origin/Referer", async (method) => { + it.each(["GET", "HEAD", "OPTIONS"] as const)("%s does not require Origin/Referer", async (method) => {As per coding guidelines
**/*.{test,spec}.{ts,tsx,js,jsx}: Tests serve as a source of truth for specifications alongside implementation code TSDoc/JSDoc.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/src/__tests__/middleware/csrfOrigin.test.ts` around lines 61 - 64, The test for safe-method bypass in the it.each block named "%s does not require Origin/Referer" currently uses ["GET","OPTIONS"]; update that array to include "HEAD" so the matrix becomes ["GET","HEAD","OPTIONS"] (the test uses createApp().request("/api/state", { method }) and expects status 200); ensure only the method list is changed so the test name and assertions remain intact.src/lib/pageRepository/StorageAdapterPageRepository.test.ts (1)
257-279: Assert that the tag bucket is never written in the wiki-only isolation test.This test only proves
addLink("s", "t", "wiki")does not read the"tag"bucket. It would still pass if the implementation accidentally calledsaveLinks(..., "tag")and wiped that bucket without reading it first. Add a negative assertion on the third argument tosaveLinksas well.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/pageRepository/StorageAdapterPageRepository.test.ts` around lines 257 - 279, The test currently only checks that adapter.getLinks was never called for "tag" but misses verifying writes; update the test surrounding repo.addLink("s","t","wiki") to also assert adapter.saveLinks was never invoked with a linkType of "tag" (i.e., inspect/mock.calls of adapter.saveLinks and ensure no call has the third argument equal to "tag") so the test fails if implementation writes to the tag bucket even without reading it.server/api/src/lib/hocuspocusInvalidation.ts (1)
67-82: Usefinallyfor timeout cleanup
clearTimeout(timeoutId)is duplicated in both success and error paths. Moving it tofinallysimplifies maintenance and avoids accidental misses if logic expands.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/src/lib/hocuspocusInvalidation.ts` around lines 67 - 82, The clearTimeout(timeoutId) call is duplicated in the try and catch around the fetch in hocuspocusInvalidation.ts; remove both duplicates and place a single clearTimeout(timeoutId) inside a finally block after the catch so the timer is always cleared regardless of success or error, keeping the existing error handling (the catch that computes name and logs/handles the error) and preserving controller.signal usage for the fetch.src/components/editor/PageEditor/useEditorAutoSave.ts (1)
63-81: DeduplicatesyncGraphFromContentto prevent contract driftThe same sync routine is defined twice (unmount and normal save path). Extracting one shared callback will reduce divergence risk for future edits.
Also applies to: 122-133
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/editor/PageEditor/useEditorAutoSave.ts` around lines 63 - 81, The sync routine is duplicated; extract the logic into a single shared async function (e.g., syncGraphFromContent) that accepts content (and uses pageId, extractWikiLinksFromContent, syncWikiLinks, extractTagsFromContent, getUniqueTagNames, and syncTags) and then call that single function from both the unmount path and the normal save path so both code paths reuse the same implementation and avoid contract drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@AGENTS.md`:
- Around line 60-61: The English sentence in AGENTS.md merges inline code spans
with adjacent words (e.g.,
``server/api/drizzle/NNNN_*.sql``and``server/api/drizzle/meta/_journal.json``
and `/api/onboarding/status`and`/api/pages`); edit the line so each backticked
path and endpoint is separated by spaces or punctuation from surrounding text
(ensure a space before and after each inline code token like
`server/api/drizzle/NNNN_*.sql`, `server/api/drizzle/meta/_journal.json`,
`/api/onboarding/status`, and `/api/pages`) so the code spans render as distinct
tokens and the examples are copyable.
In `@server/api/src/lib/extractPlainTextFromYXml.ts`:
- Around line 73-77: The buildContentPreview function currently slices to
CONTENT_PREVIEW_MAX_LENGTH and then appends "..." which makes the returned
string longer than the declared max; update the logic in buildContentPreview to
account for the ellipsis length by slicing to CONTENT_PREVIEW_MAX_LENGTH - 3 (or
to 0 if that would be negative) before appending "..." so the final returned
preview never exceeds CONTENT_PREVIEW_MAX_LENGTH, and preserve the existing
trimming/whitespace collapse behavior.
In `@src/components/editor/extensions/slashSuggestionPlugin.test.ts`:
- Around line 150-155: Update the test comments in
src/components/editor/extensions/slashSuggestionPlugin.test.ts to include
Japanese translations alongside the existing English comments: for the comment
above the makeState("/foo", plugin) assertion (currently "1) Activate with
`/foo`."), the comment about expanding the selection ("2) Expand the selection
to a range; the plugin must turn off."), and the other English-only comment
referenced later (around the second assertion at line ~184). Keep the original
English text and add concise Japanese equivalents immediately after each English
sentence so both languages appear in the same comment block; no code logic
changes required—only edit the comment text surrounding makeState,
slashSuggestionPluginKey.getState, and onStateChange usages.
In `@src/components/editor/extensions/TagExtension.test.ts`:
- Around line 223-227: Replace the ad-hoc "as any" casts and eslint-disable
comments around the context passed to addAttributes.call() with an explicit
typed context object: declare a const (e.g., ctx) with a precise type matching
what addAttributes expects (for example an object type like { parent?: Node |
undefined; [key: string]: unknown } or the specific attribute/context interface
used by the TagExtension tests), spread the extension into that typed object
(instead of casting extension as any) and pass ctx to addAttributes.call(ctx,
...). Remove the eslint-disable lines and ensure you import/alias any necessary
types (e.g., Node or the extension context interface) so the test uses explicit
TypeScript types rather than "any".
In `@src/components/editor/extensions/WikiLinkExtension.test.ts`:
- Around line 111-115: Replace the unsafe `as any` casts used when invoking
addAttributes.call(...) by creating an explicit context type that combines the
extension's attribute shape plus the parent function signature, then pass a
properly typed object to addAttributes.call; specifically, define an interface
(e.g., ExtensionContext) that includes all properties from the `extension` under
test and a `parent: () => string | undefined` (or the actual parent return type
used by addAttributes), cast the constructed context to that interface instead
of `any`, and update the same pattern in TagExtension.test (lines ~222–225) so
both tests remove eslint-disable and satisfy TypeScript strict mode.
In `@src/components/editor/extensions/WikiLinkExtension.ts`:
- Around line 121-125: The parseHTML implementation currently returns
element.getAttribute("data-target-id") which yields an empty string for
data-target-id="" and breaks code expecting string | null; update parseHTML (in
WikiLinkExtension) to normalize empty values to null by checking the returned
value and returning null when it's strictly "" or not a string, so upstream
logic that expects null for "no target" receives null instead of an empty
string.
In `@src/components/editor/TiptapEditor/useWikiLinkStatusSync.test.tsx`:
- Around line 22-25: Update the test comment to reflect that the mock contract
now constructs pageTitleToId from the inScope set rather than returning an empty
Map; mention the intent remains that the test only asserts exists/referenced
changes (i.e., resolution behavior is exercised but full id resolution is
unnecessary) and point to the specific symbols used in the test (pageTitleToId
and inScope) so future readers know the map is derived from inScope for mocking
purposes.
In `@src/hooks/useImageUpload.test.ts`:
- Around line 13-18: Update the hoisted mock so the storage helper mocks match
and forward the real function signatures: change getStorageProvider and
isStorageConfiguredForUpload to accept the same parameters as the production
functions and forward them (e.g., getStorageProvider should accept (settings:
StorageSettings, context?) and return providerUploadImage;
isStorageConfiguredForUpload should accept (settings: StorageSettings) and
return a boolean). Keep getSettingsForUpload as-is, and maintain existing mocks
providerUploadImage, getToken, and convertToWebP but ensure the mocked
functions' parameter lists mirror the real signatures so arguments are passed
through exactly as in production.
In `@src/hooks/useMermaidGenerator.test.ts`:
- Around line 41-42: Update the English-only inline comments in
src/hooks/useMermaidGenerator.test.ts to include Japanese translations as
required by repo rules: for the comment "// Safety net to keep spies (e.g.
console.error if added in future tests) and any module-level setup from leaking
between tests on assertion failures." add a Japanese equivalent on the same line
or directly below it, and do the same for the other English-only block at lines
referenced (227-234); search for the exact English comment text to locate the
spots and append concise Japanese translations that convey the same meaning.
In `@src/hooks/useWorkflowDraft.test.ts`:
- Line 228: The inline comment "Flush the queued setTimeout(..., 0) that
triggers revokeObjectURL." is English-only; add a Japanese translation alongside
it so comments follow the repo JP/EN rule—for example, update the comment in
useWorkflowDraft.test.ts (the line containing that text) to include a Japanese
sentence conveying the same meaning immediately before or after the existing
English sentence.
In `@src/hooks/useWorkflowRunSession.test.ts`:
- Around line 286-287: Update the English-only inline comments in the test to be
bilingual (English + Japanese): wherever the test drives outcomes via
mockRunWorkflowExecution (e.g., the comment "First, drive a paused outcome so
the hook captures pausedState.") and the other similar comments in this file,
add a Japanese translation alongside the English text (or convert each to an
English/Japanese pair) to comply with the repository comment policy; ensure the
wording remains clear and matches the original intent for comments near
mockRunWorkflowExecution and the other mentioned test steps.
In `@src/lib/aiServiceServer.test.ts`:
- Around line 350-366: The test's ReadableStream never terminates after
pulledOnce flips, which can hang the test; update the stream's pull
implementation (the ReadableStream created in the "abortSignal が aborted なら
ABORTED エラーを onError へ送る" test that uses abortController and pulledOnce) to
close the stream after emitting the post-abort chunk by calling
controller.close() immediately after controller.enqueue(":\n") so the stream
ends cleanly.
In `@src/stores/aiChatStore.test.ts`:
- Around line 175-190: Update the inline comments in the test block surrounding
useAIChatStore.persist.rehydrate() and the subsequent assertions so they include
both Japanese and English explanations; specifically, make the Japanese-only
comments about volatile fields and the partialize behavior bilingual (e.g., next
to the comment starting with "ここでは契約として..." and the one beginning "Pin volatile
fields explicitly...") so they follow the project guideline; keep meaning
identical and place English translations inline or on the following line for the
comments that reference useAIChatStore.persist.rehydrate, partialize behavior,
and the assertion checks for state.isOpen, state.contextEnabled, and
state.selectedModel.
---
Outside diff comments:
In `@src/components/editor/PageEditor/useEditorAutoSave.ts`:
- Around line 85-93: The unmount auto-save IIFE currently calls
syncGraphFromContent(pending.content) even if saveAction() failed; change the
flow in useEditorAutoSave so syncGraphFromContent is only invoked when
saveAction resolves successfully—e.g., introduce a success flag or return early
after the catch in the async IIFE surrounding saveAction(), and reference
saveAction, syncGraphFromContent, and pending.content when updating the logic so
graph sync is skipped if the save failed.
In `@src/components/editor/TiptapEditor/useWikiLinkStatusSync.ts`:
- Around line 109-124: After awaiting checkExistence(titles, pageId) guard
against stale results by validating the run is still current before mutating the
editor: capture the current run identity (e.g., pageId/pageNoteId/content or the
computed pageScopeSignature/currentCount) into a local variable before the await
and then compare it to the latest values (or lastCheckedRef/current refs)
immediately after the await; if they differ, return early and do not call
collectWikiLinkUpdates or applyWikiLinkUpdates. Ensure the same check is
performed before calling applyWikiLinkUpdates(editor, updates) so outdated async
responses never mutate the editor or update lastCheckedRef.current.
---
Nitpick comments:
In `@server/api/src/__tests__/middleware/csrfOrigin.test.ts`:
- Around line 61-64: The test for safe-method bypass in the it.each block named
"%s does not require Origin/Referer" currently uses ["GET","OPTIONS"]; update
that array to include "HEAD" so the matrix becomes ["GET","HEAD","OPTIONS"] (the
test uses createApp().request("/api/state", { method }) and expects status 200);
ensure only the method list is changed so the test name and assertions remain
intact.
In `@server/api/src/__tests__/services/subscriptionService.test.ts`:
- Around line 41-104: Add an explicit per-test cache reset: introduce and export
a test-only clearUserTierCache() in the module that implements getUserTier (so
tests can call it), then call clearUserTierCache() in the test suite afterEach
hook (in the describe("getUserTier") block) to guarantee isolation;
alternatively, if you prefer not to add an export, call vi.resetModules() in
afterEach and re-import getUserTier in each test to ensure the module-level
cache is cleared between tests.
In `@server/api/src/lib/hocuspocusInvalidation.ts`:
- Around line 67-82: The clearTimeout(timeoutId) call is duplicated in the try
and catch around the fetch in hocuspocusInvalidation.ts; remove both duplicates
and place a single clearTimeout(timeoutId) inside a finally block after the
catch so the timer is always cleared regardless of success or error, keeping the
existing error handling (the catch that computes name and logs/handles the
error) and preserving controller.signal usage for the fetch.
In `@src/components/editor/PageEditor/useEditorAutoSave.ts`:
- Around line 63-81: The sync routine is duplicated; extract the logic into a
single shared async function (e.g., syncGraphFromContent) that accepts content
(and uses pageId, extractWikiLinksFromContent, syncWikiLinks,
extractTagsFromContent, getUniqueTagNames, and syncTags) and then call that
single function from both the unmount path and the normal save path so both code
paths reuse the same implementation and avoid contract drift.
In `@src/hooks/useGeneralSettings.test.ts`:
- Around line 77-246: Extract a small helper (e.g. renderReady) to replace
repeated renderHook + waitFor boilerplate: it should call renderHook(() =>
useGeneralSettings()), await waitFor(() =>
expect(result.current.isLoading).toBe(false)), and return the hook result (and
optionally accept a list of vi mock functions to clear after ready). Replace
each test's renderHook + waitFor sequence with a single call to renderReady(),
and remove duplicated mockClear calls where you pass mocks into the helper;
reference useGeneralSettings, renderHook, and waitFor when locating the repeated
code to change.
In `@src/hooks/useMermaidGenerator.test.ts`:
- Around line 142-143: The test "generate catches synchronous throws from
generateMermaidDiagram and sets error" currently uses
mockGenerateMermaidDiagram.mockRejectedValueOnce which simulates an async
rejection; change the mock to throw synchronously instead (e.g., use
mockImplementationOnce to immediately throw new Error("network")) so the test
actually exercises the synchronous-throw path of generateMermaidDiagram; update
the mockGenerateMermaidDiagram call in the same test (the it(...) block) to use
the synchronous-throw implementation, or alternatively rename the test to
reflect an async rejection if you prefer that behavior.
In `@src/hooks/useWorkflowDraft.test.ts`:
- Around line 205-231: The test title says it verifies a “sanitized filename”
but the assertions only check URL lifecycle and click behavior; update the test
to either (A) add an assertion that the created anchor's download attribute
matches the expected sanitized filename (targeting the exportJson flow in
useWorkflowDraft and using the same spies
createObjectURLSpy/clickSpy/revokeObjectURLSpy and setDraft to set the name), or
(B) more simply rename the test to reflect what it actually asserts (e.g.,
change the it description to something like "exportJson opens a download link
and revokes the URL") so the title and assertions match.
In `@src/lib/agentSlashCommands/agentSlashEditorText.test.ts`:
- Around line 51-53: Replace the invisible U+FFFC literal in the assertion with
an explicit escape or constant: update the expect(...toHaveBeenCalledWith(2, 7,
"\n", "") call in the agentSlashEditorText.test.ts test to use "\uFFFC" (or a
named constant like OBJECT_REPLACEMENT = "\uFFFC") instead of the invisible
character so the test assertion on textBetween/toHaveBeenCalledWith is explicit
and safe to edit in diffs.
In `@src/lib/agentSlashCommands/executeAgentSlashCommand.test.ts`:
- Around line 96-105: The test setup mixes mockReset() and mockClear(), which
risks leftover mock implementations leaking between tests; change the two calls
using mockClear()—specifically vi.mocked(buildAgentSlashPrompt).mockClear() and
vi.mocked(getAgentSlashClaudeOptions).mockClear()—to use mockReset() instead so
all mocks are fully reset (consistent with the other mocks like
runClaudeQueryToCompletion, getEditorSelectionText, insertSlashAgentMarkdownAt,
readSlashAgentInsertPosition, clearLastSlashAgentSelection, and
getLastSlashAgentSelection) and prevent future implementation leakage.
In `@src/lib/agentSlashCommands/insertSlashAgentMarkdown.test.ts`:
- Around line 20-31: The test helper makeChainSpy currently returns
insertContentAt and chain mocks but doesn't expose the run() spy, so tests can't
assert that the chain was executed; update makeChainSpy to have insertContentAt
return an object with a run mock (e.g., const run = vi.fn()) and expose that run
on the helper return value (return { chain, insertContentAt, run }); then update
tests that use makeChainSpy to assert run was called (e.g.,
expect(run).toHaveBeenCalled()) after invoking the code that should execute the
chain; reference the makeChainSpy, chain, insertContentAt, and run symbols when
making changes.
In `@src/lib/aiChatConversationTitle.test.ts`:
- Around line 5-31: The chain(...) helper should guard against being called with
zero arguments: detect if nodes.length === 0 and throw a clear Error (e.g.
"chain requires at least one node") instead of reading nodes[nodes.length -
1].id; update the leafId assignment in the chain function to only read
nodes[nodes.length - 1].id after that guard so empty calls fail with an explicit
message and not an opaque runtime error.
In `@src/lib/pageRepository/StorageAdapterPageRepository.test.ts`:
- Around line 257-279: The test currently only checks that adapter.getLinks was
never called for "tag" but misses verifying writes; update the test surrounding
repo.addLink("s","t","wiki") to also assert adapter.saveLinks was never invoked
with a linkType of "tag" (i.e., inspect/mock.calls of adapter.saveLinks and
ensure no call has the third argument equal to "tag") so the test fails if
implementation writes to the tag bucket even without reading it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: be9c1b39-1584-4fa6-a26e-c96a7635b499
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (108)
.coderabbit.yaml.github/workflows/ci.yml.github/workflows/claude-code-review.ymlAGENTS.mdCLAUDE.mdREADME.mddb/migrations/001_add_notes_tables.sqldb/migrations/002_add_page_snapshots.sqldb/migrations/003_add_invitation_tokens.sqldb/migrations/004_add_invitation_locale_tracking.sqldb/migrations/005_add_onboarding_and_page_kind.sqlpackage.jsonpackages/shared/package.jsonpackages/shared/src/index.tspackages/shared/src/tagCharacterClass.test.tspackages/shared/src/tagCharacterClass.tspackages/shared/tsconfig.jsonpackages/shared/vitest.config.tsscripts/check-drizzle-migrations.mjsscripts/setup-develop-branch.shserver/api/drizzle/0018_add_onboarding_and_page_kind.sqlserver/api/drizzle/meta/_journal.jsonserver/api/src/__tests__/lib/userDelete.test.tsserver/api/src/__tests__/middleware/adminAuth.test.tsserver/api/src/__tests__/middleware/csrfOrigin.test.tsserver/api/src/__tests__/middleware/errorHandler.test.tsserver/api/src/__tests__/routes/pages.test.tsserver/api/src/__tests__/routes/syncPages.test.tsserver/api/src/__tests__/services/magicLinkService.test.tsserver/api/src/__tests__/services/subscriptionService.test.tsserver/api/src/__tests__/services/titleRenamePropagationService.test.tsserver/api/src/__tests__/services/ydocRenameRewrite.test.tsserver/api/src/lib/extractPlainTextFromYXml.tsserver/api/src/lib/hocuspocusInvalidation.tsserver/api/src/routes/pageSnapshots.tsserver/api/src/routes/pages.tsserver/api/src/routes/syncPages.tsserver/api/src/schema/index.tsserver/api/src/schema/pages.tsserver/api/src/services/titleRenamePropagationService.tsserver/api/src/services/ydocRenameRewrite.tssrc/components/editor/PageEditor/useEditorAutoSave.test.tssrc/components/editor/PageEditor/useEditorAutoSave.tssrc/components/editor/PageEditor/usePageEditorAutoSaveWithMutation.tssrc/components/editor/TiptapEditor/EditorBubbleMenu.test.tsxsrc/components/editor/TiptapEditor/useBubbleMenuWikiLink.test.tssrc/components/editor/TiptapEditor/useBubbleMenuWikiLink.tssrc/components/editor/TiptapEditor/useEditorBubbleMenu.test.tssrc/components/editor/TiptapEditor/useEditorLifecycle.tssrc/components/editor/TiptapEditor/useTagStatusSync.test.tsxsrc/components/editor/TiptapEditor/useTagStatusSync.tssrc/components/editor/TiptapEditor/useWikiLinkStatusSync.test.tsxsrc/components/editor/TiptapEditor/useWikiLinkStatusSync.tssrc/components/editor/extensions/TagExtension.test.tssrc/components/editor/extensions/TagExtension.tssrc/components/editor/extensions/WikiLinkExtension.test.tssrc/components/editor/extensions/WikiLinkExtension.tssrc/components/editor/extensions/slashSuggestionPlugin.test.tssrc/hooks/useAIChatPanelContentLogic.test.tssrc/hooks/useAISettings.test.tssrc/hooks/useGeneralSettings.test.tssrc/hooks/useGlobalSearch.test.tssrc/hooks/useGlobalSearch.tssrc/hooks/useImageUpload.test.tssrc/hooks/useMermaidGenerator.test.tssrc/hooks/usePageQueries.tssrc/hooks/useSyncWikiLinks.test.tssrc/hooks/useWorkflowDraft.test.tssrc/hooks/useWorkflowRunSession.test.tssrc/lib/agentSlashCommands/agentSlashEditorText.test.tssrc/lib/agentSlashCommands/buildAgentSlashPrompt.test.tssrc/lib/agentSlashCommands/buildAgentSlashPromptParts.test.tssrc/lib/agentSlashCommands/executeAgentSlashCommand.test.tssrc/lib/agentSlashCommands/hook.test.tssrc/lib/agentSlashCommands/insertPosition.test.tssrc/lib/agentSlashCommands/insertSlashAgentMarkdown.test.tssrc/lib/agentSlashCommands/slashAgentSelectionCache.test.tssrc/lib/aiChatConversationTitle.test.tssrc/lib/aiClient.test.tssrc/lib/aiService.test.tssrc/lib/aiServiceDirectProviders.test.tssrc/lib/aiServiceModels.test.tssrc/lib/aiServiceServer.test.tssrc/lib/api/types.tssrc/lib/encryption.test.tssrc/lib/pageRepository.test.tssrc/lib/pageRepository.tssrc/lib/pageRepository/StorageAdapterPageRepository.test.tssrc/lib/pageRepository/StorageAdapterPageRepository.tssrc/lib/parseStdioArgs.test.tssrc/lib/storage/index.test.tssrc/lib/storageAdapter/IndexedDBStorageAdapter.tssrc/lib/storageAdapter/StorageAdapter.tssrc/lib/storageAdapter/types.tssrc/lib/sync/syncWithApi.test.tssrc/lib/sync/syncWithApi.tssrc/lib/syncWikiLinks.tssrc/lib/tagCharacterClassSync.test.tssrc/lib/tagUtils.test.tssrc/lib/tagUtils.tssrc/lib/wikiLinkUtils.test.tssrc/lib/wikiLinkUtils.tssrc/pages/NoteSettings/index.tsxsrc/pages/SearchResults.tsxsrc/stores/aiChatStore.test.tssrc/stores/pageStore.test.tssrc/stores/pageStore.tssrc/types/page.ts
💤 Files with no reviewable changes (6)
- db/migrations/004_add_invitation_locale_tracking.sql
- scripts/setup-develop-branch.sh
- db/migrations/003_add_invitation_tokens.sql
- db/migrations/001_add_notes_tables.sql
- db/migrations/005_add_onboarding_and_page_kind.sql
- db/migrations/002_add_page_snapshots.sql
* test(P6): cover server/api routes, services, and remaining lint rules (#747) Adds unit tests for AI / billing / lint / thumbnail routes and the underlying services that were previously uncovered, following the existing app.request() + createMockDb pattern: - routes: health, onboarding, activity, lint, checkout, subscriptionManage, ai/chat (incl. SSE streaming), thumbnail/{commit, imageGenerate, imageSearch} - services: gemini, imageSearch, usageService, aiProviders (OpenAI / Anthropic / Google call + stream wrappers) - lintEngine rules: brokenLink, orphan, ghostMany — completes the 6/6 rule coverage alongside the existing titleSimilar / conflict / stale tests External APIs are stubbed via vi.mock or vi.spyOn(globalThis, "fetch"), SSE streams are exercised end-to-end through a ReadableStream, and DB chains are inspected through createMockDb to verify ownership scoping. * test(P6): address PR #756 review feedback - env isolation: add afterEach restoration of process.env in chat / subscriptionManage / thumbnail{commit,imageGenerate,imageSearch} tests so worker-shared state cannot leak between files. - chat tests: also call vi.restoreAllMocks() in afterEach and drop the per-test consoleSpy.mockRestore() calls — failing assertions can no longer leak the console.error spy. - onboarding tests: align mockInsertWelcomePage payloads with the real WelcomePageCreationResult contract (locale: "ja" | "en", null result means "page already exists"); rename the corresponding test and use a realistic existing-welcome row to exercise the COALESCE path. Add expect(mockInsertWelcomePage).not.toHaveBeenCalled() to every 400 validation path so malformed inputs are guaranteed to bypass the side effect. - subscriptionManage header comment: align with the actually-mounted /api/subscription path. - activity test: drop the assertion that codified the route forwarding an unclamped limit to listActivityForOwner — the response-side clamp is the contract under test, the downstream value is service business. - lintEngine rule tests (brokenLink / orphan / ghostMany): floor-check that the SELECT chain carries .where(...) (and .having for ghostMany) so an accidentally-removed owner / threshold filter is detected. * test(P6): apply round-2 PR #756 review feedback - subscriptionManage / thumbnail{commit,imageSearch}: add vi.restoreAllMocks() to afterEach and remove the inline consoleSpy.mockRestore() calls so a failing assertion can no longer leak a console.error spy into the next test (matches the chat tests). - subscriptionManage: split the change-plan 400 case into two tests (invalid value vs missing field) and assert currentPeriodStart / currentPeriodEnd in the paid /details response. - thumbnail/imageGenerate: assert mockGenerate is not invoked on the whitespace-only prompt path so the validation contract is locked. - thumbnail/imageSearch: bilingual JA+EN comment on the duplicate-URL fixture, per repo guidelines. - onboarding /status: assert retryWelcomePageIfNeeded receives the authenticated userId as the second argument. --------- Co-authored-by: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (6)
server/api/src/__tests__/services/gemini.test.ts (1)
33-37: Add no-fetch assertion for empty API key guardLine 33 verifies rejection, but not that the network layer is skipped. Adding that assertion hardens the guard-path contract.
Suggested patch
it("throws when apiKey is empty", async () => { await expect(generateImageWithGemini("a prompt", "")).rejects.toThrow( /prompt and api key are required/i, ); + expect(fetchSpy).not.toHaveBeenCalled(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/src/__tests__/services/gemini.test.ts` around lines 33 - 37, Update the test for generateImageWithGemini to not only expect rejection when apiKey is empty but also assert that the network layer was not invoked; after calling await expect(generateImageWithGemini("a prompt", "")).rejects.toThrow(...), add an assertion that the mocked fetch (or the fetch stub used in this test suite) was not called to ensure the implementation short-circuits before any network request. Reference generateImageWithGemini and the test's mocked fetch/HTTP client to locate where to add the "not called" assertion.server/api/src/__tests__/services/usageService.test.ts (1)
192-210: Assert the second chain performsonConflictDoUpdateLines 194-197 verify two inserts start, but don’t confirm the upsert behavior itself. Add an explicit
onConflictDoUpdateassertion to lock in the monthly-usage update contract.Suggested patch
expect(chains.length).toBe(2); expect(chains[0]?.startMethod).toBe("insert"); expect(chains[1]?.startMethod).toBe("insert"); + const upsertCall = chains[1]?.ops.find((op) => op.method === "onConflictDoUpdate"); + expect(upsertCall).toBeDefined(); // 1 件目: values() に渡された生データ。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/src/__tests__/services/usageService.test.ts` around lines 192 - 210, The test currently asserts two top-level chains but doesn't verify the upsert operation; update the test to assert that the second chain (chains[1]) contains an op with method "onConflictDoUpdate" and that its args reflect the expected conflict target and update payload (e.g., conflict keys like userId/modelId/month and the monthly usage increment/update fields). Locate the test block that inspects chains (uses chains[0] and chains[1]) and add a find/assertion for chains[1]?.ops.find(op => op.method === "onConflictDoUpdate") and validate its args to lock the monthly-usage update contract.server/api/src/__tests__/routes/subscriptionManage.test.ts (1)
78-85: TightengetEnvmock to prevent false-positive key usageLine 83 returns
"polar-token"for any env key, which can mask wrong-key regressions. Prefer a key-aware mock that throws on unexpected keys.Suggested patch
beforeEach(() => { mockSubscriptionsUpdate.mockReset(); mockGetUserTier.mockReset(); mockGetSubscription.mockReset(); mockCheckUsage.mockReset(); - mockGetEnv.mockReset().mockReturnValue("polar-token"); + mockGetEnv.mockReset().mockImplementation((key: string) => { + if (key === "POLAR_ACCESS_TOKEN") return "polar-token"; + throw new Error(`Unexpected env key in test: ${key}`); + }); process.env = { ...ORIGINAL_ENV }; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/src/__tests__/routes/subscriptionManage.test.ts` around lines 78 - 85, The test's getEnv mock (mockGetEnv) currently returns "polar-token" for any key which can hide wrong-key regressions; update the beforeEach to use a key-aware mockImplementation on mockGetEnv that returns "polar-token" only for the expected env key (e.g., "POLAR_TOKEN" or the exact key your code calls) and throws an error for any other key, so unexpected env lookups fail the test; locate mockGetEnv in the test setup and replace mockReturnValue("polar-token") with a mockImplementation that checks the incoming key and returns the token only for the correct key and throws otherwise.server/api/src/__tests__/services/aiProviders.test.ts (1)
362-366: Assert no network side-effect on sync unknown-provider throwOn Line 362, adding a fetch non-invocation assertion makes the dispatcher failure contract stricter and clearer.
Suggested patch
it("streamProvider throws synchronously for unknown providers", () => { // streamProvider は同期的に switch で投げるので、generator を返す前に throw する。 // streamProvider throws synchronously from the switch before returning a generator. expect(() => streamProvider("zzz" as never, "k", "m", messages)).toThrow(/unknown provider/i); + expect(fetchSpy).not.toHaveBeenCalled(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/src/__tests__/services/aiProviders.test.ts` around lines 362 - 366, The test for streamProvider's synchronous throw should also assert that no network calls were attempted; in the "it('streamProvider throws synchronously for unknown providers')" test, after invoking expect(() => streamProvider(...)).toThrow(...), add an assertion that the global/network fetch mock (or the fetch spy used in the test suite) was not called to ensure no side-effecting network activity occurred when streamProvider throws synchronously for an unknown provider.server/api/src/__tests__/routes/activity.test.ts (1)
142-159: Align invalid-date tests with other validator short-circuit assertionsLines 142 and 150 assert 400, but unlike invalid kind/actor they don’t verify the service isn’t called. Adding that check keeps validation-path contracts consistent.
Suggested patch
it("rejects invalid 'from' date with 400", async () => { const { app } = createTestApp([]); const res = await app.request("/api/activity?from=not-a-date", { headers: authHeaders() }); expect(res.status).toBe(400); + expect(mockListActivity).not.toHaveBeenCalled(); }); it("rejects inverted from > to range with 400", async () => { const { app } = createTestApp([]); const res = await app.request( "/api/activity?from=2026-04-10T00:00:00Z&to=2026-04-01T00:00:00Z", { headers: authHeaders() }, ); expect(res.status).toBe(400); + expect(mockListActivity).not.toHaveBeenCalled(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/src/__tests__/routes/activity.test.ts` around lines 142 - 159, The two tests ("rejects invalid 'from' date with 400" and "rejects inverted from > to range with 400") assert status 400 but don't assert the activity service wasn't invoked; update each test to pass in or obtain the mocked activity-fetching function created by createTestApp (the same mock used elsewhere in this file) and add an expectation that this mock was not called (e.g., expect(activityMock).not.toHaveBeenCalled()) after the app.request call, keeping authHeaders() and the existing status assertions intact.server/api/src/__tests__/routes/thumbnail/commit.test.ts (1)
88-98: Assert service non-invocation for whitespacesourceUrltooLine 88 only checks the 400 status. Add a non-call assertion to lock in the validation short-circuit contract.
Suggested patch
it("returns 400 when sourceUrl is whitespace only", async () => { const app = createTestApp(); const res = await app.request("/api/thumbnail/commit", { method: "POST", headers: authHeaders(), body: JSON.stringify({ sourceUrl: " " }), }); expect(res.status).toBe(400); + expect(mockCommitImage).not.toHaveBeenCalled(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/src/__tests__/routes/thumbnail/commit.test.ts` around lines 88 - 98, The test currently only asserts a 400 response for whitespace-only sourceUrl but must also assert that the thumbnail commit handler/service was not invoked; update the test in commit.test.ts to create or access the mocked thumbnail commit function used by createTestApp (the service that handles "/api/thumbnail/commit") and add an assertion like expect(mockCommitFn).not.toHaveBeenCalled() after the request to lock in the short-circuit validation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@server/api/src/__tests__/routes/activity.test.ts`:
- Around line 142-159: The two tests ("rejects invalid 'from' date with 400" and
"rejects inverted from > to range with 400") assert status 400 but don't assert
the activity service wasn't invoked; update each test to pass in or obtain the
mocked activity-fetching function created by createTestApp (the same mock used
elsewhere in this file) and add an expectation that this mock was not called
(e.g., expect(activityMock).not.toHaveBeenCalled()) after the app.request call,
keeping authHeaders() and the existing status assertions intact.
In `@server/api/src/__tests__/routes/subscriptionManage.test.ts`:
- Around line 78-85: The test's getEnv mock (mockGetEnv) currently returns
"polar-token" for any key which can hide wrong-key regressions; update the
beforeEach to use a key-aware mockImplementation on mockGetEnv that returns
"polar-token" only for the expected env key (e.g., "POLAR_TOKEN" or the exact
key your code calls) and throws an error for any other key, so unexpected env
lookups fail the test; locate mockGetEnv in the test setup and replace
mockReturnValue("polar-token") with a mockImplementation that checks the
incoming key and returns the token only for the correct key and throws
otherwise.
In `@server/api/src/__tests__/routes/thumbnail/commit.test.ts`:
- Around line 88-98: The test currently only asserts a 400 response for
whitespace-only sourceUrl but must also assert that the thumbnail commit
handler/service was not invoked; update the test in commit.test.ts to create or
access the mocked thumbnail commit function used by createTestApp (the service
that handles "/api/thumbnail/commit") and add an assertion like
expect(mockCommitFn).not.toHaveBeenCalled() after the request to lock in the
short-circuit validation behavior.
In `@server/api/src/__tests__/services/aiProviders.test.ts`:
- Around line 362-366: The test for streamProvider's synchronous throw should
also assert that no network calls were attempted; in the "it('streamProvider
throws synchronously for unknown providers')" test, after invoking expect(() =>
streamProvider(...)).toThrow(...), add an assertion that the global/network
fetch mock (or the fetch spy used in the test suite) was not called to ensure no
side-effecting network activity occurred when streamProvider throws
synchronously for an unknown provider.
In `@server/api/src/__tests__/services/gemini.test.ts`:
- Around line 33-37: Update the test for generateImageWithGemini to not only
expect rejection when apiKey is empty but also assert that the network layer was
not invoked; after calling await expect(generateImageWithGemini("a prompt",
"")).rejects.toThrow(...), add an assertion that the mocked fetch (or the fetch
stub used in this test suite) was not called to ensure the implementation
short-circuits before any network request. Reference generateImageWithGemini and
the test's mocked fetch/HTTP client to locate where to add the "not called"
assertion.
In `@server/api/src/__tests__/services/usageService.test.ts`:
- Around line 192-210: The test currently asserts two top-level chains but
doesn't verify the upsert operation; update the test to assert that the second
chain (chains[1]) contains an op with method "onConflictDoUpdate" and that its
args reflect the expected conflict target and update payload (e.g., conflict
keys like userId/modelId/month and the monthly usage increment/update fields).
Locate the test block that inspects chains (uses chains[0] and chains[1]) and
add a find/assertion for chains[1]?.ops.find(op => op.method ===
"onConflictDoUpdate") and validate its args to lock the monthly-usage update
contract.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 32666c37-be32-4fb3-a85a-6c10526d8921
📒 Files selected for processing (17)
server/api/src/__tests__/routes/activity.test.tsserver/api/src/__tests__/routes/ai/chat.test.tsserver/api/src/__tests__/routes/checkout.test.tsserver/api/src/__tests__/routes/health.test.tsserver/api/src/__tests__/routes/lint.test.tsserver/api/src/__tests__/routes/onboarding.test.tsserver/api/src/__tests__/routes/subscriptionManage.test.tsserver/api/src/__tests__/routes/thumbnail/commit.test.tsserver/api/src/__tests__/routes/thumbnail/imageGenerate.test.tsserver/api/src/__tests__/routes/thumbnail/imageSearch.test.tsserver/api/src/__tests__/services/aiProviders.test.tsserver/api/src/__tests__/services/gemini.test.tsserver/api/src/__tests__/services/imageSearch.test.tsserver/api/src/__tests__/services/usageService.test.tsserver/api/src/services/lintEngine/rules/brokenLink.test.tsserver/api/src/services/lintEngine/rules/ghostMany.test.tsserver/api/src/services/lintEngine/rules/orphan.test.ts
…nd status handlers (#758) * test(P7): add tests for claude-sidecar handlers and mcp tool registry Closes the priority test gaps in packages/claude-sidecar (4 handlers) and server/mcp/tools/index.ts identified in #748: - packages/claude-sidecar/src/handlers/status.test.ts (8 tests): Set-based active query tracker, snapshot isolation, insertion order. - packages/claude-sidecar/src/handlers/installation.test.ts (9 tests): platform argv selection plus stubbed Bun.spawn for exit-0/non-zero/throw scenarios. - packages/claude-sidecar/src/handlers/query.test.ts (36 tests): table-driven tests for the seven stream-event helpers, plus a runQuery integration suite with the SDK query() mocked, covering tool boundaries, mcp-status, abort, result success/error, and the no-result fallback. - packages/claude-sidecar/src/handlers/models.test.ts (5 tests): SDK query() wrapper semantics including cleanup-on-throw via finally. - server/mcp/src/__tests__/tools/index.test.ts (6 tests): registerAllTools registers exactly the names in ALL_TOOL_NAMES (no extras, no duplicates, zedi_ prefix invariant). Refactored to expose internals previously kept module-private: - installation.ts: claudeVersionArgv now takes an optional platform override. - query.ts: extractTextDelta / extractToolUseStart / isContentBlockStop / isToolProgressMessage / extractAssistantText / isResultMessage / isSystemInitMessage / emitResultOrError are exported with @internal. bun run test passes in both packages (61/61 and 63/63), tsc --noEmit clean, prettier --check clean. Refs #748 (parent epic #741). https://claude.ai/code/session_01QqA7osjj26a4ZRNMiJB1rW * docs(mcp/test): clarify why tools/index test duplicates ALL_TOOL_NAMES Replies to the gemini-code-assist review on PR #758: the hardcoded `required` list looks like dead duplication of `ALL_TOOL_NAMES`, but it's deliberately frozen so silent removals (where both the registry *and* `ALL_TOOL_NAMES` are dropped together) still fail CI. Document the intent inline so future maintainers don't trim it by accident. No behavior change. https://claude.ai/code/session_01QqA7osjj26a4ZRNMiJB1rW --------- Co-authored-by: Claude <noreply@anthropic.com>
Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
src/hooks/useWorkflowDraft.test.ts (1)
205-233: Align the “sanitized filename” claim with an actual filename assertion.The test name says sanitized filename, but it currently never inspects
anchor.download. Add a direct assertion (or rename the test) so this behavior is truly covered.Proposed update
it("exportJson opens a download link with sanitized filename", () => { @@ - const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}); + let downloadName = ""; + const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function ( + this: HTMLAnchorElement, + ) { + downloadName = this.download; + }); @@ expect(createObjectURLSpy).toHaveBeenCalledTimes(1); expect(clickSpy).toHaveBeenCalledTimes(1); + expect(downloadName).toMatch(/\.json$/); + expect(downloadName).not.toContain(" "); @@ });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useWorkflowDraft.test.ts` around lines 205 - 233, The test title promises to verify a "sanitized filename" but never inspects the created anchor's download attribute; update the test that calls useWorkflowDraft().exportJson to locate the created anchor (e.g., document.querySelector('a[download]') or the created anchor instance) and add an assertion that its download equals the expected sanitized filename derived from the draft name set via result.current.setDraft (or compute expected using the same sanitize logic used by exportJson); keep the existing URL.createObjectURL/click/revoke assertions and run vi.runAllTimers() as before to flush revokeObjectURL.server/hocuspocus/src/extractPlainTextFromYXml.test.ts (1)
3-3: Prefer usingCONTENT_PREVIEW_MAX_LENGTHin the assertion.Using the exported constant instead of literal
120keeps the test aligned automatically if the limit changes.♻️ Suggested diff
-import { buildContentPreview, extractTextFromYXml } from "./extractPlainTextFromYXml.js"; +import { + buildContentPreview, + CONTENT_PREVIEW_MAX_LENGTH, + extractTextFromYXml, +} from "./extractPlainTextFromYXml.js"; ... - expect(prev.length).toBeLessThanOrEqual(120); + expect(prev.length).toBeLessThanOrEqual(CONTENT_PREVIEW_MAX_LENGTH);Also applies to: 143-143
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/hocuspocus/src/extractPlainTextFromYXml.test.ts` at line 3, Replace hard-coded literal 120 in the test assertions with the exported constant CONTENT_PREVIEW_MAX_LENGTH so the test stays correct when the limit changes; update the import line to include CONTENT_PREVIEW_MAX_LENGTH (alongside buildContentPreview and extractTextFromYXml) and change the two assertions that compare against 120 to use CONTENT_PREVIEW_MAX_LENGTH instead (references: buildContentPreview, extractTextFromYXml, CONTENT_PREVIEW_MAX_LENGTH).src/components/editor/extensions/slashSuggestionPlugin.test.ts (2)
82-93: Strengthen callback assertions for transition tests
toHaveBeenCalledWithcan still pass when extra unexpected notifications happen. For state-transition tests, also assert call count and/or last call payload.Suggested pattern
- expect(onStateChange).toHaveBeenCalledWith(pluginState); + expect(onStateChange).toHaveBeenCalledTimes(1); + expect(onStateChange).toHaveBeenLastCalledWith(pluginState);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/editor/extensions/slashSuggestionPlugin.test.ts` around lines 82 - 93, The test currently verifies the plugin callback with expect(onStateChange).toHaveBeenCalledWith(pluginState) which can hide extra notifications; update the assertions around the onStateChange mock (created as onStateChange and used when calling getPlugin and reading pluginState via slashSuggestionPluginKey.getState) to also assert call count and/or last call payload (e.g., replace the single toHaveBeenCalledWith with expect(onStateChange).toHaveBeenCalledTimes(1) and expect(onStateChange).toHaveBeenLastCalledWith(pluginState) so the transition test ensures exactly one notification and the expected payload).
40-48: Guard plugin extraction with an explicit empty-array check
return plugins[0]assumes plugin creation always succeeds. A small guard here gives much clearer failures if extension wiring changes later.Suggested patch
function getPlugin(onStateChange?: (s: SlashSuggestionState) => void): Plugin { const extension = SlashSuggestionPlugin.configure({ onStateChange }); const addPlugins = extension.config.addProseMirrorPlugins; if (!addPlugins) throw new Error("addProseMirrorPlugins missing"); // Tiptap stores extension options on `this.options` inside the method. // 拡張内部の `this.options` 経由で onStateChange を渡す必要がある。 const plugins = addPlugins.call({ options: { onStateChange } } as never); - return plugins[0]; + if (plugins.length === 0) { + throw new Error("SlashSuggestionPlugin produced no ProseMirror plugins"); + } + return plugins[0]; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/editor/extensions/slashSuggestionPlugin.test.ts` around lines 40 - 48, The test helper getPlugin assumes addProseMirrorPlugins returns a non-empty array and uses plugins[0] directly; add an explicit check after calling addProseMirrorPlugins (from SlashSuggestionPlugin.configure) to verify plugins is an array with length > 0 and throw a descriptive Error if empty or not an array (e.g., "addProseMirrorPlugins returned no plugins for SlashSuggestionPlugin"), so failures surface clearly if the extension wiring changes.src/hooks/useWorkflowRunSession.test.ts (1)
90-93: Reduce repeated mock setup with one sharedbeforeEach.The same setup block is repeated across four
describeblocks. Consider promoting it to a file-levelbeforeEachto reduce drift and maintenance overhead.♻️ Proposed refactor
+beforeEach(() => { + vi.clearAllMocks(); + mockIsTauriDesktop.mockReturnValue(true); +}); + describe("useWorkflowRunSession - validation guards", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockIsTauriDesktop.mockReturnValue(true); - }); + // per-suite overrides only if needed ... }); describe("useWorkflowRunSession - run outcomes", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockIsTauriDesktop.mockReturnValue(true); - }); + // per-suite overrides only if needed ... });Also applies to: 189-192, 280-283, 370-373
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useWorkflowRunSession.test.ts` around lines 90 - 93, The repeated per-describe beforeEach blocks currently calling vi.clearAllMocks() and setting mockIsTauriDesktop.mockReturnValue(true) should be consolidated into a single file-level beforeEach at the top of the test file so every describe inherits the same setup; add one top-level beforeEach that calls vi.clearAllMocks() and mockIsTauriDesktop.mockReturnValue(true), and remove the duplicate beforeEach blocks inside the individual describe blocks to avoid drift and maintenance overhead (look for the existing vi.clearAllMocks and mockIsTauriDesktop.mockReturnValue usages to locate the duplicated setup).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/claude-sidecar/src/handlers/query.test.ts`:
- Around line 591-615: Add a regression that simulates a tool-use-start before
abort and assert that runQuery finalizes activeToolName by emitting a
corresponding tool-use-complete (or equivalent finalization) even when the abort
signal fires or an exception occurs: update the existing test that aborts before
results to instead start a tool (emit a tool-use-start through the mocked query
iterable), trigger ac.abort(), then call runQuery and expect both the aborted
error and a tool-use-complete event (or that activeToolName is cleared) in the
parsed output; also change runQuery's abort/exception handling paths to check
for an existing activeToolName and emit or finalize tool-use-complete (and clear
activeToolName) before returning so consumers won’t be left in a stuck tool
state.
In `@server/mcp/src/__tests__/tools/index.test.ts`:
- Around line 19-20: The JSDoc helper comments in the test file use a malformed
terminator sequence ("... /") which can be misread; update the block comments
for the ZediClient helper (the /** Build a fully-mocked ZediClient... */ comment
and the analogous comment later in the file) to use standard multiline JSDoc
termination ("*/") so each block is a proper /** ... */ JSDoc comment,
preserving the existing English and Japanese text and spacing.
In `@src/hooks/useMermaidGenerator.test.ts`:
- Around line 143-145: The test "generate catches synchronous throws from
generateMermaidDiagram and sets error" is using mockRejectedValueOnce (async
rejection) but should simulate a synchronous throw; change the mock for
mockGenerateMermaidDiagram to a synchronous throw (e.g.
mockImplementationOnce(() => { throw new Error("network") })) so the hook's
synchronous-throw path is exercised when the code calls generateMermaidDiagram;
keep the rest of the test assertions the same to verify error state is set.
- Around line 49-51: The tests currently call vi.clearAllMocks() in the
beforeEach in useMermaidGenerator.test but that doesn't reset mock
implementations; change the setup to reset implementations as well by replacing
or augmenting the beforeEach to call vi.resetAllMocks() (or vi.clearAllMocks();
vi.resetAllMocks()) so mocks created/configured for functions used by
useMermaidGenerator tests have their mockImplementation/mockResolvedValue
cleared between tests.
---
Nitpick comments:
In `@server/hocuspocus/src/extractPlainTextFromYXml.test.ts`:
- Line 3: Replace hard-coded literal 120 in the test assertions with the
exported constant CONTENT_PREVIEW_MAX_LENGTH so the test stays correct when the
limit changes; update the import line to include CONTENT_PREVIEW_MAX_LENGTH
(alongside buildContentPreview and extractTextFromYXml) and change the two
assertions that compare against 120 to use CONTENT_PREVIEW_MAX_LENGTH instead
(references: buildContentPreview, extractTextFromYXml,
CONTENT_PREVIEW_MAX_LENGTH).
In `@src/components/editor/extensions/slashSuggestionPlugin.test.ts`:
- Around line 82-93: The test currently verifies the plugin callback with
expect(onStateChange).toHaveBeenCalledWith(pluginState) which can hide extra
notifications; update the assertions around the onStateChange mock (created as
onStateChange and used when calling getPlugin and reading pluginState via
slashSuggestionPluginKey.getState) to also assert call count and/or last call
payload (e.g., replace the single toHaveBeenCalledWith with
expect(onStateChange).toHaveBeenCalledTimes(1) and
expect(onStateChange).toHaveBeenLastCalledWith(pluginState) so the transition
test ensures exactly one notification and the expected payload).
- Around line 40-48: The test helper getPlugin assumes addProseMirrorPlugins
returns a non-empty array and uses plugins[0] directly; add an explicit check
after calling addProseMirrorPlugins (from SlashSuggestionPlugin.configure) to
verify plugins is an array with length > 0 and throw a descriptive Error if
empty or not an array (e.g., "addProseMirrorPlugins returned no plugins for
SlashSuggestionPlugin"), so failures surface clearly if the extension wiring
changes.
In `@src/hooks/useWorkflowDraft.test.ts`:
- Around line 205-233: The test title promises to verify a "sanitized filename"
but never inspects the created anchor's download attribute; update the test that
calls useWorkflowDraft().exportJson to locate the created anchor (e.g.,
document.querySelector('a[download]') or the created anchor instance) and add an
assertion that its download equals the expected sanitized filename derived from
the draft name set via result.current.setDraft (or compute expected using the
same sanitize logic used by exportJson); keep the existing
URL.createObjectURL/click/revoke assertions and run vi.runAllTimers() as before
to flush revokeObjectURL.
In `@src/hooks/useWorkflowRunSession.test.ts`:
- Around line 90-93: The repeated per-describe beforeEach blocks currently
calling vi.clearAllMocks() and setting mockIsTauriDesktop.mockReturnValue(true)
should be consolidated into a single file-level beforeEach at the top of the
test file so every describe inherits the same setup; add one top-level
beforeEach that calls vi.clearAllMocks() and
mockIsTauriDesktop.mockReturnValue(true), and remove the duplicate beforeEach
blocks inside the individual describe blocks to avoid drift and maintenance
overhead (look for the existing vi.clearAllMocks and
mockIsTauriDesktop.mockReturnValue usages to locate the duplicated setup).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 32b43d1f-06a8-4f8a-963a-5e0646aafe08
📒 Files selected for processing (22)
AGENTS.mdpackages/claude-sidecar/src/handlers/installation.test.tspackages/claude-sidecar/src/handlers/installation.tspackages/claude-sidecar/src/handlers/models.test.tspackages/claude-sidecar/src/handlers/query.test.tspackages/claude-sidecar/src/handlers/query.tspackages/claude-sidecar/src/handlers/status.test.tsserver/api/src/lib/extractPlainTextFromYXml.tsserver/hocuspocus/src/extractPlainTextFromYXml.test.tsserver/hocuspocus/src/extractPlainTextFromYXml.tsserver/mcp/src/__tests__/tools/index.test.tssrc/components/editor/TiptapEditor/useWikiLinkStatusSync.test.tsxsrc/components/editor/extensions/TagExtension.test.tssrc/components/editor/extensions/TagExtension.tssrc/components/editor/extensions/WikiLinkExtension.test.tssrc/components/editor/extensions/WikiLinkExtension.tssrc/components/editor/extensions/slashSuggestionPlugin.test.tssrc/hooks/useMermaidGenerator.test.tssrc/hooks/useWorkflowDraft.test.tssrc/hooks/useWorkflowRunSession.test.tssrc/lib/aiServiceServer.test.tssrc/stores/aiChatStore.test.ts
✅ Files skipped from review due to trivial changes (1)
- AGENTS.md
| it("emits an aborted error when the abort signal is fired before the result arrives", async () => { | ||
| const ac = new AbortController(); | ||
| // Trigger abort before iteration begins so the loop sees it on the first message. | ||
| // 反復開始前に abort して、最初のメッセージでループを抜けるようにする。 | ||
| ac.abort(); | ||
| queryMock.mockReturnValue( | ||
| makeQueryIterable([ | ||
| partial({ | ||
| type: "content_block_delta", | ||
| delta: { type: "text_delta", text: "x" }, | ||
| }) as unknown as SDKMessage, | ||
| ]), | ||
| ); | ||
|
|
||
| await runQuery({ | ||
| id: "q1", | ||
| prompt: "p", | ||
| writeLine: (l) => writes.push(l), | ||
| abortController: ac, | ||
| tracker, | ||
| }); | ||
|
|
||
| expect(parsed()).toEqual([ | ||
| { type: "error", id: "q1", error: "Query aborted", code: "aborted" }, | ||
| ]); |
There was a problem hiding this comment.
Abort coverage is missing the active-tool cleanup case.
If cancellation happens after a tool-use-start, runQuery() currently breaks out of the loop and emits only the aborted error. No matching tool-use-complete is sent, so consumers that derive tool state from the event stream can stay stuck. Please add a regression here and finalize activeToolName in the abort/exception paths.
🧩 Possible fix in packages/claude-sidecar/src/handlers/query.ts
- if (abortController.signal.aborted) {
- emit({
- type: "error",
- id,
- error: "Query aborted",
- code: "aborted",
- });
- return;
- }
+ if (abortController.signal.aborted) {
+ if (activeToolName) {
+ emit({ type: "tool-use-complete", id, toolName: activeToolName });
+ activeToolName = null;
+ }
+ emit({
+ type: "error",
+ id,
+ error: "Query aborted",
+ code: "aborted",
+ });
+ return;
+ }
emit({
type: "stream-complete",
id,
result: { content: aggregated },
});
} catch (err) {
+ if (activeToolName) {
+ emit({ type: "tool-use-complete", id, toolName: activeToolName });
+ activeToolName = null;
+ }
const message = err instanceof Error ? err.message : String(err);
emit({
type: "error",
id,
error: message,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("emits an aborted error when the abort signal is fired before the result arrives", async () => { | |
| const ac = new AbortController(); | |
| // Trigger abort before iteration begins so the loop sees it on the first message. | |
| // 反復開始前に abort して、最初のメッセージでループを抜けるようにする。 | |
| ac.abort(); | |
| queryMock.mockReturnValue( | |
| makeQueryIterable([ | |
| partial({ | |
| type: "content_block_delta", | |
| delta: { type: "text_delta", text: "x" }, | |
| }) as unknown as SDKMessage, | |
| ]), | |
| ); | |
| await runQuery({ | |
| id: "q1", | |
| prompt: "p", | |
| writeLine: (l) => writes.push(l), | |
| abortController: ac, | |
| tracker, | |
| }); | |
| expect(parsed()).toEqual([ | |
| { type: "error", id: "q1", error: "Query aborted", code: "aborted" }, | |
| ]); | |
| if (abortController.signal.aborted) { | |
| if (activeToolName) { | |
| emit({ type: "tool-use-complete", id, toolName: activeToolName }); | |
| activeToolName = null; | |
| } | |
| emit({ | |
| type: "error", | |
| id, | |
| error: "Query aborted", | |
| code: "aborted", | |
| }); | |
| return; | |
| } | |
| emit({ | |
| type: "stream-complete", | |
| id, | |
| result: { content: aggregated }, | |
| }); | |
| } catch (err) { | |
| if (activeToolName) { | |
| emit({ type: "tool-use-complete", id, toolName: activeToolName }); | |
| activeToolName = null; | |
| } | |
| const message = err instanceof Error ? err.message : String(err); | |
| emit({ | |
| type: "error", | |
| id, | |
| error: message, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/claude-sidecar/src/handlers/query.test.ts` around lines 591 - 615,
Add a regression that simulates a tool-use-start before abort and assert that
runQuery finalizes activeToolName by emitting a corresponding tool-use-complete
(or equivalent finalization) even when the abort signal fires or an exception
occurs: update the existing test that aborts before results to instead start a
tool (emit a tool-use-start through the mocked query iterable), trigger
ac.abort(), then call runQuery and expect both the aborted error and a
tool-use-complete event (or that activeToolName is cleared) in the parsed
output; also change runQuery's abort/exception handling paths to check for an
existing activeToolName and emit or finalize tool-use-complete (and clear
activeToolName) before returning so consumers won’t be left in a stuck tool
state.
| /** Build a fully-mocked ZediClient. Tools call only the methods we register, so types are safe. / | ||
| * 全メソッドをモック化した ZediClient。 */ |
There was a problem hiding this comment.
Normalize JSDoc block formatting in helper comments.
Line [19] and Line [47] use ... / inside /** ... */, which is easy to misread as a broken terminator. Please switch to standard multiline JSDoc formatting.
💡 Suggested cleanup
-/** Build a fully-mocked ZediClient. Tools call only the methods we register, so types are safe. /
- * 全メソッドをモック化した ZediClient。 */
+/**
+ * Build a fully-mocked ZediClient. Tools call only the methods we register, so types are safe.
+ * 全メソッドをモック化した ZediClient。
+ */
function createMockClient(): ZediClient {
@@
-/** Captures registerTool calls so we can assert which tools were registered. / 登録呼び出しを記録するスタブ */
+/**
+ * Captures registerTool calls so we can assert which tools were registered.
+ * 登録呼び出しを記録するスタブ。
+ */
function createServerStub(): {Also applies to: 47-47
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/mcp/src/__tests__/tools/index.test.ts` around lines 19 - 20, The JSDoc
helper comments in the test file use a malformed terminator sequence ("... /")
which can be misread; update the block comments for the ZediClient helper (the
/** Build a fully-mocked ZediClient... */ comment and the analogous comment
later in the file) to use standard multiline JSDoc termination ("*/") so each
block is a proper /** ... */ JSDoc comment, preserving the existing English and
Japanese text and spacing.
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); |
There was a problem hiding this comment.
Reset implementations too, not just call history.
vi.clearAllMocks() does not reset mockImplementation / mockResolvedValue, so behavior can leak between tests as the suite grows. Prefer resetting these shared mocks in beforeEach.
Proposed diff
beforeEach(() => {
- vi.clearAllMocks();
+ mockGenerateMermaidDiagram.mockReset();
+ mockGetAISettingsOrThrow.mockReset();
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useMermaidGenerator.test.ts` around lines 49 - 51, The tests
currently call vi.clearAllMocks() in the beforeEach in useMermaidGenerator.test
but that doesn't reset mock implementations; change the setup to reset
implementations as well by replacing or augmenting the beforeEach to call
vi.resetAllMocks() (or vi.clearAllMocks(); vi.resetAllMocks()) so mocks
created/configured for functions used by useMermaidGenerator tests have their
mockImplementation/mockResolvedValue cleared between tests.
| it("generate catches synchronous throws from generateMermaidDiagram and sets error", async () => { | ||
| mockGenerateMermaidDiagram.mockRejectedValueOnce(new Error("network")); | ||
|
|
There was a problem hiding this comment.
This test name and setup are misaligned.
The case says “synchronous throws” but mockRejectedValueOnce exercises async rejection. Either rename the test or switch to a true sync throw to keep test intent precise.
Proposed diff (true sync throw path)
-it("generate catches synchronous throws from generateMermaidDiagram and sets error", async () => {
- mockGenerateMermaidDiagram.mockRejectedValueOnce(new Error("network"));
+it("generate catches synchronous throws from generateMermaidDiagram and sets error", async () => {
+ mockGenerateMermaidDiagram.mockImplementationOnce(() => {
+ throw new Error("network");
+ });As per coding guidelines, "Tests serve as a source of truth for specifications alongside implementation code TSDoc/JSDoc."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useMermaidGenerator.test.ts` around lines 143 - 145, The test
"generate catches synchronous throws from generateMermaidDiagram and sets error"
is using mockRejectedValueOnce (async rejection) but should simulate a
synchronous throw; change the mock for mockGenerateMermaidDiagram to a
synchronous throw (e.g. mockImplementationOnce(() => { throw new
Error("network") })) so the hook's synchronous-throw path is exercised when the
code calls generateMermaidDiagram; keep the rest of the test assertions the same
to verify error state is set.
…ents (#760) * test(P8): add admin/ and packages/ui test coverage (#749) Set up the missing vitest harness for `@zedi/ui` and add unit tests for the API helpers and stateful custom hooks under `admin/`. - packages/ui: new `vitest.config.ts` (jsdom) + `src/test/setup.ts` with a minimal `matchMedia` polyfill, wired into root `test:run` and CI. - packages/ui tests: `use-toast` reducer + subscription, `use-mobile` media-query subscription/unsubscribe, `SidebarProvider` open/cookie/ controlled/keyboard-shortcut behaviour, sidebar cookie parsing. - admin tests: `client.ts` (`adminFetch` headers + `getErrorMessage` fallbacks), `activity.ts` querystring builder + error path, `lint.ts` three async APIs, `dateUtils.formatDate`, plus the three custom hooks (`useAiModelActions` rollback/mounted-ref, `useAiModelsDragReorder` optimistic reorder + load() recovery, `useConfirmDialogs` request-id race guard for `getUserImpact`). * test(P8): address PR #760 review comments - admin/src/api/client.test.ts: switch global fetch save/restore to vi.stubGlobal/unstubAllGlobals (gemini suggestion). - admin/src/pages/ai-models/useAiModelsDragReorder.test.ts: drop the dead `act` block that referenced handleReorder without invoking anything (gemini suggestion); the actual reorder is exercised via handleDrop. - packages/ui/src/hooks/use-toast.test.ts: advance fake timers inside afterEach so REMOVE_TOAST drains the singleton memoryState before switching back to real timers (gemini suggestion). - package.json: add admin/vitest.config.ts to root `test:run` so the new admin coverage is enforced via the standard root entrypoint (codex P2). - admin/vitest.config.ts: pin `root: __dirname` so the config still scans admin/src when invoked from the workspace root (otherwise `src/**` would resolve to root/src under admin's `@` alias and fail to import). - .github/workflows/ci.yml: add `admin` and `@zedi/claude-sidecar` steps next to shared/ui so CI matches the `test:run` chain (devin P2). * test(P8): make remaining JP-only test titles/comments bilingual CodeRabbit flagged a few specs / inline comments under admin/ that were Japanese-only while the surrounding context follows the project's JP/EN bilingual convention. Append concise English translations: - admin/src/api/activity.test.ts: querystring-builder spec title. - admin/src/pages/ai-models/useAiModelsDragReorder.test.ts: three drag-state spec titles (handleDragStart / handleDragOver / handleDragEnd & handleDragLeave). - admin/src/pages/users/useConfirmDialogs.test.ts: four inline comments inside the deleteTarget race-guard test. Test behaviour is unchanged. --------- Co-authored-by: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
admin/src/pages/users/useConfirmDialogs.test.ts (1)
157-204: Add a stale-reject race test to complete the concurrency contract.You already cover stale resolve; adding the symmetric stale reject case would harden the
catch-guard path and improve mutation resistance.Suggested test addition
+ it("古い request の reject も新しい target を上書きしない / stale reject is ignored", async () => { + let rejectA: ((e?: unknown) => void) | null = null; + let resolveB: ((v: UserImpact) => void) | null = null; + vi.mocked(getUserImpact) + .mockImplementationOnce( + () => + new Promise<UserImpact>((_resolve, reject) => { + rejectA = reject; + }), + ) + .mockImplementationOnce( + () => + new Promise<UserImpact>((resolve) => { + resolveB = resolve; + }), + ); + + const { result } = makeHook(); + act(() => result.current.requestDelete(userA)); + act(() => result.current.requestDelete(userB)); + + await act(async () => { + rejectA?.(new Error("stale")); + await Promise.resolve(); + }); + expect(result.current.deleteTarget?.user.id).toBe(userB.id); + expect(result.current.deleteTarget?.loadingImpact).toBe(true); + + await act(async () => { + resolveB?.(sampleImpact); + await Promise.resolve(); + }); + expect(result.current.deleteTarget?.impact).toEqual(sampleImpact); + expect(result.current.deleteTarget?.loadingImpact).toBe(false); + });As per coding guidelines "
**/*.test.{ts,tsx}: Prioritize Mutation testing score as the primary quality metric; target 80%+ code coverage as a supporting goal".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@admin/src/pages/users/useConfirmDialogs.test.ts` around lines 157 - 204, Add a symmetric "stale reject is ignored" concurrency test that mirrors the existing stale-resolve test: mock getUserImpact with two sequential mockImplementationOnce calls that each return a new Promise and capture their reject callbacks (similar to resolveA/resolveB), call result.current.requestDelete(userA) then requestDelete(userB) using makeHook and act, then invoke the first promise's reject (rejectA) and assert the hook's state (result.current.deleteTarget, .impact, .loadingImpact) remains pointing to userB and unaffected by the stale rejection, finally rejectB and assert B's rejection path is handled as expected; ensure you use vi.mocked(getUserImpact), the same result.current.requestDelete flow, and act/await Promise.resolve() to flush microtasks so the catch-guard path is exercised.packages/ui/src/hooks/use-toast.test.ts (1)
24-24: Make this test title bilingual to match the rest of the file.Adding the English counterpart here would keep spec style consistent across the suite.
As per coding guidelines
**/*.{ts,tsx,js,jsx}: コメントやドキュメントは、原則として日本語と英語の両方を併記する。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/hooks/use-toast.test.ts` at line 24, Update the test title string in the it(...) call currently reading "ADD_TOAST は配列の先頭に追加し、TOAST_LIMIT(=1) で切り詰める" to include an English translation so it is bilingual (e.g., append " / adds toast to front of array and trims to TOAST_LIMIT(=1)"). Modify the test declaration in use-toast.test.ts where the it(...) for ADD_TOAST is defined to include both Japanese and English text while preserving the original meaning and punctuation.packages/ui/src/components/sidebar/SidebarProvider.test.tsx (2)
17-22: Consider extractingclearCookiesinto a shared test utility.The same cookie-clear helper appears in multiple sidebar test files; centralizing it would reduce drift and simplify maintenance.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/sidebar/SidebarProvider.test.tsx` around lines 17 - 22, Extract the clearCookies helper into a shared test utility and import it from tests/utils (or your existing test helpers); move the current clearCookies function (the loop that parses document.cookie and sets each cookie to max-age=0) into a named export e.g., clearCookies in a new/shared module, update SidebarProvider.test.tsx to import { clearCookies } from that utility, and remove the duplicate local function from other sidebar tests to reference the shared export so all tests use the same implementation.
74-74: Keep test spec strings bilingual for consistency.These two test titles are Japanese-only while the suite is otherwise bilingual; adding English counterparts keeps spec readability uniform.
As per coding guidelines
**/*.{ts,tsx,js,jsx}: コメントやドキュメントは、原則として日本語と英語の両方を併記する。Also applies to: 96-96
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/sidebar/SidebarProvider.test.tsx` at line 74, Update the two test spec strings to be bilingual: modify the it(...) titles that currently read "toggleSidebar で開閉が切り替わり cookie に永続化される" and the other Japanese-only title at the second occurrence to include an English translation after the Japanese (e.g. "toggleSidebar で開閉が切り替わり cookie に永続化される / toggles sidebar and persists state to cookie"); ensure both it(...) calls in SidebarProvider.test.tsx contain Japanese and English per the repo guideline.admin/src/pages/ai-models/useAiModelsDragReorder.test.ts (1)
38-44: Reduceas nevercasting by typing test doubles to hook signatures.Line 86 (and repeated call sites) force-casts strongly typed hook props to
never, which hides contract drift in tests. Prefer typed mocks aligned toReact.Dispatchandloadsignatures so test compile-time checks stay meaningful.
As per coding guidelines:**/*.{ts,tsx}: Use TypeScript strict mode; bananytype and require explicit type annotations.Proposed refactor (typed args, no
as never)interface Args { models: AiModelAdmin[]; - setModels: ReturnType<typeof vi.fn>; - setError: ReturnType<typeof vi.fn>; + setModels: React.Dispatch<React.SetStateAction<AiModelAdmin[]>>; + setError: React.Dispatch<React.SetStateAction<string | null>>; isMountedRef: { current: boolean }; - load: LoadFn & ReturnType<typeof vi.fn>; + load: LoadFn; } function createArgs(models: AiModelAdmin[]): Args { - const load = vi.fn().mockResolvedValue(undefined) as ReturnType<typeof vi.fn> & LoadFn; + const load: LoadFn = vi.fn().mockResolvedValue(undefined); return { models, - setModels: vi.fn(), - setError: vi.fn(), + setModels: vi.fn<React.Dispatch<React.SetStateAction<AiModelAdmin[]>>>(), + setError: vi.fn<React.Dispatch<React.SetStateAction<string | null>>>(), isMountedRef: { current: true }, load, }; }Also applies to: 86-90, 119-124, 148-153, 177-182, 200-205, 223-228, 245-250
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@admin/src/pages/ai-models/useAiModelsDragReorder.test.ts` around lines 38 - 44, The test doubles are being cast to `never`, hiding type mismatches; update the Args interface and the mock declarations so mocks match the real hook signatures instead of using `as never`: change `setModels` and `setError` from ReturnType<typeof vi.fn> to explicit React.Dispatch types (e.g., React.Dispatch<React.SetStateAction<AiModelAdmin[]>> and React.Dispatch<React.SetStateAction<string | null>>), type `load` as the actual LoadFn signature (or LoadFn & vi mock typed as that signature), and ensure `isMountedRef` and `models` keep their concrete types; then replace all `as never` usages with properly typed `vi.fn()` mocks that satisfy those signatures (refer to the Args interface, setModels, setError, load, isMountedRef, models and update the mock assignments used at the repeated call sites).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@admin/src/pages/users/useConfirmDialogs.test.ts`:
- Around line 62-255: The test titles in useConfirmDialogs.test.ts are
inconsistently bilingual (some only Japanese); update every it(...) description
(e.g., the titles around requestRoleChange/confirmRoleChange,
requestUnsuspend/confirmUnsuspend, requestDelete/cancelDelete/confirmDelete
tests) to include both Japanese and English (for example append " / <English
translation>" or " — <English translation>") so all it(...) strings follow the
JA+EN convention used elsewhere in the file and CI logs remain searchable; scan
for each it(...) in this file and make the title bilingual consistently.
---
Nitpick comments:
In `@admin/src/pages/ai-models/useAiModelsDragReorder.test.ts`:
- Around line 38-44: The test doubles are being cast to `never`, hiding type
mismatches; update the Args interface and the mock declarations so mocks match
the real hook signatures instead of using `as never`: change `setModels` and
`setError` from ReturnType<typeof vi.fn> to explicit React.Dispatch types (e.g.,
React.Dispatch<React.SetStateAction<AiModelAdmin[]>> and
React.Dispatch<React.SetStateAction<string | null>>), type `load` as the actual
LoadFn signature (or LoadFn & vi mock typed as that signature), and ensure
`isMountedRef` and `models` keep their concrete types; then replace all `as
never` usages with properly typed `vi.fn()` mocks that satisfy those signatures
(refer to the Args interface, setModels, setError, load, isMountedRef, models
and update the mock assignments used at the repeated call sites).
In `@admin/src/pages/users/useConfirmDialogs.test.ts`:
- Around line 157-204: Add a symmetric "stale reject is ignored" concurrency
test that mirrors the existing stale-resolve test: mock getUserImpact with two
sequential mockImplementationOnce calls that each return a new Promise and
capture their reject callbacks (similar to resolveA/resolveB), call
result.current.requestDelete(userA) then requestDelete(userB) using makeHook and
act, then invoke the first promise's reject (rejectA) and assert the hook's
state (result.current.deleteTarget, .impact, .loadingImpact) remains pointing to
userB and unaffected by the stale rejection, finally rejectB and assert B's
rejection path is handled as expected; ensure you use vi.mocked(getUserImpact),
the same result.current.requestDelete flow, and act/await Promise.resolve() to
flush microtasks so the catch-guard path is exercised.
In `@packages/ui/src/components/sidebar/SidebarProvider.test.tsx`:
- Around line 17-22: Extract the clearCookies helper into a shared test utility
and import it from tests/utils (or your existing test helpers); move the current
clearCookies function (the loop that parses document.cookie and sets each cookie
to max-age=0) into a named export e.g., clearCookies in a new/shared module,
update SidebarProvider.test.tsx to import { clearCookies } from that utility,
and remove the duplicate local function from other sidebar tests to reference
the shared export so all tests use the same implementation.
- Line 74: Update the two test spec strings to be bilingual: modify the it(...)
titles that currently read "toggleSidebar で開閉が切り替わり cookie に永続化される" and the
other Japanese-only title at the second occurrence to include an English
translation after the Japanese (e.g. "toggleSidebar で開閉が切り替わり cookie に永続化される /
toggles sidebar and persists state to cookie"); ensure both it(...) calls in
SidebarProvider.test.tsx contain Japanese and English per the repo guideline.
In `@packages/ui/src/hooks/use-toast.test.ts`:
- Line 24: Update the test title string in the it(...) call currently reading
"ADD_TOAST は配列の先頭に追加し、TOAST_LIMIT(=1) で切り詰める" to include an English translation
so it is bilingual (e.g., append " / adds toast to front of array and trims to
TOAST_LIMIT(=1)"). Modify the test declaration in use-toast.test.ts where the
it(...) for ADD_TOAST is defined to include both Japanese and English text while
preserving the original meaning and punctuation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 91893ba6-5101-4620-8f4a-1c0f264dd123
📒 Files selected for processing (17)
.github/workflows/ci.ymladmin/src/api/activity.test.tsadmin/src/api/client.test.tsadmin/src/api/lint.test.tsadmin/src/lib/dateUtils.test.tsadmin/src/pages/ai-models/useAiModelActions.test.tsadmin/src/pages/ai-models/useAiModelsDragReorder.test.tsadmin/src/pages/users/useConfirmDialogs.test.tsadmin/vitest.config.tspackage.jsonpackages/ui/package.jsonpackages/ui/src/components/sidebar/SidebarProvider.test.tsxpackages/ui/src/components/sidebar/sidebarConstants.test.tspackages/ui/src/hooks/use-mobile.test.tsxpackages/ui/src/hooks/use-toast.test.tspackages/ui/src/test/setup.tspackages/ui/vitest.config.ts
✅ Files skipped from review due to trivial changes (1)
- packages/ui/package.json
| it("requestRoleChange → confirm で onRoleChange を呼んで target を null に戻す", () => { | ||
| const { result, onRoleChange } = makeHook(); | ||
| act(() => { | ||
| result.current.requestRoleChange(userA, "admin"); | ||
| }); | ||
| expect(result.current.roleChangeTarget).toEqual({ user: userA, newRole: "admin" }); | ||
|
|
||
| act(() => { | ||
| result.current.confirmRoleChange(); | ||
| }); | ||
| expect(onRoleChange).toHaveBeenCalledWith(userA, "admin"); | ||
| expect(result.current.roleChangeTarget).toBeNull(); | ||
| }); | ||
|
|
||
| it("confirm が target なしのときは何もしない / confirm without target is a no-op", () => { | ||
| const { result, onRoleChange } = makeHook(); | ||
| act(() => { | ||
| result.current.confirmRoleChange(); | ||
| }); | ||
| expect(onRoleChange).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("cancel で target を null に戻す", () => { | ||
| const { result } = makeHook(); | ||
| act(() => { | ||
| result.current.requestRoleChange(userA, "admin"); | ||
| result.current.cancelRoleChange(); | ||
| }); | ||
| expect(result.current.roleChangeTarget).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("useConfirmDialogs - unsuspend", () => { | ||
| it("request → confirm で onUnsuspend を呼ぶ", () => { | ||
| const { result, onUnsuspend } = makeHook(); | ||
| act(() => { | ||
| result.current.requestUnsuspend(userA); | ||
| }); | ||
| expect(result.current.unsuspendTarget).toEqual(userA); | ||
|
|
||
| act(() => { | ||
| result.current.confirmUnsuspend(); | ||
| }); | ||
| expect(onUnsuspend).toHaveBeenCalledWith(userA); | ||
| expect(result.current.unsuspendTarget).toBeNull(); | ||
| }); | ||
|
|
||
| it("cancel で target を null に戻す", () => { | ||
| const { result } = makeHook(); | ||
| act(() => { | ||
| result.current.requestUnsuspend(userA); | ||
| result.current.cancelUnsuspend(); | ||
| }); | ||
| expect(result.current.unsuspendTarget).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("useConfirmDialogs - delete with impact", () => { | ||
| beforeEach(() => { | ||
| vi.mocked(getUserImpact).mockReset(); | ||
| }); | ||
|
|
||
| it("requestDelete でローディング状態 → impact 取得後に impact 反映", async () => { | ||
| vi.mocked(getUserImpact).mockResolvedValueOnce(sampleImpact); | ||
| const { result } = makeHook(); | ||
|
|
||
| act(() => { | ||
| result.current.requestDelete(userA); | ||
| }); | ||
|
|
||
| expect(result.current.deleteTarget).toEqual({ | ||
| user: userA, | ||
| impact: null, | ||
| loadingImpact: true, | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(result.current.deleteTarget?.loadingImpact).toBe(false); | ||
| }); | ||
| expect(result.current.deleteTarget?.impact).toEqual(sampleImpact); | ||
| }); | ||
|
|
||
| it("getUserImpact が失敗したら loadingImpact: false で impact は null のまま", async () => { | ||
| vi.mocked(getUserImpact).mockRejectedValueOnce(new Error("nope")); | ||
| const { result } = makeHook(); | ||
|
|
||
| act(() => { | ||
| result.current.requestDelete(userA); | ||
| }); | ||
| await waitFor(() => { | ||
| expect(result.current.deleteTarget?.loadingImpact).toBe(false); | ||
| }); | ||
| expect(result.current.deleteTarget?.impact).toBeNull(); | ||
| }); | ||
|
|
||
| it("古い request の resolve は新しい target を上書きしない / stale resolve is ignored", async () => { | ||
| let resolveA: ((v: UserImpact) => void) | null = null; | ||
| let resolveB: ((v: UserImpact) => void) | null = null; | ||
| vi.mocked(getUserImpact) | ||
| .mockImplementationOnce( | ||
| () => | ||
| new Promise<UserImpact>((resolve) => { | ||
| resolveA = resolve; | ||
| }), | ||
| ) | ||
| .mockImplementationOnce( | ||
| () => | ||
| new Promise<UserImpact>((resolve) => { | ||
| resolveB = resolve; | ||
| }), | ||
| ); | ||
|
|
||
| const { result } = makeHook(); | ||
|
|
||
| act(() => { | ||
| result.current.requestDelete(userA); | ||
| }); | ||
| expect(result.current.deleteTarget?.user.id).toBe(userA.id); | ||
|
|
||
| // ユーザー B に切り替えてから古い request A を resolve しても、状態は B のまま | ||
| // Even if old request A resolves after switching to user B, the state stays on B. | ||
| act(() => { | ||
| result.current.requestDelete(userB); | ||
| }); | ||
| expect(result.current.deleteTarget?.user.id).toBe(userB.id); | ||
|
|
||
| await act(async () => { | ||
| resolveA?.({ ...sampleImpact, notesCount: 999 }); | ||
| await Promise.resolve(); | ||
| }); | ||
| // A の結果は反映されない / A's resolved result must not be applied. | ||
| expect(result.current.deleteTarget?.user.id).toBe(userB.id); | ||
| expect(result.current.deleteTarget?.impact).toBeNull(); | ||
| expect(result.current.deleteTarget?.loadingImpact).toBe(true); | ||
|
|
||
| // B の resolve はちゃんと反映される / B's resolve still propagates correctly. | ||
| await act(async () => { | ||
| resolveB?.(sampleImpact); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect(result.current.deleteTarget?.impact).toEqual(sampleImpact); | ||
| expect(result.current.deleteTarget?.loadingImpact).toBe(false); | ||
| }); | ||
|
|
||
| it("cancelDelete は requestId をインクリメントし、後から来た resolve を無効化する", async () => { | ||
| let resolveLate: ((v: UserImpact) => void) | null = null; | ||
| vi.mocked(getUserImpact).mockImplementationOnce( | ||
| () => | ||
| new Promise<UserImpact>((resolve) => { | ||
| resolveLate = resolve; | ||
| }), | ||
| ); | ||
|
|
||
| const { result } = makeHook(); | ||
| act(() => { | ||
| result.current.requestDelete(userA); | ||
| }); | ||
| act(() => { | ||
| result.current.cancelDelete(); | ||
| }); | ||
| expect(result.current.deleteTarget).toBeNull(); | ||
|
|
||
| // resolve しても deleteTarget は null のまま / late resolve does not revive deleteTarget. | ||
| await act(async () => { | ||
| resolveLate?.(sampleImpact); | ||
| await Promise.resolve(); | ||
| }); | ||
| expect(result.current.deleteTarget).toBeNull(); | ||
| }); | ||
|
|
||
| it("confirmDelete で onDelete を呼んで target を null にする", async () => { | ||
| vi.mocked(getUserImpact).mockResolvedValueOnce(sampleImpact); | ||
| const { result, onDelete } = makeHook(); | ||
| act(() => { | ||
| result.current.requestDelete(userA); | ||
| }); | ||
| await waitFor(() => { | ||
| expect(result.current.deleteTarget?.loadingImpact).toBe(false); | ||
| }); | ||
|
|
||
| act(() => { | ||
| result.current.confirmDelete(); | ||
| }); | ||
| expect(onDelete).toHaveBeenCalledWith(userA); | ||
| expect(result.current.deleteTarget).toBeNull(); | ||
| }); | ||
|
|
||
| it("confirmDelete が target なしのときは onDelete を呼ばない", () => { | ||
| const { result, onDelete } = makeHook(); | ||
| act(() => { | ||
| result.current.confirmDelete(); | ||
| }); | ||
| expect(onDelete).not.toHaveBeenCalled(); | ||
| }); |
There was a problem hiding this comment.
Keep test titles consistently bilingual across this file.
Several it(...) descriptions are Japanese-only while others are bilingual; please align all test titles/comments to JA+EN for consistency and searchable CI logs.
As per coding guidelines "**/*.{ts,tsx,js,jsx}: コメントやドキュメントは、原則として日本語と英語の両方を併記する。".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@admin/src/pages/users/useConfirmDialogs.test.ts` around lines 62 - 255, The
test titles in useConfirmDialogs.test.ts are inconsistently bilingual (some only
Japanese); update every it(...) description (e.g., the titles around
requestRoleChange/confirmRoleChange, requestUnsuspend/confirmUnsuspend,
requestDelete/cancelDelete/confirmDelete tests) to include both Japanese and
English (for example append " / <English translation>" or " — <English
translation>") so all it(...) strings follow the JA+EN convention used elsewhere
in the file and CI logs remain searchable; scan for each it(...) in this file
and make the title bilingual consistently.
…473) (#761) CI mutation-light の `--mutate` リストに、フェーズ1・2 (#470, #471) で テストカバレッジが整い高スコアが安定したファイルを追加する。 PR で対象ファイルの退行(survived 増加)を早めに検知できるようにする。 追加ファイルとローカル計測のスコア (2026-04-26, stryker.config.mutation-changed.mjs): - src/lib/aiChatConversationTitle.ts 100.00% - src/lib/aiCostUtils.ts 91.18% - src/lib/encryption.ts 96.43% - src/lib/mcpServerImportHelpers.ts 100.00% - src/lib/onboardingState.ts 83.82% すべて break 閾値 (70%) と low 閾値 (75%) を上回り、`# errors` も 0。 9 ファイル合計のスコアは 88.55% (433 killed / 52 survived / 4 nocov / 0 errors)。 ローカル所要時間は 5m34s で、CI 上の他ジョブと並列実行のため許容範囲。 その他のフェーズ1・2 候補 (wikiLinkUtils 76.92%, parseStdioArgs 66.67%) は スコアが境界・以下のため、追加テストでスコアが安定してから別 PR で投入する。 Refs #468, closes #473 https://claude.ai/code/session_01DaxXhP83JmrLWPT8RvWY4R Co-authored-by: Claude <noreply@anthropic.com>
- claude-sidecar: emit `tool-use-complete` on abort/exception paths when a tool is still active, keeping start/complete pairs balanced for downstream consumers; add regression tests for both paths. - server/mcp tools test: normalize a malformed `/** ... /` helper comment to standard multiline JSDoc. - useMermaidGenerator tests: switch shared mock reset from `vi.clearAllMocks` to `vi.resetAllMocks` so per-test `mockImplementation`/`mockResolvedValue` no longer leak between cases. - useMermaidGenerator tests: realign "synchronous throws" case to use a true sync throw, and add a separate async-rejection case so the title matches the setup. - admin useConfirmDialogs tests: add English to Japanese-only `it(...)` titles to match the JP/EN bilingual convention. Co-authored-by: Claude <noreply@anthropic.com>
概要
developにマージ済みの機能追加・不具合修正・テスト強化・DB/API 修正をmainに取り込むリリース PR です。ハッシュタグ同期、WikiLink/タイトル連動、検索リファクタ、大規模なテスト追加、Drizzle0018マイグレーションなどが含まれます。変更点
src/lib//src/stores//src/hooks/: ハッシュタグ(#name)同期とステータス更新の E2E 連携、ページタイトル変更の WikiLink・タグ・ゴーストリンクへの伝播、同一タイトルによる誤リライト防止、buildGlobalSearchResultsの純関数化、ストレージ/同期/タグ・WikiLink ユーティリティの拡張src/lib/agentSlashCommands/: スラッシュコマンド周りのテスト追加server/api/: セキュリティ・コアサービス・ルート等のユニットテスト、onboardingおよびpages.kind用 Drizzle マイグレーション0018とジャーナル更新db/migrations/: オンボーディング・ページ kind 向け SQL マイグレーション(番号付け整備を含む).github//scripts//packages/shared/: デフォルトブランチをdevelopに揃える設定・ワークフロー・スクリプトの調整src/pages/NoteSettings/: 削除後リダイレクトの意図をドキュメント(notes)で明文化変更の種類
テスト方法
origin/developがorigin/mainより進んでいることを確認(本 PR の差分が意図どおりであること)。server/api/drizzleの0018およびdb/migrationsの該当ファイル)を適用し、onboarding/pages.kindが期待どおりであることを確認。チェックリスト
スクリーンショット(UI 変更がある場合)
エディタ・検索・NoteSettings 周りに変更があります。必要に応じて Before/After を添付してください。
関連 Issue
Related to #725, #726, #731, #732, #733, #734, #735, #736, #737, #738, #739, #740, #742, #743, #744, #745, #746, #750, #751, #752, #753, #754, #755
Summary by CodeRabbit
New Features
Database & CI
Improvements
Documentation
Tests