# Your app can export a backup it cannot restore

A successful JSON download proves that an app produced a file. It does not prove that the same app can restore it.

I reproduced that gap in a disposable local copy of [Shamil's DevTasks](https://github.com/shamilahmdt/devtasks), pinned to [commit `d59dc107`](https://github.com/shamilahmdt/devtasks/commit/d59dc107dd7fcc6e427cc683a5ee79474b4fac29). A task created through the UI with a custom category exported successfully. Importing the **same, unedited file** into a fresh browser context failed. A default-category task restored successfully.

This is an AI-assisted independent experiment from aichance. The app source was unchanged, all records were synthetic, and I did not test the current hosted deployment. The [runnable tests and setup](https://github.com/aichance/business-ai-recipes/tree/main/tools/app-crash-lab/examples/devtasks) are public, and [the reproduction was reported to the author](https://github.com/shamilahmdt/devtasks/issues/483).

## Watch the actual export and restore

![Actual local DevTasks UI: create a custom DOCS task, export its JSON, reject that unedited file in a fresh context, then restore the FEATURE control and reload.](https://aichance.github.io/business-ai-recipes/devtasks-backup-roundtrip.gif align="center")

The [21-second recording](https://aichance.github.io/business-ai-recipes/devtasks-backup-roundtrip.mp4) shows the unchanged app with synthetic records. Captions sit above the app; initial startup waits are trimmed, with playback at normal speed. This records the separate Playwright example below.

## Use the real export, in a clean destination

The test performs the same sequence for two categories:

1.  Create one task through the normal UI. For `DOCS`, first add that custom category through the UI.
    
2.  Click **Export JSON**, capture the download and keep its bytes unchanged.
    
3.  Open the app in a new browser context with no saved tasks.
    
4.  Upload the captured file.
    
5.  Require the complete restored task to equal the original, then check its visible text after reload.
    

| Task category | Export | Import into a fresh context | Reload check |
| --- | --- | --- | --- |
| Built-in `FEATURE` | File downloaded | Complete task restored | Task visible |
| UI-created `DOCS` | File downloaded | Schema validation rejected it; target store stayed absent | Not reached |

The failure displayed `File validation failed — invalid task schema detected.` The destination started empty and no task was restored. This establishes a restore failure; the test did not recheck the original context after the failed import.

![Unchanged local DevTasks rejects its own exported custom-category backup with an invalid-task-schema toast. The target inventory is empty.](https://github.com/user-attachments/assets/71d454f9-3158-4add-b9ea-27b309eb97ee align="center")

In the pinned source, the category UI accepts new strings, but the task-import validator only accepts `FEATURE`, `BUG` and `REFACTOR`. The writer's accepted values and the reader's accepted values differ.

## Keep the exporter honest

This is the relevant excerpt from the executed Playwright test. `saved` is the task record observed after creating it through the UI; `source` is that browser context.

```javascript
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);
```

`backup` is then uploaded unchanged through the destination's file input. Constructing a convenient JSON fixture would skip the exporter. Testing only a built-in category would miss the difference that triggered this failure.

The destination context matters too. Checking the original tab could mistake data already in storage for data restored from the backup. The test requires an absent target store before import and compares the full restored record afterward. The complete script also waits for the startup splash to disappear and saves the backup, screenshot and observations for inspection.

## Rejection safety and successful restoration are separate checks

I also ran [App Crash Lab](https://github.com/aichance/business-ai-recipes/tree/main/tools/app-crash-lab) against the same local source. Its three checks passed: a valid imported task survived reload; malformed JSON was rejected without changing the saved task; and a file missing `tasks` was rejected without changing it. The exported standalone Playwright checks also passed.

Those results establish the selected preservation behavior. They do not establish that every app-generated backup can be restored. App Crash Lab 0.4.0 does not capture a download as dynamic input for a later import, so the round-trip check is a **separate Playwright test**, included beside the preservation contracts.

On the pinned source, the round-trip command intentionally exits with one passing test and one failing test. It asserts the desired restore behavior; it does not assert that rejection is correct. After a future fix, both cases should pass. I have not tested a repair.

## Update, October 15: rerun the real-file check from a JSON contract

The 0.4.0 limit described above is historical. App Crash Lab 0.6+ adds `backup-roundtrip`: export through the app's real UI, compare the JSON with the selected complete saved records, pass the original bytes into a fresh context, require a new import-completion signal, and compare the restored records again after reload. It produces both a report and a standalone Playwright test.

I reran the published packages on unchanged local source with synthetic records:

| App / selected data | Runner | CLI / generated Playwright |
| --- | --- | --- |
| [DevTasks, default `FEATURE` / custom `DOCS`](https://github.com/aichance/business-ai-recipes/tree/app-crash-lab-v0.6.2/tools/app-crash-lab/examples/devtasks) | 0.6.2 | PASS / PASS for `FEATURE`; FAIL / FAIL for `DOCS` |
| [Price Table, complete saved product/price arrays plus one rendered product/count/unit-price summary](https://github.com/aichance/business-ai-recipes/tree/app-crash-lab-v0.6.2/tools/app-crash-lab/examples/price-table) | 0.6.2 | PASS / PASS |
| [Paisa, opening balances and complete transaction records](https://github.com/aichance/business-ai-recipes/tree/app-crash-lab-v0.7.0/tools/app-crash-lab/examples/paisa) | 0.7.0 | PASS / PASS |
| [BamiFitLog, complete settings, routines, completed sessions and custom exercises](https://github.com/aichance/business-ai-recipes/tree/45458003731205302e8d202aff5a5cb4e6b89a5c/tools/app-crash-lab/examples/bamifitlog) | 0.7.0 | PASS / PASS |

Paisa's actual export was 273 bytes; BamiFitLog's was 2,541 bytes. The workout control included two exercises with three sets each, weights and repetitions, a completed session's date, time, duration and notes, and seven settings. Both restored in the CLI and independently generated test. These are my compatibility checks, not reports that either author adopted the tool.

**0.7.0 also supports one explicitly declared native restore confirmation.** Only the exact message is accepted, once, during the target file delivery. Accepting a dialog is not a PASS: the new completion signal, selected saved records and reload checks must still succeed. Missing, different or repeated confirmations remain INCONCLUSIVE. Separate DOM preview/commit screens and importers that rewrite record IDs are outside this mode.

### Try your own app with your existing coding agent

Open your app's project in your coding agent, then [copy the backup setup prompt](https://aichance.github.io/business-ai-recipes/app-crash-lab.html#backup-agent). Tell it one record you need to restore. The prompt asks it to inspect your actual UI, write the contract, install the pinned local dependencies and Chromium, run the check, and show the report and standalone test. Review the proposed contract before running it on a disposable local copy with synthetic records.

The runner needs Node 22+ and npm; it makes no LLM calls and requires no account or API key. Optional setup uses your existing coding agent. [Manual contract guide](https://github.com/aichance/business-ai-recipes/blob/app-crash-lab-v0.7.0/tools/app-crash-lab/BACKUP.md) · [0.7.0 package, source ZIP and checksums](https://github.com/aichance/business-ai-recipes/releases/tag/app-crash-lab-v0.7.0).

If you try it, share the app/tool versions and result, or just the first setup step that stopped you, in a comment or [short trial report](https://github.com/aichance/business-ai-recipes/issues/new?title=App%20Crash%20Lab%20backup%20trial). Private source and real records are not needed. If you want to keep the check for your next release, a GitHub Star is an optional way to find it again.

## Run the reproduction or adapt its boundary

The [example directory](https://github.com/aichance/business-ai-recipes/tree/main/tools/app-crash-lab/examples/devtasks) contains the source, configuration, exact source revision and two-terminal setup. It was added after the 0.4.0 release archives, so use the repository version for this example. The measured environment was macOS arm64, Node 22.16.0 and Playwright 1.64.0 with Chromium.

For your own app, choose one ordinary record and one value the UI allows users to customize. Export through the real UI, import the unedited bytes into a clean destination, and compare the complete record. Then reload. This checks a specific round trip; it does not establish cross-version migration, storage-limit behavior, multiple-tab consistency or crash durability.

The reusable test sources and App Crash Lab contracts are MIT-licensed. DevTasks remains its author's project; its source is not bundled in the test pack.
