Skip to content

Latest commit

 

History

History
182 lines (160 loc) · 30.3 KB

File metadata and controls

182 lines (160 loc) · 30.3 KB

tools/gui

Real-desktop GUI tests and demo-recording scenarios for the examples. They launch the built example, drive it with synthetic mouse input, and check real window geometry and on-screen state — things unit and widget tests cannot see.

The generic machinery (UI probe, input drivers, app harness, screen recorders, remote runner) lives in .agents/skills/; read gui-test/SKILL.md there first — especially the safety rules. This directory only holds what is specific to our examples.

Script Example Platform Covers
core_application_exit_flutter_macos.py headless Flutter exit regression macOS Creates no windows and sends no input. A real Dart AppLifecycleListener asynchronously cancels quit with and without nativeapi listeners, then approves quit. Requires Flutter, CMake and the installed debug FlutterMacOS framework; substitutes only the final process termination.
core_application_exit_flutter_linux.py real Flutter secondary-window exit regression Linux X11 / Wayland No input; private Xvfb and Weston displays. Two realized secondary views exercise controller destruction, nativeapi close, GTK show/hide hooks, implicit-view preservation and normal process exit. Tests both listener removal and a manager that stays alive through process exit (18 checks per mode per backend). Flutter-only controls contain no nativeapi native assets; SDK diagnostics stay in the logs and any additional nativeapi warnings fail the test. Requires Flutter, clang, GTK3 development files, Xvfb, Weston and dbus-run-session.
core_window_close_flutter_macos.py Flutter window/controller ownership regression macOS Real FlutterEngine, multi-window controllers and bundled nativeapi Dart FFI: 19 checks for veto, explicit votes, native/public coalescing, host refusal, native close and Flutter-owned destruction, expired wrappers and handles. Test windows remain unshown and no input is sent. Requires Flutter, CMake and the matching debug framework; runs window_close_flutter.dart.
core_window_corner_preference_test.ps1 core corner preference regression Windows 11 No-input test: four DWM policies, unchanged title bar/geometry/shadow, wrapper sharing, fullscreen/maximize restoration, invalid enums and stale handles; saves a comparison image for visual inspection. Override the test executable with CORE_CORNER_TEST_EXE.
core_window_content_protection_test.ps1 core content protection regression Windows No-input test: native display affinity, shared wrappers, C ABI and actual capture exclusion/restoration of a pixel inside the test windows. Override the executable with CORE_CONTENT_PROTECTION_TEST_EXE.
core_window_title_bar_double_click_macos.py core title-bar double-click regression macOS Guarded real double clicks on bare hidden title bars: zoom/restore, Minimize, None and native Fill. Preferences are overridden only in the fixture process. Build window_title_bar_double_click_gui_macos_test in core/build first.
core_window_system_menu_test.ps1 core system menu regression Windows Native popup states and guarded selections of custom, maximize, restore, minimize and close commands; content coordinates scale once. Override the executable with CORE_SYSTEM_MENU_TEST_EXE.
core_window_snap_layout_test.ps1 core window_maximize_button_test --serve (#70) Windows 11 Real hover, no clicks: a hidden-title-bar window marks its own maximize button with SetMaximizeButtonBounds; resting the cursor there opens the snap layouts (another process's window appears below the button), the content window still receives the moves inside the button, and leaving closes the flyout and reaches the content as WM_MOUSELEAVE. Saves a screenshot. Override the executable with CORE_SNAP_LAYOUT_TEST_EXE.
flutter_window_title_bar_snap_test.ps1 window_title_bar_example (#70) Windows 11 End to end in a real Flutter app: with the title bar Hidden the strip draws a Maximize / Restore chip wrapped in MaximizeButtonArea; resting the cursor on it opens the snap layouts and leaving closes them, a click maximizes (the chip reads Restore) and another restores to the earlier width. Owner-guarded clicks, no keyboard. Saves a screenshot. Set TITLE_BAR_EXAMPLE_EXE for a build outside $RemoteWorkspace.
flutter_window_title_bar_system_menu_test.ps1 window_title_bar_example (#66) Windows End to end in a real Flutter app: a secondary click on the DragToMoveArea strip opens the native system menu at the pointer; its item states match the window (restored: Restore off; maximized: Move, Size, Maximize off); choosing Maximize maximizes and, from a second menu, Restore restores to the earlier width. Items are taken by their fixed order, so any display language works. Owner-guarded clicks, no keyboard. Set TITLE_BAR_EXAMPLE_EXE for a build outside $RemoteWorkspace.
flutter_window_title_bar_close_test.ps1 window_title_bar_example (#65) Windows End to end in a real Flutter app: with Closing set to Keep open, the example cancels WindowCloseRequestedEvent; a real click on the window's own close button (found through DWM's caption button bounds) and the system menu's Close each leave the window open and count one cancelled request; with Allow, the close button closes the window and the app exits with code 0. Owner-guarded clicks, no keyboard. Set TITLE_BAR_EXAMPLE_EXE for a build outside $RemoteWorkspace.
flutter_window_title_bar_close_test_linux.py window_title_bar_example (#65) Linux (GNOME) The Linux twin of the close test: with Keep open, two real clicks on the header bar's close button (found by its accessible name through AT-SPI) are cancelled and counted; with Allow the click closes the window and the app exits with code 0. The Flutter view is located through AT-SPI too, since the runner's header bar sits inside the X11 client area. The system menu is gnome-shell's and is not driven. Set TITLE_BAR_EXAMPLE_EXE for a build outside $REMOTE_WORKSPACE.
flutter_window_title_bar_close_test.py window_title_bar_example (#65) macOS The macOS twin of the close test: with Keep open, two real clicks on the window's close button (found through Accessibility) are cancelled and counted; with Allow the click closes the window and the app exits with code 0. On macOS 26 the button sends the private -[NSWindow __close] instead of performClose:, which core confirms too; this is the test that caught it.
flutter_window_manager_gui_test.py / .ps1 / _hyprland.py window_manager example, fixture window_manager_flutter.dart macOS / Windows / Linux (Hyprland) leanflutter/window_manager's widgets and close handling through its 0.5.x compatible API: WindowCaption drags, maximizes / restores and minimizes the window and, with setPreventClose(true), its close button and the window's own close button (DWM's caption button bounds on Windows, Accessibility on macOS, GTK's header bar and the compositor's close request on Hyprland) report onWindowClose and keep it; VirtualWindowFrame's edge resizes it; popUpWindowMenu() opens the system menu (Windows); setIgnoreMouseEvents(true, forward: true) passes hit testing through while hovering still reaches the window, with nothing pressed meanwhile; destroy() closes past prevent-close. The fixture is copied to example/lib/gui_test_main.dart and built with -t. On Hyprland a temporary rule floats the window; maximize, minimize and the resize edge are skipped there (suppressed by the default config, absent on Wayland, and the window reported maximized). Set WINDOW_MANAGER_DIR (macOS), WINDOW_MANAGER_EXE (Windows) or WINDOW_MANAGER_BUNDLE (Hyprland) for builds elsewhere.
flutter_window_reports_test.ps1 fixture window_reports_flutter.dart (#75, #76, #77, #78) Windows The window_manager reports carried over to nativeapi-core, replayed through the Flutter Window API in a fixture app built from the workspace's Dart packages (REPORTS_ARCHIVE). The app paints solid colours and announces each step; the runner measures geometry, styles, DWM caption buttons, pixels and frame-by-frame samples. Scenarios (REPORTS_SCENARIOS): startup-center (#428 no white or black frame, #504/#223 appears where it stays, centred), startup-maximize (#412/#572 maximized before the first show appears maximized and stays so), flashes (#383/#153/#155 no white or black frame while maximizing, restoring or showing after hide, #578 a part dragged back from off screen painted within 100 ms), fullscreen (#458 start in full screen and leave it, #579 caption back, #330 no stale content at the bottom), hidden (#547 top border as thick as the sides, #554/#450 maximized exactly to the work area, #378 no padding in full screen, #397 no caption flash when leaving it), transparent (#576 caption buttons and title with a transparent background), events (#294 focus last after restoring, #207 double-click on the caption maximizes and restores with Resized, #511 maximize() from onDoubleTapDown, #534 setIcon() from flutter_assets). Owner-guarded clicks on the app's own window only, no keyboard. Not covered: #480 (keyboard) and #245 (two monitors).
core_window_shadow_layout_test_linux.cpp core shadow Linux Wayland / X11 No-input regression: stable shadow geometry, contour pixels inside transparent content, input pass-through, disable and decoration restoration
flutter_window_shape_smoke_macos.py / flutter_window_shape_test_windows.ps1 shaped_window_example macOS / Windows macOS: no-input shape and pixel-alpha smoke test; Windows: native regions, real button clicks/drag, resize, clear/reapply, click-through to the underlying app window
flutter_detachable_window_test.py / .ps1 detachable_window_example macOS / Windows tear a panel off, exact content size, header stays under the cursor, dock into the other window, State preserved
gpui_detachable_window_test.py gpui_detachable_window_example (Rust, GPUI) macOS a click on a header opens nothing; tear a panel off, exact content size, header stays under the cursor, dock it back by dragging onto its slot; Pop out, then closing the floating window docks it
gpui_window_drag_areas_test.py gpui_window_drag_areas_example (Rust, GPUI) macOS Window::start_dragging from the bar: the window follows, a click does not move it, a double click maximizes and restores; Window::start_resizing: the bottom-right and left handles move exactly the edges they own
gpui_floating_toolbar_test.py gpui_floating_toolbar_example (Rust, GPUI) macOS the toolbar attached with set_parent_window starts centred 10 px above the main window and re-centres after the main window is dragged by its title bar and resized from its corner
gpui_browser_tabs_test.py gpui_browser_tabs_example (Rust, GPUI) macOS tear a tab off into a third window (tab under the cursor), move that window by its empty strip, merge its tab into the other window's strip
flutter_window_drag_areas_test.py / .ps1 window_drag_areas_example macOS / Windows DragToMoveArea: window follows the mouse, a click does not move it, double click maximizes and restores; DragToResizeArea: all eight handles, the other edges stay anchored, minimum size, enableResizeEdges, clicks pass through the middle
flutter_menu_test.py / .ps1 menu_example macOS / Windows (WinUI 3 and Native backends) context menu opens at the click point; placement Top End (also after the menu changed); item types and states (checkbox, radio group, disabled, submenu, special characters); click / open / close / submenu events; dismissing fires no click; label change, added item and detached submenu show on the next open; absolute and cursor positioning
flutter_floating_toolbar_test.py / .ps1 / _linux.py floating_toolbar_example macOS / Windows / Linux (inside only) Window.setParentWindow with two Flutter windows: the toolbar window starts centred above the main one, follows a move and re-centres after a resize (through Accessibility, --no-input stops here); follows a real drag of the title bar; a press in the toolbar counts up in the main window; detached it stays put, attached it comes back; hidden it is not brought back by moving its parent. Windows runs the same steps (moves through SetWindowPos). Linux: multi-window Flutter only runs as a Wayland client, which cannot be measured or pressed from outside, so the twin only checks from the inside that both views render, setParentWindow succeeded, the toolbar view has the size it was given (it was 52 px short while core un-decorated the window instead of hiding its header bar) and the app keeps running
flutter_window_events_test.py window_example macOS WindowManager.addListener really is called: focused on activation, moved and resized when the frame changes (through Accessibility, one click only), payloads equal to the frame the OS reports
core_window_drag_session_test.py / .ps1 / _linux.py core window_drag_session_example (C++) macOS / Windows / Linux dock by dragging onto another window, tear off anchored under the cursor, event output; the Linux twin also checks that the panel follows the cursor while carried
core_window_visual_effect_test.py / .ps1 core window_visual_effect_example (C++) macOS / Windows Window::SetVisualEffect: the example walks a window through every effect over a plain red window and the test samples the screen after each step - the materials that blend with the windows behind come out reddish, no effect does not, SetVisualEffect agrees with IsVisualEffectSupported, GetVisualEffect is the effect in force, and a background color set while an effect was active is what shows once the effect is removed. No input on macOS; on Windows one click on the example's title bar, because the system backdrops are only drawn for the active window
flutter_visual_effect_test.py / .ps1 visual_effect_example macOS / Windows the same through a Flutter window: started with VISUAL_EFFECT_AUTOPLAY=1 the example shows the red backdrop and walks through the effects itself; where Flutter paints nothing the material shows the red behind, and with the effect removed the Flutter view has its opaque backing again. No input on macOS, the one activating click on Windows
flutter_menu_theme_test.ps1 menu_example Windows (WinUI 3 and Native backends) switches Dark / Light / System through Flutter, checks native appearance results and actual menu background colors; accepts -Exe, -KeepOpen and -NativeOnly
core_menu_lifetime_test.ps1 core tests/menu_lifetime_test.cpp Windows issue 54: a Menu destroyed from a listener that runs inside the window procedure does not kill the process, and a menu created after every other menu was destroyed still gets its events. No input, but it needs a desktop session
core_menu_backend_test.ps1 core tests/menu_click_test.cpp Windows native menu backend: a top-level and a submenu item click reach the listener before Menu::Open() returns and fire exactly once; dismissing without picking fires none. The test binary owns the assertions, the script owns the mouse
core_drag_drop_test.py / .ps1 core drag_drop_example (C++) macOS / Windows DragSource → DropTarget across two windows: enter / move / drop events, dropped file path and text, drop position in content coordinates, source reports copy; a drag released where nothing accepts it reports exit and none
flutter_drag_drop_test.py / .ps1 drag_drop_example macOS / Windows DragOutArea → DropRegion in one window: a file and a text drop arrive, the source sees copy, the highlight clears, a second drag works after the first
flutter_tray_icon_test.py / .ps1 / _linux.py tray_icon_example macOS / Windows (WinUI 3 and Native menu backends) / Linux animated tray icons: frames really are rendered and pushed (counter, 30 and 60 fps, no dropped frames, Pause / Step / Resume, 3x resolution), a live widget captured into frames, the Download scene driving the title, three icons animating at once; title / tooltip / visibility / trigger / bounds read back from the native getters; the menu opened from code shows its items and states, an item click arrives, closeContextMenu closes it; "Window to icon" puts the window below the icon on macOS and above it on Windows, centred on it; the example's own checklist has no unexpected failures. Windows tray icons have no title, so the title checks become one no-op check there. On Linux the icon is a StatusNotifierItem: no bounds and no menu opened from code, so the test checks the example does not offer them; the frame pipeline is asserted at 10 fps (what 30 fps gives on the host is printed), and the icons are counted where the shell counts them: RegisteredStatusNotifierItems lists one, then three, then one again after two are removed. Not covered: clicks on the tray icon itself — the input driver refuses the menu-bar layer, those stay manual items of the example's Checklist tab
flutter_tray_popup_test_hyprland.py tray_icon_example Linux (Hyprland, the Omarchy laptop) the example's popup mode (TRAY_POPUP=1, or Properties → Popup) together with a Hyprland window rule placed by cursor_x/cursor_y: a real left click on the tray icon in the bar (located by diffing two grim captures of the bar, pressed through wlpointer, the kit's zwlr_virtual_pointer tool) maps the window floating and pinned with its right edge 20 px right of the click and its top 20 px below it, under the bar, focused; focusing another window hides it ([popup] hidden (blur) on stdout); Activate over D-Bus with the cursor in the middle of the screen maps it there (the rule is re-evaluated on every map because the example hides instead of lowering); a click while it shows hides it. The rule is installed into ~/.config/hypr/hyprland.lua for the run (a marked require line plus a file next to it) and removed again, both checked with hyprctl configerrors. Needs the item pinned in Omarchy's tray (new items sit in a collapsed drawer) and a Lua-config Hyprland (0.56+: hyprctl dispatch takes hl.dsp.* calls)
core/tests/window_shadow_remap_linux_test (in core) core hidden-title-bar shadow Linux (Hyprland; any compositor) no-input regression, run floating (hl.window_rule for its class on Hyprland): a window whose title bar is hidden keeps its content size across hide and show whatever xdg states the compositor sends - Hyprland reports every window tiled and maximized, and GDK takes no resize while they stand, so core must not add its shadow gutter before the compositor has answered. Three timings of hiding the title bar; exact content size once settled, no gutter band and no growth across shows in all of them
core_window_focus_policy_test.ps1 core tests/window_focus_policy_test Windows real clicks on a native child button preserve the anchor window's keyboard focus (set CORE_FOCUS_TEST_FOREIGN=1 to place the anchor in a separate process) in four combinations of SetFocusable and SetNonActivating; clicks remain delivered. The executable also checks native styles and mouse-activation responses, Show/Focus, state shared across wrappers, and restoring focusability. Build in $REMOTE_SCRATCH/core-build, or set CORE_FOCUS_TEST_EXE to the executable path. Linux runs the same executable without --clicks, in a desktop whose window manager honors GTK focus hints (Xvfb + Openbox also works); WSLg does not honor them
core_tray_icon_events_test_linux.sh core tray_icon_example (C++) Linux the tray icon's menu export and events, driven over D-Bus the way the shell drives a StatusNotifierItem (the input driver may not press the top bar): with the RightClicked trigger the menu is exported (Menu, its items in GetLayout) without ItemIsMenu; WindowId is an int32, as gnome-shell reads it; the host's dbusmenu events come back as menu events (opened / closed on the root once each however often they come, clicked on an item); from a host other than gnome-shell Activate is a click, a second one within the double-click time a double click (a third starts a new pair, two slow ones are two clicks), ContextMenu a right click; SecondaryActivate and Scroll report nothing yet. Not covered: gnome-shell's own Activate (a double click) and a real host's menu, which only real clicks in the top bar show — checked by hand on GNOME 46 and in KDE Plasma 6
flutter_launch_at_startup_test.py launch_at_startup's own example, in a leanflutter checkout ($LEANFLUTTER_DIR, default ~/Projects/leanflutter; see common.py) macOS the 0.5.x compatible API on nativeapi: Enable registers an SMAppService login item, isEnabled reads it back, Disable removes it, and setup(args:) neither throws nor leaves an entry behind. Always leaves the login item off, including on a failure. Written but not yet run — the display was asleep; the same sequence was verified headlessly instead
flutter_detachable_window_and_browser_tabs_demo.py / .ps1 both Flutter examples macOS / Windows the demo video scenarios; also the only coverage of browser_tabs_example (reorder, tear off, merge, move by the strip; its log ends with the preserved page state, no PASS/FAIL checks)
flutter_floating_toolbar_demo.py / .ps1 floating_toolbar_example macOS / Windows the floating toolbar demo video: the pill drives the main window (colours, Stamp), follows it when it is dragged by the title bar and resized from its corner, stays behind when detached and snaps back when attached, is hidden and shown again; ends on the counter and the log. DEMO_DRY_RUN=1 on the Windows host plays it without recording; without --record the macOS script does the same
flutter_tray_icon_demo.py tray_icon_example macOS the tray demo video: the example moves its window next to its tray icon ("Window to icon") so the real icon and the magnified preview are in one picture; gallery, widget capture, 10 → 60 fps, Pause / Step, scenes, three icons at once, menu opened and closed from code, checklist
flutter_window_shape_demo.py / .ps1 / _linux.py shaped_window_example macOS / Windows / Linux the shape demo video: every one of the twelve gallery silhouettes once, left to right / top to bottom, then the five contour-shadow presets (None, Soft, Float, Sharp, Glow); ends held on the last silhouette with its shadow. The recorder starts after the app is up and arranged, so the take opens on the app. DEMO_DRY_RUN=1 (or -DryRun) on Windows, and no --record on macOS, play it without recording; `--only shapes

common.py points the macOS scripts at the skills' harness and at the Flutter examples (examples/flutter_*; the scripts name them without the prefix). The Linux scripts import guiapp straight from the flat kit the remote-hosts skill pushes, and find the example through $REMOTE_SCRATCH.

Running

Scripts that drive input take over the mouse. They refuse to start while the mouse is moving and stop as soon as a press would land outside the example's own windows. No keyboard input is ever sent. The table identifies tests that need no input.

# macOS (examples built with `flutter build macos --debug`, or pass --build)
tools/gui/flutter_detachable_window_test.py
tools/gui/flutter_window_drag_areas_test.py
tools/gui/gpui_detachable_window_test.py --build        # cargo build in the example
tools/gui/gpui_window_drag_areas_test.py --build
tools/gui/gpui_floating_toolbar_test.py --build
tools/gui/gpui_browser_tabs_test.py --build
tools/gui/flutter_menu_test.py
tools/gui/flutter_window_events_test.py
tools/gui/flutter_floating_toolbar_test.py          # --no-input: only the part that needs no mouse
tools/gui/core_window_drag_session_test.py --build   # builds the example into core/build
tools/gui/core_drag_drop_test.py --build
tools/gui/flutter_drag_drop_test.py
tools/gui/flutter_tray_icon_test.py
tools/gui/flutter_launch_at_startup_test.py
tools/gui/core_window_visual_effect_test.py --build   # no input
tools/gui/flutter_visual_effect_test.py               # no input
tools/gui/flutter_detachable_window_and_browser_tabs_demo.py --record   # --only detachable|tabs, --keep-open
tools/gui/flutter_tray_icon_demo.py --record
tools/gui/flutter_floating_toolbar_demo.py --record
tools/gui/flutter_window_shape_demo.py --record

# Windows, from the Mac (examples built there in debug; see the remote-hosts skill)
R=.agents/skills/remote-hosts/scripts/remote.sh
$R win setup                       # "win" = the host name in remote-hosts/hosts/win.env
$R win desktop tools/gui/flutter_detachable_window_test.ps1 150
$R win desktop tools/gui/flutter_window_drag_areas_test.ps1 200
$R win desktop tools/gui/flutter_menu_test.ps1 400
$R win desktop tools/gui/core_window_drag_session_test.ps1 120
$R win desktop tools/gui/core_drag_drop_test.ps1 120
$R win desktop tools/gui/flutter_drag_drop_test.ps1 150
$R win desktop tools/gui/flutter_tray_icon_test.ps1 400
$R win desktop tools/gui/flutter_floating_toolbar_test.ps1 240
$R win desktop tools/gui/core_window_visual_effect_test.ps1 120
$R win desktop tools/gui/flutter_visual_effect_test.ps1 150
.agents/skills/record-demo/scripts/record_remote.sh win tools/gui/flutter_detachable_window_and_browser_tabs_demo.ps1 tools/gui/output
.agents/skills/record-demo/scripts/record_remote.sh win tools/gui/flutter_floating_toolbar_demo.ps1 tools/gui/output
.agents/skills/record-demo/scripts/record_remote.sh win tools/gui/flutter_window_shape_demo.ps1 tools/gui/output

# Linux, from the Mac (GNOME; "linux" = the host name in remote-hosts/hosts/linux.env)
R=.agents/skills/remote-hosts/scripts/remote.sh
$R linux setup
$R linux run tools/gui/build_core_example_linux.sh window_drag_session_example
$R linux desktop tools/gui/core_window_drag_session_test_linux.py 180
$R linux desktop tools/gui/flutter_floating_toolbar_test_linux.py 120   # no input; Wayland client
$R linux desktop tools/gui/flutter_tray_icon_test_linux.py 300   # example built in the host's checkout
$R linux run tools/gui/build_core_example_linux.sh tray_icon_example
$R linux desktop tools/gui/core_tray_icon_events_test_linux.sh 60
$R omarchy setup                                                 # Hyprland laptop: pushes wlpointer's sources with the kit
$R omarchy desktop tools/gui/flutter_tray_popup_test_hyprland.py 240   # example built in the host's staged workspace
$R linux desktop tools/gui/flutter_window_shape_demo_linux.py 240   # dry run: no recorder
.agents/skills/record-demo/scripts/record_remote_linux.sh linux \
    tools/gui/flutter_window_shape_demo_linux.py tools/gui/output   # the take

Run the Linux Flutter exit regression on a Linux host with python3 tools/gui/core_application_exit_flutter_linux.py --flutter /path/to/flutter/bin/flutter. It saves both apps, native assets and logs in an isolated work directory. Flutter 3.47.6 currently emits window-monitor diagnostics in the Flutter-only control too; Xvfb also reports compositor shader setup/cleanup warnings, and headless Weston has no keyboard seat. The runner preserves these messages and compares their counts with the control; --fatal-warnings additionally makes all SDK/GTK warnings fatal.

For the real compositor and graphics driver, run inside the logged-on session with --display desktop-wayland (or --display desktop-x11 for X11/Xwayland). These modes retain the session environment and do not force software rendering; they run the Flutter-only control plus both nativeapi modes (36 checks). The apps show and close their own windows without synthetic input. Xvfb and Weston are only required for the default private-display mode.

The shape demo runs against a debug build of the example in the host's scratch dir ($REMOTE_SCRATCH/shape-flutter-linux/examples/flutter_shaped_window_example, or SHAPE_EXAMPLE_DIR); put one there with git archive <sha> pubspec.yaml bindings/dart examples/flutter_shaped_window_example | ssh <host> "mkdir -p ... && tar -x -C ..." (the example resolves through the root pub workspace, so all three paths are needed) when the host cannot reach its own remote, then --build. A host with no monitor attached cannot record it at all — see the Linux section of record-demo/SKILL.md and remote-hosts/references/linux.md#no-display-attached.

The Linux scripts build and run out of the host's scratch directory, from a snapshot of core/ rather than the host's checkout — that keeps a test run independent of whatever is being edited there. build_core_example_linux.sh expects the snapshot in $REMOTE_SCRATCH/core-src; create it with git -C core archive HEAD | ssh <host> "mkdir -p ~/tmp/claude/core-src && tar -x -C ~/tmp/claude/core-src".

Each test prints PASS/FAIL lines and exits with the number of failures. A SKIP means the desktop did not allow a step (for example no bare desktop visible to click).

Recordings

Videos go to tools/gui/output/ (git-ignored), named after the script that made them, as recorded — nothing is trimmed or re-encoded afterwards. A new take overwrites the previous one.

File What
<script name>-<os>.mp4 the recording (--record on macOS, the record-demo skill's record_remote.sh for a remote host)
<script name>-<only>-<os>.mp4 a single scenario recorded with --only (macOS)
<script name>-<os>.log the scenario log of a remote take

A Windows host encodes the MP4 itself (ffmpeg must be installed there); record_remote.sh copies it to the output directory and removes it from the host.

Naming

<binding>_<example>_test.<ext> for tests and <binding>_<what it shows>_demo.<ext> for recording scenarios, where <binding>_<example> is the example's directory name under examples/ without its _example suffix (examples/flutter_menu_example → flutter_menu_test), and core_<example> for the C++ examples in core/examples; a test exists once per platform it runs on: .py runs on macOS, .ps1 on Windows, …_linux.py on Linux (both are Python, and names must be unique: remote hosts receive them in one flat directory).

Adding a test

Copy .agents/skills/gui-test/templates/test_template.{py,ps1} here and fill it in. Keep the .ps1 files ASCII.