recon_gen._dev.runner
Test layer chain runner — orchestrates the layered test chain with per-run output isolation + timing-diff drift detection.
Invoked via the ./run_tests.sh bash shim at repo root; the shim
exec``s into ``python -m recon_gen._dev.runner.
- Verbs:
- up_to <layer> Run the chain up to and including <layer>.
Layers: unit | db | app2 | deploy | qs_api | qs_browser (pyright folds into unit via the conftest sessionstart gate).
unitis variant-independent — it runs ONCE as a prelude before the matrix fans out (Y.2.gate.n), not once per cell. Equivalent forms:up_to=<layer>andup_to <layer>.
up [scope] Boot dependencies. scope = local | aws | all (default). down [scope] Tear down dependencies. scope as above. status [–cost] Show what’s currently running. sweep Clean orphan resources (tagged ManagedBy:recon-gen).
- Exit codes:
0 success 1 test failure (one or more layers / variants failed) 2 needs-operator (expired creds, dirty deploy refused, missing cfg, etc.) 3 config / argument error
Substrate: pytest-as-orchestrator + this thin Python wrapper. See
docs/audits/y_2_gate_b_0_runner_lang_spike.md for the design lock.
Functions
|
Y.2.gate.c.5 — return the slice of LAYERS from start through |
|
Tear down dependencies. |
|
Surface failing-layer assertions + missing-capture warnings from the most-recent run dir. |
|
Y.2.gate.b.14 — run pyright directly for fast type-check iteration. |
|
Show what's currently running. |
|
Y.2.gate.c.9 — clean orphan QuickSight resources tagged |
|
Boot dependencies. |
|
Run the test chain up to and including the named layer. |
|
Y.2.gate.c.2 — write |
|
Y.2.gate.c.3 — diff per-layer durations between two timings.json blobs. |
Y.2.gate.c.1 — <utc-ts>-<short-sha>[-dirty]. |
|
|
Y.2.gate.c.5 — run one layer; return its result. |
|
Y.2.gate.c.3 — pick the best prior run for drift comparison. |
|
Y.2.gate.b.8.impl — True iff |
|
|
|
Y.2.gate.c.8 — probe every dep |
|
Y.2.gate.c.4 — keep the most-recent |
|
Y.2.gate.c.3 — find prior run, compute drift, print report. |
Y.2.gate.c.6.xdist-safety — resolve the seed for this runner invocation. |
|
|
Y.2.gate.b.8.impl — record that |
Classes
|
Y.2.gate.c.3 — one layer's drift vs the prior run. |
|
Y.2.gate.c.5 — outcome of dispatching one layer. |
|
Y.2.gate.c.8 — a single missing or broken dependency. |
|
Y.2.gate.c.7 — operator-supplied flags threaded through dispatch. |
- class recon_gen._dev.runner.DriftEntry(layer, current_seconds, prior_seconds)[source]
Bases:
objectY.2.gate.c.3 — one layer’s drift vs the prior run.
- Parameters:
layer (str)
current_seconds (float)
prior_seconds (float | None)
- current_seconds: float
- property delta_pct: float | None
- property is_drift: bool
- layer: str
- prior_seconds: float | None
- class recon_gen._dev.runner.LayerResult(layer, exit_code, duration_seconds, skipped=False)[source]
Bases:
objectY.2.gate.c.5 — outcome of dispatching one layer.
passed checks the exit code; duration_seconds lands in the timings.json capture (c.2). Stub layers (deploy/api/browser until cfg loading lands per Y.2.gate.h.2) report skipped=True.
- Parameters:
layer (str)
exit_code (int)
duration_seconds (float)
skipped (bool)
- duration_seconds: float
- exit_code: int
- layer: str
- property passed: bool
- skipped: bool = False
- class recon_gen._dev.runner.ProbeFailure(kind, message)[source]
Bases:
objectY.2.gate.c.8 — a single missing or broken dependency.
kindis a stable token (used by tests + telemetry);messageis the operator-facing string (b.14.4 refusal pattern — actionable, points at what to do, never auto-invokes interactive flows).- Parameters:
kind (str)
message (str)
- kind: str
- message: str
- class recon_gen._dev.runner.RunOptions(only=None, parallel=1, scenarios=None, dialects=None, targets=None, variants=None, fuzz_seeds=1, fuzz_seed_value=None, skip_cheap=False, keep_on_failure=False, trace_all=False, allow_dirty_deploy=False, coverage=False)[source]
Bases:
objectY.2.gate.c.7 — operator-supplied flags threaded through dispatch.
Most flags are scaffolding today (consumed by future c-stage tasks):
only— pytest-k <expr>filter (active now in c.7).parallel— pytest-xdist worker count (active now in c.6; default 1 = serial).fuzz_seed_value— the actual fuzz seed VALUE for this run (resolved at cmd_up_to entry: env-override > random-per-invocation; persists across xdist workers in this run via env passthrough — c.6.xdist-safety lock).trace_all— Playwright capture every test (env var passthrough; consumed by c.11).allow_dirty_deploy— bypass tracked-changes refusal on layer 4+ (active now per b.10).coverage— emit per-(variant, layer).coverage.<variant>.<layer>data files (Y.2.gate.k.1.coverage). When set, every pytest layer (unit/db/app2/ api/browser) runs with--cov=recon_gen --cov-report=andCOVERAGE_FILEpointed at<run_dir>/.coverage.<run_dir.name>.<layer>. The CIcoverageaggregator job (W.8b) globscoverage-data-*artifacts and ``coverage combine``s them with no logic change. Off by default — opt in for CI; local runs don’t need it.scenarios/dialects/targets— variant matrix sub-flag narrowing (m.2.a). All None →compose_matrixreturns the 13-cellfulldefault. Any specified → cross-product mode where unspecified axes default per variant.DEFAULT_*.variants— triage escape (single/multiple<sc>_<di>_<ta>codes); mutex with the sub-flag axes. None when not pinned.fuzz_seeds— kept as count knob for future m.3 wiring (currently unused; fuzz cells insidecompose_matrixalready fan out via--scenarios=fuzz:N).skip_cheap— skip-if-already-green-this-SHA (active when cache lands; b.8).keep_on_failure— leave the variant’s ephemeral state up when the chain fails (gate.f.5; consumed in_run_one_variant’s finally — see also gate.l.2 for the lifecycle commands that clean up afterward).
- Parameters:
only (str | None)
parallel (int)
scenarios (str | None)
dialects (str | None)
targets (str | None)
variants (str | None)
fuzz_seeds (int)
fuzz_seed_value (int | None)
skip_cheap (bool)
keep_on_failure (bool)
trace_all (bool)
allow_dirty_deploy (bool)
coverage (bool)
- allow_dirty_deploy: bool = False
- coverage: bool = False
- dialects: str | None = None
- fuzz_seed_value: int | None = None
- fuzz_seeds: int = 1
- keep_on_failure: bool = False
- only: str | None = None
- parallel: int = 1
- scenarios: str | None = None
- skip_cheap: bool = False
- targets: str | None = None
- trace_all: bool = False
- variants: str | None = None
- recon_gen._dev.runner.chain_through(target)[source]
Y.2.gate.c.5 — return the slice of LAYERS from start through
target.Chain semantics (b.9 LOCKED): cross-layer is sequential.
up_to=dbmeans pyright → unit → db;up_to=qs_browsermeans the full chain.- Return type:
list[str]- Parameters:
target (str)
- recon_gen._dev.runner.cmd_down(args)[source]
Tear down dependencies. scope = local (default).
Destructive — requires –yes (Y.2.gate.b.14.3 destructive-op opt-in). Stops the named persistent Oracle containers (PG containers are ephemeral, no action needed). The aws scope was removed in CB.11.a.2 along with RDS Aurora.
- Return type:
int- Parameters:
args (Namespace)
- recon_gen._dev.runner.cmd_dump_last_errors(args)[source]
Surface failing-layer assertions + missing-capture warnings from the most-recent run dir.
Triage shortcut: instead of
find runs/ → grep stdout → reconstruct pytest output, walk the latestruns/<utc-ts>-<sha>/and dump a structured report — per failing (variant, layer) cell, with the pytest FAILED summary, the assertion text per failing test, and a pointer to (or warning about missing) AA.H.6 capture artifacts.Surfaces:
Per failing layer: layer name + exit code + duration + cell-level env (cmd.json fields:
RECON_GEN_DEPLOYMENT_NAME,RECON_GEN_FUZZ_SEED,RECON_GEN_TEST_L2_INSTANCE).Per failing test: the
FAILED ...summary line + the matched____ <test_id> ____traceback block fromstdout.log(truncated at the next____/=====).Capture-artifact pointer:
$RECON_GEN_RUN_DIR/qs_browser/<sanitized test_id>/paths, with a loud warning if AA.H.6’s 6 files (screenshot.png / dom.html / console.txt / network.txt / qs_errors.txt / trace.zip) are missing — AA.H.10 wired the hook to all three QS-driver fixtures, so a missing capture is a regression worth flagging.
Use
--run <run-id>to pick a specific run (e.g.20260516T203824Z-914fc4c); default = latest by mtime. Use--variant <name>to narrow to one cell.Exit code: always
EXIT_SUCCESS— this is a triage tool, not a gate. The caller cares about the chain’s exit; this just helps them read it faster.- Return type:
int- Parameters:
args (Namespace)
- recon_gen._dev.runner.cmd_pyright(args)[source]
Y.2.gate.b.14 — run pyright directly for fast type-check iteration.
Pyright runs via the unit layer’s conftest sessionstart hook on every up_to=unit invocation, but that pulls in the full ~9s test suite. For tight type-check loops during editing, this verb shells out to .venv/bin/pyright directly.
Stays behind the runner (per b.14.2 “every sub-tool absorbed by the orchestrator”) so an always-allow rule on ./run_tests.sh* covers it — no separate Claude-Code permission for .venv/bin/pyright.
Returns FAILURE on type errors so the chain-style &&-and-continue pattern works (./run_tests.sh pyright && ./run_tests.sh up_to=db).
- Return type:
int- Parameters:
args (Namespace)
- recon_gen._dev.runner.cmd_status(args)[source]
Show what’s currently running. Local-container only post-CB.12 (the AWS RDS section is gone). –cost flag retained for CLI compat but no longer surfaces hourly estimates (no cloud DB cost).
- Return type:
int- Parameters:
args (Namespace)
- recon_gen._dev.runner.cmd_sweep(args)[source]
Y.2.gate.c.9 — clean orphan QuickSight resources tagged
Harness:e2e.Replaces
scripts/sweep_harness_orphans.py(deletion of the standalone script is Y.2.gate.f.8). Same default: dry-run (collect + print). Pass--yes(or setRECON_GEN_RUNNER_YES=1, matching the destructive-op convention from b.14.3) to actually delete.Tag set:
Harness:e2e— production deploys don’t carry that tag (they wearManagedBy:recon-gen+ optionalL2Instance:<prefix>), so this is safe against the production resource graph.- Return type:
int- Parameters:
args (Namespace)
- Exit codes:
0 — clean (dry-run completed OR delete completed) 2 — needs operator (AWS creds expired / config not found)
- recon_gen._dev.runner.cmd_up(args)[source]
Boot dependencies. scope = local (default).
Local PG / Oracle / DuckDB spin on-demand inside
setup_variantper matrix cell — there’s no shared “local cluster” to pre-boot. Reported for symmetry withdown.- Return type:
int- Parameters:
args (Namespace)
- recon_gen._dev.runner.cmd_up_to(args)[source]
Run the test chain up to and including the named layer.
Pre-flight: probes the named layer’s required deps. On any failure, prints the operator-actionable message and exits NEEDS_OPERATOR.
For layers >= deploy, refuses on tracked-changes dirty state unless –allow-dirty-deploy (or RECON_GEN_RUNNER_YES=1) is set.
Each layer runs ONCE as a single subprocess;
pytest-xdisthandles in-layer parallelism; session-scopedpg_container_url/oracle_container_urlfixtures (tests/conftest.py CB.17.a) provision substrate on first consumption;isolated_cfggives each (file, worker) its owndeployment_name/db_table_prefixsuffix (CB.7 + CB.17.c).Cfg discovery:
RECON_GEN_CONFIGenv override →_DEFAULT_RUNNER_CFG_CANDIDATES. L2 path comes fromcfg.default_l2_instance(h.6) when present; missing →deploy/qs_api/qs_browserlayers’_layer_commandreturns None and dispatch printsdispatch-skip.Drops
env_access.jsonper run for env-var liveness diff.CB.17.d (2026-06-04): replaced the legacy 13-cell variant matrix +
_run_one_variantcell loop with this single-pytest-per-layer path. Per-(file, worker) isolation moved into pytest fixtures (isolated_cfg,seeded_cfg). The cell-loop architecture + its 1500+ LOC of supporting machinery deleted in the same pass.- Return type:
int- Parameters:
args (Namespace)
- recon_gen._dev.runner.collect_run_outputs(run_dir, layer_results)[source]
Y.2.gate.c.2 — write
timings.json+hashes.jsonafter the chain.timings.jsonaggregates per-layer wall-clock durations + per-test timings (for layers that ran pytest, via the conftest hook).hashes.jsonis a placeholder — populated by future tests/code as part ofc.13(hash-lock collapses into the runs dir).Single-source-of-drift principle (audit §7.9): both files live alongside each other under
runs/<run-id>/;c.3’s drift-diff reads them together against the prior run.- Return type:
None- Parameters:
run_dir (Path)
layer_results (Sequence[LayerResult])
- recon_gen._dev.runner.compute_drift(current, prior)[source]
Y.2.gate.c.3 — diff per-layer durations between two timings.json blobs.
Only emits entries for layers present in current (not interested in layers that ran in prior but not now — that’s chain-narrowing, not drift).
- Return type:
list[DriftEntry]- Parameters:
current (dict[str, Any])
prior (dict[str, Any])
- recon_gen._dev.runner.create_run_id()[source]
Y.2.gate.c.1 — <utc-ts>-<short-sha>[-dirty].
Stable, sortable, includes the dirty suffix so cross-run timing diffs don’t compare a clean run against a dirty one and claim spurious drift.
- Return type:
str
- recon_gen._dev.runner.dispatch_layer(layer, run_dir, options=None, *, variant_env=None, terminal_prefix='')[source]
Y.2.gate.c.5 — run one layer; return its result.
Stub layers return a skipped=True LayerResult with exit_code=0 so the chain doesn’t break — the deferred work is c.5+ follow-up, not a runner bug. Stubs print a clear dispatch-skip line so the operator knows.
Y.2.gate.b.2.impl —
variant_env(e.g.,{"RECON_GEN_DEMO_DATABASE_URL": "<container-url>"}) gets merged into the subprocess env so the variant’s resources (Docker container URL etc.) are visible to pytest fixtures + cfg loaders inside the subprocess.Per-layer subprocess capture (Y.2.gate.b.2.impl.oracle followup): every dispatch persists four artifacts under
<run_dir>/<layer>/:cmd.json— the input: cmd argv, cwd, env-overrides (deltas from inherited os.environ — the layer-specific keys + variant env, not the noisy full environ). Written before the subprocess starts; re-written after withexit_code+duration_seconds.stdout.log— subprocess stdout, also teed to operator’s terminal in real time.stderr.log— subprocess stderr, also teed to terminal.
Streams use a per-stream daemon-thread tee so a full pipe buffer on one stream can’t deadlock the other. The operator sees live output same as before; failures leave a complete trail in the run dir for post-mortem (CI artifact upload, hands-off run review).
- Return type:
- Parameters:
layer (str)
run_dir (Path)
options (RunOptions | None)
variant_env (dict[str, str] | None)
terminal_prefix (str)
- recon_gen._dev.runner.find_prior_run(current_run_id, runs_dir=None)[source]
Y.2.gate.c.3 — pick the best prior run for drift comparison.
Priority: (1) most-recent prior with the SAME SHA (closest signal — same code, lets us see real timing drift); (2) most-recent prior overall (good enough when no SHA match). Returns None if no prior runs exist.
- Return type:
Path|None- Parameters:
current_run_id (str)
runs_dir (Path | None)
- recon_gen._dev.runner.is_layer_cached_green(layer, *, variant='default')[source]
Y.2.gate.b.8.impl — True iff
layerhas a green cache marker for the current SHA + variant. Used by cmd_up_to when--skip-cheapis set to short-circuit re-runs.- Return type:
bool- Parameters:
layer (str)
variant (str)
- recon_gen._dev.runner.main(argv=None)[source]
- Return type:
int- Parameters:
argv (Sequence[str] | None)
- recon_gen._dev.runner.probe_dependencies(layer)[source]
Y.2.gate.c.8 — probe every dep
layerneeds; return all failures.Probes run sequentially (cheap; few seconds total) and gather all failures so the operator sees everything missing in one pass instead of fixing one, re-running, hitting the next, etc. No state file (LOCKED §7.12) — each invocation re-probes.
- Return type:
list[ProbeFailure]- Parameters:
layer (str)
- recon_gen._dev.runner.prune_old_runs(retain=20, runs_dir=None)[source]
Y.2.gate.c.4 — keep the most-recent
retainruns; delete the rest.“Most recent” = mtime (robust to dirs an operator touches). Only directories matching _RUN_ID_PATTERN are candidates — defensive: don’t accidentally nuke unrelated files an operator parked under runs/.
Returns the list of deleted paths (for tests / future telemetry). Idempotent: missing runs_dir → no-op; <retain runs → no-op.
Concurrency-safe: when multi-cell fan-out runs the unit suite in parallel and each unit subprocess itself calls runner.main(…) (e.g., test_up_to_creates_run_dir), sibling workers can race on the same runs/ dir.
shutil.rmtree(old)could see a path the sibling already deleted; FileNotFoundError is benign — the work is done.stat()failures during the listing pass are similarly benign (entry vanished mid-iter); skip and move on.#741 — tests no longer pollute the real
runs/:tests/ conftest.py::pytest_configureredirectsRUNS_DIRto a session tmp dir at pytest startup. So under matrix fan-out the 200+ in-processrunner.maincalls all prune within the session-tmp tree — no operator-runs/ contention, no need for an xdist-only short-circuit guard.- Return type:
list[Path]- Parameters:
retain (int)
runs_dir (Path | None)
- recon_gen._dev.runner.report_drift(current_run_dir, runs_dir=None)[source]
Y.2.gate.c.3 — find prior run, compute drift, print report.
- Return type:
None- Parameters:
current_run_dir (Path)
runs_dir (Path | None)
- Output shape:
drift: comparing against <prior_run_id> drift: pyright 1.81s (was 1.85s, -2.2%) drift: unit 15.20s (was 10.42s, +45.9%) drift: db 24.10s (was 12.30s, +96.0%) ⚠
The ⚠ marker fires on abs(delta_pct) >= DRIFT_THRESHOLD_PCT (±50%); same shape as hash-locked seed data — a sudden delta is signal, not noise.
- recon_gen._dev.runner.resolve_fuzz_seed_value()[source]
Y.2.gate.c.6.xdist-safety — resolve the seed for this runner invocation.
Priority:
RECON_GEN_FUZZ_SEEDenv (operator pin for failure repro) > random per session (secrets.randbits(32)). Per audit §7.11 (LOCKED): default = 1 random seed per run; cumulative coverage emerges across many runs. The seed is pinned across xdist workers within a single run so parametrize collection is deterministic (otherwise each worker rolls its own seed → collection diverges →Different tests were collectederror).- Return type:
int
- recon_gen._dev.runner.write_cache_marker(layer, *, duration_seconds, variant='default')[source]
Y.2.gate.b.8.impl — record that
layerpassed for the current SHA + variant. No-op if not in a git repo (_short_sha returns ‘nogit’) so directpytestinvocations don’t pollute the cache.- Return type:
None- Parameters:
layer (str)
duration_seconds (float)
variant (str)