Test Framework Consolidation Plan
Audience: test-automation and platform engineers consolidating the Stella Ops test estate. Purpose: turn the current test inventory into a consistent, predictable, manifest-driven framework — one source of truth for which suites run, where, and when.
The companion documents are TEST_MANIFEST.md (the implemented manifest contract), TEST_CI_ROUTING.md (trigger placement), and TEST_CI_EVIDENCE_DASHBOARD.md (the CI evidence standard).
Snapshot date: 2026-05-22 UTC
Root-tree consolidation (2026-05-30): the operator overrode the earlier “keep production/operator harnesses at the repo root” stance. As of 2026-05-30 there are no test code, fixture, or corpus directories at the repo root. Everything cross-cutting now lives under
src/__Tests/(e.g. the E2E analyzer corpus atsrc/__Tests/e2e/scanner-analyzer-corpus/, operator Playwright atsrc/__Tests/playwright/, reachability/fixtures/supply-chain undersrc/__Tests/, and shared seed data undersrc/__Tests/__Datasets/); module-owned suites live undersrc/<Module>/__Tests/; and former rootsamples/moved todocs/samples/. Tables and prose below that still describetests/**at the root are historical baseline; the current standard is thesrc/__Tests/layout. See sprintSPRINT_20260530_008_Tests_root_data_consolidation.md.
This plan turns the current test inventory into a consistent, predictable, and durable test framework. It is based on:
docs/technical/testing/TEST_PROJECT_CI_INVENTORY.mddocs/technical/testing/TEST_SUITE_CONSOLIDATION_DEEP_DIVE.mddocs/technical/testing/test-project-ci-inventory.csvdocs/technical/testing/test-suite-layer-inventory.csvdocs/technical/testing/test-fixture-corpus-inventory.csvdocs/technical/testing/test-e2e-scenario-inventory.csv
Current baseline:
- 666 project/runner/corpus rows.
- 597 runnable suite rows.
- 198 fixture/corpus roots.
- 364 browser E2E and Scanner SCM scenario rows.
- 368 high-value suites are not currently blocking CI.
- 23 runnable rows have no active CI mapping.
- 147 .NET suites have no static
Categorytrait. - 10 suites contain 34 static
Skip =declarations.
Target State
Testing must become manifest-driven, not filename/glob-driven.
The target system has:
- A versioned test manifest that is the source of truth for runnable suites, seed/fixture roots, CI placement, ownership, runtime cost, and gating state.
- A change-impact planner that maps each commit to the smallest correct test plan and prints why each suite was selected or skipped.
- Common project and seed-data structure across modules.
- One canonical trait taxonomy enforced by tooling.
- CI lanes separated by trigger type: per commit/PR, merge to main, nightly, weekly/release, and manual/live.
- Explicit handling for expensive, live, hardware, Docker-heavy, and demo/sample test surfaces so they are visible but do not over-trigger.
- Stable result artifacts and evidence metadata for every CI run.
Non-Goals
- Do not make every test PR-blocking.
- Do not make docs/demo/sample changes trigger product-wide test suites.
- Do not rely on
*.Tests.csprojnaming alone for discovery. - Do not hide manual/live tests because they are expensive.
- Do not treat static inventory counts as behavioral verification.
Operating Decisions
- Primary .NET filter key:
Category.run-test-category.shalready filters onCategory=<value>, andStellaOps.TestKit.TestCategoriesalready defines the main taxonomy. Lanebecomes a CI scheduling concept, not the primary test filter key. ExistingLanetraits are compatibility input during migration.- Every suite must have a manifest row before it can be relied on by CI.
- Every fixture/corpus root must have an owner, refresh policy, and offline determinism policy.
- Every CI workflow must declare whether failures are blocking, report-only, or manual-only.
- The planner output is an artifact. CI must publish the selected suites and the skip reasons for non-selected suites.
Common Tooling
Create these durable tools and artifacts:
| Artifact | Path | Purpose |
|---|---|---|
| Test manifest | docs/technical/testing/TEST_MANIFEST.yml | Versioned source of truth for suites, fixtures, triggers, owners, and gating. |
| Manifest schema | docs/technical/testing/TEST_MANIFEST.schema.json | Validates manifest structure in CI. |
| Fixture manifest schema | docs/technical/testing/FIXTURE_MANIFEST.schema.json | Validates fixture/corpus metadata. |
| Manifest linter | .gitea/scripts/test/validate-test-manifest.* | Fails when suites, fixtures, or traits drift from manifest rules. |
| Change planner | .gitea/scripts/test/plan-test-run.* | Converts changed files into a minimal test plan. |
| Plan runner | .gitea/scripts/test/run-test-plan.* | Executes manifest-selected suites across .NET, npm, Playwright, Go, and tooling runners. |
| Result normalizer | .gitea/scripts/test/collect-test-results.* | Emits stable JSON summary, TRX/JUnit links, timing, failures, and artifacts. |
| Inventory refresh | .gitea/scripts/test/refresh-test-inventory.* | Regenerates inventory appendices and compares them with the manifest. |
The scripts can be shell, PowerShell, or Python, but the interface must be stable and documented. CI should invoke the same entry points developers use locally.
Manifest Model
Each suite row should include:
suites:
- id: scanner.reachability.unit
owner: Scanner
path: src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/StellaOps.Scanner.Reachability.Tests.csproj
runner: dotnet
category: Integration
layers: [integration, golden, determinism]
scenarioFamilies: [scanner/reachability/vex]
affectedBy:
paths:
- src/Scanner/**
- src/__Libraries/StellaOps.Reachability*/**
- src/__Tests/e2e/scanner-analyzer-corpus/**
fixtures:
- scanner-analyzer-corpus
runtime:
tier: pr-targeted
cost: medium
infra: [postgres]
timeoutMinutes: 15
ci:
pr: targeted-blocking
main: targeted-blocking
nightly: full-blocking
manual: allowed
evidence:
resultFormat: trx
artifacts:
- TestResults/**
Each fixture/corpus row should include:
fixtures:
- id: scanner-analyzer-corpus
owner: Scanner
path: src/__Tests/e2e/scanner-analyzer-corpus/fixtures
type: scm-corpus
offline: true
refreshCommand: devops/scripts/e2e/provision-corpus.ps1 -Seed 42
consumers:
- scanner.e2e.analyzer-coverage
- scanner.reachability.unit
updatePolicy: explicit-review
expectedHashPolicy: manifest-recorded
Common Project Structure
Standardize project placement as follows:
| Surface | Preferred location | Notes |
|---|---|---|
| Module unit/component tests | src/<Module>/__Tests/<Project>.Tests/ | Default for module-owned tests. |
| Module integration tests | src/<Module>/__Tests/<Project>.IntegrationTests/ | Uses Testcontainers or WebApplicationFactory. |
| Module contract/security tests | `src/ | Security>.Tests/` |
| Shared test libraries | src/__Libraries/** and src/__Libraries/__Tests/** | Must be manifest-owned; changes are broad but should not always run every suite. |
| Cross-module integration/E2E | src/__Tests/Integration/** and src/__Tests/e2e/** | Explicit manifest rows; no implicit glob-only discovery. |
| Production/operator Playwright | src/__Tests/playwright/** | Live-stack/operator scenarios, not Web unit E2E. |
| Web Playwright/Vitest | src/Web/StellaOps.Web/** | Split Vitest, source-served Playwright, docker/live Playwright, and air-gap UI specs into separate manifest rows. |
| Docs sample/template tests | docs/**/samples/** or docs/dev/**/plugin-templates/** | Must be sample lane and only triggered by sample/template changes. |
Rules:
- Every runnable test project must end in
.Tests.csproj,.IntegrationTests.csproj,.E2ETests.csproj,.Benchmarks.csproj, or be explicitly registered as an exception in the manifest. - Test support libraries must not look like runnable test projects.
- There is no top-level
tests/**tree. Cross-product, production/operator, external-harness, and generated-corpus verification all live undersrc/__Tests/**(consolidated 2026-05-30); module-owned suites live undersrc/<Module>/__Tests/**. - New test projects must reference or justify not using
StellaOps.TestKit. - New suites require a manifest row in the same change.
Common Seed and Fixture Structure
Standardize fixture roots by intent:
| Directory | Use |
|---|---|
Fixtures/ | Small hand-authored inputs used by one suite. |
Seeds/ | Deterministic data loaded into a store or service. |
Golden/ | Expected output files reviewed as contract fixtures. |
Snapshots/ or __snapshots__/ | Snapshot framework output. |
Corpus/ | Larger multi-case fixture collections. |
Generated/ | Generated but committed only when intentionally reviewed. |
Every non-trivial fixture root needs a local fixtures.manifest.yml:
id: concelier-nvd-fixtures
owner: Concelier
type: connector-fixture
offline: true
source: frozen-upstream-sample
refreshCommand: src/Concelier/devops/refresh-nvd-fixtures.ps1
updatePolicy: explicit-review
consumers:
- concelier.connector.nvd.tests
expectedHashPolicy: manifest-recorded
Rules:
- Fixture refresh must be explicit, never accidental during normal tests.
- Live/upstream refresh tests are
Liveand never PR-blocking. - Golden updates require a named env var or command, plus review of the diff.
- Generated corpuses must have a seed, generator version, and ignored output policy when they are too large to commit.
- License or provenance notes are required when fixture data is copied from upstream sources.
Trait and Category Taxonomy
Every .NET test class or method must have exactly one primary Category unless the manifest records a temporary exception.
Canonical primary categories:
| Category | Trigger class | Meaning |
|---|---|---|
Unit | PR targeted | No network, no database, deterministic, fast. |
Contract | PR targeted | Schema/API/serialization contract. |
Architecture | PR targeted | Layering, dependency, naming, API conformance. |
Integration | PR targeted or main/nightly | Testcontainers, WebApplicationFactory, service/storage flows. |
Security | PR targeted | AuthN/AuthZ, crypto, injection, validation, negative cases. |
Golden | PR targeted or nightly | Frozen expected-output regression. |
Determinism | PR targeted or nightly | Repeatability, canonical ordering, replay, clocks. |
E2E | main/nightly/manual unless fast smoke | Full workflow or multi-service behavior. |
AirGap | main/nightly/manual | Offline behavior and bundle/federation checks. |
Performance | nightly/weekly/manual | Threshold-based performance smoke. |
Benchmark | weekly/manual | BenchmarkDotNet or long measurement suites. |
Chaos | nightly/manual | Fault injection and recovery. |
Interop | nightly/release/manual | External tool or version compatibility. |
Live | manual/scheduled only | Real upstream services or credentials. |
PostIncident | PR/main/release by severity | Regression from a named incident. |
Secondary traits:
| Trait | Required when |
|---|---|
Owner | All tests after migration. |
Intent | Safety, regulatory, operational, determinism, performance, or UX-critical behavior. |
Infra | Any test needing Postgres, Valkey, Docker, browser, file system, network, or hardware. |
Fixture | Any test consuming a registered fixture/corpus. |
Requirement | Contract, compliance, release, or post-incident tests. |
BlastRadius | Cross-cutting tests touching auth, API, persistence, evidence, policy, scanner, or release surfaces. |
Skip rules:
- No indefinite
Skip =without a sprint/task reference and reason. - Live tests should use opt-in guards, not static skips.
- Quarantined flaky tests need expiry and owner.
CI Placement
Per Commit / Pull Request
Purpose: fast, correct feedback for the changed surface.
Always run:
- Manifest/schema validation.
- Change-impact planner.
- Test project discovery drift check.
- Affected build graph compile.
- Unit/contract/security/static tests selected by the planner.
- Fixture manifest validation when fixtures changed.
Conditionally run:
- Targeted integration tests when code, migrations, persistence, API, or Testcontainers-backed suites are affected.
- Web Vitest changed-spec drift for Web changes.
- Small Playwright smoke only when Web route/navigation/critical UX paths or Playwright specs changed.
- Docs sample/template tests only when those samples/templates changed.
- Runner self-tests when
.gitea/scripts/test/**or test workflow files change.
Do not run automatically on every commit:
- Full generic .NET sweep.
- Full production/operator Playwright.
- Full Scanner generated SCM corpus.
- Long benchmarks.
- Live upstream connector tests.
- Hardware or OS-confinement tests.
Merge To Main
Purpose: catch integration misses after merge without making every PR pay the full cost.
Run after push to main:
- The same planner against the merged diff.
- Expanded targeted matrix for affected modules.
- Changed fixture consumers.
- Architecture and OpenAPI conformance.
- Security and determinism suites touched by the merged change.
- Any PR report-only suite that was selected by the planner.
Main-merge failures should block release promotion and produce an owner-routed failure artifact. They should not silently disappear into report-only logs.
Nightly
Purpose: complete regression and expensive deterministic coverage.
Run:
- Full manifest sweep, sharded by runtime cost and infra class.
- Cross-cutting
src/__Tests/Integration/**andsrc/__Tests/e2e/**. - Scanner SCM analyzer coverage against the deterministic local Gitea target.
- Production/operator Playwright against the controlled live stack.
- Air-gap, offline, chaos, determinism, golden, and interop suites.
- Fixture drift and manifest drift checks.
- Performance smoke with thresholds.
Nightly should publish full coverage evidence and open/refresh failure tracking, but only suites marked nightly: blocking should block release readiness.
Weekly / Release
Purpose: long, costly, or environment-dependent validation.
Run:
- BenchmarkDotNet suites.
- Long load/performance suites.
- External live connector smoke.
- OS confinement and hardware-backed crypto/provider tests.
- Compatibility matrices that are too expensive for nightly.
Manual
Purpose: controlled investigation, live target validation, and ad hoc release evidence.
Manual workflows must still consume the manifest so their scope, owners, and artifacts are visible.
Change-Impact Planner
The planner replaces broad implicit triggering with an auditable decision.
Inputs:
- Changed files.
- Test manifest.
- Fixture manifest.
- Dependency map from project references and package references.
- Optional prior failure state.
Outputs:
test-plan.jsontest-plan.md- selected suites with trigger reason
- skipped suites with skip reason
- required runners and infra
- estimated runtime and shard plan
- gating mode per suite
Examples:
| Change | Expected plan |
|---|---|
docs/** prose only | Docs checks only; no product test sweep. |
docs/dev/**/plugin-templates/** | Sample/template tests only. |
src/Web/StellaOps.Web/src/app/features/findings/** | Web unit/Vitest affected specs; targeted Playwright smoke if route/user workflow changed. |
src/Scanner/** analyzer code | Scanner unit/integration/golden/reachability suites; Scanner fixture consumers. |
src/__Tests/e2e/scanner-analyzer-corpus/** | Scanner analyzer coverage and direct corpus consumers only. |
src/__Libraries/StellaOps.TestKit/** | TestKit tests plus manifest-derived consumers; PR uses representative blocking slice, nightly runs full consumer sweep. |
global.json, Directory.*.props, run-test-plan.* | Planner self-tests, representative compile/test slices, and nightly full sweep. |
.gitea/workflows/** | Workflow syntax/plan dry-run; affected runner self-tests. |
The planner must avoid the current binary affects_all behavior where every shared change either fans out too much or defers too much to nightly.
CI Enablement Priorities
- Register all 23
ci-gaprows in the manifest. - Split Web into separate rows: Vitest, source-served Playwright, docker/live Playwright, live-search Playwright, and Air-gap notification UI specs.
- Add production/operator Playwright under
src/__Tests/playwrightas nightly/manual first, with PR smoke only for changes tosrc/__Tests/playwright/**or shared UI routes it exercises. - Add explicit rows for
src/__Tests/e2e/**andsrc/__Tests/Integration/**. - Keep former top-level
tests/Telemetryrows under owning module test roots; the P0 Telemetry metrics coverage now lives in the module-owned Telemetry core test project, and the Doctor plugin storage project has been moved undersrc/Doctor/__Tests. - Add BinaryIndex benchmark rows as weekly/manual, not PR.
- Decide whether docs sample/template tests are real runnable examples or excluded sample artifacts. Until then, trigger them only on sample/template path changes.
- Convert current report-only .NET PR fanout to targeted non-blocking, then to blocking by layer once trait coverage and planner accuracy are proven.
Phased Rollout
Phase 1 - Manifest Foundation
Deliver:
TEST_MANIFEST.yml- schema validation
- fixture manifest schema
- manifest linter
- generated inventory comparison
Exit criteria:
- All 597 runnable suite rows are represented or explicitly excluded.
- All 198 fixture/corpus roots are represented or explicitly excluded.
- CI fails if new runnable suites or fixture roots appear without manifest updates.
Phase 2 - Change Planner
Deliver:
plan-test-runtool.- Markdown and JSON plan artifacts.
- Planner dry-run job for PRs.
- Rules for docs, samples, fixtures, module code, shared libraries, workflows, and global build files.
Exit criteria:
- A docs-only change selects no product suites.
- A docs sample change selects only sample/template checks.
- A module change selects only manifest-related module suites and required shared dependencies.
- Shared-library changes produce a representative PR slice and a full nightly follow-up plan.
Phase 3 - Trait and Structure Migration
Deliver:
- Reconciled docs for
Category,Lane, and TestKit traits. - Category coverage for the 147 trait-missing .NET rows.
- Skip audit closure for the 10 suites with static skips.
- Project structure exceptions recorded in the manifest.
Exit criteria:
run-test-category.shcan use trait filters for migrated suites.- No new test project can merge without Category/owner metadata.
- Static skips require owner, reason, sprint/task reference, and expiry.
Phase 4 - CI Enablement
Deliver:
- PR targeted workflow consuming
test-plan.json. - Main-merge workflow consuming
test-plan.json. - Nightly full manifest sweep, sharded by runtime/infra.
- Manual/live workflows consuming manifest rows.
- Result normalization and artifact naming.
Exit criteria:
- The 23
ci-gaprows are no longer invisible. - High-value suites move from report-only to explicit
blocking,release-blocking,nightly-blocking, ormanual-only. - No workflow relies on implicit glob discovery as the primary source of truth.
Phase 5 - Fixture and Corpus Governance
Deliver:
fixtures.manifest.ymlfiles for major fixture roots.- Refresh commands for Scanner SCM corpus, reachability fixtures, Concelier connectors, docs/QA fixtures, and major golden corpuses.
- Hash or snapshot policy for deterministic fixtures.
- Live refresh lanes separated from offline deterministic regression lanes.
Exit criteria:
- Fixture changes route to consuming suites only.
- Corpus refresh changes are reviewable and reproducible.
- Large generated corpuses remain deterministic without being accidentally committed or accidentally skipped.
Phase 6 - Gate Hardening
Deliver:
- Blocking policy per suite and per trigger.
- Runtime budgets and shard definitions.
- Failure owner routing.
- Quarantine process with expiry.
- Release-readiness summary from PR, main, nightly, and release lanes.
Exit criteria:
- Release readiness cannot claim green status while selected blocking suites are failing or skipped.
- Report-only suites are visible and intentionally classified.
- CI runtime stays bounded because the planner avoids unrelated full-suite reruns.
Consistency Rules
- New test suite without manifest row: fail.
- New fixture root without fixture manifest: fail.
- New
[Fact]/[Theory]withoutCategory: fail after migration window. Livetest in PR plan: fail unless explicitly manual-only.- New static
Skip =without task reference and expiry: fail. - Test project under
docs/**or sample/template path cannot trigger product suites unless manifest explicitly declares a product dependency. - CI workflow that runs tests without publishing
test-planand result summary: fail once migration is complete.
Open Decisions
- Whether production/operator Playwright should become nightly-blocking or release-blocking after stabilization.
- Which high-value family moves to blocking first: cross-cutting integration/E2E, Scanner reachability/VEX, Concelier connector fixtures, or security/identity.
- Whether docs sample/template projects should stay runnable or be converted to static compile-only examples.
- Whether
Lanetraits should be mechanically removed afterCategorycoverage is complete, or retained as aliases for older docs.
Recommended First Sprint Order
- Manifest foundation and linter.
- Change planner dry-run.
- CI-gap registration.
- Web runner split.
- Trait migration for the largest/highest-value suites.
- Fixture manifest rollout for Scanner, Concelier, and cross-cutting
__Tests. - Gate hardening from report-only to blocking by proven lane.
Implementation State
The first implementation slice created the manifest foundation:
TEST_MANIFEST.ymlseeded from the suite, fixture, and E2E inventories.TEST_MANIFEST.schema.jsonandFIXTURE_MANIFEST.schema.json.refresh-test-manifest.py,validate-test-manifest.py,plan-test-run.py, andrun-test-plan.py..gitea/workflows/test-manifest-plan.ymlfor manifest/planner CI validation once pushed.
Current local evidence is recorded in TEST_MANIFEST.md and docs/implplan/SPRINT_20260522_008_DOCS_testing_framework_consolidation.md.
