Quarantine From the UI - 2026-11-01¶
Gap G2 (and quick win Q8) of Feature Gaps and Quick Wins.
Context¶
Flaky Test Quarantine - 2026-10-22 made quarantine a quarantine: list in qasight.yaml, applied by the stage gate in qasight/engine/quarantine.py. That reaches qasight run users only. People who record results with qasight ingest or pytest --qasight, often from CI against a team server, have no config file (an ingest plan didn't even read one), so qasight flaky told them to paste a snippet into a file they don't have (Q8). Trunk, Buildkite and Datadog sell the alternative: a Quarantine button, a list the server keeps, and jobs that read it, so a quarantined failure doesn't fail the build while it is fixed.
Decision¶
- A stored list. Table
quarantined_testin the run-history database (QuarantinedTestinqasight/repository/models.py), one row per cycle and test (id: sha256 of"cycle\ntest"), withreason,owner,added_by,added_at.QuarantineStoreinqasight/services/quarantine_store.py(add,remove,list,rules) followsTriageStore: the table is created on first use. A test is a name or full name as runs record it, or anfnmatchglob, matched exactly like config rules. - Applied in the one engine path.
CycleRunService.runcallsquarantine_for_runbeforerun_plan: the plan's own rules, then the rules of a config cycle with the same name (so an ingest cycle gets them too), then the stored list when the run is persisted. That coversqasight run,qasight ingest, the team server'sPOST /api/v1/ingestand API runs.qasight ingest --serveralready exits with the server'sexit_code, so the server's list decides for CI jobs that upload there, without a second check. The GitHub Action runsqasight runon the job's own history (restored from the cache), so it applies that history's list and the config; a job that should follow a team server's list records withqasight ingest --serverorpytest --qasight-server. The gate itself is unchanged: only exit1is waived, results stay as they are (mute, not skip). pytest --qasight. Inpytest_sessionfinish, when pytest exits1, the plugin reads the session's JUnit XML the wayqasight ingestdoes (import_junit_reports) and runsengine.quarantine.evaluatewith the config rules plus the stored list (this machine's, orGET /api/v1/quarantineon--qasight-server). When every failure matches,session.exitstatusbecomes0and a summary line names the tests.- Auto-release. After a persisted run, next to the issues hook,
release_after_runremoves exact-name entries whose test passed on the first try in each of its lastTESTO_QUARANTINE_RELEASE_AFTERruns (default 10,0off). Skipped and absent runs don't count; a pass after a retry breaks the streak; globs are never released. Best-effort, logged. - API.
GET /api/v1/quarantine?cycle=,PUT /api/v1/quarantine {cycle, test, reason?, owner?},DELETE /api/v1/quarantine?cycle=&test=(qasight_api/routes/quarantine.py). Writes go through the same app-wide guard as triage: the API token, or arunneraccount in accounts mode, whose name is stored inadded_by. The audit middleware records API writes;record_auditadds CLI changes and releases (userqasight, methodRELEASE) in accounts mode./api/v1/analytics/flakyitems carryquarantined(stored list, plus config when the server can read it) andquarantine_entry(the exact stored name the UI can release). - CLI.
qasight quarantine list|add|remove --cycle C [TEST], on this machine's history or--server.qasight flakymarks stored entries and suggestsqasight quarantine add --cycle C "<name>"before the yaml snippet (Q8). - UI.
QuarantineControl(frontend/src/features/quarantine/) on the cycle page's Flaky tests card and on the test page: "Quarantine", or a "Quarantined" badge with "Release" for an exact stored entry. Writes are react-query mutations throughapiClient; a refused write (no token, viewer role) shows the API's message. The static demo shows the badge without buttons.
Options considered¶
- Write the YAML from the UI (the 2026-10-22 note's deferred idea). Rejected: a team server usually has no checkout of each project to write to, and ingest users have no file at all.
- Check the list on the client in
qasight ingest --server(fetch the list, re-evaluate). Not needed: the server already judges the upload with its list and returns the exit code. Onlypytest --qasightjudges locally, because pytest's own exit code is what the job sees. - Deselect quarantined tests. Rejected for the same reason as before: no signal for when they are fixed, and auto-release needs their results.
- Release after N days instead of N runs. Rejected: a nightly cycle and a per-commit cycle would need different numbers of days; N clean runs means the same thing for both, like
TESTO_ISSUES_CLOSE_AFTER.
Consequences¶
- One more table,
quarantined_test, added by Alembic revision0005(which also givestest_resultaretriedcolumn for the "passed on retry" signal); nothing to run, the database upgrades itself on first open. - Every persisted run reads the list once (one indexed query); a database error leaves the run its config rules and one warning line. A
--no-persistrun uses only the config. - The two lists don't merge in either direction:
qasight quarantine listand the API show only stored entries, and a test quarantined byqasight.yamlor a glob shows "Quarantined" without "Release" in the UI. pytest --qasightagainst a server makes one extra request per failing session; if it fails the session keeps exit1and says why.- Tests:
tests/unit/qasight/test_quarantine_store.py(store, merge, the engine path, auto-release, audit),test_pytest_plugin.py(waiver),test_cli_quarantine.py,tests/contract/api/test_quarantine_contract.py(routes, roles, audit, server ingest, flaky items), and the frontend'sFlakyTestsCard,TestPageandQuarantineControltests. - Still open from G2/G3: a quarantined test doesn't open an issue yet, and quarantine has no expiry date.