Documentation
Guides, concepts, configuration and the complete REST API for the Testhide CI/CD platform. Deploying? See the installation guide.
Getting Started
TestHide is a self-hosted CI/CD platform built around one idea: when a build or test fails, you should get an explanation — not just a red X. It runs your pipelines on your own agents, then an AI layer (an LLM Investigator plus diagnostic ML models trained on your own history) tells you why a run failed — the likely root cause, whether it's flaky, the most similar past failures, and where to look. Everything runs on your own infrastructure; your logs and source never leave it. It ships as a Docker Compose stack from Docker Hub — no source build required. This guide takes you from an empty server to a first diagnosed build.
What is TestHide?
In one line: failure intelligence for CI/CD. TestHide ingests test reports
and runs CI builds on distributed agents, then analyses every failure with AI. The headline
is the Investigator — an LLM root-cause agent that reads the failing log,
runs tool calls across your build and test data, and returns a plain-language verdict with a
visible per-investigation cost meter. It is multi-provider (Google / OpenAI / Anthropic) and
can run a fully on-device local model, so an investigation can complete
without a single byte leaving your network. Underneath, TestHide vectorises each payload
(MiniLM embeddings for text, CLIP for screenshots) and trains specialised models on
your own history, surfacing insights through the in-app dashboard and a REST API
(/api/v3/). The diagnostic models include:
- Root-Cause Classifier — multi-modal failure categorisation (text + numeric + image), a transformer classifier on a DistilBERT / BERT / RoBERTa backbone.
- Flakiness Predictor — gradient-boosted scoring over rolling test history.
- Failure Retriever — contrastive embeddings + FAISS index for nearest-failure search.
- OOD Detector — autoencoder + isolation forest for novelty scoring.
- Visual Diff — element detection (YOLO) + learned SSIM ensemble for screenshots.
- Log Signature Miner — master-pattern extraction over log lines.
- Bug Linker — dense retrieval over linked issue (Jira) embeddings.
- Emerging Issues — changepoint / z-score trend detection over failure groups.
Which models are active on a given instance depends on the tier's AI feature flags from the synced license; the Free baseline enables root-cause and flakiness. See the tiers & pricing page for what each license unlocks.
Why teams choose TestHide
Root cause, not red/green
The LLM Investigator explains why a run failed — with visible tool calls and a per-investigation cost meter — instead of leaving you to grep logs.
Self-hosted & private
Runs entirely in your own Docker stack. Logs, source and screenshots stay on your infrastructure — with an optional on-device LLM, no data reaches any cloud provider.
Learns your suite
Diagnostic models train on your own failure history — flakiness, root-cause categories, novelty, visual diffs, log signatures — so signal improves the more you run.
Works with any framework
Official reporter plugins (pytest, Jest/Mocha/Vitest/Playwright, xUnit/NUnit/MSTest) feed one canonical report contract — your existing suite just works.
Distributed & resilient
Cross-platform .NET agents with self-healing quarantine, matrix builds across nodes, and external-CI (Jenkins / TeamCity) interop.
Cost-aware multi-provider AI
A provider fallback chain (cloud or local), a monthly spend ceiling, SSRF-guarded outbound calls, and per-tier AI feature flags driven by your license.
Architecture at a glance
A standard install is a single Docker Compose project (name: testhide) made up
of an application core plus a self-contained observability stack. Everything is pulled from
Docker Hub (thuesdays/*) — there is no host repository to clone.
frontend
Nginx + Angular SPA. Terminates TLS and publishes ports 80, 443, and 7771 (the REST API + WebSocket endpoint).
backend ×2
Python REST API + WebSocket (gunicorn), two replicas. Never trains models; talks to the Docker sidecar to launch child containers.
ai-api
LLM / FAISS / CLIP inference (HTTP only, internal). Holds inference models in RAM with an elastic memory budget.
ai-worker
Training, vectorisation, Edge AI, and the async inference queue drain. The only service that trains models.
mongo & redis
MongoDB 8 (primary datastore, auth on) and Redis 7 (cache + pub/sub, password-protected).
sidecar-docker
SEC-004 isolation sidecar that holds the Docker socket; its auth token is auto-derived from JWT_SECRET via HMAC-SHA256.
observability
Prometheus (metrics, loopback-bound), Grafana (dashboards on port 3000, HTTPS), Tempo (traces), Loki (logs), and Alloy (collector).
config-init
One-shot sidecar that syncs the baked Grafana / Loki / Prometheus / Tempo / Alloy configs into named volumes on every --force-recreate.
The default Root-Cause backbone is distilbert-base-uncased
(RC_MODEL_NAME); diagnostic summaries use a local Phi-3.5-mini GGUF model, with
an optional Gemini / OpenAI / Anthropic fallback when keys are configured.
System requirements
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 8 cores | 16 cores |
| RAM | 32 GB | 64 GB |
| Disk | 100 GB SSD | 500 GB SSD |
| Platform | Linux (Ubuntu 22.04+) · Docker 24.0+ with Compose v2 | |
The AI worker reserves 5 GB and may grow to 12 GB during training, while ai-api ranges 1.5–6 GB; the two are anti-correlated by design, so 32 GB is the realistic floor.
Quickstart
The steps below stand up a working instance. They mirror the published
docker-compose.yaml and .env.example; the
installation guide covers each variable in depth.
1 · Prepare the deployment directory
mkdir -p /opt/testhide/{ssl,data/mongo,ssh_keys,sandbox_data,releases,monitoring_scripts}
cd /opt/testhide
curl -O https://testhide.com/static/landing/docker-compose.yaml
curl -O https://testhide.com/static/landing/.env.example && cp .env.example .env
MONGO_DATA_PATH defaults to /opt/testhide/data/mongo; that host
directory must exist and be writable before first launch.
2 · Configure the environment
Edit .env. At a minimum set the public URLs, a unique
JWT_SECRET, and the database / Grafana credentials. Replace every
CHANGE_ME… placeholder before booting.
# Domain & URLs
PUBLIC_URL=https://testhide.yourcompany.com
API_URL=https://testhide.yourcompany.com:7771
WS_URL=wss://testhide.yourcompany.com:7771
FRONTEND_PUBLIC_URL=https://testhide.yourcompany.com
# Security (settings.py refuses to boot in prod/staging with the dev-default secret)
ENVIRONMENT=production
DEBUG=false
JWT_SECRET=$(python3 -c "import secrets; print(secrets.token_urlsafe(64))")
# Data
MONGO_USER=testhide_user
MONGO_PASS=<strong-password>
MONGO_DATA_PATH=/opt/testhide/data/mongo
REDIS_PASSWORD=<strong-password>
# Observability
GRAFANA_ADMIN_PASSWORD=<strong-password>
ENVIRONMENT set to
prod, production, or staging while
JWT_SECRET is still the development default
(a-very-bad-default-secret-for-local-dev) — it exits immediately (SEC-002).
Always generate a unique value with secrets.token_urlsafe(64). Because the
Docker sidecar token is derived from JWT_SECRET, generate it once and never
rotate it casually — rotating invalidates all sessions and breaks the sidecar handshake.
3 · Place TLS certificates
Nginx terminates TLS. Drop your certificate and key in ./ssl/ on the host
(that directory is bind-mounted read-only into the frontend at /etc/ssl). The
container paths are set by CERT_FILE / CERT_KEY, which default to
/etc/ssl/testhide.crt and /etc/ssl/testhide.key.
# Place your real cert + key as ssl/testhide.crt and ssl/testhide.key
chmod 600 ssl/testhide.key
# ...or a self-signed cert for evaluation only:
openssl req -x509 -nodes -newkey rsa:4096 -days 365 \
-keyout ssl/testhide.key -out ssl/testhide.crt \
-subj "/CN=testhide.local" && chmod 600 ssl/testhide.key
Grafana reuses the same cert by default (GRAFANA_CERT_HOST_CRT /
_KEY); override only if it needs a separate certificate.
4 · Launch the stack
docker compose up -d --force-recreate
docker compose logs -f backend ai-api ai-worker
First boot pulls several GB of images plus the AI models — allow 10–15 minutes. The UI
comes up on port 443, the REST API and WebSocket on 7771,
and Grafana dashboards on 3000. Prometheus binds to
127.0.0.1:9090 (loopback only) by default.
5 · Verify
docker compose ps
curl -sk https://localhost/api/v3/health | python3 -m json.tool
A healthy instance returns an ok status with Mongo and Redis connected. If a
service is restarting, check docker compose logs <service> — the most
common first-boot failure is a missing or default JWT_SECRET in production
mode (see the callout above).
Connect a build agent
Builds run on agents (nodes). Install one on any machine and point it at your server with a license key; every supported platform is covered in the installation guide. The quickest path on Linux:
curl -fsSL https://dl.testhide.com/install.sh | sudo bash
sudo testhide config --url wss://YOUR_DOMAIN:7771 --license-key YOUR_LICENSE_KEY
sudo systemctl enable --now testhide
The agent appears on the Nodes page as available once it connects. Agent WebSocket auth is always enforced — every agent must present a valid token derived from its license key or the connection is rejected before the upgrade.
Run your first build
There are two ways to get test results into TestHide:
-
Run it from TestHide. Create a job, add a
test_providerbuild step pointing at your test suite, and run it on an agent. Results, artifacts, and logs are captured automatically. recommended -
Push from your existing CI. Add the matching framework reporter plugin
(pytest, unittest, JS/TS, or .NET) so your runner emits a TestHide report file, then have
a
test_providerstep ingest it.
When a build fails, the AI layer diagnoses it automatically: open the build's
Overview and Investigate tabs to see the root cause,
similar past failures, and suspect commits. Model accuracy improves as labelled history
accumulates — watch training progress on the AI Assist dashboard at
/ai-assist.
Reports & Builds
Builds are the central entity in Testhide. Every CI run produces a build record that
aggregates its pipeline, test results, AI insights, LLM-eval steps, artifacts, and the
captured environment. The builds list lives at /builds; a single build opens at
/build/:id/overview inside a shell that keeps a left navigation rail visible
across every build view.
Build lifecycle & statuses
A build moves through a fixed set of canonical statuses. The wire values are lowercase
American spellings (the canonical source is app/build/models.py →
Build); the client tolerates British cancelled but emits
canceled.
| Status | Bucket | Meaning |
|---|---|---|
pending | queue-eligible | Queued by the dispatcher, waiting for a free node slot. |
discovering | active | Agent is resolving the test set / pipeline before execution. |
in_progress | active | Executing on an agent. |
running_provider | active | A test-provider (TPS) step is actively running. |
stopping | active | Cancel-in-flight; the agent is winding the run down. |
interrupted | active | Agent lost mid-run. Recoverable — may resume. |
passed | terminal | Finished with no failures. |
failed | terminal | One or more tests failed. |
canceled | terminal | Stopped gracefully by a user. |
terminated | terminal | Hard SIGKILL-level termination, distinct from graceful cancel. |
error | terminal | Agent / infrastructure failure during the run or finalize (not a test failure). |
aborted | terminal | Invalidated by a job-delete or license change. |
The concurrent-build limit enforced by TierEnforcementService
(max_concurrent_builds) keeps builds in pending until a slot is
free. Use Push to Top to jump a pending build to the front
of the queue.
The build page
BuildShellComponent renders a persistent navigation rail beside every build
view. Each entry routes under /build/:id.
| Tab | Route | What it shows |
|---|---|---|
| Overview | /overview | Build summary, duration, test counts, live progress, and the matrix child rollup for parent builds. |
| Output | /output | Raw agent console-log stream. |
| Parameters | /parameters | Build parameters and per-run overrides. |
| Env Vars | /envs | Environment variables captured at build time. |
| Artifacts | /artifacts | Uploaded files, screenshots and reports, browsed as a tree. |
| AI Evals | /ai-evals | Results of every llm_eval step (see Build Steps). |
| AI Predictions | /ai-predictions | Build-level model output: build root cause, log signature, similar failures, suspect commits, step anomaly. Test-level predictions appear in each test's detail panel, not here. |
| Dependencies | /dependencies | The depends_on graph of builds that must pass first. |
| Investigate | /investigate | The AI build investigator. Requires AI_ASSIST_READ. |
| JSON | /json | Raw build document for debugging. |
The rail also carries cross-build links (View / Edit Job, Run) and a job build-history panel.
Build actions & permissions
Actions are available from the rail and the overview, each gated by a
BUILDS_* permission. Routes below come from the server route table
(app/routes.py); handlers live in
app/build/build_actions_view.py,
build_start_view.py and build_delete_view.py.
| Action | Endpoint | Permission | Notes |
|---|---|---|---|
| Rebuild / Restart | POST /api/v3/builds/restart |
BUILDS_EXECUTE | Re-run a finished build with the same config. POST /api/v3/build/restart_child restarts a single matrix child. |
| Run / Start | POST /api/v3/builds/start |
BUILDS_EXECUTE | Trigger a fresh build for the job. |
| Stop | POST /api/v3/builds/stop |
BUILDS_EXECUTE | Cancel an in-flight build. |
| Push to Top | POST /api/v3/build/:id/push_to_top |
BUILDS_EXECUTE | Promote a build to the front of the queue. Only valid while the build is pending. |
| Retry Flaky Tests | POST /api/v3/builds/:id/retry-flaky |
BUILDS_EXECUTE | Re-run only tests scored flaky. Re-queues into the existing TPS state (does not create a new build). Requires the parent build to be terminal; a GET on the same URL previews candidate tests. |
| Keep Forever | POST /api/v3/build/:id/keep-forever |
BUILDS_WRITE | Exempt the build from the retention cron. |
| Set Tags | POST /api/v3/build/:id/tags |
BUILDS_WRITE | Free-form labels (each ≤ 50 chars) for filtering and search. |
| Set Comment | POST /api/v3/build/:id/comment |
BUILDS_WRITE | Free-form annotation (truncated to 2000 chars). |
| Optimization Plan | GET /api/v3/builds/:id/optimization-plan |
BUILDS_READ | "Smart Runner" suggestion: env-var overrides (e.g. PYTEST_ADDOPTS) for restructuring the run. |
| Delete | POST /api/v3/build/:id/delete |
BUILDS_DELETE | Remove the build and its data. POST /api/v3/builds/delete bulk-deletes. |
Every mutating build action additionally enforces per-project tenant scope: a caller with a
global BUILDS_* permission still cannot act on a build in a project outside
their token scopes.
Matrix builds
A matrix build has one parent document and N child builds — one per
parameter combination (OS, browser, shard, etc.). The parent stores a
child_rollup read-model so the overview can show a "N/M completed · K
failed" summary live, without recounting on the client. Restarting only failed children is
supported per child via restart_child, and Retry Flaky
Tests works at both parent (all children) and single-child scope.
Matrix builds are gated by the matrix_builds feature flag, which is off on the
Free tier and enabled by a paid license. The flag comes from the synced license, not an
environment variable.
Data retention
A daily cron deletes builds (and their jobs' artifacts and AI data) older than the tier's
retention_days; builds flagged Keep Forever are
exempt. The Free baseline is 7 days
(app/services/tier_enforcement.py); longer windows for paid tiers come from the
synced license rather than being hard-coded.
A build can also pin itself out of cleanup at the job level: when a job enables
discard_old_builds with a max_of_builds count, only the most
recent N builds are retained regardless of age.
Build Steps
Build steps are the executable units of work inside a Testhide job. The step list is edited on
the Build tab of the job editor (/jobs/edit/:id) and is stored on
the job and its builds as build_step[]. Each step has a type and a
typed configuration; the agent runs them in order. The llm_eval type adds
LLM-as-judge / snapshot evaluations as a native part of the pipeline.
Step types
The supported step types are enforced by the job validator
(VALID_STEP_TYPES in app/job/validators.py). Unknown types are
rejected with UNKNOWN_STEP_TYPE on save.
test_provider
Run a test runner (pytest, JUnit, etc.), shard tests across agents, and collect results.
Supports include/exclude filters and per-label restrictions. tests_path
defaults to tests and provider to pytest.
batch
Run a Windows batch script on the agent. Requires a non-empty script.
power_shell
Execute a PowerShell script on the agent. Requires script (the alias
powershell is also accepted).
bash
Execute a Bash / shell script on the agent. Requires script (the alias
shell is also accepted).
python
Run a Python script (uses the build's Python environment when enabled). Requires
script.
npm
Run an npm command (e.g. install) in a working directory. Requires
command.
npx
Run an npx command in a working directory. Requires command.
node
Execute a Node.js script by path, with args and binary location. Requires
script_path.
copy_artifacts
Copy artifacts from another job's build (e.g. latest successful) into a target directory.
docker beta
Run a step inside a Docker container. Requires an image; an optional
auth_user_id (24-char hex) supplies registry credentials. Currently routed to
a bash stub.
llm_eval
LLM-as-judge / snapshot evaluation step. Requires either an input_dataset or
an inline cases[] list, plus a judge object (see below).
Most script steps share an always_run flag (run even if a previous step failed)
and an optional timeout_seconds (1–86400). Steps can be reordered by
drag-and-drop in the editor; step names must be unique within a job.
llm_eval — YAML reference
The llm_eval step nests its judge and threshold under typed sub-objects. The field
names below match the backend parser (app/build/llm_eval_step.py) and the
job-editor form.
build_step:
- type: llm_eval
name: "customer-support-quality"
input_dataset: data/customer-questions.jsonl # optional candidate file (.jsonl, relative path, <= 50 MB)
# cases: # ...or inline cases instead of input_dataset
# - id: case-1
# expected: "the snapshot / golden answer"
# actual: "candidate output" # optional; omit for smoke mode (auto-pass)
judge:
kind: llm_as_judge # llm_as_judge | snapshot
model: gpt-4o-mini # judge model (default: gemini-2.0-flash)
prompt: registry://judges/customer-support-quality/v3
temperature: 0.0
# mode: exact # snapshot only: exact | fuzzy | semantic (default exact)
threshold:
metric: judge_score
min: 0.80 # pass threshold, must be in [0.0, 1.0]
parallelism: 4 # concurrent cases (>= 1, server cap 32)
cost_budget_usd: 0.50 # abort the step if estimated cost exceeds this (server cap 50.0)
on_fail: pr_check_fail # pr_check_fail | warn | continue
| Field | Default | Constraint |
|---|---|---|
input_dataset / cases | — | One is required. input_dataset must be a relative path inside the workspace and ≤ 50 MB. |
judge.kind | llm_as_judge | Executor accepts llm_as_judge or snapshot only. |
judge.model | gemini-2.0-flash | Required for llm_as_judge (and rubric). |
judge.prompt | built-in | Optional registry://<name>/<version> reference. |
judge.mode | exact | snapshot only: exact | fuzzy | semantic. |
threshold.min | 0.80 | Float in [0.0, 1.0]. |
parallelism | 4 | Integer ≥ 1 (server max 32). |
cost_budget_usd | 1.0 | > 0 (server max 50.0). |
on_fail | pr_check_fail | pr_check_fail | warn | continue. |
The backend executor only runs judge.kind of llm_as_judge or
snapshot. The job-editor schema additionally accepts experimental
rubric and pass_at_k kinds (and normalizes the alias
llm → llm_as_judge), but those are not yet executed
server-side.
For llm_as_judge, prompt may be a
registry://<name>/<version>
reference into the prompt registry; the registry browser in the editor inserts these for you.
If the reference cannot be resolved, the runner falls back to a built-in default prompt.
The validator also auto-migrates legacy shapes on save: flat
judge_kind/judge_model/judge_prompt_ref/judge_temperature
fields move into judge.*, cost_limit_usd becomes
cost_budget_usd, and per-case case_id/snapshot become
id/expected.
Step execution flow
For most step types the agent runs the script/command locally and reports an exit code. The
llm_eval step is special: it is executed server-side over WebSocket so the judge
has access to the build data and prompt registry.
- The agent's build executor reaches an
llm_evalstep and sends anLLM_EVAL_STEP_REQUESTWebSocket message (build_id,step_index,eval_yaml) to the backend. - The backend handler (
app/ws_handler/handlers/llm_eval.py) enforces the AI-evals license quota and server caps (max 500 inline cases, cost budget ≤ $50), then builds the judge —LLMJudgeforllm_as_judge(resolving anyregistry://prompt) orSnapshotJudgeforsnapshot. - The work runs as a detached task, so PING / BUILD_FINISHED keep flowing.
EvalRunner.run_for_buildscores each case, streaming anLLM_EVAL_PROGRESSframe back to the agent per case. - The result is persisted into the build document at
cfg.build_step[step_index].eval(and the run row indb_eval_runs) so the AI Evals tab can render it. - The backend returns an
LLM_EVAL_STEP_RESULTmessage (exit code, pass/fail counts, pass rate, cost, tokens, per-case judge outputs). - The agent applies
on_fail:pr_check_failmarks the step (and build) failed when below threshold, whilewarn/continuekeep the build green.
Exit codes map to step status: 0 passed, 1 failed (below
threshold), 2 aborted (cost budget exceeded), 3 error (config,
judge, license, or runner failure). If the agent WebSocket disconnects mid-run, the in-flight
eval task is cancelled server-side.
Viewing results
Open a build and select the AI Evals tab in the build rail
(/build/:id/ai-evals). It loads
GET /api/v3/builds/:id/evals (permission BUILDS_READ)
and lists every llm_eval step with:
- Step name (
eval_id) andstep_index. - Pass / fail outcome with
pass_rateand thepassed/failed/total_casescounts. - Estimated cost (
cost_usd) andtokens_used, aggregated across cases. - Regression / improvement deltas vs. the previous run.
- Expandable per-case judge output (
case_id, verdict, score, reasoning, raw response, cost, tokens, latency). - For snapshot judges, a "View diff" action opening the snapshot diff viewer.
Plugins
Testhide ingests test results from your CI pipeline. The official reporter plugins
are what you add to your test runner so it emits results in a format Testhide understands. Each
plugin writes a single Testhide Report Format v1 file
(junittests.xml) — JUnit-extended XML with a stable failure id, a resolution,
captured output, attachments and suite metadata — which the build agent then collects and
uploads. Plugins exist for pytest
unittest JavaScript / TypeScript
.NET.
The common model
Every plugin produces the same on-disk contract, so the agent and the AI layer parse results identically regardless of language. The flow is always the same:
- Add the reporter to your test runner — a CLI flag, a config-file entry, a logger, or an attribute, depending on the framework.
- Run your tests. The plugin writes
junittests.xmlincrementally: each test result is flushed immediately, so a partial report survives even an interrupted run. - The Testhide agent collects the report. A
test_providerbuild step runs the runner on an agent, which reads the report file and pushes the build to the Testhide REST API.
The agent (and any direct API caller) authenticates to the Testhide server with a
Personal Access Token. The token, the server URL, and the project a build belongs
to are configured once on the agent / job — not inside each plugin. The plugins
themselves never talk to the network (the optional exception is Jira enrichment); they only write
the report file. Run identifiers such as the build number and branch travel as suite
metadata that you attach on the command line (e.g.
build=1042, branch=main); the reserved build and
branch keys get special handling in Testhide, everything else is stored as free-form
metadata.
Stable failure ids. Every plugin derives the same
fail_id = md5("module.class.test.ExceptionType(message)"), the key Testhide uses to
deduplicate failures across builds and link them to Jira. The shared contract also includes
test_resolution (e.g. Passed, Unresolved,
Collection Error, Skipped, Known Issue), source
file / line, captured
<system-out>, and
testhide_schema_version=1. Passed and skipped tests carry an empty
fail_id.
Compatibility overview
| Framework | Package | Runner(s) | Runtime | Install |
|---|---|---|---|---|
| pytest | testhide-pytest-plugin |
pytest 7+ (+ xdist, rerunfailures) | Python ≥ 3.8 | pip install testhide-pytest-plugin |
| unittest | testhide-unittest-plugin |
stdlib unittest |
Python ≥ 3.8 | pip install testhide-unittest-plugin |
| JavaScript / TypeScript | @testhide/reporters |
Jest · Mocha · Vitest · Playwright | Node ≥ 14 | npm install --save-dev @testhide/reporters |
| .NET | Testhide.Reporting.VSTest |
xUnit · NUnit · MSTest (via dotnet test) |
.NET Standard 2.0 (.NET 6/7/8 + Framework) | dotnet add package Testhide.Reporting.VSTest |
All plugins write to junittests.xml by default and are safe under parallel / sharded
execution: each worker writes to a temp chunk that is atomically merged (file-locked) into the
final report.
pytest
Package testhide-pytest-plugin (PyPI, Python ≥ 3.8). It registers itself as a
pytest plugin via the pytest11 entry point — there is nothing to import.
Reporting is off until you pass the --report-xml flag, which both
enables the plugin and sets the output path.
Install
pip install testhide-pytest-plugin
Enable & run
pytest --report-xml=junittests.xml
Suite name & run metadata
Set the <testsuite name="..."> and attach
build/run identifiers with repeatable --meta KEY=VALUE options:
pytest --report-xml=junittests.xml \
--suite-name=api-tests \
--meta build=1042 --meta branch=main
Options
| Option | Meaning |
|---|---|
--report-xml PATH | Enable reporting and set the output file. |
--suite-name NAME | <testsuite name> (default pytest). |
--meta KEY=VALUE | Add a suite property (repeatable). build / branch are reserved run identifiers. |
--quarantine-file PATH | Deselect listed test node ids before the run (also TESTHIDE_QUARANTINE_FILE; auto-discovers .testhide_quarantine_file). The agent writes this to skip quarantined tests. |
--jira-url / --jira-username / --jira-password | Optional Jira enrichment — links failures to known issues by fail_id. |
- Parallelism — fully compatible with
pytest-xdist; add-n autoand the plugin merges results from every worker. - Flaky / retries — works with
pytest-rerunfailures; every rerun of a failing test is recorded as a separate<testcase>for accurate instability tracking. - Attachments & metadata — inject per-test artifact links and suite
metadata through the
pytest_testhide_get_test_case_propertiesandpytest_testhide_add_metadatahooks.
# parallel run with reruns
pytest -n auto --reruns 5 --report-xml=junittests.xml
unittest
Package testhide-unittest-plugin (PyPI, Python ≥ 3.8). Python's built-in
unittest has no plugin system, so the reporter ships as a thin wrapper module you
invoke instead of python -m unittest. Use the optional [jira] extra for
Jira enrichment.
Install
pip install testhide-unittest-plugin
# optional Jira enrichment:
pip install "testhide-unittest-plugin[jira]"
Enable & run (CLI)
Run the wrapper exactly like unittest; anything unittest accepts is
passed through, and the Testhide options are consumed by the wrapper:
# discover & run, writing the report:
python -m testhide_unittest discover -s tests -p "test_*.py" \
--report-xml junittests.xml --suite-name api-tests \
--meta build=1042 --meta branch=main
Options
| Option | Meaning |
|---|---|
--report-xml PATH | Output report (default junittests.xml). |
--suite-name NAME | <testsuite name> (default unittest). |
--meta KEY=VALUE | Add a suite property (repeatable) — e.g. build / branch. |
--quarantine-file PATH | Skip listed test ids (also TESTHIDE_QUARANTINE_FILE / .testhide_quarantine_file). |
--no-capture | Do not capture stdout/stderr into <system-out>. |
--jira-url / --jira-username / --jira-password | Optional Jira enrichment by fail_id. |
The wrapper exits 0 when all tests pass and 1 otherwise, so it drops straight into CI.
Programmatic use & per-test enrichment
import unittest
import testhide_unittest as th
class LoginTests(unittest.TestCase):
def test_login(self):
"User can log in with valid credentials." # docstring -> docstr (automatic)
th.attach("/tmp/screenshot.png") # repeatable; images/logs/json
th.set_info('{"env": "staging"}') # free-form context
th.set_jira("PROJ-123") # link a ticket
...
Or drive it from your own entry point with the runner, passing suite metadata directly:
import unittest
import testhide_unittest
runner = testhide_unittest.TesthideTestRunner(
report_path="junittests.xml",
metadata={"build": "1042", "branch": "main"},
)
unittest.main(testRunner=runner)
The writer is parallel-safe: each test is written to a temp chunk under
.{report}_temp/ and atomically merged (file-locked) into the final report.
JavaScript & TypeScript
Package @testhide/reporters (npm, Node ≥ 14). One package exposes a sub-path per
framework — Jest, Mocha, Vitest and
Playwright — that you register as a reporter. The frameworks are optional
peer dependencies, so you only install the one you use. Every reporter takes the same options
object: { outputPath, suiteName, meta } (outputPath also reads the
TESTHIDE_REPORT_XML env var).
Install
npm install --save-dev @testhide/reporters
Enable per framework
Jest — jest.config.js
module.exports = {
reporters: [
'default',
['@testhide/reporters/jest', {
outputPath: 'junittests.xml',
suiteName: 'api-tests',
meta: { build: '1042', branch: 'main' },
}],
],
};
Mocha — .mocharc.json or CLI
mocha --reporter @testhide/reporters/mocha \
--reporter-options outputPath=junittests.xml
Vitest — vitest.config.ts
export default {
test: {
reporters: [
['@testhide/reporters/vitest', { outputPath: 'junittests.xml' }],
],
},
};
Playwright — playwright.config.ts
export default {
reporter: [
['@testhide/reporters/playwright', { outputPath: 'junittests.xml' }],
],
};
With Playwright sharding, give each shard a distinct outputPath
— each shard writes its own report file. Per-test enrichment (docstr /
attachment) isn't part of these frameworks' result models and is omitted in v1; the
core contract (outcome, fail_id, stack, duration, file /
line, suite metadata) is fully captured.
.NET
The .NET reporter ships as a single VSTest logger that works for
xUnit, NUnit and MSTest, because they all run
through dotnet test. The only package you need is
Testhide.Reporting.VSTest (its FriendlyName is testhide);
the shared writer Testhide.Reporting.Core is pulled in automatically.
Install & use
dotnet add package Testhide.Reporting.VSTest
dotnet test --logger "testhide;LogFilePath=junittests.xml"
Per-framework meta-packages (Testhide.Reporting.Xunit, .NUnit,
.MSTest) exist purely for discoverability — they contain no code and just depend
on the VSTest logger.
If dotnet test reports the testhide logger is not found, reference the
package from the test project so its logger DLL is copied to the test output, then
re-run.
Logger parameters
Parameters are name=value pairs, ;-separated:
| Parameter | Meaning |
|---|---|
LogFilePath | Output report path (default junittests.xml; relative paths resolve under TestResults/). |
SuiteName | <testsuite name> (default dotnet). |
meta.KEY=VALUE | Add a suite property — e.g. meta.build=1042;meta.branch=main. |
JiraUrl / JiraUsername / JiraPassword | Optional Jira enrichment by fail_id. |
Example with metadata
dotnet test --logger "testhide;LogFilePath=junittests.xml;SuiteName=api-tests;meta.build=1042;meta.branch=main"
- Source mapping —
file/lineare captured when the framework provides them (e.g. with portable PDBs / SourceLink). - Per-test metadata — test traits named
docstr/jira/infomap to report properties, and result attachments becomeattachmentproperties.
Getting reports into Testhide
Once your runner writes junittests.xml, a test_provider build step on
the agent collects it and the build is pushed to the Testhide API. Point the job's
report_paths (Test reporting → report paths) at the file; the agent parses it
after the run and uploads the structured results — no extra API calls from your tests. The
agent authenticates with a Personal Access Token; the server URL and the target project are part
of the agent / job configuration, not the plugin.
Format reference. All four plugins emit Testhide Report Format v1 and ship a
conformance/ validator that runs in CI, so the agent is guaranteed to parse the
output. You normally never edit the XML by hand — let the plugin write it.
Nodes & Agents
A node (or agent) is a worker machine that runs your builds.
Each agent opens a persistent WebSocket back to the backend, advertises its labels and
capabilities, and receives build steps to execute. Nodes are managed under
Settings → Nodes; the backing collection is db_nodes. This
guide covers the node model, the health system, installing agents on every platform, pools,
per-node Python environments, the Docker agent pool, and the soft-delete lifecycle.
Static vs dynamic nodes
Every node carries two classification fields: node_type
(static or dynamic) and source
(binary or docker).
Static / binary
A long-lived agent you install on a physical or virtual host (Windows, Linux, macOS). It
reconnects across restarts and keeps its identity. This is the default
(node_type=static, source=binary).
Dynamic / docker
An ephemeral agent spawned on demand as a container
(node_type=dynamic, source=docker). It connects, runs work, and
is destroyed. See Docker pool & spawning below.
A node's stable identity is its node_uuid (MAC/hostname-independent), falling
back to secure_id then name on reconnect. The secure_id
is the agent handshake identity and is never serialized to the browser.
Node statuses
| Status | Meaning |
|---|---|
| available | Connected and idle; eligible for dispatch. |
| busy | Currently running one or more builds. |
| quarantined | Excluded from dispatch — either auto-quarantined by the health system or manually quarantined by an operator. |
| unavailable | No active WebSocket (offline). The list view shows any node without a live ws_id as unavailable regardless of its stored status. |
Disconnected nodes are recovered by a reconciliation task that fixes busy nodes
with no active builds and revives unavailable nodes that still hold a verified
WebSocket. You can run it on demand with Force Reconcile.
Node health & the penalty system
Each node accumulates a health_penalty driven only by genuine
infrastructure faults — build-content failures (your failing tests) and indeterminate
outcomes are fail-open and add zero penalty. The score shown in the UI is
derived as score = 100 − penalty × 5.
| Constant / state | Value | Effect |
|---|---|---|
QUARANTINE_THRESHOLD | 15 | Penalty at which the node is auto-quarantined. |
MAX_PENALTY | 20 | Hard cap so healing time stays bounded. |
state = healthy | penalty 0 | Nominal. |
state = warning | 0 < penalty < 15 | Accruing fault, still dispatchable. |
state = unhealthy | penalty ≥ 15 | Auto-quarantined. |
- Critical reasons (
disk_full,read_only_fs,bad_interpreter,docker_daemon_down) jump straight to the quarantine threshold. - Healing — each clean build decrements the penalty; the penalty also time-decays, so a transient infra blip recovers on its own.
- Disconnect grace — one disconnect is tolerated; restart/update-driven disconnects add no penalty.
- Release residual — releasing a node from quarantine leaves it one heal below the threshold so a still-sick host re-quarantines on the next fault rather than flapping back to available.
Installing agents
Download the agent from Settings → Nodes → Download Agent. The modal
is populated from the release manifest on the CDN (R2): it prefers
launcher_assets / launcher_version when present and falls back to the
raw client assets. Platforms surfaced: Windows x64, Linux x64, macOS Intel, and
macOS ARM.
Windows
Windows 10 (64-bit) / Server 2016 or newer. The .NET runtime is provisioned via bundled standalone binaries; run the installer and the agent registers as a background service.
Linux (.deb / .rpm)
Debian/Ubuntu (.deb) and RHEL/Fedora (.rpm) packages are
published to the APT/YUM repositories on the CDN, so hosts install and stay current
through their native package manager. Requires glibc ≥ 2.28 and libstdc++ ≥ 3.4.25;
systemd is recommended for the daemon.
macOS
Version 12 (Monterey) or newer; x86 runs through Rosetta 2 or a native build.
Docker
The official container image is thuesdays/testhide-agent (default tag
:latest) — run it directly or let the platform spawn it for you.
The launcher wraps the client binary and handles auto-update: it watches the
release manifest and migrates to the published launcher_version without a manual
reinstall, so a fleet can be rolled forward centrally.
Linux quickstart
curl -fsSL https://dl.testhide.com/install.sh | sudo bash
sudo testhide config --url wss://YOUR_DOMAIN:7771 --license-key YOUR_LICENSE_KEY
sudo systemctl enable --now testhide
Docker agent image
docker run -d \
-e TESTHIDE_URL=wss://your-host:7771 \
-e TESTHIDE_LICENSE_KEY=<license-key> \
-e TESTHIDE_LABELS=docker,ephemeral \
-e TESTHIDE_NODE_TYPE=dynamic \
-e TESTHIDE_SOURCE=docker \
--network testhide-net \
thuesdays/testhide-agent:latest
Agent authentication token
Every agent authenticates its WebSocket handshake by sending
?token=SHA256(licenseKey) as a query parameter (browsers can't set WS headers, so
the token rides on the URL). The backend validates it against the configured instance license
key and rejects with HTTP 401 before any WS upgrade — an unauthenticated
client never enters the hub.
GET /api/v3/node/connect?token=<sha256-hex-of-license-key>
# Backend accepts either SHA256(rawKey) or SHA256(UUID-normalized key).
NODE_WS_AUTH_MODE knob (off → warn → enforce) while the fleet was updated
to send the token; that knob is gone and enforcement is now unconditional. The comparison is
timing-safe (hmac.compare_digest).
The license key is provisioned out-of-band on the agent host — it is never learned from the backend. Provide it via any of:
- the
TESTHIDE_LICENSE_KEYenvironment variable, - a
testhide_override.jsonfile, or - the agent's
configcommand.
Node pools
A node pool is a named group of nodes that builds target via
execution_target: pool:<name>. Routing resolves candidate nodes by pool and
by label. Manage pools under Settings → Nodes → Pools (the
node_pools collection).
| Field | Default | Meaning |
|---|---|---|
labels | [] | Metadata tags for UI / policy use. |
max_builds_per_node | 1 | Max concurrent builds per node in this pool (-1 = node default). |
priority_boost | 0 | Score bonus added to builds targeting the pool (higher = dispatched earlier). |
max_concurrent | -1 | Total concurrent builds across all nodes in the pool (-1 = unlimited). |
A node's own concurrent_executors caps how many builds it runs at once. A pool
cannot be deleted while active nodes still reference it — unassign the pool from all nodes
first.
Per-node Python ENV management
Each node can host managed Python environments, driven over the agent WebSocket from
POST /api/v3/node/{id}/envs (requires NODES_WRITE). Supported
actions: list, create, delete,
reinstall_requirements, list_packages, check_health.
- Type —
virtualenv(created from a base interpreter) orpyenv(install/manage interpreter versions). - Location —
globalorworkspace. - Requirements — a requirements blob (≤ 64 KB) installed/reinstalled into the env.
- Inspect — list installed packages and run a health check.
POST /api/v3/node/{id}/envs
Content-Type: application/json
Requires: NODES_WRITE
{
"action": "create",
"type": "virtualenv",
"name": "api-tests",
"location": "workspace",
"base_python": "3.11",
"requirements": "pytest==8.2.0\nrequests"
}
[A-Za-z0-9][A-Za-z0-9._-]{0,63}, no shell metacharacters)
because the agent interpolates them into pyenv shell commands. If a node is offline the call
returns 424 Failed Dependency so the UI renders an inline "node offline"
notice rather than a generic error.
Per-node remote operations
From a node's row or detail view you can drive a range of remote operations. The
non-destructive ones require NODES_READ; the destructive/fleet ones require the
superuser REMOTE_CONTROL_EXECUTE permission.
- SSH terminal — a browser-based xterm. The backend provisions a one-off SSH
key to the target username, the agent confirms deployment, then the backend bridges
stdin/stdout over WebSocket. Requires
ssh_enabledon the node andREMOTE_CONTROL_EXECUTE. - Quarantine / Release — manually take a node out of dispatch or return it.
Release restores the pre-quarantine status, drops the penalty to one heal below threshold,
and refreshes the stored health snapshot. (
REMOTE_CONTROL_EXECUTE) - Reset penalty — clear
health_penalty(optionally also resetting status) for one or many nodes. (REMOTE_CONTROL_EXECUTE) - Force Reconcile — run the reconciliation task immediately to fix stuck
busynodes and revive falsely-offline ones. (REMOTE_CONTROL_EXECUTE) - Clean disk — instruct selected agents to clear their working/temp space.
(
REMOTE_CONTROL_EXECUTE) - Get log — pull the agent's recent log over RPC. The backend resolves the
node's live
ws_idfirst so multi-instance reconnects don't time out. (NODES_READ) - Remote screen — view the node's live screen / screencast for debugging GUI runs.
- Execute / Restart / Force-update / Install tooling — run a shell command
(
batch/powershell/python/bash, server-enforced 10-min wall + 5-min idle timeouts), restart the agent, force an update, or push Python/Git installers. AllREMOTE_CONTROL_EXECUTE.
Docker pool & spawning + resource limits
Ephemeral dynamic agents are spawned as containers from the image configured in
Configuration → Docker Agents. The backend never touches the Docker
socket directly — all container lifecycle goes through a hardened sidecar, and spawns are
serialized with a Redis lock so multiple backend workers can't race the
max_containers limit.
POST /api/v3/nodes/docker-agents/spawn # body: { labels, memory_limit, instance_id }
GET /api/v3/nodes/docker-agents # list tracked containers (live status)
POST /api/v3/nodes/docker-agents/{id}/stop # stop
POST /api/v3/nodes/docker-agents/{id}/start # start
DELETE /api/v3/nodes/docker-agents/{id} # destroy (stop + remove + soft-delete node)
GET|POST /api/v3/nodes/docker-agents/config # read / update config
On spawn, the backend injects the WS URL and license key as environment
(TESTHIDE_URL, TESTHIDE_KEY, TESTHIDE_LICENSE_KEY,
TESTHIDE_LABELS, TESTHIDE_NODE_TYPE=dynamic,
TESTHIDE_SOURCE=docker, plus TESTHIDE_INSTANCE_ID and
TESTHIDE_SERVICE_MODE=1), attaches the container to the testhide-net
bridge, and polls up to 90 s for the agent to connect. Spawn progress and live container
logs stream to the UI over WebSocket events.
| Resource limit | Config key | Default |
|---|---|---|
| Memory | docker_agent_memory_limit | 2g |
CPU cores (--cpus) | docker_agent_cpu_limit | 2.0 |
Max processes (--pids-limit) | docker_agent_pids_limit | 512 |
| Max running containers | docker_agent_max_containers | 5 |
| No new privileges | docker_agent_no_new_privileges | true |
| Idle auto-cleanup (min) | docker_agent_idle_timeout_min | 30 |
Each spawned agent is capped at the configured CPU and PID limits and runs with
no-new-privileges so a runaway build can't DoS the host. An idle reaper destroys
containers that have been idle longer than docker_agent_idle_timeout_min.
Destroy stops and removes the container and soft-deletes the node. Spawn,
destroy, start, stop, and config-write are all REMOTE_CONTROL_EXECUTE; listing and
reading config require NODES_READ.
Delete, restore & purge
Deleting a node is a soft delete: the document is moved from
db_nodes to the db_deleted_nodes archive with
deleted_at / deleted_by metadata. Any active builds running on the
node are failed first so they don't become permanent orphans. Single delete, bulk delete, and
Docker-agent destroy all use this path. Deleting requires the NODES_DELETE
permission.
- Deleted Nodes archive — browse soft-deleted nodes at
GET /api/v3/nodes/deleted(the "Deleted Nodes" view). RequiresNODES_READ. - Restore —
POST /api/v3/nodes/deleted/{id}/restoremoves an archived node back todb_nodes; it returns asunavailableuntil it reconnects. (NODES_WRITE) - Purge (single) —
POST /api/v3/nodes/deleted/{id}/purgepermanently hard-deletes one archived record. Cannot be undone. (NODES_DELETE) - Purge all —
POST /api/v3/nodes/deleted/purgepermanently empties the entire archive in one shot. (NODES_DELETE)
External CI (Jenkins & TeamCity)
TestHide interoperates bidirectionally with Jenkins and TeamCity. A TestHide job can depend on an external build (wait for it to pass before running), be triggered by one (an external build that finishes starts a TestHide build), and trigger one (when a TestHide build finishes it kicks off an external build). Linked builds show a live external-CI badge on their build cards, and external Jenkins/TeamCity nodes are rendered alongside internal build dependencies on the build Dependencies page.
Overview
The integration ties an external CI server to a TestHide job through three independent mechanisms — a dependency gate, an inbound trigger, and an outbound action — all configured per job on the External CI tab of the job editor. Status flows both ways: TestHide polls the external server (and accepts inbound webhook pushes) to keep build-card badges and the Dependencies graph current, and it calls the external server's API to start builds and read results.
Depend on (gate)
Hold a TestHide build in the queue until a chosen Jenkins/TeamCity build passes. A failed external dependency cancels the TestHide build; a still-running one holds it.
Triggered by (inbound)
An external build that finishes with a matching status starts this TestHide job, via an authenticated inbound webhook.
Trigger out (outbound)
When this TestHide build finishes, start an external Jenkins/TeamCity build, optionally passing parameters with placeholder substitution.
Live status & badges
Linked builds carry a provider-icon badge that updates live; external nodes appear on the build Dependencies page, color-coded by status and click-through to the external build.
1 · Connect a CI server
Register each Jenkins or TeamCity server once under Configuration → Endpoints
and add a new endpoint (type jenkins or teamcity). This requires the
CONFIGURATION admin permission.
- Jenkins — Base URL + Username + API token
(create the token under the Jenkins user's Configure → API Token). Outbound
triggers send a crumb-protected
buildWithParametersrequest. - TeamCity — Base URL + API token (a bearer access token), or a username / password pair.
- Inbound-webhook secret (optional) — a per-endpoint shared secret used to authenticate notifications that the CI server pushes back to TestHide (see Triggers).
- TLS verification — a toggle to verify the server's certificate; turn it off only for an internal server using a self-signed certificate.
After filling the form, use the Test Connection button
(POST /api/v3/integrations/ci/test) to validate the URL and credentials before
saving. A green result confirms TestHide can reach the server and authenticate; the
job discovery picker (POST /api/v3/integrations/ci/jobs) then
lists the external jobs / build configurations.
id is referenced everywhere else in the integration — by the job's
External CI tab and by the inbound webhook URL. Copy it after saving.
2 · The External CI job tab
Open a job at /jobs/edit/:id and select the External CI tab. It
has three independent subsections; configure any combination. Saving requires
JOBS_WRITE.
Dependencies (gate)
The job's builds wait until the chosen external Jenkins/TeamCity build passes before they run. Fields:
- Endpoint — which registered CI server to query.
- External job / build configuration (ref) — the Jenkins job name or TeamCity build-configuration id to watch.
- Branch — restrict the gate to a specific branch.
- Timeout (optional,
timeout_minutes) — how long to hold the build before the gate fails it.
A failed external dependency cancels the TestHide build; a
still-running one holds it in the queue. A terminal-but-not-
success state (FAILURE / ABORTED, and UNSTABLE unless the dep opts into
unstable_is_success) drives the gate to a decision rather than waiting forever.
Resolved dep states are sticky — a transient UNKNOWN from a poll error never regresses an
already-terminal dependency or re-closes an open gate.
Triggers (inbound)
When the chosen external build finishes with a matching status, it starts this TestHide
job. This is powered by an inbound webhook: point your Jenkins or TeamCity notification
at the TestHide notify endpoint and authenticate it with the endpoint's secret. The trigger's
on_status list (default ["SUCCESS"]) controls which finishing states
fire the job.
POST {TestHide}/api/v3/integrations/ci/notify/{endpointId}
X-Testhide-CI-Token:
Content-Type: application/json
Replace {TestHide} with your public base URL, {endpointId} with the
endpoint id from step 1, and the header value with that endpoint's inbound-webhook secret.
Inbound deliveries are deduplicated on (endpoint, ref, number, state) (Redis-backed,
with an in-process fallback), so a webhook retry never double-fires the job.
Actions (outbound)
When this TestHide build finishes — on success, on failure, or always — TestHide triggers an external Jenkins/TeamCity build, optionally passing parameters. Parameter values support placeholders that are substituted at trigger time:
| Placeholder | Resolves to |
|---|---|
%STATUS% | The finishing status of the TestHide build, upper-cased (e.g. PASSED / FAILED). |
%BUILD_ID% | The TestHide build id. |
%SOURCE_REVISION% | The source revision / branch of the build. |
The outbound action is the trigger_external_ci post-build step; it reads
configuration.external_ci.actions and fires once on the won terminal write so a
retried finalize never double-triggers the external build.
3 · Live status & badges
Linked builds show an External-CI badge (provider icon + state) on their build cards. Statuses update live over WebSocket: a leader-only poller refreshes external state every 15 seconds (with a 30 s startup delay and a 20 s per-pass runtime cap), and — if an inbound webhook is configured — updates arrive instantly when the external server pushes a notification.
The build Dependencies page (/build/:id/dependencies) renders
external Jenkins/TeamCity nodes alongside internal build dependencies in the same
graph. External nodes are color-coded by status and are click-through to the external build in
Jenkins/TeamCity.
4 · Security & operations
- Credentials encrypted at rest — API tokens and passwords are encrypted in
the database and never logged or returned to the browser. Set
SECRET_ENCRYPTION_KEYin the environment so the backend can encrypt and decrypt them. - Authenticated inbound webhooks — every notification to
/api/v3/integrations/ci/notify/{endpointId}must present the per-endpoint secret via theX-Testhide-CI-Tokenheader (or?token=); the compare is timing-safe and requests without a valid token are rejected with 401. - Brute-force throttle — repeated bad tokens are counted per endpoint (not per IP, so a misconfigured CI controller can't ban its own trusted internal IP); over the threshold the notify path answers 429 instead.
- On-prem CI allowed — endpoints on private (RFC 1918) networks are permitted, so internally hosted Jenkins/TeamCity servers work without exposing them publicly.
- Upstream failures surface inline — when an external server is unreachable or errors, TestHide returns an inline 424 (Failed Dependency) error rather than a maintenance redirect, so the cause is clear.
- Circuit breaker — a per-endpoint circuit breaker backs off a server that is down. While an endpoint is in cooldown the poller treats it as UNKNOWN (still pending, retry later) rather than hammering it.
SECRET_ENCRYPTION_KEY set, the backend cannot store CI credentials securely.
Generate a strong key and keep it stable — rotating it invalidates previously encrypted secrets.
5 · Limitations
- The dependency gate timeout is bounded by the job's max queue-wait — a gate
cannot hold a build longer than the job is allowed to wait in the queue (its
max_queue_wait_minutesceiling). - Without an inbound webhook, external state refreshes on a ~15 second poll cadence (leader-only); configure a webhook for instant updates.
- Inbound dedupe and trigger fan-out are bounded; during a Redis outage the in-process fallback is per-replica, so a duplicate webhook delivered to a different replica may produce at-least-once trigger semantics (the gated-dep update path stays idempotent).
Examples
Inbound webhook (trigger this job)
Point your CI server's notification at this URL with the secret header and a JSON body:
POST https://testhide.yourcompany.com/api/v3/integrations/ci/notify/65f0a1c2d3e4
X-Testhide-CI-Token: s3cr3t-endpoint-token
Content-Type: application/json
{
"status": "SUCCESS",
"job": "backend-integration",
"branch": "main",
"build_number": 1042
}
Jenkins post-build "Notify TestHide" (curl)
Add a post-build shell step (or an Execute shell stage) that posts the result back to TestHide so it can trigger the linked job:
curl -sS -X POST \
-H "X-Testhide-CI-Token: s3cr3t-endpoint-token" \
-H "Content-Type: application/json" \
-d "{\"status\":\"$BUILD_STATUS\",\"job\":\"$JOB_NAME\",\"branch\":\"$GIT_BRANCH\",\"build_number\":\"$BUILD_NUMBER\"}" \
https://testhide.yourcompany.com/api/v3/integrations/ci/notify/65f0a1c2d3e4
httpRequest pipeline
step) can post the same payload automatically on every build phase; TestHide maps the Jenkins
phase/result (and TeamCity buildResult) onto its internal
SUCCESS / FAILURE / RUNNING / ABORTED / UNSTABLE states.
AI Assist Dashboard
The dashboard at /ai-assist is the operations console for the AI-Assist subsystem
— the layered build-failure-diagnosis pipeline (the rules-v1 rule engine → the RCA
engine → optional LLM deep investigation) plus the diagnostic / prediction models that feed
it. It surfaces live worker health, per-component diagnostics, training controls, and the
federated model-exchange status. The route is guarded by the AI_ASSIST_READ
permission and the ai_feature_dashboard license capability; mutating actions
additionally require AI_ASSIST_EXECUTE.
The admin endpoints behind the dashboard are mounted under /api/v3/ai/admin/…
(registered in app/ai_assist/routes.py, handlers in
app/ai_assist/api/admin.py). The read views require
CONFIGURATION_READ and the diagnostics list / run / signal views require
CONFIGURATION_WRITE on the server side, in addition to the
AI_ASSIST_* gating on the client route.
Worker stats banner
The header polls GET /api/v3/ai/admin/stats every 60 seconds
through the shared AiService stats stream (timer(0, 60000)). The
endpoint returns the single worker document (_id="global_ai_worker") from
db_ai_worker_stats:
| Field | Meaning |
|---|---|
queue_size | Items currently waiting in the worker input queue. A pulsing dot shows whenever this is > 0. |
processed_total | Cumulative items processed since the worker started. |
success_total | Cumulative successful items. |
failed_total | Cumulative failed items. |
skipped_total | Cumulative skipped items. |
last_batch | Summary of the most recent batch the worker drained. |
worker_id / hostname / updated_at | Worker identity and last-heartbeat timestamp. |
A small health helper classifies the worker as healthy,
warning (queue_size > 1000 or error rate
> 10 %), or critical (failed / processed ratio
> 50 %). A Refresh action forces an immediate re-poll, and a collapsible
console tails the worker log via GET /api/v3/log every 2 seconds.
The log tail uses the top-level GET /api/v3/log endpoint, not a path
under /api/v3/ai/… — it streams the AI worker's process log.
Diagnostic model cards
Below the banner a responsive grid renders one card per diagnostic component. Diagnostics are
loaded from GET /api/v3/ai/admin/diagnostics, which returns the latest report per
component from db_ai_diagnostics (with the matching
training_history_<name> merged in). The current component set is:
retriever
Retriever (FAISS) — nearest-failure similarity index.
root_cause
Root-Cause Classifier — the transformer failure-labelling model.
flakiness
Flakiness Predictor — gradient-boosted instability scoring.
ood
OOD Detector — out-of-distribution / novelty scoring.
templates
Log Templates — master-pattern log-signature miner.
dataset
Training Dataset — the labelled sample pool used for training.
visual
Visual Classifier — screenshot visual-diff analyser.
emerging
Emerging Issues — change-point / trend detection over failure groups.
yolo
YOLO Detector — element detection for screenshot analysis.
corpus
Build Corpus — the build-corpus data asset (sample counts, class balance, coverage).
Each card surfaces:
- Status — healthy / warning / error / running, with a loaded-in-memory indicator. A benchmark result for a non-model card (
corpus,dataset,yolo) reads as a benign N/A (“not applicable”) rather than an error — those components are raw data stores or have their own audit route, so they have no golden-set benchmark. - Top issues — the first issues from the latest diagnostic run.
- Staleness & version — model-registry metadata flags models not trained in the last 30 days.
- Run Check — POSTs
/api/v3/ai/admin/diagnostics/:componentto mark the componentpending, then polls/api/v3/ai/admin/diagnosticson a 10 s interval until the report leaves therunning/pendingstate. - Details — opens a dialog with advanced stats, a file-integrity table, and parameter analysis.
Training & maintenance menu
A kebab menu in the header drives training and maintenance, each item gated by
AI_ASSIST_EXECUTE. These enqueue work for the AI worker via
POST /api/v3/ai/admin/diagnostics/run (body
{ "component": "<name>", "config": { … } }),
or via the per-component POST /api/v3/ai/admin/diagnostics/:component trigger. The
accepted component names are allow-listed server-side (system_train_heavy,
system_train_fast, dataset, root_cause,
retriever, templates, ood, yolo,
emerging, corpus, flakiness, build_rca,
training_lock).
- Train Fast — a quick incremental pass over the lightweight models (
system_train_fast). - Heavy Training — opens the train dialog to pick a backbone (DistilBERT / BERT / RoBERTa), epochs, batch size, learning rate, and a freeze backbone toggle (recommended for small datasets); runs as
system_train_heavywith the chosenconfig. - Train a single model — a submenu for retriever, templates, OOD, YOLO, emerging, and root_cause.
- Convert to ONNX — exports the trained classifier for faster CPU inference.
- Rebuild Index — rebuilds the retriever FAISS index.
- Force Unlock — clears a stale training lock left by a crashed run.
- Clear Queue — deletes corrupted / empty items from the worker input queue (confirm dialog).
- Build Golden Flakiness — materializes a flakiness golden set from the DB for benchmarking.
- Restart Worker — sends a restart signal to the AI worker process (confirm dialog).
A live training monitor card polls the training lock
(GET /api/v3/ai/admin/diagnostics/status/training_lock) every 30 seconds, shows
a determinate progress bar while a run is active, and offers a Terminate action
that posts { "component": "training_lock", "signal": "terminate" }
to POST /api/v3/ai/admin/diagnostics/signal. The status view also self-heals: if it
sees a running state but the on-disk training.lock file is gone (past a
short grace period) it resets the component to idle.
Training is skipped automatically when the dataset has too few samples for the target model
— check the dataset (and corpus) card before triggering a run.
GPU vs. CPU. Training auto-selects the best available device — it runs on
the GPU when one is usable and falls back to CPU otherwise. If a GPU is present but the runtime
can’t use it (the default image ships a CPU-only PyTorch wheel), the heavy-training startup
log prints a loud warning with the exact CUDA (cu124) install command. To actually
use the GPU, install a CUDA torch wheel (local) or run the CUDA image with a GPU reservation in
compose (prod) — see the configuration guide. A busy or too-small GPU is degraded to CPU
automatically rather than failing with an out-of-memory error.
Model exchange & quick links
The dashboard also exposes the federated model-sync card
(upload / download / both): it reads
GET /api/v3/ai/model-sync/status, triggers a transfer with
POST /api/v3/ai/model-sync/run, and then polls status on a 4 s interval until
any in-flight transfer clears. A Rule Catalog link is badged with the count of
pending auto-suggested Layer-1 rules from
GET /api/v3/ai/rule-suggestions/count.
Each model card's Details action opens the per-model detail page at
/ai-assist/model/:name (see the AI Model Detail guide), and the toolbar links reach
the Eval Explorer (benchmark history), the Cost dashboard
(/api/v3/ai/costs/daily), and the Activity History page
(/api/v3/ai/activity).
The Drift page is always current. Data-drift metrics (PSI) are recomputed
continuously — the training orchestrator computes a fresh snapshot for every monitored
model on each cycle (throttled to roughly once every 10 minutes) and the critical-drift
check acts on that fresh value, not on the last value stored at training time. So the
evaluated_at timestamp advances between trainings and newly-arrived drift is caught
promptly, rather than only being visible after the next retrain.
Admin endpoints used by the dashboard
| Purpose | Endpoint | Permission |
|---|---|---|
| Worker stats banner (60 s poll) | GET /api/v3/ai/admin/stats | CONFIGURATION_READ |
| Worker log tail (2 s poll) | GET /api/v3/log | AI_ASSIST_READ |
| Diagnostics list (model cards) | GET /api/v3/ai/admin/diagnostics | CONFIGURATION_WRITE |
| Run a single component check | POST /api/v3/ai/admin/diagnostics/:component | CONFIGURATION_WRITE |
| Loaded runtime model metrics | GET /api/v3/ai/admin/models | CONFIGURATION_READ |
| Queue training / maintenance task | POST /api/v3/ai/admin/diagnostics/run | CONFIGURATION_WRITE |
| Training-lock status (30 s poll) | GET /api/v3/ai/admin/diagnostics/status/training_lock | CONFIGURATION_READ |
| Terminate / pause / resume training | POST /api/v3/ai/admin/diagnostics/signal | CONFIGURATION_WRITE |
| Model-sync status / trigger | GET /api/v3/ai/model-sync/status · POST /api/v3/ai/model-sync/run | AI_ASSIST_* |
| Pending rule-suggestion badge | GET /api/v3/ai/rule-suggestions/count | AI_ASSIST_READ |
| Flush AI Redis caches (after config save) | POST /api/v3/ai/admin/cache/invalidate | CONFIGURATION_WRITE |
AI Model Detail Pages
Every diagnostic / prediction model has a dedicated detail page at
/ai-assist/model/:name. The page surfaces live diagnostics, input-drift, training
history, benchmark trends, recent predictions, and per-model controls (retrain, version
promote/rollback, inference-mode, and ad-hoc inference). The route is guarded by the
ai_feature_dashboard license capability; mutating actions require the
AI_ASSIST_EXECUTE permission.
The models
The canonical model ids are defined by KNOWN_MODELS in
app/ai_assist/model_registry.py. The detail-page route accepts any of these names
(and the corpus data asset):
root_cause
Transformer classifier (DistilBERT / BERT / RoBERTa backbone) for build-failure root-cause labelling.
flakiness
Gradient-boosting flakiness predictor trained on rolling test history.
retriever
Contrastive embedder + FAISS index for nearest-failure similarity search.
ood
Out-of-distribution / novelty detector.
visual
Visual-diff analyser (SSIM ensemble) for screenshots.
yolo
YOLO element-detection model for screenshot analysis.
templates
Log-signature miner (master-pattern templates).
emerging
Change-point detector for emerging failure groups.
bug_linker
Dense retrieval over linked-issue (Jira) embeddings.
build_rca
Build-level root-cause aggregate (the build-scope RCA model). Shares benchmark records with root_cause_classifier.
corpus
Training-corpus data asset — sample counts, class balance, coverage. Audited on demand.
Detail page tabs
The active tab is reflected in the URL via a ?tab=<slug> query param
(TAB_SLUGS = ['overview', 'training', 'benchmarks', 'configuration', 'predictions',
'actions']), so tabs are deep-linkable and survive back/forward navigation. A stat strip
above the tabs shows status, last-trained age, training duration, and the number of logged runs
and benchmarks.
| Tab | Slug | What it shows |
|---|---|---|
| Overview | overview |
An input-drift gauge (PSI value, alert level, and a PSI-trend sparkline from GET /api/v3/ai/drift/summary) plus the raw diagnostics key/value table. |
| Training | training |
The last 10 training runs (timestamp, duration, result badge, error). |
| Benchmarks | benchmarks |
Benchmark cards with PASS/FAIL regression badges, metric bars, deployed/active and DRY-RUN markers; new records can arrive live over WebSocket and pulse briefly. Links out to the Eval Explorer filtered to this model (?model=<name>). |
| Configuration | configuration |
The latest worker-stats key/value snapshot for the model. |
| Predictions | predictions |
Recent stamped predictions (build link, label, score bar, model version, timestamp); rows route to the test insight or the build predictions view depending on scope. |
| Actions | actions |
Retrain, download artifact, run benchmark, version promote/rollback, inference-mode toggle, and ad-hoc "run on sample". |
The DRY-RUN marker means a benchmark ran without a golden eval set: the primary metric is a
placeholder (1.0) that only proves the runner wiring and artifact loadability.
Drop a golden.jsonl under helpers/<model>_eval/ to get a real
metric.
Actions tab
Each action below requires AI_ASSIST_EXECUTE:
- Trigger Retrain — queues a training run for this model.
- Download Model — downloads the trained artifact (
GET /api/v3/ai/models/:name/download, served as a ZIP). - Run Benchmark — queues the benchmark suite against the golden set.
- Model version — pick a registered version from
GET /api/v3/ai/model-registry/:name/versionsand Promote it (POST …/:version/promote) or Rollback to the previous active one (POST …/rollback). - Inference mode — switch between
rules(heuristics only),shadow(ML runs but does not act), andml(ML active) viaGET/POST /api/v3/ai/models/:name/mode. - Run on sample — ad-hoc inference for the models in the backend allow-list
_VALID_MODELS = {root_cause, retriever, yolo}: paste a log/error string, an image URL (yolo), or a JSON payload. It POSTs to/api/v3/ai/inferenceand pollsGET /api/v3/ai/inference/:request_idfor the result.
"Run on sample" is only offered for root_cause, retriever, and
yolo; for any other model the panel shows an "ad-hoc inference isn't available for
this model type" notice and the server rejects the request with a validation error.
The model-detail endpoint
All tabs (except drift and predictions, which have their own endpoints) are populated from a
single call. The handler is AIAssistModelDetailView in
app/ai_assist/api/admin.py:
GET /api/v3/ai/admin/model/:name
Authorization: Bearer <token>
Requires: CONFIGURATION_READ
Example response (name=root_cause):
{
"ok": true,
"data": {
"name": "root_cause",
"diagnostics": {
"component": "root_cause",
"status": "healthy",
"issues": [],
"last_trained": "2026-06-05T08:14:02",
"training_result": "ok",
"training_duration_ms": 41200,
"advanced_stats": { "accuracy": 0.91, "labels": 12 }
},
"training_history": [
{ "ts": "2026-06-05T08:14:02", "result": "ok", "error": null, "duration_ms": 41200 }
],
"benchmarks": [
{ "model_name": "root_cause_classifier", "version": "1.3.0",
"passed": true, "primary_metric": 0.88, "created_at": "2026-06-05T09:01:00" }
],
"worker_stats": { "queue_size": 0, "processed_total": 5123 }
}
}
The endpoint is backed by db_ai_diagnostics (the current diagnostics doc plus
training_history_<name>, last 10 runs) and db_model_benchmarks
(last 10 runs). Benchmark rows are looked up under both the UI short name and the runner's
canonical name (e.g. flakiness ↔ flakiness_predictor,
build_rca → root_cause_classifier) so they survive a reload. For
the corpus asset the audit is computed and persisted on demand when no cached doc
exists, so a fresh install still renders a populated page.
Navigation
From the AI Assist Dashboard, click Details on any model card. The page lazy-loads via the Angular router and re-fetches on every visit; subsequent refreshes are silent (content stays visible while a background reload runs).
Cloud LLM model selection (chat, diagnosis, Auto-PR)
The detail pages above cover the bundled ML models. The separate cloud LLM that powers Ask AI chat, deep diagnosis, and Auto-PR is config-driven from the LLM Providers page — there are no model environment variables. You pick the provider and model in the UI; only the API keys and provider priority live in settings.
Models are chosen per tier, so each kind of task can use a different model:
- Chat — fast, cheaper model for the Ask AI conversational assistant.
- Deep diagnosis — stronger model for root-cause investigation.
- Code (Auto-PR) — code-specialised model for generating fixes; if no code model is configured for a provider it falls back to that provider's deep-diagnosis model.
Changes take effect instantly, with no restart: saving a provider key, reordering provider priority, or changing a catalog default model on the LLM Providers page is picked up by every running backend process within seconds — across all three tiers, including Code.
AI Insights Tab
The AI Insights tab on the build detail page gives a unified view of what the
background ML pipeline concluded about each test in a build — status, flakiness label and
score, root-cause prediction, coarse failure type, and which model produced each insight. The
tab is loaded on demand (a lazy child of the build-detail view) and reads the build's insight
documents. It requires the BUILDS_READ permission. The handlers live in
app/build/build_insights_view.py.
What it shows
On open, the tab fires two endpoints in parallel (a single
forkJoin) via BuildInsightsService:
| Endpoint | Returns |
|---|---|
GET /api/v3/builds/:id/insights/summary |
Aggregated counts: total, status_counts, prediction_labels, flaky_count, flakiness_breakdown, coarse_failure_types, and source_kinds. Rendered as summary cards and bar charts. |
GET /api/v3/builds/:id/insights |
Paginated list of individual insight documents (default 100 per page, max 500), excluding synthesized artifact rows (is_artifact != true). Each row shows the test name, status badge, flaky label, a prediction chip with confidence, and the flakiness score. |
flaky_count tallies any insight whose flaky label is present and not
Stable (i.e. the sum of every non-Stable bucket in
flakiness_breakdown).
Both endpoints enforce project scope: the build's project id (pi) is checked
against the caller's allowed projects before any insight is returned — a caller with a
global BUILDS_READ still gets 403 for a build in a project outside
their token scopes.
Insight document fields
Insights are stored in db_ai_insights using compact short keys; the list endpoint
expands them to the names below. (The list view surfaces only the prediction
label + score; uncertainty is detail-level.)
| Field | DB key | Meaning |
|---|---|---|
test_name | tn | Test identifier. |
status | st | Test result: failed / passed / error / skipped. |
flaky | fl | Flakiness label: Rare / Random / Systematic / Stable. |
flakiness_score | fs | Flakiness Predictor score (rendered as a percentage). |
prediction | ap | Root-cause classifier output: label + score. |
source_kind | sk | Which model / source produced the insight (e.g. root_cause, flakiness, ood). |
decision_category | dc | High-level decision bucket. |
coarse_failure_type | ct | Broad failure category (memory, network, assertion…). |
The build id is stored under the short key b and the test name under
tn in the raw documents — the long-form test_name key only
exists after the docs are expanded. The list endpoint queries the raw documents and reads
tn directly, so test names render correctly in the tab.
Pagination
The list uses keyset pagination on _id (newest first). Pass the
next_cursor from the previous response as the cursor query param;
cursors are ObjectId strings. next_cursor is null on the final page
(returned only when the page was full). An optional status filter narrows the list
to one status value.
GET /api/v3/builds/<id>/insights?limit=100&cursor=<next_cursor>&status=failed
The list response (v2 envelope) looks like:
{
"ok": true,
"data": {
"build_id": "665f1c2a9b1e4a0012ab34cd",
"total": 47,
"items": [
{
"insight_id": "665f1c2a9b1e4a0012ab34ce",
"test_name": "tests/api/test_login.py::test_expired_token",
"status": "failed",
"flaky": "Random",
"flakiness_score": 72,
"prediction": { "label": "env_issue", "score": 0.84 },
"source_kind": "root_cause",
"decision_category": "auto",
"coarse_failure_type": "network",
"created_at": "2026-06-05T09:12:44+00:00"
}
],
"next_cursor": "665f1c2a9b1e4a0012ab34ce"
}
}
The summary response aggregates the same collection:
{
"ok": true,
"data": {
"build_id": "665f1c2a9b1e4a0012ab34cd",
"total": 47,
"status_counts": { "failed": 23, "passed": 24 },
"prediction_labels": { "env_issue": 5, "product_bug": 10, "infra_issue": 3 },
"flaky_count": 8,
"flakiness_breakdown": { "Random": 4, "Rare": 3, "Systematic": 1 },
"coarse_failure_types": { "memory": 3, "network": 2 },
"source_kinds": { "root_cause": 12, "flakiness": 8, "ood": 3 }
}
}
Insights vs. the build-level diagnosis
AI Insights are per-test background ML inference that runs automatically on every result — no configuration required. They are distinct from two neighbouring surfaces:
-
The compact build diagnosis banner / chip (the denormalized
aisummary on the build:src,cat,conf,verdict,recs) summarises the whole build's failure cause from the rules-v1 → RCA → LLM pipeline — one verdict per build, not per test. -
The AI Evals tab shows results of LLM-judge evaluations you explicitly
configure as
llm_evalbuild steps, whereas AI Insights are produced automatically.
A related read-only endpoint, GET /api/v3/ai/insights/:id/bundle
(AIAssistInvestigationBundleView), returns the full investigation bundle for a
single insight — failure message, attachments, classification, and
ai_predict — for drill-down from a row.
AI Evals & Benchmarks
Testhide ships two related but distinct evaluation surfaces. The Eval Explorer
at /ai-assist/eval-explorer is the benchmark history for the ML prediction models
(it requires AI_ASSIST_READ). The Evals section at
/evals is the LLM-eval suite — golden-set evaluations run by pluggable
judges (it requires EVALS_READ). This page covers both.
Eval Explorer — model benchmark history
Reachable from the AI Assist Dashboard, the Eval Explorer is a flat table of benchmark runs
across the prediction models (e.g. flakiness, build_rca). Each row is
one benchmark evaluation against a golden set: a PASS/FAIL regression badge, the model name,
version, the primary metric, all metrics, and the run date. Filter by model pill and by status
(All / PASS / FAIL). The same benchmark data also appears on each model's
detail page under the Benchmarks tab.
Benchmark records that ran without a golden set are tagged DRY-RUN:
the primary metric is a placeholder (1.0) that only proves the runner wiring and
artifact loadability. Drop a golden.jsonl under
helpers/<model>_eval/ to get a real metric.
Evals — the LLM-eval suite
The /evals section lists LLM evaluations and lets you drill into each one. An eval
scores model or prompt output against a golden set using a pluggable judge.
Evals are aggregated from the build records themselves: every llm_eval build step
stores its result under cfg.build_step[i].eval, and the eval endpoints group those
results by eval_id (a build carries an eval_ids[] array for fast
lookup). There is no separate eval document — the eval name is the
eval_id from the step YAML.
The judge registry
Judges live in app/ai_core/eval/judges and are exported from the package
__init__. The registry currently includes:
| Judge | Class | What it scores |
|---|---|---|
llm | LLMJudge | LLM-as-judge: prompts a judge model with (input, expected, actual) and parses a structured JSON verdict. |
snapshot | SnapshotJudge | Match against an approved baseline output. Mode is exact, fuzzy, or semantic (MiniLM embedding similarity). |
classifier_benchmark | ClassifierBenchmarkJudge | Accuracy / F1 metrics for a classifier model. |
retrieval_recall | RetrievalRecallJudge | Recall@k for the failure retriever. |
detection_map | DetectionMapJudge | Mean-average-precision for the YOLO visual detector. |
ood_threshold | OODThresholdJudge | Out-of-distribution threshold scoring. |
distribution_drift | DistributionDriftJudge | Input-distribution drift check. |
feature_coverage | FeatureCoverageJudge | Feature-extraction coverage check. |
In an llm_eval build step the judge.kind field only accepts
llm_as_judge or snapshot — the snapshot judge's
mode is one of exact / fuzzy / semantic.
The remaining judges in the table above are benchmark judges driven by the Eval Explorer /
model-benchmark pipeline, not by pipeline steps.
The eval detail page
Open an eval from /evals to reach its detail page. It is backed by the following
endpoints (all require EVALS_READ and enforce per-project token scope):
| Tab | Endpoint | What it shows |
|---|---|---|
| Overview | GET /api/v3/evals/{eval_id} | Aggregate stats: current pass rate, status (passing / failing / stale), total runs, last-run cost, regressions in the last 7 days, and month-to-date cost. A pass-rate sparkline comes from GET /api/v3/evals/sparklines. |
| Runs | GET /api/v3/evals/{eval_id}/runs | Paginated run history. Each run links to its run-detail page at /evals/:id/runs/:runId (GET /api/v3/evals/{eval_id}/runs/{run_id}) with the full judge output per case. |
| Regressions | GET /api/v3/evals/{eval_id}/regressions | Failed cases on the most-recent run that are not already flagged as known issues. Each can be triaged. |
| Prompt history | — | The registry prompt versions used by this eval. |
| Costs | — | Token spend and cost per run for LLM-judge evals. |
A run's status maps from the stored exit_code: 0 → passed,
1 → failed, 2 → aborted,
3 → error. The eval-level status is stale when the
last run is older than 7 days, otherwise passing / failing against
the eval's own threshold (default 0.8).
Judge output
Every eval run stores its per-case judge output in eval.judge_outputs[] (surfaced
on the run-detail page as cases). For the llm judge the model is asked
to return a JSON object that is parsed and validated; the required keys are
score, verdict, and reasoning:
{
"score": 0.92,
"verdict": "pass",
"reasoning": "The output correctly identifies the root cause and is concise."
}
score— a float in[0.0, 1.0]; anything outside the range is rejected.verdict— one ofpass,fail,regression,improvement.reasoning— one short paragraph explaining the verdict.
Each executed judge call is also written to the prompt-registry audit log
(db_prompt_uses) and to db_ai_calls for cost tracking, with the parsed
output, token count, cost, and latency. A malformed or timed-out judge response is recorded as
verdict: fail rather than crashing the run, and a per-case
cost_budget_usd cap returns verdict: cost_exceeded when the call costs
more than the budget.
Baseline approval
Snapshot evals use an approval workflow. An approver holding the EVALS_APPROVE
permission can promote a run's output to the new baseline so future snapshot runs diff against
it and only fail when the output meaningfully changes. The relevant endpoints (all
EVALS_APPROVE, all project-scoped):
| Action | Endpoint |
|---|---|
| Approve a snapshot output as the new baseline | POST /api/v3/evals/{eval_id}/snapshots/{case_id}/approve |
| Approve a failing case as the accepted baseline | POST /api/v3/evals/{eval_id}/cases/{case_id}/approve-baseline |
| Mark a case as a known issue (requires a substantive reason) | POST /api/v3/evals/{eval_id}/cases/{case_id}/mark-known-issue |
| Clear a known-issue flag | DELETE /api/v3/evals/{eval_id}/cases/{case_id}/known-issue |
Mark-known-issue requires a real explanation: the reason must be at least 10 non-whitespace characters and is rejected if it looks like a rubber-stamp (e.g. "ok", "lgtm", "fixed"). The snapshot-approve body caps the new snapshot at 500 KB and the reason at 10,000 characters.
CLI
The eval harness has a CLI for running and inspecting evals directly from a build record:
# Run a snapshot eval (mode overrides the manifest default)
python -m app.ai_core.eval run --eval my-eval --candidate cases.jsonl --mode semantic
# Run an llm_eval step against a specific build + step index
python -m app.ai_core.eval run-for-build --build <build_id> --step 2 \
--model gemini-2.0-flash --threshold 0.80 --cost-budget 1.0 \
--parallelism 4 --on-fail pr_check_fail
# Show recent eval runs from build records
python -m app.ai_core.eval show --eval my-eval --runs 10 --json
Prompt Registry
The Prompt Registry stores versioned prompt templates with SHA-256 content deduplication and
semantic versioning. Prompts are referenced by eval configurations and llm_eval
steps through the registry:// URI scheme, and every resolved-and-executed use is
logged for cost and audit reporting. The package is app/ai_assist/prompt_registry;
documents live in db_prompts, db_prompt_versions, and
db_prompt_uses.
registry:// references
A reference resolves a prompt name (which is path-like, e.g.
judges/customer-support-quality) to a concrete version:
registry://<name>/<version>
registry://<name>/latest
registry://<name> # bare name is treated as latest
# Examples
registry://judges/sql-correctness/1.2.0
registry://judges/customer-support-quality/latest
registry://root-cause-summarizer
At resolve time the backend looks up the prompt in db_prompts and the matching
record in db_prompt_versions. When the version token is latest (or
omitted), the most-recently-inserted non-deprecated version is selected. The resolved prompt
exposes its model, temperature, declared variables, and
an optional output_schema; render() substitutes the
{{input}},
{{expected}}, and
{{actual}} placeholders.
The LLMJudge pins the prompt at the version it resolved when the judge was
constructed, so publishing a new version mid-run cannot silently change the prompt under a
running eval. If a reference cannot be resolved, the judge falls back to a built-in default
prompt and records the version as inline.
Versioning & SHA-256 dedup
Versions are not supplied by hand — the registry assigns them. On
upsert, the new content is hashed (SHA-256) and compared against the latest stored
version for that name:
- If the hash matches the latest version, the existing version is returned unchanged — a true dedup, no new record is written.
- If the content differs, the patch component is auto-bumped (e.g.
1.2.0→1.2.1) and a newPromptVersionis inserted. - The first version of a brand-new prompt starts at
0.1.0.
Versions must be valid MAJOR.MINOR.PATCH semver, and a version can be flagged
deprecated so latest skips it.
Only the patch component is ever bumped automatically. A minor or major version change is a deliberate, manual operation — the auto-bump path never advances MINOR or MAJOR on its own.
Document model
| Collection | One document per | Key fields |
|---|---|---|
db_prompts | logical prompt name | name, description, owner, output_schema, created_at, updated_at |
db_prompt_versions | (prompt, semver) pair | prompt_id, version, content, content_sha256, model, temperature, variables, deprecated, created_by |
db_prompt_uses | resolved + executed call | prompt_id, prompt_version, inputs_hash, output, output_parsed, cost_usd, tokens_used, latency_ms, build/eval/job ids |
Browsing prompts
The frontend PromptRegistryService reads the registry through two REST endpoints,
both gated by the PROMPTS_READ permission (granted to any authenticated user by
default — prompts are judge configurations, not secrets):
| Endpoint | Returns |
|---|---|
GET /api/v3/prompts | List of summaries: name, latest_version, description, owner, updated_at. |
GET /api/v3/prompts/resolve?ref=<registry-uri> | The full resolved version: name, version, content, model, temperature, variables, output_schema. |
The prompt picker dialog (prompt-dialog) lists available prompts, previews the
selected version's template, and inserts the chosen
registry://<name>/<version> reference back into the
form field. Both endpoints also support the Testhide-API-Version: 2 envelope while
remaining backwards-compatible with the legacy v1 shape.
CLI usage
# List all prompt names, latest versions, and owners
python -m app.ai_assist.prompt_registry list
# Show a resolved prompt version (content + model + temperature)
python -m app.ai_assist.prompt_registry show registry://judges/sql-correctness/1.2.0
# Unified diff between two versions
python -m app.ai_assist.prompt_registry diff \
registry://judges/sql-correctness/1.0.0 \
registry://judges/sql-correctness/1.2.0
# Upsert from a file (auto-assigns / bumps the version; dedups on identical content)
python -m app.ai_assist.prompt_registry upsert judges/sql-correctness prompt.txt
upsert via the CLI records the version author as cli and prints the
resulting version (e.g. OK: judges/sql-correctness @ 1.2.1). If the file content
is byte-identical to the current latest version, the same version is returned and no new
record is created.
LLM Cost & Pricing
Every cloud LLM call Testhide makes — AI investigation, RCA summarisation, the
llm eval judge, and auto-fix drafting — is priced and recorded so spend is
visible and capped. The single source of truth for cost computation is
app/ai_core/pricing.py; the runtime knobs live in settings.py as
LLM_* environment variables.
Supported providers (bring-your-own key)
The AI Narrator / Investigator routes to whichever provider you have configured a key for. Native SDKs back Google Gemini, OpenAI and Anthropic Claude; one OpenAI-compatible backend additionally serves Groq, Mistral, DeepInfra, OpenRouter and Hugging Face at their own base URLs. A fully local GGUF model is the always-available, offline last resort.
Zero-data-retention gate. The two pass-through routers (OpenRouter, Hugging
Face) are fail-closed: they stay disabled until an admin explicitly enables ZDR for
them, because prompts would otherwise transit a third party that may retain them. OpenRouter
calls additionally pin provider.zdr = true per request.
Keys, base URLs, per-provider monthly budgets and daily request limits are stored in the
database (db_llm_providers) and editable in the UI — no redeploy. The
per-tier provider chain is likewise DB-configurable (db_llm_routing),
overriding the LLM_*_PROVIDER_CHAIN env defaults below.
The cost formula
Cost is computed per call from the token split and the model's per-1K-token input/output prices:
cost_usd = (prompt_tokens / 1000) * input_price_per_1k
+ (completion_tokens / 1000) * output_price_per_1k
The result is rounded to 6 decimal places. compute_cost(model, prompt_tokens,
completion_tokens) returns (cost_usd, unknown); unknown is
True only when the provider cannot even be inferred from the model name, in which
case the cost is 0.0 and a warning is logged (an accounting gap to fix, never a
blocked call).
Where costs are recorded
db_ai_calls— one row per cloud LLM call withcaller,model,provider,cost_usd,prompt_tokens,completion_tokens,latency_ms, and the build / eval / job / project ids. This is what the Cost dashboard reads.db_prompt_uses— the prompt-registry audit log; each resolved-and-executed prompt records its owncost_usdandtokens_used.
The Cost dashboard endpoint is GET /api/v3/ai/costs/daily (requires
AI_ASSIST_READ). It aggregates db_ai_calls by day with
group_by of model (default), caller, user,
project, or all, over a window of up to 90 days.
Environment variables
| Variable | Default | Meaning |
|---|---|---|
LLM_PRICING_OVERRIDES |
{} |
JSON object keyed by model prefix → [input_per_1k, output_per_1k]. Highest priority — checked before the built-in tables (longest matching prefix wins). |
LLM_PROVIDER_DEFAULT_PRICES |
{} |
JSON object keyed by provider id (openai / anthropic / google) → [input_per_1k, output_per_1k]. Overrides the built-in per-provider default rate for unlisted models. |
LLM_LOCAL_MODEL_FREE |
true |
When true, local / self-hosted models (prefixes local, llama, mistral-local, phi-) are billed at $0. Set false to attribute an internal compute cost. |
LLM_MONTHLY_COST_CEILING_USD |
0 |
Opt-in global monthly ceiling. When > 0, new calls are rejected once the current UTC month's summed db_ai_calls.cost_usd reaches it (cached ~60 s per worker). 0 = unlimited. The license capability ai_monthly_budget_usd layers under this: the effective ceiling is the smallest positive of the two, plus any per-provider monthly_budget_usd. |
LLM_CHAT_PROVIDER_CHAIN |
google,openai,anthropic,local |
Ordered fallback chain for chat-tier calls. Providers without a configured key are skipped; local is the last resort. |
LLM_DEEP_PROVIDER_CHAIN |
google,anthropic,openai,local |
Ordered fallback chain for deep / investigation-tier calls. |
Per-user-per-day call quotas (chat / deep) are configured in the UI — AI Assist
settings, defaults 25 / 2; the per-call timeout (60 s) is baked in. The former
AI_QUOTA_* and LLM_REQUEST_TIMEOUT_S env vars were removed.
Overrides are applied at import time by merging the env JSON over the built-in tables, so a rate can be corrected without a code change. A malformed entry is skipped (the built-in default stands) and never crashes cost computation.
Price-resolution order
compute_cost resolves a model's rate in this order, stopping at the first match:
- Model override —
LLM_PRICING_OVERRIDES, longest matching prefix wins. - Provider table (when the caller passes
provider=) — the same model id can cost differently on different hosts (e.g. an open-weight Llama on Groq vs DeepInfra), so a provider-specific table is consulted first when the provider is known. - Local model — if
LLM_LOCAL_MODEL_FREEis on and the model is a local prefix and the resolved provider is local/empty, cost is$0(a hosted Llama is never mistaken for free). - Gemini table — built-in Google / Gemini per-model prices.
- Anthropic table — built-in Anthropic per-model prices (longer prefixes matched first).
- OpenAI table — built-in OpenAI per-model prices.
- Groq / Mistral / DeepInfra tables — built-in per-model prices for the open-weight router hosts.
- Per-provider default — an unlisted model from a known provider is priced at a conservative provider-typical rate (so a new/renamed model is never billed at
$0). - Blended legacy fallback — old model aliases keyed by a single blended per-1K rate.
If none of the above match (the provider cannot be inferred), the model is truly unknown: cost
is 0.0 and unknown=True.
Built-in default price tables
Prices are USD per 1K tokens (input / output), reviewed 2026-06-14. These are the
code-level defaults; the live, operator-editable rates live in the model catalog
(db_llm_models) and win over these tables. A representative subset is shown below
— pricing.py additionally carries Groq, Mistral and DeepInfra tables.
Google / Gemini
| Model prefix | Input / 1K | Output / 1K |
|---|---|---|
gemini-2.5-flash | 0.0003 | 0.0025 |
gemini-2.5-pro | 0.00125 | 0.010 |
gemini-2.0-flash | 0.0001 | 0.0004 |
gemini-2.0-pro | 0.00125 | 0.005 |
gemini-1.5-flash | 0.000075 | 0.0003 |
gemini-1.5-pro | 0.00125 | 0.005 |
gemini-1.0-pro | 0.0005 | 0.0015 |
Anthropic
| Model prefix | Input / 1K | Output / 1K |
|---|---|---|
claude-opus-4 | 0.015 | 0.075 |
claude-sonnet-4 | 0.003 | 0.015 |
claude-haiku-4 | 0.001 | 0.005 |
claude-3-7-sonnet | 0.003 | 0.015 |
claude-3-5-sonnet | 0.003 | 0.015 |
claude-3-5-haiku | 0.0008 | 0.004 |
claude-3-opus | 0.015 | 0.075 |
claude-3-sonnet | 0.003 | 0.015 |
claude-3-haiku | 0.00025 | 0.00125 |
OpenAI
| Model prefix | Input / 1K | Output / 1K |
|---|---|---|
gpt-4o-mini | 0.00015 | 0.0006 |
gpt-4o | 0.0025 | 0.010 |
gpt-4.1-mini | 0.0004 | 0.0016 |
gpt-4.1 | 0.002 | 0.008 |
gpt-4-turbo | 0.01 | 0.03 |
gpt-4 | 0.03 | 0.06 |
gpt-3.5-turbo | 0.0005 | 0.0015 |
o1-mini | 0.0011 | 0.0044 |
o1 | 0.015 | 0.06 |
o3-mini | 0.0011 | 0.0044 |
Per-provider defaults (unlisted models)
| Provider | Input / 1K | Output / 1K |
|---|---|---|
openai | 0.005 | 0.015 |
anthropic | 0.003 | 0.015 |
google | 0.00125 | 0.005 |
Examples
Correct an out-of-date Gemini Flash rate and add a price for a brand-new model without touching code:
LLM_PRICING_OVERRIDES='{"gemini-2.5-flash":[0.0003,0.0025],"gpt-4o":[0.005,0.015]}'
Set a conservative per-provider default for any unlisted model from those providers:
LLM_PROVIDER_DEFAULT_PRICES='{"openai":[0.005,0.015],"google":[0.00125,0.005]}'
Cap total monthly spend at $250 and stop billing local inference at zero:
LLM_MONTHLY_COST_CEILING_USD=250
LLM_LOCAL_MODEL_FREE=false
Worked figure: a gemini-2.0-flash call with 800 prompt tokens and 200 completion
tokens costs (800/1000) × 0.0001 + (200/1000) × 0.0004 = 0.00016 USD.
When LLM_MONTHLY_COST_CEILING_USD is reached, new calls are rejected for the rest
of the UTC month — AI features that depend on cloud LLMs will fail until the month rolls
over, the ceiling is raised, or the local provider in the fallback chain takes
over. Set it deliberately.
Permissions & Roles
Access control in Testhide is permission-based. Route handlers are decorated with
@require_permission('CODE') (the caller must hold all listed codes)
or @require_role('role'). The authenticated user's role determines which
permission codes they carry. Superusers and service accounts bypass every permission
check — the decorator short-circuits for them, so they implicitly hold everything.
The RESOURCE_OPERATION model
A permission code is <RESOURCE>_<OPERATION>. There are exactly four
operations — READ, WRITE, EXECUTE, and DELETE.
Each role is expanded from a base list of resources crossed with all four operations, plus a few
explicit extras (evals, security-admin). Granting the BUILDS resource to a role
therefore yields BUILDS_READ, BUILDS_WRITE, BUILDS_EXECUTE,
and BUILDS_DELETE.
The guest operation set is READ only. Permission writes are validated
against a flat catalog: a caller can only assign permissions they already hold (no escalation),
and dynamic PROJECT_<id>_<OP> codes are validated by prefix. Only
superusers and service accounts may assign the privileged superuser /
service_account roles.
Resources by role
The two base resource lists are USER_PERM (granted to the standard
user role) and SUPERUSER_PERM (granted to superuser and
service_account). The table below is taken verbatim from
app/permissions.py.
| Resource | Regular user | Superuser / service account |
|---|---|---|
REPORTS | Yes | Yes |
JOBS | Yes | Yes |
PROJECTS | Yes | Yes |
BUILDS | Yes | Yes |
TOOLS | Yes | Yes |
MONITORING | Yes | Yes |
NODES | Yes | Yes |
MESSAGES | Yes | Yes |
AI_ASSIST | Yes | Yes |
PROMPTS | Yes | Yes |
CONFIGURATION | No | Yes |
USERS / GLOBAL_USERS | No | Yes |
VCENTER | No | Yes |
REMOTE_CONTROL | No | Yes |
SECURITY | No | Yes |
SYSTEM | No | Yes |
AI_FIX | No (default off) | Yes |
EVALS | READ + EXECUTE only | All operations + APPROVE |
EVALS is not a plain resource — it is expanded explicitly. Regular users get
EVALS_READ and EVALS_EXECUTE only; superusers and service accounts
get all four EVALS_* codes plus EVALS_APPROVE.
Notable gated operations
- REMOTE_CONTROL_EXECUTE — gates dangerous fleet / node operations:
quarantine, Docker container spawn, force-reconcile, clean disk, install-python, and PassEncode.
Superuser / service account only — the
REMOTE_CONTROLresource is not inUSER_PERM. - NODES_DELETE — gates node delete and purge. Regular users carry it
(
NODESis a user resource), but it is checked explicitly on the destructive node routes. - AI_FIX_READ / AI_FIX_WRITE — gate the Auto-PR-Fix feature.
READshows the auto-fix badge and the "Suggested fix routing" card on Investigate;WRITEenables the Draft proposal / Create PR actions.AI_FIXis inSUPERUSER_PERMonly, so it is off by default for regular users until an admin grants it. - SYSTEM_READ / SYSTEM_WRITE — the System Errors panel. Superuser / service account only, because it exposes internal infrastructure detail.
- EVALS_APPROVE — approve snapshot baselines and mark / clear known issues.
Held by superusers, service accounts, and the named
eval_approverrole. - SECURITY_ADMIN — manage banned IPs (the banned-IPs admin endpoints). Held by
superusers, service accounts, and the named
security_adminrole. Not granted to regular users.
Built-in roles
The role catalog is defined in PERMISSIONS in app/permissions.py:
user
The standard interactive role. Carries READ / WRITE / EXECUTE / DELETE on the ten user
resources, plus EVALS_READ and EVALS_EXECUTE. Does
not get SECURITY_ADMIN.
guest
Read-only. The GUEST_OPERATIONS set is READ only.
superuser
All permissions, including the admin-only resources and SECURITY_ADMIN.
Bypasses every @require_permission check.
service_account
Headless role with the same full permission set as superuser. Used by the CI agent /
testhide_client plugin. Also bypasses permission checks.
eval_approver
User permissions plus the full EVALS_* set including EVALS_APPROVE
(EVALS_READ, EVALS_WRITE, EVALS_EXECUTE,
EVALS_APPROVE).
security_admin
User permissions plus SECURITY_ADMIN (and the standard user EVALS extras). For
ops / security staff who manage IP bans without full superuser.
Permission checks are bypassed entirely for the superuser and
service_account roles — the decorator returns early before comparing codes, so they
implicitly hold everything.
Service accounts
Service accounts are headless users without UI access. The CI agent authenticates its WebSocket
handshake with a hashed license-key token, while the account authenticates against the REST API
like any other user (typically via a Personal Access Token). Service accounts inherit the full
superuser permission set and bypass @require_permission.
LDAP / SSO
Directory integration (available on Cloud Team & Enterprise tiers) maps directory users onto
Testhide roles. LDAP uses LDAPS by default — LDAP_USE_SSL=0 drops to plain LDAP and
is warned against loudly. The relevant environment variables:
LDAP_URL=ldaps://ldap.corp.example.com:636
LDAP_BIND_DN=cn=svc-testhide,ou=services,dc=corp,dc=example,dc=com
LDAP_BIND_PASSWORD=<password>
LDAP_USER_BASE=ou=users,dc=corp,dc=example,dc=com
LDAP_USE_SSL=1 # 1 = LDAPS (default), 0 = plain LDAP (warns loudly)
LDAP_TLS_INSECURE=0 # 1 = skip cert verification (not recommended)
LDAP_TLS_VERSION=TLSv1.2 # minimum TLS version for LDAPS
SSO is also supported via OIDC. Both LDAP and OIDC resolve to one of the built-in roles above,
so all downstream @require_permission / @require_role checks behave
identically regardless of how the user authenticated.
Security
Testhide ships with defense-in-depth hardening: JWT secret enforcement, authenticated agent and frontend WebSockets, per-project webhook HMAC signatures, container isolation via a Docker sidecar, supply-chain scanning (SBOM + Trivy + pinned images), an IP allowlist on the database dump endpoint, and TLS on outbound integrations.
JWT authentication
Browser sessions use signed JWTs. The backend refuses to start in
production / staging when JWT_SECRET is still the
development default, and warns if it is shorter than 32 characters. Generate a strong secret:
python -c "import secrets; print(secrets.token_urlsafe(64))"
A standard login token is valid for 1 hour; a "remember me" login extends it to
7 days. To revoke all outstanding sessions for a user, increment
token_version on their user document — the auth middleware rejects any JWT whose
embedded version no longer matches.
Service-account password rotation also bumps token_version, immediately
invalidating any JWT issued under the old credentials.
Agent WebSocket authentication
Every node agent must authenticate its WebSocket handshake. The agent computes
SHA256(licenseKey) (lowercase hex) and sends it as a query parameter:
wss://<host>/api/v3/node/connect?token=<SHA256(licenseKey)>
The backend validates the token against the instance licenseKey (accepting both the
raw-key hash and a UUID-normalized hash) with a constant-time comparison. A missing or invalid
token is rejected with HTTP 401 before the WebSocket is upgraded, so an
unauthenticated client never enters the connection hub.
This enforcement is now unconditional. It was briefly staged behind a
NODE_WS_AUTH_MODE knob (off → warn → enforce) while the fleet was updated
to send the token; that knob has since been removed.
Frontend WebSocket authentication
The UI WebSocket is not public. The auth middleware runs on the handshake (the
token is passed via the ?token= query parameter, since browsers cannot set custom WS
headers), rejecting anonymous connections and injecting the authenticated user plus their
accessible project IDs onto the session. The pub/sub router then enforces
per-topic, project-scoped authorization on every subscribe — a client can only
subscribe to topics for projects it can access.
Node secrets
The legacy root_password and vnc_password node fields were dead code and
have been removed entirely. A node's secure_id (its agent-handshake
identity) is never serialized to browser clients — it is stripped from all node
API responses unless an internal caller explicitly opts in.
Inbound webhook HMAC signatures
Inbound git webhooks (POST /api/v3/git/notifyCommit) are verified with HMAC-SHA256
using a per-project secret. The signature is computed over the raw request body and the project
is identified by header:
X-Testhide-Project: <project_id> # ObjectId, hex 24
X-Testhide-Signature: sha256=<hex_digest> # HMAC-SHA256(secret, raw_body)
# Generate / rotate a project's webhook secret at any time:
POST /api/v3/project/<id>/webhook_secret/rotate # requires PROJECTS_WRITE
Set STRICT_WEBHOOK_HMAC=1 in production to enforce signing: a call is rejected
(401) unless it carries X-Testhide-Project, that project has a webhook secret, and
the signature verifies. The default is off so that standard
GitHub / GitLab / Bitbucket webhooks (which don't send our custom header) keep working during
migration — but a configured secret is always enforced in both modes; the soft
path only covers the header-absent / secret-absent cases.
Docker sidecar isolation
/var/run/docker.sock is not mounted into the backend container. All
Docker operations go through a narrow sidecar service over an internal bridge network. The sidecar
enforces allowlists for images, exec patterns, and volumes, and requires a bearer token.
The sidecar token is auto-derived from JWT_SECRET via
HMAC-SHA256(JWT_SECRET, "testhide:sidecar:v1") — zero-config. The sidecar receives
JWT_SECRET and independently computes the same token, so no separate secret needs
managing. You may still set SIDECAR_AUTH_TOKEN explicitly for backward
compatibility; if present it takes priority over the derived value.
Supply chain
SBOM
syft generates an SPDX-JSON bill of materials after every image build, published
as a GitHub Actions artifact (90-day retention).
Trivy CVE scan
Filesystem + image scan on every CI run; fails on CRITICAL / HIGH unpatched CVEs. SARIF results are uploaded to the GitHub Security tab.
Pinned base images
All Dockerfiles pin to a digest, e.g. python:3.12-slim@sha256:…. Refresh monthly
with scripts/security/refresh-base-digests.sh.
Database-dump IP allowlist
The database dump endpoint is guarded by an IP allowlist (decorator
@require_ip_allowlist(env_var="DB_DUMP_ALLOWED_IPS")). Entries may be single IPs or
CIDR ranges:
DB_DUMP_ALLOWED_IPS=127.0.0.1,10.0.0.0/8
TESTHIDE_TRUST_PROXY=1 # trust X-Forwarded-For behind a load balancer (else use the TCP peer)
An empty or unset DB_DUMP_ALLOWED_IPS fails closed — the endpoint
returns 403 for everyone. With TESTHIDE_TRUST_PROXY=0, the X-Forwarded-For
header is ignored and only the real TCP peer IP is matched, preventing header spoofing when not
behind a trusted proxy.
Integration security — Jira & Slack
Outbound integrations use TLS and are credentialed through environment variables (never persisted to browser clients):
# Jira (Bug Linker)
JIRA_BASE_URL=https://yourorg.atlassian.net
JIRA_EMAIL=svc-testhide@yourorg.com
JIRA_TOKEN=<api-token>
# Slack notifications
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../...
Jira and Slack require the jira_integration and slack_integration tier
feature flags (Cloud Starter and above). Stored secrets are encrypted at rest with AES-256-GCM
when SECRET_ENCRYPTION_KEY (64 hex chars) is set.
API Tokens (Personal Access Tokens)
Personal Access Tokens (PATs) authenticate against the Testhide REST API without a session cookie. Tokens are long-lived, optionally scoped, and revocable — the recommended auth method for CI scripts, build agents, and external integrations. Every token belongs to the user who created it and inherits that user's project access; a token can never exceed its owner's reach.
Token format
Every token starts with the prefix th_live_ followed by 40 lowercase hex characters
(48 characters total). The plaintext is shown once at creation and never stored —
only a SHA-256 hash is kept in the db_api_tokens collection.
th_live_53449c66048ca4916f6b357ff45440b113b63c59
Store it immediately. Copy the token right after creation — there is no way to retrieve the plaintext again. If you lose it, revoke the token and create a new one.
Who can create tokens
Any authenticated user can create, list, and revoke their own tokens — there is
no special role requirement, and managing your own tokens requires no scope. The
optional project_scopes narrowing (below) is validated against the caller's own
project access, so a user cannot mint a token for a project they cannot reach. Superusers and
service accounts are exempt from that project-access check because they already see every project.
Creating a token
Open Settings → Configuration → API Tokens and click New API Token.
| Field | Required | Notes |
|---|---|---|
name | No | Human-readable label, e.g. ci-runner. Defaults to token-YYYYMMDD if omitted. |
expires_days | No | Omit or use a non-positive value for a token that never expires. Otherwise the token expires that many days from creation. |
scopes | No | Empty list (or *) = Full Access. Otherwise an array of scope strings (see Scopes). Invalid names are rejected. |
project_scopes | No | Array of project ObjectId strings (24-char hex). Omit / empty = inherit the owner's full project access. Each ID is validated for format, existence, and caller access. |
Via the REST API:
curl -X POST https://localhost:8080/api/v3/auth/api-tokens \
-H "Authorization: Bearer <your-jwt>" \
-H "Content-Type: application/json" \
-d '{"name":"ci-runner","expires_days":90,"scopes":["builds:write","jobs:read"]}'
Response 201 Created:
{
"ok": true,
"data": {
"token": "th_live_…",
"id": "6643a1b2c3d4e5f6a7b8c9d0",
"user_id": "…",
"name": "ci-runner",
"scopes": ["builds:write", "jobs:read"],
"project_scopes": null,
"created_at": "2026-05-13T12:00:00+00:00",
"expires_at": "2026-08-11T12:00:00+00:00",
"last_used_at": null,
"request_count": 0
}
}
Using a token
Pass the token in the standard Authorization header (or, where headers are
unavailable, as a ?token= query parameter):
curl https://localhost:8080/api/v3/builds \
-H "Authorization: Bearer th_live_53449c66048ca4916f6b357ff45440b113b63c59"
The auth middleware detects the th_live_ prefix, hashes the value, looks it up,
validates expiry and (if scoped) scopes, then injects the owning user into the request context
identically to a JWT session. Each successful request atomically updates last_used_at
and increments request_count.
Scopes
A token with no scopes (or containing the wildcard *) has Full Access.
Each write scope automatically implies its read counterpart, so you never need to select both.
Scope enforcement maps the request method + URL path prefix to a required scope; if the token's
scopes don't satisfy it, the request is denied. The full scope vocabulary lives in
app/auth/scopes.py:
| Scope | Grants access to |
|---|---|
* | Full Access (same as an empty scope list) |
builds:read | List / view builds, artifacts, evals, insights, node builds |
builds:write | Import builds, trigger re-runs, write evals (implies builds:read) |
jobs:read | List / view jobs and pipeline configuration |
jobs:write | Create / update / delete jobs; git/notifyCommit (implies jobs:read) |
reports:read | Reports, test history, tests, insights |
reports:write | Create / update reports and test ops (implies reports:read) |
ai:read | AI insights, model detail, worker stats |
ai:write | Trigger re-inference, update AI config (implies ai:read) |
config:read | Configuration, prompts, webhooks, MS Teams, vCenter, monitoring, watchers, pipeline templates |
config:write | Change the above settings (implies config:read) |
projects:read | List / view projects |
projects:write | Create / update / delete projects (implies projects:read) |
admin:read | Users, nodes, security, license, cron, tier, global users, AI admin, agent |
admin:write | Manage users, nodes, security, license, restart-backend (implies admin:read) |
Managing your own tokens (list / create / revoke under /api/v3/auth/api-tokens)
requires no scope — it is always available to the authenticated owner.
Project scoping
Pass project_scopes as an array of project IDs to restrict a token to specific
projects, independent of its scope grants. The backend validates that each ID is a well-formed
24-character ObjectId, that the project exists, and that the caller can access it. An empty or
omitted value means the token inherits the owner's full project access.
Listing tokens
GET /api/v3/auth/api-tokens
Response: { "ok": true, "data": { "tokens": [...], "count": 1 } }
Token hashes are never returned. Up to 200 of the most recently created tokens
are listed (newest first). last_used_at and request_count reflect the
most recent successful use.
Revoking tokens
DELETE /api/v3/auth/api-tokens/{token_id} — revoke one (must be yours)
DELETE /api/v3/auth/api-tokens — revoke ALL for the current user
Revoking a token you don't own (or an unknown ID) returns 404.
Limits & best practices
- Maximum 50 tokens per user. Creating beyond the cap returns 422 — revoke unused tokens first.
- Use short-lived tokens (e.g. 90 days) for CI jobs and rotate them in your secrets manager.
- Grant only the scopes your integration actually needs; add
project_scopesto confine the blast radius. - Never embed tokens in source code — use environment variables or a secrets vault.
Error responses
| HTTP status | Cause |
|---|---|
400 Bad Request | Invalid scope name, or malformed / unknown / inaccessible project_scopes entry (VALIDATION_FAILED) |
401 Unauthorized | Token not found, revoked, expired, or owner deactivated |
403 Forbidden | Token scopes do not cover the requested endpoint (SCOPE_DENIED) |
404 Not Found | Revoking a token that does not exist or is not yours |
422 Unprocessable | Token limit reached (50 per user) |
Tiers & Quotas
Plan limits are enforced inside the backend by TierEnforcementService. The
per-tier limits and feature flags are never configured by environment variables —
they are delivered by the signed license your instance syncs from the TestHide license server and
cached in configuration.license_data. The only fallback is a hard-coded Free-tier
baseline (FREE_TIER_LIMITS / FREE_TIER_CAPABILITIES in
app/license/manager.py), applied before the first sync and on any downgrade. A daily
cron handles billing-period rollover.
Enforcement is always on
Enforcement is a hard-coded constant (_ENFORCE = True) baked into
app/services/tier_enforcement.py. There is deliberately no env var or config
flag that can switch it to “log only”: the former
TIER_ENFORCEMENT_MODE knob was removed precisely because anyone who could read or run
the image could otherwise disable tier limits. Enforcement can only ever be made stricter, never
looser.
< 0 (the -1 sentinel; the server’s 0
“unlimited” is normalised to -1 on sync), which short-circuits every
quota check straight to “allow”. Only unlicensed, over-limit, or expired-past-grace
instances — which the license manager downgrades to Free — actually hit the gates. Before the
first sync the generous defaults apply, so startup never bricks.
Plan comparison
The values below are the canonical tier capabilities seeded on the license server
(testhide_service/population.py → Tier / TierCapability),
which is what a signed license actually carries. The cloud prices are taken from the same seed data;
the authoritative limits for any given instance are whatever its synced license reports.
| Capability | Free | Cloud Starter $49 / mo |
Cloud Team $299 / mo |
Enterprise custom |
|---|---|---|---|---|
Concurrent builds (max_concurrent_builds) | 1 | 5 | 25 | unlimited |
Agent nodes (max_nodes) | 5 | 10 | 50 | unlimited |
User seats (max_users) | 1 | 3 | 15 | unlimited |
AI requests / month (ai_requests_monthly) | unlimited (self-hosted) | 10,000 | 100,000 | unlimited |
AI cloud budget, USD / mo (ai_monthly_budget_usd) | unlimited | unlimited† | unlimited† | unlimited |
Data retention (retention_days) | 7 days | 30 days | 365 days | unlimited |
Matrix builds (matrix_builds) | ✗ | ✓ | ✓ | ✓ |
AI Root-Cause Classifier (ai_root_cause) | ✓ | ✓ | ✓ | ✓ |
AI Flakiness Predictor (ai_flakiness) | ✓ | ✓ | ✓ | ✓ |
AI dashboard (ai_feature_dashboard) | ✗ | ✓ | ✓ | ✓ |
AI Failure Retriever / FAISS (ai_faiss) | ✗ | ✓ | ✓ | ✓ |
AI OOD Detector (ai_ood) | ✗ | ✓ | ✓ | ✓ |
AI Log Signature Miner (ai_log_signatures) | ✗ | ✓ | ✓ | ✓ |
AI Emerging Issues (ai_emerging_issues) | ✗ | ✓ | ✓ | ✓ |
AI Bug Linker / Jira (ai_bug_linker) | ✗ | ✓ | ✓ | ✓ |
AI Visual Diff Analyzer (ai_visual_classifier) | ✗ | ✓ | ✓ | ✓ |
AI Narrator + Investigator Agent (LLM) (ai_investigator) | ✗ | ✗ | ✓ | ✓ |
SSO / LDAP / SAML (sso_enabled) | ✗ | ✗ | ✗ | ✓ |
White-label (white_label_enabled) | ✗ | ✗ | ✗ | ✓ |
Custom AI training (custom_ai_training) | ✗ | ✗ | ✗ | ✓ |
† ai_monthly_budget_usd defaults to 0
(unlimited) on every tier; it is an operator-tunable per-license ceiling on cloud-LLM
spend, layered under the per-provider and global cost caps.
ai_investigator) — the conversational LLM agent with multi-provider
bring-your-own-key support. SSO/SAML, white-label, custom AI training, and on-prem local
LLM are Enterprise-only. (A dedicated success manager is included with
Enterprise as a service, not a software flag.)
Free-tier baseline (hard-coded fallback)
When no license has synced yet — or after a downgrade — the backend falls back to these exact
values (app/license/manager.py, mirrored in tier_enforcement.py):
max_concurrent_builds = 1
max_nodes = 5
max_users = 1
retention_days = 7
ai_requests_monthly = 0 # 0 = unlimited (self-hosted has no cloud AI quota)
ai_monthly_budget_usd = 0 # 0 = unlimited
# capabilities: only these two are on; every other flag is False
ai_root_cause = True
ai_flakiness = True
On the Free tier every other AI model and platform feature is denied until a paid license is
synced. Note the 0 → unlimited convention for ai_requests_monthly: a
self-hosted instance never has a cloud AI-request quota, so Free is “unlimited” there even
though it is the lowest tier.
How limits are sourced
- License sync —
LicenseManager.check_and_sync()calls the license server, then writes{ valid, tier, capabilities, limits }intoconfiguration.license_data. A limit of0from the server (meaning “unlimited”) is normalised to the-1sentinel. - Cadence — the license worker re-syncs on a periodic schedule and on instance/user changes; it reports usage telemetry, then refreshes capabilities. A successful sync timestamp drives the grace window (below).
- Reads —
TierEnforcementServicereads limits fromlicense_data["limits"]and feature flags fromlicense_data["capabilities"];LicenseManager.can(feature)/can_use_feature_sync()delegate to the same source, so there is a single source of truth.
What gets gated
- Concurrent builds —
can_start_build()counts builds inBuild.ACTIVE_STATUSESand blocks when the count reachesmax_concurrent_builds; over-cap builds staypendinguntil a slot frees. - User seats — enforced in
auth/views.py::UserAddandservices/user.py::UserService.create_useragainstmax_users(service accounts and soft-deleted users are excluded from the count). - Connected nodes —
max_nodesis propagated to the node fleet; nodes beyond the cap are flaggedlicense_validity=false. - AI evals / month — the live gate is
LicenseManager.check_ai_evals_limit()(called fromai_assist/api/general.py::_check_license), counting authoritativeconfiguration.global_options['ai_requests_this_month']. The parallelTierEnforcementService.can_use_ai_eval()exists only for parity/tests and must not be wired in as a second gate. - AI feature flags —
can_use_feature()/can_use_ai_model()gate individual models (ai_faiss,ai_ood,ai_visual_classifier,ai_investigator,ai_feature_dashboard, …) against the licensed capabilities.
app/managers/quota.py) throttles
QUOTA_BUILDS_PER_DAY (default 1000),
QUOTA_AI_REQUESTS_PER_HOUR (default 500), and an advisory
QUOTA_ARTIFACT_GB_TOTAL (default 100 GB). These are env-driven
instance defaults with optional per-project overrides in db_projects.quotas, backed
by short-TTL Redis counters; unrestricted (admin) callers bypass them.
Quota banners
- 80% used — info banner, e.g. “AI evals: 8,123 / 10,000 (81%) this billing period”.
- 95% used — yellow “approaching quota” warning banner.
- 100% used — red banner, and AI calls return
429 Quota Exceeded.
On the self-hosted Free tier ai_requests_monthly is 0 (unlimited), so no
monthly AI-request banner is shown. Per-user/day soft limits still apply via the UI-editable
quotas in AI Assist settings (defaults: 25 chat / 2 deep; the former
AI_QUOTA_* env vars were removed).
Billing-period rollover
A cron at 00:05 UTC daily (app/cron_tasks.py →
tier_billing_rollover, spec 5 0 * * *) calls
reset_billing_period_if_due(). When billing_period_end has passed, the
active usage counter (db_usage_counters) is archived to db_usage_history
and a fresh counter opens for the next period. A Redis lock
(cron:lock:billing_rollover) prevents a double rollover across instances. The
rollover is idempotent — re-running after a partial failure is safe.
License grace & downgrade
Transient licensing outages keep the last-good tier, so a normal license-server blip never disrupts paying customers. The grace window is generous:
- Within grace — the last successfully synced tier and limits stay in effect.
A
5xxfrom the license server or a connectivity error keeps existing state. - Beyond grace — no successful sync for longer than
LICENSE_GRACE_SECONDS(default 14 days) stops the instance from failing open: it is downgraded to Free and the invalid state is propagated to all nodes. The first transient error never triggers this — a prior recorded success is required. - Explicit rejection — a known-bad code
(
license.not_found/inactive/revoked), or a license the server reports asvalid=false(expired / inactive), causes an immediate downgrade to Free regardless of grace.
Tier /
TierCapability records described here).
Configuration & Environment Variables
Every TestHide component is configured through environment variables. This is the authoritative
reference: operator-facing settings (server, datastores, security, observability,
LLM providers, license, integrations, agents) each get a full row with default and purpose; the
deep AI/ML tuning knobs are listed in compact tables at the end. Defaults shown
are the in-code defaults from testhide/settings.py, database.py,
gunicorn.conf.py, the app/ service modules, the license server’s
config/settings.py, the Angular scripts/set-env.js, and the .NET
ConfigService.cs. The curated operator subset is mirrored in
static/landing/.env.example.
JWT_SECRET, MONGO_PASS,
REDIS_PASSWORD, SECRET_ENCRYPTION_KEY, every *_API_KEY,
Stripe keys, and email passwords must live only in your deployment’s .env /
secret store, which must be git-ignored. Generate strong values, e.g.
python3 -c "import secrets; print(secrets.token_urlsafe(64))".
configuration.license_data —
not by any environment variable. There is deliberately no env knob to relax tier enforcement (the
old TIER_ENFORCEMENT_MODE override was removed entirely — including from
.env.example; enforcement is always on). See
the Tiers & Quotas guide.
Backend — Server, networking & TLS
| Variable | Default | Purpose |
|---|---|---|
ENVIRONMENT required | dev | Deployment environment: dev / test / staging / prod / production. Drives security guards (e.g. SEC-002 refuses to boot with the default JWT_SECRET in prod/staging) and CORS defaults. |
DEBUG | false | Master debug flag. When on, enables local-dev behaviour. Keep false in production. |
PORT | 8080 | Internal aiohttp/Gunicorn bind port (also used by the nginx proxy upstream). Compose sets 8080. |
USE_SSL | false | Terminate TLS in the Python backend directly. Keep false when nginx terminates SSL (standard deployment); set true only for direct-HTTPS backends. |
CERT_FILE | (none) | Path to the TLS certificate (inside the container) when USE_SSL=true. Example: /etc/ssl/testhide.crt. |
CERT_KEY | (none) | Path to the TLS private key. Example: /etc/ssl/testhide.key. |
APP_EXTERNAL_URL | value of API_URL else https://localhost:8080 | External URL of the backend API; injected into build environments as TESTHIDE_URL. |
API_URL | https://localhost:8080 | Public API base URL. Also consumed by the Angular build (see Frontend). |
FRONTEND_PUBLIC_URL | (none) | Public URL of the web UI, used in generated links / redirects. |
CORS_ALLOWED_ORIGINS | * | Comma-separated allowed origins, or * for open mode. In dev/test the Angular dev origins (http(s)://localhost:4200) are auto-added. A * value in production logs a SEC-C-3 warning — set explicit origins. Example: https://app.example.com,https://staging.example.com. |
TESTHIDE_TRUST_PROXY | 0 | Trust the X-Forwarded-For header for the real client IP (set to 1 behind nginx). |
TRUSTED_PROXIES | (empty set) | Comma-separated proxy IPs allowed to set X-Forwarded-For. When empty, only private/loopback hops are trusted. |
TESTHIDE_TRUSTED_ORIGINS | (empty) | Comma-separated extra trusted origins for snapshot/eval write endpoints (CSRF-style allowlist). |
CLIENT_MAX_SIZE_MB | 4096 | Max accepted request body size (MB) for the aiohttp app (large artifact uploads). |
STRICT_WEBHOOK_HMAC | (off) | When 1/true, the inbound git webhook (/api/v3/git/notifyCommit) is rejected unless it carries a valid project header + HMAC signature. Off by default so standard SCM webhooks keep working; a configured secret is always enforced regardless. |
BITBUCKET_API_VERSION | 1.0 | Bitbucket REST API version the SCM provider speaks. |
INTERNAL_API_URL / INTERNAL_WS_URL | http://backend:8080 / ws://backend:8080 | Internal Docker service URLs used by the nginx proxy. Do not change for standard deployments. |
Backend — MongoDB
| Variable | Default | Purpose |
|---|---|---|
MONGO_USER required | testhide_user | MongoDB username. |
MONGO_PASS required | (empty) | MongoDB password. URL-encoded into the connection string. Set a strong value. |
MONGO_HOST | localhost | MongoDB host (Compose uses the service name mongo). |
MONGO_PORT | 27017 | MongoDB port. |
MONGO_DB_NAME | testhide_database | Database name. |
MONGO_AUTH_SOURCE | value of MONGO_DB_NAME | Auth database for the Mongo user. |
The effective URI is assembled as mongodb://USER:PASS@HOST:PORT/DB?authSource=AUTH. The host data path (MONGO_DATA_PATH) is a Docker-compose volume binding, not read by the app.
Backend — Redis
| Variable | Default | Purpose |
|---|---|---|
REDIS_PASSWORD required | (none) | Redis password. Set a strong value in production. |
REDIS_HOST | localhost | Redis host (Compose uses redis). |
REDIS_PORT | 6379 | Redis port. |
REDIS_DB | 0 | Redis logical DB index. |
Backend — Auth & security
| Variable | Default | Purpose |
|---|---|---|
JWT_SECRET required | a-very-bad-default-secret-for-local-dev | HMAC signing key for JWT auth and the auto-derived sidecar token. The backend refuses to start in prod/staging if this is still the dev default (SEC-002). Generate secrets.token_urlsafe(64); rotating invalidates all sessions and breaks the sidecar handshake. |
SECRET_ENCRYPTION_KEY | (none) | AES-256-GCM key (64 hex chars / 32 bytes) for encrypting stored secrets. Generate with openssl rand -hex 32. If unset, falls back to legacy zlib+base64 obfuscation with a warning. |
SIDECAR_AUTH_TOKEN | auto-derived from JWT_SECRET | Optional explicit token for the Docker sidecar. Normally left unset — backend and sidecar both compute HMAC-SHA256(JWT_SECRET, "testhide:sidecar:v1"). Set only to use a static token (takes priority over the derived value). |
ALLOW_DEFAULT_SERVICE_ACCOUNT | (off) | Dev-only: when 1 (and ENVIRONMENT=dev) keeps the default service account instead of disabling it. Never set in production. |
TESTHIDE_ALLOW_INSECURE_WEBHOOKS | 0 | SEC-011: when 1, permits unverified TLS for outbound webhooks / MS Teams — but only when the individual webhook row also sets allow_insecure_tls. Both gates must agree. Default verifies TLS. |
DB_DUMP_ALLOWED_IPS | (empty — fail closed) | SEC-014: CSV of IPs/CIDRs allowed to hit /api/v3/db/dump. Empty denies all (403). Example: 10.0.0.0/8,127.0.0.1. |
Backend — Gunicorn
| Variable | Default | Purpose |
|---|---|---|
GUNICORN_WORKERS | 2 | Worker processes per backend replica. Rule of thumb: (2 × cores) + 1. |
AI_API_GUNICORN_WORKERS | 2 | Worker count for the ai-api service (compose-level knob; mirrors GUNICORN_WORKERS for that container). |
GUNICORN_BIND | 0.0.0.0:8080 | Bind address:port. |
GUNICORN_THREADS | 1 | Threads per worker. |
GUNICORN_TIMEOUT | 120 | Worker request timeout (s). |
GUNICORN_GRACEFUL_TIMEOUT | 30 | Graceful shutdown timeout (s). |
GUNICORN_KEEPALIVE | 5 | Keep-alive seconds for idle connections. |
GUNICORN_REUSE_PORT | true | Enable SO_REUSEPORT on the listen socket. |
GUNICORN_MAX_REQUESTS | 10000 | Recycle a worker after N requests (caps memory creep). |
GUNICORN_MAX_REQUESTS_JITTER | 500 | Random jitter on the recycle threshold (avoids thundering-herd restarts). |
GUNICORN_LOG_LEVEL | info | Gunicorn log level. |
Backend — Process-mode toggles
The same image runs as backend, ai-api, or ai-worker depending on these flags (set in docker-compose).
| Variable | Default | Purpose |
|---|---|---|
LOAD_AI_MODELS | (unset → false) | Load AI model artifacts at startup. true on ai-api/ai-worker; false on backend replicas. |
RUN_AI_WORKER | (unset → false) | Run the embedded heavy AI worker loop (training/jobs). true only on ai-worker. |
RUN_INFERENCE_WORKER | (compose sets true on ai-worker) | Drain the async inference queue. Without it, /ai/inference/{id} polling never resolves. Do not unset on the worker. |
RUN_MODEL_SYNC | true when MODEL_SYNC_ENABLED=true | Run the model-sync upload/download crons on this process. |
DISABLE_CRON | (off) | When true, skip cron leader election, system tasks, and monitoring plugins (set on replicas that must not run crons). |
DISABLE_SYSTEM_TASKS | (off) | Disable background system maintenance tasks. |
SKIP_BUILD_RECOVERY | (off) | Skip the startup build-recovery pass (set on non-leader replicas). |
FORCE_INDEX_UPDATE | (off) | Force MongoDB index recreation at startup even if config is unchanged. |
AI_WORKER_HOST | ai-worker | Host the backend pokes to wake the AI worker. |
AI_WORKER_WAKE_HOST | 0.0.0.0 | Bind host for the worker’s wake listener. |
AI_WORKER_WAKE_PORT | 8100 | Wake-listener port. |
AI_WORKER_WAKE_TIMEOUT_S | 2.0 | Per-attempt wake HTTP timeout (s). |
AI_WORKER_WAKE_RETRIES | 3 | Wake retry attempts. |
Backend — Build/queue reliability
| Variable | Default | Purpose |
|---|---|---|
TPS_HEARTBEAT_TIMEOUT_SEC | 1200 | Seconds before a TPS executor session is considered dead (missing heartbeat). |
MAX_TPS_CONCURRENT_SESSIONS | 0 | Cap on concurrent TPS executor children across all builds. 0 disables the cap. |
TPS_RUNNING_PROVIDER_TIMEOUT_HOURS | 4 | Hard timeout (h) for a RUNNING_PROVIDER TPS build. 0 disables. |
INTERRUPTED_AUTO_CANCEL_MINUTES | 0 | Minutes an INTERRUPTED build may linger before the reconciler cancels it. 0 disables. |
QUOTA_BUILDS_PER_DAY | 1000 | Per-project daily build ceiling (Redis counter; per-project overrides in db_projects.quotas). |
QUOTA_AI_REQUESTS_PER_HOUR | 500 | Per-project hourly AI-request ceiling. |
QUOTA_ARTIFACT_GB_TOTAL | 100 | Per-project artifact storage cap (GB, advisory gauge). |
Per-user AI quotas (Ask-AI chat / deep diagnosis) are configured in the UI (AI Assist
settings; defaults 25 / 2 per day) — the former AI_QUOTA_* env vars were removed.
The deprecated MAX_BUILD_RETRIES and TIER_ENFORCEMENT_MODE vars were
removed entirely — enforcement is hard-coded on (see the callout above).
Backend — Observability
| Variable | Default | Purpose |
|---|---|---|
LOG_FORMAT | json | Log format: json (structured, for Loki) or text. |
LOG_LEVEL | INFO | Application log level. |
METRICS_ENABLED | true | Expose the Prometheus /api/v3/metrics endpoint. |
OTEL_TRACES_ENABLED | false (.env.example sets true) | Enable OpenTelemetry tracing. |
OTEL_EXPORTER_OTLP_ENDPOINT | http://tempo:4318 | OTLP/HTTP collector endpoint (Tempo). |
OTEL_SAMPLE_RATE | 1.0 (prod recommendation 0.1) | Trace sampling fraction (1.0 = 100%). |
SERVICE_NAME | testhide | Service label reported in traces/metrics. |
GRAFANA_ADMIN_PASSWORD, GRAFANA_ROOT_URL, GRAFANA_CERT_HOST_CRT/KEY,
PROMETHEUS_PORT_BIND, and PUBLIC_URL are consumed by the
docker-compose / Grafana / nginx stack, not by the Python app directly — see
the Deployment table below.
Backend — LLM providers & keys
| Variable | Default | Purpose |
|---|---|---|
GOOGLE_AI_API_KEY | (empty) | Google Gemini API key. When set, the local GGUF model is not loaded (saves ~2 GB RAM). Optional — local LLM works without it. Keys set here are synced into db_llm_providers at startup; the admin UI is the runtime source of truth. |
OPENAI_API_KEY | (empty) | OpenAI API key (fallback provider). |
ANTHROPIC_API_KEY | (empty) | Anthropic API key (fallback provider). |
Model selection is config‑driven, not env‑driven. There are three tiers — Chat (Ask‑AI), Deep diagnosis (investigate / narrate), and Code (Auto‑PR fixes). The per‑tier default model is no longer an env var (the old *_CHAT_MODEL / *_DEEP_MODEL / *_CODE_MODEL knobs were removed); it is picked on the LLM Providers page (model catalog stored in the database), falling back to per‑provider built‑in defaults. Provider API keys, routing priority (chain order), and the per‑tier default model are all set on that page and take effect instantly — no backend restart (the change is broadcast to every backend process over Redis pub/sub, busting the key / routing / cooldown caches). The env chains below remain only as the initial provider order. | ||
LLM_CHAT_PROVIDER_CHAIN | google,openai,anthropic,groq,deepinfra,local | Initial provider fallback order for the Chat tier (providers without a key are skipped; local is last-resort). The routing priority is overridable on the LLM Providers page and applied instantly. |
LLM_DEEP_PROVIDER_CHAIN | google,anthropic,openai,groq,mistral,deepinfra,local | Initial provider fallback order for the Deep diagnosis tier (investigate / narrate). |
LLM_CODE_PROVIDER_CHAIN | google,anthropic,openai,mistral,groq,deepinfra,local | Initial provider fallback order for the Code tier (Auto-PR fixes). The dedicated code preference adds Mistral codestral-latest and DeepInfra Qwen2.5-Coder as code-specialized defaults; an unconfigured code request resolves to the provider’s Deep model. |
LLM_TRANSIENT_RETRIES | 2 | Retries on transient provider errors (e.g. Gemini 503). |
LLM_TRANSIENT_BACKOFF_S | 0.8 | Backoff (s) between transient-error retries. |
LLM_MONTHLY_COST_CEILING_USD | 0 | Global monthly LLM spend cap (USD). 0 = unlimited; when >0, new calls are rejected once the month’s summed cost reaches it. |
LLM_LOCAL_MODEL_FREE | true | Treat local/self-hosted inference as $0. Set false to attribute an internal compute cost. |
AUTO_PR_FIX_MAX_CONCURRENCY | 4 | Max concurrent auto-PR-fix patch generations. 0 = unlimited. |
LLM_PRICING_OVERRIDES | {} | JSON map of model-prefix → [input_per_1k, output_per_1k] to override published token prices without a code change. |
LLM_PROVIDER_DEFAULT_PRICES | {} | JSON map of provider id → [in, out] default rate used when a model isn’t explicitly priced. |
LLM_PRICING_OVERRIDES='{"gemini-2.5-flash":[0.0003,0.0025],"gpt-4o":[0.005,0.015]}'
LLM_PROVIDER_DEFAULT_PRICES='{"openai":[0.005,0.015],"google":[0.00125,0.005]}'
Backend — Local LLM (on-device GGUF)
| Variable | Default | Purpose |
|---|---|---|
AI_LLM_DIR | releases/llm | Directory holding GGUF model files. |
AI_LLM_MODEL | (empty) | GGUF filename to load (e.g. Phi-3.5-mini-instruct-Q5_K_M.gguf). Auto-picked when exactly one *.gguf is present; set only for multi-model installs. .env.example uses AI_LLM_REPO as the download spec. |
AI_LLM_CTX | 8192 | Context window (tokens). Lower to save KV-cache RAM. |
ALLOW_LOCAL_LLM_FALLBACK | true | Allow on-device fallback when cloud providers are exhausted. |
LOCAL_LLM_MAX_TOKENS | 768 | Cap on local output tokens (latency control). |
LOCAL_LLM_REQUEST_TIMEOUT_S | 600 | Per-call budget for the local provider (cold GGUF loads are slow). |
HF_TOKEN | (none) | HuggingFace token for pulling gated/private model repos. |
HF_HUB_OFFLINE / TRANSFORMERS_OFFLINE | 0 | Set both to 1 for air-gapped mode after first boot (models persist in the HF cache volume). |
Inference threads, GPU-layer offload and CPU/GPU selection are now sized automatically from the
host (CPU cores, and CUDA + free VRAM when present) — the former AI_LLM_THREADS,
AI_LLM_GPU_LAYERS and AI_FORCE_CPU env vars are no longer required.
Use AI_TRAIN_CPU_ONLY=1 if you need to force CPU on a GPU host.
Backend — Edge AI (on-agent inference)
| Variable | Default | Purpose |
|---|---|---|
EDGE_AI_ENABLED | false | Master switch for distributed map-reduce inference on C# nodes. |
EDGE_NODE_TOKEN | (empty) | Shared bearer token C# edge nodes present on the edge data-plane. When set, result/status/payload handlers require it (forgery protection). Empty logs a warning when Edge AI is on. |
EDGE_LLM_MODEL | phi-3-mini-q4.gguf | GGUF model name the agent runs. |
EDGE_EMBEDDER_MODEL | minilm-l6-v2.onnx | ONNX embedder the agent runs. |
EDGE_VISION_MODEL | clip_image.onnx | CLIP image-tower ONNX for the visual-diff pre-screen. |
EDGE_VISION_ENABLED | 0 | Opt-in switch for the Edge vision pre-screen. When off, the manifest omits the vision entry. |
EDGE_AI_TIMEOUT_SEC | 90 (.env.example 600) | Edge investigation overall timeout (s). |
EDGE_AI_STALL_SEC | 60 | Stall timeout (s) before an edge investigation is abandoned. |
EDGE_TASK_LEASE_SEC | 45 | Lease duration for an edge task assigned to a node. |
EDGE_PARTIAL_REDUCE_SEC | 30 | Interval (s) for partial map-reduce aggregation. |
EDGE_LOG_CHUNK_LINES | 150 | Lines per log chunk dispatched to nodes. |
EDGE_LOG_OVERLAP_LINES | 20 | Overlap lines between consecutive chunks. |
Backend — AI worker memory & concurrency
| Variable | Default | Purpose |
|---|---|---|
AI_HARD_EXIT_PERCENT | 88 | RSS percent at which the worker hard-exits to dodge an OOM kill. |
AI_RETRAIN_QUEUE_BUSY_THRESHOLD | 200 | Skip retraining when the build queue backlog exceeds this. |
AI_LOCK_STALE_SECS | 300 | Stale-lock eviction timeout (s). |
AI_V2_JOB_CONCURRENCY | 1 | Concurrent v2 AI jobs the worker runs. |
The worker’s soft memory ceiling and its executor pool sizes (inference / IO / LLM / model-load)
are auto-derived from the container’s RAM budget and usable CPU cores — the former
AI_MEMORY_LIMIT_MB and AI_ASSIST_*_WORKERS env vars are no longer required.
Backend — Model-sync (federated model exchange)
| Variable | Default | Purpose |
|---|---|---|
MODEL_SYNC_ENABLED | true | Enable local model-sync crons (still gated by license capabilities and authorized per transfer by the server). |
MODEL_SYNC_AUTO_PROMOTE | true | Auto-promote a downloaded base model after validation. |
MODEL_SYNC_REQUIRE_BENCHMARK | false | Require a passing benchmark before promotion. |
MODEL_SYNC_R2_HOST_ALLOWLIST | (empty) | CSV of allowed R2 hostnames for presigned PUT/GET (anti-SSRF). Empty allows any https host with a warning. |
MODEL_SYNC_MANIFEST_PUBKEY | baked vendor key | Ed25519 public key for verifying download manifests. The vendor key is hardcoded in the image (verification is fail-closed by default); the env var exists only as a dev-stand override. |
LICENSE_TLS_VERIFY | true | Verify the license-server / model-sync control-plane TLS cert (default ON; the production license server has a real public cert). Set 0 only behind a TLS-inspecting proxy or against a private-CA staging endpoint. |
Backend — License
| Variable | Default | Purpose |
|---|---|---|
LICENSE_GRACE_SECONDS | 1209600 (14 days) | How long the last-good tier survives without a successful sync before downgrade to Free. |
LICENSE_REVOKE_SECRET | (empty) | HMAC key for verifying the inbound license-revocation webhook (X-License-Revoke-Sig). Empty skips the signature check. |
LICENSE_REVOKE_ALLOWED_IPS | 127.0.0.1,::1 | CSV of IPs/CIDRs allowed to call the revocation endpoint. |
Backend — Docker sidecar (SEC-004)
| Variable | Default | Purpose |
|---|---|---|
SIDECAR_URL | http://sidecar-docker:8081 | Base URL the backend uses to reach the sidecar. |
SIDECAR_ALLOWED_IMAGES | (set in compose) | Allow-list of images the sidecar may docker run. Example: thuesdays/testhide-agent:latest,thuesdays/testhide-backend:latest. |
SIDECAR_ALLOWED_EXEC_PATTERNS | ^python\s,^bash\s,^sh\s | Regex prefixes the sidecar accepts in docker exec. |
SIDECAR_ALLOWED_NETWORKS | testhide-internal,bridge,testhide-net | Networks spawned containers may attach to. |
SIDECAR_ALLOWED_VOLUME_SOURCES | (unset) | CSV of host path prefixes allowed as bind-mount sources. |
SIDECAR_ALLOWED_VOLUME_NAME_PATTERNS | (unset) | Regex patterns for named-volume sources the sidecar accepts. |
SIDECAR_ALLOW_PORT_BINDING | 0 | Set 1 only if spawned containers must bind host ports. |
DOCKER_API_VERSION | (compose pins 1.41) | Docker Engine API version the backend/sidecar speaks. |
Backend — LDAP / Active Directory (SEC-007)
| Variable | Default | Purpose |
|---|---|---|
LDAP_HOST | (empty → local auth only) | LDAP/AD server hostname. Leave empty to disable LDAP. |
LDAP_PORT | 636 | LDAP port (636 = LDAPS, the secure default). |
LDAP_USE_SSL | 1 | Use LDAPS/TLS. Setting 0 is grandfathered with a loud warning. |
LDAP_CA_BUNDLE | (empty) | Path to the LDAP server’s CA PEM bundle. |
LDAP_TLS_INSECURE | 0 | When 1, do not require/verify the server cert. |
LDAP_TLS_VERSION | TLSv1.2 | Minimum TLS version for LDAPS. |
Backend — vCenter
| Variable | Default | Purpose |
|---|---|---|
VCENTER_VERIFY_TLS | false | Default TLS verification for vCenter connections (corp vCenters often use self-signed certs). A connection may override via its insecure flag. |
VCENTER_MAX_SYNC_ITEMS | 20000 | Cap on hosts/VMs persisted per connection (keeps the doc under Mongo’s 16 MB limit). |
Backend — Integrations & monitoring sandbox
| Variable | Default | Purpose |
|---|---|---|
JIRA_URL | (empty) | Jira base URL. Empty disables the Jira auto-linker. |
JIRA_USERNAME / JIRA_PASSWORD | (empty) | Jira credentials (use an API token, not your password). |
JIRA_EMBEDDINGS_PROJECTS | (empty) | CSV of Jira project keys to index for the Bug Linker embeddings cron. |
MONITORING_PLUGINS_DIR | <root>/monitoring_scripts | Directory for user monitoring scripts (must be writable). |
MONITORING_SCRIPT_TIMEOUT_S | 900 | Wall-clock kill for a sandboxed monitoring script (15 min). |
MONITORING_SCRIPT_CPU_S | 870 | POSIX RLIMIT_CPU for the sandbox. |
MONITORING_SCRIPT_MEM_MB | 512 | POSIX RLIMIT_AS (virtual memory) for the sandbox. |
MONITORING_SCRIPT_OUTPUT_MAX_BYTES | 8388608 (8 MiB) | Max captured stdout bytes. |
MONITORING_UPLOAD_MAX_BYTES | 1048576 (1 MiB) | Max monitoring upload payload. |
MONITORING_SCRIPT_MAX_CONCURRENCY | 4 | Concurrent sandboxed runs (each occupies a worker thread). |
MONITORING_RUNNER_PATH | (auto-resolved) | Override path to the sandbox runner.py (for custom layouts/tests). |
Backend — Native math-lib thread caps
The native math-lib thread-fan-out caps (OMP_NUM_THREADS, MKL_NUM_THREADS,
OPENBLAS_NUM_THREADS, NUMEXPR_NUM_THREADS) and the related Intel/HF knobs
(KMP_BLOCKTIME, KMP_AFFINITY, KMP_DUPLICATE_LIB_OK,
TOKENIZERS_PARALLELISM, HF_DATASETS_NUM_PROC,
PYTORCH_CUDA_ALLOC_CONF) are sized automatically from the usable CPU-core count and
set by the training pipeline — they are no longer required on a normal install. They remain
available as a deployment-environment override only for an exotic host.
Frontend (Angular build-time)
Consumed by testhide_ui/scripts/set-env.js when generating src/assets/env.js — baked into the bundle at build time.
| Variable | Default | Purpose |
|---|---|---|
API_URL | else PUBLIC_API_URL, else https://localhost:8080 | Backend REST base URL embedded in the bundle. |
WS_URL | else PUBLIC_WS_URL, else wss://localhost:8080 | WebSocket base URL embedded in the bundle. |
PRODUCTION | false | Sets Angular production mode in the generated env. |
License server (Django — config/settings.py)
| Variable | Default | Purpose |
|---|---|---|
SECRET_KEY required | (none — hard fail) | Django secret key. No fallback — the server refuses to start if unset. Generate secrets.token_urlsafe(64). |
DEBUG | False | Django debug. When off, enables SSL redirect, secure cookies, and HSTS. |
DATABASE_URL | (falls back to local SQLite) | Database connection URL (parsed by dj_database_url, TLS required). Unset uses db.sqlite3. |
RENDER_EXTERNAL_HOSTNAME | (none) | Render-provided hostname auto-appended to ALLOWED_HOSTS. |
DJANGO_LOG_LEVEL | INFO | Django logger level. |
TRUSTED_PROXY_IPS | (empty) | CSV of proxy IPs trusted to set X-Forwarded-For. |
CONTACT_EMAIL | thuesdays@gmail.com | Destination for contact-form submissions. |
EMAIL_SYNC_IN_PROCESS | True | Run the in-process IMAP→DB webmail sync thread. Set False when using the cron command. |
EMAIL_HOST | smtp.gmail.com | SMTP host (production email backend). |
EMAIL_PORT | 587 | SMTP port (STARTTLS). |
EMAIL_USE_TLS | True | Use STARTTLS for SMTP. |
EMAIL_HOST_USER | (empty) | SMTP username. |
EMAIL_HOST_PASSWORD | (empty) | SMTP password (use a Gmail App Password, not your login). |
DEFAULT_FROM_EMAIL | EMAIL_HOST_USER else noreply@testhide.com | Default From: address. |
STRIPE_SECRET_KEY | (empty) | Stripe secret API key. |
STRIPE_WEBHOOK_SECRET | (empty) | Stripe webhook signing secret. |
STRIPE_PRICE_STARTER_MONTHLY / _YEARLY | price_starter_monthly / price_starter_yearly | Stripe price IDs for the Starter tier. |
STRIPE_PRICE_TEAM_MONTHLY / _YEARLY | price_team_monthly / price_team_yearly | Stripe price IDs for the Team tier. |
STRIPE_PRICE_ID_PRO10 / PRO50 / PRO100 / ENTERPRISE | (none) | Legacy Stripe price IDs (kept for the existing handler). |
MODELSYNC_REQUIRE_SIGNATURE | True | Require the HMAC signature on every /model-sync/* request. A False value with DEBUG off logs a CRITICAL warning (auth disabled). |
MODELSYNC_FERNET_KEY | (empty → derived from SECRET_KEY) | Dedicated Fernet key (url-safe base64, 32 bytes) for the R2 secret vault. Set in prod to decouple it from SECRET_KEY. |
.NET agent (TESTHIDE_* overrides)
These override the agent’s on-disk config.json (resolved at
C:\ProgramData\Testhide\config.json / ~/.config/testhide). They take
effect on next launch.
| Variable | Default | Purpose |
|---|---|---|
TESTHIDE_LICENSE_KEY | (none) | License key; its SHA-256 hash is stored and forces re-registration when changed. |
TESTHIDE_WEBSOCKET_URL | (none) | Backend WebSocket URL the agent connects to. |
TESTHIDE_INSTANCE_ID | (none) | Override the agent instance ID. |
TESTHIDE_LICENSE_API_URL | (none) | Override the license API URL the agent uses. |
TESTHIDE_POOL_NAME | (config value) | Node pool name advertised to the backend (overrides config). |
TESTHIDE_NODE_TYPE | (none) | Node type label in the environment fingerprint. |
TESTHIDE_SERVICE_MODE | (none) | When 1, the launcher runs in Windows-service mode. |
TESTHIDE_NO_UPDATE | (none) | When 1, disables the agent auto-updater. |
TESTHIDE_UPDATE_URL | https://dl.testhide.com/stable/latest.json | Update manifest URL. |
WORKSPACE | (current directory) | Build/checkout workspace root used by the pipeline + issue detector. |
Deployment / Docker (compose, nginx, Grafana)
These are read by docker-compose / the surrounding stack rather than the Python app directly.
| Variable | Default | Purpose |
|---|---|---|
PUBLIC_URL required | (your domain) | Public-facing URL; feeds CORS, Grafana, and Angular bundles. Must match the SSL cert domain. |
WS_URL | wss://YOUR_DOMAIN:7771 | Public WebSocket URL embedded at build time. |
PRODUCTION | true | Compose/Angular production flag. |
MONGO_DATA_PATH | (host path) | Host directory where MongoDB stores data (compose volume bind). |
GRAFANA_ADMIN_PASSWORD required | (none) | Grafana admin password — change before first deploy. |
GRAFANA_ROOT_URL | PUBLIC_URL:3000 | Grafana public URL for UI links / OAuth redirects. |
GRAFANA_CERT_HOST_CRT / _KEY | ./ssl/testhide.crt / .key | Grafana SSL cert/key host paths. |
PROMETHEUS_PORT_BIND | 127.0.0.1:9090 | Prometheus bind (loopback only — never 0.0.0.0 in prod; Prometheus has no auth). |
SIDECAR_ALLOWED_* | (see Sidecar table) | Sidecar allow-lists, typically supplied through compose. |
Compose also accepts ${VAR:-default} substitutions, e.g.:
SIDECAR_ALLOWED_IMAGES=${SIDECAR_ALLOWED_IMAGES:-thuesdays/testhide-agent:latest,thuesdays/testhide-backend:latest}
SIDECAR_ALLOWED_EXEC_PATTERNS=${SIDECAR_ALLOWED_EXEC_PATTERNS:-^python\s,^bash\s,^sh\s}
Advanced AI/ML tuning
AITrainingConfig, so the admin UI can override them per training run).
Batch widths, thread/worker counts, dataset caps, memory ceilings and CPU/GPU selection are now
sized automatically from your host and have been dropped from the tables below — a fresh
install needs zero AI tuning env vars; set only secrets, URLs and policy toggles.
Root-cause classifier (RC_*)
| Variable | Default | Purpose |
|---|---|---|
RC_MODEL_NAME | distilbert-base-uncased | Base transformer for the classifier (must match across ai-api/ai-worker, shared HF cache). |
RC_EPOCHS | 3 | Training epochs (overrides adaptive tier default). |
RC_LR | 2e-5 | Learning rate. |
RC_WEIGHT_DECAY | 0.01 | Optimizer weight decay. |
RC_FREEZE_BACKBONE | False | Freeze the transformer backbone (train head only). |
RC_SEED | 42 | Random seed. |
RC_MAX_LEN | 256 | Tokenizer max length. |
RC_NUM_WORKERS | 0 | DataLoader worker processes. |
RC_AUTO_CONVERT_ONNX | False | Auto-export to ONNX after training. |
The classifier batch size is auto-sized from the capacity tier (the former RC_BATCH_SIZE
env var is no longer required). The RC_TEST_SIZE and RC_CLEANLAB_* env vars
were removed — they had no effect on the shipped CPU pipeline.
Failure retriever / FAISS (RC_* retriever knobs)
| Variable | Default | Purpose |
|---|---|---|
RC_FAISS_KIND | (auto) | FAISS index kind: flat / hnsw / ivfpq. Auto-picked from corpus size; set to override (invalid falls back to flat). |
RC_GROUP_BY | test_name | Contrastive-pair grouping key. |
RC_MODE | precomputed | Retriever training mode. |
RC_BATCH | 512 | Retriever batch size. |
RC_TAU | 0.06 | Contrastive temperature. |
RC_PAIRS_TOPK | 12 | Top-K positives per anchor. |
RC_PAIRS_MAX_PER_GROUP | 16 | Max pairs per group. |
RC_TH_TEXT | 0.60 | Text-similarity threshold. |
RC_TH_STRUCT | 0.70 | Structured-similarity threshold. |
RC_SAMPLE_GROUPS_FRAC | 0.0 | Fraction of groups to sample (0 = all). |
RC_SANITY | False | Run a quick sanity training pass. |
RC_CPU_ONLY | False | Force CPU retriever training. |
RC_PACK_DTYPE | f32 | Packed-vector dtype. |
RC_PACK_BLOCK | 0 | Pack block size (0 = auto). |
RC_PACK_TARGET_MB | 800 | Target packed-shard size (MB). |
RC_PACK_THREADS | 0 | Packing threads (0 = auto). |
RC_TORCH_COMPILE | True | Use torch.compile. |
RC_USE_BF16 | False | Use bfloat16. |
RC_USE_AMP | True | Use automatic mixed precision. |
RC_LOG_EVERY | 1000 | Log cadence (steps). |
RC_EVAL_EVERY | 1 | Eval cadence (epochs). |
RC_CLUSTER_EVAL | True | Run cluster-quality eval. |
RC_KMEANS_K | 100 | K-means clusters for eval. |
RC_DBSCAN_EPS | 0.5 | DBSCAN epsilon. |
RC_DBSCAN_MINPTS | 10 | DBSCAN min points. |
RC_FAISS_CHUNK | 8000 | FAISS add chunk size. |
RC_FAISS_PCA | 512 | PCA dim before FAISS. |
RC_FAISS_FP16 | False | Store FAISS vectors in fp16. |
RC_D_OUT | 512 | Output embedding dim. |
RC_CLIP_NORM | 1.0 | Gradient clip norm. |
RC_QUEUE | 160000 | Memory-queue size for negatives. |
RC_HARD_NEG_K | 0 | Hard-negative count (0 = off). |
RC_ACCUM | 4 | Gradient accumulation steps. |
RC_TEMPLATES_DIR | (release dir) | Override the log-template directory. |
Build RCA classifier (BUILD_RCA_*)
| Variable | Default | Purpose |
|---|---|---|
BUILD_RCA_ML_MODE | shadow | Inference mode: shadow / active / disabled. |
BUILD_RCA_ML_CONFIDENCE_THRESHOLD | 0.60 | Min ML confidence to use the prediction over rules-v1 (active mode). |
BUILD_RCA_RETRAIN_THRESHOLD | 200 | New corpus samples that trigger auto-retrain (set high on the backend so training only runs in ai-worker). |
BUILD_RCA_REDIS_LOCK_TTL | 3600 | Training-lock TTL (s). |
BUILD_RCA_WEEKLY_MIN_SAMPLES | 50 | Min new samples before the weekly cron retrains. |
BUILD_RCA_SHADOW_MIN_SAMPLES | 50 | Min shadow predictions before considering promotion. |
BUILD_RCA_SHADOW_MIN_AGREEMENT | 0.70 | Min ML/rules agreement rate to auto-promote. |
BUILD_RCA_SHADOW_DAYS | 7 | Look-back window (days) for the agreement check. |
BUILD_RCA_SHADOW_ROLLBACK_THRESHOLD | 0.60 | Agreement rate below which an active model is auto-rolled-back. |
BUILD_RCA_SEED_PATH | (auto-resolved) | Path to the bundled seed corpus (needed in the compiled/protected image). |
STEP_ANOMALY_Z_THRESHOLD | 2.5 | Z-score threshold for the build step-duration anomaly detector. |
OOD detector (AI_OOD_*)
| Variable | Default | Purpose |
|---|---|---|
AI_OOD_AE_HIDDEN | 256 | Autoencoder hidden width. |
AI_OOD_AE_BOTTLENECK | 64 | Autoencoder bottleneck width. |
AI_OOD_AE_EPOCHS | 10 | Autoencoder epochs. |
AI_OOD_AE_LR | 1e-3 | Autoencoder learning rate. |
AI_OOD_IF_ESTIMATORS | 200 | Isolation-forest estimators. |
AI_OOD_FUSION_ALPHA | 0.5 | AE/IF fusion weight. |
AI_OOD_THRESHOLD_QUANTILE | 0.97 | Score quantile for the OOD threshold. |
AI_OOD_SEED | 42 | Random seed. |
Visual classifier / YOLO (AI_VISUAL_*, AI_YOLO_*)
| Variable | Default | Purpose |
|---|---|---|
AI_VISUAL_EPOCHS | 5 | Visual classifier epochs. |
AI_VISUAL_HIDDEN_LAYERS | 256 | Hidden layer width. |
AI_VISUAL_MAX_ITER | 500 | Max solver iterations. |
AI_VISUAL_TEST_SIZE | 0.2 | Validation split. |
AI_VISUAL_SEED | 42 | Random seed. |
AI_YOLO_MODEL | yolov8n.pt | YOLO weights file. |
AI_YOLO_EPOCHS | 5 | YOLO training epochs. |
AI_YOLO_FREEZE | 10 | Frozen backbone layers. |
AI_YOLO_MIN_ANNOTATIONS | 10 | Skip training below N annotated screenshots. |
AI_YOLO_WEIGHT_CORRECTION | 10 | Sample weight for correction annotations. |
AI_YOLO_WEIGHT_MANUAL | 5 | Sample weight for manual annotations. |
GPU vs. CPU selection and the YOLO batch size are auto-detected (CUDA + free VRAM) — the
former AI_VISUAL_USE_GPU, AI_YOLO_FORCE_CPU and AI_YOLO_BATCH
env vars are no longer required.
Flakiness predictor (AI_FLAKY_*)
| Variable | Default | Purpose |
|---|---|---|
AI_FLAKY_MAX_ITER | 200 | Max solver iterations. |
AI_FLAKY_RANDOM_STATE | 42 | Random state. |
The flakiness predictor is a gradient-boosted classifier with no epoch concept — the former
AI_FLAKINESS_EPOCHS env var had no effect and was removed.
Log-signature miner (LOGSIGN_* / AI_LOGSIGN_*)
| Variable | Default | Purpose |
|---|---|---|
LOGSIGN_DIR | (templates_out release dir) | Output directory for mined Drain3 templates. |
LOGSIGN_MAX_JOINED_LEN (alias AI_LOGSIGN_MAX_JOINED_LEN) | 8000 | Max joined log length fed to the miner. |
LOGSIGN_REUSE_TEMPLATES | 1 | Reuse persisted templates between runs. |
AI_LOGSIGN_TOP_EXAMPLES | 3 | Example snippets stored per template. |
AI_LOGSIGN_MIN_COUNT | 1 | Min cluster size to keep a template. |
AI_LOGSIGN_SNIPPET_LEN | 600 | Stored snippet length (chars). |
Dataset, budget & lock knobs
| Variable | Default | Purpose |
|---|---|---|
AI_TRAINING_LOCK_TTL_SEC | 3600 | Training-lock TTL (single session at a time). |
AI_VECTORS_SIDECAR_ENABLED | 1 | Extract vectors into float32 sidecar files (smaller dataset, faster training). |
AI_USE_ARTIFACT_LOADER | (off) | Route model (re)loads through the ModelArtifactLoader ABCs. |
AI_LLM_SEED | 42 | Local LLM sampling seed. |
TEXT_CACHE_SNAPSHOT_MIN_NEW | 50 | Min new cached embeddings before a snapshot flush. |
AI_PRECOMPUTE_STATE_TTL | 604800 (7 d) | Redis TTL for precompute-state keys. |
AI_PRECOMPUTE_LOCK_TTL | 900 | Precompute lock TTL (s). |
AI_INVESTIGATION_BUDGET_S | 600 | Time budget for an LLM investigation (under the 900 s claim lease). |
AI_WARM_FC_MAX | 30 | Cap on warm failure-context precomputations per batch. |
AI_FG_ENRICH_BATCH | 100 | Failure-group enrichment batch size. |
AI_RULE_SUGGESTION_MIN_COUNT | 2 | Min occurrences before a Layer-1 rule suggestion is drafted. |
AI_TIER2_AUTO_GC | 1 | Auto-GC raw insights after Tier-2 failure-history distillation. |
SHORT_KEY_FALLBACK_SAMPLE_EVERY | 1 | Throttle for short-key fallback sampling logs. |
Dataset shard/row/size caps, the memory-budget chunking factors, corpus and embedding-cache caps,
the inference batch cap and vectorizer row concurrency are now derived automatically from the
container’s RAM budget and CPU-core count — the former AI_DATASET_MAX_MB,
AI_DATASET_MAX_ROWS, AI_DATASET_SHARD_ROWS, AI_COMPACT_AFTER_SHARDS,
AI_BUDGET_*, AI_CORPUS_MAX_PER_CLASS, AI_TEXT_CACHE_MAX_ENTRIES,
AI_INFER_MAX_BATCH, ROW_CONCURRENCY, AI_PRECOMPUTE_MAX_CONCURRENT
and OFFLINE_EMBED env vars are no longer required.
ETA estimator
The ETA estimator's statistical tunings (sample thresholds, duration sanity range,
retention, cache TTLs, robust-stats parameters) are baked-in constants — the former
ETA_* env vars were removed; there is nothing to configure.
SCM circuit breaker (SCM_CB_*)
| Variable | Default | Purpose |
|---|---|---|
SCM_CB_COOLDOWN_SEC | 60 | Cooldown after an SCM connection failure (short-circuits new calls). |
SCM_CB_WARN_THROTTLE_SEC | 300 | Min interval between repeated WARN logs for the same endpoint. |
testhide/settings.py holds most operator defaults, the
app/ai_assist/…/config.py and app/managers/ai_config.py modules hold
the AI/ML knobs, and static/landing/.env.example is the curated operator template.
API Reference
Authorization: Bearer <token>. Obtain a token via POST /api/v3/authenticate or create a long-lived API token in Settings → API Tokens.Health
Server liveness probe. No authentication required.
Returns server status and version. No auth required. Use as a Kubernetes liveness probe or uptime-monitor endpoint.
Required headers
| Header | Value |
|---|---|
| — | None required |
curl https://your-testhide-server/api/v3/healthz
{"status": "ok", "version": "3.14.2", "build": "a1b2c3d"}
Authentication
Obtain and manage JWT tokens. Session tokens expire in 8 hours; API tokens never expire unless revoked.
Exchange username and password for a JWT session token. Pass the returned token in the Authorization header for all subsequent requests.
Required headers
| Header | Value |
|---|---|
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| username | string | User email or login name |
| password | string | Account password |
curl -X POST https://your-testhide-server/api/v3/authenticate \
-H "Content-Type: application/json" \
-d '{"username": "admin@corp.com", "password": "s3cr3t!"}'
{"token": "eyJhbGci...", "expires_at": "2026-05-21T14:00:00Z", "user_id": 42}
Invalidate the current session token immediately. The token is server-side blacklisted.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl -X POST https://your-testhide-server/api/v3/logout \ -H "Authorization: Bearer $TOKEN"
(no content)
Return the profile of the currently authenticated user including role, permissions, and accessible project IDs.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl https://your-testhide-server/api/v3/me \ -H "Authorization: Bearer $TOKEN"
{"id": 42, "email": "admin@corp.com", "role": "admin", "projects": [1, 3, 7]}
Return server-level configuration visible to the current user — feature flags, enabled integrations, AI settings, and global pipeline options.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl https://your-testhide-server/api/v3/config \ -H "Authorization: Bearer $TOKEN"
{"version": "3.14.2", "global_options": {"ai_assist": {"enabled": true}, "max_parallel_builds": 20}}
Authenticate via SAML SSO. Exchange a base64-encoded IdP-signed SAML response for a Testhide JWT session token.
Required headers
| Header | Value |
|---|---|
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| saml_response | string | Base64-encoded SAML response from IdP |
curl -X POST https://your-testhide-server/api/v3/sso \
-H "Content-Type: application/json" \
-d '{"saml_response": "PHNhbWxwOlJlc3BvbnNl..."}'
{"token": "eyJhbGci...", "user_id": 42, "expires_at": "2026-05-21T14:00:00Z"}
Request a password reset email. A one-use time-limited link is sent to the address if an account exists.
Required headers
| Header | Value |
|---|---|
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| string | Account email address |
curl -X POST https://your-testhide-server/api/v3/reset-password \
-H "Content-Type: application/json" \
-d '{"email": "dev@corp.com"}'
{"detail": "Reset link sent if the account exists."}
Complete a password reset using the token from the reset email and the new desired password.
Required headers
| Header | Value |
|---|---|
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| token | string | Reset token from the email link |
| new_password | string | New password (min 8 chars) |
curl -X POST https://your-testhide-server/api/v3/reset-password/confirm \
-H "Content-Type: application/json" \
-d '{"token": "abc123xyz", "new_password": "Str0ng!Pass"}'
{"detail": "Password updated successfully."}
List all long-lived API tokens for the current user. Token secrets are never returned after creation.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl https://your-testhide-server/api/v3/api-tokens \ -H "Authorization: Bearer $TOKEN"
[{"id": 1, "name": "CI Bot", "created_at": "2026-01-10T09:00:00Z", "last_used": "2026-05-19T12:30:00Z"}]
Create a new long-lived API token. The token secret is returned only once — store it securely immediately.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| name | string | Human-readable label, e.g. "GitHub Actions" |
curl -X POST https://your-testhide-server/api/v3/api-tokens \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "GitHub Actions Bot"}'
{"id": 5, "name": "GitHub Actions Bot", "secret": "th_live_xK9p...Q2rZ", "created_at": "2026-05-20T10:00:00Z"}
Permanently revoke an API token. Takes effect immediately.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| token_id | integer | ID of the token to revoke |
curl -X DELETE https://your-testhide-server/api/v3/api-tokens/5 \ -H "Authorization: Bearer $TOKEN"
(no content)
Users
Manage platform users. Admin role required for create/update operations.
Return a paginated list of all platform users with their roles and project assignments.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Optional query params
| Param | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| limit | integer | Items per page (default: 50, max: 200) |
| search | string | Filter by email or name |
curl "https://your-testhide-server/api/v3/users?page=1&limit=20" \ -H "Authorization: Bearer $TOKEN"
{"total": 84, "results": [{"id": 1, "email": "alice@corp.com", "role": "admin", "active": true}]}
Create a new platform user. An invitation email is sent to the provided address.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| string | User email address | |
| role | string | admin | developer | viewer |
Optional body
| Field | Type | Description |
|---|---|---|
| first_name | string | First name |
| last_name | string | Last name |
| project_ids | array[int] | Grant access to these project IDs |
curl -X POST https://your-testhide-server/api/v3/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "bob@corp.com", "role": "developer", "project_ids": [1, 3]}'
{"id": 88, "email": "bob@corp.com", "role": "developer", "invited_at": "2026-05-20T10:00:00Z"}
Fetch full profile of a single user by ID including role, project access, and last login.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| user_id | integer | ID of the user |
curl https://your-testhide-server/api/v3/users/88 \ -H "Authorization: Bearer $TOKEN"
{"id": 88, "email": "bob@corp.com", "role": "developer", "active": true, "projects": [1, 3], "last_login": "2026-05-19T08:00:00Z"}
Update a user's role, active status, or project assignments.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| user_id | integer | ID of the user to update |
Optional body
| Field | Type | Description |
|---|---|---|
| role | string | admin | developer | viewer |
| active | boolean | Enable or disable the account |
| project_ids | array[int] | Replace project access list entirely |
curl -X PUT https://your-testhide-server/api/v3/users/88 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "admin", "project_ids": [1, 3, 7]}'
{"id": 88, "email": "bob@corp.com", "role": "admin", "project_ids": [1, 3, 7]}
Return the resolved effective permission set for a user across all projects and platform resources.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| user_id | integer | ID of the user |
curl https://your-testhide-server/api/v3/users/88/permissions \ -H "Authorization: Bearer $TOKEN"
{"user_id": 88, "can_start_builds": true, "can_manage_nodes": false, "is_admin": false, "projects": {"1": "write", "3": "read"}}
Projects
Projects group jobs, builds, and settings into isolated workspaces.
List all projects the current user has access to.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl https://your-testhide-server/api/v3/projects \ -H "Authorization: Bearer $TOKEN"
[{"id": 1, "name": "Backend API", "slug": "backend-api", "jobs_count": 12}]
Create a new project. The creating user is automatically assigned as project admin.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| name | string | Project display name |
Optional body
| Field | Type | Description |
|---|---|---|
| description | string | Short description |
| scm_endpoint_id | integer | Default SCM connection ID |
curl -X POST https://your-testhide-server/api/v3/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Mobile App", "description": "iOS/Android tests", "scm_endpoint_id": 2}'
{"id": 12, "name": "Mobile App", "slug": "mobile-app", "webhook_secret": "wh_sec_abc..."}
Fetch full details of a project including webhook URL, SCM configuration, and job statistics.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| project_id | integer | Project ID |
curl https://your-testhide-server/api/v3/projects/12 \ -H "Authorization: Bearer $TOKEN"
{"id": 12, "name": "Mobile App", "jobs_count": 8, "last_build_at": "2026-05-19T18:00:00Z", "webhook_url": "https://your-testhide-server/webhook/proj/12"}
Update project name, description, or SCM endpoint association.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| project_id | integer | Project ID |
Optional body
| Field | Type | Description |
|---|---|---|
| name | string | New display name |
| description | string | Updated description |
| scm_endpoint_id | integer | Default SCM connection |
curl -X PUT https://your-testhide-server/api/v3/projects/12 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Mobile App v2", "scm_endpoint_id": 3}'
{"id": 12, "name": "Mobile App v2", "scm_endpoint_id": 3}
Generate a new webhook HMAC secret. The old secret is invalidated immediately — update your SCM webhook settings after rotation.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| project_id | integer | Project ID |
curl -X POST https://your-testhide-server/api/v3/projects/12/rotate-webhook-secret \ -H "Authorization: Bearer $TOKEN"
{"webhook_secret": "wh_sec_xK9p...Q2rZ"}
Jobs
Jobs define the test pipeline — what runs, on which nodes, with which configuration. A Job produces Builds when triggered.
List all jobs accessible to the current user, optionally filtered by project.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Optional query params
| Param | Type | Description |
|---|---|---|
| project_id | integer | Filter by project |
| search | string | Filter by job name |
| enabled | boolean | Filter by enabled state |
curl "https://your-testhide-server/api/v3/jobs?project_id=1&enabled=true" \ -H "Authorization: Bearer $TOKEN"
[{"id": 55, "name": "Regression Suite", "project_id": 1, "node_pool_id": 2, "enabled": true}]
Create a new job within a project.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| name | string | Job display name |
| project_id | integer | Parent project ID |
Optional body
| Field | Type | Description |
|---|---|---|
| node_pool_id | integer | Target node pool (default: any available) |
| timeout_minutes | integer | Build timeout in minutes (default: 60) |
| max_parallel | integer | Max concurrent builds (default: 1) |
| pipeline_template_id | integer | Pre-defined pipeline template to apply |
curl -X POST https://your-testhide-server/api/v3/jobs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Smoke Tests",
"project_id": 1,
"node_pool_id": 2,
"timeout_minutes": 30,
"max_parallel": 3
}'
{"id": 56, "name": "Smoke Tests", "project_id": 1, "enabled": true}
Fetch full job details including configuration, node pool, and last build status.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| job_id | integer | Job ID |
curl https://your-testhide-server/api/v3/jobs/55 \ -H "Authorization: Bearer $TOKEN"
{"id": 55, "name": "Regression Suite", "node_pool_id": 2, "last_build_id": 1024, "last_build_status": "passed"}
Update job settings: name, node pool, timeout, enabled state, or max parallel builds.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| job_id | integer | Job ID |
Optional body
| Field | Type | Description |
|---|---|---|
| name | string | New display name |
| enabled | boolean | Enable or pause the job |
| node_pool_id | integer | Reassign to a different pool |
| timeout_minutes | integer | Build timeout in minutes |
curl -X PUT https://your-testhide-server/api/v3/jobs/55 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": false, "timeout_minutes": 90}'
{"id": 55, "enabled": false, "timeout_minutes": 90}
Permanently delete a job and all its associated builds. This action cannot be undone.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| job_id | integer | Job ID to delete |
curl -X DELETE https://your-testhide-server/api/v3/jobs/55 \ -H "Authorization: Bearer $TOKEN"
(no content)
Return the full pipeline configuration — setup commands, test command, environment variables, and artifact paths.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| job_id | integer | Job ID |
curl https://your-testhide-server/api/v3/jobs/55/configuration \ -H "Authorization: Bearer $TOKEN"
{"setup": ["pip install -r requirements.txt"], "command": "pytest tests/ -x", "env": {"ENV": "staging"}, "artifacts": ["reports/*.xml"]}
Replace the full pipeline configuration for a job. The next build will use the updated config.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| job_id | integer | Job ID |
Required body
| Field | Type | Description |
|---|---|---|
| command | string | Main test execution command |
| setup | array[string] | Shell commands run before the main command |
| env | object | Environment variables key/value map |
| artifacts | array[string] | Glob patterns for artifact collection |
curl -X PUT https://your-testhide-server/api/v3/jobs/55/configuration \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"setup": ["pip install -r requirements.txt"],
"command": "pytest tests/ --junitxml=report.xml",
"env": {"ENV": "staging", "DB_HOST": "db.internal"},
"artifacts": ["report.xml", "logs/*.log"]
}'
{"job_id": 55, "updated_at": "2026-05-20T10:15:00Z"}
Export a job as a portable JSON bundle including configuration, pipeline template, and all settings. Pair with /jobs/import to migrate between servers.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| job_id | integer | Job ID to export |
curl -X POST https://your-testhide-server/api/v3/jobs/55/export \ -H "Authorization: Bearer $TOKEN" \ -o job_55_export.json
{"export_version": "1", "job": {"name": "Regression Suite", ...}, "configuration": {...}}
Import a job from a bundle exported via /jobs/{id}/export. Creates a new job in the specified target project.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| project_id | integer | Target project for the imported job |
| bundle | object | Export bundle from /jobs/{id}/export |
curl -X POST https://your-testhide-server/api/v3/jobs/import \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"project_id": 3, "bundle": {...}}'
{"id": 99, "name": "Regression Suite (imported)", "project_id": 3}
Builds
Builds are triggered executions of a Job. Each build runs the pipeline on CI agents and collects test results and artifacts.
Manually trigger a build for a job. Accepts optional branch, commit SHA, and runtime environment variable overrides.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| job_id | integer | Job to trigger |
Optional body
| Field | Type | Description |
|---|---|---|
| branch | string | Git branch (default: job default branch) |
| commit_sha | string | Exact commit SHA to test |
| env | object | Runtime env var overrides for this build only |
| priority | integer | Queue priority 1–10 (higher = sooner) |
curl -X POST https://your-testhide-server/api/v3/builds/start \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"job_id": 55,
"branch": "main",
"commit_sha": "a1b2c3d4",
"env": {"RUN_SLOW": "true"}
}'
{"id": 1025, "job_id": 55, "status": "queued", "branch": "main", "commit_sha": "a1b2c3d4", "created_at": "2026-05-20T10:00:00Z"}
List builds with filtering by job, status, branch, or date range. Returns paginated results sorted by creation time descending.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Optional query params
| Param | Type | Description |
|---|---|---|
| job_id | integer | Filter by job |
| status | string | queued | running | passed | failed |
| branch | string | Filter by branch name |
| page | integer | Page number (default: 1) |
| limit | integer | Results per page (default: 25) |
curl "https://your-testhide-server/api/v3/builds?job_id=55&status=failed&limit=10" \ -H "Authorization: Bearer $TOKEN"
{"total": 3, "results": [{"id": 1024, "status": "failed", "branch": "main", "duration_s": 312, "failed_tests": 7}]}
Fetch full build details: status, timings, node assignment, test counts, and triggered-by information.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID |
curl https://your-testhide-server/api/v3/builds/1024 \ -H "Authorization: Bearer $TOKEN"
{"id": 1024, "job_id": 55, "status": "passed", "duration_s": 287, "total_tests": 412, "passed": 410, "failed": 0, "skipped": 2, "node_id": 7}
Re-queue a finished build using the same commit SHA and configuration. Optionally restart only failed tests.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID to restart |
Optional body
| Field | Type | Description |
|---|---|---|
| failed_only | boolean | Run only tests that failed in previous build |
curl -X POST https://your-testhide-server/api/v3/builds/1024/restart \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"failed_only": true}'
{"id": 1026, "status": "queued", "restarted_from": 1024}
Abort a running or queued build. The build status is set to aborted.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID to stop |
curl -X POST https://your-testhide-server/api/v3/builds/1025/stop \ -H "Authorization: Bearer $TOKEN"
{"id": 1025, "status": "aborted"}
Permanently delete a build and all associated test results, artifacts, and logs.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID to delete |
curl -X DELETE https://your-testhide-server/api/v3/builds/1020 \ -H "Authorization: Bearer $TOKEN"
(no content)
Move a queued build to the top of the job queue so it executes next.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID (must be in queued state) |
curl -X POST https://your-testhide-server/api/v3/builds/1025/push_to_top \ -H "Authorization: Bearer $TOKEN"
{"id": 1025, "queue_position": 1}
Pin a build so it is excluded from automatic cleanup and retention policies.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID to pin |
curl -X POST https://your-testhide-server/api/v3/builds/1024/keep-forever \ -H "Authorization: Bearer $TOKEN"
{"id": 1024, "keep_forever": true}
Return paginated pipeline-level logs (setup, teardown, and orchestration output) for a build. For test-level output use /builds/{id}/stream.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID |
Optional query params
| Param | Type | Description |
|---|---|---|
| offset | integer | Log line offset for pagination |
| limit | integer | Lines to return (default: 500) |
curl "https://your-testhide-server/api/v3/builds/1024/pipeline_logs?offset=0&limit=200" \ -H "Authorization: Bearer $TOKEN"
{"total_lines": 1240, "lines": ["[10:00:01] Setup started", "[10:00:03] pip install OK", "..."]}
Server-Sent Events stream of real-time build output. The connection stays open until the build finishes. Each event is a JSON line with type and data.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Accept | text/event-stream |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID to stream |
curl -N https://your-testhide-server/api/v3/builds/1025/stream \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: text/event-stream"
data: {"type": "log", "data": "[10:00:05] PASSED tests/test_api.py::test_login"}
data: {"type": "status", "data": "passed"}
data: {"type": "done"}
Add a text comment to a build for team annotations or incident notes.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID |
Required body
| Field | Type | Description |
|---|---|---|
| text | string | Comment text |
curl -X POST https://your-testhide-server/api/v3/builds/1024/comment \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Flaky DB connection — infra issue, not code."}'
{"id": 88, "build_id": 1024, "text": "Flaky DB connection...", "author": "alice@corp.com"}
Re-run only the tests that the AI has classified as flaky in this build. Creates a new build targeting those specific test node IDs.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID containing flaky tests |
curl -X POST https://your-testhide-server/api/v3/builds/1024/retry-flaky \ -H "Authorization: Bearer $TOKEN"
{"id": 1027, "status": "queued", "test_count": 4, "restarted_from": 1024}
Notify Testhide of a new commit or pull-request event from an SCM that doesn't support native webhooks. Testhide evaluates matching job triggers and queues builds as appropriate.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| project_id | integer | Project to notify |
| branch | string | Branch that was pushed |
| commit_sha | string | Full commit SHA |
Optional body
| Field | Type | Description |
|---|---|---|
| author | string | Commit author email |
| message | string | Commit message |
curl -X POST https://your-testhide-server/api/v3/builds/git-notify \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"project_id": 1,
"branch": "feature/login-fix",
"commit_sha": "d4e5f6a7",
"author": "bob@corp.com",
"message": "Fix auth token refresh"
}'
{"queued": 2, "build_ids": [1028, 1029]}
Artifacts
Files produced by builds — test reports, screenshots, binaries, logs. Stored per-build and downloadable on demand.
List artifacts for a build, optionally filtered by file type or name pattern.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build to list artifacts for |
Optional query params
| Param | Type | Description |
|---|---|---|
| type | string | report | log | screenshot | binary |
| search | string | Filter by filename substring |
curl "https://your-testhide-server/api/v3/artifacts?build_id=1024" \ -H "Authorization: Bearer $TOKEN"
[{"id": 201, "name": "report.xml", "type": "report", "size_bytes": 48200, "created_at": "2026-05-20T10:05:00Z"}]
Return the artifact directory tree for a build — useful for navigating complex artifact hierarchies.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID |
curl "https://your-testhide-server/api/v3/artifacts/tree?build_id=1024" \ -H "Authorization: Bearer $TOKEN"
{"name": "/", "children": [{"name": "reports", "children": [{"name": "report.xml", "id": 201}]}, {"name": "logs", "children": []}]}
Upload an artifact file for a build. Use multipart/form-data. Called by CI agents automatically; use manually to attach external reports.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | multipart/form-data |
Required form fields
| Field | Type | Description |
|---|---|---|
| build_id | integer | Target build ID |
| file | file | File to upload |
Optional form fields
| Field | Type | Description |
|---|---|---|
| path | string | Virtual path inside the artifact tree (e.g. reports/) |
curl -X POST https://your-testhide-server/api/v3/artifacts/upload \ -H "Authorization: Bearer $TOKEN" \ -F "build_id=1024" \ -F "path=reports/" \ -F "file=@report.xml"
{"id": 202, "name": "report.xml", "size_bytes": 48200, "path": "reports/report.xml"}
Delete a single artifact file. Pinned builds cannot have artifacts deleted.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| artifact_id | integer | Artifact ID to delete |
curl -X DELETE https://your-testhide-server/api/v3/artifacts/201 \ -H "Authorization: Bearer $TOKEN"
(no content)
Download the raw artifact file. Returns the file with Content-Disposition: attachment.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| artifact_id | integer | Artifact ID to download |
curl https://your-testhide-server/api/v3/artifacts/201/download \ -H "Authorization: Bearer $TOKEN" \ -o report.xml
(binary file content with Content-Type matching the artifact)
Reports & Tests
Access test execution results, history, and defect classification for individual test cases.
Return the aggregated test report for a build: pass/fail/skip counts, duration, and suite breakdown.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID |
curl https://your-testhide-server/api/v3/report/1024 \ -H "Authorization: Bearer $TOKEN"
{"build_id": 1024, "total": 412, "passed": 400, "failed": 7, "skipped": 5, "duration_s": 287, "suites": [{"name": "auth", "passed": 45, "failed": 1}]}
Paginated list of individual test results for a build, with filtering by status, suite, or search string.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| build_id | integer | Build ID to query tests for |
Optional query params
| Param | Type | Description |
|---|---|---|
| status | string | passed | failed | skipped |
| suite | string | Filter by test suite name |
| search | string | Filter by test name substring |
| page | integer | Page number (default: 1) |
| limit | integer | Results per page (default: 50) |
curl "https://your-testhide-server/api/v3/tests?build_id=1024&status=failed" \ -H "Authorization: Bearer $TOKEN"
{"total": 7, "results": [{"id": 5001, "node_id": "tests/test_auth.py::test_token_refresh", "status": "failed", "duration_ms": 1230, "message": "AssertionError: Expected 200, got 401"}]}
Return the execution history for a specific test case across recent builds — useful for spotting flakiness trends.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| test_id | integer | Test result ID |
Optional query params
| Param | Type | Description |
|---|---|---|
| limit | integer | Number of past runs to return (default: 20) |
curl "https://your-testhide-server/api/v3/tests/5001/history?limit=10" \ -H "Authorization: Bearer $TOKEN"
{"node_id": "tests/test_auth.py::test_token_refresh", "history": [{"build_id": 1024, "status": "failed"}, {"build_id": 1023, "status": "passed"}, {"build_id": 1022, "status": "failed"}]}
Manually override the AI-assigned classification for a failed test result. Classifications guide quarantine and retry decisions.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| test_id | integer | Test result ID |
Required body
| Field | Type | Description |
|---|---|---|
| classification | string | product_bug | infra | flaky | test_bug | no_defect |
curl -X POST https://your-testhide-server/api/v3/tests/5001/classification \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"classification": "product_bug"}'
{"test_id": 5001, "classification": "product_bug", "set_by": "alice@corp.com"}
Link a failed test to a defect ticket (Jira, GitHub Issues, etc.) for tracking.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| test_id | integer | Test result ID |
Required body
| Field | Type | Description |
|---|---|---|
| defect_key | string | Ticket key, e.g. AUTH-421 |
| defect_url | string | Full URL to the ticket |
curl -X POST https://your-testhide-server/api/v3/tests/5001/defect \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"defect_key": "AUTH-421", "defect_url": "https://jira.corp.com/browse/AUTH-421"}'
{"test_id": 5001, "defect_key": "AUTH-421", "defect_url": "https://jira.corp.com/browse/AUTH-421"}
Set a resolution note for a failed test — explains the root cause or fix applied.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| test_id | integer | Test result ID |
Required body
| Field | Type | Description |
|---|---|---|
| resolution | string | Free-text explanation of the resolution |
curl -X POST https://your-testhide-server/api/v3/tests/5001/resolution \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"resolution": "Fixed in PR #847 — token expiry now handles edge case."}'
{"test_id": 5001, "resolution": "Fixed in PR #847...", "set_by": "alice@corp.com"}
Link a failed test to a tracked known-issue entry so future identical failures are auto-classified without manual review.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| test_id | integer | Test result ID |
Required body
| Field | Type | Description |
|---|---|---|
| known_issue_id | integer | ID of an existing known-issue entry |
curl -X POST https://your-testhide-server/api/v3/tests/5001/known-issue \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"known_issue_id": 17}'
{"test_id": 5001, "known_issue_id": 17, "auto_classify_future": true}
Quarantine
Quarantined tests are excluded from build pass/fail determination while remaining tracked. Manage quarantine lists to stabilize noisy pipelines.
List all quarantined test node IDs for a job.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| job_id | integer | Job ID to query quarantine for |
curl "https://your-testhide-server/api/v3/quarantine?job_id=55" \ -H "Authorization: Bearer $TOKEN"
[{"id": 1, "node_id": "tests/test_auth.py::test_token_refresh", "reason": "flaky", "quarantined_at": "2026-05-10T09:00:00Z"}]
Quarantine a single test node ID. Future builds will still run it but its failure won't affect the build result.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| job_id | integer | Job to apply quarantine to |
| node_id | string | Full test node ID (e.g. tests/test_auth.py::test_login) |
| reason | string | flaky | infra | wip |
curl -X POST https://your-testhide-server/api/v3/quarantine \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"job_id": 55, "node_id": "tests/test_auth.py::test_token_refresh", "reason": "flaky"}'
{"id": 2, "node_id": "tests/test_auth.py::test_token_refresh", "reason": "flaky"}
Quarantine multiple tests in a single request, or remove a list of node IDs from quarantine.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| job_id | integer | Job to apply changes to |
| action | string | add | remove |
| node_ids | array[string] | List of test node IDs |
curl -X POST https://your-testhide-server/api/v3/quarantine/bulk \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"job_id": 55, "action": "add", "node_ids": ["tests/test_a.py::test_x", "tests/test_b.py::test_y"]}'
{"added": 2, "skipped": 0}
Return quarantine statistics for a job — total quarantined, by reason, and age distribution.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| job_id | integer | Job ID |
curl "https://your-testhide-server/api/v3/quarantine/stats?job_id=55" \ -H "Authorization: Bearer $TOKEN"
{"total": 14, "by_reason": {"flaky": 9, "infra": 3, "wip": 2}, "oldest_days": 42}
Return a plain list of quarantined test node IDs for a job — suitable for passing to pytest with --deselect or for CI agent configuration.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| job_id | integer | Job ID |
curl "https://your-testhide-server/api/v3/quarantine/nodeids?job_id=55" \ -H "Authorization: Bearer $TOKEN"
{"node_ids": ["tests/test_auth.py::test_token_refresh", "tests/test_db.py::test_conn_pool"]}
Pipeline Templates
Reusable pipeline configuration blueprints. Apply a template to multiple jobs to share setup commands, environment, and artifact patterns.
List all pipeline templates available to the current user.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl https://your-testhide-server/api/v3/pipeline-templates \ -H "Authorization: Bearer $TOKEN"
[{"id": 3, "name": "Python pytest", "description": "Standard Python test pipeline", "jobs_using": 12}]
Create a new pipeline template.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| name | string | Template name |
| setup | array[string] | Setup shell commands |
| env | object | Default environment variables |
Optional body
| Field | Type | Description |
|---|---|---|
| description | string | Human-readable description |
| artifacts | array[string] | Default artifact glob patterns |
curl -X POST https://your-testhide-server/api/v3/pipeline-templates \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Node.js Jest",
"setup": ["npm ci"],
"env": {"NODE_ENV": "test"},
"artifacts": ["coverage/**", "test-results/*.xml"]
}'
{"id": 4, "name": "Node.js Jest", "jobs_using": 0}
Update an existing pipeline template. All jobs using this template will pick up the changes on their next build.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| template_id | integer | Template ID to update |
curl -X PUT https://your-testhide-server/api/v3/pipeline-templates/4 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"setup": ["npm ci", "npx playwright install --with-deps"]}'
{"id": 4, "name": "Node.js Jest", "updated_at": "2026-05-20T11:00:00Z"}
Delete a pipeline template. Will fail if any jobs still reference it — reassign those jobs first.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| template_id | integer | Template ID to delete |
curl -X DELETE https://your-testhide-server/api/v3/pipeline-templates/4 \ -H "Authorization: Bearer $TOKEN"
(no content)
Nodes / CI Agents
Physical or virtual machines running the Testhide agent binary. Nodes pick up builds from the queue and execute test pipelines.
List all registered nodes with their status, pool assignment, and current load.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Optional query params
| Param | Type | Description |
|---|---|---|
| pool_id | integer | Filter by node pool |
| status | string | online | offline | quarantined |
curl "https://your-testhide-server/api/v3/nodes?status=online" \ -H "Authorization: Bearer $TOKEN"
[{"id": 7, "name": "agent-01", "status": "online", "pool_id": 2, "running_builds": 1, "cpu_pct": 62}]
Register a new CI agent node. Returns an agent token for the node to use when connecting to the server.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| name | string | Unique node name |
| pool_id | integer | Node pool to join |
Optional body
| Field | Type | Description |
|---|---|---|
| tags | array[string] | Labels for routing (e.g. ["linux", "gpu"]) |
| max_parallel | integer | Max concurrent builds on this node (default: 1) |
curl -X POST https://your-testhide-server/api/v3/nodes \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "agent-05", "pool_id": 2, "tags": ["linux", "docker"], "max_parallel": 2}'
{"id": 12, "name": "agent-05", "pool_id": 2, "agent_token": "agt_xK9p...Q2rZ"}
Find a node by name. Useful for scripted registration checks before adding a node.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| name | string | Exact node name to look up |
curl "https://your-testhide-server/api/v3/nodes/find?name=agent-05" \ -H "Authorization: Bearer $TOKEN"
{"id": 12, "name": "agent-05", "status": "online", "pool_id": 2}
Fetch full details for a single node: hardware info, uptime, running builds, and error counts.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| node_id | integer | Node ID |
curl https://your-testhide-server/api/v3/nodes/12 \ -H "Authorization: Bearer $TOKEN"
{"id": 12, "name": "agent-05", "status": "online", "cpu_cores": 8, "ram_gb": 16, "uptime_s": 86400, "running_builds": 1}
Update a node's name, pool assignment, tags, or max_parallel builds.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| node_id | integer | Node ID |
Optional body
| Field | Type | Description |
|---|---|---|
| pool_id | integer | Move to a different node pool |
| tags | array[string] | Replace the full tag list |
| max_parallel | integer | Max concurrent builds |
curl -X PUT https://your-testhide-server/api/v3/nodes/12 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"pool_id": 3, "max_parallel": 4}'
{"id": 12, "pool_id": 3, "max_parallel": 4}
Deregister a CI node. Any running builds will be aborted. The agent binary can then be uninstalled from the host.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| node_id | integer | Node ID to deregister |
curl -X DELETE https://your-testhide-server/api/v3/nodes/12 \ -H "Authorization: Bearer $TOKEN"
(no content)
Quarantine a node — it stops receiving new build assignments but remains registered for inspection.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| node_id | integer | Node ID |
curl -X POST https://your-testhide-server/api/v3/nodes/12/quarantine \ -H "Authorization: Bearer $TOKEN"
{"id": 12, "status": "quarantined"}
Return real-time health metrics for a node: CPU, memory, disk, and agent heartbeat age.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| node_id | integer | Node ID |
curl https://your-testhide-server/api/v3/nodes/12/health \ -H "Authorization: Bearer $TOKEN"
{"node_id": 12, "cpu_pct": 62, "ram_pct": 48, "disk_free_gb": 120, "last_heartbeat_s": 4}
Send a restart command to the Testhide agent process on a node. Useful after configuration changes or to clear stuck builds.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path parameters
| Param | Type | Description |
|---|---|---|
| node_id | integer | Node ID |
curl -X POST https://your-testhide-server/api/v3/nodes/12/restart \ -H "Authorization: Bearer $TOKEN"
{"detail": "Restart command sent to node 12."}
Run an ad-hoc shell command on a node and return stdout/stderr. Useful for diagnostics. Requires admin role.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path parameters
| Param | Type | Description |
|---|---|---|
| node_id | integer | Node ID |
Required body
| Field | Type | Description |
|---|---|---|
| command | string | Shell command to execute |
Optional body
| Field | Type | Description |
|---|---|---|
| timeout_s | integer | Max execution time in seconds (default: 30) |
curl -X POST https://your-testhide-server/api/v3/nodes/12/execute \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "df -h /", "timeout_s": 10}'
{"exit_code": 0, "stdout": "Filesystem Size Used Avail Use%\n/dev/sda1 500G 380G 120G 76%", "stderr": ""}
Return download URLs and checksums for the latest Testhide agent binary for each supported platform.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl https://your-testhide-server/api/v3/nodes/agent/latest \ -H "Authorization: Bearer $TOKEN"
{"version": "3.14.2", "linux_amd64": {"url": "https://your-server/agent/3.14.2/linux-amd64", "sha256": "abc123..."}, "windows_amd64": {"url": "...", "sha256": "..."}}
Node Pools
Group CI agents into named pools for targeted build routing. A build targets a pool by setting its execution target to pool:<name>; the dispatcher then only runs it on nodes that belong to that pool, honouring the pool's concurrency caps and priority boost.
Pools are identified by their name (unique, immutable after creation) and are global — they are not scoped to a project. Reads require NODES_READ; create / update / delete require NODES_WRITE.
List all node pools with their labels and routing rules.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Response fields
| Field | Type | Description |
|---|---|---|
| id | string | Internal identifier |
| name | string | Pool name (used in pool:<name> targets) |
| labels | string[] | Free-form metadata tags |
| maxBuildsPerNode | int | Concurrent builds per node in the pool (-1 = node default) |
| maxConcurrent | int | Total concurrent builds across the pool (-1 = unlimited) |
| priorityBoost | int | Dispatch-score bonus for builds targeting this pool |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/node-pools
{"data": [{"id": "665f1c8e2a", "name": "linux-large",
"labels": ["arch:amd64", "size:large"],
"maxBuildsPerNode": 2, "maxConcurrent": 20, "priorityBoost": 10,
"createdAt": "2026-06-08T10:00:00+00:00",
"updatedAt": "2026-06-08T10:00:00+00:00"}]}
Fetch a single pool by name. Returns 404 if no such pool exists.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| name | string | Pool name |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/node-pools/linux-large
{"data": {"id": "665f1c8e2a", "name": "linux-large",
"labels": ["arch:amd64", "size:large"],
"maxBuildsPerNode": 2, "maxConcurrent": 20, "priorityBoost": 10}}
Create a node pool. Requires NODES_WRITE. Returns 409 if the name already exists, 400 for an invalid name or out-of-range value.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| name | string | Unique pool name. Must start with a letter or digit, then letters / digits / . _ -, max 63 chars. Cannot be a reserved word (none, null, default, all, any). |
Optional body
| Field | Type | Description |
|---|---|---|
| labels | string[] | Metadata tags. Default []. |
| maxBuildsPerNode | int | Per-node concurrency. -1 (node default) or a positive integer. Default 1. |
| maxConcurrent | int | Pool-wide concurrency. -1 (unlimited) or a positive integer. Default -1. |
| priorityBoost | int | Dispatch-score bonus, -1000…1000. Default 0. |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"linux-large","labels":["arch:amd64","size:large"],"maxBuildsPerNode":2,"maxConcurrent":20,"priorityBoost":10}' \
https://your-testhide-server/api/v3/node-pools
{"data": {"id": "665f1c8e2a", "name": "linux-large",
"labels": ["arch:amd64", "size:large"],
"maxBuildsPerNode": 2, "maxConcurrent": 20, "priorityBoost": 10}}
Update a pool's labels or concurrency / priority settings. Requires NODES_WRITE. The pool name is immutable — any name in the body is ignored. Raising a cap automatically re-evaluates builds that were parked while the pool was full.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path params
| Param | Type | Description |
|---|---|---|
| name | string | Pool name |
Optional body
| Field | Type | Description |
|---|---|---|
| labels | string[] | Replacement label set |
| maxBuildsPerNode | int | -1 or positive integer |
| maxConcurrent | int | -1 or positive integer |
| priorityBoost | int | -1000…1000 |
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"maxConcurrent":40,"labels":["arch:amd64","size:xlarge"]}' \
https://your-testhide-server/api/v3/node-pools/linux-large
{"data": {"id": "665f1c8e2a", "name": "linux-large",
"labels": ["arch:amd64", "size:xlarge"],
"maxBuildsPerNode": 2, "maxConcurrent": 40, "priorityBoost": 10}}
Delete a pool. Requires NODES_WRITE. Returns 409 if any node is still assigned to the pool, or if any active/queued build still targets it — unassign the nodes and drain the builds first. Deleting the pool does not delete its nodes.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| name | string | Pool name |
curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/node-pools/linux-large
{"data": {"deleted": true}}
List the nodes currently assigned to a pool. Returns 404 if the pool does not exist.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| name | string | Pool name |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/node-pools/linux-large/nodes
{"data": [{"id": "66a0...", "name": "worker-03",
"status": "available", "host": "10.0.0.13",
"labels": ["linux"], "pools": ["linux-large"], "enabled": true}]}
Docker Agents
Manage ephemeral Docker-based CI agents. Spawn containers on demand, stream logs, and destroy them when done.
List all Docker agent containers across all hosts. Returns status, image, and assigned build.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Optional query params
| Param | Type | Description |
|---|---|---|
| status | string | running | stopped | error |
| project_id | int | Filter by project |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/docker-agents?status=running"
[{"id": "da-7f3c", "host": "build-host-01", "image": "testhide/agent:3.14",
"status": "running", "build_id": 9812, "started_at": "2025-06-10T08:12:00Z"}]
Spawn a new Docker agent container on a specified host. The container registers itself with the server and is immediately available to pick up queued jobs.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| host | string | Docker host identifier or IP |
| image | string | Docker image to use (e.g. testhide/agent:3.14) |
Optional body
| Field | Type | Description |
|---|---|---|
| pool_id | int | Assign to a node pool |
| env | object | Extra environment variables injected into container |
| memory_mb | int | Memory limit in MB |
| cpus | float | CPU quota (e.g. 2.0) |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"host":"build-host-01","image":"testhide/agent:3.14","pool_id":3,"cpus":4.0,"memory_mb":8192}' \
https://your-testhide-server/api/v3/docker-agents/spawn
{"id": "da-9a1e", "host": "build-host-01", "status": "starting",
"image": "testhide/agent:3.14", "pool_id": 3}
Start a stopped Docker agent container without re-spawning it.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | string | Docker agent container ID |
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/docker-agents/da-7f3c/start
{"id": "da-7f3c", "status": "running"}
Gracefully stop a running Docker agent. The container is paused and can be restarted later.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | string | Docker agent container ID |
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/docker-agents/da-7f3c/stop
{"id": "da-7f3c", "status": "stopped"}
Destroy and remove a Docker agent container. The container is stopped and deleted from the host. Any running build is aborted.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | string | Docker agent container ID |
curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/docker-agents/da-7f3c
— No content —
Fetch the last N lines of stdout/stderr from a Docker agent container.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | string | Docker agent container ID |
Optional query params
| Param | Type | Description |
|---|---|---|
| tail | int | Number of lines to return (default 200) |
| stream | bool | If true, returns SSE stream of live log lines |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/docker-agents/da-7f3c/logs?tail=100"
{"lines": ["[08:12:01] Agent started", "[08:12:02] Connected to server",
"[08:12:05] Picked up build 9812"]}
Webhooks
Register HTTP endpoints to receive real-time events when builds start, finish, or change state.
List all registered webhooks with their event subscriptions and delivery stats.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Optional query params
| Param | Type | Description |
|---|---|---|
| project_id | int | Filter by project |
| active | bool | Return only active/inactive hooks |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/webhooks
[{"id": 14, "url": "https://hooks.example.com/testhide",
"events": ["build.finished","build.failed"], "active": true,
"deliveries_ok": 312, "deliveries_failed": 2}]
Register a new webhook. Testhide signs each delivery with HMAC-SHA256 using the secret you provide.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| url | string | HTTPS endpoint to deliver events to |
| events | string[] | Event types: build.started, build.finished, build.failed, build.cancelled, test.flaky, test.quarantined, node.offline |
Optional body
| Field | Type | Description |
|---|---|---|
| secret | string | HMAC secret for signature verification |
| project_id | int | Scope to a single project |
| active | bool | Enable immediately (default true) |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.example.com/testhide","events":["build.finished","build.failed"],"secret":"s3cr3t"}' \
https://your-testhide-server/api/v3/webhooks
{"id": 14, "url": "https://hooks.example.com/testhide",
"events": ["build.finished","build.failed"], "active": true}
Update webhook URL, event subscriptions, secret, or active state.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Webhook ID |
Optional body
| Field | Type | Description |
|---|---|---|
| url | string | New target URL |
| events | string[] | Replacement event list |
| secret | string | New HMAC secret |
| active | bool | Enable or disable the hook |
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"active":false}' \
https://your-testhide-server/api/v3/webhooks/14
{"id": 14, "active": false, "events": ["build.finished","build.failed"]}
Permanently delete a webhook registration.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Webhook ID |
curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/webhooks/14
— No content —
Send a test ping delivery to the webhook URL to verify connectivity and signature validation.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Webhook ID |
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/webhooks/14/test
{"delivered": true, "status_code": 200, "latency_ms": 142}
Retrieve recent delivery history for a webhook, including response codes and error details.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Webhook ID |
Optional query params
| Param | Type | Description |
|---|---|---|
| limit | int | Max deliveries to return (default 50) |
| status | string | ok | failed — filter by delivery outcome |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/webhooks/14/history?limit=20&status=failed"
[{"delivery_id": "d-882f", "event": "build.failed", "status_code": 503,
"error": "connection refused", "attempted_at": "2025-06-10T07:55:12Z"}]
Delete multiple webhooks in a single request.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| ids | int[] | Array of webhook IDs to delete |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids":[14,15,16]}' \
https://your-testhide-server/api/v3/webhooks/bulk-delete
{"deleted": 3, "ids": [14, 15, 16]}
MS Teams
Configure Microsoft Teams notification channels to receive build and test result alerts directly in Teams channels.
List all configured MS Teams notification channels.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/ms-teams
[{"id": 5, "name": "CI Alerts", "project_id": 12,
"webhook_url": "https://outlook.office.com/webhook/...",
"events": ["build.failed","test.flaky"], "active": true}]
Register a new MS Teams incoming webhook for build notifications. Get the webhook URL from the Teams channel connector settings.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| name | string | Display name for this integration |
| webhook_url | string | MS Teams incoming webhook URL |
| project_id | int | Project to monitor |
| events | string[] | Events: build.failed, build.finished, test.flaky, test.quarantined, node.offline |
Optional body
| Field | Type | Description |
|---|---|---|
| mention_on_fail | string | Teams user email to @mention on failures |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"CI Alerts","webhook_url":"https://outlook.office.com/webhook/xxx","project_id":12,"events":["build.failed","test.flaky"]}' \
https://your-testhide-server/api/v3/ms-teams
{"id": 5, "name": "CI Alerts", "active": true,
"events": ["build.failed","test.flaky"]}
Update a Teams notification channel — change events, webhook URL, or toggle active state.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Integration ID |
Optional body
| Field | Type | Description |
|---|---|---|
| events | string[] | Replacement event list |
| webhook_url | string | New webhook URL |
| active | bool | Enable or disable |
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"events":["build.failed","build.finished","test.flaky"],"active":true}' \
https://your-testhide-server/api/v3/ms-teams/5
{"id": 5, "active": true, "events": ["build.failed","build.finished","test.flaky"]}
Remove a Teams notification integration.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Integration ID |
curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/ms-teams/5
— No content —
Send a test message to the Teams channel to verify the webhook is reachable and the integration is working.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Integration ID |
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/ms-teams/5/test
{"sent": true, "status_code": 200, "latency_ms": 198}
SCM Endpoints
Connect source control repositories (GitHub, GitLab, Bitbucket) to link builds to commits, branches, and pull requests.
List all SCM integrations configured on the server.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/scm
[{"id": 2, "type": "github", "host": "github.com",
"name": "Main GitHub", "project_id": 12, "repo": "acme/backend"}]
Create an SCM integration. The token is stored encrypted and used to fetch commit metadata and set build statuses on PRs.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| type | string | github | gitlab | bitbucket |
| name | string | Display name |
| project_id | int | Associated project |
| repo | string | Repo path (owner/name) |
| token | string | Personal access or app token with repo read + status write |
Optional body
| Field | Type | Description |
|---|---|---|
| host | string | Self-hosted SCM base URL (default: public SaaS) |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"github","name":"Main GitHub","project_id":12,"repo":"acme/backend","token":"ghp_xxxxx"}' \
https://your-testhide-server/api/v3/scm
{"id": 2, "type": "github", "name": "Main GitHub",
"repo": "acme/backend", "project_id": 12}
Retrieve a single SCM integration by ID. The stored token is never returned.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | SCM integration ID |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/scm/2
{"id": 2, "type": "github", "host": "github.com",
"name": "Main GitHub", "repo": "acme/backend", "project_id": 12}
Update an SCM integration — rotate the token or change the repo target.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | SCM integration ID |
Optional body
| Field | Type | Description |
|---|---|---|
| token | string | New access token |
| repo | string | New repo path |
| name | string | New display name |
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"token":"ghp_newtoken_yyyy"}' \
https://your-testhide-server/api/v3/scm/2
{"id": 2, "type": "github", "repo": "acme/backend", "updated": true}
Remove an SCM integration. Existing builds retain their linked commit data.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | SCM integration ID |
curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/scm/2
— No content —
List branches in the connected repository. Useful for populating branch selectors in build trigger UIs.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | SCM integration ID |
Optional query params
| Param | Type | Description |
|---|---|---|
| q | string | Filter branches by name prefix |
| limit | int | Max results (default 50) |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/scm/2/branches?q=release"
["release/3.14", "release/3.13", "release/3.12"]
List recent commits for a branch, used to link builds to specific SHAs.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | SCM integration ID |
Optional query params
| Param | Type | Description |
|---|---|---|
| branch | string | Branch name (default: default branch) |
| limit | int | Max commits (default 20) |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/scm/2/commits?branch=main&limit=5"
[{"sha": "a1b2c3d", "message": "Fix race condition in worker pool",
"author": "alice", "date": "2025-06-10T09:30:00Z"}]
vCenter
Integrate with VMware vCenter to provision and manage virtual machine CI agents. Testhide can clone, power-on, and revert VMs automatically for isolated build environments.
List all vCenter integrations configured on the server.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/vcenter
[{"id": 1, "name": "Prod vCenter", "host": "vcenter.corp.local",
"datacenter": "DC-East", "connected": true}]
Add a vCenter server integration. Credentials are stored encrypted and used for all VM lifecycle operations.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| name | string | Display name |
| host | string | vCenter FQDN or IP |
| username | string | vCenter service account username |
| password | string | vCenter service account password |
| datacenter | string | Target datacenter name |
Optional body
| Field | Type | Description |
|---|---|---|
| insecure | bool | Skip TLS certificate validation (not recommended) |
| cluster | string | Default compute cluster for VM placement |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Prod vCenter","host":"vcenter.corp.local","username":"svc-testhide","password":"s3cr3t","datacenter":"DC-East"}' \
https://your-testhide-server/api/v3/vcenter
{"id": 1, "name": "Prod vCenter", "host": "vcenter.corp.local",
"datacenter": "DC-East", "connected": true}
Retrieve a single vCenter integration by ID.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | vCenter integration ID |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/vcenter/1
{"id": 1, "name": "Prod vCenter", "host": "vcenter.corp.local",
"datacenter": "DC-East", "cluster": "Compute-01", "connected": true}
Test connectivity to the vCenter server using the stored credentials. Returns connection status and vCenter API version.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | vCenter integration ID |
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/vcenter/1/test
{"connected": true, "vcenter_version": "8.0.2", "latency_ms": 45}
Trigger a sync to refresh the VM inventory from vCenter. Updates power state and resource pool assignments for all known VMs.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | vCenter integration ID |
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/vcenter/1/sync
{"synced_vms": 24, "updated": 3, "duration_ms": 1840}
Power on or off a VM agent managed by this vCenter integration. Used to bring up spare capacity or suspend idle agents.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | vCenter integration ID |
Required body
| Field | Type | Description |
|---|---|---|
| vm_id | string | vCenter VM managed object ID (e.g. vm-1042) |
| action | string | power_on | power_off | reboot | suspend |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"vm_id":"vm-1042","action":"power_on"}' \
https://your-testhide-server/api/v3/vcenter/1/vm/power
{"vm_id": "vm-1042", "action": "power_on", "status": "powered_on"}
GitOps
Trigger pipeline synchronization from Git repository events and configure GitOps-style deployment webhooks.
Trigger a GitOps sync for a project — pulls the latest pipeline configuration from the configured repository branch and applies it. Use to force a config refresh without waiting for the next push event.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| project_id | int | Project to sync pipeline config for |
Optional body
| Field | Type | Description |
|---|---|---|
| branch | string | Branch to pull config from (default: main) |
| dry_run | bool | Validate config without applying changes |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"project_id":12,"branch":"main","dry_run":false}' \
https://your-testhide-server/api/v3/gitops/sync
{"synced": true, "project_id": 12, "branch": "main",
"sha": "a1b2c3d", "jobs_updated": 4, "jobs_created": 1, "jobs_removed": 0}
Inbound webhook endpoint that receives push events from GitHub/GitLab/Bitbucket and automatically triggers a GitOps sync for the matching project. Point your SCM push webhook at this URL.
Required headers
| Header | Value |
|---|---|
| X-Hub-Signature-256 | sha256=<hmac> (GitHub) — or platform equivalent |
Required body
| Field | Type | Description |
|---|---|---|
| (payload) | object | Standard SCM push event payload (passed through verbatim) |
Optional query params
| Param | Type | Description |
|---|---|---|
| project_id | int | Override project detection from repo URL |
# Simulating a GitHub push event
curl -X POST \
-H "X-Hub-Signature-256: sha256=abc123..." \
-H "Content-Type: application/json" \
-d '{"ref":"refs/heads/main","repository":{"full_name":"acme/backend"}}' \
https://your-testhide-server/api/v3/gitops/webhook
{"accepted": true, "project_id": 12, "branch": "main", "sync_queued": true}
Jira
Link Testhide test failures and builds to Jira issues. Automatically create or update bug tickets when tests fail.
List all Jira integration configurations.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/jira
[{"id": 3, "name": "ACME Jira", "host": "https://acme.atlassian.net",
"project_key": "BACK", "auto_create": true}]
Create a Jira integration. Provide an API token (not your login password) from Atlassian account settings.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| name | string | Display name for this integration |
| host | string | Jira base URL (e.g. https://acme.atlassian.net) |
| string | Atlassian account email for authentication | |
| api_token | string | Atlassian API token |
| project_key | string | Jira project key where issues are created (e.g. BACK) |
Optional body
| Field | Type | Description |
|---|---|---|
| auto_create | bool | Auto-create issues when new tests fail (default false) |
| issue_type | string | Jira issue type (default: Bug) |
| testhide_project_id | int | Scope to a specific Testhide project |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"ACME Jira","host":"https://acme.atlassian.net","email":"ci@acme.com","api_token":"ATATT...","project_key":"BACK","auto_create":true}' \
https://your-testhide-server/api/v3/jira
{"id": 3, "name": "ACME Jira", "project_key": "BACK",
"auto_create": true, "host": "https://acme.atlassian.net"}
Update a Jira integration — rotate token, change project key, or toggle auto-create.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Jira integration ID |
Optional body
| Field | Type | Description |
|---|---|---|
| api_token | string | New API token |
| project_key | string | New project key |
| auto_create | bool | Toggle auto issue creation |
| issue_type | string | Jira issue type to use |
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"auto_create":false,"project_key":"QA"}' \
https://your-testhide-server/api/v3/jira/3
{"id": 3, "project_key": "QA", "auto_create": false}
Remove a Jira integration. Existing issue links on tests are retained.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Jira integration ID |
curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/jira/3
— No content —
AI Chat & Investigation
Conversational AI interface for investigating build failures, diagnosing flaky tests, and getting plain-language explanations of CI results.
Answers in your language. Ask AI replies in the language you pick in the header language switcher — English, Ukrainian, Russian, German, French, Dutch, or Spanish. Change the locale and the next answer follows it; no extra setting required.
Clear message when the AI is unavailable. If the cloud AI provider can't be reached — for example it's out of balance or has a billing problem — the chat tells you the real reason inline (e.g. "out of balance — top up or switch providers in Admin → LLM Routing") instead of a generic "couldn't complete" error. The data and sources the AI already gathered still show, so nothing you collected is lost.
Send a message to the AI assistant. Optionally attach build or test context so the AI can answer questions about specific failures.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| message | string | User message / question |
Optional body
| Field | Type | Description |
|---|---|---|
| build_id | int | Build to investigate |
| test_id | int | Specific test case to investigate |
| session_id | string | Continue a previous conversation thread |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Why did this build fail?","build_id":9812}' \
https://your-testhide-server/api/v3/ai/chat
{"session_id": "sess-a8f3", "reply": "Build 9812 failed in stage 'unit-tests'. The test...",
"sources": ["build:9812", "test:47821"]}
Same as POST /ai/chat but returns the AI reply as a Server-Sent Events stream for progressive rendering in the UI.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
| Accept | text/event-stream |
Required body
| Field | Type | Description |
|---|---|---|
| message | string | User message |
Optional body
| Field | Type | Description |
|---|---|---|
| build_id | int | Build context |
| session_id | string | Continue a thread |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"message":"Summarize this build","build_id":9812}' \
--no-buffer \
https://your-testhide-server/api/v3/ai/chat/stream
data: {"token": "Build"}
data: {"token": " 9812"}
data: {"token": " failed"}
data: [DONE]
Check which LLM provider and model are configured on the server, and whether AI features are available.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/ai/llm/status
{"provider": "openai", "model": "gpt-4o", "available": true,
"features": ["chat", "investigation", "test_health"]}
Get the current AI token usage and quota limits for the authenticated user or organization.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/ai/quota
{"tokens_used": 142800, "tokens_limit": 1000000,
"resets_at": "2025-07-01T00:00:00Z", "percent_used": 14.3}
Run AI-powered root-cause analysis on a failed build. Returns a structured diagnosis with likely causes and suggested fixes.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| build_id | int | ID of the failed build to analyze |
Optional body
| Field | Type | Description |
|---|---|---|
| include_logs | bool | Include full build logs in the analysis (default true) |
| max_tests | int | Max number of failed tests to analyze (default 20) |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"build_id":9812,"include_logs":true}' \
https://your-testhide-server/api/v3/ai/diagnose
{"build_id": 9812, "root_cause": "OOM in test worker",
"confidence": 0.87, "suggested_fix": "Increase JVM heap size or reduce parallelism",
"affected_tests": 14}
Retrieve AI-generated insights for a project — recurring failure patterns, top flaky tests, and build health trends.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| project_id | int | Project to generate insights for |
Optional query params
| Param | Type | Description |
|---|---|---|
| days | int | Look-back window in days (default 7) |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/ai/insights?project_id=12&days=14"
{"project_id": 12, "period_days": 14, "build_pass_rate": 0.82,
"top_flaky_tests": [{"test": "UserLoginTest", "flake_rate": 0.34}],
"failure_patterns": ["Database connection timeout on CI network"]}
Get a plain-language AI summary of a build — what passed, what failed, and what likely caused the failures.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| build_id | int | Build ID |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/ai/summary/9812
{"build_id": 9812, "summary": "Build 9812 ran 1,240 tests, 14 failed. Most failures cluster around database timeout errors in the payment module. 3 tests appear flaky based on history."}
Start a long-running AI analysis job (e.g. full project audit). Returns a job ID; poll /ai/job/{job_id}/result for the result.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| type | string | Job type: project_audit | failure_cluster | regression_scan |
| project_id | int | Target project |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"project_audit","project_id":12}' \
https://your-testhide-server/api/v3/ai/job/stream
{"job_id": "aijob-f2c8", "status": "queued", "estimated_seconds": 45}
Poll the result of a long-running AI analysis job. Returns status while running, full result when complete.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| job_id | string | AI job ID from /ai/job/stream |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/ai/job/aijob-f2c8/result
{"job_id": "aijob-f2c8", "status": "complete",
"result": {"audit_score": 74, "recommendations": ["Quarantine 8 chronic flakes", "..."]}}
Cancel a running AI analysis job.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| job_id | string | AI job ID |
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/ai/job/aijob-f2c8/cancel
{"job_id": "aijob-f2c8", "status": "cancelled"}
Semantic search over build logs, test failure messages, and AI summaries using natural language queries.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| q | string | Natural language search query |
| project_id | int | Scope search to a project |
Optional query params
| Param | Type | Description |
|---|---|---|
| limit | int | Max results (default 10) |
| type | string | Filter: build | test | log |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/ai/search?q=database+timeout&project_id=12"
{"results": [{"type": "build", "id": 9812, "score": 0.94,
"snippet": "...connection to postgres timed out after 30s..."}]}
AI Test Health
AI-powered test health analysis — detect emerging flaky tests before they become chronic, and get per-test health scores.
Get aggregated AI test health scores for a project. Returns a ranked list of tests by health risk, with flake rates and trend direction.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| project_id | int | Project to analyze |
Optional query params
| Param | Type | Description |
|---|---|---|
| days | int | Look-back window (default 14) |
| limit | int | Max tests to return (default 50) |
| min_runs | int | Minimum run count to include a test (default 5) |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/ai/tests/health?project_id=12&days=14"
{"tests": [{"test_id": 47821, "name": "UserLoginTest",
"health_score": 32, "flake_rate": 0.34, "trend": "worsening",
"risk": "high", "recommended_action": "quarantine"}]}
Get detailed AI health analysis for a single test — includes failure pattern breakdown, correlated builds, and actionable recommendation.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | int | Test case ID |
Optional query params
| Param | Type | Description |
|---|---|---|
| days | int | Look-back window (default 30) |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/ai/tests/47821/health?days=30"
{"test_id": 47821, "health_score": 32, "flake_rate": 0.34,
"failure_patterns": ["Race condition on login session", "Timeout on slow CI"],
"correlated_builds": [9800, 9790, 9781],
"recommendation": "Add explicit wait or quarantine until fixed"}
Get the latest list of emerging flaky tests — tests that have started failing more than their historical baseline over the last N builds. Useful for catching new flakiness before it spreads.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| project_id | int | Project to check |
Optional query params
| Param | Type | Description |
|---|---|---|
| threshold | float | Minimum flake-rate increase to flag (default 0.1) |
| window | int | Recent build count to compare against (default 10) |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/ai/emerging/latest?project_id=12"
{"emerging": [{"test_id": 52004, "name": "PaymentFlowTest",
"baseline_rate": 0.02, "recent_rate": 0.18, "delta": 0.16,
"first_seen_build": 9808}]}
Retrieve the historical log of emerging-flaky detections for a project. Useful for auditing when tests first became problematic.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| project_id | int | Project to query |
Optional query params
| Param | Type | Description |
|---|---|---|
| days | int | History window in days (default 30) |
| test_id | int | Filter to a specific test |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/ai/emerging/history?project_id=12&days=30"
[{"detected_at": "2025-06-05T10:00:00Z", "test_id": 52004,
"name": "PaymentFlowTest", "delta": 0.16, "resolved": false},
{"detected_at": "2025-05-28T08:00:00Z", "test_id": 48120,
"name": "CacheWarmupTest", "delta": 0.22, "resolved": true}]
LLM Evaluations
Run and retrieve evaluations of the LLM models powering AI features. Use to benchmark quality, track regressions after model updates, and compare prompt strategies.
List all evaluation runs with their status, scores, and model configuration.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Optional query params
| Param | Type | Description |
|---|---|---|
| status | string | pending | running | complete | failed |
| limit | int | Max results (default 20) |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/evals
[{"id": "eval-c3d9", "model": "gpt-4o", "status": "complete",
"score": 0.89, "cases_total": 100, "cases_passed": 89,
"created_at": "2025-06-10T07:00:00Z"}]
Get full details of a single evaluation run, including per-case results and failure examples.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Path params
| Param | Type | Description |
|---|---|---|
| id | string | Evaluation run ID |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/evals/eval-c3d9
{"id": "eval-c3d9", "model": "gpt-4o", "score": 0.89,
"cases_total": 100, "cases_passed": 89, "cases_failed": 11,
"failures": [{"case": "diagnose_oom", "expected": "OOM", "got": "timeout"}],
"duration_s": 142}
Start a new evaluation run against the configured LLM. The run is queued and executes asynchronously. Poll /evals/{id} for results.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
| Content-Type | application/json |
Required body
| Field | Type | Description |
|---|---|---|
| suite | string | Eval suite to run: diagnosis | chat | health | full |
Optional body
| Field | Type | Description |
|---|---|---|
| model_override | string | Test a specific model instead of the configured one |
| sample_size | int | Number of eval cases to run (default: full suite) |
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"suite":"diagnosis","model_override":"gpt-4o-mini","sample_size":50}' \
https://your-testhide-server/api/v3/evals/runs
{"id": "eval-d7a1", "status": "pending", "suite": "diagnosis",
"model": "gpt-4o-mini", "estimated_s": 90}
Compare the latest eval run to the previous run and return any quality regressions — eval cases that previously passed but now fail.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Optional query params
| Param | Type | Description |
|---|---|---|
| baseline_id | string | Eval ID to use as baseline (default: last complete run) |
| current_id | string | Eval ID to compare against baseline |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/evals/regressions?baseline_id=eval-c3d9¤t_id=eval-d7a1"
{"baseline": "eval-c3d9", "current": "eval-d7a1",
"score_delta": -0.06, "regressions": [
{"case": "diagnose_oom", "baseline": "pass", "current": "fail"}
], "improvements": []}
Search
Search test results & failures across builds (failure messages, tracebacks, test names, file paths). Requires REPORTS_READ; results are project-scoped to the caller's projects.
Searches AI test-insight records (failure message fm, traceback, file path, and test name) plus compressed logs, newest-first. Use the prefix form testid@@<id> or testid#<id> for an exact test-id lookup.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
Required query params
| Param | Type | Description |
|---|---|---|
| q | string | Search term (min 2 chars). Empty/short queries return an empty result. Unicode-aware (case-folded). |
Optional query params
| Param | Type | Description |
|---|---|---|
| status | string | failed (default), passed, or all |
| page | int | 1-based page number (default 1) |
| pageSize | int | Results per page (default 10, max 100) |
| includeBranch | string | Comma-separated branch names to include |
| excludeBranch | string | Comma-separated branch names to exclude |
curl -H "Authorization: Bearer $TOKEN" \ "https://your-testhide-server/api/v3/search?q=payment+timeout&status=failed&pageSize=20"
{"ok": true, "total": 37, "data": [
{"_id": "665...", "test": "PaymentFlowTest", "status": "Failed",
"message": "timed out waiting for payment gateway response",
"branch": "main", "build": 9812, "report_id": "664...", "date": "2026-06-06 10:23:45"}
], "meta": {"request_id": "...", "ts": "...", "scanned": 37, "capped": false, "duration_ms": 48}}
License
Retrieve the current license status, tier, and feature entitlements for this Testhide installation.
Returns the active license tier, expiry date, seat count, and which feature capabilities are enabled. Use to check entitlements before calling tier-gated endpoints.
Required headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl -H "Authorization: Bearer $TOKEN" \ https://your-testhide-server/api/v3/license/status
{"tier": "enterprise", "valid": true,
"expires_at": "2026-12-31T23:59:59Z",
"seats_licensed": 100, "seats_used": 42,
"capabilities": {
"ai_chat": true,
"ai_test_health": true,
"llm_evals": true,
"white_label": true,
"vcenter": true,
"sso": true,
"max_users": 100
}}