Files
pulse/scripts/tests/test_frontend_real_embed.py
pulse-triage[bot] 7a509b3331 Retain checkout-read failure before real-embed compilation
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
2026-10-10 14:01:13 +01:00

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()