v0.33.2 57f21d6

test_systemview_startup.py RawHistoryFolder

1#!/usr/bin/env python3
2"""Systemview startup fixture (Plan 05 P4.2).
3
4Static gates always run headless: the shell must fetch the WASM exactly
5once (clone-based fallback), carry the readiness contract markers, and
6the generated (untracked) manifest must match disk artifacts.
7
8The live launch smoke starts the candidate binary with an isolated
9profile, watches for panics/fatal crashes, then terminates it gracefully.
10Without a display the live part skips cleanly; in-page branches
11(streaming fallback, HTTP/MIME/compile failures, mount/bind failure, Go
12panic exit, disabled JS) follow the printed display protocol, whose
13markers are asserted statically here.
14
15Exit codes: 0 static gates and launch smoke green (in-page branches stay
16MANUAL), 1 a gate failed, 2 static gates green with live parts skipped.
17"""
18
19import argparse
20import json
21import os
22import re
23import shutil
24import subprocess
25import sys
26import tempfile
27from pathlib import Path
28
29REPO_ROOT = Path(__file__).resolve().parent.parent
30SHELL_PATH = REPO_ROOT / "assets" / "systemviews" / "index.html"
31MANIFEST_PATH = REPO_ROOT / "assets" / "systemviews" / "asset-manifest.json"
32DOM_ADAPTER_PATH = REPO_ROOT / "internal" / "ui" / "systemviews" / "dom_js.go"
33GENERATOR_PKG = "./cmd/systemviews-assets"
34
35EXIT_OK = 0
36EXIT_FAIL = 1
37EXIT_SKIP_LIVE = 2
38
39
40def fail(message):
41    print(f"systemview-startup: FAIL: {message}", flush=True)
42    return EXIT_FAIL
43
44
45def check_shell_single_fetch():
46    """P4.1: one fetch per branch, clone-based fallback, no refetch."""
47    try:
48        shell = SHELL_PATH.read_text(encoding="utf-8")
49    except OSError as exc:
50        return fail(f"cannot read shell: {exc}")
51    problems = []
52    if "response.clone()" not in shell:
53        problems.append("missing response.clone() fallback")
54    if "fallback.arrayBuffer()" not in shell:
55        problems.append("fallback does not consume the retained clone")
56    if re.search(r"return\s+instantiateFromBytes\(\)", shell):
57        problems.append("zero-arg instantiateFromBytes() refetches (old double-fetch)")
58    if shell.count("fetch(wasmURL") != 2:
59        problems.append("expected exactly two fetch(wasmURL...) branch sites")
60    if "response.ok" not in shell:
61        problems.append("missing response.ok fast-fail")
62    if problems:
63        return fail("shell fetch structure: " + "; ".join(problems))
64    print("systemview-startup: shell fetches WASM once per branch, fallback reuses the clone")
65    return EXIT_OK
66
67
68def check_shell_readiness_contract():
69    """P4.2: aria-busy entry, data markers, terminal guard, single go.run."""
70    try:
71        shell = SHELL_PATH.read_text(encoding="utf-8")
72    except OSError as exc:
73        return fail(f"cannot read shell: {exc}")
74    required = [
75        'aria-busy="true"',
76        "removeAttribute(",
77        "aria-busy",
78        'data-failed',
79        "terminalErrorReported",
80    ]
81    missing = [marker for marker in required if marker not in shell]
82    if missing:
83        return fail("shell readiness contract missing: " + ", ".join(missing))
84    if shell.count("go.run(") != 1:
85        return fail("runtime must execute exactly once (single go.run call site)")
86    for marker in ("go.exit =", "goExitCode", "exit status"):
87        if marker not in shell:
88            return fail(f"shell exit hook missing: {marker}")
89    try:
90        adapter = DOM_ADAPTER_PATH.read_text(encoding="utf-8")
91    except OSError as exc:
92        return fail(f"cannot read DOM adapter: {exc}")
93    if '"data-ready"' not in adapter:
94        return fail("DOM adapter never marks data-ready")
95    print("systemview-startup: shell carries the readiness contract markers")
96    return EXIT_OK
97
98
99def check_manifest_on_disk():
100    """P3.1: generated manifest parses and matches generator schema."""
101    try:
102        raw = MANIFEST_PATH.read_bytes()
103    except OSError as exc:
104        return fail(f"cannot read manifest (run make build-systemviews): {exc}")
105    try:
106        manifest = json.loads(raw)
107    except json.JSONDecodeError as exc:
108        return fail(f"manifest is not JSON: {exc}")
109    if manifest.get("version") != 1:
110        return fail("manifest version != 1")
111    files = manifest.get("files", {})
112    if sorted(files) != ["systemviews.css", "systemviews.wasm", "wasm_exec.js"]:
113        return fail(f"manifest pins {sorted(files)}, want the three servables")
114    for name, entry in files.items():
115        digest = entry.get("sha256", "")
116        if not re.fullmatch(r"[0-9a-f]{64}", digest):
117            return fail(f"manifest pin for {name} is not a sha256 hex digest")
118        if not isinstance(entry.get("size"), int) or entry["size"] < 0:
119            return fail(f"manifest pin for {name} has a bad size")
120    print(f"systemview-startup: manifest pins {len(files)} assets with valid digests")
121    return EXIT_OK
122
123
124def check_manifest_generator():
125    """P3.1: generator --check against disk artifacts (needs go toolchain)."""
126    if shutil.which("go") is None:
127        print("systemview-startup: SKIP generator check (no go toolchain)")
128        return EXIT_SKIP_LIVE
129    proc = subprocess.run(
130        ["go", "run", GENERATOR_PKG, "-check", "-dir", "assets/systemviews"],
131        cwd=REPO_ROOT,
132        capture_output=True,
133        text=True,
134        timeout=300,
135    )
136    if proc.returncode != 0:
137        sys.stderr.write(proc.stderr)
138        return fail("generator --check rejected the working tree")
139    print(f"systemview-startup: {proc.stdout.strip()}")
140    return EXIT_OK
141
142
143CRASH_PATTERNS = ["panic:", "fatal error:", "SIGTRAP", "runtime error:", "Go panic"]
144
145
146def display_available():
147    return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
148
149
150def sanitized_launch_env(profile):
151    """Build the child's environment: owned XDG/HOME/CEF dirs, no dev-mode.
152
153    The fixture must not read the operator's configuration (XDG_CONFIG_HOME)
154    nor follow ENV=dev, which would redirect all profile directories under
155    the working directory instead of the isolated profile.
156    """
157    env = dict(os.environ)
158    env.pop("ENV", None)
159    env["HOME"] = str(profile / "home")
160    env["XDG_DATA_HOME"] = str(profile / "data")
161    env["XDG_STATE_HOME"] = str(profile / "state")
162    env["XDG_CACHE_HOME"] = str(profile / "cache")
163    env["XDG_CONFIG_HOME"] = str(profile / "config")
164    return env
165
166
167def run_launch_smoke(binary, cef_dir, timeout):
168    """Start the candidate with an isolated profile, watch, terminate.
169
170    This is a startup smoke test only: it proves the binary launches and
171    survives the window without crashes. It never navigates to a
172    systemview, so fetch counts, readiness inspection, streaming fallback
173    and failure injection stay MANUAL via the printed display protocol.
174    """
175    if not display_available():
176        print("systemview-startup: SKIP live launch (no DISPLAY or WAYLAND_DISPLAY)")
177        return EXIT_SKIP_LIVE
178    profile = Path(tempfile.mkdtemp(prefix="sv-startup-profile-"))
179    env = sanitized_launch_env(profile)
180    env["DUMBER_CEF_ROOT_CACHE_PATH"] = str(profile / "cef-cache")
181    env["DUMBER_CEF_DIR"] = str(cef_dir)
182    print(f"systemview-startup: launching {binary} with isolated profile {profile}")
183    try:
184        proc = subprocess.Popen(
185            [str(binary)],
186            env=env,
187            cwd=profile,
188            stdout=subprocess.PIPE,
189            stderr=subprocess.STDOUT,
190            text=True,
191        )
192    except OSError as exc:
193        return fail(f"cannot execute binary: {exc}")
194    try:
195        try:
196            output, _ = proc.communicate(timeout=timeout)
197        except subprocess.TimeoutExpired:
198            output = None
199        if output is not None:
200            # Any early exit is unexpected for the GUI binary: a crash
201            # without markers is still a failure, never a pass.
202            print(f"systemview-startup: binary exited during smoke with code {proc.returncode}")
203            print((output or "")[-4000:])
204            for pattern in CRASH_PATTERNS:
205                if pattern in (output or ""):
206                    return fail(f"crash marker in startup output: {pattern}")
207            return fail(f"binary exited during the smoke window (code {proc.returncode}); expected it to stay alive")
208        # Still alive past the window: the startup path survived; terminate
209        # gracefully, then force-kill only our own child on refusal.
210        proc.terminate()
211        try:
212            output, _ = proc.communicate(timeout=15)
213        except subprocess.TimeoutExpired:
214            proc.kill()
215            output, _ = proc.communicate(timeout=15)
216            print("systemview-startup: WARN binary ignored SIGTERM and was killed")
217        for pattern in CRASH_PATTERNS:
218            if pattern in (output or ""):
219                print((output or "")[-4000:])
220                return fail(f"crash marker in startup output: {pattern}")
221        print("systemview-startup: launch smoke survived the window with no crash markers")
222        return EXIT_OK
223    finally:
224        shutil.rmtree(profile, ignore_errors=True)
225
226
227DISPLAY_PROTOCOL = """\
228systemview-startup display protocol -- MANUAL STEPS (not executed by this script):
229  1. Open dumb://history -> #app gains data-ready="true", aria-busy gone.
230     Shell HTML references .wasm/.js/.css with ?v=<64-hex> (view-source).
231  2. DevTools: exactly one systemviews.wasm request per load; reload ->
232     .wasm served 200 with Cache-Control immutable; shell answers no-store.
233  3. Streaming fallback: serve .wasm as octet-stream via a local proxy ->
234     console shows the streaming-fallback warning, page still mounts ready.
235  4. HTTP failure: point the shell at a missing .wasm -> terminal
236     "Failed to load systemviews WebAssembly: Error: HTTP 404" rendered
237     once in .sv-error, data-failed="true", aria-busy gone.
238  5. Compile failure: corrupt the .wasm bytes -> same terminal error path.
239  6. Disabled JS: noscript message visible, no console activity.
240  7. Go panic path: break bridge init -> nonzero process exit, console
241     carries the error, error renders exactly once.
242"""
243
244
245def main():
246    parser = argparse.ArgumentParser(description="systemview startup fixture")
247    parser.add_argument("--binary", required=True, help="candidate dumber binary")
248    parser.add_argument("--cef-dir", required=True, help="compatible CEF runtime dir")
249    parser.add_argument("--launch-timeout", type=int, default=20, help="smoke window in seconds")
250    args = parser.parse_args()
251
252    binary = Path(args.binary)
253    if not binary.is_file() or not os.access(binary, os.X_OK):
254        return fail(f"binary is not executable: {binary}")
255    cef_dir = Path(args.cef_dir)
256    if not cef_dir.is_dir():
257        return fail(f"CEF runtime dir missing: {cef_dir}")
258    # The child runs with cwd set to the isolated profile: resolve both
259    # paths now, while they still mean "relative to the invoker".
260    binary = binary.resolve()
261    cef_dir = cef_dir.resolve()
262
263    results = [
264        check_shell_single_fetch(),
265        check_shell_readiness_contract(),
266        check_manifest_on_disk(),
267        check_manifest_generator(),
268    ]
269    if any(result == EXIT_FAIL for result in results):
270        return EXIT_FAIL
271    live = run_launch_smoke(binary, cef_dir, args.launch_timeout)
272    print(DISPLAY_PROTOCOL)
273    if live == EXIT_FAIL:
274        return EXIT_FAIL
275    if live == EXIT_SKIP_LIVE or any(result == EXIT_SKIP_LIVE for result in results):
276        print("systemview-startup: static gates green, live parts skipped")
277        print("systemview-startup: in-page branches were NOT executed here; they are MANUAL via the protocol above")
278        return EXIT_SKIP_LIVE
279    print("systemview-startup: static gates and launch smoke green")
280    print("systemview-startup: in-page branches were NOT executed here; they are MANUAL via the protocol above")
281    return EXIT_OK
282
283
284if __name__ == "__main__":
285    sys.exit(main())

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close