別のタブで名前を変更して保存したあと、先に開いていたタブで画像だけを変更する。画像は保存できたのに、名前と保存チェックポイントが以前の値へ戻るケースがありました。
OSW StudioのBrowser Modeで再現し、作者によるv1.106.2の修正後にテスト本文と設定を変えず、2件成功・1件失敗から3件成功へ変わったことを確認しました。ここでは、IndexedDBを使うアプリでも起きる更新の境界と、自分のアプリで検査するときに残したい条件を説明します。
これはAI支援で行ったローカル検証です。合成プロジェクトを使い、APIキーやモデルは使用していません。作者の修正と、こちらの再検証は別の作業です。
画像変更で何が巻き戻ったか
対象はOSW Studio v1.106.1、b4a7a592です。
- プロジェクト一覧を2タブで開く。
- 主タブで実ファイルをアップロードし、手動保存して新しいチェックポイントを作る。
- 主タブで名前を
CRASHLAB-old-SAVEDへ変更し、実際の保存を確認する。 - 先に開いたタブで、サムネイル画像だけをアップロードする。
4のあと、画像は更新されましたが、保存レコードのname、lastSavedCheckpointId、lastSavedAtが古い値へ戻りました。再読み込み後も同じでした。ファイル本体は残っており、この検証でファイル消失や破損を確認したわけではありません。
修正前。青い画像は保存されていますが、カードの名前がCRASHLAB-oldへ戻っています。
更新するフィールドを、画面が持つ古いレコードから分ける
作者の修正コミット57b60ee8では、ギャラリー側がプロジェクトのコピー全体を渡す形から、IDと変更フィールドを渡す形へ変わりました。更新側は保存済みレコードを読み、その変更だけを重ねます。
構造を簡略化すると、次の違いです。実装用の完成コードではありません。
// 先に開いた画面のsnapshotには、他タブでの変更がない
await store.write({ ...snapshot, previewImage: newImage });
// 更新直前に読んだレコードへ、自分が変えるフィールドだけを適用
const current = await store.read(id);
await store.write({ ...current, ...changedFields });
画像変更側が渡すのは画像関連フィールド、名前変更側が渡すのは名前と説明です。画像変更が、別タブで保存された名前やチェックポイントを一緒に書き戻さない形になりました。
ここで検査したのは、主タブの保存完了後に、先に開いたタブが別のフィールドを保存する順序です。このreadとwriteの形だけで、両タブの完全同時更新に対する原子性や競合解決まで保証するものではありません。
同じテストで修正前後を比べる
修正版のv1.106.2、57b60ee8を新しくビルドし、同じnative Playwrightファイルとconfigで比較しました。
| 画像を変更する画面 | v1.106.1 | v1.106.2 |
|---|---|---|
| 保存した主タブ | 成功 | 成功 |
| 保存後に開いたタブ | 成功 | 成功 |
| 保存前から開いていたタブ | 保存済みメタデータの比較で失敗 | 成功 |
修正版は3件成功・スキップ0・exit 0、テスト合計6.9秒でした。依存準備とビルドの時間は含みません。
修正後も、画像変更前の古いタブはCRASHLAB-oldを表示していました。その状態から実際に画像を保存し、名前とチェックポイントが保たれることを確認しています。テストが古いタブを作らなくなったことで緑になった、という結果ではありません。
画面の画像やファイル数だけでは判定していません。実際の読み取り専用IndexedDBトランザクションで、次を保存前・画像変更後・再読み込み後に比較しました。
- プロジェクトの全フィールド。ただし意図した
previewImage、previewUpdatedAt、updatedAtを除く。 - 保存済みファイル7件の完全な内容。
- ファイルツリー4件とチェックポイント3件の完全なレコード。
カードのファイル数には古い表示が残るため、その数を保存内容の代わりには使っていません。修正版でも、サムネイルの実保存とUIの画像が一致することを先に確かめています。
環境はmacOS arm64、Node 22.16.0、Next 16.3.0、Playwright 1.62.1のChromiumです。修正版から抽出した元アプリ1,475ファイルはビルド・試験後も上流のGit blobと一致し、元ソースを追加修正していません。公開.env.exampleは持ち込まず、依存のlifecycle scriptsを無効にし、必要な公開アセットだけ配置しています。
SQLite依存のバックグラウンド処理にはnative binding不足のエラーがあり、サンドボックス内のservice workerエラーも両版の対照ケースに出ています。制限は緩めていません。今回の成功はBrowser Modeのグリッドでの3ケースに限り、サーバー機能、デスクトップ版、他ブラウザの成功を意味しません。
作者による修正版公開の報告、こちらから返した実測結果、機械可読の再検証結果を公開しています。作者がこちらのPlaywrightファイルを実行したと確認できた、という主張はしていません。
自分のアプリの検査へ持ち帰る
テスト1ファイルとconfig、完全な準備手順を公開しています。OSWで再現する場合は、比較する版のソースと、リンク先の依存準備・アセット配置・Browser Modeのビルドが必要です。App Crash Lab本体のインストールは不要で、OSWのlockに入っているPlaywrightを使います。
起動したローカルサーバーにAPP_URLを合わせて実行します。本稿の修正版はポート57959を使いました。
APP_URL=http://127.0.0.1:57959 npx --no-install playwright test --config=e2e/crashlab-osw/playwright.config.mjs
各実行でobservation.jsonとsnapshots.jsonが出力されます。比較時にファイルのバイト列を変えなかったため、spec中のsourcePin表示は旧測定のb4a7a592のままです。修正版の実際のソースSHAは、再検証結果のsource.commitに別途記録しています。
別アプリへ転用するときは、そのまま貼るだけで自動検査されるものではありません。実際の起動完了条件、UI操作、DBとストアの構造、保護するフィールドを置き換えてください。次の条件は残します。
- 主タブで、本当に保存された新しい値を作る。
- 保存前に開いたタブを使い、無関係なフィールドだけを変更する。
- その変更自体が実保存されたことを確認する。
- 主タブ内と新規タブの対照ケース、保存内容全体と再読み込み後の比較を保つ。
この確認ができると、修正前に落ちていた同じテストを、次のリリースでまた壊れていないか確かめるために残せます。他の保存境界の例とコピー元もまとめています。自分のアプリで試した場合は、アプリ名・版・実行結果、または最初につまずいた手順を実行結果の窓口へ教えてください。