Files
pulse/scripts/tests/test_dev_prepush_frontend.py
T
pulse-triage[bot] bc89ac1b89 Exercise final browser proof in local delivery preflight
Reproduce the local pre-push gap with real disposable Git histories and the existing browser receipt guard: missing, stale, partial, wrongly parented and nonpassing proof must not receive a local green verdict. Valid own-parent receipts and merges must remain usable without rebinding; upstream-only changes are not outgoing work.

Contract-Neutral: Test-only local delivery admission controls; no runtime, browser receipt rules or protected delivery policy change.
Change-source: pulse-maintainer
2026-10-06 10:29:16 +01:00

392 lines
19 KiB
Python

"""Exercise the local delivery gate, not a live advisory service or Go suite."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
import yaml
ROOT = Path(__file__).resolve().parents[2]
PREPUSH = ROOT / "scripts/dev-prepush.sh"
AUDIT = ROOT / "scripts/npm-audit-retry.sh"
class DevPrepushFixture(unittest.TestCase):
def setUp(self) -> None:
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.repo = Path(self.directory.name) / "repo"
self.repo.mkdir()
self.bin = Path(self.directory.name) / "bin"
self.bin.mkdir()
self.calls = Path(self.directory.name) / "npm-calls"
self.guard_calls = Path(self.directory.name) / "guard-calls"
self.env = os.environ.copy()
for key in list(self.env):
if key.startswith(("GIT_", "NPM_AUDIT_")):
self.env.pop(key)
self.env.update(
PATH=str(self.bin) + os.pathsep + self.env["PATH"],
GIT_CONFIG_NOSYSTEM="1",
GIT_CONFIG_GLOBAL=os.devnull,
GIT_AUTHOR_NAME="Local gate fixture",
GIT_AUTHOR_EMAIL="gate@example.invalid",
GIT_COMMITTER_NAME="Local gate fixture",
GIT_COMMITTER_EMAIL="gate@example.invalid",
PREPUSH_NPM_CALLS=str(self.calls),
PREPUSH_GUARD_CALLS=str(self.guard_calls),
PREPUSH_AUDIT_MODE="clean",
PREPUSH_TYPE_STATUS="0",
PREPUSH_GUARD_STATUS="0",
# Own the canned-provider budget, not the real runner's default.
NPM_AUDIT_ATTEMPTS="2",
NPM_AUDIT_ATTEMPT_TIMEOUT="3",
NPM_AUDIT_MAX_SECONDS="15",
NPM_AUDIT_RETRY_DELAY="0",
)
self.write("scripts/dev-prepush.sh", PREPUSH.read_text())
self.write("scripts/npm-audit-retry.sh", AUDIT.read_text())
self.write("frontend-modern/package.json", '{"private":true}\n')
self.write("frontend-modern/package-lock.json", '{"lockfileVersion":3}\n')
self.write("frontend-modern/src/example.ts", "export const value = 1;\n")
self.write("README.md", "Local gate fixture\n")
# The completion guard has its own exact-source tests. This fixture
# records the real prepush invocation and exit handling only; it does
# not claim canonical contract acceptance.
self.write(
"scripts/release_control/canonical_completion_guard.py",
"import json,os,sys\n"
"with open(os.environ['PREPUSH_GUARD_CALLS'],'a') as output:\n"
" output.write(json.dumps({'argv':sys.argv[1:],"
"'reason':os.environ.get('PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT'),"
"'files':sys.stdin.read().splitlines()})+'\\n')\n"
"sys.exit(int(os.environ['PREPUSH_GUARD_STATUS']))\n",
)
# Audit/type-check orchestration cases do not claim browser proof.
# The browser cases below replace this double with the actual guard.
self.write(
"scripts/release_control/browser_verification_guard.py",
"import sys\nsys.exit(0)\n",
)
npm = self.bin / "npm"
npm.write_text(
"#!/usr/bin/env python3\n"
"import json,os,sys\n"
"args=sys.argv[1:]\n"
"with open(os.environ['PREPUSH_NPM_CALLS'],'a') as output:\n"
" output.write(json.dumps({'args':args,'cwd':os.getcwd(),"
"'require':os.environ.get('NPM_AUDIT_REQUIRE_RESULT')})+'\\n')\n"
"if args == ['run','type-check']:\n"
" sys.exit(int(os.environ['PREPUSH_TYPE_STATUS']))\n"
"if args != ['audit','--json']:\n"
" sys.exit(64)\n"
"mode=os.environ['PREPUSH_AUDIT_MODE']\n"
"counts=dict.fromkeys(['info','low','moderate','high','critical','total'],0)\n"
"if mode in ('moderate','critical'):\n"
" counts[mode]=counts['total']=1\n"
"if mode == 'unavailable':\n"
" print(json.dumps({'error':{'code':'ENOAUDIT'}}))\n"
"else:\n"
" print(json.dumps({'metadata':{'vulnerabilities':counts}}))\n"
"sys.exit(0 if mode == 'clean' else 42)\n",
encoding="utf-8",
)
npm.chmod(0o755)
self.git("init", "--quiet")
self.git("add", ".")
self.git("commit", "--quiet", "-m", "Fixture base")
self.base = self.git("rev-parse", "HEAD").stdout.strip()
def write(self, relative: str, content: str) -> None:
path = self.repo / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def git(self, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
["git", *args], cwd=self.repo, env=self.env,
text=True, capture_output=True, check=True, timeout=20,
)
def change(self, relative: str, *, trailer: str | None = None) -> str:
path = self.repo / relative
previous = path.read_text() if path.exists() else ""
if relative.endswith(".json"):
content = json.loads(previous)
content["fixture_delta"] = True
self.write(relative, json.dumps(content) + "\n")
else:
self.write(relative, previous + "\n# fixture delta\n")
self.git("add", "--", relative)
message = ["-m", "Fixture change"]
if trailer:
message += ["-m", "Contract-Neutral: " + trailer]
self.git("commit", "--quiet", *message)
return self.git("rev-parse", "HEAD").stdout.strip()
def run_gate(self, *, dependencies: bool = True):
if dependencies:
(self.repo / "frontend-modern/node_modules").mkdir(exist_ok=True)
result = subprocess.run(
["bash", "scripts/dev-prepush.sh", self.base],
cwd=self.repo, env=self.env, text=True, capture_output=True,
check=False, timeout=30,
)
calls = [json.loads(line) for line in self.calls.read_text().splitlines()] if self.calls.exists() else []
guards = [json.loads(line) for line in self.guard_calls.read_text().splitlines()] if self.guard_calls.exists() else []
return result, calls, guards
def assert_refused_before_source_checks(self, result, calls, guards, reads=1):
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertNotIn("All pre-push checks passed", result.stdout)
self.assertIn("remaining source checks have not run", result.stderr)
self.assertEqual([call["args"] for call in calls], [["audit", "--json"]] * reads)
self.assertTrue(all(call["require"] == "true" for call in calls))
self.assertEqual(guards, [])
class DevPrepushFrontendTest(DevPrepushFixture):
def test_changed_manifest_rejects_critical_findings(self):
self.change("frontend-modern/package.json")
self.env["PREPUSH_AUDIT_MODE"] = "critical"
# An ambient tolerant caller must not relax this changed-graph gate.
self.env["NPM_AUDIT_REQUIRE_RESULT"] = "false"
self.assert_refused_before_source_checks(*self.run_gate())
def test_changed_lock_rejects_moderate_findings(self):
self.change("frontend-modern/package-lock.json")
self.env["PREPUSH_AUDIT_MODE"] = "moderate"
self.assert_refused_before_source_checks(*self.run_gate())
def test_changed_audit_runner_requires_a_result(self):
self.change("scripts/npm-audit-retry.sh")
self.env["PREPUSH_AUDIT_MODE"] = "unavailable"
self.assert_refused_before_source_checks(*self.run_gate(), reads=2)
def test_failed_zero_report_is_not_a_passing_local_gate(self):
self.change("frontend-modern/package-lock.json")
self.env["PREPUSH_AUDIT_MODE"] = "zero-failure"
self.assert_refused_before_source_checks(*self.run_gate(), reads=2)
def test_clean_changed_graph_runs_typecheck_and_preserves_commit_binding(self):
trailer = "fixture graph has no API delta"
head = self.change("frontend-modern/package-lock.json", trailer=trailer)
result, calls, guards = self.run_gate()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("All pre-push checks passed", result.stdout)
self.assertEqual([call["args"] for call in calls], [["audit", "--json"], ["run", "type-check"]])
self.assertEqual(calls[0]["require"], "true")
self.assertTrue(all(call["cwd"] == str(self.repo / "frontend-modern") for call in calls))
self.assertEqual(len(guards), 1)
self.assertEqual(guards[0]["reason"].strip(), trailer)
self.assertEqual({key: value for key, value in guards[0].items() if key != "reason"}, {
"argv": ["--files-from-stdin", "--diff-base", head + "^", "--commit", head],
"files": ["frontend-modern/package-lock.json"],
})
def test_large_change_list_does_not_skip_strict_audit(self):
self.change("frontend-modern/package.json")
for index in range(600):
self.write("zzz/" + str(index) + "x" * 140 + ".txt", "fixture\n")
self.git("add", "--", "zzz")
self.git("commit", "--quiet", "-m", "Large changed-file fixture")
self.assertGreater(len(self.git("diff", "--name-only", self.base, "HEAD").stdout), 65536)
self.env["PREPUSH_AUDIT_MODE"] = "critical"
self.assert_refused_before_source_checks(*self.run_gate())
def test_unchanged_graph_is_not_queried(self):
self.change("frontend-modern/src/example.ts")
self.env["PREPUSH_AUDIT_MODE"] = "critical"
result, calls, guards = self.run_gate()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual([call["args"] for call in calls], [["run", "type-check"]])
self.assertEqual(len(guards), 1)
def test_missing_frontend_dependencies_do_not_pass(self):
self.change("frontend-modern/src/example.ts")
result, calls, guards = self.run_gate(dependencies=False)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn("frontend-modern/node_modules missing", result.stdout)
self.assertNotIn("All pre-push checks passed", result.stdout)
self.assertEqual(calls, [])
self.assertEqual(len(guards), 1)
def test_typecheck_failure_remains_a_failure(self):
self.change("frontend-modern/src/example.ts")
self.env["PREPUSH_TYPE_STATUS"] = "7"
result, calls, guards = self.run_gate()
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertNotIn("All pre-push checks passed", result.stdout)
self.assertEqual([call["args"] for call in calls], [["run", "type-check"]])
self.assertEqual(len(guards), 1)
def test_canonical_failure_is_not_cleared_by_a_clean_audit(self):
self.change("frontend-modern/package.json")
self.env["PREPUSH_GUARD_STATUS"] = "4"
result, calls, guards = self.run_gate()
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertNotIn("All pre-push checks passed", result.stdout)
self.assertEqual([call["args"] for call in calls], [["audit", "--json"], ["run", "type-check"]])
self.assertEqual(len(guards), 1)
def test_documentation_only_has_no_frontend_checks(self):
self.change("README.md")
result, calls, guards = self.run_gate(dependencies=False)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(calls, [])
self.assertEqual(len(guards), 1)
def test_no_outgoing_commit_has_no_checks(self):
result, calls, guards = self.run_gate(dependencies=False)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("No commits ahead", result.stdout)
self.assertEqual(calls, [])
self.assertEqual(guards, [])
def test_audit_path_lookalike_does_not_query(self):
self.change("scripts/npm-audit-retry.sh.extra")
result, calls, guards = self.run_gate(dependencies=False)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(calls, [])
self.assertEqual(len(guards), 1)
def test_local_strict_paths_match_ci(self):
workflow = yaml.safe_load((ROOT / ".github/workflows/build-and-test.yml").read_text())
classification = next(step for step in workflow["jobs"]["changes"]["steps"] if step.get("id") == "filter")["run"]
paths = r"^frontend-modern/package(-lock)?\.json$|^scripts/npm-audit-retry\.sh$"
self.assertIn(paths, classification)
self.assertIn(paths, PREPUSH.read_text())
class DevPrepushBrowserTest(DevPrepushFixture):
"""Real Git and receipt admission; fixture receipts are not browser runs."""
SOURCE = "frontend-modern/src/example.ts"
def setUp(self):
super().setUp()
for name in ("browser_verification_guard.py", "format_staged_frontend.py"):
self.write(
"scripts/release_control/" + name,
(ROOT / "scripts/release_control" / name).read_text(),
)
self.git("add", "--", "scripts/release_control")
self.git("commit", "--quiet", "-m", "Use real browser guard in fixture")
self.base = self.git("rev-parse", "HEAD").stdout.strip()
def receipt(self, paths, *, parent=None, result="passed"):
digests = {
path: hashlib.sha256((self.repo / path).read_bytes()).hexdigest()
for path in paths
}
fingerprint = hashlib.sha256(
json.dumps(digests, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()[:16]
path = "frontend-modern/browser-verification/" + fingerprint + ".json"
self.write(path, json.dumps({
"version": 1,
"base_sha": parent or self.git("rev-parse", "HEAD").stdout.strip(),
"verified_at": "2026-10-06T09:00:00Z",
"result": result,
"changed_paths": paths,
"content_sha256": digests,
"routes": ["/fixture"],
"viewports": [{"width": 1280, "height": 800}, {"width": 390, "height": 844}],
"states": ["simulated receipt-schema fixture, not product acceptance"],
"interactions": ["simulated interaction for admission tests only"],
}) + "\n")
return path
def commit_source(self, *, proof=True, parent=None):
self.write(self.SOURCE, "export const value = 2;\n")
paths = [self.SOURCE]
if proof:
paths.append(self.receipt([self.SOURCE], parent=parent))
self.git("add", "--", *paths)
self.git("commit", "--quiet", "-m", "Frontend fixture change")
return self.git("rev-parse", "HEAD").stdout.strip()
def assert_browser_blocked(self, result, calls, guards):
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn("BLOCKED: browser verification", result.stderr)
self.assertNotIn("All pre-push checks passed", result.stdout)
# Receipt admission is cheap and precedes source compilation.
self.assertEqual(calls, [])
self.assertTrue(guards)
def test_missing_browser_proof_does_not_pass_a_successful_typecheck(self):
self.commit_source(proof=False)
self.assert_browser_blocked(*self.run_gate())
def test_valid_own_parent_receipt_is_accepted(self):
self.commit_source()
result, calls, _ = self.run_gate()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("Browser verification guard passed", result.stdout)
self.assertEqual([call["args"] for call in calls], [["run", "type-check"]])
def test_later_edit_cannot_reuse_the_old_verified_bytes(self):
self.commit_source()
self.write(self.SOURCE, "export const value = 3;\n")
self.git("add", "--", self.SOURCE)
self.git("commit", "--quiet", "-m", "Changed bytes after fixture proof")
result, calls, guards = self.run_gate()
self.assert_browser_blocked(result, calls, guards)
self.assertIn("different verified content", result.stderr)
def test_receipt_for_only_one_of_two_final_files_is_insufficient(self):
self.commit_source()
self.write("frontend-modern/src/other.ts", "export const other = 2;\n")
self.git("add", "--", "frontend-modern/src/other.ts")
self.git("commit", "--quiet", "-m", "Another unverified fixture file")
result, calls, guards = self.run_gate()
self.assert_browser_blocked(result, calls, guards)
self.assertIn("frontend-modern/src/other.ts final content", result.stderr)
def test_receipt_with_the_wrong_parent_stays_blocked(self):
self.commit_source(parent="a" * 40)
result, calls, guards = self.run_gate()
self.assert_browser_blocked(result, calls, guards)
self.assertIn("base_sha must match", result.stderr)
def test_metadata_only_nonpassing_receipt_stays_blocked(self):
path = self.receipt([self.SOURCE], result="replace-with-passed-after-verification")
self.git("add", "--", path)
self.git("commit", "--quiet", "-m", "Nonpassing fixture receipt")
self.assert_browser_blocked(*self.run_gate())
def test_merge_preserves_original_receipt_without_rebinding(self):
verified = self.commit_source()
receipt_before = self.git("show", verified + ":frontend-modern/browser-verification").stdout
self.git("checkout", "--quiet", "--detach", self.base)
self.change("README.md")
unrelated = self.git("rev-parse", "HEAD").stdout.strip()
self.git("checkout", "--quiet", "--detach", verified)
self.git("merge", "--quiet", "--no-ff", "-m", "Merge fixture documentation", unrelated)
result, _, _ = self.run_gate()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("Browser verification guard passed", result.stdout)
self.assertEqual(self.git("show", "HEAD:frontend-modern/browser-verification").stdout, receipt_before)
def test_diverged_upstream_does_not_require_proof_for_incoming_source(self):
original_base = self.base
upstream = self.commit_source(proof=False)
self.git("checkout", "--quiet", "--detach", original_base)
self.change("README.md")
self.base = upstream
result, calls, _ = self.run_gate(dependencies=False)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("Browser verification guard skipped", result.stdout)
self.assertEqual(calls, [])
if __name__ == "__main__":
unittest.main()