JSONを出力し、新しいブラウザ環境で読み込み、リロードしても元のレコードが残る。ここまで通っても、元の金額の計算が正しいとは限りません。
公開OSSのNestSave Savings Plannerを変更せずローカルで検査したところ、次の二つが同時に成立しました。
- 目標・入出金履歴・支出・予算・設定のバックアップ往復はPASS。
- 新規目標の「Already Saved」に5,000を入れると、目標残高は10,000になる。履歴は5,000の1件だけ。
この記事では、この結果を一つの「テスト成功」にまとめず、保存の検査と仕様の検査に分けた方法を説明します。
AI支援で運用しているaichanceの実験です。合成データだけを使い、対象アプリの98個の追跡ファイルは変更していません。作者による確認や公開サービスの不具合を主張するものではありません。
この検証に使ったOSSはApp Crash Labです。自分のアプリで試すなら、バックアップ用の導入プロンプトを普段のコーディングAIへ渡し、保存と実JSONの復元を検査する設定を作れます。Node22とnpmが必要です。テストランナー自体はLLM・APIキーを使わず、結果を単独Playwrightテストとして残せます。
同じ入力操作で、5,000と0を比べる
対象はNestSaveのこのコミットです。macOS、Node22.16.0、Playwright1.64.0のChromiumで、別々の新しいブラウザコンテキストを使いました。サービスワーカーと外部へのリクエストは止めています。
通常の画面から購入目標を作り、目標額75,000、初期貯金5,000を入力します。日程・積立・収支の設定を進めて確定すると、目標画面は次の状態になりました。
| 初期貯金の入力 | 作成後の目標残高 | 入金履歴 | 判定 |
|---|---|---|---|
| 5,000 | 10,000 | 1件、合計5,000 | 期待する5,000と不一致 |
| 0 | 0 | 0件 | 一致 |
画面だけでなく、既存のIndexedDBを読み取り専用トランザクションで確認しました。目標レコードも10,000、入金レコードの合計は5,000です。表示の整形だけの差ではありませんでした。
コードでは、目標作成時にcurrentAmountへ初期貯金を入れ、作成後に同額の初回入金を追加しています。入金の保存処理は現在残高に金額を加算するため、二重計上になっていると考えられます。これは固定した版についての原因の推定です。
バックアップの比較がPASSになる理由
別の検査では、同じ種類の目標に加え、追加入金、支出、月次予算、設定を通常の画面で作りました。App Crash Lab0.9で実際のJSONをダウンロードし、その2,112バイトを変更せず、新しいブラウザコンテキストに渡して復元します。
検査するのは次の五つです。
- 目標の完全なレコード。
- 入出金履歴の完全なレコード。
- 支出の完全なレコード。
- 予算の完全なレコード。
- 設定の値全体。
ID、金額、メモ、日付などを含め、出力ファイル、復元直後、移動後、リロード後で比較しました。動的IDのある四つのストアは、同じIDを持つ完全なレコードの集合として比較します。外側の配列の並び順だけを比較対象から外し、IDや入れ子の配列順序は保持します。
この比較はPASSでした。しかし「計算結果が元から間違っていて、それが正確に保存された」場合もPASSになります。保存前と保存後の一致だけでは、初期貯金5,000という入力と計算結果の関係は保証できません。
バックアップ用の設定は、観測した10,000、追加入金後の12,000を保持するかを調べています。その値を正しい計算結果として認定したわけではありません。
また、新しいコンテキストは完全な未初期化状態ではありません。通常の設定画面で初期の収支設定を保存し、その値と、四つの業務ストアが空であることを、設定ファイルに宣言してランナーで確認してから読み込みます。復元元の目標や履歴を事前に注入してはいません。空欄の任意項目がIndexedDBにundefinedとして保存される経路は、このJSON検査ではINCONCLUSIVEです。
修正後に緑になるテストを残す
算術の再現テストでは、観測した10,000を期待値にしません。入力5,000なら、保存された目標残高も画面の金額も5,000であることを期待します。
expect(actual.transactionSum).toBe(initial);
expect(actual.amount).toBe(initial);
initialを5,000と0にした二つのケースを実行します。固定した版では前者が失敗し、後者が通ります。固定した版での終了コード1は、5,000の期待値に一致しないためで、起動エラーではありません。修正後は二つとも通る設計です。完全な操作手順と保存値の読み取りは単独で動くPlaywrightテストにあります。
再実行用のセットには、固定したアプリの起動手順、算術テスト、バックアップ往復のJSON設定、検査の範囲をまとめました。算術テストはPlaywrightだけで実行でき、App Crash Labのインストールは不要です。バックアップ設定から生成したテストも、Playwrightだけの別フォルダで2件とも通ることを確認しました。
ブラウザ内のJSONバックアップを持つ自分のアプリで試すなら、バックアップ用の導入プロンプトから、普段使うコーディングAIに設定作成と実行を頼めます。テストランナー自体はLLMもAPIキーも使いません。自分のアプリで使ったPASS/FAILの要約や導入で止まった箇所は、実データを除いてGitHubへ寄せてもらえると改善に使えます。
検査済みなのは選択した五つの領域です。エクスポートに含まれない連続利用日数・バッジ、作成していないマイルストーン、暗号化バックアップ、Android版、電源断は対象外です。通常の再読込レポートには保存値やトレースが含まれるため、実データへ適用したものをそのまま公開しないでください。
追記:値の一致と、画面で使えることを分ける(2026-10-16)
その後、変更していない別のローカルアプリでも、検査対象を分ける必要がありました。
| 固定したアプリ | 実測したこと | 残した期待値 |
|---|---|---|
| VoxelDraftの取得時スナップショット | JSON復元でブロックは3個のまま。座標と色も同じだが、2個の所属レイヤーが変化 | 元の所属レイヤーを維持する |
Repwise 4032141
|
完了した運動記録のJSON往復はPASS。一方、復元前からHistoryに表示されない | 完了した記録のタイトルが1件表示される |
VoxelDraftの再実行用セットは、1レイヤーにだけブロックを置いた対照がPASS、2レイヤーへ配置した検査がFAILでした。圧縮されたファイル表現を扱うため、この往復検査は汎用JSON設定とは別のPlaywrightテストです。レイヤーを平坦化することが意図した仕様かは未確認です。
Repwiseの二つの設定と起動手順では、通常のゲスト画面で1セット12.5 kg×8回を完了し、保存レコードのisCompleted: trueとtotalVolumeKg: 100を確認しました。App Crash Lab 0.9は選択した6ストアの完全なレコードを、実ファイル・新しい環境への復元・再読み込みで比較してPASS。一方、Historyの表示件数は期待1に対して0なので、表示を含む検査は開始時点でINCONCLUSIVEです。既に表示されない状態から、再読み込みで壊れたとは判定しません。初期の種目・設定ストアは、この汎用設定の比較対象外です。
元の値の意味、出力ファイルによる保全、画面での表示を別々に観測すると、直すべき箇所を絞れます。失敗した期待値はそのまま残し、将来の更新で通るかを同じ手順で確かめます。いずれも合成データによるこちらのローカル実測で、作者の実行報告や修正確認ではありません。
追記:キーを値とは別に渡すIndexedDBの検査(0.10)
同じJSON保存でも、IndexedDBのオブジェクトストアがkeyPath: nullで、値とは別にキーを渡すアプリがあります。App Crash Lab 0.10ではkeyMode: "out-of-line"を明示し、外側の主キーとレコード自身のidが同じ型・値であることを確認して、完全なレコードを比較できます。IDのない値や一致しない主キーを、比較しやすい値へ変換して通す仕組みではありません。
変更していないMy Ideals a65fb102では、通常の画面から合成プロフィールを二つ作り、実際の「Export All Profiles」で出た1,965バイトのJSONを空の環境へ復元しました。両プロフィールのID、名前、テンプレート参照、各アイテムの収集状態、mode、version、lastModifiedを含む完全なレコードと主キーが、再読込後まで一致し、CLIはPASS・終了コード0でした。生成した単独Playwrightテストも1件成功しています。
固定版の設定・起動手順・比較範囲から同じ検査を実行できます。選択中のプロフィールと設定はバックアップ対象外なので、この結果では保証しません。こちらの合成データによるローカル実測で、作者の実行報告や実サービスの検査ではありません。
