From 616222ae7aa9d358512e681683338887d58c25ac Mon Sep 17 00:00:00 2001 From: 8Dionysus Date: Sat, 5 Sep 2026 20:56:48 -0600 Subject: [PATCH 1/3] perf(validation): expose ordinary subprocess test route --- pytest.ini | 1 + scripts/pytest_scheduler_experiment.py | 197 ++++++++++++++---- tests/test_validation_scheduler_experiment.py | 80 +++++++ 3 files changed, 240 insertions(+), 38 deletions(-) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..eea2c180 --- /dev/null +++ b/pytest.ini @@ -0,0 +1 @@ +[pytest] diff --git a/scripts/pytest_scheduler_experiment.py b/scripts/pytest_scheduler_experiment.py index c22867e6..8929c6c3 100644 --- a/scripts/pytest_scheduler_experiment.py +++ b/scripts/pytest_scheduler_experiment.py @@ -11,31 +11,19 @@ import signal import subprocess import sys +import tempfile import time import xml.etree.ElementTree as ET from dataclasses import dataclass -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import Any, Sequence import validation_identity +import validation_lanes REPO_ROOT = Path(__file__).resolve().parents[1] -SOURCE_TEST_TARGETS = ( - "tests/test_session_memory.py", - "tests/test_session_memory_outbox_core.py", - "tests/test_session_memory_doctor.py", - "tests/test_session_memory_outbox.py", - "tests/test_session_memory_task_lifecycle.py", - "tests/test_session_memory_tool_usage.py", - "tests/test_session_memory_episode_search.py", - "tests/test_session_memory_episode_maintenance.py", - "tests/test_session_memory_episode_temporal.py", - "tests/test_session_memory_capture.py", - "tests/test_session_memory_sweep.py", - "tests/test_public_tree_audit.py", - "tests/test_git_history_audit.py", -) +SOURCE_TEST_STEP_LABEL = "portable source tests" PROBE_MODULE = "pytest_scheduler_probe" PROBE_LOG_ENV = "AOA_SESSION_MEMORY_PYTEST_REPORT_LOG" TAIL_CHARACTERS = 16_000 @@ -84,6 +72,62 @@ class ExperimentError(RuntimeError): pass +def source_test_targets( + manifest_path: Path | None = None, +) -> tuple[str, ...]: + """Return the current full source-test selection from the owner lane.""" + try: + steps = validation_lanes.lane_command_sequence( + "standalone-full", + manifest_path or validation_lanes.MANIFEST_PATH, + ) + except validation_lanes.ManifestError as exc: + raise ExperimentError(f"cannot load source-test lane: {exc}") from exc + source_steps = [step for step in steps if step.label == SOURCE_TEST_STEP_LABEL] + if len(source_steps) != 1: + raise ExperimentError( + "standalone-full must contain exactly one portable source-test step" + ) + command = source_steps[0].command + prefix = ( + sys.executable, + "-m", + "pytest", + "-q", + "-p", + "no:cacheprovider", + ) + if command[: len(prefix)] != prefix: + raise ExperimentError( + "portable source-test step must be a plain pytest command" + ) + targets = tuple(command[len(prefix) :]) + def is_safe_target(target: str) -> bool: + path = PurePosixPath(target) + if ( + not target.startswith("tests/") + or path.is_absolute() + or ".." in path.parts + or "\\" in target + or "\x00" in target + or path.as_posix() != target + ): + return False + resolved = (REPO_ROOT / Path(*path.parts)).resolve() + try: + resolved.relative_to(REPO_ROOT) + except ValueError: + return False + return True + + valid_targets = all(is_safe_target(target) for target in targets) + if not targets or len(set(targets)) != len(targets) or not valid_targets: + raise ExperimentError( + "portable source-test step must contain only repo-relative tests/ targets" + ) + return targets + + def _write_json(path: Path, payload: dict[str, Any]) -> None: path = path.resolve() path.parent.mkdir(parents=True, exist_ok=True) @@ -175,6 +219,7 @@ def _pytest_argv( *, junit_path: Path, nodeids: Sequence[str] | None = None, + targets: Sequence[str] | None = None, collect_only: bool = False, ) -> list[str]: argv = [ @@ -184,6 +229,12 @@ def _pytest_argv( "-q", "-p", "no:cacheprovider", + # Worktrees live below /srv/abyss-machine; keep pytest from inheriting + # that host's root/config and conftest fixtures. + "--rootdir", + str(REPO_ROOT), + "--confcutdir", + str(REPO_ROOT), "-p", PROBE_MODULE, ] @@ -195,7 +246,11 @@ def _pytest_argv( argv.extend(("-n", str(method.workers), "--dist", method.scheduler.removeprefix("xdist-"))) if not collect_only: argv.extend(("--junitxml", str(junit_path))) - argv.extend(nodeids if nodeids is not None else SOURCE_TEST_TARGETS) + argv.extend( + nodeids + if nodeids is not None + else (targets if targets is not None else source_test_targets()) + ) return argv @@ -452,6 +507,7 @@ def _run_static( *, env: dict[str, str], artifact_root: Path, + targets: Sequence[str], timeout_seconds: float, timing_junit: Path | None, timing_receipt: Path | None, @@ -464,6 +520,7 @@ def _run_static( _pytest_argv( method, junit_path=artifact_root / "unused.xml", + targets=targets, collect_only=True, ), env=collect_env, @@ -472,7 +529,17 @@ def _run_static( ) collection_wall = time.monotonic() - collection_started if collection_result["returncode"] != 0: - raise ExperimentError("static corpus collection failed") + details = [] + for stream_name in ("stdout", "stderr"): + stream = collection_result[stream_name] + details.append( + f"collection {stream_name} ({stream['path']}):\n{stream['tail']}" + ) + raise ExperimentError( + "static corpus collection failed " + f"(returncode={collection_result['returncode']})\n" + + "\n".join(details) + ) collection = _collection_from_events(_load_probe_events(collection_log)) nodeids = collection["nodeids"] timing_source: dict[str, Any] | None = None @@ -536,23 +603,52 @@ def run_shard(index: int, shard: list[str]) -> tuple[dict[str, Any], list[dict[s def run_trial(args: argparse.Namespace) -> dict[str, Any]: + artifact_arg = args.artifact_root + receipt_arg = args.receipt + if receipt_arg is not None and artifact_arg is None: + raise ExperimentError("--receipt requires --artifact-root") + experiment_options = (args.pycache_root, args.pair_id, args.trial) + if receipt_arg is None and any(value is not None for value in experiment_options): + raise ExperimentError("experiment options require --receipt") + if artifact_arg is not None: + return _run_trial( + args, + artifact_root=_require_external_path(artifact_arg, "artifact root"), + receipt_path=( + _require_external_path(receipt_arg, "receipt") + if receipt_arg is not None + else None + ), + ) + with tempfile.TemporaryDirectory( + prefix="aoa-session-memory-pytest-", + dir=tempfile.gettempdir(), + ) as temporary_root: + return _run_trial( + args, + artifact_root=Path(temporary_root), + receipt_path=None, + ) + + +def _run_trial( + args: argparse.Namespace, + *, + artifact_root: Path, + receipt_path: Path | None, +) -> dict[str, Any]: method = METHODS[args.method] - artifact_root = _require_external_path(args.artifact_root, "artifact root") - receipt_path = _require_external_path(args.receipt, "receipt") artifact_root.mkdir(parents=True, exist_ok=True) if any(artifact_root.iterdir()): raise ExperimentError(f"artifact root must start empty: {artifact_root}") + experiment = receipt_path is not None started_at = dt.datetime.now(dt.UTC) started = time.monotonic() - before = validation_identity.repository_identity() - environment = validation_identity.environment_identity() - env, cache = _cache_environment( - os.environ.copy(), - pycache_root=args.pycache_root, - repository=before, - environment=environment, - method=method, - ) + before = validation_identity.repository_identity() if experiment else {} + environment = validation_identity.environment_identity() if experiment else {} + targets: tuple[str, ...] = () + env: dict[str, str] = {} + cache: dict[str, Any] = {"enabled": False, "observed_state_before": "disabled"} error: str | None = None steps: list[dict[str, Any]] = [] nodeids: list[str] = [] @@ -566,11 +662,20 @@ def run_trial(args: argparse.Namespace) -> dict[str, Any]: } sharding: dict[str, Any] | None = None try: + targets = source_test_targets() + env, cache = _cache_environment( + os.environ.copy(), + pycache_root=args.pycache_root, + repository=before, + environment=environment, + method=method, + ) if method.static: steps, events, nodeids, sharding = _run_static( method, env=env, artifact_root=artifact_root, + targets=targets, timeout_seconds=args.timeout_seconds, timing_junit=args.timing_junit, timing_receipt=args.timing_receipt, @@ -583,6 +688,7 @@ def run_trial(args: argparse.Namespace) -> dict[str, Any]: _pytest_argv( method, junit_path=artifact_root / "pytest.junit.xml", + targets=targets, ), env=run_env, artifact_root=artifact_root, @@ -599,8 +705,8 @@ def run_trial(args: argparse.Namespace) -> dict[str, Any]: execution = _execution_from_events(events, nodeids) except (ExperimentError, OSError, subprocess.SubprocessError) as exc: error = str(exc) - after = validation_identity.repository_identity() - stable = before == after + after = validation_identity.repository_identity() if experiment else {} + stable = before == after if experiment else True corpus = corpus_identity(nodeids) all_steps_passed = bool(steps) and all( step.get("returncode") == 0 and not step.get("timed_out") for step in steps @@ -628,29 +734,34 @@ def run_trial(args: argparse.Namespace) -> dict[str, Any]: "wall_seconds": round(time.monotonic() - started, 6), "ok": ok, "error": error, - "repository_identity": {"before": before, "after": after, "stable": stable}, - "environment_identity": environment, + "repository_identity": ( + {"before": before, "after": after, "stable": stable} + if experiment + else None + ), + "environment_identity": environment if experiment else None, "cache": cache, - "targets": list(SOURCE_TEST_TARGETS), + "targets": list(targets), "corpus": corpus, "execution": execution, "sharding": sharding, "steps": steps, - "receipt_path": str(receipt_path), + "receipt_path": str(receipt_path) if receipt_path is not None else None, "authority_boundary": ( "non-authoritative owner-local scheduler comparison only; no owner gate, " "routing, reuse, release, publication, or sibling-rollout authority" ), } - _write_json(receipt_path, payload) + if receipt_path is not None: + _write_json(receipt_path, payload) return payload def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--method", required=True, choices=sorted(METHODS)) - parser.add_argument("--artifact-root", required=True, type=Path) - parser.add_argument("--receipt", required=True, type=Path) + parser.add_argument("--artifact-root", type=Path) + parser.add_argument("--receipt", type=Path) parser.add_argument("--pycache-root", type=Path) timing = parser.add_mutually_exclusive_group() timing.add_argument("--timing-junit", type=Path) @@ -676,6 +787,8 @@ def main(argv: Sequence[str] | None = None) -> int: "corpus_count": payload["corpus"]["count"], "coverage_complete": payload["execution"]["coverage_complete"], "failed_nodeids": payload["execution"]["failed_nodeids"], + "skipped_nodeids": payload["execution"]["skipped_nodeids"], + "error": payload["error"], "receipt": payload["receipt_path"], } print( @@ -683,6 +796,14 @@ def main(argv: Sequence[str] | None = None) -> int: if args.json else summary ) + if not payload["ok"] and payload["receipt_path"] is None: + if payload["error"]: + print(f"scheduler error: {payload['error']}", file=sys.stderr) + for step in payload["steps"]: + for stream_name in ("stdout", "stderr"): + tail = step[stream_name]["tail"] + if tail: + print(f"[{step['id']} {stream_name}]\n{tail}", file=sys.stderr) return 0 if payload["ok"] else 1 diff --git a/tests/test_validation_scheduler_experiment.py b/tests/test_validation_scheduler_experiment.py index bec3cad4..338eedf8 100644 --- a/tests/test_validation_scheduler_experiment.py +++ b/tests/test_validation_scheduler_experiment.py @@ -1,9 +1,12 @@ from __future__ import annotations import importlib +import json import sys from pathlib import Path +import pytest + REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "scripts")) @@ -12,6 +15,70 @@ validation_scheduler_experiment = importlib.import_module( "validation_scheduler_experiment" ) +validation_lanes = importlib.import_module("validation_lanes") + + +def test_scheduler_targets_follow_current_full_lane() -> None: + step = next( + item + for item in validation_lanes.lane_command_sequence("standalone-full") + if item.label == "portable source tests" + ) + + prefix = (sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider") + assert step.command[: len(prefix)] == prefix + targets = step.command[len(prefix) :] + assert pytest_scheduler_experiment.source_test_targets() == targets + assert all(target.startswith("tests/") for target in targets) + + +@pytest.mark.parametrize( + "bad_target", + ( + "--lf", + "../tests/outside.py", + "tests/../outside.py", + "tests\\inside.py", + "tests/bad\x00.py", + "tests/test_session_memory.py", + ), +) +def test_scheduler_target_binding_rejects_invalid_metadata( + tmp_path: Path, bad_target: str +) -> None: + payload = json.loads(validation_lanes.MANIFEST_PATH.read_text(encoding="utf-8")) + source_step = next( + item + for item in payload["command_sequences"]["standalone_full"] + if item["label"] == "portable source tests" + ) + source_step["command"].append(bad_target) + manifest = tmp_path / "validation_lanes.json" + manifest.write_text(json.dumps(payload), encoding="utf-8") + + with pytest.raises( + pytest_scheduler_experiment.ExperimentError, + match="only repo-relative tests/ targets", + ): + pytest_scheduler_experiment.source_test_targets(manifest) + + +def test_scheduler_cli_allows_ordinary_route_without_experiment_receipts() -> None: + args = pytest_scheduler_experiment.build_parser().parse_args( + ["--method", "static2"] + ) + + assert args.artifact_root is None + assert args.receipt is None + with pytest.raises( + pytest_scheduler_experiment.ExperimentError, + match="--receipt requires --artifact-root", + ): + pytest_scheduler_experiment.run_trial( + pytest_scheduler_experiment.build_parser().parse_args( + ["--method", "static2", "--receipt", "/tmp/trial.json"] + ) + ) def test_scheduler_plan_keeps_all_candidates_in_shadow() -> None: @@ -72,6 +139,19 @@ def test_duration_balanced_shards_preserve_corpus_and_balance_heavy_cases() -> N assert max(projected) - min(projected) <= 1.0 +def test_duration_balanced_shards_keep_cases_without_hints() -> None: + nodeids = [f"tests/test_example.py::test_case_{index}" for index in range(5)] + + shards, _ = pytest_scheduler_experiment.duration_balanced_static_shards( + nodeids, + 2, + {nodeids[0]: 10.0}, + ) + + assert set().union(*map(set, shards)) == set(nodeids) + assert sum(len(shard) for shard in shards) == len(nodeids) + + def _receipt( method: str, pair_id: str, From 502e48e2cfa038606ef6f560c2b0bdf3c922bcab Mon Sep 17 00:00:00 2001 From: 8Dionysus Date: Sat, 5 Sep 2026 21:13:48 -0600 Subject: [PATCH 2/3] ci(validation): run portable tests through ordinary shards --- .github/workflows/repo-validation.yml | 17 +--------- VALIDATION.md | 34 +++++++++---------- .../validation/validation_evidence_graph.json | 3 ++ docs/validation/validation_lanes.json | 1 + scripts/pytest_scheduler_experiment.py | 30 ++++++++++++++++ scripts/validation_evidence_graph.py | 1 + tests/test_validation_scheduler_experiment.py | 24 +++++++++++++ 7 files changed, 76 insertions(+), 34 deletions(-) diff --git a/.github/workflows/repo-validation.yml b/.github/workflows/repo-validation.yml index e77cb3ab..d27ff77a 100644 --- a/.github/workflows/repo-validation.yml +++ b/.github/workflows/repo-validation.yml @@ -54,22 +54,7 @@ jobs: run: python scripts/audit_git_history.py --repo . --fail-on none - name: Run portable source tests - run: >- - python -m pytest -q -p no:cacheprovider --durations=20 - tests/test_session_memory.py - tests/test_session_memory_privacy_core.py - tests/test_session_memory_outbox_core.py - tests/test_session_memory_doctor.py - tests/test_session_memory_outbox.py - tests/test_session_memory_task_lifecycle.py - tests/test_session_memory_tool_usage.py - tests/test_session_memory_episode_search.py - tests/test_session_memory_episode_maintenance.py - tests/test_session_memory_episode_temporal.py - tests/test_session_memory_capture.py - tests/test_session_memory_sweep.py - tests/test_public_tree_audit.py - tests/test_git_history_audit.py + run: python scripts/pytest_scheduler_experiment.py --method static2 - name: Run MCP package release check working-directory: packages/aoa-session-memory-mcp diff --git a/VALIDATION.md b/VALIDATION.md index 03224aa4..96e263b7 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -6,25 +6,23 @@ Run session-memory checks on demand after source or session-pipeline changes: env -u PYTHONDONTWRITEBYTECODE \ PYTHONPYCACHEPREFIX="${PYTHONPYCACHEPREFIX:-${TMPDIR:-/tmp}/aoa-session-memory-pycache}" \ python3 -m py_compile scripts/aoa_session_memory.py -env -u PYTHONDONTWRITEBYTECODE \ - PYTHONPYCACHEPREFIX="${PYTHONPYCACHEPREFIX:-${TMPDIR:-/tmp}/aoa-session-memory-pycache}" \ - python3 -m pytest -q -p no:cacheprovider \ - tests/test_session_memory.py \ - tests/test_session_memory_privacy_core.py \ - tests/test_session_memory_outbox_core.py \ - tests/test_session_memory_doctor.py \ - tests/test_session_memory_outbox.py \ - tests/test_session_memory_task_lifecycle.py \ - tests/test_session_memory_tool_usage.py \ - tests/test_session_memory_episode_search.py \ - tests/test_session_memory_episode_maintenance.py \ - tests/test_session_memory_episode_temporal.py \ - tests/test_session_memory_capture.py \ - tests/test_session_memory_sweep.py +python3 scripts/pytest_scheduler_experiment.py --method static2 python3 scripts/aoa_session_memory.py validate --workspace-root /path/to/workspace --aoa-root /path/to/workspace/.aoa python3 scripts/aoa_session_memory.py doctor --workspace-root /path/to/workspace --aoa-root /path/to/workspace/.aoa ``` +The ordinary `static2` route reads the current portable source-test targets +from `docs/validation/validation_lanes.json`, collects them once, and runs an +exact two-process partition. It creates a fresh bytecode prefix inside its +temporary invocation directory, writes no receipt, and does not require +repository or environment identity. Use `--method serial` as the direct +fallback when process parallelism is unsuitable. Receipt, artifact, and +identity options remain comparison-only; this local route is feedback and +does not replace the full release or installed-protocol gates. +If a static child fails, its captured pytest tails are emitted when that shard +completes instead of waiting for the sibling; this is an early shard-completion +signal, not per-test streaming or an incremental release verdict. + For a pure predicate edit to a standalone producer sibling, run only the corresponding focused route before the full suite. Use a fresh bytecode prefix outside the checkout so a same-size, same-second source edit cannot reuse an @@ -33,12 +31,12 @@ older `.pyc`; these direct tests include interpreter and module startup: ```bash # Privacy sibling edit: privacy_core_pycache="$(mktemp -d "${TMPDIR:-/tmp}/aoa-session-memory-privacy.XXXXXX")" -env -u PYTHONDONTWRITEBYTECODE PYTHONPYCACHEPREFIX="$privacy_core_pycache" \ +env -u PYTHONDONTWRITEBYTECODE PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONPYCACHEPREFIX="$privacy_core_pycache" \ python3 -m pytest -q -p no:cacheprovider --rootdir=. --confcutdir=. \ tests/test_session_memory_privacy_core.py # Outbox sibling edit: outbox_core_pycache="$(mktemp -d "${TMPDIR:-/tmp}/aoa-session-memory-outbox.XXXXXX")" -env -u PYTHONDONTWRITEBYTECODE PYTHONPYCACHEPREFIX="$outbox_core_pycache" \ +env -u PYTHONDONTWRITEBYTECODE PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONPYCACHEPREFIX="$outbox_core_pycache" \ python3 -m pytest -q -p no:cacheprovider --rootdir=. --confcutdir=. \ tests/test_session_memory_outbox_core.py ``` @@ -49,7 +47,7 @@ either sibling, add the monolith identity regression: ```bash identity_pycache="$(mktemp -d "${TMPDIR:-/tmp}/aoa-session-memory-identity.XXXXXX")" -env -u PYTHONDONTWRITEBYTECODE PYTHONPYCACHEPREFIX="$identity_pycache" \ +env -u PYTHONDONTWRITEBYTECODE PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONPYCACHEPREFIX="$identity_pycache" \ python3 -m pytest -q -p no:cacheprovider --rootdir=. --confcutdir=. \ tests/test_session_memory.py \ -k 'generation_identity or loaded_producer_source' diff --git a/docs/validation/validation_evidence_graph.json b/docs/validation/validation_evidence_graph.json index 58ded0c7..252b1718 100644 --- a/docs/validation/validation_evidence_graph.json +++ b/docs/validation/validation_evidence_graph.json @@ -102,8 +102,10 @@ "patterns": [ "scripts/aoa_session_memory.py", "scripts/aoa_session_memory_outbox.py", + "scripts/aoa_session_memory_privacy.py", "scripts/benchmark_session_projection.py", "tests/test_session_memory.py", + "tests/test_session_memory_privacy_core.py", "tests/test_session_memory_outbox_core.py", "tests/session_memory_test_support.py", "tests/test_session_memory_doctor.py", @@ -221,6 +223,7 @@ "-p", "no:cacheprovider", "tests/test_session_memory.py", + "tests/test_session_memory_privacy_core.py", "tests/test_session_memory_outbox_core.py", "tests/test_session_memory_doctor.py", "tests/test_session_memory_outbox.py", diff --git a/docs/validation/validation_lanes.json b/docs/validation/validation_lanes.json index 1e2c1336..9a26e802 100644 --- a/docs/validation/validation_lanes.json +++ b/docs/validation/validation_lanes.json @@ -54,6 +54,7 @@ "-p", "no:cacheprovider", "tests/test_session_memory.py", + "tests/test_session_memory_privacy_core.py", "tests/test_session_memory_outbox_core.py", "tests/test_session_memory_doctor.py", "tests/test_session_memory_outbox.py", diff --git a/scripts/pytest_scheduler_experiment.py b/scripts/pytest_scheduler_experiment.py index 8929c6c3..9606b6bb 100644 --- a/scripts/pytest_scheduler_experiment.py +++ b/scripts/pytest_scheduler_experiment.py @@ -458,6 +458,7 @@ def _cache_environment( base: dict[str, str], *, pycache_root: Path | None, + ordinary_pycache_root: Path | None = None, repository: dict[str, Any], environment: dict[str, Any], method: Method, @@ -467,6 +468,18 @@ def _cache_environment( filter(None, (str(REPO_ROOT / "scripts"), env.get("PYTHONPATH"))) ) if pycache_root is None: + if ordinary_pycache_root is not None: + ordinary_root = _require_external_path( + ordinary_pycache_root, "ordinary pycache root" + ) + ordinary_root.mkdir(parents=True, exist_ok=True) + env.pop("PYTHONDONTWRITEBYTECODE", None) + env["PYTHONPYCACHEPREFIX"] = str(ordinary_root) + return env, { + "enabled": True, + "observed_state_before": "fresh-per-invocation", + "reusable": False, + } env["PYTHONDONTWRITEBYTECODE"] = "1" return env, {"enabled": False, "observed_state_before": "disabled"} root = _require_external_path(pycache_root, "pycache root") @@ -511,6 +524,7 @@ def _run_static( timeout_seconds: float, timing_junit: Path | None, timing_receipt: Path | None, + emit_live_failures: bool, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[str], dict[str, Any]]: collection_log = artifact_root / "collection.probe.jsonl" collect_env = {**env, PROBE_LOG_ENV: str(collection_log)} @@ -577,6 +591,18 @@ def run_shard(index: int, shard: list[str]) -> tuple[dict[str, Any], list[dict[s timeout_seconds=timeout_seconds, ) result["selection"] = corpus_identity(shard) + if emit_live_failures and ( + result["returncode"] != 0 or result["timed_out"] + ): + print( + f"[{step_id}] failed before sibling shards completed " + f"(returncode={result['returncode']}, timed_out={result['timed_out']})\n" + f"stdout tail:\n{result['stdout']['tail']}\n" + f"stderr tail:\n{result['stderr']['tail']}", + file=sys.stderr, + flush=True, + ) + result["failure_reported_live"] = True return result, _load_probe_events(probe_log) started = time.monotonic() @@ -666,6 +692,7 @@ def _run_trial( env, cache = _cache_environment( os.environ.copy(), pycache_root=args.pycache_root, + ordinary_pycache_root=(artifact_root / "pycache" if not experiment else None), repository=before, environment=environment, method=method, @@ -679,6 +706,7 @@ def _run_trial( timeout_seconds=args.timeout_seconds, timing_junit=args.timing_junit, timing_receipt=args.timing_receipt, + emit_live_failures=receipt_path is None, ) else: probe_log = artifact_root / "trial.probe.jsonl" @@ -800,6 +828,8 @@ def main(argv: Sequence[str] | None = None) -> int: if payload["error"]: print(f"scheduler error: {payload['error']}", file=sys.stderr) for step in payload["steps"]: + if step.get("failure_reported_live"): + continue for stream_name in ("stdout", "stderr"): tail = step[stream_name]["tail"] if tail: diff --git a/scripts/validation_evidence_graph.py b/scripts/validation_evidence_graph.py index a641afc0..261596a4 100644 --- a/scripts/validation_evidence_graph.py +++ b/scripts/validation_evidence_graph.py @@ -28,6 +28,7 @@ "-p", "no:cacheprovider", "tests/test_session_memory.py", + "tests/test_session_memory_privacy_core.py", "tests/test_session_memory_outbox_core.py", "tests/test_session_memory_doctor.py", "tests/test_session_memory_outbox.py", diff --git a/tests/test_validation_scheduler_experiment.py b/tests/test_validation_scheduler_experiment.py index 338eedf8..3373a7f9 100644 --- a/tests/test_validation_scheduler_experiment.py +++ b/tests/test_validation_scheduler_experiment.py @@ -81,6 +81,30 @@ def test_scheduler_cli_allows_ordinary_route_without_experiment_receipts() -> No ) +def test_ordinary_route_uses_fresh_external_bytecode_prefix(tmp_path: Path) -> None: + pycache_root = tmp_path / "artifact" / "pycache" + env, cache = pytest_scheduler_experiment._cache_environment( + { + "PYTHONDONTWRITEBYTECODE": "1", + "PYTHONPYCACHEPREFIX": "/stale-prefix", + }, + pycache_root=None, + ordinary_pycache_root=pycache_root, + repository={}, + environment={}, + method=pytest_scheduler_experiment.METHODS["static2"], + ) + + assert env["PYTHONPYCACHEPREFIX"] == str(pycache_root.resolve()) + assert "PYTHONDONTWRITEBYTECODE" not in env + assert cache == { + "enabled": True, + "observed_state_before": "fresh-per-invocation", + "reusable": False, + } + assert pycache_root.is_dir() + + def test_scheduler_plan_keeps_all_candidates_in_shadow() -> None: plan = validation_scheduler_experiment.candidate_plan() methods = {item["name"]: item for item in plan["methods"]} From 5cdd61c282137e4a1fbe353cd3f4156e0dc26394 Mon Sep 17 00:00:00 2001 From: 8Dionysus Date: Sat, 5 Sep 2026 21:28:45 -0600 Subject: [PATCH 3/3] fix(validation): keep portable scheduler source host-neutral --- scripts/pytest_scheduler_experiment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/pytest_scheduler_experiment.py b/scripts/pytest_scheduler_experiment.py index 9606b6bb..cadf0238 100644 --- a/scripts/pytest_scheduler_experiment.py +++ b/scripts/pytest_scheduler_experiment.py @@ -229,8 +229,8 @@ def _pytest_argv( "-q", "-p", "no:cacheprovider", - # Worktrees live below /srv/abyss-machine; keep pytest from inheriting - # that host's root/config and conftest fixtures. + # Worktrees can be nested under a host checkout; keep pytest from + # inheriting an ancestor root/config and conftest fixtures. "--rootdir", str(REPO_ROOT), "--confcutdir",