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.

In a hurry? The five-step quickstart below gets a working instance up. For the full production deployment reference — sizing, reverse proxies, air-gapped mode, LDAP, and every environment variable — see the dedicated installation guide.

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

ResourceMinimumRecommended
CPU8 cores16 cores
RAM32 GB64 GB
Disk100 GB SSD500 GB SSD
PlatformLinux (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>
The backend will not start with 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:

  1. Run it from TestHide. Create a job, add a test_provider build step pointing at your test suite, and run it on an agent. Results, artifacts, and logs are captured automatically. recommended
  2. 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_provider step 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.

Next steps. Generate a long-lived token under Settings → API Tokens to drive TestHide from scripts or CI, then explore the REST API reference for builds, nodes, reports, and the AI endpoints.

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.pyBuild); the client tolerates British cancelled but emits canceled.

StatusBucketMeaning
pendingqueue-eligibleQueued by the dispatcher, waiting for a free node slot.
discoveringactiveAgent is resolving the test set / pipeline before execution.
in_progressactiveExecuting on an agent.
running_provideractiveA test-provider (TPS) step is actively running.
stoppingactiveCancel-in-flight; the agent is winding the run down.
interruptedactiveAgent lost mid-run. Recoverable — may resume.
passedterminalFinished with no failures.
failedterminalOne or more tests failed.
canceledterminalStopped gracefully by a user.
terminatedterminalHard SIGKILL-level termination, distinct from graceful cancel.
errorterminalAgent / infrastructure failure during the run or finalize (not a test failure).
abortedterminalInvalidated 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.

TabRouteWhat it shows
Overview/overviewBuild summary, duration, test counts, live progress, and the matrix child rollup for parent builds.
Output/outputRaw agent console-log stream.
Parameters/parametersBuild parameters and per-run overrides.
Env Vars/envsEnvironment variables captured at build time.
Artifacts/artifactsUploaded files, screenshots and reports, browsed as a tree.
AI Evals/ai-evalsResults of every llm_eval step (see Build Steps).
AI Predictions/ai-predictionsBuild-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/dependenciesThe depends_on graph of builds that must pass first.
Investigate/investigateThe AI build investigator. Requires AI_ASSIST_READ.
JSON/jsonRaw 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.

ActionEndpointPermissionNotes
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
FieldDefaultConstraint
input_dataset / casesOne is required. input_dataset must be a relative path inside the workspace and ≤ 50 MB.
judge.kindllm_as_judgeExecutor accepts llm_as_judge or snapshot only.
judge.modelgemini-2.0-flashRequired for llm_as_judge (and rubric).
judge.promptbuilt-inOptional registry://<name>/<version> reference.
judge.modeexactsnapshot only: exact | fuzzy | semantic.
threshold.min0.80Float in [0.0, 1.0].
parallelism4Integer ≥ 1 (server max 32).
cost_budget_usd1.0> 0 (server max 50.0).
on_failpr_check_failpr_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 llmllm_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.

  1. The agent's build executor reaches an llm_eval step and sends an LLM_EVAL_STEP_REQUEST WebSocket message (build_id, step_index, eval_yaml) to the backend.
  2. 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 — LLMJudge for llm_as_judge (resolving any registry:// prompt) or SnapshotJudge for snapshot.
  3. The work runs as a detached task, so PING / BUILD_FINISHED keep flowing. EvalRunner.run_for_build scores each case, streaming an LLM_EVAL_PROGRESS frame back to the agent per case.
  4. The result is persisted into the build document at cfg.build_step[step_index].eval (and the run row in db_eval_runs) so the AI Evals tab can render it.
  5. The backend returns an LLM_EVAL_STEP_RESULT message (exit code, pass/fail counts, pass rate, cost, tokens, per-case judge outputs).
  6. The agent applies on_fail: pr_check_fail marks the step (and build) failed when below threshold, while warn / continue keep 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) and step_index.
  • Pass / fail outcome with pass_rate and the passed/failed/total_cases counts.
  • Estimated cost (cost_usd) and tokens_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:

  1. Add the reporter to your test runner — a CLI flag, a config-file entry, a logger, or an attribute, depending on the framework.
  2. Run your tests. The plugin writes junittests.xml incrementally: each test result is flushed immediately, so a partial report survives even an interrupted run.
  3. The Testhide agent collects the report. A test_provider build 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

FrameworkPackageRunner(s)RuntimeInstall
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

OptionMeaning
--report-xml PATHEnable reporting and set the output file.
--suite-name NAME<testsuite name> (default pytest).
--meta KEY=VALUEAdd a suite property (repeatable). build / branch are reserved run identifiers.
--quarantine-file PATHDeselect 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-passwordOptional Jira enrichment — links failures to known issues by fail_id.
  • Parallelism — fully compatible with pytest-xdist; add -n auto and 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_properties and pytest_testhide_add_metadata hooks.
# 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

OptionMeaning
--report-xml PATHOutput report (default junittests.xml).
--suite-name NAME<testsuite name> (default unittest).
--meta KEY=VALUEAdd a suite property (repeatable) — e.g. build / branch.
--quarantine-file PATHSkip listed test ids (also TESTHIDE_QUARANTINE_FILE / .testhide_quarantine_file).
--no-captureDo not capture stdout/stderr into <system-out>.
--jira-url / --jira-username / --jira-passwordOptional 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:

ParameterMeaning
LogFilePathOutput report path (default junittests.xml; relative paths resolve under TestResults/).
SuiteName<testsuite name> (default dotnet).
meta.KEY=VALUEAdd a suite property — e.g. meta.build=1042;meta.branch=main.
JiraUrl / JiraUsername / JiraPasswordOptional 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 mappingfile / line are captured when the framework provides them (e.g. with portable PDBs / SourceLink).
  • Per-test metadata — test traits named docstr / jira / info map to report properties, and result attachments become attachment properties.

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

StatusMeaning
availableConnected and idle; eligible for dispatch.
busyCurrently running one or more builds.
quarantinedExcluded from dispatch — either auto-quarantined by the health system or manually quarantined by an operator.
unavailableNo 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 / stateValueEffect
QUARANTINE_THRESHOLD15Penalty at which the node is auto-quarantined.
MAX_PENALTY20Hard cap so healing time stays bounded.
state = healthypenalty 0Nominal.
state = warning0 < penalty < 15Accruing fault, still dispatchable.
state = unhealthypenalty ≥ 15Auto-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).
Token auth is always on. It used to sit behind a staged 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_KEY environment variable,
  • a testhide_override.json file, or
  • the agent's config command.

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).

FieldDefaultMeaning
labels[]Metadata tags for UI / policy use.
max_builds_per_node1Max concurrent builds per node in this pool (-1 = node default).
priority_boost0Score bonus added to builds targeting the pool (higher = dispatched earlier).
max_concurrent-1Total 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.

  • Typevirtualenv (created from a base interpreter) or pyenv (install/manage interpreter versions).
  • Locationglobal or workspace.
  • 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"
}
Validated at the boundary. Env names and interpreter versions are validated at the API boundary ([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_enabled on the node and REMOTE_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 busy nodes 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_id first 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. All REMOTE_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 limitConfig keyDefault
Memorydocker_agent_memory_limit2g
CPU cores (--cpus)docker_agent_cpu_limit2.0
Max processes (--pids-limit)docker_agent_pids_limit512
Max running containersdocker_agent_max_containers5
No new privilegesdocker_agent_no_new_privilegestrue
Idle auto-cleanup (min)docker_agent_idle_timeout_min30

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). Requires NODES_READ.
  • RestorePOST /api/v3/nodes/deleted/{id}/restore moves an archived node back to db_nodes; it returns as unavailable until it reconnects. (NODES_WRITE)
  • Purge (single)POST /api/v3/nodes/deleted/{id}/purge permanently hard-deletes one archived record. Cannot be undone. (NODES_DELETE)
  • Purge allPOST /api/v3/nodes/deleted/purge permanently empties the entire archive in one shot. (NODES_DELETE)
Restore is the only way back from a soft delete. Purge and Purge all are irreversible — the archived document is removed from the database entirely.

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.

Previous-build dependencies (one TestHide job waiting on another TestHide build) are handled by the existing internal trigger chain. The external-CI gate described here is specifically for external Jenkins/TeamCity builds.

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.

  • JenkinsBase URL + Username + API token (create the token under the Jenkins user's Configure → API Token). Outbound triggers send a crumb-protected buildWithParameters request.
  • TeamCityBase 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.

An endpoint's 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:

PlaceholderResolves 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_KEY in 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 the X-Testhide-CI-Token header (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.
Without 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_minutes ceiling).
  • 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
For Jenkins, the official Notification plugin (or a 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:

FieldMeaning
queue_sizeItems currently waiting in the worker input queue. A pulsing dot shows whenever this is > 0.
processed_totalCumulative items processed since the worker started.
success_totalCumulative successful items.
failed_totalCumulative failed items.
skipped_totalCumulative skipped items.
last_batchSummary of the most recent batch the worker drained.
worker_id / hostname / updated_atWorker 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/:component to mark the component pending, then polls /api/v3/ai/admin/diagnostics on a 10 s interval until the report leaves the running/pending state.
  • 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_heavy with the chosen config.
  • 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

PurposeEndpointPermission
Worker stats banner (60 s poll)GET /api/v3/ai/admin/statsCONFIGURATION_READ
Worker log tail (2 s poll)GET /api/v3/logAI_ASSIST_READ
Diagnostics list (model cards)GET /api/v3/ai/admin/diagnosticsCONFIGURATION_WRITE
Run a single component checkPOST /api/v3/ai/admin/diagnostics/:componentCONFIGURATION_WRITE
Loaded runtime model metricsGET /api/v3/ai/admin/modelsCONFIGURATION_READ
Queue training / maintenance taskPOST /api/v3/ai/admin/diagnostics/runCONFIGURATION_WRITE
Training-lock status (30 s poll)GET /api/v3/ai/admin/diagnostics/status/training_lockCONFIGURATION_READ
Terminate / pause / resume trainingPOST /api/v3/ai/admin/diagnostics/signalCONFIGURATION_WRITE
Model-sync status / triggerGET /api/v3/ai/model-sync/status · POST /api/v3/ai/model-sync/runAI_ASSIST_*
Pending rule-suggestion badgeGET /api/v3/ai/rule-suggestions/countAI_ASSIST_READ
Flush AI Redis caches (after config save)POST /api/v3/ai/admin/cache/invalidateCONFIGURATION_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.

TabSlugWhat it shows
Overviewoverview 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.
Trainingtraining The last 10 training runs (timestamp, duration, result badge, error).
Benchmarksbenchmarks 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>).
Configurationconfiguration The latest worker-stats key/value snapshot for the model.
Predictionspredictions 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.
Actionsactions 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/versions and 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), and ml (ML active) via GET/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/inference and polls GET /api/v3/ai/inference/:request_id for 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. flakinessflakiness_predictor, build_rcaroot_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:

EndpointReturns
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.)

FieldDB keyMeaning
test_nametnTest identifier.
statusstTest result: failed / passed / error / skipped.
flakyflFlakiness label: Rare / Random / Systematic / Stable.
flakiness_scorefsFlakiness Predictor score (rendered as a percentage).
predictionapRoot-cause classifier output: label + score.
source_kindskWhich model / source produced the insight (e.g. root_cause, flakiness, ood).
decision_categorydcHigh-level decision bucket.
coarse_failure_typectBroad 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 ai summary 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_eval build 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:

JudgeClassWhat it scores
llmLLMJudgeLLM-as-judge: prompts a judge model with (input, expected, actual) and parses a structured JSON verdict.
snapshotSnapshotJudgeMatch against an approved baseline output. Mode is exact, fuzzy, or semantic (MiniLM embedding similarity).
classifier_benchmarkClassifierBenchmarkJudgeAccuracy / F1 metrics for a classifier model.
retrieval_recallRetrievalRecallJudgeRecall@k for the failure retriever.
detection_mapDetectionMapJudgeMean-average-precision for the YOLO visual detector.
ood_thresholdOODThresholdJudgeOut-of-distribution threshold scoring.
distribution_driftDistributionDriftJudgeInput-distribution drift check.
feature_coverageFeatureCoverageJudgeFeature-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):

TabEndpointWhat it shows
OverviewGET /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.
RunsGET /api/v3/evals/{eval_id}/runsPaginated 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.
RegressionsGET /api/v3/evals/{eval_id}/regressionsFailed cases on the most-recent run that are not already flagged as known issues. Each can be triaged.
Prompt historyThe registry prompt versions used by this eval.
CostsToken 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 of pass, 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):

ActionEndpoint
Approve a snapshot output as the new baselinePOST /api/v3/evals/{eval_id}/snapshots/{case_id}/approve
Approve a failing case as the accepted baselinePOST /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 flagDELETE /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.01.2.1) and a new PromptVersion is 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

CollectionOne document perKey fields
db_promptslogical prompt namename, description, owner, output_schema, created_at, updated_at
db_prompt_versions(prompt, semver) pairprompt_id, version, content, content_sha256, model, temperature, variables, deprecated, created_by
db_prompt_usesresolved + executed callprompt_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):

EndpointReturns
GET /api/v3/promptsList 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 with caller, 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 own cost_usd and tokens_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

VariableDefaultMeaning
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:

  1. Model overrideLLM_PRICING_OVERRIDES, longest matching prefix wins.
  2. 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.
  3. Local model — if LLM_LOCAL_MODEL_FREE is 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).
  4. Gemini table — built-in Google / Gemini per-model prices.
  5. Anthropic table — built-in Anthropic per-model prices (longer prefixes matched first).
  6. OpenAI table — built-in OpenAI per-model prices.
  7. Groq / Mistral / DeepInfra tables — built-in per-model prices for the open-weight router hosts.
  8. 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).
  9. 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 prefixInput / 1KOutput / 1K
gemini-2.5-flash0.00030.0025
gemini-2.5-pro0.001250.010
gemini-2.0-flash0.00010.0004
gemini-2.0-pro0.001250.005
gemini-1.5-flash0.0000750.0003
gemini-1.5-pro0.001250.005
gemini-1.0-pro0.00050.0015

Anthropic

Model prefixInput / 1KOutput / 1K
claude-opus-40.0150.075
claude-sonnet-40.0030.015
claude-haiku-40.0010.005
claude-3-7-sonnet0.0030.015
claude-3-5-sonnet0.0030.015
claude-3-5-haiku0.00080.004
claude-3-opus0.0150.075
claude-3-sonnet0.0030.015
claude-3-haiku0.000250.00125

OpenAI

Model prefixInput / 1KOutput / 1K
gpt-4o-mini0.000150.0006
gpt-4o0.00250.010
gpt-4.1-mini0.00040.0016
gpt-4.10.0020.008
gpt-4-turbo0.010.03
gpt-40.030.06
gpt-3.5-turbo0.00050.0015
o1-mini0.00110.0044
o10.0150.06
o3-mini0.00110.0044

Per-provider defaults (unlisted models)

ProviderInput / 1KOutput / 1K
openai0.0050.015
anthropic0.0030.015
google0.001250.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.

ResourceRegular userSuperuser / service account
REPORTSYesYes
JOBSYesYes
PROJECTSYesYes
BUILDSYesYes
TOOLSYesYes
MONITORINGYesYes
NODESYesYes
MESSAGESYesYes
AI_ASSISTYesYes
PROMPTSYesYes
CONFIGURATIONNoYes
USERS / GLOBAL_USERSNoYes
VCENTERNoYes
REMOTE_CONTROLNoYes
SECURITYNoYes
SYSTEMNoYes
AI_FIXNo (default off)Yes
EVALSREAD + EXECUTE onlyAll 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_CONTROL resource is not in USER_PERM.
  • NODES_DELETE — gates node delete and purge. Regular users carry it (NODES is 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. READ shows the auto-fix badge and the "Suggested fix routing" card on Investigate; WRITE enables the Draft proposal / Create PR actions. AI_FIX is in SUPERUSER_PERM only, 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_approver role.
  • SECURITY_ADMIN — manage banned IPs (the banned-IPs admin endpoints). Held by superusers, service accounts, and the named security_admin role. 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.

FieldRequiredNotes
nameNoHuman-readable label, e.g. ci-runner. Defaults to token-YYYYMMDD if omitted.
expires_daysNoOmit or use a non-positive value for a token that never expires. Otherwise the token expires that many days from creation.
scopesNoEmpty list (or *) = Full Access. Otherwise an array of scope strings (see Scopes). Invalid names are rejected.
project_scopesNoArray 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:

ScopeGrants access to
*Full Access (same as an empty scope list)
builds:readList / view builds, artifacts, evals, insights, node builds
builds:writeImport builds, trigger re-runs, write evals (implies builds:read)
jobs:readList / view jobs and pipeline configuration
jobs:writeCreate / update / delete jobs; git/notifyCommit (implies jobs:read)
reports:readReports, test history, tests, insights
reports:writeCreate / update reports and test ops (implies reports:read)
ai:readAI insights, model detail, worker stats
ai:writeTrigger re-inference, update AI config (implies ai:read)
config:readConfiguration, prompts, webhooks, MS Teams, vCenter, monitoring, watchers, pipeline templates
config:writeChange the above settings (implies config:read)
projects:readList / view projects
projects:writeCreate / update / delete projects (implies projects:read)
admin:readUsers, nodes, security, license, cron, tier, global users, AI admin, agent
admin:writeManage 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_scopes to confine the blast radius.
  • Never embed tokens in source code — use environment variables or a secrets vault.

Error responses

HTTP statusCause
400 Bad RequestInvalid scope name, or malformed / unknown / inaccessible project_scopes entry (VALIDATION_FAILED)
401 UnauthorizedToken not found, revoked, expired, or owner deactivated
403 ForbiddenToken scopes do not cover the requested endpoint (SCOPE_DENIED)
404 Not FoundRevoking a token that does not exist or is not yours
422 UnprocessableToken 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.

Paying customers are never locked out. Enterprise / unlimited tiers report a limit of < 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.pyTier / 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)1525unlimited
Agent nodes (max_nodes)51050unlimited
User seats (max_users)1315unlimited
AI requests / month (ai_requests_monthly)unlimited
(self-hosted)
10,000100,000unlimited
AI cloud budget, USD / mo (ai_monthly_budget_usd)unlimitedunlimitedunlimitedunlimited
Data retention (retention_days)7 days30 days365 daysunlimited
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.

Two distinctions worth noting. The full diagnostic AI suite — including the AI dashboard, FAISS retriever, OOD, log signatures, emerging issues, Bug Linker, and visual diff — turns on at Cloud Starter, not Cloud Team. The one AI capability reserved for Cloud Team and above is the AI Narrator / Investigator agent (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 syncLicenseManager.check_and_sync() calls the license server, then writes { valid, tier, capabilities, limits } into configuration.license_data. A limit of 0 from the server (meaning “unlimited”) is normalised to the -1 sentinel.
  • 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).
  • ReadsTierEnforcementService reads limits from license_data["limits"] and feature flags from license_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 buildscan_start_build() counts builds in Build.ACTIVE_STATUSES and blocks when the count reaches max_concurrent_builds; over-cap builds stay pending until a slot frees.
  • User seats — enforced in auth/views.py::UserAdd and services/user.py::UserService.create_user against max_users (service accounts and soft-deleted users are excluded from the count).
  • Connected nodesmax_nodes is propagated to the node fleet; nodes beyond the cap are flagged license_validity=false.
  • AI evals / month — the live gate is LicenseManager.check_ai_evals_limit() (called from ai_assist/api/general.py::_check_license), counting authoritative configuration.global_options['ai_requests_this_month']. The parallel TierEnforcementService.can_use_ai_eval() exists only for parity/tests and must not be wired in as a second gate.
  • AI feature flagscan_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.
Per-project quotas are a separate layer. Independently of tier limits, a per-project ceiling (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.pytier_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 5xx from 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 as valid=false (expired / inactive), causes an immediate downgrade to Free regardless of grace.
See also. The live, DB-backed comparison of every plan is on the pricing page (rendered straight from the same 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.

Never commit secrets. 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))".
Tier limits do not come from env vars. Per-tier limits (concurrent builds, seats, nodes, AI quotas, retention) and feature flags are delivered by the signed license your instance syncs from the license server and cached in 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

VariableDefaultPurpose
ENVIRONMENT requireddevDeployment 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.
DEBUGfalseMaster debug flag. When on, enables local-dev behaviour. Keep false in production.
PORT8080Internal aiohttp/Gunicorn bind port (also used by the nginx proxy upstream). Compose sets 8080.
USE_SSLfalseTerminate 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_URLvalue of API_URL else https://localhost:8080External URL of the backend API; injected into build environments as TESTHIDE_URL.
API_URLhttps://localhost:8080Public 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_PROXY0Trust 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_MB4096Max 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_VERSION1.0Bitbucket REST API version the SCM provider speaks.
INTERNAL_API_URL / INTERNAL_WS_URLhttp://backend:8080 / ws://backend:8080Internal Docker service URLs used by the nginx proxy. Do not change for standard deployments.

Backend — MongoDB

VariableDefaultPurpose
MONGO_USER requiredtesthide_userMongoDB username.
MONGO_PASS required(empty)MongoDB password. URL-encoded into the connection string. Set a strong value.
MONGO_HOSTlocalhostMongoDB host (Compose uses the service name mongo).
MONGO_PORT27017MongoDB port.
MONGO_DB_NAMEtesthide_databaseDatabase name.
MONGO_AUTH_SOURCEvalue of MONGO_DB_NAMEAuth 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

VariableDefaultPurpose
REDIS_PASSWORD required(none)Redis password. Set a strong value in production.
REDIS_HOSTlocalhostRedis host (Compose uses redis).
REDIS_PORT6379Redis port.
REDIS_DB0Redis logical DB index.

Backend — Auth & security

VariableDefaultPurpose
JWT_SECRET requireda-very-bad-default-secret-for-local-devHMAC 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_TOKENauto-derived from JWT_SECRETOptional 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_WEBHOOKS0SEC-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

VariableDefaultPurpose
GUNICORN_WORKERS2Worker processes per backend replica. Rule of thumb: (2 × cores) + 1.
AI_API_GUNICORN_WORKERS2Worker count for the ai-api service (compose-level knob; mirrors GUNICORN_WORKERS for that container).
GUNICORN_BIND0.0.0.0:8080Bind address:port.
GUNICORN_THREADS1Threads per worker.
GUNICORN_TIMEOUT120Worker request timeout (s).
GUNICORN_GRACEFUL_TIMEOUT30Graceful shutdown timeout (s).
GUNICORN_KEEPALIVE5Keep-alive seconds for idle connections.
GUNICORN_REUSE_PORTtrueEnable SO_REUSEPORT on the listen socket.
GUNICORN_MAX_REQUESTS10000Recycle a worker after N requests (caps memory creep).
GUNICORN_MAX_REQUESTS_JITTER500Random jitter on the recycle threshold (avoids thundering-herd restarts).
GUNICORN_LOG_LEVELinfoGunicorn 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).

VariableDefaultPurpose
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_SYNCtrue when MODEL_SYNC_ENABLED=trueRun 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_HOSTai-workerHost the backend pokes to wake the AI worker.
AI_WORKER_WAKE_HOST0.0.0.0Bind host for the worker’s wake listener.
AI_WORKER_WAKE_PORT8100Wake-listener port.
AI_WORKER_WAKE_TIMEOUT_S2.0Per-attempt wake HTTP timeout (s).
AI_WORKER_WAKE_RETRIES3Wake retry attempts.

Backend — Build/queue reliability

VariableDefaultPurpose
TPS_HEARTBEAT_TIMEOUT_SEC1200Seconds before a TPS executor session is considered dead (missing heartbeat).
MAX_TPS_CONCURRENT_SESSIONS0Cap on concurrent TPS executor children across all builds. 0 disables the cap.
TPS_RUNNING_PROVIDER_TIMEOUT_HOURS4Hard timeout (h) for a RUNNING_PROVIDER TPS build. 0 disables.
INTERRUPTED_AUTO_CANCEL_MINUTES0Minutes an INTERRUPTED build may linger before the reconciler cancels it. 0 disables.
QUOTA_BUILDS_PER_DAY1000Per-project daily build ceiling (Redis counter; per-project overrides in db_projects.quotas).
QUOTA_AI_REQUESTS_PER_HOUR500Per-project hourly AI-request ceiling.
QUOTA_ARTIFACT_GB_TOTAL100Per-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

VariableDefaultPurpose
LOG_FORMATjsonLog format: json (structured, for Loki) or text.
LOG_LEVELINFOApplication log level.
METRICS_ENABLEDtrueExpose the Prometheus /api/v3/metrics endpoint.
OTEL_TRACES_ENABLEDfalse (.env.example sets true)Enable OpenTelemetry tracing.
OTEL_EXPORTER_OTLP_ENDPOINThttp://tempo:4318OTLP/HTTP collector endpoint (Tempo).
OTEL_SAMPLE_RATE1.0 (prod recommendation 0.1)Trace sampling fraction (1.0 = 100%).
SERVICE_NAMEtesthideService 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

VariableDefaultPurpose
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_CHAINgoogle,openai,anthropic,groq,deepinfra,localInitial 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_CHAINgoogle,anthropic,openai,groq,mistral,deepinfra,localInitial provider fallback order for the Deep diagnosis tier (investigate / narrate).
LLM_CODE_PROVIDER_CHAINgoogle,anthropic,openai,mistral,groq,deepinfra,localInitial 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_RETRIES2Retries on transient provider errors (e.g. Gemini 503).
LLM_TRANSIENT_BACKOFF_S0.8Backoff (s) between transient-error retries.
LLM_MONTHLY_COST_CEILING_USD0Global monthly LLM spend cap (USD). 0 = unlimited; when >0, new calls are rejected once the month’s summed cost reaches it.
LLM_LOCAL_MODEL_FREEtrueTreat local/self-hosted inference as $0. Set false to attribute an internal compute cost.
AUTO_PR_FIX_MAX_CONCURRENCY4Max 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)

VariableDefaultPurpose
AI_LLM_DIRreleases/llmDirectory 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_CTX8192Context window (tokens). Lower to save KV-cache RAM.
ALLOW_LOCAL_LLM_FALLBACKtrueAllow on-device fallback when cloud providers are exhausted.
LOCAL_LLM_MAX_TOKENS768Cap on local output tokens (latency control).
LOCAL_LLM_REQUEST_TIMEOUT_S600Per-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_OFFLINE0Set 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)

VariableDefaultPurpose
EDGE_AI_ENABLEDfalseMaster 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_MODELphi-3-mini-q4.ggufGGUF model name the agent runs.
EDGE_EMBEDDER_MODELminilm-l6-v2.onnxONNX embedder the agent runs.
EDGE_VISION_MODELclip_image.onnxCLIP image-tower ONNX for the visual-diff pre-screen.
EDGE_VISION_ENABLED0Opt-in switch for the Edge vision pre-screen. When off, the manifest omits the vision entry.
EDGE_AI_TIMEOUT_SEC90 (.env.example 600)Edge investigation overall timeout (s).
EDGE_AI_STALL_SEC60Stall timeout (s) before an edge investigation is abandoned.
EDGE_TASK_LEASE_SEC45Lease duration for an edge task assigned to a node.
EDGE_PARTIAL_REDUCE_SEC30Interval (s) for partial map-reduce aggregation.
EDGE_LOG_CHUNK_LINES150Lines per log chunk dispatched to nodes.
EDGE_LOG_OVERLAP_LINES20Overlap lines between consecutive chunks.

Backend — AI worker memory & concurrency

VariableDefaultPurpose
AI_HARD_EXIT_PERCENT88RSS percent at which the worker hard-exits to dodge an OOM kill.
AI_RETRAIN_QUEUE_BUSY_THRESHOLD200Skip retraining when the build queue backlog exceeds this.
AI_LOCK_STALE_SECS300Stale-lock eviction timeout (s).
AI_V2_JOB_CONCURRENCY1Concurrent 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)

VariableDefaultPurpose
MODEL_SYNC_ENABLEDtrueEnable local model-sync crons (still gated by license capabilities and authorized per transfer by the server).
MODEL_SYNC_AUTO_PROMOTEtrueAuto-promote a downloaded base model after validation.
MODEL_SYNC_REQUIRE_BENCHMARKfalseRequire 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_PUBKEYbaked vendor keyEd25519 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_VERIFYtrueVerify 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

VariableDefaultPurpose
LICENSE_GRACE_SECONDS1209600 (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_IPS127.0.0.1,::1CSV of IPs/CIDRs allowed to call the revocation endpoint.

Backend — Docker sidecar (SEC-004)

VariableDefaultPurpose
SIDECAR_URLhttp://sidecar-docker:8081Base 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\sRegex prefixes the sidecar accepts in docker exec.
SIDECAR_ALLOWED_NETWORKStesthide-internal,bridge,testhide-netNetworks 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_BINDING0Set 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)

VariableDefaultPurpose
LDAP_HOST(empty → local auth only)LDAP/AD server hostname. Leave empty to disable LDAP.
LDAP_PORT636LDAP port (636 = LDAPS, the secure default).
LDAP_USE_SSL1Use 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_INSECURE0When 1, do not require/verify the server cert.
LDAP_TLS_VERSIONTLSv1.2Minimum TLS version for LDAPS.

Backend — vCenter

VariableDefaultPurpose
VCENTER_VERIFY_TLSfalseDefault TLS verification for vCenter connections (corp vCenters often use self-signed certs). A connection may override via its insecure flag.
VCENTER_MAX_SYNC_ITEMS20000Cap on hosts/VMs persisted per connection (keeps the doc under Mongo’s 16 MB limit).

Backend — Integrations & monitoring sandbox

VariableDefaultPurpose
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_scriptsDirectory for user monitoring scripts (must be writable).
MONITORING_SCRIPT_TIMEOUT_S900Wall-clock kill for a sandboxed monitoring script (15 min).
MONITORING_SCRIPT_CPU_S870POSIX RLIMIT_CPU for the sandbox.
MONITORING_SCRIPT_MEM_MB512POSIX RLIMIT_AS (virtual memory) for the sandbox.
MONITORING_SCRIPT_OUTPUT_MAX_BYTES8388608 (8 MiB)Max captured stdout bytes.
MONITORING_UPLOAD_MAX_BYTES1048576 (1 MiB)Max monitoring upload payload.
MONITORING_SCRIPT_MAX_CONCURRENCY4Concurrent 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.

VariableDefaultPurpose
API_URLelse PUBLIC_API_URL, else https://localhost:8080Backend REST base URL embedded in the bundle.
WS_URLelse PUBLIC_WS_URL, else wss://localhost:8080WebSocket base URL embedded in the bundle.
PRODUCTIONfalseSets Angular production mode in the generated env.

License server (Django — config/settings.py)

VariableDefaultPurpose
SECRET_KEY required(none — hard fail)Django secret key. No fallback — the server refuses to start if unset. Generate secrets.token_urlsafe(64).
DEBUGFalseDjango 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_LEVELINFODjango logger level.
TRUSTED_PROXY_IPS(empty)CSV of proxy IPs trusted to set X-Forwarded-For.
CONTACT_EMAILthuesdays@gmail.comDestination for contact-form submissions.
EMAIL_SYNC_IN_PROCESSTrueRun the in-process IMAP→DB webmail sync thread. Set False when using the cron command.
EMAIL_HOSTsmtp.gmail.comSMTP host (production email backend).
EMAIL_PORT587SMTP port (STARTTLS).
EMAIL_USE_TLSTrueUse 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_EMAILEMAIL_HOST_USER else noreply@testhide.comDefault From: address.
STRIPE_SECRET_KEY(empty)Stripe secret API key.
STRIPE_WEBHOOK_SECRET(empty)Stripe webhook signing secret.
STRIPE_PRICE_STARTER_MONTHLY / _YEARLYprice_starter_monthly / price_starter_yearlyStripe price IDs for the Starter tier.
STRIPE_PRICE_TEAM_MONTHLY / _YEARLYprice_team_monthly / price_team_yearlyStripe 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_SIGNATURETrueRequire 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.

VariableDefaultPurpose
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_URLhttps://dl.testhide.com/stable/latest.jsonUpdate 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.

VariableDefaultPurpose
PUBLIC_URL required(your domain)Public-facing URL; feeds CORS, Grafana, and Angular bundles. Must match the SSL cert domain.
WS_URLwss://YOUR_DOMAIN:7771Public WebSocket URL embedded at build time.
PRODUCTIONtrueCompose/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_URLPUBLIC_URL:3000Grafana public URL for UI links / OAuth redirects.
GRAFANA_CERT_HOST_CRT / _KEY./ssl/testhide.crt / .keyGrafana SSL cert/key host paths.
PROMETHEUS_PORT_BIND127.0.0.1:9090Prometheus 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

For ML practitioners only. The knobs below tune model training and inference. The defaults are well-balanced for the shipped models — change them only when you understand the trade-off. Most are read inside the heavy training worker (and are also injected by it from the DB-backed 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_*)

VariableDefaultPurpose
RC_MODEL_NAMEdistilbert-base-uncasedBase transformer for the classifier (must match across ai-api/ai-worker, shared HF cache).
RC_EPOCHS3Training epochs (overrides adaptive tier default).
RC_LR2e-5Learning rate.
RC_WEIGHT_DECAY0.01Optimizer weight decay.
RC_FREEZE_BACKBONEFalseFreeze the transformer backbone (train head only).
RC_SEED42Random seed.
RC_MAX_LEN256Tokenizer max length.
RC_NUM_WORKERS0DataLoader worker processes.
RC_AUTO_CONVERT_ONNXFalseAuto-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)

VariableDefaultPurpose
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_BYtest_nameContrastive-pair grouping key.
RC_MODEprecomputedRetriever training mode.
RC_BATCH512Retriever batch size.
RC_TAU0.06Contrastive temperature.
RC_PAIRS_TOPK12Top-K positives per anchor.
RC_PAIRS_MAX_PER_GROUP16Max pairs per group.
RC_TH_TEXT0.60Text-similarity threshold.
RC_TH_STRUCT0.70Structured-similarity threshold.
RC_SAMPLE_GROUPS_FRAC0.0Fraction of groups to sample (0 = all).
RC_SANITYFalseRun a quick sanity training pass.
RC_CPU_ONLYFalseForce CPU retriever training.
RC_PACK_DTYPEf32Packed-vector dtype.
RC_PACK_BLOCK0Pack block size (0 = auto).
RC_PACK_TARGET_MB800Target packed-shard size (MB).
RC_PACK_THREADS0Packing threads (0 = auto).
RC_TORCH_COMPILETrueUse torch.compile.
RC_USE_BF16FalseUse bfloat16.
RC_USE_AMPTrueUse automatic mixed precision.
RC_LOG_EVERY1000Log cadence (steps).
RC_EVAL_EVERY1Eval cadence (epochs).
RC_CLUSTER_EVALTrueRun cluster-quality eval.
RC_KMEANS_K100K-means clusters for eval.
RC_DBSCAN_EPS0.5DBSCAN epsilon.
RC_DBSCAN_MINPTS10DBSCAN min points.
RC_FAISS_CHUNK8000FAISS add chunk size.
RC_FAISS_PCA512PCA dim before FAISS.
RC_FAISS_FP16FalseStore FAISS vectors in fp16.
RC_D_OUT512Output embedding dim.
RC_CLIP_NORM1.0Gradient clip norm.
RC_QUEUE160000Memory-queue size for negatives.
RC_HARD_NEG_K0Hard-negative count (0 = off).
RC_ACCUM4Gradient accumulation steps.
RC_TEMPLATES_DIR(release dir)Override the log-template directory.

Build RCA classifier (BUILD_RCA_*)

VariableDefaultPurpose
BUILD_RCA_ML_MODEshadowInference mode: shadow / active / disabled.
BUILD_RCA_ML_CONFIDENCE_THRESHOLD0.60Min ML confidence to use the prediction over rules-v1 (active mode).
BUILD_RCA_RETRAIN_THRESHOLD200New corpus samples that trigger auto-retrain (set high on the backend so training only runs in ai-worker).
BUILD_RCA_REDIS_LOCK_TTL3600Training-lock TTL (s).
BUILD_RCA_WEEKLY_MIN_SAMPLES50Min new samples before the weekly cron retrains.
BUILD_RCA_SHADOW_MIN_SAMPLES50Min shadow predictions before considering promotion.
BUILD_RCA_SHADOW_MIN_AGREEMENT0.70Min ML/rules agreement rate to auto-promote.
BUILD_RCA_SHADOW_DAYS7Look-back window (days) for the agreement check.
BUILD_RCA_SHADOW_ROLLBACK_THRESHOLD0.60Agreement 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_THRESHOLD2.5Z-score threshold for the build step-duration anomaly detector.

OOD detector (AI_OOD_*)

VariableDefaultPurpose
AI_OOD_AE_HIDDEN256Autoencoder hidden width.
AI_OOD_AE_BOTTLENECK64Autoencoder bottleneck width.
AI_OOD_AE_EPOCHS10Autoencoder epochs.
AI_OOD_AE_LR1e-3Autoencoder learning rate.
AI_OOD_IF_ESTIMATORS200Isolation-forest estimators.
AI_OOD_FUSION_ALPHA0.5AE/IF fusion weight.
AI_OOD_THRESHOLD_QUANTILE0.97Score quantile for the OOD threshold.
AI_OOD_SEED42Random seed.

Visual classifier / YOLO (AI_VISUAL_*, AI_YOLO_*)

VariableDefaultPurpose
AI_VISUAL_EPOCHS5Visual classifier epochs.
AI_VISUAL_HIDDEN_LAYERS256Hidden layer width.
AI_VISUAL_MAX_ITER500Max solver iterations.
AI_VISUAL_TEST_SIZE0.2Validation split.
AI_VISUAL_SEED42Random seed.
AI_YOLO_MODELyolov8n.ptYOLO weights file.
AI_YOLO_EPOCHS5YOLO training epochs.
AI_YOLO_FREEZE10Frozen backbone layers.
AI_YOLO_MIN_ANNOTATIONS10Skip training below N annotated screenshots.
AI_YOLO_WEIGHT_CORRECTION10Sample weight for correction annotations.
AI_YOLO_WEIGHT_MANUAL5Sample 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_*)

VariableDefaultPurpose
AI_FLAKY_MAX_ITER200Max solver iterations.
AI_FLAKY_RANDOM_STATE42Random 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_*)

VariableDefaultPurpose
LOGSIGN_DIR(templates_out release dir)Output directory for mined Drain3 templates.
LOGSIGN_MAX_JOINED_LEN (alias AI_LOGSIGN_MAX_JOINED_LEN)8000Max joined log length fed to the miner.
LOGSIGN_REUSE_TEMPLATES1Reuse persisted templates between runs.
AI_LOGSIGN_TOP_EXAMPLES3Example snippets stored per template.
AI_LOGSIGN_MIN_COUNT1Min cluster size to keep a template.
AI_LOGSIGN_SNIPPET_LEN600Stored snippet length (chars).

Dataset, budget & lock knobs

VariableDefaultPurpose
AI_TRAINING_LOCK_TTL_SEC3600Training-lock TTL (single session at a time).
AI_VECTORS_SIDECAR_ENABLED1Extract vectors into float32 sidecar files (smaller dataset, faster training).
AI_USE_ARTIFACT_LOADER(off)Route model (re)loads through the ModelArtifactLoader ABCs.
AI_LLM_SEED42Local LLM sampling seed.
TEXT_CACHE_SNAPSHOT_MIN_NEW50Min new cached embeddings before a snapshot flush.
AI_PRECOMPUTE_STATE_TTL604800 (7 d)Redis TTL for precompute-state keys.
AI_PRECOMPUTE_LOCK_TTL900Precompute lock TTL (s).
AI_INVESTIGATION_BUDGET_S600Time budget for an LLM investigation (under the 900 s claim lease).
AI_WARM_FC_MAX30Cap on warm failure-context precomputations per batch.
AI_FG_ENRICH_BATCH100Failure-group enrichment batch size.
AI_RULE_SUGGESTION_MIN_COUNT2Min occurrences before a Layer-1 rule suggestion is drafted.
AI_TIER2_AUTO_GC1Auto-GC raw insights after Tier-2 failure-history distillation.
SHORT_KEY_FALLBACK_SAMPLE_EVERY1Throttle 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_*)

VariableDefaultPurpose
SCM_CB_COOLDOWN_SEC60Cooldown after an SCM connection failure (short-circuits new calls).
SCM_CB_WARN_THROTTLE_SEC300Min interval between repeated WARN logs for the same endpoint.
Authoritative source. When in doubt, the code is the source of truth: 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

Base URL  https://your-testhide-server
All endpoints require 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.

GET/api/v3/healthz

Returns server status and version. No auth required. Use as a Kubernetes liveness probe or uptime-monitor endpoint.

Required headers

HeaderValue
None required
curl https://your-testhide-server/api/v3/healthz
Response 200
{"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.

POST/api/v3/authenticate

Exchange username and password for a JWT session token. Pass the returned token in the Authorization header for all subsequent requests.

Required headers

HeaderValue
Content-Typeapplication/json

Required body

FieldTypeDescription
usernamestringUser email or login name
passwordstringAccount password
curl -X POST https://your-testhide-server/api/v3/authenticate \
  -H "Content-Type: application/json" \
  -d '{"username": "admin@corp.com", "password": "s3cr3t!"}'
Response 200
{"token": "eyJhbGci...", "expires_at": "2026-05-21T14:00:00Z", "user_id": 42}
POST/api/v3/logout

Invalidate the current session token immediately. The token is server-side blacklisted.

Required headers

HeaderValue
AuthorizationBearer <token>
curl -X POST https://your-testhide-server/api/v3/logout \
  -H "Authorization: Bearer $TOKEN"
Response 204
(no content)
GET/api/v3/me

Return the profile of the currently authenticated user including role, permissions, and accessible project IDs.

Required headers

HeaderValue
AuthorizationBearer <token>
curl https://your-testhide-server/api/v3/me \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 42, "email": "admin@corp.com", "role": "admin", "projects": [1, 3, 7]}
GET/api/v3/config

Return server-level configuration visible to the current user — feature flags, enabled integrations, AI settings, and global pipeline options.

Required headers

HeaderValue
AuthorizationBearer <token>
curl https://your-testhide-server/api/v3/config \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"version": "3.14.2", "global_options": {"ai_assist": {"enabled": true}, "max_parallel_builds": 20}}
POST/api/v3/sso

Authenticate via SAML SSO. Exchange a base64-encoded IdP-signed SAML response for a Testhide JWT session token.

Required headers

HeaderValue
Content-Typeapplication/json

Required body

FieldTypeDescription
saml_responsestringBase64-encoded SAML response from IdP
curl -X POST https://your-testhide-server/api/v3/sso \
  -H "Content-Type: application/json" \
  -d '{"saml_response": "PHNhbWxwOlJlc3BvbnNl..."}'
Response 200
{"token": "eyJhbGci...", "user_id": 42, "expires_at": "2026-05-21T14:00:00Z"}
POST/api/v3/reset-password

Request a password reset email. A one-use time-limited link is sent to the address if an account exists.

Required headers

HeaderValue
Content-Typeapplication/json

Required body

FieldTypeDescription
emailstringAccount email address
curl -X POST https://your-testhide-server/api/v3/reset-password \
  -H "Content-Type: application/json" \
  -d '{"email": "dev@corp.com"}'
Response 202
{"detail": "Reset link sent if the account exists."}
POST/api/v3/reset-password/confirm

Complete a password reset using the token from the reset email and the new desired password.

Required headers

HeaderValue
Content-Typeapplication/json

Required body

FieldTypeDescription
tokenstringReset token from the email link
new_passwordstringNew 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"}'
Response 200
{"detail": "Password updated successfully."}
GET/api/v3/api-tokens

List all long-lived API tokens for the current user. Token secrets are never returned after creation.

Required headers

HeaderValue
AuthorizationBearer <token>
curl https://your-testhide-server/api/v3/api-tokens \
  -H "Authorization: Bearer $TOKEN"
Response 200
[{"id": 1, "name": "CI Bot", "created_at": "2026-01-10T09:00:00Z", "last_used": "2026-05-19T12:30:00Z"}]
POST/api/v3/api-tokens

Create a new long-lived API token. The token secret is returned only once — store it securely immediately.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
namestringHuman-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"}'
Response 201
{"id": 5, "name": "GitHub Actions Bot", "secret": "th_live_xK9p...Q2rZ", "created_at": "2026-05-20T10:00:00Z"}
DELETE/api/v3/api-tokens/{token_id}

Permanently revoke an API token. Takes effect immediately.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
token_idintegerID of the token to revoke
curl -X DELETE https://your-testhide-server/api/v3/api-tokens/5 \
  -H "Authorization: Bearer $TOKEN"
Response 204
(no content)

Users

Manage platform users. Admin role required for create/update operations.

GET/api/v3/users

Return a paginated list of all platform users with their roles and project assignments.

Required headers

HeaderValue
AuthorizationBearer <token>

Optional query params

ParamTypeDescription
pageintegerPage number (default: 1)
limitintegerItems per page (default: 50, max: 200)
searchstringFilter by email or name
curl "https://your-testhide-server/api/v3/users?page=1&limit=20" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"total": 84, "results": [{"id": 1, "email": "alice@corp.com", "role": "admin", "active": true}]}
POST/api/v3/users

Create a new platform user. An invitation email is sent to the provided address.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
emailstringUser email address
rolestringadmin | developer | viewer

Optional body

FieldTypeDescription
first_namestringFirst name
last_namestringLast name
project_idsarray[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]}'
Response 201
{"id": 88, "email": "bob@corp.com", "role": "developer", "invited_at": "2026-05-20T10:00:00Z"}
GET/api/v3/users/{user_id}

Fetch full profile of a single user by ID including role, project access, and last login.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
user_idintegerID of the user
curl https://your-testhide-server/api/v3/users/88 \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 88, "email": "bob@corp.com", "role": "developer", "active": true, "projects": [1, 3], "last_login": "2026-05-19T08:00:00Z"}
PUT/api/v3/users/{user_id}

Update a user's role, active status, or project assignments.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
user_idintegerID of the user to update

Optional body

FieldTypeDescription
rolestringadmin | developer | viewer
activebooleanEnable or disable the account
project_idsarray[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]}'
Response 200
{"id": 88, "email": "bob@corp.com", "role": "admin", "project_ids": [1, 3, 7]}
GET/api/v3/users/{user_id}/permissions

Return the resolved effective permission set for a user across all projects and platform resources.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
user_idintegerID of the user
curl https://your-testhide-server/api/v3/users/88/permissions \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"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.

GET/api/v3/projects

List all projects the current user has access to.

Required headers

HeaderValue
AuthorizationBearer <token>
curl https://your-testhide-server/api/v3/projects \
  -H "Authorization: Bearer $TOKEN"
Response 200
[{"id": 1, "name": "Backend API", "slug": "backend-api", "jobs_count": 12}]
POST/api/v3/projects

Create a new project. The creating user is automatically assigned as project admin.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
namestringProject display name

Optional body

FieldTypeDescription
descriptionstringShort description
scm_endpoint_idintegerDefault 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}'
Response 201
{"id": 12, "name": "Mobile App", "slug": "mobile-app", "webhook_secret": "wh_sec_abc..."}
GET/api/v3/projects/{project_id}

Fetch full details of a project including webhook URL, SCM configuration, and job statistics.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
project_idintegerProject ID
curl https://your-testhide-server/api/v3/projects/12 \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"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"}
PUT/api/v3/projects/{project_id}

Update project name, description, or SCM endpoint association.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
project_idintegerProject ID

Optional body

FieldTypeDescription
namestringNew display name
descriptionstringUpdated description
scm_endpoint_idintegerDefault 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}'
Response 200
{"id": 12, "name": "Mobile App v2", "scm_endpoint_id": 3}
POST/api/v3/projects/{project_id}/rotate-webhook-secret

Generate a new webhook HMAC secret. The old secret is invalidated immediately — update your SCM webhook settings after rotation.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
project_idintegerProject ID
curl -X POST https://your-testhide-server/api/v3/projects/12/rotate-webhook-secret \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"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.

GET/api/v3/jobs

List all jobs accessible to the current user, optionally filtered by project.

Required headers

HeaderValue
AuthorizationBearer <token>

Optional query params

ParamTypeDescription
project_idintegerFilter by project
searchstringFilter by job name
enabledbooleanFilter by enabled state
curl "https://your-testhide-server/api/v3/jobs?project_id=1&enabled=true" \
  -H "Authorization: Bearer $TOKEN"
Response 200
[{"id": 55, "name": "Regression Suite", "project_id": 1, "node_pool_id": 2, "enabled": true}]
POST/api/v3/jobs

Create a new job within a project.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
namestringJob display name
project_idintegerParent project ID

Optional body

FieldTypeDescription
node_pool_idintegerTarget node pool (default: any available)
timeout_minutesintegerBuild timeout in minutes (default: 60)
max_parallelintegerMax concurrent builds (default: 1)
pipeline_template_idintegerPre-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
  }'
Response 201
{"id": 56, "name": "Smoke Tests", "project_id": 1, "enabled": true}
GET/api/v3/jobs/{job_id}

Fetch full job details including configuration, node pool, and last build status.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
job_idintegerJob ID
curl https://your-testhide-server/api/v3/jobs/55 \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 55, "name": "Regression Suite", "node_pool_id": 2, "last_build_id": 1024, "last_build_status": "passed"}
PUT/api/v3/jobs/{job_id}

Update job settings: name, node pool, timeout, enabled state, or max parallel builds.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
job_idintegerJob ID

Optional body

FieldTypeDescription
namestringNew display name
enabledbooleanEnable or pause the job
node_pool_idintegerReassign to a different pool
timeout_minutesintegerBuild 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}'
Response 200
{"id": 55, "enabled": false, "timeout_minutes": 90}
DELETE/api/v3/jobs/{job_id}

Permanently delete a job and all its associated builds. This action cannot be undone.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
job_idintegerJob ID to delete
curl -X DELETE https://your-testhide-server/api/v3/jobs/55 \
  -H "Authorization: Bearer $TOKEN"
Response 204
(no content)
GET/api/v3/jobs/{job_id}/configuration

Return the full pipeline configuration — setup commands, test command, environment variables, and artifact paths.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
job_idintegerJob ID
curl https://your-testhide-server/api/v3/jobs/55/configuration \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"setup": ["pip install -r requirements.txt"], "command": "pytest tests/ -x", "env": {"ENV": "staging"}, "artifacts": ["reports/*.xml"]}
PUT/api/v3/jobs/{job_id}/configuration

Replace the full pipeline configuration for a job. The next build will use the updated config.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
job_idintegerJob ID

Required body

FieldTypeDescription
commandstringMain test execution command
setuparray[string]Shell commands run before the main command
envobjectEnvironment variables key/value map
artifactsarray[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"]
  }'
Response 200
{"job_id": 55, "updated_at": "2026-05-20T10:15:00Z"}
POST/api/v3/jobs/{job_id}/export

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

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
job_idintegerJob ID to export
curl -X POST https://your-testhide-server/api/v3/jobs/55/export \
  -H "Authorization: Bearer $TOKEN" \
  -o job_55_export.json
Response 200
{"export_version": "1", "job": {"name": "Regression Suite", ...}, "configuration": {...}}
POST/api/v3/jobs/import

Import a job from a bundle exported via /jobs/{id}/export. Creates a new job in the specified target project.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
project_idintegerTarget project for the imported job
bundleobjectExport 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": {...}}'
Response 201
{"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.

POST/api/v3/builds/start

Manually trigger a build for a job. Accepts optional branch, commit SHA, and runtime environment variable overrides.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
job_idintegerJob to trigger

Optional body

FieldTypeDescription
branchstringGit branch (default: job default branch)
commit_shastringExact commit SHA to test
envobjectRuntime env var overrides for this build only
priorityintegerQueue 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"}
  }'
Response 201
{"id": 1025, "job_id": 55, "status": "queued", "branch": "main", "commit_sha": "a1b2c3d4", "created_at": "2026-05-20T10:00:00Z"}
GET/api/v3/builds

List builds with filtering by job, status, branch, or date range. Returns paginated results sorted by creation time descending.

Required headers

HeaderValue
AuthorizationBearer <token>

Optional query params

ParamTypeDescription
job_idintegerFilter by job
statusstringqueued | running | passed | failed
branchstringFilter by branch name
pageintegerPage number (default: 1)
limitintegerResults per page (default: 25)
curl "https://your-testhide-server/api/v3/builds?job_id=55&status=failed&limit=10" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"total": 3, "results": [{"id": 1024, "status": "failed", "branch": "main", "duration_s": 312, "failed_tests": 7}]}
GET/api/v3/builds/{build_id}

Fetch full build details: status, timings, node assignment, test counts, and triggered-by information.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
build_idintegerBuild ID
curl https://your-testhide-server/api/v3/builds/1024 \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 1024, "job_id": 55, "status": "passed", "duration_s": 287, "total_tests": 412, "passed": 410, "failed": 0, "skipped": 2, "node_id": 7}
POST/api/v3/builds/{build_id}/restart

Re-queue a finished build using the same commit SHA and configuration. Optionally restart only failed tests.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
build_idintegerBuild ID to restart

Optional body

FieldTypeDescription
failed_onlybooleanRun 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}'
Response 201
{"id": 1026, "status": "queued", "restarted_from": 1024}
POST/api/v3/builds/{build_id}/stop

Abort a running or queued build. The build status is set to aborted.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
build_idintegerBuild ID to stop
curl -X POST https://your-testhide-server/api/v3/builds/1025/stop \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 1025, "status": "aborted"}
DELETE/api/v3/builds/{build_id}

Permanently delete a build and all associated test results, artifacts, and logs.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
build_idintegerBuild ID to delete
curl -X DELETE https://your-testhide-server/api/v3/builds/1020 \
  -H "Authorization: Bearer $TOKEN"
Response 204
(no content)
POST/api/v3/builds/{build_id}/push_to_top

Move a queued build to the top of the job queue so it executes next.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
build_idintegerBuild ID (must be in queued state)
curl -X POST https://your-testhide-server/api/v3/builds/1025/push_to_top \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 1025, "queue_position": 1}
POST/api/v3/builds/{build_id}/keep-forever

Pin a build so it is excluded from automatic cleanup and retention policies.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
build_idintegerBuild ID to pin
curl -X POST https://your-testhide-server/api/v3/builds/1024/keep-forever \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 1024, "keep_forever": true}
GET/api/v3/builds/{build_id}/pipeline_logs

Return paginated pipeline-level logs (setup, teardown, and orchestration output) for a build. For test-level output use /builds/{id}/stream.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
build_idintegerBuild ID

Optional query params

ParamTypeDescription
offsetintegerLog line offset for pagination
limitintegerLines to return (default: 500)
curl "https://your-testhide-server/api/v3/builds/1024/pipeline_logs?offset=0&limit=200" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"total_lines": 1240, "lines": ["[10:00:01] Setup started", "[10:00:03] pip install OK", "..."]}
GET/api/v3/builds/{build_id}/stream

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

HeaderValue
AuthorizationBearer <token>
Accepttext/event-stream

Path parameters

ParamTypeDescription
build_idintegerBuild ID to stream
curl -N https://your-testhide-server/api/v3/builds/1025/stream \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream"
SSE stream
data: {"type": "log", "data": "[10:00:05] PASSED tests/test_api.py::test_login"}
data: {"type": "status", "data": "passed"}
data: {"type": "done"}
POST/api/v3/builds/{build_id}/comment

Add a text comment to a build for team annotations or incident notes.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
build_idintegerBuild ID

Required body

FieldTypeDescription
textstringComment 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."}'
Response 201
{"id": 88, "build_id": 1024, "text": "Flaky DB connection...", "author": "alice@corp.com"}
POST/api/v3/builds/{build_id}/retry-flaky

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

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
build_idintegerBuild ID containing flaky tests
curl -X POST https://your-testhide-server/api/v3/builds/1024/retry-flaky \
  -H "Authorization: Bearer $TOKEN"
Response 201
{"id": 1027, "status": "queued", "test_count": 4, "restarted_from": 1024}
POST/api/v3/builds/git-notify

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

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
project_idintegerProject to notify
branchstringBranch that was pushed
commit_shastringFull commit SHA

Optional body

FieldTypeDescription
authorstringCommit author email
messagestringCommit 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"
  }'
Response 200
{"queued": 2, "build_ids": [1028, 1029]}

Artifacts

Files produced by builds — test reports, screenshots, binaries, logs. Stored per-build and downloadable on demand.

GET/api/v3/artifacts

List artifacts for a build, optionally filtered by file type or name pattern.

Required headers

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
build_idintegerBuild to list artifacts for

Optional query params

ParamTypeDescription
typestringreport | log | screenshot | binary
searchstringFilter by filename substring
curl "https://your-testhide-server/api/v3/artifacts?build_id=1024" \
  -H "Authorization: Bearer $TOKEN"
Response 200
[{"id": 201, "name": "report.xml", "type": "report", "size_bytes": 48200, "created_at": "2026-05-20T10:05:00Z"}]
GET/api/v3/artifacts/tree

Return the artifact directory tree for a build — useful for navigating complex artifact hierarchies.

Required headers

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
build_idintegerBuild ID
curl "https://your-testhide-server/api/v3/artifacts/tree?build_id=1024" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"name": "/", "children": [{"name": "reports", "children": [{"name": "report.xml", "id": 201}]}, {"name": "logs", "children": []}]}
POST/api/v3/artifacts/upload

Upload an artifact file for a build. Use multipart/form-data. Called by CI agents automatically; use manually to attach external reports.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typemultipart/form-data

Required form fields

FieldTypeDescription
build_idintegerTarget build ID
filefileFile to upload

Optional form fields

FieldTypeDescription
pathstringVirtual 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"
Response 201
{"id": 202, "name": "report.xml", "size_bytes": 48200, "path": "reports/report.xml"}
DELETE/api/v3/artifacts/{artifact_id}

Delete a single artifact file. Pinned builds cannot have artifacts deleted.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
artifact_idintegerArtifact ID to delete
curl -X DELETE https://your-testhide-server/api/v3/artifacts/201 \
  -H "Authorization: Bearer $TOKEN"
Response 204
(no content)
GET/api/v3/artifacts/{artifact_id}/download

Download the raw artifact file. Returns the file with Content-Disposition: attachment.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
artifact_idintegerArtifact ID to download
curl https://your-testhide-server/api/v3/artifacts/201/download \
  -H "Authorization: Bearer $TOKEN" \
  -o report.xml
Response 200
(binary file content with Content-Type matching the artifact)

Reports & Tests

Access test execution results, history, and defect classification for individual test cases.

GET/api/v3/report/{build_id}

Return the aggregated test report for a build: pass/fail/skip counts, duration, and suite breakdown.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
build_idintegerBuild ID
curl https://your-testhide-server/api/v3/report/1024 \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"build_id": 1024, "total": 412, "passed": 400, "failed": 7, "skipped": 5, "duration_s": 287, "suites": [{"name": "auth", "passed": 45, "failed": 1}]}
GET/api/v3/tests

Paginated list of individual test results for a build, with filtering by status, suite, or search string.

Required headers

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
build_idintegerBuild ID to query tests for

Optional query params

ParamTypeDescription
statusstringpassed | failed | skipped
suitestringFilter by test suite name
searchstringFilter by test name substring
pageintegerPage number (default: 1)
limitintegerResults per page (default: 50)
curl "https://your-testhide-server/api/v3/tests?build_id=1024&status=failed" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"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"}]}
GET/api/v3/tests/{test_id}/history

Return the execution history for a specific test case across recent builds — useful for spotting flakiness trends.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
test_idintegerTest result ID

Optional query params

ParamTypeDescription
limitintegerNumber of past runs to return (default: 20)
curl "https://your-testhide-server/api/v3/tests/5001/history?limit=10" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"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"}]}
POST/api/v3/tests/{test_id}/classification

Manually override the AI-assigned classification for a failed test result. Classifications guide quarantine and retry decisions.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
test_idintegerTest result ID

Required body

FieldTypeDescription
classificationstringproduct_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"}'
Response 200
{"test_id": 5001, "classification": "product_bug", "set_by": "alice@corp.com"}
POST/api/v3/tests/{test_id}/defect

Link a failed test to a defect ticket (Jira, GitHub Issues, etc.) for tracking.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
test_idintegerTest result ID

Required body

FieldTypeDescription
defect_keystringTicket key, e.g. AUTH-421
defect_urlstringFull 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"}'
Response 200
{"test_id": 5001, "defect_key": "AUTH-421", "defect_url": "https://jira.corp.com/browse/AUTH-421"}
POST/api/v3/tests/{test_id}/resolution

Set a resolution note for a failed test — explains the root cause or fix applied.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
test_idintegerTest result ID

Required body

FieldTypeDescription
resolutionstringFree-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."}'
Response 200
{"test_id": 5001, "resolution": "Fixed in PR #847...", "set_by": "alice@corp.com"}
POST/api/v3/tests/{test_id}/known-issue

Link a failed test to a tracked known-issue entry so future identical failures are auto-classified without manual review.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
test_idintegerTest result ID

Required body

FieldTypeDescription
known_issue_idintegerID 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}'
Response 200
{"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.

GET/api/v3/quarantine

List all quarantined test node IDs for a job.

Required headers

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
job_idintegerJob ID to query quarantine for
curl "https://your-testhide-server/api/v3/quarantine?job_id=55" \
  -H "Authorization: Bearer $TOKEN"
Response 200
[{"id": 1, "node_id": "tests/test_auth.py::test_token_refresh", "reason": "flaky", "quarantined_at": "2026-05-10T09:00:00Z"}]
POST/api/v3/quarantine

Quarantine a single test node ID. Future builds will still run it but its failure won't affect the build result.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
job_idintegerJob to apply quarantine to
node_idstringFull test node ID (e.g. tests/test_auth.py::test_login)
reasonstringflaky | 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"}'
Response 201
{"id": 2, "node_id": "tests/test_auth.py::test_token_refresh", "reason": "flaky"}
POST/api/v3/quarantine/bulk

Quarantine multiple tests in a single request, or remove a list of node IDs from quarantine.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
job_idintegerJob to apply changes to
actionstringadd | remove
node_idsarray[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"]}'
Response 200
{"added": 2, "skipped": 0}
GET/api/v3/quarantine/stats

Return quarantine statistics for a job — total quarantined, by reason, and age distribution.

Required headers

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
job_idintegerJob ID
curl "https://your-testhide-server/api/v3/quarantine/stats?job_id=55" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"total": 14, "by_reason": {"flaky": 9, "infra": 3, "wip": 2}, "oldest_days": 42}
GET/api/v3/quarantine/nodeids

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

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
job_idintegerJob ID
curl "https://your-testhide-server/api/v3/quarantine/nodeids?job_id=55" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"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.

GET/api/v3/pipeline-templates

List all pipeline templates available to the current user.

Required headers

HeaderValue
AuthorizationBearer <token>
curl https://your-testhide-server/api/v3/pipeline-templates \
  -H "Authorization: Bearer $TOKEN"
Response 200
[{"id": 3, "name": "Python pytest", "description": "Standard Python test pipeline", "jobs_using": 12}]
POST/api/v3/pipeline-templates

Create a new pipeline template.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
namestringTemplate name
setuparray[string]Setup shell commands
envobjectDefault environment variables

Optional body

FieldTypeDescription
descriptionstringHuman-readable description
artifactsarray[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"]
  }'
Response 201
{"id": 4, "name": "Node.js Jest", "jobs_using": 0}
PUT/api/v3/pipeline-templates/{template_id}

Update an existing pipeline template. All jobs using this template will pick up the changes on their next build.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
template_idintegerTemplate 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"]}'
Response 200
{"id": 4, "name": "Node.js Jest", "updated_at": "2026-05-20T11:00:00Z"}
DELETE/api/v3/pipeline-templates/{template_id}

Delete a pipeline template. Will fail if any jobs still reference it — reassign those jobs first.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
template_idintegerTemplate ID to delete
curl -X DELETE https://your-testhide-server/api/v3/pipeline-templates/4 \
  -H "Authorization: Bearer $TOKEN"
Response 204
(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.

GET/api/v3/nodes

List all registered nodes with their status, pool assignment, and current load.

Required headers

HeaderValue
AuthorizationBearer <token>

Optional query params

ParamTypeDescription
pool_idintegerFilter by node pool
statusstringonline | offline | quarantined
curl "https://your-testhide-server/api/v3/nodes?status=online" \
  -H "Authorization: Bearer $TOKEN"
Response 200
[{"id": 7, "name": "agent-01", "status": "online", "pool_id": 2, "running_builds": 1, "cpu_pct": 62}]
POST/api/v3/nodes

Register a new CI agent node. Returns an agent token for the node to use when connecting to the server.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
namestringUnique node name
pool_idintegerNode pool to join

Optional body

FieldTypeDescription
tagsarray[string]Labels for routing (e.g. ["linux", "gpu"])
max_parallelintegerMax 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}'
Response 201
{"id": 12, "name": "agent-05", "pool_id": 2, "agent_token": "agt_xK9p...Q2rZ"}
GET/api/v3/nodes/find

Find a node by name. Useful for scripted registration checks before adding a node.

Required headers

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
namestringExact node name to look up
curl "https://your-testhide-server/api/v3/nodes/find?name=agent-05" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 12, "name": "agent-05", "status": "online", "pool_id": 2}
GET/api/v3/nodes/{node_id}

Fetch full details for a single node: hardware info, uptime, running builds, and error counts.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
node_idintegerNode ID
curl https://your-testhide-server/api/v3/nodes/12 \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 12, "name": "agent-05", "status": "online", "cpu_cores": 8, "ram_gb": 16, "uptime_s": 86400, "running_builds": 1}
PUT/api/v3/nodes/{node_id}

Update a node's name, pool assignment, tags, or max_parallel builds.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
node_idintegerNode ID

Optional body

FieldTypeDescription
pool_idintegerMove to a different node pool
tagsarray[string]Replace the full tag list
max_parallelintegerMax 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}'
Response 200
{"id": 12, "pool_id": 3, "max_parallel": 4}
DELETE/api/v3/nodes/{node_id}

Deregister a CI node. Any running builds will be aborted. The agent binary can then be uninstalled from the host.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
node_idintegerNode ID to deregister
curl -X DELETE https://your-testhide-server/api/v3/nodes/12 \
  -H "Authorization: Bearer $TOKEN"
Response 204
(no content)
POST/api/v3/nodes/{node_id}/quarantine

Quarantine a node — it stops receiving new build assignments but remains registered for inspection.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
node_idintegerNode ID
curl -X POST https://your-testhide-server/api/v3/nodes/12/quarantine \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"id": 12, "status": "quarantined"}
GET/api/v3/nodes/{node_id}/health

Return real-time health metrics for a node: CPU, memory, disk, and agent heartbeat age.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
node_idintegerNode ID
curl https://your-testhide-server/api/v3/nodes/12/health \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"node_id": 12, "cpu_pct": 62, "ram_pct": 48, "disk_free_gb": 120, "last_heartbeat_s": 4}
POST/api/v3/nodes/{node_id}/restart

Send a restart command to the Testhide agent process on a node. Useful after configuration changes or to clear stuck builds.

Required headers

HeaderValue
AuthorizationBearer <token>

Path parameters

ParamTypeDescription
node_idintegerNode ID
curl -X POST https://your-testhide-server/api/v3/nodes/12/restart \
  -H "Authorization: Bearer $TOKEN"
Response 202
{"detail": "Restart command sent to node 12."}
POST/api/v3/nodes/{node_id}/execute

Run an ad-hoc shell command on a node and return stdout/stderr. Useful for diagnostics. Requires admin role.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path parameters

ParamTypeDescription
node_idintegerNode ID

Required body

FieldTypeDescription
commandstringShell command to execute

Optional body

FieldTypeDescription
timeout_sintegerMax 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}'
Response 200
{"exit_code": 0, "stdout": "Filesystem  Size  Used Avail Use%\n/dev/sda1   500G  380G  120G  76%", "stderr": ""}
GET/api/v3/nodes/agent/latest

Return download URLs and checksums for the latest Testhide agent binary for each supported platform.

Required headers

HeaderValue
AuthorizationBearer <token>
curl https://your-testhide-server/api/v3/nodes/agent/latest \
  -H "Authorization: Bearer $TOKEN"
Response 200
{"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.

GET/api/v3/node-pools

List all node pools with their labels and routing rules.

Required headers

HeaderValue
AuthorizationBearer <token>

Response fields

FieldTypeDescription
idstringInternal identifier
namestringPool name (used in pool:<name> targets)
labelsstring[]Free-form metadata tags
maxBuildsPerNodeintConcurrent builds per node in the pool (-1 = node default)
maxConcurrentintTotal concurrent builds across the pool (-1 = unlimited)
priorityBoostintDispatch-score bonus for builds targeting this pool
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/node-pools
Response 200
{"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"}]}
GET/api/v3/node-pools/{name}

Fetch a single pool by name. Returns 404 if no such pool exists.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
namestringPool name
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/node-pools/linux-large
Response 200
{"data": {"id": "665f1c8e2a", "name": "linux-large",
  "labels": ["arch:amd64", "size:large"],
  "maxBuildsPerNode": 2, "maxConcurrent": 20, "priorityBoost": 10}}
POST/api/v3/node-pools

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

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
namestringUnique 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

FieldTypeDescription
labelsstring[]Metadata tags. Default [].
maxBuildsPerNodeintPer-node concurrency. -1 (node default) or a positive integer. Default 1.
maxConcurrentintPool-wide concurrency. -1 (unlimited) or a positive integer. Default -1.
priorityBoostintDispatch-score bonus, -10001000. 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
Response 200
{"data": {"id": "665f1c8e2a", "name": "linux-large",
  "labels": ["arch:amd64", "size:large"],
  "maxBuildsPerNode": 2, "maxConcurrent": 20, "priorityBoost": 10}}
PUT/api/v3/node-pools/{name}

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

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path params

ParamTypeDescription
namestringPool name

Optional body

FieldTypeDescription
labelsstring[]Replacement label set
maxBuildsPerNodeint-1 or positive integer
maxConcurrentint-1 or positive integer
priorityBoostint-10001000
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
Response 200
{"data": {"id": "665f1c8e2a", "name": "linux-large",
  "labels": ["arch:amd64", "size:xlarge"],
  "maxBuildsPerNode": 2, "maxConcurrent": 40, "priorityBoost": 10}}
DELETE/api/v3/node-pools/{name}

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

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
namestringPool name
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/node-pools/linux-large
Response 200
{"data": {"deleted": true}}
GET/api/v3/node-pools/{name}/nodes

List the nodes currently assigned to a pool. Returns 404 if the pool does not exist.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
namestringPool name
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/node-pools/linux-large/nodes
Response 200
{"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.

GET/api/v3/docker-agents

List all Docker agent containers across all hosts. Returns status, image, and assigned build.

Required headers

HeaderValue
AuthorizationBearer <token>

Optional query params

ParamTypeDescription
statusstringrunning | stopped | error
project_idintFilter by project
curl -H "Authorization: Bearer $TOKEN" \
  "https://your-testhide-server/api/v3/docker-agents?status=running"
Response 200
[{"id": "da-7f3c", "host": "build-host-01", "image": "testhide/agent:3.14",
  "status": "running", "build_id": 9812, "started_at": "2025-06-10T08:12:00Z"}]
POST/api/v3/docker-agents/spawn

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

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
hoststringDocker host identifier or IP
imagestringDocker image to use (e.g. testhide/agent:3.14)

Optional body

FieldTypeDescription
pool_idintAssign to a node pool
envobjectExtra environment variables injected into container
memory_mbintMemory limit in MB
cpusfloatCPU 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
Response 201
{"id": "da-9a1e", "host": "build-host-01", "status": "starting",
  "image": "testhide/agent:3.14", "pool_id": 3}
POST/api/v3/docker-agents/{id}/start

Start a stopped Docker agent container without re-spawning it.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idstringDocker agent container ID
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/docker-agents/da-7f3c/start
Response 200
{"id": "da-7f3c", "status": "running"}
POST/api/v3/docker-agents/{id}/stop

Gracefully stop a running Docker agent. The container is paused and can be restarted later.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idstringDocker agent container ID
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/docker-agents/da-7f3c/stop
Response 200
{"id": "da-7f3c", "status": "stopped"}
DELETE/api/v3/docker-agents/{id}

Destroy and remove a Docker agent container. The container is stopped and deleted from the host. Any running build is aborted.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idstringDocker agent container ID
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/docker-agents/da-7f3c
Response 204
— No content —
GET/api/v3/docker-agents/{id}/logs

Fetch the last N lines of stdout/stderr from a Docker agent container.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idstringDocker agent container ID

Optional query params

ParamTypeDescription
tailintNumber of lines to return (default 200)
streamboolIf 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"
Response 200
{"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.

GET/api/v3/webhooks

List all registered webhooks with their event subscriptions and delivery stats.

Required headers

HeaderValue
AuthorizationBearer <token>

Optional query params

ParamTypeDescription
project_idintFilter by project
activeboolReturn only active/inactive hooks
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/webhooks
Response 200
[{"id": 14, "url": "https://hooks.example.com/testhide",
  "events": ["build.finished","build.failed"], "active": true,
  "deliveries_ok": 312, "deliveries_failed": 2}]
POST/api/v3/webhooks

Register a new webhook. Testhide signs each delivery with HMAC-SHA256 using the secret you provide.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
urlstringHTTPS endpoint to deliver events to
eventsstring[]Event types: build.started, build.finished, build.failed, build.cancelled, test.flaky, test.quarantined, node.offline

Optional body

FieldTypeDescription
secretstringHMAC secret for signature verification
project_idintScope to a single project
activeboolEnable 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
Response 201
{"id": 14, "url": "https://hooks.example.com/testhide",
  "events": ["build.finished","build.failed"], "active": true}
PUT/api/v3/webhooks/{id}

Update webhook URL, event subscriptions, secret, or active state.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path params

ParamTypeDescription
idintWebhook ID

Optional body

FieldTypeDescription
urlstringNew target URL
eventsstring[]Replacement event list
secretstringNew HMAC secret
activeboolEnable 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
Response 200
{"id": 14, "active": false, "events": ["build.finished","build.failed"]}
DELETE/api/v3/webhooks/{id}

Permanently delete a webhook registration.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintWebhook ID
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/webhooks/14
Response 204
— No content —
POST/api/v3/webhooks/{id}/test

Send a test ping delivery to the webhook URL to verify connectivity and signature validation.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintWebhook ID
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/webhooks/14/test
Response 200
{"delivered": true, "status_code": 200, "latency_ms": 142}
GET/api/v3/webhooks/{id}/history

Retrieve recent delivery history for a webhook, including response codes and error details.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintWebhook ID

Optional query params

ParamTypeDescription
limitintMax deliveries to return (default 50)
statusstringok | failed — filter by delivery outcome
curl -H "Authorization: Bearer $TOKEN" \
  "https://your-testhide-server/api/v3/webhooks/14/history?limit=20&status=failed"
Response 200
[{"delivery_id": "d-882f", "event": "build.failed", "status_code": 503,
  "error": "connection refused", "attempted_at": "2025-06-10T07:55:12Z"}]
POST/api/v3/webhooks/bulk-delete

Delete multiple webhooks in a single request.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
idsint[]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
Response 200
{"deleted": 3, "ids": [14, 15, 16]}

MS Teams

Configure Microsoft Teams notification channels to receive build and test result alerts directly in Teams channels.

GET/api/v3/ms-teams

List all configured MS Teams notification channels.

Required headers

HeaderValue
AuthorizationBearer <token>
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/ms-teams
Response 200
[{"id": 5, "name": "CI Alerts", "project_id": 12,
  "webhook_url": "https://outlook.office.com/webhook/...",
  "events": ["build.failed","test.flaky"], "active": true}]
POST/api/v3/ms-teams

Register a new MS Teams incoming webhook for build notifications. Get the webhook URL from the Teams channel connector settings.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
namestringDisplay name for this integration
webhook_urlstringMS Teams incoming webhook URL
project_idintProject to monitor
eventsstring[]Events: build.failed, build.finished, test.flaky, test.quarantined, node.offline

Optional body

FieldTypeDescription
mention_on_failstringTeams 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
Response 201
{"id": 5, "name": "CI Alerts", "active": true,
  "events": ["build.failed","test.flaky"]}
PUT/api/v3/ms-teams/{id}

Update a Teams notification channel — change events, webhook URL, or toggle active state.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path params

ParamTypeDescription
idintIntegration ID

Optional body

FieldTypeDescription
eventsstring[]Replacement event list
webhook_urlstringNew webhook URL
activeboolEnable 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
Response 200
{"id": 5, "active": true, "events": ["build.failed","build.finished","test.flaky"]}
DELETE/api/v3/ms-teams/{id}

Remove a Teams notification integration.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintIntegration ID
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/ms-teams/5
Response 204
— No content —
POST/api/v3/ms-teams/{id}/test

Send a test message to the Teams channel to verify the webhook is reachable and the integration is working.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintIntegration ID
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/ms-teams/5/test
Response 200
{"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.

GET/api/v3/scm

List all SCM integrations configured on the server.

Required headers

HeaderValue
AuthorizationBearer <token>
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/scm
Response 200
[{"id": 2, "type": "github", "host": "github.com",
  "name": "Main GitHub", "project_id": 12, "repo": "acme/backend"}]
POST/api/v3/scm

Create an SCM integration. The token is stored encrypted and used to fetch commit metadata and set build statuses on PRs.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
typestringgithub | gitlab | bitbucket
namestringDisplay name
project_idintAssociated project
repostringRepo path (owner/name)
tokenstringPersonal access or app token with repo read + status write

Optional body

FieldTypeDescription
hoststringSelf-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
Response 201
{"id": 2, "type": "github", "name": "Main GitHub",
  "repo": "acme/backend", "project_id": 12}
GET/api/v3/scm/{id}

Retrieve a single SCM integration by ID. The stored token is never returned.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintSCM integration ID
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/scm/2
Response 200
{"id": 2, "type": "github", "host": "github.com",
  "name": "Main GitHub", "repo": "acme/backend", "project_id": 12}
PUT/api/v3/scm/{id}

Update an SCM integration — rotate the token or change the repo target.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path params

ParamTypeDescription
idintSCM integration ID

Optional body

FieldTypeDescription
tokenstringNew access token
repostringNew repo path
namestringNew 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
Response 200
{"id": 2, "type": "github", "repo": "acme/backend", "updated": true}
DELETE/api/v3/scm/{id}

Remove an SCM integration. Existing builds retain their linked commit data.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintSCM integration ID
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/scm/2
Response 204
— No content —
GET/api/v3/scm/{id}/branches

List branches in the connected repository. Useful for populating branch selectors in build trigger UIs.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintSCM integration ID

Optional query params

ParamTypeDescription
qstringFilter branches by name prefix
limitintMax results (default 50)
curl -H "Authorization: Bearer $TOKEN" \
  "https://your-testhide-server/api/v3/scm/2/branches?q=release"
Response 200
["release/3.14", "release/3.13", "release/3.12"]
GET/api/v3/scm/{id}/commits

List recent commits for a branch, used to link builds to specific SHAs.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintSCM integration ID

Optional query params

ParamTypeDescription
branchstringBranch name (default: default branch)
limitintMax commits (default 20)
curl -H "Authorization: Bearer $TOKEN" \
  "https://your-testhide-server/api/v3/scm/2/commits?branch=main&limit=5"
Response 200
[{"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.

GET/api/v3/vcenter

List all vCenter integrations configured on the server.

Required headers

HeaderValue
AuthorizationBearer <token>
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/vcenter
Response 200
[{"id": 1, "name": "Prod vCenter", "host": "vcenter.corp.local",
  "datacenter": "DC-East", "connected": true}]
POST/api/v3/vcenter

Add a vCenter server integration. Credentials are stored encrypted and used for all VM lifecycle operations.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
namestringDisplay name
hoststringvCenter FQDN or IP
usernamestringvCenter service account username
passwordstringvCenter service account password
datacenterstringTarget datacenter name

Optional body

FieldTypeDescription
insecureboolSkip TLS certificate validation (not recommended)
clusterstringDefault 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
Response 201
{"id": 1, "name": "Prod vCenter", "host": "vcenter.corp.local",
  "datacenter": "DC-East", "connected": true}
GET/api/v3/vcenter/{id}

Retrieve a single vCenter integration by ID.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintvCenter integration ID
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/vcenter/1
Response 200
{"id": 1, "name": "Prod vCenter", "host": "vcenter.corp.local",
  "datacenter": "DC-East", "cluster": "Compute-01", "connected": true}
POST/api/v3/vcenter/{id}/test

Test connectivity to the vCenter server using the stored credentials. Returns connection status and vCenter API version.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintvCenter integration ID
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/vcenter/1/test
Response 200
{"connected": true, "vcenter_version": "8.0.2", "latency_ms": 45}
POST/api/v3/vcenter/{id}/sync

Trigger a sync to refresh the VM inventory from vCenter. Updates power state and resource pool assignments for all known VMs.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintvCenter integration ID
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/vcenter/1/sync
Response 200
{"synced_vms": 24, "updated": 3, "duration_ms": 1840}
POST/api/v3/vcenter/{id}/vm/power

Power on or off a VM agent managed by this vCenter integration. Used to bring up spare capacity or suspend idle agents.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path params

ParamTypeDescription
idintvCenter integration ID

Required body

FieldTypeDescription
vm_idstringvCenter VM managed object ID (e.g. vm-1042)
actionstringpower_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
Response 200
{"vm_id": "vm-1042", "action": "power_on", "status": "powered_on"}

GitOps

Trigger pipeline synchronization from Git repository events and configure GitOps-style deployment webhooks.

POST/api/v3/gitops/sync

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

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
project_idintProject to sync pipeline config for

Optional body

FieldTypeDescription
branchstringBranch to pull config from (default: main)
dry_runboolValidate 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
Response 200
{"synced": true, "project_id": 12, "branch": "main",
  "sha": "a1b2c3d", "jobs_updated": 4, "jobs_created": 1, "jobs_removed": 0}
POST/api/v3/gitops/webhook

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

HeaderValue
X-Hub-Signature-256sha256=<hmac> (GitHub) — or platform equivalent

Required body

FieldTypeDescription
(payload)objectStandard SCM push event payload (passed through verbatim)

Optional query params

ParamTypeDescription
project_idintOverride 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
Response 200
{"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.

GET/api/v3/jira

List all Jira integration configurations.

Required headers

HeaderValue
AuthorizationBearer <token>
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/jira
Response 200
[{"id": 3, "name": "ACME Jira", "host": "https://acme.atlassian.net",
  "project_key": "BACK", "auto_create": true}]
POST/api/v3/jira

Create a Jira integration. Provide an API token (not your login password) from Atlassian account settings.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
namestringDisplay name for this integration
hoststringJira base URL (e.g. https://acme.atlassian.net)
emailstringAtlassian account email for authentication
api_tokenstringAtlassian API token
project_keystringJira project key where issues are created (e.g. BACK)

Optional body

FieldTypeDescription
auto_createboolAuto-create issues when new tests fail (default false)
issue_typestringJira issue type (default: Bug)
testhide_project_idintScope 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
Response 201
{"id": 3, "name": "ACME Jira", "project_key": "BACK",
  "auto_create": true, "host": "https://acme.atlassian.net"}
PUT/api/v3/jira/{id}

Update a Jira integration — rotate token, change project key, or toggle auto-create.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Path params

ParamTypeDescription
idintJira integration ID

Optional body

FieldTypeDescription
api_tokenstringNew API token
project_keystringNew project key
auto_createboolToggle auto issue creation
issue_typestringJira 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
Response 200
{"id": 3, "project_key": "QA", "auto_create": false}
DELETE/api/v3/jira/{id}

Remove a Jira integration. Existing issue links on tests are retained.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintJira integration ID
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/jira/3
Response 204
— 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.

POST/api/v3/ai/chat

Send a message to the AI assistant. Optionally attach build or test context so the AI can answer questions about specific failures.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
messagestringUser message / question

Optional body

FieldTypeDescription
build_idintBuild to investigate
test_idintSpecific test case to investigate
session_idstringContinue 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
Response 200
{"session_id": "sess-a8f3", "reply": "Build 9812 failed in stage 'unit-tests'. The test...",
  "sources": ["build:9812", "test:47821"]}
POST/api/v3/ai/chat/stream

Same as POST /ai/chat but returns the AI reply as a Server-Sent Events stream for progressive rendering in the UI.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json
Accepttext/event-stream

Required body

FieldTypeDescription
messagestringUser message

Optional body

FieldTypeDescription
build_idintBuild context
session_idstringContinue 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
Response 200 (SSE)
data: {"token": "Build"}
data: {"token": " 9812"}
data: {"token": " failed"}
data: [DONE]
GET/api/v3/ai/llm/status

Check which LLM provider and model are configured on the server, and whether AI features are available.

Required headers

HeaderValue
AuthorizationBearer <token>
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/ai/llm/status
Response 200
{"provider": "openai", "model": "gpt-4o", "available": true,
  "features": ["chat", "investigation", "test_health"]}
GET/api/v3/ai/quota

Get the current AI token usage and quota limits for the authenticated user or organization.

Required headers

HeaderValue
AuthorizationBearer <token>
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/ai/quota
Response 200
{"tokens_used": 142800, "tokens_limit": 1000000,
  "resets_at": "2025-07-01T00:00:00Z", "percent_used": 14.3}
POST/api/v3/ai/diagnose

Run AI-powered root-cause analysis on a failed build. Returns a structured diagnosis with likely causes and suggested fixes.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
build_idintID of the failed build to analyze

Optional body

FieldTypeDescription
include_logsboolInclude full build logs in the analysis (default true)
max_testsintMax 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
Response 200
{"build_id": 9812, "root_cause": "OOM in test worker",
  "confidence": 0.87, "suggested_fix": "Increase JVM heap size or reduce parallelism",
  "affected_tests": 14}
GET/api/v3/ai/insights

Retrieve AI-generated insights for a project — recurring failure patterns, top flaky tests, and build health trends.

Required headers

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
project_idintProject to generate insights for

Optional query params

ParamTypeDescription
daysintLook-back window in days (default 7)
curl -H "Authorization: Bearer $TOKEN" \
  "https://your-testhide-server/api/v3/ai/insights?project_id=12&days=14"
Response 200
{"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/api/v3/ai/summary/{build_id}

Get a plain-language AI summary of a build — what passed, what failed, and what likely caused the failures.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
build_idintBuild ID
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/ai/summary/9812
Response 200
{"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."}
POST/api/v3/ai/job/stream

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

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
typestringJob type: project_audit | failure_cluster | regression_scan
project_idintTarget 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
Response 202
{"job_id": "aijob-f2c8", "status": "queued", "estimated_seconds": 45}
GET/api/v3/ai/job/{job_id}/result

Poll the result of a long-running AI analysis job. Returns status while running, full result when complete.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
job_idstringAI job ID from /ai/job/stream
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/ai/job/aijob-f2c8/result
Response 200
{"job_id": "aijob-f2c8", "status": "complete",
  "result": {"audit_score": 74, "recommendations": ["Quarantine 8 chronic flakes", "..."]}}
POST/api/v3/ai/job/{job_id}/cancel

Cancel a running AI analysis job.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
job_idstringAI job ID
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/ai/job/aijob-f2c8/cancel
Response 200
{"job_id": "aijob-f2c8", "status": "cancelled"}
GET/api/v3/ai/search

Semantic search over build logs, test failure messages, and AI summaries using natural language queries.

Required headers

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
qstringNatural language search query
project_idintScope search to a project

Optional query params

ParamTypeDescription
limitintMax results (default 10)
typestringFilter: build | test | log
curl -H "Authorization: Bearer $TOKEN" \
  "https://your-testhide-server/api/v3/ai/search?q=database+timeout&project_id=12"
Response 200
{"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/api/v3/ai/tests/health

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

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
project_idintProject to analyze

Optional query params

ParamTypeDescription
daysintLook-back window (default 14)
limitintMax tests to return (default 50)
min_runsintMinimum 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"
Response 200
{"tests": [{"test_id": 47821, "name": "UserLoginTest",
  "health_score": 32, "flake_rate": 0.34, "trend": "worsening",
  "risk": "high", "recommended_action": "quarantine"}]}
GET/api/v3/ai/tests/{id}/health

Get detailed AI health analysis for a single test — includes failure pattern breakdown, correlated builds, and actionable recommendation.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idintTest case ID

Optional query params

ParamTypeDescription
daysintLook-back window (default 30)
curl -H "Authorization: Bearer $TOKEN" \
  "https://your-testhide-server/api/v3/ai/tests/47821/health?days=30"
Response 200
{"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/api/v3/ai/emerging/latest

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

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
project_idintProject to check

Optional query params

ParamTypeDescription
thresholdfloatMinimum flake-rate increase to flag (default 0.1)
windowintRecent build count to compare against (default 10)
curl -H "Authorization: Bearer $TOKEN" \
  "https://your-testhide-server/api/v3/ai/emerging/latest?project_id=12"
Response 200
{"emerging": [{"test_id": 52004, "name": "PaymentFlowTest",
  "baseline_rate": 0.02, "recent_rate": 0.18, "delta": 0.16,
  "first_seen_build": 9808}]}
GET/api/v3/ai/emerging/history

Retrieve the historical log of emerging-flaky detections for a project. Useful for auditing when tests first became problematic.

Required headers

HeaderValue
AuthorizationBearer <token>

Required query params

ParamTypeDescription
project_idintProject to query

Optional query params

ParamTypeDescription
daysintHistory window in days (default 30)
test_idintFilter to a specific test
curl -H "Authorization: Bearer $TOKEN" \
  "https://your-testhide-server/api/v3/ai/emerging/history?project_id=12&days=30"
Response 200
[{"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.

GET/api/v3/evals

List all evaluation runs with their status, scores, and model configuration.

Required headers

HeaderValue
AuthorizationBearer <token>

Optional query params

ParamTypeDescription
statusstringpending | running | complete | failed
limitintMax results (default 20)
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/evals
Response 200
[{"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/api/v3/evals/{id}

Get full details of a single evaluation run, including per-case results and failure examples.

Required headers

HeaderValue
AuthorizationBearer <token>

Path params

ParamTypeDescription
idstringEvaluation run ID
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/evals/eval-c3d9
Response 200
{"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}
POST/api/v3/evals/runs

Start a new evaluation run against the configured LLM. The run is queued and executes asynchronously. Poll /evals/{id} for results.

Required headers

HeaderValue
AuthorizationBearer <token>
Content-Typeapplication/json

Required body

FieldTypeDescription
suitestringEval suite to run: diagnosis | chat | health | full

Optional body

FieldTypeDescription
model_overridestringTest a specific model instead of the configured one
sample_sizeintNumber 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
Response 202
{"id": "eval-d7a1", "status": "pending", "suite": "diagnosis",
  "model": "gpt-4o-mini", "estimated_s": 90}
GET/api/v3/evals/regressions

Compare the latest eval run to the previous run and return any quality regressions — eval cases that previously passed but now fail.

Required headers

HeaderValue
AuthorizationBearer <token>

Optional query params

ParamTypeDescription
baseline_idstringEval ID to use as baseline (default: last complete run)
current_idstringEval 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"
Response 200
{"baseline": "eval-c3d9", "current": "eval-d7a1",
  "score_delta": -0.06, "regressions": [
    {"case": "diagnose_oom", "baseline": "pass", "current": "fail"}
  ], "improvements": []}

License

Retrieve the current license status, tier, and feature entitlements for this Testhide installation.

GET/api/v3/license/status

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

HeaderValue
AuthorizationBearer <token>
curl -H "Authorization: Bearer $TOKEN" \
  https://your-testhide-server/api/v3/license/status
Response 200
{"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
  }}