> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobster.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Reports, logs and video

> Read a suite's HTML, JUnit and JSON reports, and the app's log, crash report and video for each check.

Every run writes a folder, `.mobster/test-results/<suite-id>/`, that stands on its own: upload it as a CI artifact and every link in it works. Reports stay in your project folder unless you [share them with your team](https://docs.mobster.dev/testing/share.md).

## Find the files a run writes

| file | what it holds |
| - | - |
| `index.html` | The checks that failed, their screenshots, logs and video, then every check and its verdict |
| `junit.xml` | Check results for your CI, grouped by device. [JUnit fields](#read-junit-results) |
| `results.json` | Structured results with schema `mobster.test/1`. [JSON fields](#read-resultsjson) |
| `evidence-inventory.json` | The size and SHA-256 hash of `results.json`, JUnit, the HTML reports and each evidence file they name, taken when the suite finished |
| `runs/<run-id>.html` | A copy of each run's own report (self-contained, frames included) |
| `runs/<run-id>/logs/app.log`, `runs/<run-id>/crashes/*.ips` | The app's log and crash report of each run that didn't pass |
| `videos/` | Each run's screen, on a simulator or an iPhone, with `--video` |

A safety stop is a JUnit error even when its check is quarantined. A stopped or other nonzero suite with no failing check gets a `Suite verdict` error, so CI doesn't report it as passed.

`--junit FILE` and `--html DIR` also write the JUnit file and the HTML report (with `runs/` and `videos/`) where your CI expects them. `--json` prints `results.json` on stdout.

## Read the HTML report

Open `index.html` in the folder that `mobster test` prints. It starts with the checks that didn't pass, then lists every check's file, tags and verdict on each device. `--spread` groups the checks by device.

Open a failed check to see its expectations, what was found instead, the verdict frame, and its [Logs and Crash pane](#read-logs-crashes-and-video). The report links to each run's report, log and video, then lists flaky checks. Every link stays inside the folder except **Mobster** in the footer, which opens mobster.dev.

## Read JUnit results

`junit.xml` has one `<testsuite>` per device and one `<testcase classname="checks.<file>">` per check:

* `<failure>` has type `assertion`, `app_crashed` or `needs_review`.
* `<error type="couldnt_run">` marks a check that couldn't run. `<skipped>` marks quarantine.
* Each check has the properties `status`, `attempts`, `flaky`, `passRate` and `report`. When measured, it also has `perf.launchMs`, `perf.memoryMb` and `perf.hangs`.
* Each suite has `git.branch` and `git.head` when you run in a git checkout.
* A crash's top frames appear in `<failure>`. The app's log around the failure appears in `<system-out>`.

## Read results.json

```json theme={"theme":"css-variables"}
{"schema": "mobster.test/1", "suite": "20261007-142233-9c1e", "exitCode": 1,
 "git": {"branch": "bookmarks-tab", "head": "217873d10389c1f0a7f4f1e0c2b9d6e5a4f3b2c1"},
 "devices": [{"key": "sim:iPhone 17 Pro@iOS 26.4", "name": "iPhone 17 Pro · iOS 26.4", "kind": "simulator"}],
 "checks": [{"name": "Bookmarks opens from the toolbar", "file": ".mobster/checks/bookmarks.yaml",
             "results": [{"device": "iPhone 17 Pro · iOS 26.4", "status": "passed", "attempts": 1, "flaky": false,
                          "report": "runs/20261007-142240-a1b2.html", "durationMs": 21400, "healed": [],
                          "replayed": true, "noModel": true,
                          "replay": {"mode": "replayed", "steps": 1, "replayed": 1, "healed": 0,
                                     "seconds": 4.1, "costUsd": 0},
                          "runs": [{"attempt": 1, "runId": "20261007-142240-a1b2", "verdict": "passed"}]}]}],
 "summary": {"passed": 1, "failed": 0, "needsReview": 0, "couldntRun": 0, "flaky": 0, "quarantined": 0}}
```

| Field | Meaning |
| - | - |
| `git` | The branch and commit the suite tested, read as it starts. Left out outside a git checkout. On a CI's detached checkout the branch comes from the CI: `GITHUB_HEAD_REF` (a pull request), `GITHUB_REF_NAME`, `CI_COMMIT_REF_NAME`, `BITRISE_GIT_BRANCH`, `BUILDKITE_BRANCH`, `CIRCLE_BRANCH` or `CI_BRANCH`. Mobster for Mac groups a project's runs by it |
| `status` | A verify verdict: `passed`, `failed`, `needs_review` or `couldnt_run`. A flaky check counts under its status and under `flaky` |
| `evidence` | On a result that didn't pass: [below](#read-logs-crashes-and-video) |
| `passRate` | With `--repeat` |
| `replay.mode` | `replayed`: no model ran any step. `healed`: Mobster asked the model for some steps, to heal a step that changed or to type text it wrote. `live`: the model ran every step |
| `replayed` | True when the result replayed its recorded steps, whether or not a step healed |
| `noModel` | True when every attempt replayed its recorded steps with no model. An attempt that can't start makes it false |
| `summary.replayed`, `summary.cacheHitRate` | The results with `noModel` true, and the share with `replayed` true |

## Read logs, crashes and video

For every run on a simulator or a USB iPhone, Mobster keeps the app's log and, when the app crashed, its crash report. With `--video` it also records the screen.

```text theme={"theme":"css-variables"}
✗ failed  Crash fixture stays up after launch on iPhone 17 Pro, iOS 26.4
    Crash Fixture crashed: EXC_BREAKPOINT (SIGTRAP), in specialized CrashTrigger.crashNow() (CrashFixtureApp.swift:11).
    The report's Logs and Crash pane shows the stack and the app's log around the crash.
```

* **A crash fails the check** as `failed`, class `app_crashed`, even when the expectations held. Mobster knows the app crashed the moment SpringBoard logs that its process ended on a signal (`SIGSEGV`, `SIGABRT`, `SIGTRAP`), the watchdog (`0x8badf00d`) or jetsam. Relaunches that Mobster starts don't count.
* **The log** is the app's unified log (`os_log`, `Logger`, `NSLog`) from before the launch to the verdict. On a simulator Mobster reads it with `xcrun simctl spawn <udid> log stream`, at the info level and above. On an iPhone it reads the syslog with `idevicesyslog`. Each line carries the run clock, in seconds since the run started. What your app prints to stdout with `print` isn't in it.
* **The log around a failure** in `results.json` marks each line `app: true` when it's the app's own (its process, not an Apple framework). Mobster for Mac shows those lines first and folds the rest behind **Show system log**.
* **The video** is a simulator's screen through `xcrun simctl io recordVideo`, or an iPhone's through WebDriverAgent's video stream, encoded on the Mac to an H.264 MP4 590 pixels wide. Nothing on the phone changes. Mobster doesn't store a frame that repeats the one before, so a still screen costs nothing.
* **The crash report** comes from `~/Library/Logs/DiagnosticReports` for a simulator, and from the iPhone with `idevicecrashreport --keep`, which copies it and leaves it on the phone. Mobster names your app's frames with `atos` and the dSYM beside the check's `.app` or in `.mobster/build/Build/Products`. A Debug build already carries its names.

The run's report puts the **Logs and Crash pane** under the step the failure came in: the crash's top frames, your own in full ink, then the app's log 10 seconds either side of the failure with the moment marked. A launch-only check has it as its own section. `index.html` shows the same pane under the expectations and links to the full log and the `.ips`, and `junit.xml` carries the crash in `<failure type="app_crashed">` and the log window in `<system-out>`. In Mobster for Mac, each project's page shows its runs by branch and the same pane beside each failed check's steps, with **Run checks** to run them all again and **Run again** under the failed checks to run only those that didn't pass.

`results.json` gives each result that didn't pass an `evidence` object. [Proof for every step](https://docs.mobster.dev/testing.md#see-proof-for-every-step) adds the slices for each step.

| Field | Meaning |
| - | - |
| `source` | `simulator` or `iphone` |
| `log` | `path` (relative to the suite folder), `lines`, `bytes`, and `dropped`: lines left out past the 1 MB cap |
| `note` | Why there's no log, such as "No log: the log tool isn't installed." |
| `atMs` | The failure on the run clock: the crash, else the verdict read, else the moment the run couldn't go on |
| `step` | The step the failure came in: the first one recorded after it (a step is recorded once its action and the screen settle), else the last |
| `window` | `fromMs`, `toMs` and `lines` (`atMs`, `level`, `process`, `text`): the log within 10 seconds of `atMs`, at most 60 lines, errors and your app's own lines first, and none of XCTest's (WebDriverAgent reading the app) |
| `crash` | `type` (`EXC_BAD_ACCESS`), `signal`, `message` (Swift's or Objective-C's last words, when the report has them), `thread`, `pid`, `frames` (`index`, `image`, `own`, `symbol`, `file`, `line`, top 8), `symbolicated`, and `report`, the `.ips` copy. `report` is `null` and `frames` empty when macOS hadn't written the report |

macOS writes a simulator's crash report some time after the crash, so `mobster test` waits up to 60 seconds once, at the end of the suite, for the reports it's missing. A report that comes later stays in `~/Library/Logs/DiagnosticReports`, and the check still says it crashed and on which signal.

Each log line and every string in a crash report goes through the secret filter before it's written: passwords, one-time codes, card numbers and API keys become `••••`. They stay in the run folder on this Mac, and Mobster sends them nowhere. Over MCP, the result your coding agent reads (`verify_finish`, `run_tests`) carries `evidence` as data. The log stops at 1 MB, then keeps its last 2,000 lines and says how many it dropped.

On an iPhone over Wi-Fi, the log and crash reports need libimobiledevice to reach the phone on the network. When it can't, the result's `note` says there's no log. A check on a WebDriverAgent address gets no log or crash report, because Mobster can't tell which phone it is.

## Export a suite

From the project folder, name the finished suite:

```sh theme={"theme":"css-variables"}
mobster test capsule show 20261007-142233-9c1e
mobster test capsule export 20261007-142233-9c1e --output ./suite-capsule
```

`show` prints the suite verdict and evidence count. `--json` gives opaque check and device IDs, statuses and evidence hashes, with no check names, device names, paths, app text or pixels. `export` writes that descriptor to a new local folder and uploads nothing. Both need the suite's `evidence-inventory.json` and an unchanged `results.json`, and refuse a suite without them.

To copy raw evidence, pick each kind:

```sh theme={"theme":"css-variables"}
mobster test capsule export 20261007-142233-9c1e --output ./suite-capsule-raw \
  --include junit --include report
```

Selected files are unredacted and stay on this Mac until you share them. A selected HTML report or JUnit file can contain app text, log lines or crash details. `--output` names a new folder. If a selected file is missing or changed, export refuses before it creates the folder. Logs, crash reports and videos stay in the suite folder and are never copied into a capsule.

## Compare two suites

```sh theme={"theme":"css-variables"}
mobster test compare BASE_SUITE_ID CANDIDATE_SUITE_ID --json
```

You pick both suites. Mobster compares their saved results and reports each check and device's `observedTransition` apart from `verifiedFix`. It needs each suite's inventory and an unchanged `results.json`. `verifiedFix` is true only when clean, single-attempt simulator runs show an app-state field ([Fixtures](https://docs.mobster.dev/fixtures.md)) fail on one build and pass on another, while the check, fixture, environment and policy stay the same. An input Mobster couldn't pin ([What a suite pins](https://docs.mobster.dev/testing/flaky.md#know-what-a-suite-pins)) never counts toward a verified fix.

## Shrink a failing check

When a comparison verified a fix, `mobster test minimize` looks for the fewest steps that still fail on the bad build and pass on the good one. Keep both simulator `.app` builds in the project, then run:

```sh theme={"theme":"css-variables"}
mobster test minimize BAD_SUITE_ID CONTROL_SUITE_ID CASE_REF TARGET_REF \
  --bad-app path/to/bad.app --control-app path/to/control.app --isolated
```

`--isolated` says you isolated any outside state for every trial: Mobster resets the app as the check requires, but it can't reset your backend. It first reruns the original pair. It keeps a step's removal only when the bad build fails the same assertion twice and the control build passes once between them. An unknown outcome stops without a retry. It runs at most 8 suites, the original pair included (`--max-attempts N` takes 5 to 8).

```text theme={"theme":"css-variables"}
Reduction smaller reproducer: 5 trial suites; 3 original steps.
Review .mobster/drafts/repro/<pair-and-refs>/candidate.yaml (2 steps).
```

The result is `.mobster/drafts/repro/<pair-and-refs>/result.json`, with `candidate.yaml` only when a smaller check is confirmed. Mobster never edits your check or its approved path. It exits 0 when it finished, even with no smaller check, 3 on bad input or an unknown outcome, and 130 when you stop it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.