JSONがダウンロードできたことと、そのJSONからデータを復元できることは、別のテストでした。
タスク管理OSS DevTasksの公開ソースをコミット d59dc107に固定して、変更せずローカルで起動。画面から作った架空のタスクをエクスポートし、そのファイルを一切編集せず、保存データのない別ブラウザ環境へ読み込むと、次の差が出ました。
| 作成したタスクの分類 | JSON出力 | 空の環境への復元 | 再読込後 |
|---|---|---|---|
標準の FEATURE
|
成功 | 元のレコード全体と一致 | 表示された |
UIで追加した DOCS
|
成功 | スキーマ検査で拒否 | 復元されず、再読込検査には進まない |
これは2026年10月12日の独立したローカル検証です。現在の公開サービスの動作や、本番データの消失を示すものではありません。作者には再現手順を報告しています。
実際に起きたこと
21秒の動画は実際のローカル操作です。出力元と空の復元先の映像をつなぎ、起動待ちを省いています。操作部分は等速です。
DOCS の読み込みでは File validation failed — invalid task schema detected. と表示され、復元先の保存領域にタスクは入りませんでした。失敗後の元タブは再確認していないので、元データが残ったかどうかはこの検証では判断していません。
固定したソースでは、分類を追加するUIは任意の新しい文字列を受け付けます。一方、読み込み側の検査が受け付ける分類は FEATURE・BUG・REFACTOR の3種類でした。書き出せる値と、読み込める値の範囲が一致していなかったわけです。
1. テスト用JSONを手作りせず、本物のダウンロードを使う
以下は実行済みのテストからの抜粋です。source.page はUIでタスクを作成したページ、saved はそのあとに読み取った保存済みの完全なレコードです。
const downloading = source.page.waitForEvent('download');
await source.page.getByRole('button', { name: 'Export JSON', exact: true }).click();
const download = await downloading;
const backup = await readFile(await download.path());
expect(JSON.parse(backup.toString()).tasks).toEqual(saved);
先にdownloadイベントの待機を登録し、実際のボタンを押して取得します。「正しそうなJSON」をテスト側で組み立てると、書き出し処理が落とすフィールドや、書き出す側と読み込む側の食い違いを飛ばしてしまいます。
2. 元のタブへ戻さず、新しいcontextへ読み込む
同じタブを使うと、もともと残っていたデータを「復元できた」と取り違える可能性があります。復元先は browser.newContext() で分け、最初にタスクの保存値がないことを確認しました。
取得した backup のバイト列を変更せず、復元先のファイル入力へ渡します。次も同じテストからの抜粋です。
expect(await target.page.evaluate(() => localStorage.getItem('tasks'))).toBeNull();
await target.page.locator('input[type=file]').setInputFiles({
name: 'unedited-backup.json', mimeType: 'application/json', buffer: backup,
});
新しいcontextで分離できるのはブラウザの保存領域です。共有のサーバーDBは分離されないため、このまま本番アカウントやサーバー保存型アプリへ向ける手順ではありません。
3. 成功表示や件数だけで終えず、完全なレコードを比較する
このテストは、読み込み後のタスク全体が元データと一致するまで待ち、その後に一覧画面でタスクの表示を確認します。最後に再読込し、もう一度表示を確認します。
FEATURE と DOCS のどちらにも「復元できる」を要求しています。固定したソースでは 1件PASS・1件FAIL になり、コマンドも終了コード1になります。現在の拒否を正解に変えるテストではありません。将来修正されたら両方PASSになる想定ですが、修正版は未検証です。
保存の維持がPASSでも、往復できるとは限らない
自作のApp Crash Labでも同じローカルアプリを検証しました。正常な読み込み後の再読込、壊れたJSONの拒否後、必要な tasks のないJSONの拒否後――選んだ保存値を比較する3件は、CLIと生成したPlaywrightテストの両方でPASSでした。
そこから言えるのは、選んだ操作のあとに選んだ値が保たれたことまでです。「アプリが出力する有効なバックアップを復元できる」は別の条件なので、10月12日の検証では往復を独立したPlaywrightテストにしました。当時のApp Crash Lab 0.4.0のCLI自体には、downloadを次のimportへ渡す機能はありません。
10月15日追記:同じ往復検査をCLIで繰り返せるようにした
この検査をアプリごとに書き直さずに済むよう、App Crash Lab 0.6.2にbackup-roundtripを用意しました。使い捨てのローカルアプリで、実際に出力したJSONのバイト列を新しいcontextへ読み込み、選んだ保存値全体と再読込後の画面を比較します。アカウント、実行時のLLM、APIキーは不要です。
配布する最終tgzから依存を新規インストールし、未変更のアプリで再実行しました。
| 対象と固定ソース | CLI | 生成した単独Playwright |
|---|---|---|
DevTasks d59dc107 / FEATURE |
PASS | PASS |
DevTasks d59dc107 / DOCS |
FAIL(import成功を確認できない) | 同じ失敗 |
底値表 02a4d498 / 通常のJSON出力・読込 |
PASS | PASS |
底値表は、ボタンを押したときだけ作るファイル入力と、読込直後の自動リロードを使います。その実際の操作でも、未編集のJSONで保存レコードが一致しました。いずれもこちらのローカル検証で、作者側の採用や本番の復旧を示すものではありません。
DevTasksで再実行する
Node22以上とnpmが必要です。まず別ターミナルで、固定ソースの起動手順に従い、使い捨てDevTasksをhttp://127.0.0.1:54439で起動します。App Crash Lab自体のcheckoutは不要です。
空の作業フォルダーで、公開済みの設定とバージョンを固定して実行します。
curl -fL https://raw.githubusercontent.com/aichance/business-ai-recipes/app-crash-lab-v0.6.2/tools/app-crash-lab/examples/devtasks/roundtrip-feature.json -o feature.json
curl -fL https://raw.githubusercontent.com/aichance/business-ai-recipes/app-crash-lab-v0.6.2/tools/app-crash-lab/examples/devtasks/roundtrip-docs.json -o docs.json
npx --yes --package=@playwright/test@1.64.0 playwright install chromium
npx --yes --package=https://github.com/aichance/business-ai-recipes/releases/download/app-crash-lab-v0.6.2/aichance-app-crash-lab-0.6.2.tgz app-crash-lab run feature.json --out feature-first
npx --yes --package=https://github.com/aichance/business-ai-recipes/releases/download/app-crash-lab-v0.6.2/aichance-app-crash-lab-0.6.2.tgz app-crash-lab run docs.json --out docs-first
固定ソースでは、最初は終了コード0、最後は終了コード1が期待結果です。既存の出力先は上書きしないので、再実行するときは--outを別名にしてください。レポート内のrepro.spec.mjsは、App Crash Labに依存しない単独Playwrightテストです。
自分のアプリへ合わせる項目は、バックアップ設定の説明にまとめました。localStorageとIndexedDBの観測に対応しますが、今回の実アプリ測定はlocalStorageです。ZIP形式、サーバーDB全体、複数ページにまたがる復旧はこの検査の対象に含めていません。
成功トーストや件数が合っていても、1項目だけ失う可能性があります。また、別画面を開くコードが不足値を自動補完すると、その後の比較だけでは欠損を見逃します。0.6.2では復元後の画面移動より前に選択した保存値全体を比較し、その後に画面・再読込を確認します。意図的に隠れた項目を落とし、次の画面で補完する負例でも、画面移動前にFAILとなることを確認しました。
0.6.1は同梱DevTasks設定の画面移動を設定検査で拒否する不具合があったため、この手順では0.6.2を指定しています。全91件のテストに同梱設定の検査と上記の負例を追加しました。
ソースZIP・実行パッケージ・チェックサムを公開しています。自分のアプリで試した結果や、導入で止まった箇所はコメントまたは試用報告フォームへどうぞ。ソースや実データの公開は不要です。次のリリースでも検査を使いたいと思えたら、GitHubのStarで保存してもらえると助かります。
10月15日夜追記:確認ダイアログ付きの復元にも対応した
App Crash Lab 0.7.0では、実ファイルを読み込むときのネイティブ確認ダイアログを、宣言した文言と完全一致した場合に一度だけ受け入れられるようにしました。確認を押せただけではPASSにせず、その後の新しい完了表示・保存レコード全体・再読込後の一致を要求します。未対応の操作を、成功扱いで通すための設定ではありません。
未変更ソースと合成データを使い、次の2アプリでも実際のファイルを往復させました。
| 対象と再実行用設定 | 実ファイル | 比較した保存値 | CLI / 生成Playwright |
|---|---|---|---|
| Paisa | 273 bytes | 開始残高と入出金の全項目、残高・履歴表示 | PASS / PASS |
| BamiFitLog | 2,541 bytes | 設定・ルーチン・完了した運動記録・独自種目を含む保存データ全体とルーチン表示 | PASS / PASS |
BamiFitLogでは、2種目×3セットの重量・回数、日時、メモ、設定値も含めて比較しました。実際のJSONを編集せず、保存領域が空の別contextへ戻し、確認はどちらも一度だけです。これらはこちらの互換性検証で、作者本人による採用や実利用の報告ではありません。
自分のアプリ向けの設定を一から書く負担を減らすため、バックアップ検査専用の導入プロンプトも用意しました。普段のコーディングAIでアプリのプロジェクトを開き、プロンプトと「復元したいレコード」を一つ渡す入口です。実行結果と、単独で再実行できるテストの保存先まで確認します。
対象はブラウザ内に保存するJSONバックアップです。プレビュー後に別途確定する操作や、IDを書き換えて取り込む方式はこのモードの対象外です。設定で止まった場合はアプリの不具合と決めつけず、停止した手順を切り分けます。試した方は、アプリ・ツールの版と結果、または最初に詰まった手順だけ教えてください。
自分のアプリに持っていくには
テスト一式と2ターミナルの起動手順をMITで公開しています。検証環境はmacOS arm64・Node 22.16.0・Playwright 1.64.0 / Chromiumです。10月12日の独立したテストを読む場合はリンク先のリポジトリ版を使ってください。10月15日追記のCLI用設定は0.6.2の配布ファイルに同梱しています。
既存のPlaywrightプロジェクトに移すときは、次の4か所を自分のアプリに合わせます。
- ローカルURLとデータ2種類。普通の値と、UIで許可される境界の値を選ぶ。
- データを作るUIと、保存済みレコード全体の読み取り。
- 実際の出力・読み込みボタン、ファイル形式、準備完了や結果の表示。
- 復元先の完全なレコード比較と、再読込後の画面確認。
これはセレクターを自動発見するテストではありません。まず1件の普通のデータで往復を通し、次にユーザーがカスタマイズできる値を足すと、セットアップの問題と今回のようなデータ依存の問題を切り分けやすくなります。
DevTasksは元の作者のMITプロジェクトで、テスト一式にそのソースは同梱していません。aichanceは人間の責任者のもとでAIを使って開発・実行検証・執筆・運用しています。検証対象の作者によるApp Crash Lab採用を示す記事ではありません。
