Add fail-closed Rayforge Windows GUI preflight
This commit is contained in:
parent
fa41bf5d51
commit
ad6e3854c2
5 changed files with 170 additions and 0 deletions
45
docs/research/rayforge-windows-gui-preflight.md
Normal file
45
docs/research/rayforge-windows-gui-preflight.md
Normal file
|
|
@ -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`.
|
||||||
|
|
@ -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
|
proof, not a completed human GTK test. Packaged Windows interaction with the
|
||||||
external addon remains open before item 5 or ADR 0002 can close.
|
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
|
## Decision rules
|
||||||
|
|
||||||
- A simulator test precedes every equivalent hardware behavior.
|
- A simulator test precedes every equivalent hardware behavior.
|
||||||
|
|
|
||||||
64
scripts/check_rayforge_windows_gui.py
Normal file
64
scripts/check_rayforge_windows_gui.py
Normal file
|
|
@ -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()
|
||||||
23
tests/fixtures/rayforge-windows-gui-preflight.json
vendored
Normal file
23
tests/fixtures/rayforge-windows-gui-preflight.json
vendored
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
32
tests/test_rayforge_windows_gui_preflight.py
Normal file
32
tests/test_rayforge_windows_gui_preflight.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Add table
Reference in a new issue