Repository navigation
test(checkout): 受注通知の 4 経路の応答の表と、自社 URL の判定の表を固定する (PR-12) - #1095
Open
cipherwebllc wants to merge 3 commits into
Open
cipherwebllc wants to merge 3 commits into
cipherwebllc wants to merge 3 commits into
Conversation
第 10 回レビューの F-01・F-02 の前段。この後の PR-13 (受注通知の状態と effect を hook へ出す) と PR-14 (自社の notify URL の解析を 1 つにする) が、このテストを 1 字も変えずに通せるように、いまの components/CheckoutForm.tsx の受注通知の動きを characterization test で固定する。production のコードは変えない。 固定したもの - 応答の表。経路 A (完了の通知)・B (前の会計の注文)・C (束縛した注文)・D (通常決済の部分の通知) のそれぞれに、200・409 processing・422・429・5xx・JSON でない本文・通信の失敗などを返し、fetch の回数・時刻 (再試行の間隔)・宛先・method・本文と、描かれる文・role・リンク・ボタン、sessionStorage の控えとメモ、success_url への移動、logger の記録を見る。 - 自社 URL の判定の表。判定している所ごとに、h が 1 つ・無い・2 つ・空・大文字・@ つき・別のお店・別の origin・別の path・読めない URL などで、送るか・どこへ送るか・何を出すかを見る。所によって方針が違う所は、違いのまま書く。 - 「自動登録できませんでした」の案内 (state の名前は orderNotifyTooOld) が出る条件。経路 A・D では 422 tx_too_old のときだけ、経路 C では登録を確かめられなかった応答すべてで出て、登録できれば消える。 mock は境界だけ (wagmi・fetch・時計・logger・支払いの hook・経路を決める関数)。受注通知の状態・effect・URL の解析と、lib/orderDelivery・lib/orderBind は実物。時計は偽物で、本物の timer と network は使わない。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…ount 後の片づけを固定する PR-12 のレビューの指摘への対応。production のコードは変えない。 - 経路 A・D の失敗の記録 (logger.warn) は、宛先の hash の WebCrypto の digest を待ってから出る。digest は偽の時計と関係なく終わるので、時計を進めるだけの待ち方では、digest が遅い環境で記録を見る test が空振りし、遅れた記録が次の test にまたがる。digest の呼び出しを境界で数え (中身は実物)、settle と afterEach が終わりを待つ形にした。記録を見る test・見ない test の両方に効く。 - 経路 C・B で、応答を待つ間に画面を離れた (unmount) 後に応答が届いたときの、sessionStorage の控えとメモの片づけを足した。 - 経路 A の success_url への移動が、確定からちょうど 3 秒後 (2,999 ms ではまだ・3,000 ms で移る) であることを足した。経路 C も登録からちょうど 3 秒後にした。 - 経路 A・D の額の 4 つの欄が、払うボタンを押した時点の値であることを足した (確定と同じ描画で会計の額やガス代の見積もりが変わっても、今の値に置き換えない)。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
PR-12 のレビュー (2 回目) の指摘への対応。production のコードは変えない。 - digest の終わりを待つ helper は、終わらない digest があると止まり、afterEach が timeout して、location・timer・spy の復元の前で終わっていた。数えていた digest も残るので、関係のない次の test まで落ちていた。 - 待ちに期限 (本物の時計で 2 秒) を付けた。期限までに終わらない digest があれば、その test を「crypto.subtle.digest が 2000 ms で終わらなかった」の文で落とし、数えていた分は捨てる。黙って先へは進まない。event loop が止まっていただけの場合に誤って落とさないよう、期限の後に 1 巡だけ流してから数える。 - afterEach の片づけ (数えていた digest を空にする・location・timer・spy の復元) を try / finally で無条件にした。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
何をするか
第 10 回レビューの Phase 3 の 1 本目 (PR-12・F-01 と F-02 の前段)。テストだけの PR で、production のコードは 1 字も変えていません。
この後の PR-13 (受注通知の状態と effect を
hooks/useCheckoutOrderNotify.tsへ出す) と PR-14 (自社の notify URL の解析を 1 つの関数にする) が、このテストを 1 字も変えずに緑のまま通せるように、いまのcomponents/CheckoutForm.tsxの受注通知の動きを characterization test で固定します。tests/components/CheckoutForm.orderNotify.characterization.test.tsx(test 350 個・単独で約 17 秒)window.location.assign・logger) だけです。部品の中の state や関数の名前には触れていません。レビューへの対応 (2 つ目・3 つ目の commit)
Codex のレビューの MUST 3 件・SHOULD 2 件に対応しました。テストと、この本文だけを直しています (production のコードは変えていません)。
logger.warn) は、宛先の hash のcrypto.subtle.digestを待ってから出ます (lib/telemetryRedaction.ts:93・components/CheckoutForm.tsx:914・:924・:1199・:1209)。digest は偽の時計と関係なく終わるので、時計を進めるだけの待ち方では、digest が遅い環境で空振りします。digest の呼び出しを境界で数え (中身は実物のまま)、settle()とafterEachが終わりを待つ形にしました。待つ所を 1 か所 (settle()) に寄せたので、記録を見る test にも、見ないが後から記録が出る test にも効きます。同じ形はレビューの 2 本だけではありませんでした: digest を 250 ms 遅らせると、直す前は 342 個のうち 68 個が落ちました (経路 A・D の応答の表が 55 個・D の後に A が続く test が 3 個・記録の中身の test が 4 個。残りの 6 個は経路 C・B の test で、前の test の遅れた記録が次の test にまたがって落ちたもの)。直した後は、同じ遅い digest で 350 個すべて通ります (下の「変異で落ちることの確認」)。:1039-1040)・terminal への書き換え (:1055-1056)・メモの削除 (:1046) を行います。経路 C に 3 行 (登録できた・打ち切り・再試行できる)、経路 B に 1 本の test を足し、本文の「固定できなかった動き」を直しました。2 回目のレビュー (3 つ目の commit)
afterEachが timeout して location・timer・spy の復元の前で終わっていました。数えていた digest も残るので、関係のない次の test まで落ちていました。待ちに期限 (本物の時計で 2 秒・test の既定の timeout 5 秒より短い) を付け、期限までに終わらない digest があれば、その test を「crypto.subtle.digest が 2000 ms で終わらなかった (N 件)」の文で落とします (黙って先へ進みません)。数えていた分はそこで捨てます。afterEachの片づけは try / finally で必ず走ります。確認の結果は下の「変異で落ちることの確認」の表です。洗い出した経路
受注通知を送る経路 = 4 つ (計画の「4 経路」と同じ)
components/CheckoutForm.tsx:801-977(送信:891-902)webhookの文字列そのまま:855-890)backgroundOrderDeliveriesに確定済みの控えがある:981-1018(送信:999-1001)/api/order/notify?h=<handle>notifyBodyそのままorderDelivery) が確定:1022-1071(送信:1032-1035):1076-1252(送信:1177-1187)webhookの文字列そのまま:1152-1176・feeUncollected: true)4 つとも
postOrderNotify(:141-159) を通り、409 で本文が{ error: 'processing' }のときだけ、同じ本文を 250ms → 1s → 4s → 15s → 60s → 60s の間隔で送り直します (:123-125)。計画 (verified.md の F-01) は「
setOrderNotifyTooOldを呼ぶ effect は 3 つ確かめた・4 つ目は実装の前に特定する」としていました。数え直した結果: 通知を送る effect は 4 つ、そのうちorderNotifyTooOldに触るのは A・C・D の 3 つだけです。4 つ目の B はorderNotifyTooOldを使わず、自分の state (previousOrderNotice) で小さな案内 (role=status) を出します。「自社の notify URL か」の判定 = CheckoutForm の中に 5 か所 (実装は 3 つ) + lib に 1 つ
計画の「3 経路」は、強さの違う実装の数としては合っています。使っている所は 5 か所で、ほかに lib の照合が 1 つあります。
:445-446isOwnOrderNotifyUrl(:161-169・origin と path) + JPYCsendsOrderNotify):827isOwnOrderNotifyUrl:2152isOwnOrderNotifyUrl+ relay:1099-1112storeHandleと大文字小文字を除いて同じ):1448-1450lib/orderDeliveryContext.ts:22-25(CheckoutForm.tsx:268から呼ぶ)経路 B と、この画面で払った経路 C は、URL の
webhookを見ません (保存した handle から宛先を組みます)。固定した表
応答の表 (経路 × 応答)
応答ごとに、fetch の回数と時刻 (最初の送信からの ms)・宛先・method・headers・
mode・keepalive・本文、描かれる文と role、sessionStorage、success_urlへの移動、logger.warnを見ています。経路 A・D (18 行 × A の 2 経路 + D の 2 経路)。見える結果は 2 つだけです。
{ ok: true }・200 本文なし・200{ ok: true, bindingConflict: true }success_urlへ 3 秒で移る (notify の応答を待たない)。success_urlへ移らない。logger.warnだけです。success_urlへの移動はどの応答でもありません。経路 C (21 行 × 「読み直して復元」と「この画面で払った」)。見える結果は 3 つです。
{ ok: true }・200{ ok: true, duplicate: true }{ ok: true, bindingConflict: true }・200{ ok: false }・200 本文なし・200 JSON でない本文{ bindingConflict: true }success_urlへ移ります (復元した注文は移りません)。notify-pendingのまま。押すと同じ本文をもう 1 回送り、送っている間はボタンを消して案内は残します。登録できたら案内が消え、隠れていた大きな完了画面が戻ります。terminalにします。経路 B (18 行)。今の会計には触れず、小さな案内 (role=status) だけが変わります。
{ ok: true }・409 processing → 200notify-pending){ ok: true, bindingConflict: true }・200{ ok: false }・200 本文なし・200 JSON でない本文・409 JSON でない本文・409 reserved_order・422 tx_too_old・422 JSON でない本文・400 invalid_handle・503{ bindingConflict: true }terminal)自社 URL の判定の表 (URL の形 × 判定している所)
18 個の URL の形 × 8 つの見方 (A は gasless と通常決済の 2 つ)。方針が違う所は、違いのまま固定しています。
?h=alice)?h=ALICE)?h=%40alice)?h=bob)?h=)?h=alice&h=bob)/?h=ALICE&utm=xの URL でも?h=aliceへ)。lib/urlの parse (sanitizeUrl) を通った値では起きない形です。部品の境界の動きとして固定しています。orderNotifyTooOldという名前の state が実際に表すもの「支払いは確認できましたが、注文を自動登録できませんでした。…」の案内 (role=alert) が出る条件を、経路 × 応答の 21 行で固定しました。
{ error: 'tx_too_old' }のときだけ。success_urlの案内と移動を止めます。名前は too_old ですが、実際は「この画面の注文の登録を確かめられていない」です (PR-13 が
orderNotifyFailedに直す予定)。テストは名前に依存していません。ほかに固定したもの
amount・merchantAmount・feeAmount・customerPaysは、払うボタンを押した時点の値です。確定と同じ描画で会計の額が変わっても (3000 → 5000 JPYC)、ガス代の見積もりが変わっても (USDC・0.1 → 0.3)、今の値に置き換えません。logger.warnは status と、宛先の origin・短い hash だけ (URL の query に載った秘密は出さない)。経路 C は status だけ・通信の失敗は理由の符号だけ。経路 B は何も出しません。checkout.successとcheckout.order.delivered_fee_uncollectedは 1 回ずつ。mock の境界
wagmi・fetch(vi.spyOn(globalThis, 'fetch'))・時計 (vi.useFakeTimers)・@/lib/logger・支払いの hook (useBatchPayment・useStandardPayment・useJpycEip3009Payment・useSmartAccount・useGasQuote*・useRelayHealth)・経路を決める env 由来の関数 (resolveJpycGaslessProvider・resolveUsdcGaslessProvider・jpycForwarderFor・resolvePaymasterMode)・@/lib/envの 2 つの flag・next/navigation・ConnectButton(stub)・window.location.assign。兄弟のtests/components/CheckoutForm.test.tsxと同じ境界です。lib/orderDelivery・lib/orderDeliveryContext・lib/orderBind・lib/paymentFlowGuards・lib/paymentRoute、sessionStorage (jsdom)。hooks/useCheckoutOrderNotify.tsとlib/order/ownNotifyUrl.tsは、import も mock もしていません。Responseで、送信の時刻は偽の時計で測っています。crypto.subtle.digestは中身を実物のまま、呼び出しだけを数えています (vi.spyOnで包み、返ってきた promise の終わりをsettle()とafterEachが待つ)。偽の時計では進まない本物の非同期を、決まった所で待つためです。待ちには期限 (本物の時計で 2 秒) があり、期限までに終わらなければその test を落とします。PR-13・14 で受注通知を hook や lib へ出しても、digest を呼ぶ所は変わらないので、この境界は変えずに済みます。変異で落ちることの確認
production のコードに 1 か所ずつ変異を入れ、新しいテストが落ちることを確かめて
git checkoutで戻しました (変異は commit していません)。T01・経路 CT02・定数T03)・4 秒にする (T04)、経路 A の額の 4 つの欄を今の値にする (N01〜N04)、経路 D の同じ 4 つ (N05〜N08)。対応の前は、秒読みを 1 秒にする変異と、経路 A の額を今の値にする変異が生き残っていました (Codex の指摘)。from)。git checkout -- <file>で戻し、git status --porcelainが空であることを確かめています。最後の作業ツリーは clean です。遅い digest (テスト側の変異):
crypto.subtle.digestの完了を、結果はそのままで 250 ms (本物の時計) 遅らせました。exit=1 / Tests 68 failed / 274 passed (342) / Duration 16.48sexit=0 / Tests 350 passed (350) / Duration 53.43s終わらない digest (テスト側の変異): 経路 A の test 1 つ (「メモは応答が失敗でも消える」) の中だけ、digest が終わらないようにし、関係のない経路 B の test (「確定していない控え (signed) は送らない」) と 2 つ続けて流しました。
exit=1 / Tests 2 failed / 348 skipped (350) / Duration 30.74s(2 つとも落ちる: test の timeout 5 秒 → afterEach の timeout 10 秒 → 次の test も timeout)exit=1 / Tests 1 failed / 1 passed / 348 skipped (350) / Duration 2.86s(digest を止めた test だけが期限の文で落ち、次の test は通る)exit=0 / Tests 350 passed (350) / Duration 53.53s上の 116 個の変異の数字は、2 つ目の commit のテストに対するものです。3 つ目の commit はテストの待ち方だけを変えたので、全部は流し直さず、12 個を抜き取って流し直しました (R01・A03・A11・A20・D05・C03・B05・S01・V01・T01・N04・L01 → 12 個とも落ちる)。
変異の表 (全部)
いまのコードで気づいたこと (直していません。PR-13・14 への申し送り)
どれも、いまの動きのままテストに固定してあります。直すかどうかは PR-13・14 かその後で決めてください。行は
components/CheckoutForm.tsx(main 8c1a317)。所によって方針が違う (F-02 の中身)
:827) と、この画面で払った経路 D (:1102-1105) は URL の文字列そのままへ送ります。relay の束縛 (:1450) は「Checkout URL が不正です」で止めます。同じリンクが、relay なら払えず、Pimlico の gasless・通常決済なら払えて notify も送ります。?h=%40alice): relay の束縛と経路 C の照合は @ を外して alice と読みます。経路 D の復元 (:1108-1109) は小文字にするだけなので一致せず、読み直した後の部分の通知を送りません。?h=bob・storeHandleは alice): relay の束縛は URL の h (bob) で注文を束ねます (:1458・params.storeHandleと照らしません)。storeHandleと照らすのは経路 D の復元だけです。経路 D も、この画面で払ったときはstoreHandleを見ません (:1105)。:445-446) は JPYC の会計だけですが、経路 A (:827) は通貨を見ません。USDC の会計でも、自社の URL なら notify を送ります。new URLが投げ、:1463-1467の catch が「注文の復元情報をこのブラウザに保存できないため…」(保存の文) を出して止めます。理由と文が合っていません。lib/urlのsanitizeUrlを通った値では起きません。mode: 'cors'を付け、経路 B・C は付けません (同一 origin なので結果は同じ)。バグの疑い
sessionStorageのopenpay:order-memo:<受付番号>) は、送った直後に応答を待たずに消します (:943-951・:1221-1229)。通常決済で、部分の通知 (D) が通信の失敗 → 完了の通知 (A) は成功、のとき、メモは D と一緒に消えていて A の本文に載りません。D がサーバーに届いていないので、メモをお店へ届ける道が残りません (サーバー側の扱いはこの PR では見ていません。client が送る本文だけの話です)。経路 A だけのときも、失敗した通知と一緒に消えます (送り直しはありません)。logger.warnだけで、案内も再試行のボタンも出ません。経路 A では、お客様にはそのまま完了が出ます。経路 D だけではまだ完了になりません (利用料の送信が残っていて、画面は利用料の送信中・再試行の欄のまま)。利用料が後で成功したときに完了が出ます。どちらの場合も、お店に注文が届いていないことはお客様に伝わりません。経路 C は同じ応答で案内と「注文登録を再試行」を出します。コメント (:826) のとおりの設計ですが、経路で差があります。「注文状況を見る」も、経路 A では notify の応答を待たずに出ます (:840-843)。503 のときは、リンク先の token がサーバーに無い見込みです。success_urlへの移動が、経路 A の再試行を待たない: 移動は確定の 3 秒後で、409 processing の再試行は 250ms・1.25 秒・5.25 秒・… と続きます。テストは画面が残るので 7 回と固定していますが、実機では移動の後の再試行は走らない見込みです (keepaliveが効くのは送信済みの 1 回)。読まれない state・二重の守り (PR-13 で動かすときの注意)
setBindingStorageError(true)(:1040・:1056) は画面に出ません。案内 (:2142) は!relay.data?.successを求め、経路 C はrelay.data.successが前提だからです。控えを消せない・terminal に書けないときも、見える動きは変わりません (テストで固定)。:912(A の tx_too_old でのsetRedirectIn(null))・:1050の!relay.restoredIntent・:1364-1365のboundOrderRef.current && !boundNotifyAcknowledged・:1013の catch のupdateNotice()・:1106の!!params.storeHandle(外すとundefined.toLowerCase()が try の中で投げて、同じ「送らない」になる)。別の条件が同じ結果を作っています。このテストでは守れていない行なので、PR-13 で触るなら目で確かめてください。:1046)。タブを閉じるまで残ります。orderNotifyTooOld(state:185・messages の key も同じ) は、経路 A・D では名前どおり (422 tx_too_old)、経路 C では「登録を確かめられていない」(:1060・:1066) です。固定できなかった動き
bindingStorageError: 上の 10 のとおり画面に出ないので、「出ない」ことだけを固定しています。:986のpreviousNotificationMountedRef): unmount の後は案内 (state) を更新しない、という守りです。React は unmount の後の更新を捨てるので、守りを外しても DOM にも外への呼び出しにも差が出ず、テストにできません。unmount の後に届いた応答で 外に残る記録 (経路 C・B の sessionStorage の控えとメモ) の方は、テストで固定しました (上の「ほかに固定したもの」)。keepaliveと、移動の後の再試行 (上の 9)。jsdom では画面が残ります。isOrderTokenLike) と「本文・リンク・sessionStorage で同じ値」だけを見ています。orderDelivery・backgroundOrderDeliveriesを返すか: 支払いの hook は境界として mock しています。実物の hook との組み合わせは、兄弟のCheckoutForm.test.tsxのA2c saved-order-only notificationが見ています。この PR が変えないもの
components/・hooks/・lib/・app/)・公開文言・env・依存・既存のテスト。差分は新しい test file 1 つだけです。検証
./node_modules/.bin/tsc --noEmit: exit 0./node_modules/.bin/eslint tests/components/CheckoutForm.orderNotify.characterization.test.tsx: exit 0 (指摘なし)Test Files 1 passed (1)・Tests 350 passed (350)・exit 0・17.12 秒 (期限を付ける前は 17.03 秒)Test Files 927 passed (927)・Tests 25971 passed (25971)・exit 0・88.50 秒 (終わったときの load average 14.46。新しい file は並列の負荷の中でも通りました)next buildと bundle の予算は流していません。npm ci/npm installは実行していません。🤖 Generated with Claude Code
https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc