Skip to content

Commit 0bc8fd9

Browse files
authored
Merge pull request #373 from PyAutoLabs/feature/repos-sync-config-checks
feat(repos_sync): check Heart version_skew and Hands workspaces.yaml identity (stamper deferred)
2 parents 15f1854 + d6cefb2 commit 0bc8fd9

2 files changed

Lines changed: 330 additions & 1 deletion

File tree

scripts/repos_sync.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@
4242
4343
--check (always run) verifies, against the manifest:
4444
45-
* PyAutoHeart/config/repos.yaml — polled repos exist, owners match
45+
* PyAutoHeart/config/repos.yaml — polled repos exist, owners match,
46+
smoke: and version_skew: name manifest repos and manifest package names
4647
* PyAutoHands/pre_build.sh — run_workspace repos exist
48+
* PyAutoHands/autohands/config/workspaces.yaml — run_all repos, library
49+
names/packages and slow_skip_default repos exist
4750
* PyAutoBrain/bin/ensure_workspace_labels.sh — owner/name pairs match
4851
* the hygiene conductor — the repo sets it scans are derived from this
4952
manifest, and no repo name has been hardcoded back into an array
@@ -386,6 +389,84 @@ def check_heart(root, repos):
386389
problems.append(
387390
f"Heart smoke import_names key '{name}' — not in the manifest"
388391
)
392+
# version_skew: <workspace repo> -> {library, package}. Heart compares the
393+
# library version a workspace pins against the library repo's own, so every
394+
# field here is identity the body map owns — and none of it was checked
395+
# until now, while the polled list beside it was checked from the start.
396+
# A workspace or library renamed in the map, or a package renamed on PyPI,
397+
# skewed Heart silently. Soft-skip when absent, like the smoke: block: a
398+
# Heart checkout predating it is not drift, and Heart's own loader is what
399+
# decides the block is required.
400+
for name, spec in (data.get("version_skew") or {}).items():
401+
spec = spec or {}
402+
if name not in repos:
403+
problems.append(
404+
f"Heart version_skew '{name}' — not in the manifest"
405+
)
406+
library = spec.get("library")
407+
if library not in repos:
408+
problems.append(
409+
f"Heart version_skew '{name}' library '{library}' — "
410+
f"not in the manifest"
411+
)
412+
continue
413+
expected = repos[library].get("package")
414+
if expected is None:
415+
problems.append(
416+
f"Heart version_skew '{name}' names library '{library}', "
417+
f"which has no 'package:' in the manifest"
418+
)
419+
elif spec.get("package") != expected:
420+
problems.append(
421+
f"Heart version_skew '{name}' package "
422+
f"'{spec.get('package')}', manifest says '{expected}'"
423+
)
424+
return problems
425+
426+
427+
def check_hands_workspaces(root, repos):
428+
"""The Build run matrix names repos too.
429+
430+
`PyAutoHands/autohands/config/workspaces.yaml` says in its own header that
431+
repo identity must match the body map and that this check flags drift. It
432+
said so from the day it was extracted and no leg read it, so the claim was
433+
aspirational. Policy stays Hands' — the short keys, the report directories,
434+
the release matrix order are all its own; only the names are checked."""
435+
path = root / "PyAutoHands/autohands/config/workspaces.yaml"
436+
if not path.exists():
437+
return []
438+
data = yaml.safe_load(path.read_text()) or {}
439+
problems = []
440+
for key, spec in (data.get("run_all") or {}).items():
441+
repo = (spec or {}).get("repo")
442+
if repo not in repos:
443+
problems.append(
444+
f"Hands run_all '{key}' repo '{repo}' — not in the manifest"
445+
)
446+
for entry in data.get("libraries") or ():
447+
entry = entry or {}
448+
name = entry.get("name")
449+
if name not in repos:
450+
problems.append(
451+
f"Hands libraries entry '{name}' — not in the manifest"
452+
)
453+
continue
454+
expected = repos[name].get("package")
455+
if expected is None:
456+
problems.append(
457+
f"Hands libraries entry '{name}' has no 'package:' in the "
458+
f"manifest"
459+
)
460+
elif entry.get("package") != expected:
461+
problems.append(
462+
f"Hands libraries '{name}' package '{entry.get('package')}', "
463+
f"manifest says '{expected}'"
464+
)
465+
for name in data.get("slow_skip_default") or ():
466+
if name not in repos:
467+
problems.append(
468+
f"Hands slow_skip_default '{name}' — not in the manifest"
469+
)
389470
return problems
390471

391472

@@ -1257,6 +1338,8 @@ def main():
12571338
checks = {
12581339
"PyAutoHeart/config/repos.yaml": lambda: check_heart(root, repos),
12591340
"PyAutoHands/pre_build.sh": lambda: check_pre_build(root, repos),
1341+
"PyAutoHands/autohands/config/workspaces.yaml":
1342+
lambda: check_hands_workspaces(root, repos),
12601343
"ensure_workspace_labels.sh": lambda: check_labels(root, repos),
12611344
"hygiene conductor coverage":
12621345
lambda: check_hygiene_coverage(root, repos, mind_root),
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
"""The organ *config* surfaces must not drift from the body map.
2+
3+
Two mirrors sat unchecked while the file headers claimed otherwise:
4+
5+
1. **Heart `version_skew:`** — `<workspace repo>: {library, package}`. Every
6+
field is identity the body map owns, and `check_heart` read the polled-repo
7+
list, the owners and (since PyAutoMind#198) the `smoke:` block, but never
8+
this one. A workspace renamed in the map, or a library whose package name
9+
changed, would have skewed Heart silently.
10+
2. **Hands `autohands/config/workspaces.yaml`** — its own header says "Repo
11+
IDENTITY must match PyAutoMind/repos.yaml (the body map); repos_sync.py
12+
--check flags drift". No leg read the file. The claim was aspirational from
13+
the day it was written.
14+
15+
Same two rules as the hygiene-coverage tests next door:
16+
17+
1. **Fictional fixtures only.** `tests/**` is KEEP-copied verbatim into the
18+
public template (see `test_spawn_privacy.py`), so nothing here names a real
19+
repository, and the tests assert the checks' logic rather than the state of
20+
whatever happens to be checked out.
21+
2. **Prove each leg FAILS.** A drift check that cannot fail is decoration.
22+
Every leg below is driven with input that must trip it.
23+
"""
24+
25+
import subprocess
26+
import sys
27+
from pathlib import Path
28+
29+
import yaml
30+
31+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
32+
33+
import repos_sync # noqa: E402
34+
35+
HEART_REL = "PyAutoHeart/config/repos.yaml"
36+
HANDS_REL = "PyAutoHands/autohands/config/workspaces.yaml"
37+
38+
# A fictional organism: two packaged libraries, one packaged organ, one library
39+
# the map declares without a package, plus a workspace and a howto.
40+
MANIFEST = {
41+
"OrganOne": {
42+
"github": "FictionalOrg/OrganOne",
43+
"category": "organ",
44+
"package": "organone",
45+
},
46+
"LibTwo": {
47+
"github": "FictionalOrg/LibTwo",
48+
"category": "library",
49+
"package": "libtwo",
50+
},
51+
"LibThree": {
52+
"github": "FictionalOrg/LibThree",
53+
"category": "library",
54+
"package": "libthree",
55+
},
56+
# Declared without a `package:` — the body map carries one only for the
57+
# libraries/organs that ship as a distribution.
58+
"LibFour": {"github": "FictionalOrg/LibFour", "category": "library"},
59+
"libtwo_workspace": {
60+
"github": "FictionalOrg/libtwo_workspace",
61+
"category": "workspace",
62+
},
63+
"HowToTwo": {"github": "FictionalOrg/HowToTwo", "category": "howto"},
64+
}
65+
66+
HEART_REPOS = {
67+
"libraries": [
68+
{"name": "LibTwo", "owner": "FictionalOrg"},
69+
{"name": "LibThree", "owner": "FictionalOrg"},
70+
],
71+
"workspaces": [{"name": "libtwo_workspace", "owner": "FictionalOrg"}],
72+
}
73+
74+
VERSION_SKEW_OK = {
75+
"libtwo_workspace": {"library": "LibTwo", "package": "libtwo"},
76+
"HowToTwo": {"library": "LibThree", "package": "libthree"},
77+
}
78+
79+
WORKSPACES_OK = {
80+
"run_all": {
81+
"libtwo": {"repo": "libtwo_workspace", "report": "libtwo"},
82+
"howtotwo": {"repo": "HowToTwo", "report": "howtotwo"},
83+
},
84+
"libraries": [
85+
{"name": "LibTwo", "package": "libtwo"},
86+
{"name": "LibThree", "package": "libthree"},
87+
],
88+
"slow_skip_default": ["libtwo_workspace"],
89+
}
90+
91+
92+
def _heart(tmp_path, *, version_skew=..., repos=None):
93+
"""Write a Heart config; `version_skew=None` omits the block entirely."""
94+
data = {"repos": HEART_REPOS if repos is None else repos}
95+
skew = VERSION_SKEW_OK if version_skew is ... else version_skew
96+
if skew is not None:
97+
data["version_skew"] = skew
98+
path = tmp_path / HEART_REL
99+
path.parent.mkdir(parents=True, exist_ok=True)
100+
path.write_text(yaml.safe_dump(data))
101+
return repos_sync.check_heart(tmp_path, MANIFEST)
102+
103+
104+
def _hands(tmp_path, data=None):
105+
path = tmp_path / HANDS_REL
106+
path.parent.mkdir(parents=True, exist_ok=True)
107+
path.write_text(yaml.safe_dump(WORKSPACES_OK if data is None else data))
108+
return repos_sync.check_hands_workspaces(tmp_path, MANIFEST)
109+
110+
111+
# --------------------------------------------------------------------------
112+
# Heart: version_skew
113+
# --------------------------------------------------------------------------
114+
115+
def test_a_version_skew_block_matching_the_map_is_clean(tmp_path):
116+
assert _heart(tmp_path) == []
117+
118+
119+
def test_a_version_skew_key_the_map_does_not_declare_is_drift(tmp_path):
120+
skew = {**VERSION_SKEW_OK, "ghost_workspace": {"library": "LibTwo",
121+
"package": "libtwo"}}
122+
123+
problems = _heart(tmp_path, version_skew=skew)
124+
125+
assert any("ghost_workspace" in p and "not in the manifest" in p
126+
for p in problems)
127+
128+
129+
def test_a_version_skew_library_the_map_does_not_declare_is_drift(tmp_path):
130+
# A library renamed in the body map leaves Heart comparing against nothing.
131+
skew = {"libtwo_workspace": {"library": "LibGhost", "package": "libtwo"}}
132+
133+
problems = _heart(tmp_path, version_skew=skew)
134+
135+
assert any("LibGhost" in p and "not in the manifest" in p
136+
for p in problems)
137+
138+
139+
def test_a_version_skew_package_that_is_not_the_maps_package_is_drift(tmp_path):
140+
# The exact skew the check exists for: right library, stale import name.
141+
skew = {"libtwo_workspace": {"library": "LibTwo", "package": "libtwo_old"}}
142+
143+
problems = _heart(tmp_path, version_skew=skew)
144+
145+
assert any("libtwo_old" in p and "libtwo" in p for p in problems)
146+
147+
148+
def test_a_version_skew_library_with_no_package_in_the_map_is_drift(tmp_path):
149+
# Nothing to compare against — the map does not claim this repo ships a
150+
# package, so Heart must not be asserting one.
151+
skew = {"libtwo_workspace": {"library": "LibFour", "package": "libfour"}}
152+
153+
problems = _heart(tmp_path, version_skew=skew)
154+
155+
assert any("LibFour" in p and "package" in p for p in problems)
156+
157+
158+
def test_a_heart_config_without_a_version_skew_block_is_tolerated(tmp_path):
159+
# A Heart checkout predating the block is not drift; only Heart's own
160+
# loader decides whether it is required.
161+
assert _heart(tmp_path, version_skew=None) == []
162+
163+
164+
def test_the_heart_leg_skips_when_heart_is_not_checked_out(tmp_path):
165+
# Partial/web checkouts are normal; a missing organ is skipped, not failed.
166+
assert repos_sync.check_heart(tmp_path, MANIFEST) == []
167+
168+
169+
# --------------------------------------------------------------------------
170+
# Hands: workspaces.yaml
171+
# --------------------------------------------------------------------------
172+
173+
def test_a_workspaces_yaml_matching_the_map_is_clean(tmp_path):
174+
assert _hands(tmp_path) == []
175+
176+
177+
def test_a_run_all_repo_the_map_does_not_declare_is_drift(tmp_path):
178+
data = {**WORKSPACES_OK, "run_all": {
179+
**WORKSPACES_OK["run_all"],
180+
"ghost": {"repo": "ghost_workspace", "report": "ghost"},
181+
}}
182+
183+
problems = _hands(tmp_path, data)
184+
185+
assert any("ghost_workspace" in p and "not in the manifest" in p
186+
for p in problems)
187+
188+
189+
def test_a_libraries_name_the_map_does_not_declare_is_drift(tmp_path):
190+
data = {**WORKSPACES_OK,
191+
"libraries": [{"name": "LibGhost", "package": "libghost"}]}
192+
193+
problems = _hands(tmp_path, data)
194+
195+
assert any("LibGhost" in p and "not in the manifest" in p for p in problems)
196+
197+
198+
def test_a_libraries_package_that_is_not_the_maps_package_is_drift(tmp_path):
199+
# The release board renders versions from this package name; a stale one
200+
# reads as "not on PyPI" rather than as drift.
201+
data = {**WORKSPACES_OK,
202+
"libraries": [{"name": "LibTwo", "package": "libtwo_old"}]}
203+
204+
problems = _hands(tmp_path, data)
205+
206+
assert any("libtwo_old" in p and "libtwo" in p for p in problems)
207+
208+
209+
def test_a_slow_skip_default_repo_the_map_does_not_declare_is_drift(tmp_path):
210+
data = {**WORKSPACES_OK, "slow_skip_default": ["ghost_workspace"]}
211+
212+
problems = _hands(tmp_path, data)
213+
214+
assert any("ghost_workspace" in p for p in problems)
215+
216+
217+
def test_the_hands_leg_skips_when_hands_is_not_checked_out(tmp_path):
218+
assert repos_sync.check_hands_workspaces(tmp_path, MANIFEST) == []
219+
220+
221+
def test_the_hands_leg_is_registered_so_only_can_select_it(tmp_path):
222+
# --only selects by the printed label, and an unregistered leg never runs
223+
# however good the function is. `--only <unknown>` lists every registered
224+
# label, so it is the cheapest proof the leg is wired in.
225+
proc = subprocess.run(
226+
[sys.executable, str(Path(repos_sync.__file__)), "--check",
227+
"--root", str(tmp_path), "--only", "no-such-check"],
228+
capture_output=True, text=True,
229+
)
230+
231+
assert proc.returncode != 0
232+
assert f"'{HANDS_REL}'" in proc.stderr
233+
234+
235+
# --------------------------------------------------------------------------
236+
# The live tree
237+
# --------------------------------------------------------------------------
238+
239+
def test_the_real_organ_configs_match_the_real_body_map():
240+
"""The live workspace, if it is checked out here."""
241+
mind_root = Path(__file__).resolve().parents[1]
242+
root = mind_root.parent
243+
_, repos = repos_sync.load_manifest(mind_root)
244+
245+
assert repos_sync.check_heart(root, repos) == []
246+
assert repos_sync.check_hands_workspaces(root, repos) == []

0 commit comments

Comments
 (0)