diff --git a/.agentctl/project.toml b/.agentctl/project.toml index 3f181e4ef..544d3fc0e 100644 --- a/.agentctl/project.toml +++ b/.agentctl/project.toml @@ -36,11 +36,11 @@ verification-graph = ["devtools/verify.py", "pyproject.toml"] [operations.verify_affected] description = "Run Polylogue's affected-test verification plan" -exec = ["env", "POLYLOGUE_PYTEST_WORKERS=2", "devtools", "verify"] +exec = ["devtools", "verify"] pool = "pytest" result = "pytest" cache = "tree+environment" -timeout_seconds = 3600 +timeout_seconds = 7200 [operations.verify_quick] description = "Run Polylogue's static fast verification gates" diff --git a/devtools/pytest_slot.py b/devtools/pytest_slot.py index f37247056..76cd17786 100644 --- a/devtools/pytest_slot.py +++ b/devtools/pytest_slot.py @@ -319,7 +319,7 @@ def _write_launch( "argv": list(argv), "working_directory": cwd, "environment": dict(env), - "timeout_seconds": 3600, + "timeout_seconds": 7200, "result_kind": "exit", "log_path": str(log_path), } diff --git a/devtools/run_tests.py b/devtools/run_tests.py index ba3006d39..2adcf4956 100644 --- a/devtools/run_tests.py +++ b/devtools/run_tests.py @@ -27,10 +27,11 @@ import subprocess import sys import time +from collections.abc import Mapping from pathlib import Path from typing import Any, cast -from devtools.agent_env import agent_worker_cap, inside_agent_job +from devtools.agent_env import HARNESS_RUN_ENV, agent_worker_cap, inside_agent_job from devtools.checkout_guard import ( CheckoutImportMismatchError, assert_polylogue_matches_checkout, @@ -370,16 +371,23 @@ def build_pytest_cmd(selection: list[str]) -> list[str]: *collection_args, # A focused run traces into the one checkout datafile and writes back, # so the graph is advanced by every managed run. It never selects: the - # caller already named what to run. - "--testmon", - f"--testmon-env={TESTMON_ENVIRONMENT}", - "--testmon-noselect", + # caller already named what to run. A run spawned from inside another + # managed run (a test exercising the harness) must not touch that + # datafile: its session would reset the outer run's pending graph. + *_testmon_args(os.environ), *selection, *worker_args, *_xdist_distribution_args(selection, worker_args), ] +def _testmon_args(env: Mapping[str, str]) -> tuple[str, ...]: + """testmon flags for a focused run; none when nested in a managed run.""" + if env.get(HARNESS_RUN_ENV): + return ("-p", "no:testmon") + return ("--testmon", f"--testmon-env={TESTMON_ENVIRONMENT}", "--testmon-noselect") + + def _selection_targets_benchmarks(selection: list[str]) -> bool: """Keep benchmark collection available only when the caller asks for it.""" return any("tests/benchmarks" in argument for argument in selection) diff --git a/devtools/verify.py b/devtools/verify.py index 8fd2a242c..f40d4bd45 100644 --- a/devtools/verify.py +++ b/devtools/verify.py @@ -76,9 +76,12 @@ PYTEST_SUMMARY_PATH = PYTEST_REPORT_DIR / "current-pytest-summary.json" PYTEST_OUTPUT_PATH = PYTEST_REPORT_DIR / "current-pytest-output.log" PYTEST_JUNIT_REPORT_DIR = PYTEST_REPORT_DIR / "junit" -#: SQLite archive construction makes the corpus IO-bound. Two workers provide -#: overlap without multiplying cache churn or exhausting the pytest cgroup. -CORPUS_MAX_WORKERS = 2 +#: One fixed width for the corpus and the runner's affected tier, sized to the +#: pytest pool's 12 GiB cgroup ceiling (eight workers peak near 10 GB) rather +#: than host cores or free RAM. Measured 2026-09-03 uncontended: 47 minutes for +#: 20,860 tests at eight workers; at two the same run takes about seven hours +#: and the required check cannot finish inside its slot timeout. +CORPUS_MAX_WORKERS = 8 _AGENTCTL_OPERATION_ARGV = {"verify_affected": (), "verify_quick": ("--quick",), "verify_all": ("--all",)} _PROJECT_DESCRIPTOR = ".agentctl/project.toml" # These tests read the AgentCTL descriptor directly. They are the bounded @@ -293,6 +296,24 @@ def _read_json(path: Path) -> dict[str, Any] | None: MAX_RERUN_NODEIDS = 300 +def _report_nodeid_to_selector(nodeid: str) -> str: + """Strip xdist's ``@`` suffix so a report node id selects again. + + ``--dist=loadgroup`` reports ``path::test[param]@group``; pytest cannot + collect that literal, so a rerun built from it errors before running. + A parametrization id may itself contain ``@``, so only a suffix after the + closing bracket (or after the bare test name) is removed. + """ + head, sep, tail = nodeid.rpartition("@") + if not sep or "::" not in head: + return nodeid + if "[" in tail or "]" in tail or "/" in tail or "::" in tail: + return nodeid + if head.endswith("]") or "[" not in head.rsplit("::", 1)[-1]: + return head + return nodeid + + def _rerun_failed_once(command: Sequence[str], *, env: Mapping[str, str], artifacts: Any) -> dict[str, Any] | None: """Rerun exactly the failed tests once, alone and unselected. @@ -306,7 +327,7 @@ def _rerun_failed_once(command: Sequence[str], *, env: Mapping[str, str], artifa if not isinstance(report, Mapping): return None failed = [ - str(test["nodeid"]) + _report_nodeid_to_selector(str(test["nodeid"])) for test in report.get("tests", []) if isinstance(test, Mapping) and test.get("outcome") in {"failed", "error"} and test.get("nodeid") ] diff --git a/devtools/verify_testmon_selection.py b/devtools/verify_testmon_selection.py index 57f78b01a..eb75b930b 100644 --- a/devtools/verify_testmon_selection.py +++ b/devtools/verify_testmon_selection.py @@ -10,7 +10,7 @@ from devtools.pytest_invocation import MANAGED_PLUGIN_ARGS from devtools.toolchain import venv_python -from devtools.verify import _pytest_worker_args +from devtools.verify import CORPUS_MAX_WORKERS, _pytest_worker_args def main(_argv: list[str] | None = None) -> int: @@ -27,8 +27,8 @@ def main(_argv: list[str] | None = None) -> int: finally: if configured_workers is not None: os.environ["POLYLOGUE_PYTEST_WORKERS"] = configured_workers - if default_worker_args != ["--dist=loadgroup", "-n", "2"]: - print("testmon-selection: managed verification does not default to two workers") + if default_worker_args != ["--dist=loadgroup", "-n", str(CORPUS_MAX_WORKERS)]: + print(f"testmon-selection: managed verification does not default to {CORPUS_MAX_WORKERS} workers") return 1 with tempfile.TemporaryDirectory(prefix="polylogue-testmon-gate-") as temporary: root = Path(temporary) @@ -93,7 +93,7 @@ def main(_argv: list[str] | None = None) -> int: if not selected or not total or selected * 100 >= total * 5: print(f"testmon-selection: selected {selected} of {total}, expected under 5%\n{output}") return 1 - print(f"testmon-selection: selected {selected} of {total}; workers=2") + print(f"testmon-selection: selected {selected} of {total}; workers={CORPUS_MAX_WORKERS}") return 0 diff --git a/tests/unit/devtools/test_pytest_slot.py b/tests/unit/devtools/test_pytest_slot.py index f7413dc03..848816de2 100644 --- a/tests/unit/devtools/test_pytest_slot.py +++ b/tests/unit/devtools/test_pytest_slot.py @@ -155,7 +155,7 @@ def test_outside_a_task_the_run_is_queued(tmp_path: Path, monkeypatch: pytest.Mo assert launch["operation"] == "test" assert launch["pool"] == "pytest" assert launch["result_kind"] == "exit" - assert launch["timeout_seconds"] == 3600 + assert launch["timeout_seconds"] == 7200 assert [call["argv"][0] for call in _calls(record)] == ["add", "wait", "status"] diff --git a/tests/unit/devtools/test_run_tests.py b/tests/unit/devtools/test_run_tests.py index 3e4c467f4..bd02c86cf 100644 --- a/tests/unit/devtools/test_run_tests.py +++ b/tests/unit/devtools/test_run_tests.py @@ -625,3 +625,14 @@ def test_absent_paths_are_resolved_against_the_checkout_not_the_caller_cwd( ] assert run_tests.absent_selection_paths(selection, root=checkout) == ["tests/unit/test_deleted.py"] + + +def test_nested_managed_run_never_traces_into_the_checkout_datafile(monkeypatch: pytest.MonkeyPatch) -> None: + """Anti-vacuity: tracing unconditionally lets a test that spawns + `devtools test` reset the outer corpus run's graph (2026-09-05: a full + corpus left a one-test datafile).""" + from devtools.agent_env import HARNESS_RUN_ENV + from devtools.run_tests import _testmon_args + + assert "--testmon" in _testmon_args({}) + assert tuple(_testmon_args({HARNESS_RUN_ENV: "run-1"})) == ("-p", "no:testmon") diff --git a/tests/unit/devtools/test_verify.py b/tests/unit/devtools/test_verify.py index 1876fa101..192f3f15f 100644 --- a/tests/unit/devtools/test_verify.py +++ b/tests/unit/devtools/test_verify.py @@ -914,3 +914,15 @@ def fake_run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[s assert exit_code == 0 assert recorded["capture_output"] is True + + +def test_rerun_selector_strips_the_xdist_group_suffix() -> None: + """Anti-vacuity: passing the report node id through unchanged makes the + rerun error with "not found" for every grouped test, which is what wiped + the 2026-09-05 corpus rerun.""" + from devtools.verify import _report_nodeid_to_selector + + assert _report_nodeid_to_selector("tests/a.py::test_x@web-reader") == "tests/a.py::test_x" + assert _report_nodeid_to_selector("tests/a.py::T::test_x[p]@grp") == "tests/a.py::T::test_x[p]" + assert _report_nodeid_to_selector("tests/a.py::test_x[a@b]") == "tests/a.py::test_x[a@b]" + assert _report_nodeid_to_selector("tests/a.py::test_x") == "tests/a.py::test_x"