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 ./outThe 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 -qThe 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.