refactor: ロジック層テスト拡充 + onerror 漏れ修正 + qr-ticket シリアライザ整理 (#169) - #218
Conversation
…Utf8Bytes のテストを追加 BOM 付き UTF-8/UTF-16 検出、サイズ制限例外、ASCII/日本語/絵文字の往復変換、 BOM 付き変換の先頭バイト確認など encoding.ts の未テスト関数を網羅。
範囲内/外の入力・handleChange 時の即時更新・handleBlur 時のクランプ確定・ 非数値/空文字列の min クランプ・小数点切り捨てなどを検証。
…URL を呼び出す img.onerror ハンドラが未定義だったため、ロードエラー時に ObjectURL が解放されずメモリリークが 発生していた。svgContentToPngBlob と同様に onerror でも revokeObjectURL を呼ぶよう修正。
… を throw 方針に変更 - PAYLOAD_FIELDS 配列から PAYLOAD_FIELD_COUNT を導出(リテラル廃止) - serializeTicket(payload) / parseQrString(raw) の対称ペアを新規公開 - sanitizeField を「| 含有時に throw」する事前バリデーションに変更(旧: スペース置換) - 既存テストの「| はスペースに置換」ケースを「throw する」に更新 - 新ペアの単体テストおよび PAYLOAD_FIELD_COUNT のテストを追加
PR #149 のリグレッション保護として以下を追加: - 空入力 ("") を渡した時に debounce を待たず output / error / isPending が即時クリアされる - debounce 中に setInput を再呼び出しすると前回 schedule された変換がキャンセルされ、 最後の入力にだけ transform が走る(連続 3 回入力でも 1 回しか呼ばれないことも確認) - transform が throw した後でも後続の正常入力で error がクリアされ recover できる
fumtas1k
left a comment
There was a problem hiding this comment.
レビューありがとうございます。3 つの独立改修すべて方向性として妥当で、特にテスト拡充の質が高く感心しました。マージブロッカーは無いですが、項目 3 の破壊的変更まわりで UX/ドキュメント観点の確認が 2 件あります。
良かった点
- テスト拡充 (項目 1) が非常に丁寧:
useClampedInput.test.tsxは初期値 / handleChange / handleBlur / 文字列→数値変換を網羅した 12 ケースで、parseInt('3.9', 10) = 3の小数切り捨てまで明示的にテストしているのは良い保険。useCodec.test.tsの追加は PR #149 のリグレッション保護(空入力即時クリア / debounce キャンセル / throw 後 recover)にちゃんと焦点が当たっていて、retrospective として理想形。useQrCamera.test.tsのNotAllowedError/NotFoundError/ その他 / unmount 後 throw しない、の 4 ケースでgetUserMediareject 経路が安全に閉じることを保証しているのが良い。encoding.test.tsのconvertBytes往復テスト・絵文字(4 バイト)・日本語といった境界ケースまで揃っている。
download.tsのonerror修正 (項目 2) は単純で必要な修正。svgContentToPngBlobとの挙動整合が取れてメモリリークの懸念が消えた。PAYLOAD_FIELD_COUNT = PAYLOAD_FIELDS.length + 1は+ 1 は signature 分のコメント込みでマジックナンバーが解消されており、保守性が上がっている。
質問・改善提案(要確認 2 件)
1. sanitizeField の throw 化が UI 入力経路でハンドリングされていない疑い(要確認・優先度: 中)
旧 sanitizeField は | を半角スペースに silent 置換していたところを、本 PR で「| を含む場合は throw」に変更しています。silent な値破壊から明示エラーへという方針自体は正しいですが、QrTicket.tsx の呼び出し側を確認したところ:
handleGenerate内のfor (const row of tickets)ループでsignTicket(payload, ...)→buildPayload→sanitizeFieldで throw すると、外側の汎用try/catchに拾われて"QRコードの生成中にエラーが発生しました"という汎用メッセージが表示されます。- UI 側に
|入力を事前 reject する validation はありません(isSafeTicketIdは ticket ID にしか効きません。eventId/name/categoryは素通しです)。 - 結果: 旧挙動では「
|がスペースに置換された QR が生成される」 → 新挙動では「汎用エラーで原因が分からない」になり、ユーザー視点ではむしろ UX が劣化する可能性があります。
提案: 以下のいずれか(マージ前にどちらかを選ぶか、フォローアップ issue を切るのを推奨)。
- (a)
handleGenerateの事前 validation に「いずれかのフィールドに|が含まれていないか」のチェックを追加して、専用メッセージ(例:"フィールドに | を含めることはできません")を出す。 - (b)
try/catch側でerror instanceof Error && error.message.includes('|')を判定して、専用メッセージをsetGenerateErrorに流す。 - (c) 当面はフォローアップ issue で扱う旨を PR description に明記する。
2. PR description / docs/decisions.md への破壊的変更の明示(優先度: 中)
項目 3 は qr-ticket.ts の public API 表面の変更(serializeTicket / parseQrString 新規 export + sanitizeField の挙動変更)です。とくに 「| をスペースに置換 → throw」は外向き挙動の破壊的変更にあたります。
- 現 PR description では「
sanitizeFieldを validation に方針変更」と 1 行で済ませていますが、migration note として「旧来|を入力していた場合は throw に変わる」を目立つ位置に書くと、リリースノート / 後追い時の参照性が上がります。 - CLAUDE.md 4 章のドキュメント更新ルールに照らすと、
docs/decisions.mdへの記録対象に該当する可能性があります(API 表面の変更 + 既存挙動の破壊的変更)。確認をお願いします。
nit(任意・優先度: 低)
3. detectEncoding UTF-16 BE BOM テストの assertion が曖昧
expect(['UTF16LE', 'UTF16BE']).toContain(result.encoding);encoding-japanese の内部マッピングに依存した「どちらでも通る」assertion になっており、ライブラリのアップデートで silent に挙動が変わる可能性があります。hasBom: true の確認のみに留めるか、ライブラリ挙動をピン止めするテストとして明示コメント付けると堅牢です。コメントで lib 挙動を説明している点は good ですが、テストとしての契約が緩いのが気になります。
4. parseQrString の lastIndexOf('|') の前提コメント
Ed25519 base64url 署名は
|を含まない
実装上は ECDSA P-256 + base64url なので「base64url 文字集合(A-Za-z0-9_-)に | は含まれない」が正確な前提になります。parseQrString の JSDoc に 「signature には | が含まれない(base64url 仕様)前提」 と一行追記すると将来読む人に親切です。
5. serializeTicket と buildPayload の重複
serializeTicket は buildPayload の単純な delegate です。長期的にはどちらか一方に統一する方針があると良いです(例: buildPayload を private 化、serializeTicket を公開 API として残す等)。ただし破壊的変更なので 本 PR ではそのままで OK、フォローアップ issue 候補として記録するのを推奨。
6. parseQrString が現時点で未使用
verifyTicket は依然として自前で rawData.split('|') していて、新規 export した parseQrString を使用していません。今 PR では「対称ペアの整理」として理解できるので問題なしですが、verifyTicket を parseQrString ベースに書き換えるフォローアップがあると整理が完結します。
7. download.ts の onerror パスのテスト
jsdom で Image.onerror を再現するのは難しいので必須ではありませんが、URL.revokeObjectURL が onerror でも呼ばれることを spy で確認するテストがあると完璧でした。優先度は低いです。
8. 1 PR / 3 改修の構成
issue #169 の項目 1〜3 を 1 PR に統合した点は意図として理解できますが、「sanitizeField の throw 化」は単独でレビュー単位として独立性が高かったかなと感じました。次回以降、API 表面が変わる項目は分割した方がレビュー観点を集中させやすそうです(マージブロッカーではありません)。
まとめ
- コード品質・テスト品質ともに高く、項目 1〜2 は LGTM。
- 項目 3 の
sanitizeFieldthrow 化に関して UI 側の補助 validation か、エラーメッセージの専用化を検討してください(質問 1)。 - PR description / decisions.md に破壊的変更を明記してください(質問 2)。
- 以上はマージブロッカーではなく、「マージ前に方針合意 or フォローアップ issue 化」のいずれかで対応可能です。
Generated by Claude Code
Generated by Claude Code
- parseQrString に signature が `A-Za-z0-9_-` のみで `|` を含まない前提と
lastIndexOf('|') で境界を一意特定できる根拠を JSDoc で明文化
- verifyTicket 内の rawData.split('|') を parseQrString(rawData) 呼び出しに置換し、
serializeTicket / parseQrString の対称ペアに verifyTicket を一本化
- 公開 API の戻り値構造・エラーメッセージは完全に維持
- 既存ユニットテストはすべて緑のまま動作
|
レビューありがとうございます。マージブロッカーではないとの評価でしたが、要対応 2 件と nit を以下のように振り分けて対応しました。 反映済み(本 PR 内追コミット)
別 PR で対応
別 issue 起票
検証は `npm run test` 473 件緑 / `astro check` 0 errors / `npm run test:e2e` 142 passed (クリーンラン)。再確認お願いします。 |
fumtas1k
left a comment
There was a problem hiding this comment.
再レビュー (head: 1760704)
前回レビューで指摘した重要 (a)(b) と nit 6 件への対応を確認しました。総じて丁寧な対応で、追加修正もペアの対称性が完結しており品質が向上しています。マージ可能 と判断します。残課題は軽微なフォローアップ提案のみです。
解消状況の評価
重要 (a) — sanitizeField throw 化の UX 劣化 → フォローアップ deferral
- PR description で「PR #221 (QrTicket 3 hook 分割) merge 後のフォローアップ PR で対応」と明示。
QrTicket.tsx衝突回避の判断は妥当です。 - ただし: フォローアップ専用 issue がまだ起票されていません (
QrTicket sanitize/sanitizeField/パイプ UX等で検索しヒットなし)。docs/shared-agent-rules.md6.4 章「先送り時は必ず issue 化」に沿うため、PR #221 merge 待ちの追跡 issue を 1 件起票しておくことを推奨します (タイトル例:fix(QrTicket): | 含有時の専用エラーメッセージ追加 (PR #221 merge 後))。
重要 (b) — 破壊的変更の文書化 → docs/decisions.md [060]
- 背景 / 決断 / 影響・移行 / 却下した選択肢 / 関連 PR・issue の 5 セクション構造で、既存 [058] [059] と整合的。silent 置換維持・別文字置換・エスケープ方式を理由付きで却下している点も網羅的で、後から経緯を追える資産になりました。
- PR description の「
⚠️ Migration note」セクションも目立つ位置に配置されており、読み手への配慮として完璧です。
nit 6 件 → #222 で起票
- #222 起票済みを確認。encoding test assertion / serializer 統一 /
download.ts onerrorテストの 3 件が内容含めて適切に整理されています。残りの 3 件 (parseQrString と verifyTicket の重複 / serializeTicket と buildPayload の重複等) は本 PR 内の追加修正 (commit1760704) で実質解消済みのため、#222 の 3 件で track 範囲は十分です。
追加修正の精査
verifyTicket の parseQrString ベース化 (commit 1760704)
挙動同等性を確認しました:
| 観点 | 旧実装 | 新実装 |
|---|---|---|
| フィールド数 6 の検証 | parts.length !== PAYLOAD_FIELD_COUNT |
parseQrString 内で payloadParts.length !== PAYLOAD_FIELDS.length (= 5) + signature 非空チェック |
| signature 空ガード | なし (旧コードは !s チェックで間接的に検出) |
parseQrString で if (!signature) return null の早期 return + verifyTicket 内 !s チェック残置で二重防御 |
| エラーメッセージ | 同一 (QRデータの形式が不正です) |
同一 |
→ 挙動は等価かつ若干堅牢化 しており、serializeTicket (内部 buildPayload) ↔ parseQrString の対称ペアが完結。設計改善として優秀です。
parseQrString JSDoc 拡充
ECDSA P-256 + base64url の文字集合 (A-Za-z0-9_-) が | を含まないため lastIndexOf('|') が一意境界を返す、という仕様前提が明記されました。将来 qr-ticket.ts を触る人が lastIndexOf の正当性を一目で理解できます。完璧。
軽微な提案 (任意・後追い可)
PAYLOAD_FIELD_COUNTが dead export 化:verifyTicketのリファクタにより、リポジトリ全体検索で実コード参照は 0 件になりました (qr-ticket.ts 内 JSDoc コメントの記述のみ)。export 維持コストは微小ですが、#222 の「serializer 統一」と合わせて削除候補になります。- UX 改善 follow-up issue の起票: 上記 (a) 解消状況の通り、PR #221 merge 後の作業として 1 件起票しておくと追跡漏れが防げます。
検証結果
npm run test: 473 件全件緑astro check: 0 errors / 0 warningsnpm run test:e2e: 142 passed / 1 skipped (42.8s)
すべてクリーンラン。前回指摘事項は本 PR 内対応分・別 issue track 分ともに解消済みで、マージ進めて問題ありません。お疲れさまでした。
Generated by Claude Code
Generated by Claude Code
|
再レビューありがとうございます。残課題 2 件を別 issue で track する形で対応しました。
検証結果 (473 件緑 / astro check 0 / e2e 142 passed) は維持されており、ご指示の通り merge を進めて問題ない状態かと思います。 |
* docs(rules): shared-agent-rules.md に Tailwind v4 variant 警告と subagent 運用補足を昇格 agent-lessons.md からの規約昇格: - 7.1 章 (新設): @layer components 内手書き class は hover:/focus: variant 非対応の警告。 silent regression 事故 (PR #277) と検証手順を明示。 - 6.7 章 (新設): subagent 運用補足。完了報告は項目別ステータス必須 (PR #218 事例) / package.json 変更時 lock 同期確認 (PR #181 事例)。 これで agent-lessons.md の該当 lesson を本ファイルへ集約し、agent-lessons.md は 継続検討中の lesson のみに整理する基盤を作る。 * docs(lessons): agent-lessons.md から完了済 / memory 重複 6 lesson を削除 shared-agent-rules.md 11 章「教訓の運用」に従い、規約昇格済 / 完了済 / Claude memory に集約済の lesson を削除。継続検討中の 6 lesson のみに整理。 削除した 6 lesson: - [2026-05-07] Tailwind v4 @layer components hover variant 非対応 → shared-rules 7.1 章へ昇格 - [2026-05-01] subagent isolation:"worktree" 必須 → memory feedback_worktree_and_isolation に集約 - [2026-05-01] worktree 古い node_modules で E2E timeout → 後続 [062] で廃止確認済 (scripts/agent-worktree-setup.sh 削除) - [2026-05-01] PR 本文同期は親 → memory feedback_subagent_workflow (現 shared-rules 6.6) に集約 - [2026-05-01] worktree 内部 branch 取り違え → memory feedback_worktree_merge_order に集約 - [2026-05-02] subagent 絶対パス → memory feedback_worktree_and_isolation に集約 保持する 6 lesson: - [2026-04-28] QRチケット 160px (将来検討事項あり) - [2026-05-01] devDependency lock 同期 (規約昇格候補、但し現状 subagent 完了確認で十分対応可) - [2026-05-02] React effect/memo 矛盾指示 (プロンプト設計 lesson) - [2026-05-02] subagent 完了報告漏れ (規約昇格候補) - [2026-05-04] memory dir Bash rm 不可 (Claude Code harness bug、回避不能) - [2026-05-04] sandbox profile mirror 仮説 (未確認) * docs(claude): CLAUDE.md の PR 4 点必須を要約に圧縮し playbook を SoT 明示 旧表記の sub-bullet 詳細 (pre-create check 3 つを各行で展開等) は pr-creation.md 3 章と shared-agent-rules.md 6.x 章で完全カバー済のため、CLAUDE.md は要約 4 点と 正本 pointer に圧縮。同内容の二重管理による drift を防止。 * docs(rules): PR #296 review 指摘 3 件を反映 - 7.1 章末尾: markdown content scan 副次発見 (hover:bg-blue-50 等の utility 名リテラルを docs/ 配下から拾って unused utility が build CSS に混入する リスク + src/ コメント内では分割記述する) を追記。 - 6.7 章: 「PR 本文の更新は親で実行」 bullet を追加。gh pr edit --body-file は ask permission で subagent から非対話 deny される (PR #189 事例)。 - 6.3 章: 「main 向けはリリース PR のみ」を明記。release-only branch policy。 PR #296 review (Conditional Approve) で指摘された 4 件のうち #2 / #3 / #4 の対応。#1 (PR description 数字) は別途 gh pr edit で対応。
概要
#169 のうち項目 1〜3 を対応。項目 4 (qrcode-generator 副作用 import 解消) は #216 として独立起票済み。
改修内容
項目 1: ロジック層テスト拡充
項目 2: `download.ts` onerror 漏れ修正
`src/utils/download.ts` `downloadPngFromSvgElement` の `img.onerror` で `URL.revokeObjectURL` を呼ぶハンドラ追加。`svgContentToPngBlob` との整合確保。
項目 3: `qr-ticket.ts` シリアライザ整理⚠️ 破壊的変更を含む
`src/utils/qr-ticket.ts`:
`sanitizeField` の挙動変更により、`|` を含む値が `serializeTicket` / `parseQrString` 経由で渡された場合、従来は半角スペースに silent 置換されていたところが throw に変わる。
レビュー反映履歴
97748c39e2bc8f985700d4904b02724384632c00e373ae4531760704スコープ外
検証
関連 issue