Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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"
28 changes: 19 additions & 9 deletions tests/test_pre_build_staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()

Expand All @@ -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.
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
Loading