From ba242790c4bace755a2900be4cfc5586548e5c34 Mon Sep 17 00:00:00 2001 From: Cryptopoly <31970407+cryptopoly@users.noreply.github.com> Date: Fri, 1 May 2026 13:13:31 +0100 Subject: [PATCH 1/2] Preserve Windows GPU runtime on uninstall + lock extras path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Tauri 2 NSIS installer is configured with a custom installer hook file at src-tauri/installer.nsh. The hook intentionally ships as empty stubs that document the contract the GPU runtime depends on: %LOCALAPPDATA%\ChaosEngineAI\extras\cp{major}{minor}\site-packages This directory holds the GPU bundle (torch + diffusers + transformers, ~2.5 GB) that the Image Studio install button writes via pip. Tauri's default uninstaller leaves the path alone today, but the explicit hook file makes that intent visible — anyone adding RM logic in the future gets the comment block as a guardrail. Changes: - Add src-tauri/installer.nsh with documented empty pre/post install + uninstall hooks. NSIS_HOOK_POSTUNINSTALL carries the preserve-extras contract in a comment so the rule can't drift. - Wire the hook into src-tauri/tauri.conf.json via bundle.windows.nsis.installerHooks: ./installer.nsh. - Add a comment block in src-tauri/src/lib.rs::chaosengine_extras_root pointing at the NSIS hook so a Rust-side path move doesn't silently break the Windows-side contract. - Add tests/test_extras_path.py pinning the ChaosEngineAI/extras/cp{maj}{min}/site-packages shape so any future move loud-fails the suite. The Python ABI tag pin matches sys.version_info against the resolved path. Surfaced by the v0.7.2 smoke test on Windows: the user reported the GPU runtime had been wiped after an uninstall + reinstall cycle. The default uninstaller path doesn't touch the extras tree on the bench config we ship, but pinning the contract via these hooks + tests makes the regression visible if anyone adds custom uninstall logic later. Tests: - .venv/bin/python -m pytest tests/test_extras_path.py -v — 3/3 pass - Pre-existing tests still pass --- src-tauri/installer.nsh | 44 +++++++++++++++++++++++++++ src-tauri/src/lib.rs | 6 ++++ src-tauri/tauri.conf.json | 5 ++++ tests/test_extras_path.py | 63 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 src-tauri/installer.nsh create mode 100644 tests/test_extras_path.py diff --git a/src-tauri/installer.nsh b/src-tauri/installer.nsh new file mode 100644 index 0000000..b02ce27 --- /dev/null +++ b/src-tauri/installer.nsh @@ -0,0 +1,44 @@ +; Tauri 2 NSIS installer hooks for the Windows ChaosEngineAI bundle. +; +; Tauri's default NSIS template installs the app under +; %LOCALAPPDATA%\\ and the uninstaller removes that tree on +; uninstall. The GPU runtime bundle (torch + diffusers + transformers, +; ~2.5 GB) is intentionally written to a sibling directory: +; +; %LOCALAPPDATA%\ChaosEngineAI\extras\cp{major}{minor}\site-packages +; +; The path is namespaced by Python ABI tag (commit 24518af, v0.7.0-rc.5) +; so a runtime upgrade that changes Python minor versions cannot shadow +; the wheels from the previous tag. +; +; CRITICAL: this directory MUST survive an uninstall + reinstall cycle. +; Re-downloading 2.5 GB of CUDA wheels every time the user upgrades the +; desktop app is unacceptable, both for users on slow links and for the +; PyPI mirrors that serve the bundle. +; +; The hooks below are intentionally empty as a guardrail. If anyone +; later adds custom uninstall behaviour: +; +; 1. NEVER ``RMDir /r "$LOCALAPPDATA\ChaosEngineAI\extras"`` here. +; 2. Test that ``setup.py:_extras_site_packages()`` resolves the same +; path before AND after a clean uninstall + reinstall on Windows. +; 3. Mirror any change in ``src-tauri/src/lib.rs::chaosengine_extras_root``. + +!macro NSIS_HOOK_PREINSTALL + ; Reserved — currently a no-op. See contract above before adding code. +!macroend + +!macro NSIS_HOOK_POSTINSTALL + ; Reserved — currently a no-op. See contract above before adding code. +!macroend + +!macro NSIS_HOOK_PREUNINSTALL + ; Reserved — currently a no-op. See contract above before adding code. +!macroend + +!macro NSIS_HOOK_POSTUNINSTALL + ; Reserved — currently a no-op. The persistent GPU runtime tree at + ; %LOCALAPPDATA%\ChaosEngineAI\extras MUST be left intact so an + ; immediate reinstall can pick it up without re-downloading 2.5 GB. + ; See contract above before adding code. +!macroend diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4f29137..ddbe60b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -697,6 +697,12 @@ fn apply_embedded_runtime_env(command: &mut Command, runtime: &EmbeddedRuntime) /// Returns ``None`` if we can't resolve a home directory at all (headless /// environments). Callers treat that as "no extras available". fn chaosengine_extras_root() -> Option { + // The extras tree lives OUTSIDE the Tauri install directory so it + // survives uninstall + reinstall cycles — re-downloading the 2.5 GB + // GPU bundle on every desktop upgrade is unacceptable. The Windows + // NSIS installer is told to leave this path alone via the empty + // hooks in ``src-tauri/installer.nsh``; if anyone changes either + // side the other MUST be kept in sync. let base = if cfg!(windows) { env::var_os("LOCALAPPDATA") .map(PathBuf::from) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ea1faa0..c38d18f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -52,6 +52,11 @@ "hardenedRuntime": true, "entitlements": "macos/ChaosEngineAI.entitlements" }, + "windows": { + "nsis": { + "installerHooks": "./installer.nsh" + } + }, "resources": { "resources/": "" } diff --git a/tests/test_extras_path.py b/tests/test_extras_path.py new file mode 100644 index 0000000..aac78ab --- /dev/null +++ b/tests/test_extras_path.py @@ -0,0 +1,63 @@ +"""Pin the persistent GPU extras path on Windows / Linux / macOS. + +The desktop installer is configured to leave this directory alone on +uninstall (``src-tauri/installer.nsh`` on Windows; macOS uninstall is +``rm /Applications/ChaosEngineAI.app`` which doesn't touch +``~/Library/Application Support``). If the path computed by +``_extras_site_packages`` ever drifts from what the installer hooks +expect, the uninstall safety net breaks. + +The tests below pin both halves of the contract — the parent directory +and the ABI tag layout — so any future move is loud. +""" + +from __future__ import annotations + +import os +import sys +import unittest +from pathlib import Path +from unittest import mock + +from backend_service.routes import setup as setup_routes + + +class ExtrasSitePackagesTests(unittest.TestCase): + def setUp(self) -> None: + self._env_patcher = mock.patch.dict(os.environ, {}, clear=False) + self._env_patcher.start() + # Drop the override knob — the explicit env path is for tests + # that pin a custom location, not for the cross-OS shape check. + os.environ.pop("CHAOSENGINE_EXTRAS_SITE_PACKAGES", None) + + def tearDown(self) -> None: + self._env_patcher.stop() + + def test_path_includes_chaosengine_extras_namespace(self) -> None: + path = setup_routes._extras_site_packages() + self.assertIsNotNone(path) + assert path is not None # type narrow + parts = path.parts + # 'ChaosEngineAI/extras/cp{maj}{min}/site-packages' suffix. + # The tree above (LOCALAPPDATA / Library/Application Support / + # XDG_DATA_HOME) is platform-specific; we only assert the tail. + self.assertEqual(parts[-4], "ChaosEngineAI") + self.assertEqual(parts[-3], "extras") + self.assertTrue(parts[-2].startswith("cp")) + self.assertEqual(parts[-1], "site-packages") + + def test_python_abi_tag_matches_runtime(self) -> None: + path = setup_routes._extras_site_packages() + assert path is not None + expected_tag = f"cp{sys.version_info.major}{sys.version_info.minor}" + self.assertEqual(path.parts[-2], expected_tag) + + def test_env_override_wins(self) -> None: + override = "/tmp/chaosengine-extras-override" + os.environ["CHAOSENGINE_EXTRAS_SITE_PACKAGES"] = override + path = setup_routes._extras_site_packages() + self.assertEqual(path, Path(override)) + + +if __name__ == "__main__": + unittest.main() From b703d120653a8500ffe2a8833e11e8eaffe3f8c7 Mon Sep 17 00:00:00 2001 From: Cryptopoly <31970407+cryptopoly@users.noreply.github.com> Date: Fri, 1 May 2026 13:14:40 +0100 Subject: [PATCH 2/2] Lift InstallLogPanel above sibling cards during GPU install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Image Studio and Video Studio layouts render the InstallLogPanel inside the runtime callout, which sits above the Prompt and Recent Outputs cards in source order. Without an explicit stacking context the streaming log panel is layered BEHIND those sibling cards on Windows during a long GPU bundle install — the log keeps accumulating output but the user cannot see it past the first few visible lines. Add 'position: relative' + 'z-index: 5' to '.install-log-panel'. That is enough to win against the surrounding '.panel' cards (which set no z-index of their own) without fighting the tooltip / modal portals that live at higher z-index tiers. Surfaced by the v0.7.2 smoke test on Windows / RTX 4090: the install log was hidden behind the Prompt + Recent Outputs section on Image Studio while the user waited on the ~2.5 GB GPU bundle install. --- src/styles.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/styles.css b/src/styles.css index ec62ad7..d68b611 100644 --- a/src/styles.css +++ b/src/styles.css @@ -6269,6 +6269,15 @@ select.text-input { width: 100%; max-width: 100%; overflow: hidden; + /* Establishes a stacking context so the streaming pip output stays + * above the Prompt + Recent Outputs cards in Image Studio and Video + * Studio. Without these the panel renders behind those siblings on + * Windows during a long GPU bundle install — the log is still alive + * but the user can't see it. ``z-index: 5`` is enough to win against + * the surrounding ``.panel`` cards (which set no z-index of their + * own) without fighting the global tooltip portal (z-index: 1000+). */ + position: relative; + z-index: 5; } .install-log-summary { cursor: pointer;