QA Platform Product Vision
Status: product vision plus first local MVP slice Date: 2026-05-19
Implementation note 2026-05-19
First API slice now exists in the shared QA platform library:
GET /v1/runner/policyreports runner-only mode, the requiredcorbet/unraidresource class, and the dispatchable CI job allowlist.GET /v1/runner/statusis the dispatch gate source of truth. It combines runner inventory, running task count, unclaimed task count, available local capacity, blocked reasons, andcloud_fallback: false.GET /v1/behaviours,GET /v1/behaviours/:id, andGET /v1/behaviours/:id/proofexpose the semantic behaviour index.POST /v1/qa/semantic-indexaccepts a generated wiki/storySpec index and writes behaviours, proof suites, suite-to-behaviour links, and suite-to-test links into D1.GET /v1/suites,GET /v1/suites/:id,POST /v1/suites/:id/plan, andPOST /v1/suites/:id/dispatchexpose test batteries as API resources.qa/api/migrations/0002_qa_semantics.sqladdsbehaviours,behaviours_fts,suites,suite_behaviours, andsuite_tests.qa/api/src/worker.tsseeds CareerVector suite definitions from the current CircleCI lanes, keeping catalog-selected Playwright/Vitest suites visible but modeled as suite witnesses rather than a primary Tests-page dispatch surface.scripts/ci/build-behaviour-index.tsbuilds the reindex payload fromtmp/story-coverage.jsonand, when present,tmp/test-catalog.json. Against the current repo snapshot it produces behaviours, proof suites, suite-to-behaviour links, and suite-to-test links.qa/uinow projects the five MVP tabs in order: Behaviour, Investigate, Queue, Suites, Tests. The header right slot shows the runner gate from/v1/runner/status, including blocked reasons and local capacity. The tabs consume the graph-shaped API surfaces rather than direct test-to-behaviour shortcuts.qa/uinow has behaviour detail pages at/behaviour/:behaviourIdso a human can inspect the behaviour shape and suite-mediated proof tests without using MCP.qa/uinow gives the Investigate tab a deterministic pre-AI loop overPOST /v1/investigate/changes: paste changed paths or a unified diff, get candidate tests plus suite-mediated proof paths, matched workspaces, path tokens, and skipped paths.scripts/ci/list-test-catalog.tsnow preserves e2e compartments as catalog tags instead of leaving that knowledge only in the filesystem. The generated catalog marks Playwright rows withe2epluslayer:l1stories,layer:l2multi-actor,layer:l3persistence,layer:l4MCP contracts,layer:l5perf,layer:l6visual, andlayer:l7journeys where the file path proves it. The Vitest JSON parser also skips structured log lines before the actual list output, so noisy workspaces still contribute catalog rows.GET /v1/tests/facetssummarizes the catalog by workspace, suite, status, tag, and file compartment. The Tests tab uses that API for quick facet chips, facet chips use exacttag=filters, andstatus=never-runmaps to catalog rows whoselast_statusis null.- Suite dispatch, single-job dispatch, and selected-test dispatch now call the
runner gate before creating dispatch records or calling CircleCI. If the
runner is not ready, the API returns
503 runner_not_readywith the full runner status payload. GET /v1/queuenow reconciles accepted dispatch records against CircleCI before responding. The worker reads the pipeline's workflows/jobs, upsertsworkflow_runsandworkflow_jobs, and marks the dispatchcompletedorfailedonce CircleCI reaches a terminal status. This turns manual runner dispatches into the same durable evidence ledger as ingested CI summaries.- Manual, Suite, and Tests dispatch surfaces now show the accepted pipeline and poll queue/dispatch evidence until the matching evidence link is available.
GET /v1/queue/dispatches/:dispatch_idexposes one dispatch audit row and reconciles it before returning when CircleCI data is available. This gives agents a narrow follow-up path after dispatch instead of asking them to scan recent history.- The UI dispatch evidence strip now uses
/queue/evidence?ids=..., a thin SvelteKit route over the targeted/v1/queue/dispatches/:dispatch_idAPI. Recent dispatch polling belongs to/v1/queue/dispatches. - The QA MCP now wraps the new API resources through thin tools:
get_runner_policy,get_runner_status,list_behaviours,inspect_behaviour,list_suites,inspect_suite,plan_suite,dispatch_suite,list_tests,inspect_test,investigate_changes,get_qa_graph,get_qa_neighborhood,get_ci_providers,list_queue,inspect_queue_item,list_queue_dispatches,inspect_queue_dispatch, andget_test_facets.list_testsforwards full-textq, exacttag,suite, andlimitfilters. Queue is the canonical execution surface;dispatch_jobtargets/v1/queue/dispatch.
Validation run for this slice:
bun --filter @cv/health check
bun --filter @cv/health test
bun --filter @cv/qa-platform check
bun --filter @cv/qa-platform test
bun --filter @cv/qa-api check
bun --filter @cv/qa-api test
bun --filter @cv/qa-ui check
bun --filter @cv/qa-ui test
bun --filter @cv/qa-ui build
bun --filter @cv/qa-mcp check
bun --filter @cv/qa-mcp test
bun --filter @cv/scripts-ci check
bun --filter @cv/scripts-ci test
bun scripts/audit-story-coverage.ts --json --allow-known-product-bugs
bun run scripts/ci/list-test-catalog.ts --out=tmp/test-catalog.json
bun --filter @cv/scripts-ci build:behaviour-index -- --coverage=tmp/story-coverage.json --catalog=tmp/test-catalog.json --out=tmp/behaviour-index.json
Local browser smoke:
- local qa-api:
http://127.0.0.1:8790 - local qa-ui:
http://127.0.0.1:5191/or the next Vite port if occupied - qa-ui override:
QA_API_FALLBACK_BASE=http://127.0.0.1:8790 bun --filter @cv/qa-ui dev -- --host 127.0.0.1 --port 5191 - local D1 migrated through
.wrangler-shared/statewith migrations0001_init.sqland0002_qa_semantics.sql - posted
tmp/test-catalog.jsonthrough/v1/ingest/runas a catalog-only local seed: 3,172 catalog rows - posted
tmp/behaviour-index.jsonto/v1/qa/semantic-index: behaviours, proof suites, suite-behaviour links, and suite-test links - checked
/behaviour,/investigate,/queue,/suites,/tests - checked
/behaviour/E7 - verified header tab order:
Behaviour | Investigate | Queue | Suites | Tests - verified runner light renders in the header right slot
- verified runner gate stays visible and expandable at 1440px and 390px widths
- verified local runner status reports
runner blocked,circleci_token_unconfigured, andcloud_fallback:falsewhen no CircleCI token is present - verified local suite dispatch returns
503 runner_not_readybefore CircleCI dispatch when the runner gate is blocked - verified Behaviour renders seeded rows from the local API
- verified behaviour detail renders 3 linked tests and links into the Tests tab
- verified Queue renders the local catalog seed item and its detail route
- verified Suites renders the configured test batteries from the local API
- verified Tests renders generated catalog rows from the local API
- verified the five tabs render without visible API error states
- screenshot:
tmp/qa-ui-behaviour-local.png - screenshot:
tmp/qa-ui-behaviour-detail-local.png - screenshot:
tmp/qa-ui-tests-local.png - screenshot:
tmp/qa-ui-runner-blocked-local.png - screenshot:
tmp/qa-ui-runner-blocked-mobile-local.png
Remaining before first deployed MVP:
- deploy/apply the D1 migration in the QA API environment;
- POST the generated test catalog through
/v1/ingest/run; - POST
tmp/behaviour-index.jsonto/v1/qa/semantic-index; - smoke the live API endpoints after migration;
- browser-smoke the deployed UI.
North star
The QA platform is the agent-facing quality cockpit for CareerVector.
Its primary user is a coding agent trying to answer: "What behavior am I changing, which tests prove that behavior, what is the cheapest useful proof I can run now, and what evidence did the runner return?"
The build order is:
- API first: Hono endpoints and D1 data model are the product contract.
- MCP second: thin tools over the same API, with no separate business logic.
- UI third: human dashboard over the API, using the shared header/footer frame.
The UI has five tabs, in this order:
- Behaviour
- Investigate
- Queue
- Suites
- Tests
The API must make those tabs obvious without coupling itself to tab names. Each tab should be a view over durable QA resources, not a bespoke UI feature.
Hard constraints
- QA-triggered test execution runs only on the approved self-hosted runner
class. The current CI adapter is CircleCI and the current runner class is
corbet/unraid. - Runner offline means QA execution offline. Do not silently fall back to cloud CI resources.
- The default spend posture is zero budget: no paid providers, quota-sensitive APIs, or live external services unless an explicit opt-in exists outside the default path.
- The product already has many tests. The platform should index and route the existing suite before inventing new test conventions.
- Behaviour is currently strongest in wiki story catalogs and weaker in code. The platform should turn that into a first-class searchable model.
Current repo assets
The product is not starting from zero.
Behaviour source
The canonical behaviour material already lives in:
wiki/content/stories/USER-STORIES.mdwiki/content/stories/SYSTEM-STORIES.mdwiki/content/stories/EFFICIENCY-STORIES.mdwiki/content/stories/story-coverage-waivers.mdwiki/content/stories/MISSING-TESTS-HANDOFF.md
These files contain the human-readable behaviour catalog, severity, status, known debt, and the zero-budget policy.
Executable story metadata
ui/e2e/helpers/stories/story.ts already provides a lightweight BDD layer:
storySpec({ id, persona, goal, given, when, then })- canonical story IDs such as
P1-G6-S1,X-G6-S4,ADV-S7,REC-S1,RAD-S1,S1, andE1 - Playwright annotations for
story-id,persona,goal,given,when, andthen[n]
This is the bridge between Behaviour and Tests.
Test taxonomy
wiki/content/runbooks/TESTING-ARCHITECTURE.md defines the semantic layers:
| Layer | Meaning |
|---|---|
| L1 | Story specs |
| L2 | Multi-actor convergence |
| L3 | Persistence and recovery |
| L4 | MCP contracts |
| L5 | Performance budgets |
| L6 | Visual regression |
| L7 | Real-world journeys |
The important principle is that tests are organized by what they prove, not which implementation file they touch.
CI and catalog ingestion
.circleci/config.yml already has useful QA lanes:
fastrunner_smokebrowser_qabrowser_cutoverbrowser_storiesbrowser_contractsbrowser_dashboardbrowser_cvlbrowser_realtimebrowser_smokeplaywright_specsvitest_specsperfsummarize_and_ingest
All current jobs declare resource_class: corbet/unraid, and the config has an
inline policy forbidding cloud fallback without an explicit structural reason.
scripts/ci/list-test-catalog.ts already lists Playwright and Vitest tests
without running them, then emits a catalog that the QA API can ingest.
Existing QA API
The shared Hono app in lib/qa-platform/src/api-core/app.ts already exposes:
- health
- runner health/capacity
- runs
- gates
- tests
- artifacts
- ingest
- dispatch jobs
- dispatch suites
The existing D1 schema in qa/api/migrations/0001_init.sql has the foundation:
workflow_runsworkflow_jobsquality_eventstest_catalogtest_resultsartifactstrigger_requests
The missing piece is not "a QA API". The missing piece is a semantic QA model that links behaviours, suites, test catalog rows, runner policy, and run evidence.
Research notes
BDD shape
Gherkin/Cucumber is the common external model for BDD: Feature, Scenario, Given, When, Then, tags, and domain-language examples. The relevant lesson is not that CareerVector should add Cucumber. The lesson is that behaviour should stay in the language users and domain experts use.
CareerVector's storySpec() already captures the parts we need: Given/When/Then
text, story IDs, persona, goal, and a normal TypeScript/Playwright body. Adding
Cucumber now would create a parallel runtime and another translation layer.
Decision: keep storySpec() as the executable BDD bridge.
Playwright metadata
Playwright supports tags, annotations, runtime annotations, report visibility,
and --grep filtering. This aligns with the current storySpec() approach:
keep behaviour metadata attached to normal tests, then let the QA platform index
it.
Decision: use Playwright annotations and catalog output as first-class metadata. Do not introduce a second test-management metadata format for MVP.
Vitest metadata
Vitest supports listing and JSON-style output/reporters. The repo already uses
vitest list --json in scripts/ci/list-test-catalog.ts.
Decision: keep Vitest as catalog-driven file/title inventory for MVP. Add richer metadata only where a real API selection need appears.
CI provider and runner policy
The current adapter is CircleCI, which uses resource_class to bind jobs to
self-hosted runners. Pipeline parameters can select which workflow path runs
when triggering via API. CircleCI's runner API also exposes runner inventory
plus running and unclaimed task counts per resource class.
Decision: the QA API should never accept arbitrary provider commands. It should
accept behaviour IDs, suite IDs, test IDs, or an allowlisted CI job for the MVP,
then map them to provider parameters whose jobs are known to run on the approved
self-hosted runner class. Before dispatch, the API must prove the runner is
reachable, has no unclaimed backlog, has available local capacity, and that
cloud fallback is disabled. CircleCI is an adapter behind /v1/ci/providers,
not the QA domain model.
D1 search
Cloudflare D1 supports SQLite semantics and the FTS5 extension for full-text search.
Decision: use D1 as the MVP search engine. The corpus is small enough that external search is unnecessary. Add FTS tables for behaviours, tests, suites, and run failure text when the schema grows.
Reporting tools
Allure and ReportPortal are useful references for history, retries, metadata, attachments, and flaky-test views. They are not necessary dependencies for the MVP because the repo already ingests CircleCI summaries, Playwright artifacts, test results, and catalog rows.
Decision: borrow the product concepts, not the stack.
References:
- Playwright annotations and tags: https://playwright.dev/docs/test-annotations
- Cucumber Gherkin reference: https://cucumber.io/docs/gherkin/reference
- CircleCI self-hosted runner overview: https://circleci.com/docs/guides/execution-runner/runner-overview/
- CircleCI runner API: https://circleci.com/docs/guides/execution-runner/runner-api/
- CircleCI resource class overview: https://circleci.com/docs/guides/execution-managed/resource-class-overview/
- CircleCI pipeline parameters: https://circleci.com/docs/pipeline-variables/
- Vitest CLI: https://vitest.dev/guide/cli
- Cloudflare D1 SQL statements and FTS5: https://developers.cloudflare.com/d1/sql-api/sql-statements/
- Allure history and retries: https://allurereport.org/docs/history-and-retries/
- Allure Playwright metadata: https://allurereport.org/docs/playwright/
- ReportPortal Playwright integration: https://reportportal.io/docs/log-data-in-reportportal/test-framework-integration/JavaScript/Playwright
Domain model
Behaviour
A behaviour is a human-readable product promise.
Source examples:
P1-G6-S1: Solo applicant tailors a CV for the first time on a job.S5: Quality service is a product surface.E2: Fast checks stay fast enough for local loops.RAD-S1: Future RADAR story.
Fields:
| Field | Meaning |
|---|---|
behaviour_id |
Canonical story ID |
source |
user-story, system-story, efficiency-story, waiver, manual |
title |
Short human title |
persona |
Optional persona |
goal |
Optional goal |
severity |
Critical, High, Medium, Low, Adversarial |
status |
defined, green, debt, fixme, manual, future, missing |
given |
Plain-language preconditions |
when |
Plain-language action |
then |
Plain-language observations |
search_text |
Indexed combined text |
source_path |
Wiki source path |
Test
A test is an executable proof candidate.
Fields extend the current test_catalog row:
| Field | Meaning |
|---|---|
test_id |
Stable catalog ID |
suite |
playwright or vitest |
workspace |
Workspace/package |
file |
Test file path |
title |
Test title |
layer |
L1-L7 when known |
behaviour_ids |
Linked behaviour IDs |
tags |
Playwright/Vitest tags |
last_status |
Last normalized result |
flake_rate |
Rolling flake signal |
avg_duration_ms |
Runtime estimate |
runner_requirement |
Expected corbet/unraid for QA dispatch |
Suite
A suite is a named battery of tests with intent and cost posture.
Fields:
| Field | Meaning |
|---|---|
suite_id |
Stable ID |
label |
Human/agent name |
intent |
Why this battery exists |
selection_rule |
Static tests, behaviour IDs, tags, paths, or CI job |
budget |
zero-budget, opt-in-paid, manual |
expected_runtime_ms |
Estimate from history |
runner_required |
Always true for QA-triggered CircleCI |
circleci_job |
Optional direct job lane |
Initial suites should mirror existing CI lanes before adding clever selection:
| Suite | Meaning |
|---|---|
runner-smoke |
Prove Unraid runner can claim work |
fast |
Typecheck, story audit, spend sentinel, flake budget, unit/contracts |
qa-core |
Routine browser proof |
story-gate |
Story-focused browser specs |
contract-gate |
Multi-actor, op, persistence contracts |
dashboard-gate |
Dashboard/kanban/views |
cvl-gate |
CVL editor contracts |
realtime-gate |
Realtime/mirror/read-only |
smoke-gate |
Workspace smoke and agent surfaces |
perf-gate |
Performance budgets |
selected-playwright |
Catalog-selected Playwright files |
selected-vitest |
Catalog-selected Vitest files |
release-gate |
Full CI evidence before release |
Run
A run is a concrete execution and its evidence.
Fields already mostly exist in workflow_runs, workflow_jobs,
test_results, quality_events, and artifacts.
The API should normalize all run DTOs to the public schema at the edge. The UI
and MCP should never need to know storage column names like started_at_ms or
job_name.
Plan
A plan is a proposed execution set before dispatch.
The agent asks:
- "Given these changed paths, what should I run?"
- "Given this behaviour ID, what proves it?"
- "Given this failure text, what should I rerun?"
The API answers:
- selected suites
- proof witnesses
- estimated cost/runtime
- runner requirement
- rationale
- skipped or blocked work
Plans can be transient in MVP. Persist them later if they become useful audit artifacts.
API shape
The API should be boring and explicit.
Behaviour
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/behaviours |
Search/list behaviours |
GET |
/v1/behaviours/:id |
Behaviour detail |
GET |
/v1/behaviours/:id/proof |
Suite-mediated proof for this behaviour |
POST |
/v1/qa/semantic-index |
Rebuild from wiki/storySpec metadata |
Tests
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/tests |
Search/list executable tests |
GET |
/v1/tests/facets |
Catalog counts by workspace, suite, status, tag, and file compartment |
GET |
/v1/tests/:id/proof |
Test detail with suite, behaviour, implementation, and recent run evidence |
GET |
/v1/tests/:id/history |
Existing per-test history |
GET |
/v1/tests/:id/flake |
Existing flake signal |
POST |
/v1/investigate/changes |
Change-to-proof graph investigation |
Suites
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/suites |
Search/list batteries |
GET |
/v1/suites/:id |
Suite detail |
GET |
/v1/suites/:id/tests |
Tests in battery |
POST |
/v1/suites/:id/plan |
Dry-run selection and runtime estimate |
POST |
/v1/suites/:id/dispatch |
Dispatch battery |
Queue
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/queue |
Queue list, normalized |
GET |
/v1/queue/:id |
Queue item detail, normalized |
GET |
/v1/queue/:id/evidence |
Provider evidence and QA evidence for one queue item |
POST |
/v1/queue/dispatch |
Dispatch allowlisted queue work through the configured CI adapter |
GET |
/v1/queue/:id/trace |
Axiom/deeplink where configured |
CI providers
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/ci/providers |
Adapter readiness, runner policy, and dispatchable jobs |
Planning
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/plans |
Given paths, behaviours, text, or failure, propose tests/suites |
POST |
/v1/plans/:id/dispatch |
Future persisted-plan dispatch |
Runner
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/runner/health |
Existing live runner light source |
GET |
/v1/runner/capacity |
Existing declared capacity |
GET |
/v1/runner/policy |
Runner-only policy and allowed resource classes |
GET |
/v1/runner/status |
Dispatch gate: inventory, task counts, capacity, and block reasons |
POST |
/v1/runner/smoke |
Dispatch runner_smoke only |
Data model additions
MVP additions to the existing D1 schema:
CREATE TABLE behaviours (
behaviour_id TEXT PRIMARY KEY,
source TEXT NOT NULL,
title TEXT NOT NULL,
persona TEXT,
goal TEXT,
severity TEXT,
status TEXT NOT NULL,
given_text TEXT,
when_text TEXT,
then_json TEXT,
source_path TEXT NOT NULL,
search_text TEXT NOT NULL,
updated_at_ms INTEGER NOT NULL
);
CREATE VIRTUAL TABLE behaviours_fts USING fts5(
behaviour_id UNINDEXED,
title,
persona,
goal,
given_text,
when_text,
then_text,
search_text
);
CREATE TABLE suites (
suite_id TEXT PRIMARY KEY,
label TEXT NOT NULL,
intent TEXT NOT NULL,
budget TEXT NOT NULL,
selection_json TEXT NOT NULL,
circleci_job TEXT,
runner_required INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 0,
updated_at_ms INTEGER NOT NULL
);
CREATE TABLE suite_behaviours (
suite_id TEXT NOT NULL,
behaviour_id TEXT NOT NULL,
source TEXT NOT NULL,
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (suite_id, behaviour_id),
FOREIGN KEY (suite_id) REFERENCES suites(suite_id),
FOREIGN KEY (behaviour_id) REFERENCES behaviours(behaviour_id)
);
CREATE TABLE suite_tests (
suite_id TEXT NOT NULL,
test_id TEXT NOT NULL,
source TEXT NOT NULL,
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (suite_id, test_id),
FOREIGN KEY (suite_id) REFERENCES suites(suite_id),
FOREIGN KEY (test_id) REFERENCES test_catalog(test_id)
);
CREATE TABLE suites (
suite_id TEXT PRIMARY KEY,
label TEXT NOT NULL,
intent TEXT NOT NULL,
budget TEXT NOT NULL,
selection_json TEXT NOT NULL,
circleci_job TEXT,
runner_required INTEGER NOT NULL DEFAULT 1,
enabled INTEGER NOT NULL DEFAULT 1,
updated_at_ms INTEGER NOT NULL
);
CREATE TABLE suite_tests (
suite_id TEXT NOT NULL,
test_id TEXT NOT NULL,
source TEXT NOT NULL,
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (suite_id, test_id),
FOREIGN KEY (suite_id) REFERENCES suites(suite_id),
FOREIGN KEY (test_id) REFERENCES test_catalog(test_id)
);
Later additions:
plansplan_itemsrun_behaviourstest_search_ftsrun_failure_ftsrunner_policy_audits
Dispatch policy
Dispatch must be intentionally narrow.
The API accepts:
- suite IDs
- test IDs
- behaviour IDs
- diff paths
- failure text
The API does not accept:
- arbitrary shell commands
- arbitrary CI job names outside an allowlist
- arbitrary
resource_class - cloud fallback flags
Implementation rules:
- Resolve the request to an allowlisted suite/test plan.
- Assert the target CI job is configured for the approved runner class.
- Assert
/v1/runner/statusis dispatch-ready forcorbet/unraid. - Trigger the configured CI provider with declared adapter parameters only.
- Record a
trigger_requestsrow before the external API call. - Mark dispatch accepted/rejected with provider pipeline metadata.
- Ingest result evidence through the existing ingest path.
Add a CI/API policy check that parses .circleci/config.yml and fails if any
QA-dispatchable job lacks resource_class: corbet/unraid.
UI projection
The UI should use the shared HeaderBar and footer from layout. Page files
remain content-only.
The upper-right header area should show a runner light:
| State | Meaning |
|---|---|
| Green | Runner live, capacity available |
| Yellow | Runner live, saturated or degraded |
| Red | Runner unreachable or API cannot confirm runner |
| Gray | Runner status unknown/not configured |
Tabs:
Behaviour
Human-first search over wiki stories and executable story metadata.
Primary affordances:
- full-text search
- severity/status filters
- coverage status
- linked tests
- latest proving run
- "plan tests for this behaviour"
Investigate
Future joker tab for integrated AI.
MVP should reserve the slot but avoid pretending the hard feature exists. It can show previous investigation plans later, but initially it should point at the API concept: problem text -> plan -> dispatch -> evidence.
Queue
Operational truth.
Primary affordances:
- recent CI/runner runs
- status, branch, commit, duration
- jobs, artifacts, traces
- failed tests and failure text
- linked behaviours and suites when known
Suites
Batteries of tests.
Primary affordances:
- suite intent
- runtime estimate
- budget/spend posture
- runner requirement
- dry-run plan
- dispatch
Tests
Executable library.
Primary affordances:
- full-text search by title/file/tags/story ID
- layer, workspace, suite filters
- history/flakiness
- linked suites and behaviours
- direct dispatch
MVP sequence
Phase 0: stabilize current QA surface
- Normalize API DTOs so UI and MCP consume the same shapes.
- Fix existing route drift between API, UI, and MCP.
- Keep auth out of scope for now.
Phase 1: index behaviours
- Parse wiki story catalogs into
behaviours. - Parse suite-owned selections into
suite_behavioursandsuite_tests. - Add
/v1/behavioursand/v1/behaviours/:id/proof. - Add D1 FTS for behaviour search.
Phase 2: model suites
- Seed suites from existing CircleCI lanes.
- Add
/v1/suitesand/v1/suites/:id/proof. - Add suite dry-run planning.
- Add runner-policy validation for all dispatchable jobs.
Phase 3: planning API
- Add
/v1/investigate/changes. - Support inputs: paths, behaviour IDs, test IDs, suite IDs, free text.
- Return suites and proof witnesses with rationale and runtime estimate.
Phase 4: UI projection
- Replace current QA UI pages with the five-tab frame: Behaviour, Investigate, Queue, Suites, Tests.
- Add runner light to the upper right of the header.
- Keep page-level chrome out of individual pages.
Phase 5: MCP thin wrapper
- Expose MCP tools that map one-to-one to API operations:
list_behaviours,inspect_behaviour,list_suites,inspect_suite,list_tests,inspect_test,investigate_changes,get_ci_providers,list_queue,inspect_queue_item,list_queue_dispatches,inspect_queue_dispatch,dispatch_suite,dispatch_job,get_runner_status, andget_runner_policy. - MCP should not duplicate selection, CI-adapter, or runner-policy logic.
Open decisions
- Behaviour parser: parse markdown tables and story bodies directly, or add a generated JSON artifact committed from a script?
- StorySpec extraction: enhance
list-test-catalog.tsto preserve annotations, or add a separate Playwright metadata extraction step? - Suite ownership: seed suites in SQL migrations, static TypeScript config, or wiki-backed YAML/JSON?
- Runtime estimates: derive from
test_resultsonly, or allow manual estimates until enough history exists? - Investigate tab: visible placeholder in MVP, or hide until the planning API is useful enough to support it?
Working principle
The platform should not ask an agent to remember test folklore.
The agent should ask the API:
- What behaviour is this?
- What proves it?
- What is the cheapest useful proof now?
- Can the Unraid runner run it?
- What happened?
Everything else is an implementation detail.