diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ffffcbc..2919508 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,11 +18,20 @@ name: Hands Tests # test_python_matrix_workflow.py asserting the pre-promotion contract. The # guard test sat failing with nothing to report it. # -# Deliberately ONLY pytest. It must not invoke `autohands generate` / `run_all` +# pytest plus ONE drift leg. It must not invoke `autohands generate` / `run_all` # / `pre_build` against live workspaces, and must not reach the network — those # need sibling workspace checkouts and belong to release.yml and the scheduled # drivers, not to a PR gate. What runs here is stdlib plus four small packages, -# so it stays fast (~6s) and flake-free. +# so it stays fast (~6s) and flake-free. The one non-pytest step is the +# tenant-firewall gate (PyAutoMind#198): every firewall finding to date merged +# through a green PR because no PR CI ran the check, so the leg a Hands PR can +# cause is verified here, in the PR that authors it. `--only` keeps Mind-side +# legs (map blocks, registries) from reddening Hands PRs on drift Hands cannot +# cause; organs not checked out (Brain, Heart) are skipped by the check itself, +# so this gates exactly Hands. PyAutoMind is checked out as a sibling purely to +# supply the checker and the body map — it is pinned to main, so this gate +# measures Hands against the CURRENT organism identity, the same pairing a local +# workspace has. Both repos are public, so the default GITHUB_TOKEN suffices. # # Dependencies are named explicitly rather than `-r requirements.txt`, which # would drag in jupyterlab + ipykernel and turn a 6s gate into a slow one: @@ -71,7 +80,17 @@ jobs: # stay equal, so this list cannot silently fall behind that policy. python-version: ["3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - name: Checkout PyAutoHands + uses: actions/checkout@v4 + with: + path: PyAutoHands + # The checker + body map the firewall gate runs. Pinned to main: this + # gate measures Hands against the CURRENT organism identity. + - name: Checkout PyAutoMind (the firewall checker + body map) + uses: actions/checkout@v4 + with: + repository: PyAutoLabs/PyAutoMind + path: PyAutoMind - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -80,4 +99,10 @@ jobs: - name: Install (the whole dependency set — see header) run: pip install pytest PyYAML ipynb-py-convert Pillow - name: Run tests + working-directory: PyAutoHands run: pytest tests/ -q + - name: Tenant firewall (instance facts stay in declared config surfaces) + run: > + python3 PyAutoMind/scripts/repos_sync.py --check + --only "tenant firewall (organ code)" + --root "$GITHUB_WORKSPACE" diff --git a/tests/test_pre_build_staging.py b/tests/test_pre_build_staging.py index d30b434..f0821fc 100644 --- a/tests/test_pre_build_staging.py +++ b/tests/test_pre_build_staging.py @@ -26,6 +26,12 @@ # The repos pre_build walks, parsed from the script so this fixture cannot # drift away from the executor the way a second hand-written list would. +# +# CONVENTION (tenant firewall, PyAutoMind issue #198): every repo name below +# this line is DERIVED from SPECS — never written out as a literal. A hardcoded +# satellite name here is an instance fact in organ code, which +# `PyAutoMind/scripts/repos_sync.py --check` flags as tenant-firewall drift, and +# it would drift against pre_build.sh besides. Pick with SPECS[i][0]. SPECS = re.findall( r'^\s+"(\S+)\s+(\S+)\s+(\S+)\s+(\S+)"', PRE_BUILD.read_text(), re.MULTILINE ) @@ -56,9 +62,11 @@ def _commit_all(repo, message): @pytest.fixture def base(tmp_path): """A throwaway PYAUTOBASE: PyAutoHands + every workspace pre_build walks.""" - assert SPECS, "failed to parse WORKSPACE_SPECS out of pre_build.sh" + assert len(SPECS) > 1, "failed to parse WORKSPACE_SPECS out of pre_build.sh" - pyautobase = tmp_path / "PyAutoLabs" + # Any name does: pre_build.sh derives PYAUTOBASE from its own location, so + # the root is never matched by name. + pyautobase = tmp_path / "organism" # PyAutoHands itself, holding the script under test. pre_build requires it # on a clean main before it will do anything. @@ -138,7 +146,8 @@ def _run(base, stub_bin): def test_untracked_wip_aborts_before_anything_is_touched(base, stub_bin): """A human's uncommitted script blocks the release and is left alone.""" - victim = base / "autolens_assistant" / "scripts" / "wip_private.py" + victim_repo = SPECS[-1][0] + victim = base / victim_repo / "scripts" / "wip_private.py" victim.write_text("secret = 1\n") original = victim.read_bytes() @@ -147,7 +156,7 @@ def test_untracked_wip_aborts_before_anything_is_touched(base, stub_bin): assert result.returncode != 0, result.stdout assert "ABORT: uncommitted work" in result.stderr # Names the repo and the exact path, so one run surfaces the whole problem. - assert "autolens_assistant" in result.stderr + assert victim_repo in result.stderr assert "scripts/wip_private.py" in result.stderr # Untouched on disk: the abort precedes black. @@ -173,7 +182,7 @@ def test_wip_in_any_repo_is_reported_together(base, stub_bin): def test_gitignored_files_do_not_block_a_release(base, stub_bin): """`--exclude-standard` keeps output/ and other ignored cruft out.""" - work = base / "autolens_workspace" + work = base / SPECS[0][0] (work / ".gitignore").write_text("scripts/scratch/\n") _commit_all(work, "fixture gitignore") (work / "scripts" / "scratch").mkdir() @@ -201,19 +210,20 @@ def test_generated_notebooks_are_still_staged(base, stub_bin): def test_missing_checkout_aborts_in_the_preflight(base, stub_bin): """A missing repo fails clearly up front, not as a bare `cd` error midway.""" - shutil.rmtree(base / "HowToFit") + missing, survivor = SPECS[0][0], SPECS[-1][0] + shutil.rmtree(base / missing) result = _run(base, stub_bin) assert result.returncode != 0 - assert "HowToFit is missing or is not a git repo" in result.stderr + assert f"{missing} is missing or is not a git repo" in result.stderr # Nothing was published before the failure was noticed. - assert "pre build" not in _git(base / "autofit_workspace", "log", "--oneline") + assert "pre build" not in _git(base / survivor, "log", "--oneline") def test_tracked_deletions_are_staged(base, stub_bin): """`git add -u` must carry a retired notebook's deletion into the commit.""" - work = base / "autolens_workspace" + work = base / SPECS[0][0] (work / "notebooks" / "committed.ipynb").unlink() result = _run(base, stub_bin)