REST API
Reference for the FastAPI endpoints exposed by test_server.py on port 8083.
All endpoints accept and return JSON unless noted.
|
Auth model. In the multi-stand deployment every request enters through the
gateway ( |
Gateway
These endpoints are served by the gateway itself (gateway_service/app.py),
not proxied to a worker. They handle login, central account / stand / env-profile
management, and run dispatch onto a worker pool. Cross-ref: Environments & the Gateway
and Accounts, Roles & Audit.
Auth
| Method | Path | Purpose |
|---|---|---|
POST |
|
Authenticate. Body |
POST |
|
Invalidate the caller’s session token. Allowlisted. |
GET |
|
Current session identity |
Users (admin)
| Method | Path | Purpose |
|---|---|---|
GET |
|
List all accounts. |
POST |
|
Create an account. Body |
POST |
|
Set a new password. Body |
DELETE |
|
Delete an account. |
Audit (admin)
| Method | Path | Purpose |
|---|---|---|
GET |
|
Central activity / audit log. Query: |
Env Profiles
Profiles are named bundles of worker settings (emulator_url, ACS URL, …)
that the gateway materialises onto a worker at dispatch time.
| Method | Path | Purpose |
|---|---|---|
GET |
|
List env profiles (any authenticated user). |
POST |
|
Create / replace a profile. Body |
GET |
|
Full profile settings (admin). |
DELETE |
|
Delete a profile (admin). Nothing else is torn down: the emulator MCP sidecars are bundled per worker. |
Stands / Workers (admin)
| Method | Path | Purpose |
|---|---|---|
GET |
|
List worker stands. |
POST |
|
Register / upsert a stand. Body |
PATCH |
|
Update a stand (e.g. |
DELETE |
|
Deregister a stand. |
POST |
|
Auto-provision a new worker stack via Portainer. Body |
Run Dispatch
| Method | Path | Purpose |
|---|---|---|
POST |
|
Materialise |
POST |
|
Same, for a batch. Body |
POST |
|
Open the caller’s live view: the worker running the caller’s own run (or a running batch of theirs) plus a one-time ticket for its noVNC stream. Returns |
GET |
|
Name of the worker whose live browser the caller may watch right now (their own active run, or a running batch of theirs). Returns `{stand: name |
null}`. |
POST |
|
A one-time ticket for the caller’s progress stream of that run (see WebSocket). Returns |
GET |
|
| The gateway proxies the worker endpoints in the sections below per-caller and owner-scopes them (see Runs). Batch-status, per-run reads, WebSocket and stop are fanned out across the worker pool so they reach the caller’s run on whichever worker it landed. |
|
Which worker answers, and why it matters
A batch lives on exactly ONE worker, so every per-batch read and write reaches
the worker that holds that batch, never the first one. The sub-routes of a
saved report — Case routes take the other path. Each worker keeps its own |
Test Cases
| Method | Path | Purpose |
|---|---|---|
GET |
|
List all YAML cases. A row is |
GET |
|
Parsed JSON view of a single case, plus |
GET |
|
Raw YAML text of a case. |
POST |
|
Create a new case from a JSON body. |
PUT |
|
Replace an existing case. |
POST |
|
Regenerate |
POST |
|
Upload a hand-edited YAML file. |
DELETE |
|
Move a case to the trash (restorable) — see Test Management. |
DELETE |
|
Move all cases to the trash; |
GET |
|
Distinct values of |
Case Organization
Projects and per-case org fields (project, references, priority, type) used to group and filter cases in the editor. See Organizing Cases: Projects & Sections for the user-facing workflow.
| Method | Path | Purpose |
|---|---|---|
GET |
|
List projects, each with a |
POST |
|
Create a project. Body |
PATCH |
|
Rename ( |
DELETE |
|
Delete a project; its cases are orphaned to Unassigned ( |
POST |
|
Bulk-assign cases to a project. Body |
GET |
|
Read-only, non-secret UI config (currently |
POST |
|
Backfill org fields (project, references, priority, type) from TestRail into local YAMLs — no conversion / Stage 2 / cache bump. Body |
Test Management
Trash, versioned history, duplicates, bulk edits, attachments and case XML. See Test Management: Trash, History, Attachments & Run Reports for the user-facing workflow.
Case Lifecycle
| Method | Path | Purpose |
|---|---|---|
GET |
|
Trashed cases, newest delete first: |
POST |
|
Move a trashed case back; 409 when a live case of that name exists (admin / editor). |
DELETE |
|
Purge one trashed case for good — this is what also drops its attachments and its version snapshots; a trash move and a restore keep both (admin / editor). |
DELETE |
|
Empty the trash: purge every case in it, attachments and history included. Returns |
POST |
|
Copy a case under a fresh id. Body |
POST |
|
Change one metadata field across many cases. Body |
QA-Verified Records
| Method | Path | Purpose |
|---|---|---|
GET |
|
The QA-verified records: |
PUT |
|
Set or clear QA-verified records (admin only). Body |
A QA-verified record freezes the case. The record is not a sign-off.
runs is optional. It maps a test id from test_ids to the run that QA used: {"C123": {"run_id": "ab12cd34", "stand": "live", "at": "2026-09-20T09:30:00Z", "git_sha": null}}.
-
run_idhas 8 lowercase hex characters. -
standis the name of the worker. -
atis a UTC time in ISO 8601 form with a trailingZ. -
git_shahas 7 to 40 hex characters. Sendnullwhen the build is unknown.
The test agent answers 400 and writes nothing for a bad run, an id that is not in test_ids, or runs together with clear.
A repeated call without runs keeps the run of the record when by is the same. If by changes, the test agent drops the run. The history entry and the audit line name the result.
History
| Method | Path | Purpose |
|---|---|---|
GET |
|
Version rows |
GET |
|
|
POST |
|
Write the snapshot back as the current case, logging |
Attachments
| Method | Path | Purpose |
|---|---|---|
GET |
|
List a case’s files: |
POST |
|
Upload one (multipart |
GET |
|
Download one file. |
DELETE |
|
Remove one file (admin / editor). |
Case XML
| Method | Path | Purpose |
|---|---|---|
POST |
|
Export cases as one native |
POST |
|
Import cases (multipart |
Batch Reports
| Method | Path | Purpose |
|---|---|---|
GET |
|
The batch’s comments, oldest first (any authenticated user; owner-scoped — a batch you do not own is 404). |
POST |
|
Add one. Body |
DELETE |
|
Remove one (admin / editor); 403 unless you are its author or an admin. |
PATCH |
|
Set |
GET |
|
Export the report. |
POST |
|
Import an exported run (multipart |
PATCH |
|
Edit a batch in flight. Body |
Reading a report and exporting it are open to every role, but writing one
is not: commenting, annotating a row and importing a run all require admin or
editor. Owner scoping does not stand in for that rule — import-xml mints a
report owned by whoever posts it, so ownership can never refuse a viewer.
|
Trees
XML tree files live under /opt/mcp-shared/ on the test-runner host and are
referenced from each YAML’s emulator_setup.tree_file. The editor’s Tree
File dropdown is populated from these endpoints.
| Method | Path | Purpose |
|---|---|---|
GET |
|
List |
POST |
|
Upload an |
DELETE |
|
Delete an |
# List
curl -s http://ai.friendly-tech.com:8083/api/trees | python -m json.tool
# Upload (or replace)
curl -F "file=@./181.xml" http://ai.friendly-tech.com:8083/api/trees/upload
Uploads are accepted only for .xml. Path traversal attempts (../, \\,
/) and dot-aliases (., ..) are rejected with HTTP 400.
|
Runs
| Method | Path | Purpose |
|---|---|---|
POST |
|
Start a single run. Query parameters: |
POST |
|
Force-stop the caller’s own run (owner-scoped, no body). Not "all runs" — through the gateway it fans out to every worker so it reaches the caller’s run wherever it landed. |
POST |
|
Start a batch. Body: |
GET |
|
Current batch status: |
GET |
|
Latest in-memory batch report JSON. |
GET |
|
Compact status of the batch running here — deliberately not owner-scoped: this is what the cross-account Wave in progress card and the Workers Busy column read. Returns |
GET |
|
Archived batch reports as |
GET |
|
Full archived batch report (summary + per-test results). |
DELETE |
|
Wipe archived reports on disk. |
GET |
|
One page of the caller’s completed runs, newest first, from memory and disk. Query: |
GET |
|
Per-UTC-day counters behind the Overview KPIs, over every run the caller may see. Query |
GET |
|
The caller’s own currently running run as a single object, or |
GET |
|
The registry of reds that are already understood: |
GET |
|
Full report for one run. |
GET |
|
Image of the viewport at the first failing step, PNG or JPEG (owner-only; |
GET |
|
JPEG of the screen at the first inconclusive step (owner-only; |
GET |
|
Device parameter tree captured at failure (owner-only; written for a failed run that applied emulator parameters; |
POST |
|
Live step-progress hook called by the generated Playwright script. Authenticated by the per-run |
DELETE |
|
Clear the run registry. Spares a live batch’s runs, and removes each run’s failure screenshot / tree sidecars. |
/api/runs/active is owner-scoped — never use it to decide a stand is idleIt answers about your runs only. A colleague’s wave occupying the stand is
invisible to it. To ask "is anything running here, whoever owns it", use
|
|
Owner isolation. Run reads, batch reports, WebSocket attach and stop are
owner-scoped — a run is visible only to the account that started it
( |
POST /api/run uses query parameters, not a JSON body. Curl example:
|
curl -X POST "http://ai.friendly-tech.com:8083/api/run?test_file=C7677.yaml&model=opus"
GET /api/runs/{run_id} is the canonical poll endpoint. Note mcp_log is
snake_case and is withheld while the run is still running; the two
failure_*_file fields appear only in this detail response, never in the
list. Returns:
{
"run_id":"...",
"test_id":"C7677",
"title":"...",
"status":"passed|failed|error|running|blocked",
"judge":"programmatic|keyword|llm|none",
"environment":"default-oracle",
"elapsed_ms":42031,
"phase":"loading|pages|executing|parsing|done",
"steps":[{"step":1,"status":"pass|fail|inconclusive|skip","judge":"programmatic","description":"..."}],
"report":{"status":"passed|failed|inconclusive", "steps":[...]},
"output":"...",
"mcp_log":[...],
"failure_screenshot_file":"<run_id>-failshot.png",
"failure_tree_file":"<run_id>-failtree.xml"
}
WebSocket
WS /ws/run/{run_id} — streams live updates for a run.
Through the gateway (every deployed stand) the stream opens with a one-time
ticket, never the session token: first POST /api/runs/{run_id}/ws-ticket
with the usual Authorization: Bearer header, then connect to
/ws/run/{run_id}?ticket=<ticket>. The ticket is bound to the caller’s
account, role and that run, expires after 60 s and is spent by its first use,
so each (re)connect needs a new one. The server logs every WebSocket URL with
its query string; a ticket found there has already been spent. The gateway
refuses with 4401 a missing, unknown, spent or expired ticket, one issued for
another run or for the live view, one whose account was disabled, deleted or
given another role in the meantime, and a ?token=<session token> (no longer
accepted there). It relays to the worker with the ticket holder’s forwarded
identity and no query string; the worker still refuses a run that is not the
caller’s. The gateway has already accepted the client socket by then, so the
client sees a normal close rather than the worker’s 4403.
A worker reached directly, without the gateway (TRUST_GATEWAY unset, local
development only), still authenticates the stream with
?token=<session token>, but the SPA no longer sends one and the worker has no
ws-ticket route: an SPA served that way follows runs by polling only.
Frames are {type, data}, not the shape of GET /api/runs/{run_id}. Types:
phase, info, output, step, ai_step, nav, mcp_call, result,
status, error, image/png, explore, batch_progress, tree_coverage,
tree_coverage_intent, ping. A step frame carries the step’s judge; a
result frame carries the final status, the run judge and its summary; a
terminal status frame is how a blocked run is announced.
TestRail Integration
| Method | Path | Purpose |
|---|---|---|
GET |
|
Connection check. |
GET |
|
List projects. |
GET |
|
Query: |
GET |
|
Query: |
POST |
|
Preview converter output without writing. |
POST |
|
Batch import. Body: |
POST |
|
Single reimport, synchronous. Body: |
POST |
|
Bulk reimport as one background job on the import slot of the worker. Body: |
POST |
|
Start an import job for the cases of the last imported suite that changed in TestRail since their import. Cases never imported are only counted, not imported. The job skips an adapted copy without case files if its source case is on the stand. The result of the copy is |
Converter
Read-only routes behind the Converter tab (scripts/converter_api.py). The journal
lives on the shared case volume, so each worker answers for the jobs of all workers.
See Converter Tab.
| Method | Path | Purpose |
|---|---|---|
GET |
|
The state of the stand: |
GET |
|
The newest jobs of the last three months, newest first, without per-case results; |
GET |
|
One job of the last 12 months with its per-case |
GET |
|
The history of one TestRail case over 12 months: its journal results and its Editor history, newest first. |
GET |
|
A TestRail run or suite compared with the stand, case by case. Query: |
XML Import (API only — no UI)
The XML paste / batch modals were removed from the Editor. A TestRail XML export only names the cases: the import reads each one from TestRail, so it needs a working TestRail connection. See Importing Tests → XML Import API.
| Method | Path | Purpose |
|---|---|---|
POST |
|
Convert a single |
POST |
|
Start an import job for the cases a TestRail XML export names; each case is read from TestRail and converted like an import from the TestRail modal. Returns |
POST |
|
Convert + run a |
Tools
| Method | Path | Purpose |
|---|---|---|
POST |
|
NL → TR-181 tree via |
POST |
|
Run |
GET |
|
Probe each MCP server. Returns |
GET |
|
Detailed MCP diagnostic dump (per-server URLs, last error, last successful tool list). |
Settings
| Method | Path | Purpose |
|---|---|---|
GET |
|
Current persisted settings. |
PUT |
|
Replace settings. Body: full settings JSON. |
DELETE |
|
Wipe UI overrides; effective values fall back to docker-compose env vars and then to hardcoded defaults. |
Admin / Ops
Ops endpoints used by the test-runner UI and by the host-side credential
refresh script. They lean on Portainer (PORTAINER_URL /
PORTAINER_USERNAME / PORTAINER_PASSWORD env vars) and return 503 when
that wiring is missing.
| Method | Path | Purpose |
|---|---|---|
POST |
|
Restart the sibling |
GET |
|
Lightweight probe so the UI can hide the Restart button when Portainer env is unset. Returns |
GET |
|
Inspect on-disk Claude CLI credentials. Returns |
POST |
|
Push fresh |
POST |
|
Push the current |
Validation Against TestRail
| Method | Path | Purpose |
|---|---|---|
GET |
|
Every stored validation report: |
GET |
|
One report. |
POST |
|
Validate up to 50 cases. Body |
POST |
|
Scan a whole project. Body |
GET |
|
|
GET |
|
The case as TestRail holds it today — preconditions and steps, HTML stripped. This is the endpoint behind TestRail source in the editor, and the way to audit a green against the original text. |
Explore and Drafts
| Method | Path | Purpose |
|---|---|---|
POST |
|
Start a read-only survey run. Body |
POST |
|
Turn an explore report into 1–3 proposed QA cases. Body |
GET |
|
Which |
POST |
|
Compile one of them. Body |
DRAFT- and EXPLORE- files are excluded from the test list, from
batches and from the pass-rate maths. Stale drafts auto-purge after 60 days.
|
Diagnosis / Learning
All five require FT_LLM_DIAGNOSE=1 on the worker; with the flag off the
Learning screen has nothing to show.
| Method | Path | Purpose |
|---|---|---|
GET |
|
|
POST |
|
Accept a diagnosis. Body |
POST |
|
Dismiss one. Body |
POST |
|
Accept a learned navigation override. Body |
POST |
|
Dismiss one. Body |