Repository navigation
Reporters Subsystem Port 2026 07 21
The Run Detail page's "Report Links" section was always empty for every cycle run — GET /api/v1/runs/{run_id}/reports returned static_links: {} unconditionally. Flagged as an open follow-up in both API-Engine Contract Drift Fix - 2026-07-04 and Report Links and Artifacts Missing Fix - 2026-07-06.
-
The reporters subsystem was never merged into
main. It existed only on branchcursor/report-infra-e976a, which had diverged 2,548 files / ~140k lines frommain(mostly committed Allure JSON results and HTML reports, not real source) — a full merge was not viable, sincemainhad 39 independent commits of engine/persistence evolution (repository pattern, CLI-first refactor) the branch never saw.testo_core/reporting/reporters/was an empty directory;run_configured_reporters()didn't exist anywhere. -
A second, deeper gap, found while investigating the first: even with the reporters subsystem present, the Run Detail page's
static_linksis read exclusively fromSTATIC_HISTORY_ROOT/<run_id>/allure_report/index.html(testo_core/run_history.py::list_run_sessions()). Tracing every code path on both branches turned up no writer for that location at all for the modern engine-sourced run flow (testo run --cycle/ the API'scycle_execution_manager.py) — it was leftover plumbing from an older Docker-based headless-runner design (testo_core/runners.py,UQO_AUDIT_RUN_ID) that was never wired to the currentrun_plan()engine. The only thing that did write nearstatic/was the unrelated legacysync_all_reports_to_static()(used by the deprecated Streamlit UI), which mirrors intostatic/allure_reports/— no run id, incompatible layout.
Porting the reporters subsystem alone would have generated HTML at <artifacts_root>/allure-report/, which static_links never looks at. Both gaps had to be closed together.
Pure copy, zero conflicts: testo_core/reporting/reporters/{__init__,base,factory,orchestrate,allure_reporter,extent_reporter,extent_builder,reportportal_reporter,reportportal_client,testbeats_reporter,testbeats_payloads}.py + templates/extent_dashboard.html.j2, plus testo_core/reporting/{allure_results,allure_delta_transform,allure_history_serve,allure_summary_widgets,pyramid_viz}.py.
Hand-reconciled (existed on both branches, main had evolved independently since the fork):
-
collector.py— appendedcollect_results_docker_run()(additive only). -
history_inject.py—try_inject_prior_history()gained atrend_depth: int = 1param (behavior-preserving at the default). -
allure.py— rewritten to delegate toallure_cli.py(the project's actual Allure-3-via-npm resolution, already used everywhere else) instead of a stale bareshutil.which("allure")lookup. Not optional cleanup — the portedAllureReporterimportsgenerate_html/AllureCLINotFoundErrorfrom this module, and the old version would have silently used the wrong Allure resolution strategy. -
entry.py,exporter.py,paths.py,server.py— left untouched.main's independent versions already satisfy everything the ported code calls into; the cursor branch's diffs forpaths.py(deletes thesafe_child_path()traversal guard) andserver.py(a net regression vs.main's rewrite) were deliberately not ported.
New config surface: testo_core/config/schema.py gained SUPPORTED_REPORTER_TYPES, ReporterSpec, and TestosteroneConfig.reporters. testo_core/config/loader.py gained _parse_reporters(), wired into both _build_cycle_config() and _build_legacy_runs_config(). pyproject.toml gained Jinja2>=3.1.0 (required by extent_builder.py's Jinja templating). testosterone.yaml gained a top-level reporters: block (currently - type: allure).
run_plan() (testo_core/engine/orchestrator.py) already called composite_backend(...).persist(plan_result) as its last step, and DbBackend.persist() already called repo.create_run(...) — which returns the created RunRecord (with id assigned, and metadata["run_id"] set to it before insert). Both return values were being discarded.
Fix: DbBackend.persist() and _CompositeBackend.persist() now return the persisted run id (str | None); run_plan() captures it and attaches it to the (previously unused) PlanResult.extra["run_id"] field via dataclasses.replace().
Rather than generating Allure HTML to <artifacts_root>/allure-report/ and then copying/mirroring it into STATIC_HISTORY_ROOT/<run_id>/allure_report/ afterward, the wiring computes that destination path up front and passes it as out_dir straight into run_configured_reporters() / AllureReporter.publish() — both already accept an explicit out_dir and write directly there when given. One I/O pass, no separate mirror step, no second failure mode.
Wiring is a single shared helper, not duplicated logic: _maybe_run_configured_reporters() lives in testo_core/cli/runner.py next to the existing _maybe_archive_cycle_report() (the analogous archive-to-DB hook), and both runner.py's own call site and testo_api/cycle_execution_manager.py (which already imports _maybe_archive_cycle_report from runner.py for the sibling step) call into it.
- Full test suite: 558 passed, 4 skipped (up from 549 passed pre-change; added 9 new tests covering
reporters:YAML parsing,DbBackend/composite return-value propagation, andPlanResult.extra["run_id"]). -
testo run --cycle sample-pytestsend-to-end: CLI printedReporter: Allure HTML at .../static/history/<run_id>/allure_report/index.html; confirmed the file exists on disk; confirmedGET /api/v1/runs/<run_id>/reportsreturns"static_links": {"pytest": "history/<run_id>/allure_report/index.html"}via a liveuvicornprocess. - Repeated via the API-triggered path (
POST /api/v1/cycles/sample-pytests/executions) — same result, confirmingcycle_execution_manager.py's wiring matches the CLI's. -
.gitignoregainedstatic/history/(sibling to the existingstatic/allure_report/etc. entries) — this path didn't exist before this change, so nothing had ignored it yet.
Resolved same day — see "Follow-up: per-framework links, 404 fix, Extent scoping" below.testo_api/main.py mounts no StaticFiles route anywhere...
Live-tested sample-all-frameworks-stochastic through the React dashboard (3 stages: pytest, native Behave, BehaveX) and found the "Resolved 2026-07-21" state above still only ever surfaced a single link labeled "pytest", regardless of which frameworks actually ran, and clicking it 404'd. Three more root causes, on top of the already-tracked static-mount gap:
-
Every cycle run was mislabeled "pytest".
_maybe_run_configured_reporters()(testo_core/cli/runner.py) pointed Allure'sout_dirat one unifiedSTATIC_HISTORY_ROOT/<run_id>/allure_report/(singular) merging all stages together.list_run_sessions()only recognized that singular path via its back-compat branch, which unconditionally maps it tolinks["pytest"]— a behave/behavex cycle got the same misleading label. -
Extent could never be linked, and clobbered across runs.
ExtentReporter.publish()ignoredcontext.out_direntirely, always writing to the shared<artifacts_root>/reports/extent/— not run-scoped, and not underSTATIC_HISTORY_ROOT, so no run's Extent output was ever individually addressable. -
Hardcoded framework taxonomy. Even the existing per-framework "new layout" branch only checked a fixed tuple
("pytest", "behavex", "behave_native")— but the native Behave adapter (testo_core/frameworks/behave_adapter.py) names its results subdir"behave", not"behave_native", so it could never have matched.
-
ReportContext(reporters/base.py) gained arun_report_root: Path | Nonefield — the per-run base dir each reporter derives its own subdir from, alongside (not replacing) the existing single-purposeout_dir. -
AllureReporter.publish()now generates one Allure report per framework (reusing the already-present-but-previously-unusedCollectedResults.by_framework) underrun_report_root/allure_reports/<framework>/index.html, whenrun_report_rootis set and no explicitout_diris given. The CLI's single-merged-report path (testo report, explicitout_dir) is unchanged. -
ExtentReporter.publish()now defaults torun_report_root/extent_report/when set, instead of the shared unscoped path. -
orchestrate.py/cli/runner.pythreadrun_report_root = STATIC_HISTORY_ROOT/<run_id>through instead of the old singularout_dir. -
run_history.py'slist_run_sessions()now scansallure_reports/dynamically (link key = actual subdir name) instead of a hardcoded 3-key tuple, fixing thebehave/behave_nativemismatch for free, plus checksextent_report/index.html→links["extent"]. The old singular back-compat branch is untouched, so already-persisted historical runs keep their existing (if mislabeled) link. -
testo_api/main.pynow mountsapp.mount("/history", StaticFiles(directory=STATIC_HISTORY_ROOT), name="history")(creating the directory first, since it's.gitignore'd and may not exist on a fresh checkout). -
frontend/src/lib/api-client.tsexportsAPI_BASE;RunDetailPage.tsx's Report Links card prefixes each href with it.
ReportPortal (remote launch URL, no local HTML) and TestBeats (webhook + JSON summary, no HTML index) don't fit the index.html link-card pattern and weren't touched.
Live-checking the "behavex" link above showed Allure's UI rendering BehaveX's results — accurate, but not BehaveX's own report. BehaveX runs with its own -o <dir> (behavex_adapter.py) producing a separate, self-contained native HTML report (behave_reports/report.html, its own "Test Execution Report" UI with screenshots/tags/retry detail) that was never linked anywhere, before or after the fix above.
Considered a behavex-specific branch in run_history.py (fast) vs. a generic adapter capability (more upfront work, but avoids repeating the exact hardcoded-per-framework mistake just fixed for the next framework that ships its own native report). Went generic:
-
testo_core/frameworks/base.py: newNativeReport(root_dir, entry_relpath)dataclass;FrameworkAdapterprotocol gainsnative_report(stage_dir) -> NativeReport | None. -
PytestAdapter/BehaveAdapter.native_report()returnNone(no native report format);BehaveXAdapter.native_report()returnsNativeReport(stage_dir/"behave_reports", "report.html")when that file exists. -
testo_core/cli/runner.py: new_maybe_snapshot_native_reports(), called unconditionally (unlike the reporters hook — this just copies an artifact the framework already wrote, noreporters:config or Allure CLI involved) from both the CLI and API call sites. For each stage, asks its adapter fornative_report()and — if present —shutil.copytrees the whole bundle toSTATIC_HISTORY_ROOT/<run_id>/native_reports/<framework>/, then derives anindex.htmlfromentry_relpathif the names differ. -
run_history.py: same dynamic-scan pattern asallure_reports/— anynative_reports/<framework>/index.htmlbecomeslinks[f"{framework}-native"](suffix avoids colliding with the Allure-rendered key of the same framework name).
Verified live: ran sample-all-frameworks-stochastic, Run Detail page showed behave, behavex, pytest, behavex-native side by side; opening behavex-native rendered BehaveX's actual branded report (title "Test Execution Report", working Filters/Metrics UI, correct relative CSS/JS from the copied outputs/ bundle) — not Allure's. Full suite: 565 passed (558 + 7 new), 4 skipped, same pre-existing baseline, zero regressions.
Generated from docs/Archive/Reporters Subsystem Port - 2026-07-21.md. Edit it there; changes made in the wiki are overwritten.
Architecture
CLI Commands
Processes & Guides
- CI-CD Pipeline Setup
- E2E Harness Operations Guide
- GitLab Pages Demo
- Publishing Docker Images
- Publishing to Artifactory
- Publishing to PyPI
Prompts & Snippets
Roadmap & Strategy
Specs & ADRs
- Architecture Consolidation - 2026-10-06
- BehaveX Reports - 2026-10-21
- Command Adapter and JUnit Import - 2026-09-25
- Delta Comparison Policy
- Docs Site - 2026-10-19
- Docs Vault and Wiki Sync - 2026-10-07
- GitHub Action PR Comment - 2026-10-19
- Interview Readiness Fixes - 2026-10-07
- Library Packaging
- Open Source Features - 2026-10-19
- Phase 3 Delta Comparison Engine Plan
- Phase 3 Unified Dashboard Plan
- Phase 4 BYOK and Failure Analysis
- Phase 5 UI Redesign - Cycles-First Navigation
- Playwright Adapter - 2026-10-19
- Plugin API - 2026-10-19
- Specs & ADRs
- Repository Pattern - Database-Agnostic Refactor
- UI Restyle - 2026-10-19
Testing Workflows
Vault