mountable.io

pytest

Run a tiny pytest suite, upload its closed JUnit XML and failure note, and check those same bytes from a fresh process.

A test worker can write a JUnit report on its own disk. When that process exits, the disk is gone.

mountable.io is a persistent filesystem for AI agents. This recipe uploads the closed JUnit XML and a short failure note. A later process reads those same files.

mountable.io maintains the runnable example, separately from pytest. The suite is synthetic. It holds no customer logs or tokens. This is not a test-reporting service.

Prerequisites

Use an existing mountable.io account with a saved payment method, an existing filesystem and an API key that can access it. Storage and file operations follow the usage prices. The example creates no account, filesystem or credits.

You also need Python 3.11 or newer and uv. Set MOUNTABLE_API_KEY in your private environment or secret manager. Never put a key in a command argument. Use your filesystem ID in place of FS_ID below. Pick a new run id for each run. A run id is 1 to 40 lowercase letters, digits or hyphens, and it must start with a letter or digit.

git clone https://github.com/writeitai/mountable.git
cd mountable/examples/pytest-reports
uv sync --locked
export FS_ID="your-filesystem-id"
export RUN="run1"

Your backend or CI job chooses the filesystem and the run id. Do not take either from test output.

The example uses the Python SDK and the Files API. It does not mount the filesystem.

Run the suite and upload its report

run executes the synthetic suite in sample_tests/. It writes junit.xml and failure.txt into ./out. It then uploads pytest/<run>/junit.xml and pytest/<run>/failure.txt.

uv run python reports.py run \
  --filesystem "$FS_ID" \
  --run "$RUN" \
  --dir ./out
echo "pytest exit code: $?"

The command prints each remote path, its SHA-256 and its size. It does not print the API key.

Each file has to be at most 1 MiB. The Files API allows up to 5 GiB. This recipe refuses anything larger before it uploads.

The write uses If-None-Match: *, so an existing file is left as it is. After a timeout, read the path before writing it again. Use a new run id for the next run.

The command returns pytest's exit code. The synthetic failing test makes that code 1, so the calling job still fails. An upload failure is printed as upload_failed. It does not change that code.

Keep the write key in the trusted job's secrets. A reviewer who only needs to read the report gets a read-only handle, as check below uses. A read-only reviewer cannot overwrite the report.

A 402 with filesystem_spending_cap_reached stops that filesystem's billable mountable.io work. Other filesystems keep working.

Check them from a fresh process

check is a new process. It opens the filesystem read-only, downloads both files, compares them with the files in --dir, and parses the JUnit XML.

uv run python reports.py check \
  --filesystem "$FS_ID" \
  --run "$RUN" \
  --dir ./out

The command prints match, the size and SHA-256 of each file, and the JUnit test and failure counts when every byte matches.

Read the report later

You do not need ./out to read the report. From any machine:

mountable files read "$FS_ID" "pytest/$RUN/junit.xml" > junit.xml
mountable files read "$FS_ID" "pytest/$RUN/failure.txt"

Use check above when ./out is still on your machine and you want the byte-for-byte comparison.

Clean up

uv run python reports.py cleanup \
  --filesystem "$FS_ID" \
  --run "$RUN"

This deletes both files. A file that is already gone is left gone.

Offline checks

From the example directory:

uv run pytest test_reports.py -q

The tests use a fake filesystem and run the real synthetic suite in a subprocess. They make no paid cloud calls. They check that the failing test returns exit code 1, the JUnit XML parses, a second upload to the same run is refused, changed bytes are detected, and cleanup leaves no files.

Sources: pytest JUnit XML, Files API.