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
26 changes: 19 additions & 7 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@ name: Brain Tests
# seams ran in no CI at all and a Brain PR's only gate was whatever the
# authoring session happened to run locally.
#
# Deliberately ONLY pytest. It must not run `pyauto-brain <agent>` against live
# repos or reach the network — those need the full workspace and belong to the
# scheduled health/nightly drivers, not a PR gate. The suite is stdlib + PyYAML
# only, so it stays fast (~30s) and flake-free.
# pytest plus ONE drift leg. The suite must not run `pyauto-brain <agent>`
# against live repos or reach the network — those need the full workspace and
# belong to the scheduled health/nightly drivers, not a PR gate. The suite is
# stdlib + PyYAML only, so it stays fast (~30s) 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 Brain PR can cause is verified here, in the PR that authors it.
# `--only` keeps Mind-side legs (map blocks, registries) from reddening Brain
# PRs on drift Brain cannot cause; organs not checked out (Heart, Hands) are
# skipped by the check itself, so this gates exactly Brain.
#
# TWO repos are checked out on purpose. PyAutoBrain alone cannot even COLLECT
# the suite: `agents/faculties/sizing/_sizing.py` reads the body map
# (`BRAIN_HOME.parent / "PyAutoMind" / "repos.yaml"`) at import time and is
# deliberately strict, so `test_policy_seams.py` and `test_sizing_paths.py`
# error out before a single test runs. Checking PyAutoMind out as a sibling
# reproduces the workspace layout the code assumes. Both repos are public, so
# the default GITHUB_TOKEN suffices — no PAT. No other sibling repo is needed
# (verified by running the suite against exactly this two-repo layout).
# reproduces the workspace layout the code assumes — the same layout the
# firewall gate's `--root` needs. Both repos are public, so the default
# GITHUB_TOKEN suffices — no PAT. No other sibling repo is needed (verified by
# running the suite against exactly this two-repo layout).

# One run per commit: PR events carry the CI; pushes only build main.
# Superseded runs are cancelled on PR refs only — a cancelled main run would
Expand Down Expand Up @@ -62,3 +69,8 @@ jobs:
- name: Run tests
working-directory: PyAutoBrain
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"
10 changes: 7 additions & 3 deletions tests/test_intake_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,14 @@ def test_every_backlog_prompt_is_a_bullet_not_a_wide_table_row(tmp_path):
# --------------------------------------------------------------------------- #
# in flight: the issue link is the registry's, and the status is live
# --------------------------------------------------------------------------- #
# The issue URLs are deliberately synthetic (ExampleOrg/Widgets): the dashboard
# regex captures whole URLs and never inspects the owner, so a real GitHub
# owner here would be an instance fact in organ code — the tenant firewall's
# concern (PyAutoMind/scripts/repos_sync.py) — for no test value.
ACTIVE_MD = """# Active Tasks

## widget-rework
- issue: https://github.com/PyAutoLabs/Widgets/issues/42 (opened after the spike)
- issue: https://github.com/ExampleOrg/Widgets/issues/42 (opened after the spike)
- status: library-dev — branch pushed, awaiting review
- prompt: active/widget_rework.md
"""
Expand All @@ -118,7 +122,7 @@ def test_in_flight_links_the_registry_issue_and_its_live_status(tmp_path):
active={"widget_rework.md": _prompt("Widget rework")},
registries={"active.md": ACTIVE_MD})
flight = _page(mind).split("## In flight")[1].split("## Parked")[0]
assert "[issue #42](https://github.com/PyAutoLabs/Widgets/issues/42)" in flight, \
assert "[issue #42](https://github.com/ExampleOrg/Widgets/issues/42)" in flight, \
"the link must be the matched URL, not the field's trailing prose"
assert "(opened after the spike)" not in flight
assert "library-dev" in flight
Expand All @@ -129,7 +133,7 @@ def test_in_flight_links_the_registry_issue_and_its_live_status(tmp_path):
def test_in_flight_prompt_with_no_registry_row_claims_no_issue(tmp_path):
"""Silence beats a wrong link: prose issue URLs are usually cross-references."""
body = _prompt("Orphan task") + \
"\nFollow-up to https://github.com/PyAutoLabs/Widgets/issues/7 (unrelated).\n"
"\nFollow-up to https://github.com/ExampleOrg/Widgets/issues/7 (unrelated).\n"
mind = _mind(tmp_path, active={"orphan.md": body})
flight = _page(mind).split("## In flight")[1].split("## Parked")[0]
assert "Orphan task" in flight
Expand Down
10 changes: 7 additions & 3 deletions tests/test_profiling_conductor.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Contract tests for the profiling conductor's CLI footing.

Hermetic: every test builds a synthetic `autolens_profiling` fixture in a temp
Hermetic: every test builds a synthetic `profiling_workspace` fixture in a temp
dir and passes it via `--workspace`, so the assertions never depend on the state
of the real checkout (whose corpus grows every campaign).
of the real checkout (whose corpus grows every campaign). The fixture directory
name is deliberately NOT the real workspace's: every invocation passes
`--workspace` explicitly (proving the override is honoured), so a real repo name
here would be an instance fact in organ code — the tenant firewall's concern
(PyAutoMind/scripts/repos_sync.py) — for no test value.

Profiling was the only conductor without a test file when the compile axis was
added; the runtime-axis cases here exist to hold that surface still while the
Expand Down Expand Up @@ -60,7 +64,7 @@ def _record(**kw):


def _workspace(tmp_path, records_by_file=None):
ws = tmp_path / "autolens_profiling"
ws = tmp_path / "profiling_workspace"
lr = ws / "scripts" / "misc" / "likelihood_runtime"
lr.mkdir(parents=True)
(lr / "sweep.py").write_text(FIXTURE_CELLS)
Expand Down
58 changes: 32 additions & 26 deletions tests/test_worktree_conflict_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
start_library/start_workspace references still document:

- repos:
- PyAutoFit: feature/foo
- RepoA: feature/foo

Every writer drifted to the paren form instead:

- repos:
- autolens_workspace (feature/foo)
- RepoB (feature/foo)

which the `": "` split swallowed whole, so `repo` never equalled the requested
repo name and the guard exited 0 for every task. The parser now accepts both
Expand All @@ -28,13 +28,19 @@

WORKTREE_SH = Path(__file__).resolve().parents[1] / "bin" / "worktree.sh"

# Fixture repo names are deliberately synthetic (RepoA…RepoG): the awk in
# worktree.sh does plain string compares and validates against no repo list,
# so a real name here would be an instance fact in organ code — the tenant
# firewall's concern (PyAutoMind/scripts/repos_sync.py) — for no test value.
# Keep them synthetic.

PAREN = """# Active Tasks

## paren-task
- issue: http://x
- worktree: ~/wt/paren-task
- repos:
- autolens_workspace (feature/paren-task)
- RepoB (feature/paren-task)
"""

COLON = """# Active Tasks
Expand All @@ -43,15 +49,15 @@
- issue: http://y
- worktree: ~/wt/colon-task
- repos:
- PyAutoFit: feature/colon-task
- RepoA: feature/colon-task
"""

# `repos:` before `worktree:` — both orderings occur in the real ledger.
REPOS_FIRST = """# Active Tasks

## rbw-task
- repos:
- PyAutoArray (feature/rbw-task)
- RepoC (feature/rbw-task)
- worktree: ~/wt/rbw-task
"""

Expand All @@ -61,24 +67,24 @@
## first-task
- worktree: ~/wt/first-task
- repos:
- autolens_workspace (feature/first-task)
- RepoB (feature/first-task)

## second-task
- worktree: ~/wt/second-task
- repos:
- autolens_workspace (feature/second-task)
- RepoB (feature/second-task)
"""

# Both shapes carry trailing notes in the real ledger, and some claims name no
# branch at all. Sampled from active.md history: 258 claim lines, 162 colon-form,
# 139 paren-form, plus bare names like ` - PyAutoReduce`.
# 139 paren-form, plus bare names like ` - RepoG`.
ANNOTATED = """# Active Tasks

## annotated-task
- worktree: ~/wt/annotated-task
- repos:
- HowToFit: feature/howto-smoke (base 65e8fbd == origin/main)
- PyAutoReduce
- RepoF: feature/howto-smoke (base 65e8fbd == origin/main)
- RepoG
"""

# A task that claims nothing — the `repos-none-claimed:` shape live entries use.
Expand Down Expand Up @@ -120,29 +126,29 @@ def _claims(tmp_path: Path, active_body: str) -> list[list[str]]:
# --- the regression: the paren form is what every writer emits ----------------

def test_paren_form_claim_conflicts(tmp_path):
proc = _run(tmp_path, PAREN, "worktree_check_conflict new-task autolens_workspace")
proc = _run(tmp_path, PAREN, "worktree_check_conflict new-task RepoB")
assert proc.returncode == 1, "paren-form claim did not register as a conflict"
assert "paren-task" in proc.stderr


def test_paren_form_repo_is_bare(tmp_path):
task, repo, branch, wt = _claims(tmp_path, PAREN)[0]
assert repo == "autolens_workspace"
assert repo == "RepoB"
assert branch == "feature/paren-task"
assert wt == "~/wt/paren-task"


# --- back-compat: the documented colon form must keep working -----------------

def test_colon_form_claim_conflicts(tmp_path):
proc = _run(tmp_path, COLON, "worktree_check_conflict new-task PyAutoFit")
proc = _run(tmp_path, COLON, "worktree_check_conflict new-task RepoA")
assert proc.returncode == 1
assert "colon-task" in proc.stderr


def test_colon_form_repo_is_bare(tmp_path):
task, repo, branch, wt = _claims(tmp_path, COLON)[0]
assert repo == "PyAutoFit"
assert repo == "RepoA"
assert branch == "feature/colon-task"


Expand All @@ -152,46 +158,46 @@ def test_worktree_captured_when_repos_precede_it(tmp_path):
# The awk set `wt` on sight, so a `repos:` block above `worktree:` emitted
# "-" and repo_cleanup lost the worktree path for that claim.
task, repo, branch, wt = _claims(tmp_path, REPOS_FIRST)[0]
assert repo == "PyAutoArray"
assert repo == "RepoC"
assert wt == "~/wt/rbw-task", "worktree: must be captured regardless of field order"


def test_worktree_does_not_leak_between_tasks(tmp_path):
body = REPOS_FIRST + "\n## later-task\n- repos:\n - PyAutoLens (feature/later)\n"
body = REPOS_FIRST + "\n## later-task\n- repos:\n - RepoD (feature/later)\n"
rows = {r[0]: r for r in _claims(tmp_path, body)}
assert rows["later-task"][3] == "-", "a task with no worktree: must not inherit one"


# --- the guard's own contract -------------------------------------------------

def test_task_does_not_conflict_with_itself(tmp_path):
proc = _run(tmp_path, PAREN, "worktree_check_conflict paren-task autolens_workspace")
proc = _run(tmp_path, PAREN, "worktree_check_conflict paren-task RepoB")
assert proc.returncode == 0, proc.stderr


def test_unclaimed_repo_does_not_conflict(tmp_path):
proc = _run(tmp_path, PAREN, "worktree_check_conflict new-task PyAutoGalaxy")
proc = _run(tmp_path, PAREN, "worktree_check_conflict new-task RepoE")
assert proc.returncode == 0, proc.stderr


def test_conflict_names_every_claiming_task(tmp_path):
proc = _run(tmp_path, TWO_CLAIMS, "worktree_check_conflict new-task autolens_workspace")
proc = _run(tmp_path, TWO_CLAIMS, "worktree_check_conflict new-task RepoB")
assert proc.returncode == 1
assert "first-task" in proc.stderr
assert "second-task" in proc.stderr


def test_trailing_note_does_not_corrupt_repo(tmp_path):
# ` - HowToFit: feature/howto-smoke (base 65e8fbd == origin/main)` — the
# ` - RepoF: feature/howto-smoke (base 65e8fbd == origin/main)` — the
# repo name is what the guard compares, so the note must not reach it.
rows = {r[1]: r for r in _claims(tmp_path, ANNOTATED)}
assert set(rows) == {"HowToFit", "PyAutoReduce"}
assert rows["HowToFit"][2].startswith("feature/howto-smoke")
assert set(rows) == {"RepoF", "RepoG"}
assert rows["RepoF"][2].startswith("feature/howto-smoke")


def test_claim_without_a_branch_still_claims(tmp_path):
# ` - PyAutoReduce` with no branch is a real shape; it must still conflict.
proc = _run(tmp_path, ANNOTATED, "worktree_check_conflict new-task PyAutoReduce")
# ` - RepoG` with no branch is a real shape; it must still conflict.
proc = _run(tmp_path, ANNOTATED, "worktree_check_conflict new-task RepoG")
assert proc.returncode == 1
assert "annotated-task" in proc.stderr

Expand Down Expand Up @@ -222,7 +228,7 @@ def test_conflict_guard_fails_closed_when_the_registry_is_missing(tmp_path):
decides whether a task may start — so "I could not check" has to stop the
caller, not wave it through.
"""
proc = _run(tmp_path, None, "worktree_check_conflict some-task PyAutoFit")
proc = _run(tmp_path, None, "worktree_check_conflict some-task RepoA")
assert proc.returncode != 0
assert "CANNOT VERIFY" in proc.stderr
assert "PYAUTO_MAIN" in proc.stderr # names what to set
Expand All @@ -231,6 +237,6 @@ def test_conflict_guard_fails_closed_when_the_registry_is_missing(tmp_path):
def test_conflict_guard_can_be_forced_past_a_missing_registry(tmp_path):
"""The escape hatch is explicit and loud — never the default."""
proc = _run(tmp_path, None,
"worktree_check_conflict --allow-missing-registry t PyAutoFit")
"worktree_check_conflict --allow-missing-registry t RepoA")
assert proc.returncode == 0
assert "UNGUARDED" in proc.stderr
Loading