mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
7a509b3331
Separate the source observation from printf so Bash propagates its failure. The existing same-checkout CI contract is unchanged; test the unavailable-identity case without claiming a compiler result. Contract-Neutral: Preserve the existing exact-checkout real-embed contract by propagating its Git read failure; no source, graph, permissions, build, audit or deadline contract changes. Change-source: pulse-maintainer
155 lines
8.8 KiB
Python
155 lines
8.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Check ordinary CI's real-embed phase without a host build or network.
|
|
|
|
The exact workflow shell runs with private asset fixtures and compiler/Git
|
|
doubles. This is recipe/admission proof, not real frontend/backend acceptance.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
WORKFLOW = Path(os.environ.get("PULSE_BUILD_WORKFLOW_UNDER_TEST", ROOT / ".github/workflows/build-and-test.yml")).resolve()
|
|
GO_SETUP = "Set up Go for real-embed verification"
|
|
VERIFY = "Verify backend with real frontend embed"
|
|
|
|
|
|
def recipe(source):
|
|
job = re.search(r"(?ms)^ frontend:\n(.*?)(?=^ [a-z][a-z0-9-]*:|\Z)", source)
|
|
if not job:
|
|
raise AssertionError("frontend job missing")
|
|
job = job[1]
|
|
if "timeout-minutes: 30" not in job or "if: needs.changes.outputs.code == 'true'" not in job:
|
|
raise AssertionError("ordinary frontend admission/deadline changed")
|
|
parsed = re.findall(r"(?ms)^ - name: ([^\n]+)\n(.*?)(?=^ - name:|\Z)", job)
|
|
steps = dict(parsed)
|
|
if len(parsed) != len(steps):
|
|
raise AssertionError("duplicate named frontend step")
|
|
order = list(steps)
|
|
required = ["Checkout repository", "Build frontend bundle (with embed copy)",
|
|
"Check frontend bundle size budget", "Frontend unit tests", GO_SETUP, VERIFY,
|
|
"Require frontend dependency audit"]
|
|
if any(name not in order for name in required) or [order.index(name) for name in required] != sorted(order.index(name) for name in required):
|
|
raise AssertionError("same-checkout real build, tests, compiler or audit ordering missing")
|
|
if sum("uses: actions/checkout@" in step for step in steps.values()) != 1 or "run: make frontend" not in steps[required[1]]:
|
|
raise AssertionError("must use this checkout's production frontend build")
|
|
if re.search(r"^ (ref|repository):", steps[required[0]], re.M):
|
|
raise AssertionError("frontend proof must use the ordinary exact checkout")
|
|
if "go-version-file: go.mod" not in steps[GO_SETUP] or "actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e" not in steps[GO_SETUP]:
|
|
raise AssertionError("declared compiler setup missing")
|
|
for name in (GO_SETUP, VERIFY):
|
|
if re.search(r"^ (if|continue-on-error|working-directory):", steps[name], re.M):
|
|
raise AssertionError("real-embed verification must run at root and retain failure")
|
|
run = re.search(r"(?ms)^ run: \|\n((?: [^\n]*\n)+)", steps[VERIFY])
|
|
if not run:
|
|
raise AssertionError("real-embed shell missing")
|
|
shell = "\n".join(line[10:] for line in run[1].splitlines()) + "\n"
|
|
for command in ("set -euo pipefail", "checkout_sha=$(git rev-parse HEAD)", "test -s frontend-modern/dist/index.html",
|
|
"test -d frontend-modern/dist/assets",
|
|
"diff -qr --no-dereference frontend-modern/dist internal/api/frontend-modern/dist",
|
|
'go build -mod=readonly -p=2 -o "$RUNNER_TEMP/pulse-real-embed" ./cmd/pulse',
|
|
"go vet -mod=readonly -p=2 ./cmd/pulse ./internal/api"):
|
|
if command not in shell:
|
|
raise AssertionError("required real-embed command missing: " + command)
|
|
return shell
|
|
|
|
|
|
COMPILER = r'''#!/usr/bin/env python3
|
|
import json, os, sys
|
|
from pathlib import Path
|
|
args = sys.argv[1:]
|
|
with Path(os.environ["TRACE"]).open("a") as out:
|
|
out.write(json.dumps(args) + "\n")
|
|
if args[0] == os.environ["FAIL_PHASE"]:
|
|
sys.exit(43)
|
|
if args[0] == "build":
|
|
Path(args[args.index("-o") + 1]).write_bytes(b"fixture binary, not a real build\n")
|
|
elif args != ["version"] and args[0] != "vet":
|
|
sys.exit(97)
|
|
'''
|
|
|
|
|
|
class FrontendRealEmbed(unittest.TestCase):
|
|
def run_recipe(self, *, mutation="", fail_phase="", git_exit=0):
|
|
shell = recipe(WORKFLOW.read_text())
|
|
with tempfile.TemporaryDirectory(prefix="pulse-real-embed-") as directory:
|
|
f = Path(directory)
|
|
for tree in ("frontend-modern/dist", "internal/api/frontend-modern/dist"):
|
|
(f / tree / "assets").mkdir(parents=True)
|
|
(f / tree / "index.html").write_text("<html>fixture production index</html>\n")
|
|
(f / tree / "assets/app.js").write_text("fixture application bytes\n")
|
|
embed = f / "internal/api/frontend-modern/dist"
|
|
if mutation == "missing index": (f / "frontend-modern/dist/index.html").unlink()
|
|
if mutation == "empty index": (f / "frontend-modern/dist/index.html").write_text("")
|
|
if mutation == "missing assets":
|
|
(f / "frontend-modern/dist/assets/app.js").unlink()
|
|
(f / "frontend-modern/dist/assets").rmdir()
|
|
if mutation == "changed served byte": (embed / "assets/app.js").write_text("different bytes\n")
|
|
if mutation == "extra served file": (embed / "extra.js").write_text("extra bytes\n")
|
|
if mutation == "missing embedded file": (embed / "assets/app.js").unlink()
|
|
for name in ("tools", "runner-temp"): (f / name).mkdir()
|
|
(f / "tools/go").write_text(COMPILER)
|
|
(f / "tools/git").write_text("#!/bin/sh\n[ \"$*\" = 'rev-parse HEAD' ] || exit 97\n[ \"$GIT_EXIT\" = 0 ] || exit \"$GIT_EXIT\"\necho 0123456789012345678901234567890123456789\n")
|
|
for name in ("go", "git"): (f / "tools" / name).chmod(0o700)
|
|
env = dict(os.environ, PATH=str(f / "tools") + os.pathsep + os.environ["PATH"],
|
|
TRACE=str(f / "trace"), FAIL_PHASE=fail_phase, GIT_EXIT=str(git_exit), RUNNER_TEMP=str(f / "runner-temp"))
|
|
result = subprocess.run(["bash", "-c", shell], cwd=f, env=env, capture_output=True, text=True, timeout=10)
|
|
calls = [json.loads(line) for line in (f / "trace").read_text().splitlines()] if (f / "trace").exists() else []
|
|
output = f / "runner-temp/pulse-real-embed"
|
|
return result, calls, str(output), output.exists()
|
|
|
|
def test_success_uses_readonly_graph_and_same_checkout_assets(self):
|
|
result, calls, output, built = self.run_recipe()
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertEqual(calls, [["version"], ["build", "-mod=readonly", "-p=2", "-o", output, "./cmd/pulse"],
|
|
["vet", "-mod=readonly", "-p=2", "./cmd/pulse", "./internal/api"]])
|
|
self.assertTrue(built)
|
|
self.assertIn("REAL_EMBED_CHECKOUT=0123456789012345678901234567890123456789", result.stdout)
|
|
|
|
def test_missing_or_changed_embed_refuses_before_compiler(self):
|
|
for mutation in ("missing index", "empty index", "missing assets", "changed served byte", "extra served file", "missing embedded file"):
|
|
with self.subTest(mutation=mutation):
|
|
result, calls, _, built = self.run_recipe(mutation=mutation)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertEqual(calls, [])
|
|
self.assertFalse(built)
|
|
|
|
def test_build_and_vet_failures_are_terminal(self):
|
|
for phase, called in (("build", ["version", "build"]), ("vet", ["version", "build", "vet"])):
|
|
with self.subTest(phase=phase):
|
|
result, calls, _, _ = self.run_recipe(fail_phase=phase)
|
|
self.assertEqual(result.returncode, 43)
|
|
self.assertEqual([call[0] for call in calls], called)
|
|
|
|
def test_failed_checkout_read_is_not_a_build_grant(self):
|
|
result, calls, _, built = self.run_recipe(git_exit=46)
|
|
self.assertEqual(result.returncode, 46)
|
|
self.assertEqual(calls, [])
|
|
self.assertFalse(built)
|
|
self.assertNotIn("REAL_EMBED_CHECKOUT=", result.stdout)
|
|
|
|
def test_missing_hidden_or_foreign_workflow_phases_are_rejected(self):
|
|
source = WORKFLOW.read_text()
|
|
for before, after in (("run: make frontend", "run: echo stub"),
|
|
("go-version-file: go.mod", "go-version: stable"),
|
|
(f"- name: {VERIFY}", "- name: unrelated phase"),
|
|
(f"- name: {VERIFY}\n", f"- name: {VERIFY}\n continue-on-error: true\n"),
|
|
(f"- name: {VERIFY}\n", f"- name: {VERIFY}\n if: false\n"),
|
|
(f"- name: {VERIFY}\n", f"- name: {VERIFY}\n working-directory: frontend-modern\n"),
|
|
(f"- name: {GO_SETUP}\n", "- name: Checkout repository\n"),
|
|
("persist-credentials: false\n", "persist-credentials: false\n ref: other-source\n"),
|
|
("go vet -mod=readonly -p=2 ./cmd/pulse ./internal/api", "echo skipped vet")):
|
|
with self.subTest(after=after):
|
|
self.assertIn(before, source)
|
|
with self.assertRaises(AssertionError): recipe(source.replace(before, after))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|