From ad6e3854c28bc13b0bde9bc0342a1f08740c4bda Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 19 Oct 2026 19:47:51 +0200 Subject: [PATCH] Add fail-closed Rayforge Windows GUI preflight --- .../rayforge-windows-gui-preflight.md | 45 +++++++++++++ docs/roadmap.md | 6 ++ scripts/check_rayforge_windows_gui.py | 64 +++++++++++++++++++ .../rayforge-windows-gui-preflight.json | 23 +++++++ tests/test_rayforge_windows_gui_preflight.py | 32 ++++++++++ 5 files changed, 170 insertions(+) create mode 100644 docs/research/rayforge-windows-gui-preflight.md create mode 100644 scripts/check_rayforge_windows_gui.py create mode 100644 tests/fixtures/rayforge-windows-gui-preflight.json create mode 100644 tests/test_rayforge_windows_gui_preflight.py diff --git a/docs/research/rayforge-windows-gui-preflight.md b/docs/research/rayforge-windows-gui-preflight.md new file mode 100644 index 0000000..5cf0407 --- /dev/null +++ b/docs/research/rayforge-windows-gui-preflight.md @@ -0,0 +1,45 @@ +# Rayforge Windows GUI preflight + +- Date: 2026-10-19 +- Scope: isolated no-device launch prerequisite check +- Transport: none +- Installation performed: no + +## Result + +The pinned Rayforge source and the external no-device JCZ addon are present, +but the current test environment cannot launch Rayforge's GTK application. +The observed launch failure is: + +```text +ModuleNotFoundError: No module named 'gi' +``` + +Rayforge's Windows development path expects an MSYS2 UCRT64 installation, +`PyGObject`, GTK4 and Libadwaita. Neither `C:\msys64\msys2_shell.cmd` nor the +checkout's generated `.msys2_env` is present. The lightweight Python venv used +for the transport-free encoder spikes therefore cannot substitute for the GUI +runtime. + +The deterministic preflight result is stored in +`tests/fixtures/rayforge-windows-gui-preflight.json`. It deliberately reports +the manual GUI checkpoint as blocked rather than treating source/model tests +as a successful application launch. + +## Safety and scope + +The attempted instance used an isolated configuration directory and contained +only the external no-device addon. The addon has no discovery or transport +implementation. No USB endpoint or controller command was reached. + +No dependency or runtime was installed. Installing Rayforge's full Windows +toolchain is a separate, reviewable action: upstream's setup includes MSYS2, +GTK, compiler/build packages and network downloads, so it should not be an +implicit side effect of a workflow test. + +## Next action + +Provide an already reviewed Rayforge Windows build/runtime, or explicitly +approve installation of the pinned checkout's MSYS2/UCRT64 dependencies. Then +rerun the preflight and perform the packaged no-device interaction checklist +from `rayforge-graphical-workflow-spike.md`. diff --git a/docs/roadmap.md b/docs/roadmap.md index 39891df..7cb1e38 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -306,6 +306,12 @@ without machine code or transport. This is a headless UI-contract and pipeline proof, not a completed human GTK test. Packaged Windows interaction with the external addon remains open before item 5 or ADR 0002 can close. +The first Windows launch preflight is recorded separately. It fails closed +because the available spike venv has no PyGObject `gi` module and this machine +has no MSYS2/UCRT64 Rayforge runtime. No dependencies were installed and no +GUI success is claimed. The packaged interaction test therefore remains +blocked on a reviewed Windows GTK runtime, not on controller behavior. + ## Decision rules - A simulator test precedes every equivalent hardware behavior. diff --git a/scripts/check_rayforge_windows_gui.py b/scripts/check_rayforge_windows_gui.py new file mode 100644 index 0000000..bb86daa --- /dev/null +++ b/scripts/check_rayforge_windows_gui.py @@ -0,0 +1,64 @@ +"""Fail-closed preflight for the pinned Rayforge Windows GUI checkpoint.""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +from pathlib import Path + + +def inspect(rayforge: Path, python_env: Path) -> dict[str, object]: + msys2_shell = Path("C:/msys64/msys2_shell.cmd") + msys2_env = rayforge / ".msys2_env" + gi_available = importlib.util.find_spec("gi") is not None + app_entry = rayforge / "rayforge" / "__main__.py" + addon_manifest = ( + Path(__file__).parents[1] + / "spikes" + / "rayforge-jcz-no-device-addon" + / "rayforge-addon.yaml" + ) + checks = { + "pinned_source_present": (rayforge / ".git").is_dir(), + "application_entry_present": app_entry.is_file(), + "no_device_addon_present": addon_manifest.is_file(), + "python_environment_present": python_env.is_file(), + "pygobject_gi_available": gi_available, + "msys2_ucrt64_shell_present": msys2_shell.is_file(), + "rayforge_msys2_environment_present": msys2_env.is_file(), + } + ready = all(checks.values()) + return { + "transport": None, + "controller_backend_reachable": False, + "checks": checks, + "ready_for_manual_gui_test": ready, + "status": "ready" if ready else "blocked", + "missing_requirements": [key for key, passed in checks.items() if not passed], + "observed_launch_error": None if gi_available else "ModuleNotFoundError: No module named 'gi'", + "installation_performed": False, + "next_action": ( + "launch isolated no-device GUI test" + if ready + else "provide a reviewed Rayforge Windows GTK runtime or approve its installation" + ), + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("rayforge", type=Path) + parser.add_argument("python", type=Path) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + result = inspect(args.rayforge.resolve(), args.python.resolve()) + rendered = json.dumps(result, indent=2, sort_keys=True) + "\n" + if args.output: + args.output.write_text(rendered, encoding="utf-8", newline="\n") + else: + print(rendered, end="") + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/rayforge-windows-gui-preflight.json b/tests/fixtures/rayforge-windows-gui-preflight.json new file mode 100644 index 0000000..c94ae53 --- /dev/null +++ b/tests/fixtures/rayforge-windows-gui-preflight.json @@ -0,0 +1,23 @@ +{ + "checks": { + "application_entry_present": true, + "msys2_ucrt64_shell_present": false, + "no_device_addon_present": true, + "pinned_source_present": true, + "pygobject_gi_available": false, + "python_environment_present": true, + "rayforge_msys2_environment_present": false + }, + "controller_backend_reachable": false, + "installation_performed": false, + "missing_requirements": [ + "pygobject_gi_available", + "msys2_ucrt64_shell_present", + "rayforge_msys2_environment_present" + ], + "next_action": "provide a reviewed Rayforge Windows GTK runtime or approve its installation", + "observed_launch_error": "ModuleNotFoundError: No module named 'gi'", + "ready_for_manual_gui_test": false, + "status": "blocked", + "transport": null +} diff --git a/tests/test_rayforge_windows_gui_preflight.py b/tests/test_rayforge_windows_gui_preflight.py new file mode 100644 index 0000000..59d7c00 --- /dev/null +++ b/tests/test_rayforge_windows_gui_preflight.py @@ -0,0 +1,32 @@ +import json +import unittest +from pathlib import Path + + +ROOT = Path(__file__).parents[1] +RESULT = ROOT / "tests" / "fixtures" / "rayforge-windows-gui-preflight.json" + + +class RayforgeWindowsGuiPreflightTests(unittest.TestCase): + def setUp(self): + self.result = json.loads(RESULT.read_text(encoding="utf-8")) + + def test_preflight_is_transport_free(self): + self.assertIsNone(self.result["transport"]) + self.assertFalse(self.result["controller_backend_reachable"]) + + def test_missing_runtime_blocks_gui_claim(self): + self.assertEqual("blocked", self.result["status"]) + self.assertFalse(self.result["ready_for_manual_gui_test"]) + self.assertFalse(self.result["installation_performed"]) + self.assertIn("pygobject_gi_available", self.result["missing_requirements"]) + + def test_source_and_no_device_addon_are_present(self): + checks = self.result["checks"] + self.assertTrue(checks["pinned_source_present"]) + self.assertTrue(checks["application_entry_present"]) + self.assertTrue(checks["no_device_addon_present"]) + + +if __name__ == "__main__": + unittest.main()