Skip to content

refactor: ロジック層テスト拡充 + onerror 漏れ修正 + qr-ticket シリアライザ整理 (#169) - #218

Merged
fumtas1k merged 8 commits into
developfrom
feature/refactor-169-logic-tests
May 2, 2026
Merged

refactor: ロジック層テスト拡充 + onerror 漏れ修正 + qr-ticket シリアライザ整理 (#169)#218
fumtas1k merged 8 commits into
developfrom
feature/refactor-169-logic-tests

Conversation

@fumtas1k

@fumtas1k fumtas1k commented May 2, 2026

Copy link
Copy Markdown
Owner

概要

#169 のうち項目 1〜3 を対応。項目 4 (qrcode-generator 副作用 import 解消) は #216 として独立起票済み。

改修内容

項目 1: ロジック層テスト拡充

  • `src/utils/tests/encoding.test.ts`: `detectEncoding` (BOM 検出 / サイズ制限 throw)、`decodeToText` (UTF-8 / ASCII / 日本語)、`convertBytes` (BOM 付き変換 / 往復)、`textToUtf8Bytes` (ASCII / 絵文字 / 日本語) のテスト追加
  • `src/hooks/tests/useClampedInput.test.tsx`: 新規作成(12 ケース、handleChange / handleBlur 周り)
  • `src/hooks/tests/useCodec.test.ts`: 既存 6 → 11 テスト(空入力即時クリア / debounce キャンセル / throw 後 recover)
  • `src/hooks/tests/useQrCamera.test.ts`: 既存 8 → 12 テスト(`getUserMedia` reject 経路の安全性)
  • `qr-reader.test.ts` は既に `canvas.getContext` null パスを含む充実したテストが存在したため追加なし

項目 2: `download.ts` onerror 漏れ修正

`src/utils/download.ts` `downloadPngFromSvgElement` の `img.onerror` で `URL.revokeObjectURL` を呼ぶハンドラ追加。`svgContentToPngBlob` との整合確保。

項目 3: `qr-ticket.ts` シリアライザ整理 ⚠️ 破壊的変更を含む

`src/utils/qr-ticket.ts`:

  • `serializeTicket` / `parseQrString` 対称ペアを公開エクスポート
  • `sanitizeField` を「`|` 含有時 throw する validation」に方針変更(従来のスペース silent 置換から) ⚠️
  • `PAYLOAD_FIELD_COUNT` を `PAYLOAD_FIELDS` 配列の length から導出
  • `verifyTicket` を `parseQrString` ベースに書き換えて対称ペアを完結(commit `1760704`)
  • `parseQrString` の JSDoc に「signature は base64url で `|` を含まない前提」を明記(commit `1760704`)

⚠️ Migration note (破壊的変更)

`sanitizeField` の挙動変更により、`|` を含む値が `serializeTicket` / `parseQrString` 経由で渡された場合、従来は半角スペースに silent 置換されていたところが throw に変わる

レビュー反映履歴

commit 内容
97748c3 encoding.test.ts 拡充
9e2bc8f useClampedInput.test.tsx 新規
985700d download.ts onerror 修正
4904b02 qr-ticket シリアライザ整理(serializeTicket / parseQrString / sanitizeField throw 化)
7243846 useCodec.test.ts 拡充
32c00e3 useQrCamera.test.ts 拡充
73ae453 レビュー反映: decisions.md [060] エントリ追加
1760704 レビュー反映: verifyTicket を parseQrString ベース化 + JSDoc 拡充

スコープ外

検証

  • `npm run test`: 464 → 473 件、全件緑
  • `astro check`: 0 errors / 0 warnings
  • `npm run test:e2e`: 142 passed / 1 skipped、全件緑(クリーンラン 42.8s)
  • `aria-*` / `role=` の削除なし

関連 issue

fumtas1k added 6 commits May 2, 2026 14:00
…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 できる
PR #210 / #196 で AbortSignal 経由のアンマウント race 対策が入っているが、
カメラ取得失敗時 (getUserMedia reject) の hook 挙動は未テストだったため追加:
- NotAllowedError → "カメラへのアクセスが拒否" メッセージ、cameraActive: false
- NotFoundError → "カメラが見つかりません" メッセージ
- その他のエラー → "カメラの起動に失敗" 汎用メッセージ
- getUserMedia 失敗後に unmount しても例外を throw しない
@fumtas1k fumtas1k self-assigned this May 2, 2026

@fumtas1k fumtas1k left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビューありがとうございます。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.tsNotAllowedError / NotFoundError / その他 / unmount 後 throw しない、の 4 ケースで getUserMedia reject 経路が安全に閉じることを保証しているのが良い。
    • encoding.test.tsconvertBytes 往復テスト・絵文字(4 バイト)・日本語といった境界ケースまで揃っている。
  • download.tsonerror 修正 (項目 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, ...)buildPayloadsanitizeField で 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.tspublic 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. parseQrStringlastIndexOf('|') の前提コメント

Ed25519 base64url 署名は | を含まない

実装上は ECDSA P-256 + base64url なので「base64url 文字集合(A-Za-z0-9_-)に | は含まれない」が正確な前提になります。parseQrString の JSDoc に 「signature には | が含まれない(base64url 仕様)前提」 と一行追記すると将来読む人に親切です。

5. serializeTicketbuildPayload の重複

serializeTicketbuildPayload の単純な delegate です。長期的にはどちらか一方に統一する方針があると良いです(例: buildPayload を private 化、serializeTicket を公開 API として残す等)。ただし破壊的変更なので 本 PR ではそのままで OK、フォローアップ issue 候補として記録するのを推奨。

6. parseQrString が現時点で未使用

verifyTicket は依然として自前で rawData.split('|') していて、新規 export した parseQrString を使用していません。今 PR では「対称ペアの整理」として理解できるので問題なしですが、verifyTicketparseQrString ベースに書き換えるフォローアップがあると整理が完結します。

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 の sanitizeField throw 化に関して UI 側の補助 validation か、エラーメッセージの専用化を検討してください(質問 1)。
  • PR description / decisions.md に破壊的変更を明記してください(質問 2)。
  • 以上はマージブロッカーではなく、「マージ前に方針合意 or フォローアップ issue 化」のいずれかで対応可能です。

Generated by Claude Code


Generated by Claude Code

fumtas1k added 2 commits May 2, 2026 14:48
PR #218 で実施した「sanitizeField を silent 置換から throw に変更」が
外向き挙動の破壊的変更であるため、背景・決断・影響・却下案を decisions.md に
記録する。UX 観点の専用エラーメッセージ化は PR #221 merge 後の
フォローアップ PR で対応する旨も明記。
- parseQrString に signature が `A-Za-z0-9_-` のみで `|` を含まない前提と
  lastIndexOf('|') で境界を一意特定できる根拠を JSDoc で明文化
- verifyTicket 内の rawData.split('|') を parseQrString(rawData) 呼び出しに置換し、
  serializeTicket / parseQrString の対称ペアに verifyTicket を一本化
- 公開 API の戻り値構造・エラーメッセージは完全に維持
- 既存ユニットテストはすべて緑のまま動作
@fumtas1k

fumtas1k commented May 2, 2026

Copy link
Copy Markdown
Owner Author

レビューありがとうございます。マージブロッカーではないとの評価でしたが、要対応 2 件と nit を以下のように振り分けて対応しました。

反映済み(本 PR 内追コミット)

  • 質問 2(破壊的変更の明示): `docs/decisions.md` に [060] エントリを追加し、PR description にも Migration note セクションを目立つ形で追加しました(commit `73ae453`)
  • nit 4(`parseQrString` の JSDoc): signature は ECDSA P-256 + base64url で `|` を含まない前提を JSDoc に明記しました(commit `1760704`)
  • nit 6(`verifyTicket` を `parseQrString` ベースに書き換え): 自前 `split('|')` を新ペア利用に置き換えて対称ペアを完結させました。既存テスト(パイプ数不一致 / timestamp 数値でない 等)が同じエラーメッセージを返すことを確認しました(commit `1760704`)

別 PR で対応

別 issue 起票

検証は `npm run test` 473 件緑 / `astro check` 0 errors / `npm run test:e2e` 142 passed (クリーンラン)。再確認お願いします。

@fumtas1k fumtas1k left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

再レビュー (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.md 6.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 内の追加修正 (commit 1760704) で実質解消済みのため、#222 の 3 件で track 範囲は十分です。

追加修正の精査

verifyTicketparseQrString ベース化 (commit 1760704)

挙動同等性を確認しました:

観点 旧実装 新実装
フィールド数 6 の検証 parts.length !== PAYLOAD_FIELD_COUNT parseQrString 内で payloadParts.length !== PAYLOAD_FIELDS.length (= 5) + signature 非空チェック
signature 空ガード なし (旧コードは !s チェックで間接的に検出) parseQrStringif (!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 の正当性を一目で理解できます。完璧。


軽微な提案 (任意・後追い可)

  1. PAYLOAD_FIELD_COUNT が dead export 化: verifyTicket のリファクタにより、リポジトリ全体検索で実コード参照は 0 件になりました (qr-ticket.ts 内 JSDoc コメントの記述のみ)。export 維持コストは微小ですが、#222 の「serializer 統一」と合わせて削除候補になります。
  2. UX 改善 follow-up issue の起票: 上記 (a) 解消状況の通り、PR #221 merge 後の作業として 1 件起票しておくと追跡漏れが防げます。

検証結果

  • npm run test: 473 件全件緑
  • astro check: 0 errors / 0 warnings
  • npm run test:e2e: 142 passed / 1 skipped (42.8s)

すべてクリーンラン。前回指摘事項は本 PR 内対応分・別 issue track 分ともに解消済みで、マージ進めて問題ありません。お疲れさまでした。

Generated by Claude Code


Generated by Claude Code

@fumtas1k

fumtas1k commented May 2, 2026

Copy link
Copy Markdown
Owner Author

再レビューありがとうございます。残課題 2 件を別 issue で track する形で対応しました。

検証結果 (473 件緑 / astro check 0 / e2e 142 passed) は維持されており、ご指示の通り merge を進めて問題ない状態かと思います。

@fumtas1k
fumtas1k merged commit d03c6f7 into develop May 2, 2026
2 checks passed
@fumtas1k
fumtas1k deleted the feature/refactor-169-logic-tests branch May 2, 2026 08:07
fumtas1k added a commit that referenced this pull request May 2, 2026
* docs(lessons): 2026-05-02 セッションで得た 3 教訓を追記

- effect 依存配列を一次入力に切り替える指示が eslint-disable を生んだ件 (PR #217)
- subagent isolation worktree からの Edit は worktree 配下の絶対パスが必須 (PR #219)
- サブエージェントがスコープ箇条書きの一部のみで完了報告する件 (PR #218)

* docs(lessons): #220 review 反映 — 親プロンプト責任を強調 + 関連 commit 補注
fumtas1k added a commit that referenced this pull request May 7, 2026
* 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 で対応。
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.

1 participant