diff --git a/.github/workflows/equal-height-mass.yml b/.github/workflows/equal-height-mass.yml index 0e9c09e88..d4cd9c64b 100644 --- a/.github/workflows/equal-height-mass.yml +++ b/.github/workflows/equal-height-mass.yml @@ -5,6 +5,7 @@ name: Equal-height mass controls paths: - 'frontiers/equal_height_mass_value_20261001/**' - '.github/workflows/equal-height-mass.yml' + - 'tests/test_equal_height_workflow.py' # bound merged sources (consumed and cited_only in SOURCES.json) - 'frontiers/c7_zero_gap_limit_20260929/PROOF.md' - 'frontiers/three_fronts_20260924/LIFETIME_REMAINDER.md' @@ -31,6 +32,13 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 with: python-version: '3.11.16' + - name: Exercise complete output contracts in both modes + env: + PYTHONDONTWRITEBYTECODE: '1' + run: | + set -euo pipefail + python -B -S -m unittest discover -s tests -p test_equal_height_workflow.py -v + python -B -O -S -m unittest discover -s tests -p test_equal_height_workflow.py -v - name: Verify sources, replay both modes, reject mutants env: PYTHONDONTWRITEBYTECODE: '1' @@ -58,19 +66,31 @@ jobs: raise ValueError('merged source drifted: ' + entry['path']) if manifest.get('consumed_unmerged'): raise ValueError('this packet declares no unmerged dependency') + mutants = manifest.get('mutants') + if type(mutants) is not list or mutants != ['M1', 'M2', 'M3']: + raise RuntimeError('complete ordered M1-M3 inventory required') + # Fixed full reports measured from unchanged checker 92ba3cde in both modes. + # Byte identity is intentional; changed diagnostics need a reviewed rebind. + contracts = { + 'M1': (933, 'b6f95f60a211e6abf9c5595fcc26ebd74ea5d3c6c5ade551e94ac1ce4e98952f'), + 'M2': (938, '4cd2c6f39ed2c920d0fc69fb3a1826b9cec35465c3013d00996b4b26ce788c49'), + 'M3': (935, 'ac69c631970c2b5f6b1cba6c9457b5601148d0ca7ea087cc012e2772ffd9fa09'), + } script = str(root / 'equal_height_mass.py') expected = (root / 'RESULTS.json').read_bytes() for flags in (['-B', '-S'], ['-B', '-O', '-S']): - out = subprocess.run([sys.executable, *flags, script], check=True, capture_output=True, timeout=600).stdout - if out != expected: - raise RuntimeError('output differs from RESULTS.json under ' + ' '.join(flags)) - for m in manifest['mutants']: - rc = subprocess.run([sys.executable, *flags, script, '--mutant', m], capture_output=True, timeout=600).returncode - if rc != 1: - raise RuntimeError('mutant ' + m + ' not rejected') - rc = subprocess.run([sys.executable, *flags, script, '--mutant', 'M9'], capture_output=True, timeout=600).returncode - if rc != 2: - raise RuntimeError('unknown mutant label not rejected with exit 2') + result = subprocess.run([sys.executable, *flags, script], capture_output=True, timeout=600) + if result.returncode != 0 or result.stdout != expected or result.stderr != b'': + raise RuntimeError('baseline exit/stdout/stderr differs under ' + ' '.join(flags)) + for m in mutants: + result = subprocess.run([sys.executable, *flags, script, '--mutant', m], capture_output=True, timeout=600) + size, digest = contracts[m] + if (result.returncode != 1 or result.stderr != b'' or len(result.stdout) != size + or hashlib.sha256(result.stdout).hexdigest() != digest): + raise RuntimeError('mutant ' + m + ' differs from its complete fixed rejection report') + result = subprocess.run([sys.executable, *flags, script, '--mutant', 'M9'], capture_output=True, timeout=600) + if result.returncode != 2 or result.stdout != b'' or result.stderr != b'unknown mutant label\n': + raise RuntimeError('unknown mutant label differs from fixed exit/stdout/stderr') print(json.dumps({'scientific_effect': 'NONE', 'mathematical_acceptance': False})) PY git diff --exit-code diff --git a/reviews/equal_height_protocol_20261006/SCOPE.md b/reviews/equal_height_protocol_20261006/SCOPE.md new file mode 100644 index 000000000..747ab0598 --- /dev/null +++ b/reviews/equal_height_protocol_20261006/SCOPE.md @@ -0,0 +1,120 @@ +# Equal-height mass workflow protocol repair + +Dylan Roy — delegated AI engineering. Actual author: OpenAI / GPT-6 Astra Pro, +`remainder-followthrough-equal-height-20261006-r3`; pickup Math-#322/6017205382. +Scientific effect NONE; organizational-independence credit 0. No author self-merge. + +## Exact source and unchanged scope + +Source/base: `37bd017be7b1a48455fe651977fedc5b7105d188`. + +| Source | Bytes | Git blob | SHA256 | +|---|---:|---|---| +| Original equal-height-mass.yml |3899|663324e708fbb552fb30e9bed21a697d132989a9|389736b7bc181915b9395ce59bff5f7208ed2dbc38147c3f07e3e93a6b57e9ed| +| equal_height_mass.py |10792|92ba3cdecb4344771d6d8c3f1ba22ca4fc96e864|88a00dafb5710009bd1eeb6267a6b9a197e2cca304ce2f90df5cd138951d1ace| +| RESULTS.json |929|98001bf939cdaa4a47418ebff0c8b1e13d49d35b|9054e435878a57a6bea42b8af08c0bd51770474503990334687559f7260b06c3| + +The last two files are under `frontiers/equal_height_mass_value_20261001/`. +All three complete local byte strings were authenticated before execution. +No scientific checker, NOTE, README, RESULTS, GAMMA, gamma exploration, +SOURCES, consumed/cited proof, formal source or scientific register is modified. +No Monte Carlo regeneration or mathematical/numerical enclosure is claimed. + +## Actual measured report contracts + +The unchanged CLI was executed for baseline, M1, M2, M3 and M9 in both child +Python modes: ten complete real subprocesses, local CPython 3.13.5. +All corresponding stdout/stderr pairs match exactly; baseline equals RESULTS. + +| Stage | Exit | stdout bytes | stdout SHA256 | stderr | +|---|---:|---:|---|---| +| baseline |0|929|9054e435878a57a6bea42b8af08c0bd51770474503990334687559f7260b06c3|empty| +| M1 |1|933|b6f95f60a211e6abf9c5595fcc26ebd74ea5d3c6c5ade551e94ac1ce4e98952f|empty| +| M2 |1|938|4cd2c6f39ed2c920d0fc69fb3a1826b9cec35465c3013d00996b4b26ce788c49|empty| +| M3 |1|935|ac69c631970c2b5f6b1cba6c9457b5601148d0ca7ea087cc012e2772ffd9fa09|empty| +| M9 |2|0|e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855|`unknown mutant label` plus LF| + +M1 fails Q2 and Q3; M2 fails Q2, Q3 AND Q4; M3 fails Q3 only. Q1 remains true. +The full numeric values and all still-passing check details belong to the fixed +reports. A one-failed-check schema would not describe these genuine controls. +The stdout hashes do not certify the numerical values or locate an arbitrary +program's internal failure; they authenticate these measured output bytes. + +The repair requires the native ordered LIST M1/M2/M3 before any child; exact +baseline exit/stdout with empty stderr; each mutant's exit1/empty stderr and +entire frozen stdout size/hash; and M9's exit2/empty stdout/exact stderr. +Report byte identity, including formatting, is intentional. A changed +deterministic report requires reviewed rebinding, not a tolerance change. +Expectations are fixed before validation, never learned from the child under test. + +Ten additional genuine subprocesses through the exact changed loop matched +every characterization exit and stream hash. A pass-through recorder retained +the real results without replacing them. This loop-only replay is distinct +from a complete real-packet manifest/dependency replay, which requires hosted CI. + +## Test-first whole-shell regression + +Eight unittest methods contain 96 distinct, complete-shell synthetic scenarios. +Real committed temporary Git repositories, real Bash/Python children and +nonempty packet/consumed/cited inputs are used. No subprocess outputs are mocked. +Each fixture executes the actual workflow verification shell. Expected reports +are independently captured literals in the test source, not imported from the +production matcher. Test-only signing/maintenance switches apply only to +disposable commits; no user or persistent project setting is changed. + +The same final test bytes were run against both original and repaired workflows: + +| Workflow / outer mode | Methods / fixture outcome | Real children | +|---|---|---:| +| original / normal |74 intended assertion failures, 0 errors; 96 cases|811| +| original / optimized |74 intended assertion failures, 0 errors; 96 cases|811| +| repair / normal |8/8 methods; 96/96 expected outcomes|471| +| repair / optimized |8/8 methods; 96/96 expected outcomes|471| + +The original's 74 assertions comprise 68 noncanonical report admissions +(62 semantic protocol failures, six formatting-only cases), five bad inventory +admissions and one missing wiring assertion. The original valid control and +22 source/exit/postflight rejections behave correctly. The repaired source +admits one valid case and rejects 95 cases. Both complete repaired runs finish +without skips or partitioning (28.933s and 28.593s locally). + +Tests reach every named mutant in each child mode, including silence, unrelated +crash, wrong/noisy/trailing reports, wrong label/exit and deliberate formatting. +Same-length text and nested-type corruptions distinguish hash validation from +length-only checks. Baseline stderr and M9 output/reason are separately tested. +Every protocol rejection must stop at the exact ordered target prefix; seven +source guards and five malformed inventories stop before any child. The +tracked-output mutation reaches all ten children before final Git rejection. + +An earlier 84-case test revision produced 62 intended assertions in both modes; +it was augmented with twelve equal-length corruption cases BEFORE the fix. +Those earlier executions are retained as development history, not added to the +384 final fixture executions or reported as different product defects. +Two unavailable execution launches produced no credited numerical result; +all counted commands completed with captured outputs. + +## Source conservation and limitations + +The complete 1467-byte original source preflight is byte-identical, SHA256 +`d625514545e6d76b6271dc2f8641eafe94bb7197e2c1a6b9d0d0ed24362f860a`. +The final 137-byte JSON/Git postflight is byte-identical, SHA256 +`752aa03685dbbe54ec40c3e91b8709eb6453c41c146e9636b295b2129c586cdc`. +Sizes include YAML indentation. Only the process validation loop, one test +trigger and two full discovery commands are changed/added. Existing action +pins, Python 3.11.16, read-only permission, inherited cwd, 600-second child +budget and 20-minute job budget remain unchanged. Launch/timeout exceptions +are not caught. The final Git guard and original scientific qualifications stay. + +The local checkout is an authenticated source subset, not the full repository. +No local Lean or complete real-packet dependency replay is claimed. Hosted +dedicated/general/applicable broader results must be read on the actual +candidate; local Python 3.13.5 does not substitute for pinned-runtime success. +This is not a hostile-source sandbox, bounded-output-memory service, durable +evidence guarantee, numerical enclosure, theorem proof or global wrapper audit. +#322 remains open for the wider affected class. + +One exact-source nonauthor engineering review, any required successor read, +and actual current-candidate checks precede a separate eligible nonauthor/ +nonreader integration. Preserve original failed evidence, all reviewed blobs +and source interfaces; use guarded normal integration and verify the landed +tree and its own push evidence. A later base does not inherit old CI receipts. diff --git a/tests/test_equal_height_workflow.py b/tests/test_equal_height_workflow.py new file mode 100644 index 000000000..936e696bf --- /dev/null +++ b/tests/test_equal_height_workflow.py @@ -0,0 +1,214 @@ +"""Real whole-shell controls for the equal-height execution wrapper. + +Fixed reports are independent captures of original checker 92ba3cde; synthetic +children do not run quadrature or import production validation. Source controls, +ordered stage traces, and final Git cleanliness are exercised by the real shell. +""" +import hashlib +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github/workflows/equal-height-mass.yml" +PACKET = "frontiers/equal_height_mass_value_20261001" +STAGES = ("baseline", "M1", "M2", "M3", "M9") +REPORTS = {'baseline': '{\n "checks": {\n "Q1_conditional_hessian_law": {\n "info": "scaled GOE: Var f_ii = 2, Var f_ij = 1, all other pairs 0; E[f_ii | f = b] = -b",\n "passed": true\n },\n "Q2_scaled_GOE_normalisation": {\n "info": {\n "2": 20.0530262,\n "3": 213.258381\n },\n "passed": true\n },\n "Q3_total_densities_closed_forms": {\n "info": {\n "2": {\n "n_max": 0.09188814923,\n "n_max_closed": 0.09188814924,\n "n_sad": 0.1837762985,\n "n_sad_closed": 0.1837762985\n },\n "3": {\n "n_max": 0.03486240891,\n "n_max_closed": 0.03486240894,\n "n_sad": 0.106507305,\n "n_sad_closed": 0.106507305\n }\n },\n "passed": true\n },\n "Q4_beta_two_resolutions": {\n "info": {\n "24^3 beta_3": 10.23061,\n "beta_2": 0.003122769186,\n "beta_3": 0.0007400614572\n },\n "passed": true\n }\n },\n "mutant": null,\n "object": "CL-EQUAL-HEIGHT-MASS-20261001-v1",\n "passed": true,\n "scientific_effect": "NONE"\n}\n', 'M1': '{\n "checks": {\n "Q1_conditional_hessian_law": {\n "info": "scaled GOE: Var f_ii = 2, Var f_ij = 1, all other pairs 0; E[f_ii | f = b] = -b",\n "passed": true\n },\n "Q2_scaled_GOE_normalisation": {\n "info": {\n "2": 36.83976148,\n "3": 719.7470357\n },\n "passed": false\n },\n "Q3_total_densities_closed_forms": {\n "info": {\n "2": {\n "n_max": 0.1064044144,\n "n_max_closed": 0.09188814924,\n "n_sad": 0.2923863004,\n "n_sad_closed": 0.1837762985\n },\n "3": {\n "n_max": 0.0370511896,\n "n_max_closed": 0.03486240894,\n "n_sad": 0.1972540471,\n "n_sad_closed": 0.106507305\n }\n },\n "passed": false\n },\n "Q4_beta_two_resolutions": {\n "info": {\n "24^3 beta_3": 21.824481,\n "beta_2": 0.006405517566,\n "beta_3": 0.001578738502\n },\n "passed": true\n }\n },\n "mutant": "M1",\n "object": "CL-EQUAL-HEIGHT-MASS-20261001-v1",\n "passed": false,\n "scientific_effect": "NONE"\n}\n', 'M2': '{\n "checks": {\n "Q1_conditional_hessian_law": {\n "info": "scaled GOE: Var f_ii = 2, Var f_ij = 1, all other pairs 0; E[f_ii | f = b] = -b",\n "passed": true\n },\n "Q2_scaled_GOE_normalisation": {\n "info": {\n "2": 12.56637061,\n "3": 44.54662398\n },\n "passed": false\n },\n "Q3_total_densities_closed_forms": {\n "info": {\n "2": {\n "n_max": 0.1200418013,\n "n_max_closed": 0.09188814924,\n "n_sad": 0.08092865947,\n "n_sad_closed": 0.1837762985\n },\n "3": {\n "n_max": 0.06896003859,\n "n_max_closed": 0.03486240894,\n "n_sad": 0.03138667793,\n "n_sad_closed": 0.106507305\n }\n },\n "passed": false\n },\n "Q4_beta_two_resolutions": {\n "info": {\n "24^3 beta_3": 6.5702789,\n "beta_2": 0.001937897205,\n "beta_3": 0.0004752805934\n },\n "passed": false\n }\n },\n "mutant": "M2",\n "object": "CL-EQUAL-HEIGHT-MASS-20261001-v1",\n "passed": false,\n "scientific_effect": "NONE"\n}\n', 'M3': '{\n "checks": {\n "Q1_conditional_hessian_law": {\n "info": "scaled GOE: Var f_ii = 2, Var f_ij = 1, all other pairs 0; E[f_ii | f = b] = -b",\n "passed": true\n },\n "Q2_scaled_GOE_normalisation": {\n "info": {\n "2": 20.0530262,\n "3": 213.258381\n },\n "passed": true\n },\n "Q3_total_densities_closed_forms": {\n "info": {\n "2": {\n "n_max": 0.09188814923,\n "n_max_closed": 0.09188814924,\n "n_sad": 0.09188814923,\n "n_sad_closed": 0.1837762985\n },\n "3": {\n "n_max": 0.03486240891,\n "n_max_closed": 0.03486240894,\n "n_sad": 0.03486240891,\n "n_sad_closed": 0.106507305\n }\n },\n "passed": false\n },\n "Q4_beta_two_resolutions": {\n "info": {\n "24^3 beta_3": 5.9884942,\n "beta_2": 0.002847545995,\n "beta_3": 0.0004331954693\n },\n "passed": true\n }\n },\n "mutant": "M3",\n "object": "CL-EQUAL-HEIGHT-MASS-20261001-v1",\n "passed": false,\n "scientific_effect": "NONE"\n}\n'} + +CHILD = r""" +import json, os, sys +from pathlib import Path +args = sys.argv[1:] +stage = "baseline" if not args else args[1] +if args and (len(args) != 2 or args[0] != "--mutant"): + raise RuntimeError("bad synthetic command") +mode = sys.flags.optimize +with open(os.environ["EH_TRACE"], "a") as stream: + stream.write(json.dumps([mode, stage]) + "\n") +data = json.loads(Path(__file__).with_name("reports.json").read_text()) +out = b"" if stage == "M9" else data[stage].encode() +err = b"unknown mutant label\n" if stage == "M9" else b"" +code = 0 if stage == "baseline" else 2 if stage == "M9" else 1 +case = os.environ["EH_CASE"] +if case == "dirty": + Path(os.environ["EH_RESULTS"]).write_text("changed after preflight\n") +if stage == os.environ["EH_STAGE"] and mode == int(os.environ["EH_MODE"]): + if case == "crash": raise RuntimeError("unrelated synthetic error") + if case == "silent": out = b""; err = b"" + elif case == "garbage": out = b"unrelated\n" + elif case == "stderr": err += b"unexpected\n" + elif case == "trailing": out += b"{}\n" + elif case == "prefix": out = b"prefix\n" + out + elif case == "format": out = b"\n" + out + elif case == "same-size": out = out.replace(b'scaled GOE', b'scaled BAD', 1) + elif case == "nested-null": out = out.replace(b'"passed": true', b'"passed": null', 1) + elif case == "wrong-label": out = data["M2" if stage == "M1" else "M1"].encode() + elif case == "wrong-exit": code = 0 if stage != "baseline" else 1 + elif case == "unknown-stdout": out = b"unexpected\n" + elif case == "unknown-reason": err = b"unrelated failure\n" +sys.stdout.buffer.write(out) +sys.stderr.buffer.write(err) +sys.exit(code) +""" + + +def shell_text(text): + marker = " - name: Verify sources, replay both modes, reject mutants\n" + if text.count(marker) != 1: + raise ValueError("ambiguous replay step") + lines = text.split(marker, 1)[1].split(" run: |\n", 1)[1].splitlines() + result = [] + for line in lines: + if line and not line.startswith(" "): + break + result.append(line[10:] if line else "") + if not result or result[0] != "set -euo pipefail": + raise ValueError("strict shell missing") + return "\n".join(result) + "\n" + + +def entry(path, data): + return {"path": path, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest(), + "git_blob": hashlib.sha1(b"blob " + str(len(data)).encode() + b"\0" + data).hexdigest()} + + +class EqualHeightWorkflowTests(unittest.TestCase): + def fixture(self, case="valid", stage="baseline", mode=0, guard=None): + with tempfile.TemporaryDirectory(prefix="equal-height-fixture-") as temporary: + home = Path(temporary) + repo = home / "repo" + packet = repo / PACKET + packet.mkdir(parents=True) + (packet / "equal_height_mass.py").write_text(CHILD) + (packet / "RESULTS.json").write_text(REPORTS["baseline"]) + (packet / "reports.json").write_text(json.dumps(REPORTS)) + manifest = {"files": [entry(p.name, p.read_bytes()) for p in sorted(packet.iterdir())], + "mutants": ["M1", "M2", "M3"], "consumed": [], "cited_only": []} + for kind in ("consumed", "cited_only"): + data = ("nonempty fixed " + kind + "\n").encode() + (repo / (kind + ".txt")).write_bytes(data) + manifest[kind] = [entry(kind + ".txt", data)] + (packet / "SOURCES.json").write_text(json.dumps(manifest)) + env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")} + env.update(HOME=str(home), GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull, + GIT_AUTHOR_NAME="Fixture", GIT_AUTHOR_EMAIL="fixture@example.invalid", + GIT_COMMITTER_NAME="Fixture", GIT_COMMITTER_EMAIL="fixture@example.invalid", + PYTHONDONTWRITEBYTECODE="1", EH_TRACE=str(home/"trace"), + EH_RESULTS=str(packet/"RESULTS.json"), EH_CASE=case, + EH_STAGE=stage, EH_MODE=str(mode)) + env["PATH"] = str(Path(sys.executable).parent) + os.pathsep + env.get("PATH", "") + def git(*args): + return subprocess.run(["git", *args], cwd=repo, env=env, check=True, + capture_output=True, timeout=15) + git("init", "-q") + git("config", "core.hooksPath", os.devnull) + git("add", ".") + git("-c", "commit.gpgsign=false", "-c", "maintenance.auto=false", "commit", "-qm", "fixture") + if guard == "source": + with (packet/"equal_height_mass.py").open("a") as stream: + stream.write("\n# changed source\n") + elif guard in ("consumed", "cited_only"): + (repo/(guard+".txt")).write_bytes(b"changed pin\n") + elif guard == "extra": + (packet/"extra").write_text("unexpected") + elif guard == "missing": + (packet/"RESULTS.json").unlink() + elif guard == "symlink": + (home/"original").write_text(REPORTS["baseline"]) + (packet/"RESULTS.json").unlink() + (packet/"RESULTS.json").symlink_to(home/"original") + elif guard == "unmerged": + manifest["consumed_unmerged"] = [{"path": "missing"}] + elif guard and guard.startswith("inventory-"): + manifest["mutants"] = { + "inventory-empty": [], + "inventory-missing": ["M1", "M2"], + "inventory-reverse": ["M3", "M2", "M1"], + "inventory-duplicate": ["M1", "M1", "M3"], + "inventory-mapping": {"M1": "Q2", "M2": "Q2", "M3": "Q3"}, + }[guard] + if guard == "unmerged" or (guard and guard.startswith("inventory-")): + (packet/"SOURCES.json").write_text(json.dumps(manifest)) + git("add", ".") + git("-c", "commit.gpgsign=false", "-c", "maintenance.auto=false", "commit", "-qm", "inventory fixture") + result = subprocess.run(["bash", "--noprofile", "--norc", "-c", shell_text(WORKFLOW.read_text())], + cwd=repo, env=env, capture_output=True, timeout=30) + calls = [json.loads(line) for line in (home/"trace").read_text().splitlines()] if (home/"trace").exists() else [] + if os.environ.get("EH_SAVE"): + dest = Path(os.environ["EH_SAVE"]) / f"{case}-{stage}-{mode}-{guard}" + dest.mkdir(parents=True, exist_ok=True) + (dest/"stdout").write_bytes(result.stdout) + (dest/"stderr").write_bytes(result.stderr) + (dest/"record.json").write_text(json.dumps({"case": case, "stage": stage, "mode": mode, + "guard": guard, "returncode": result.returncode, "calls": calls}, sort_keys=True)+"\n") + return result, calls + + def reject(self, case, stage, mode): + result, calls = self.fixture(case, stage, mode) + order = [[m, s] for m in (0, 1) for s in STAGES] + self.assertIn([mode, stage], calls, "target was never reached") + self.assertNotEqual(result.returncode, 0, "invalid protocol admitted") + self.assertEqual(calls, order[:order.index([mode, stage])+1], "wrong stopping stage") + + def test_valid_inventory(self): + result, calls = self.fixture() + self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace")) + self.assertEqual(calls, [[m, s] for m in (0, 1) for s in STAGES]) + + def test_mutant_full_reports(self): + for case in ("silent", "crash", "garbage", "stderr", "trailing", "prefix", "format", "same-size", "nested-null", "wrong-label", "wrong-exit"): + for stage in ("M1", "M2", "M3"): + for mode in (0, 1): + with self.subTest(case=case, stage=stage, mode=mode): + self.reject(case, stage, mode) + + def test_baseline_contract(self): + for case in ("stderr", "silent", "garbage", "wrong-exit"): + for mode in (0, 1): + with self.subTest(case=case, mode=mode): + self.reject(case, "baseline", mode) + + def test_unknown_contract(self): + for case in ("silent", "unknown-reason", "unknown-stdout", "wrong-exit"): + for mode in (0, 1): + with self.subTest(case=case, mode=mode): + self.reject(case, "M9", mode) + + def test_source_guards(self): + reasons = {"source": b"packet file identity mismatch", "consumed": b"merged source drifted", + "cited_only": b"merged source drifted", "extra": b"packet tree differs", + "missing": b"packet tree differs", "symlink": b"symlink:", + "unmerged": b"this packet declares no unmerged dependency"} + for guard, reason in reasons.items(): + with self.subTest(guard=guard): + result, calls = self.fixture(guard=guard) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(calls, []) + self.assertIn(reason, result.stderr) + + def test_inventory_guard(self): + for guard in ("inventory-empty", "inventory-missing", "inventory-reverse", "inventory-duplicate", "inventory-mapping"): + with self.subTest(guard=guard): + result, calls = self.fixture(guard=guard) + self.assertNotEqual(result.returncode, 0, "invalid inventory admitted") + self.assertEqual(calls, []) + + def test_final_clean_tree_guard(self): + result, calls = self.fixture(case="dirty") + self.assertNotEqual(result.returncode, 0) + self.assertEqual(calls, [[m, s] for m in (0, 1) for s in STAGES]) + self.assertIn(b"changed after preflight", result.stdout) + + def test_workflow_test_wiring(self): + text = WORKFLOW.read_text() + self.assertIn("- 'tests/test_equal_height_workflow.py'", text) + for flags in ("-B -S", "-B -O -S"): + self.assertIn("python "+flags+" -m unittest discover -s tests -p test_equal_height_workflow.py -v", text) + + +if __name__ == "__main__": + unittest.main()