Skip to content

A test that runs agac runs the tree it lives in (1270) - #1321

Open
Muizzkolapo wants to merge 6 commits into
claude/unassociated-issues-triage-ox33e9-1275from
claude/unassociated-issues-triage-ox33e9-1270
Open

Muizzkolapo wants to merge 6 commits into
claude/unassociated-issues-triage-ox33e9-1275from
claude/unassociated-issues-triage-ox33e9-1270

Conversation

@Muizzkolapo

@Muizzkolapo Muizzkolapo commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Description

Stacked on #1313 (#1275).

Validated, not trusted

  • Several integration tests run Path(sys.executable).parent / "agac": true. Nine launches in six files; _agac in test_retry_selection_under_batch.py serves three more.
  • That script imports agent_actions from wherever the editable install points: true. With PYTHONVERBOSE, the console script run from a worktree loads agent_actions/__init__ from the main checkout, and the worktree's once PYTHONPATH names it. From a neutral cwd, import agent_actions resolves to the main checkout too.
  • With the main checkout behind, all four tests of test_a_batch_action_keeps_no_row_its_guard_filters.py fail on a worktree that holds their fix: true. The main checkout was 41 commits behind. With PYTHONPATH unset, the files that run agac gave 4 failed, exactly those, 155 passed and 1 xfailed. With it set, the four pass.
  • In-process CliRunner tests are unaffected: true. They use the pytest process's path, which pytest.ini sets.
  • "The subprocess gets no PYTHONPATH": partly. The launches inherit os.environ, so a PYTHONPATH set on the command line reaches them. pytest.ini's pythonpath never becomes an environment variable.
  • The suggested [sys.executable, "-m", "agent_actions.cli.main"] would not fix it. The tests run the CLI from a copy of the project, and -m puts that directory on the path, not the tree. From a directory outside the tree, python -m agent_actions.cli.main --version loads the main checkout.

Root cause

A console script's path is its bin directory, then site-packages, where the editable install's .pth names the main checkout. pytest.ini's pythonpath = . changes only the pytest process's path and never reaches a subprocess.

The fix

  • One launcher. tests/_support/agac_cli.py::run_agac runs the console script with the tree the tests live in first on PYTHONPATH. All nine launches call it, and the shared _agac and _run_workflow helpers wrap it. The console script stays: -m would not help, and it is what users run.
  • A PYTHONPATH already set stays, behind the tree, whether inherited or passed in env=.
  • A probe. agac list-udfs imports a tool that records agent_actions.__file__ from inside the CLI process, and the test asserts it is this tree's. It runs with PYTHONPATH cleared, and with a decoy agent_actions on PYTHONPATH that raises on import. A PYTHONPATH entry outranks the editable install, so this fails in CI too if the tree is not first or the entry is lost.
  • An audit. An AST walk fails on any test outside tests/manual that names agac or agent_actions.cli.main as a string of its own, or hands a call a command line starting with agac, positionally or as args=. A positive control shows it finds nine launch shapes.

Behaviour changes, stated

  • Test-only: no package code changed. The changelog entry is filed under "Under the Hood".
  • A test that runs agac as a subprocess now imports agent_actions from its own tree, with or without PYTHONPATH. In CI and the main checkout the tree is the editable install, so nothing changes there.
  • Launches that passed no env now get os.environ plus PYTHONPATH. The timeout stays 300 s.
  • A test outside tests/manual that launches agac without run_agac fails the audit.

Not covered here

  • Tests that start sys.executable -c subprocesses (in test_topological_sort_is_deterministic.py, test_inferred_dependency_order_is_deterministic.py and test_cli_hardening.py) get the tree only from the cwd; two also drop PYTHONPATH. Run from another directory, they import the editable install.
  • The tests/manual smoke runner runs the agac on PATH. It is exempt from the audit because it is run by hand.

Related Issue

Fixes #1270

Type of Change

  • Bug fix

Checklist

  • I have added a changelog entry (task changelog:new)
  • I have added/updated tests as needed
    • tests/integration/test_a_cli_test_runs_the_tree_it_lives_in.py:
      • the agac a test runs imports agent_actions from this tree;
      • a PYTHONPATH already set, inherited or passed, stays on the path behind this tree;
      • the audit finds agac launched in each of nine shapes;
      • no test launches agac but through the shared launcher.
    • The six files that launched agac themselves now call run_agac.
  • task check passes (ruff, format, mypy)
  • task test passes on this stack (12687 passed at the top of A retry resumes a halted action in full instead of narrowing it (1267) #1327 (agac retry completes a halted action on the records it names, never answering the ones past the halt #1267); only the 8 known sandbox failures; agac subprocess tests run with PYTHONPATH set). At this branch's tip, its own tests and the cli, workflow, processing and llm/batch unit suites pass.

Verification

  • RED first: on the parent, with PYTHONPATH unset, the probe fails because the CLI process imported the main checkout's agent_actions/__init__.py. With PYTHONPATH set it passes, which is the masking the issue describes. With the fix, every file that runs agac passes with PYTHONPATH unset, where four tests failed before.
  • Mutation: each mutant was run with PYTHONPATH unset, set to the tree, and set to the main checkout, and each is killed in all three:
    • removing the fix;
    • putting an inherited entry first, or dropping it;
    • applying the caller's env after PYTHONPATH;
    • resolving the tree one level too shallow;
    • an audit that never matches (all nine shape cases fail), the old one-line regex (six fail), and one that ignores args=.
  • Review (two lenses): YES_WITH_ISSUES from both. Addressed:
    • the audit was a regex that missed with_name("agac"), -m agent_actions.cli.main, a wrapped list and a shell string, while the docs said it caught any other launch;
    • the audit had no positive control;
    • a regression inside the launcher was caught only in a second worktree with PYTHONPATH unset, and a dropped PYTHONPATH not at all;
    • a PYTHONPATH passed in env= was dropped;
    • the tests/manual exemption matched any folder named manual.
    • Declined: filing the changelog as "Bug Fix", since no package user sees a difference and the repo files test-infrastructure changes under the hood; and the python -c subprocesses, which are outside this issue's console-script scope.

Restacked onto #1313

The integration tests that drive the CLI launch the venv's `agac` console
script as a subprocess. That script imports `agent_actions` from wherever the
editable install points, and pytest.ini's `pythonpath = .` reaches only the
pytest process. From a second worktree the subprocess therefore runs the main
checkout's code, and those tests pass or fail on it whatever the change under
test.

The new test drops a tool into a project and lets `agac list-udfs` import it
through the launcher the batch CLI tests share. The tool records which
`agent_actions` the CLI process imported. Run from a worktree with no
PYTHONPATH set, it reports the main checkout's package.
The integration tests that drive the CLI as a subprocess launched the venv's
`agac` console script themselves, nine times across six files. The script
imports `agent_actions` from wherever the editable install points, and
pytest.ini's `pythonpath = .` reaches only the pytest process. From a second
worktree with no PYTHONPATH set, those tests ran the main checkout's code.
With the main checkout 41 commits behind, all four tests in
test_a_batch_action_keeps_no_row_its_guard_filters.py failed on a worktree
that held the fix they pin, and passed once PYTHONPATH named the worktree.

Every launch now goes through run_agac in tests/_support/agac_cli.py. It runs
the console script with this tree first on PYTHONPATH, keeping any PYTHONPATH
already set after it. The RED test now calls it directly. A second test fails
on any test outside tests/manual that launches the console script some other
way. Such a launch passes in CI, where the editable install is the tree under
test, so nothing else would catch it.

The issue's other suggestion, `python -m agent_actions.cli.main`, does not fix
it alone. The tests run the CLI from a copy of the project, -m puts that
directory on the path, and the editable install still wins.

tests/integration/README.md says how to run agac from a test. The changelog
entry is filed under the hood: no user of the package sees a difference.
Nothing pinned what run_agac does with a PYTHONPATH already set. The probe
test inherited the shell's PYTHONPATH, which under the usual test command
and in CI names the tree, so it passed with the fix reverted inside
run_agac; putting the inherited entry first, or dropping it, passed
everywhere. A PYTHONPATH a caller passed in env= was silently replaced by
the tree rather than kept behind it.

run_agac now reads PYTHONPATH from the caller's env merged over the
process's, and keeps it behind the tree either way. The probe test clears
PYTHONPATH so it measures run_agac rather than the shell. A new test,
inherited and passed in env=, sets PYTHONPATH to a decoy whose
`agent_actions` raises on import and whose own module the probe imports:
the run fails unless the tree is first and the entry is kept. A PYTHONPATH
entry outranks the editable install, so it fails in CI too. Reverting the
fix, putting the entry first, dropping it, or pointing REPO one level too
shallow each fails it with PYTHONPATH unset, naming the tree, or naming
another checkout; the previous run_agac fails its env= case.
The audit matched one regex per line, for `agac` after `/`, `[` or
`which(` on the same line. The changelog, the integration README and the
previous commit said it failed on any test that launched `agac` some other
way, but `Path(sys.executable).with_name("agac")`, `python -m
agent_actions.cli.main`, a list ruff has wrapped so `"agac",` sits on its
own line, and a shell string all passed it while running the main
checkout. Nothing showed the pattern matched anything: a regex that never
matched passed too. Its tests/manual exemption skipped any directory named
manual anywhere under tests/.

The audit now parses each test file and flags a string that is exactly
`agac` or `agent_actions.cli.main`, wherever it sits, and a call whose
first argument is a command line, plain or f-string, starting with `agac`.
A parametrized test shows it finding each shape: a path segment, a sibling
name, a list element, a wrapped list, a which() lookup, -m, and a shell
string and f-string. The old regex misses five of the eight, and a
detector that finds nothing misses all of them. Only tests/manual itself
is exempt, beside the launcher and the audit, which names what it looks
for. Over today's tree the audit flags nothing.

The README and the changelog entry now say which shapes are caught and
that the launcher keeps an existing PYTHONPATH behind the tree.
The audit flagged a call whose first positional argument was a command
line starting with `agac`, so `subprocess.run(args="agac run ...",
shell=True)` passed it while running the main checkout, though the
README and the changelog entry say a call handed such a command line
fails it. The audit now reads the `args` keyword when a call has no
positional argument, and the test that shows it finding each launch
shape has a case for it, which fails without the change.
…othing

The audit fails on any test that names `agac` as a string of its own or
hands a call a command line starting with `agac`. The docs check,
tests/unit/cli/test_a_documented_command_exists.py, does both: it picks
out the code-block lines whose first word is `agac`, gives the command
tree it walks `agac` as its name, and writes a page whose first line is
prose starting with `agac`. It reads those commands as text and walks
the command tree in-process; it launches nothing, so it runs this tree
whatever the editable install points at.

The audit now passes over that file beside the launcher and itself, and
the integration README says so.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant