Skip to content

fix: loadSettings の migration 保存失敗で全設定が既定値に倒れるのを防ぐ (#448) - #450

Merged
ymnao merged 3 commits into
mainfrom
fix/load-settings-migration-save-failure
Jul 31, 2026
Merged

ymnao merged 3 commits into
mainfrom
fix/load-settings-migration-save-failure

Conversation

@ymnao

@ymnao ymnao commented Jul 31, 2026

Copy link
Copy Markdown
Owner

概要

loadSettings の失敗ハンドリングを 3 段に分割し、migration の disk 保存が失敗しただけでセッション全体が全設定既定値へ倒れる欠陥 (#448) を解消する。3 経路とも error toast で通知するようにし、無通知の巻き戻りを無くす。

従来は loadSettings 全体が単一の try/catch で包まれており、migration 適用後の settingsSave() が throw すると settings.json 自体は読めているのに catch が { ...DEFAULTS } を返していた。結果として loadRemoteImages も既定 (true = リモート画像許可) へ戻るが、ユーザーへの通知は一切なかった。

関連 Issue

Closes #448

移行 Stage

  • Stage 6: 仕上げ・配布・切り替え

変更内容

  • 失敗の粒度を 3 段に分割 — ① applyMigrations 自体の失敗 ② migration 結果の disk 保存失敗 ③ 読み出しループの失敗。①② では既定値へ倒さず読み出しを継続する(settings:set は main 側 cache を即更新するので、移行結果も他の設定値も今回の起動中は有効)。全既定値へ倒すのは settings IPC 自体が壊れている ③ のみ
  • 3 経路とも error toast で通知 — 従来は全経路が無通知だった
  • 文言は「対象集合の全要素で真な事実」に絞る(PR fix: 設定の保存失敗を toast で通知し、無通知の巻き戻りを防ぐ (#446) #449 の教訓を適用)
    • ② の移行結果は、後続の persist(settings:save / window state 保存)が 1 回でも成功すれば _schemaVersion ごと disk に載り、一度も成功しなければ次回起動で migration が再実行される。どちらでも失われないので、文言は「今回の起動では有効・失われることはありません」まで降ろす(「次回起動時に再試行します」は前者で偽、saveSetting の「元の値へ戻ります」も成立しない)
    • ③ は「既定値をファイルへ書き戻すことはありません」。「設定ファイルは変更されません」は ② が成功した直後に ③ へ入る世界で偽になる
  • load 側の通知は saveSetting のスロットル窓を共有しない — 共有すると起動時の通知が直後のユーザー操作起因の save 失敗通知を黙らせ、設定の保存失敗が無通知で、プライバシー設定が次回起動時に巻き戻りうる #446 で可視化した巻き戻り警告が再び無通知になる
  • migration の再実行耐性(冪等性)を明文化 — 本 PR で「migration が失敗しても読み出しを継続し、次回起動で再実行され得る」が正式な回復経路になったため、要件を docs/specification.md の移行フローと Migration interface のコメントに書き下ろした
  • テストは失敗系 6 ケースを追加(既存 loadSettings テストの stateful mock も共通ヘルパーへ一本化)

動作確認

  • ユニットテスト 2905 passed / 2 skipped(134 files)
  • mutation 検証 9/9 KILLED(try/catch 削除・toast 削除・スロットル窓の共有化・文言の旧表現への回帰を含む)
  • 型チェック 3 tsconfig(node / web / e2e)すべて通過
  • codex-review security PASS
検証エビデンス

リスク分類

tier: medium — reasons: (classify-risk.sh の出力は {"tier": "medium", "reasons": []}。特別な加点要因なしの既定 tier)

実行した検証

種別 コマンド 結果
テスト ./node_modules/.bin/vitest run PASS 2905 passed / 2 skipped(134 files)
テスト ./node_modules/.bin/vitest run src/lib/store.test.ts src/lib/store-migration.test.ts PASS 41 passed
mutation python3 scratchpad/s148-mutate.py / s148-mutate2.py 9/9 KILLED
Lint ./node_modules/.bin/biome check --write src/lib/store.ts src/lib/store.test.ts src/lib/store-migration.ts PASS(No fixes applied)
型チェック ./node_modules/.bin/tsc --noEmit -p tsconfig.node.json / tsconfig.web.json / tsconfig.e2e.json 3 本とも PASS
レビュー /simplify(reuse / simplification / efficiency / altitude の 4 agents 並列) 4 findings → 1 fixed, 3 skipped(理由は下表)
レビュー code-reviewer (fable) round 1 Critical 0 / Warning 2 / Suggestion 4 → Warning 2 + Suggestion 1 を fix
レビュー code-reviewer (fable) round 2(delta e36a4d8..54f759a Critical 0 / Warning 1 / Suggestion 4 → Warning 1 + Suggestion 3 を fix、1 件は Tier3
レビュー codex-review security PASS(0 findings)
CI GitHub Actions(16 checks) 全 PASS
e2e (renderer-only) CI pnpm test:e2e 186 passed + 1 flaky = 187(main の 187 と同数 = 新規 e2e なし、意図どおり)。flaky は e2e/search.spec.ts:232#300 の上限 notice)で本 PR の変更経路とは無関係、retry で green
e2e (実 Electron) CI pnpm test:e2e:electron 22 passed(main と同数)
ユニット (CI) CI test job PASS

レビュー指摘と対応

# Finding 対応
S-1 store.test.tsmockStatefulStore が既存テストのインライン stateful mock と重複(reuse / simplification の 2 観点が収束) (a) fix: describe 上位へ hoist して既存テストと共用化
S-2 store.ts — 3 段の try/catch がボイラープレートの反復 skip: 段ごとに「続行するか」「何を返すか」が異なり、共通化すると分岐が読みにくくなる(指摘元 agent も低優先と評価)
S-3 store.tsnotifyLoadFailurenotifySaveFailure のレジストリへ統合すべき(altitude) skip: スロットル窓の有無という本質的に異なるセマンティクスを 1 テーブルへ畳むと、窓を無効化する番兵値を throttle 判定から除外する分岐が要る。分岐理由は in-code コメントと test で固定済み
S-4 efficiency 観点 該当なし(成功パスの I/O・await 直列度は不変、起動時ブロッキング増なし)
R1-1 store.ts — 「次回起動時に移行を再試行します」は全ケースで真にならない(後続 persist の成功で _schemaVersion ごと disk に載る) (a) fix: main 側 persist() が cache 全体を atomic write することを実装で裏取りし、両ケースで真な文言へ変更
R1-2 docs / store-migration.ts — migration の再実行耐性が未文書化のまま回復機構として依存されている (a) fix: 移行フローと Migration interface に冪等性要件を明記
R1-3 store.ts — ③ の「設定ファイルは変更されません」は ② 成功後に ③ が失敗する世界で偽 (a) fix: 「既定値をファイルへ書き戻すことはありません」へ
R1-4 dev の StrictMode 二重 mount で同一 toast が 2 枚積まれる Tier3: dev 限定の外観のみ
R1-5 mock の未設定 key 戻り値が undefined(実 IPC は null Tier3: PARSERS / applyMigrations は両者を同一に扱い、既存 default mock とも整合
R2-1 store.test.ts — test 名とコメントが「次回起動で再試行」の旧主張のまま(同一 commit が否定した内容) (a) fix
R2-2 store.ts — ③ のコメント「settings.json は無傷のまま」が同型の過剰主張 (a) fix
R2-3 docs / store-migration.ts — 「再実行される」は断定しすぎ(後続 persist が一度も成功しなかった場合に限る) (a) fix: 「再実行され得る」へ
R2-4 store.test.ts — 「既定値をファイルへ書き戻すことはありません」が pin されておらず旧文言への回帰が素通り (a) fix: assertion 追加(mutation で KILLED 確認)
R2-5 store-migration.test.ts — 冪等性専用の pin テストが未追加 Tier3: 既存テストが同一入力(部分適用済み状態)を実行済みで、将来の網羅性向上型

追跡先

backlog delta: 起票 0 件 / 本 PR で close 1 件 (#448) / 現在 open 17 件

本 PR で fix しない finding ((b) 起票 / (c) 対応しない): なし

Draft 判定

  • 判定: normal
  • 根拠: step 4 — レビューで確認された Tier1 / Tier2 finding はすべて本 PR で (a) fix 済み。(b) 起票対象・(c) 対応しない対象ともに 0 件

ymnao added 3 commits August 1, 2026 00:51
- 単一 try/catch を 3 段(migration 実行 / migration の disk 保存 / 読み出し
  ループ)に分割し、それぞれの失敗で既定値へ倒す範囲を分ける
- migration の settingsSave 失敗では読み出せた設定を返す(settings:set 済みの
  値は main cache 上で有効なため、セッション全体を既定値にする理由がない)
- 3 経路とも error toast で通知する。従来は無通知で loadRemoteImages が
  既定 (true) へ戻り、ユーザーが気付けなかった
- migration 保存失敗の文言は「次回起動時に移行を再試行」。_schemaVersion も
  disk に載らないため値は巻き戻らず再実行されるので、saveSetting の
  「元の値へ戻ります」はこの経路では成立しない
- load 側の通知は saveSetting のスロットル窓を共有しない(起動時通知が直後の
  save 失敗通知を黙らせるのを防ぐ)
- store.test.ts に失敗系 6 ケースを追加
- 「次回起動時に移行を再試行します」は全ケースで真にならない。main 側 persist()
  は cache 全体を書き出すため、後続の settings:save や window state 保存が 1 回
  でも成功すれば _schemaVersion ごと disk に載り再実行されない。両ケースで真な
  「今回の起動では有効・移行結果は失われない」に絞る
- 読み出し失敗の文言も「設定ファイルは変更されません」→「既定値をファイルへ
  書き戻すことはありません」。migration 保存が成功した直後に読み出しが失敗する
  ケースでは前者は偽になる
- migration の再実行耐性 (冪等性) を docs/specification.md と Migration interface
  に明文化。本 PR で「失敗しても読み出しを継続し次回起動で再実行」が正式な回復
  経路になったため、要件を暗黙にしない
- store.test.ts の mockStatefulStore を describe 上位へ hoist し、既存 migration
  テストのインライン stateful mock と一本化
- test 名とコメントが「次回起動で migration が再実行される」という旧主張のまま
  だったのを、直前 commit が否定した内容に合わせて更新
- 読み出し失敗経路のコメント「settings.json は無傷のまま」も同型の過剰主張。
  段 2 の migration 保存が成功した後にこの経路へ入る世界では偽になる
- 「既定値をファイルへ書き戻すことはありません」の節を test で pin。旧文言への
  回帰が素通りしていた(mutation で KILLED を確認)
- migration の再実行は後続 persist が一度も成功しなかった場合に限るため、
  docs / interface コメントの断定を「再実行され得る」に修正
@ymnao
ymnao merged commit 8072693 into main Jul 31, 2026
17 checks passed
@ymnao
ymnao deleted the fix/load-settings-migration-save-failure branch July 31, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

loadSettings の migration 保存失敗で全設定が既定値に倒れ、無通知でセッション全体に効く

1 participant