Files
komorebi/docs/dev/e2e-to-unit.md
T
LGUG2Z 18d7f9a506 docs(e2e): how the macOS guest works and how to run it
A README for e2e/platforms/tart_macos: what a run does, why the grants live in the golden image and
are inherited through Terminal, why the agent is a Rust program (the stock image has no developer
tools), and the limits (one display, no input, two guests per host, Apple hardware only, so not in
CI). docs/dev/ci.md and e2e-to-unit.md said macOS had no integration coverage; they now say where it
runs and what it cannot reach.
2026-11-06 14:40:50 -08:00

147 lines
16 KiB
Markdown

# From e2e to unit: what the shared crates made testable natively
> **Paths (cutover stack):** this document was written before the directories were renamed and keeps the paths of its time. Now: `komorebi/` is `komorebi-windows/`, `komorebi-for-kde/komorebi/` is `komorebi-linux/`, `komorebi-for-mac/komorebi/` is `komorebi-macos/`, `komorebi-for-kde/komorebi-hotkeys/` is `komorebi-hotkeys/`, `komorebi-for-kde/nix/` and `komorebi-for-mac/nix/` are `nix/linux/` and `nix/macos/`, `komorebi-for-kde/docs/` and `komorebi-for-mac/docs/` are `docs/linux/` and `docs/macos/`, `schema*.json` are in `schemas/{windows,linux,macos}/`, and every package is at version 0.2.0 (the 0.1.x numbers below are history).
Base: `wip-cand5` plus the revisions that add the tests named here. Written 2026-10-31.
Before the shared crates, the Windows daemon could only be tested through a Windows guest (its crate
does not compile on Linux) and the macOS daemon through nothing. Now Windows, KDE and macOS call the
same `komorebi-core` (`Container`, `Workspace`, `Monitor`, `update_shared` and the platform hooks),
`komorebi-layouts`, `komorebi-scrolling`, `komorebi-settings`, `komorebi-protocol`,
`komorebi-config`, `komorebi-transport` and `komorebic-core`, so a native test of that code, on the
stub platform (`crates/komorebi-core/tests/common`) or `FakeBackend`, protects all three. This
document lists every e2e scenario, says whether that is now done, and ranks what is left.
## What stays out of reach
The daemons' own `process_command` / `process_event` are per platform (Windows: Win32 types; macOS:
Mac types), only the KDE one runs on `FakeBackend`. So a native test of "send this command, then the
state is that" exists for KDE only; for Windows and macOS it holds as far as the command's work is
done by shared `Workspace`/`Monitor` methods. Rule matching (`should_act_individual`) exists three
times (Windows `komorebi/src/window.rs:1178`, KDE `window.rs:661`, macOS `window.rs:1864`), not in
`komorebi-config`. Every row below says which of these a test reaches.
## Classes
- A: already covered natively. B: coverable natively with the shared API, not yet. C: needs a real OS
(compositor, Win32, timing, pointer, rendering, several processes).
- "Now" marks what this work added (revisions on `wip-u-e2e2unit`).
- Value: share of the gate it could stand in for. Effort S/M/L.
- Check counts: dynamic numbers where the gate printed them (layouts 111, commands 265), else the
number of check call sites in the scenario file (static, lower than the runs).
## Windows-origin scenarios (first: only e2e-testable on Windows, now native)
| Scenario (checks) | Behaviour | Class | Native test (cited, or proposed) | Eff. | Value |
|---|---|---|---|---|---|
| layouts (111) | 8 layouts x counts: each window where the layout says, inside the work area, not over another | A now (arithmetic); C (OS frame equals tile) | `crates/komorebi-core/tests/e2e_layouts.rs` `every_layout_tiles_every_container_inside_the_work_area_and_off_the_others` (shared `update_shared`), `komorebi-layouts/tests/layouts_everywhere.rs` (calculate, 3 areas, 1-12 windows) | S | high: 3 min, can shrink to 1 layout smoke |
| flipping (3) | flip-layout mirrors every place, flip back restores | A now | `e2e_layouts.rs` `a_flip_mirrors_the_tiles_of_the_workspace_as_a_set`; `layouts_everywhere.rs` flips | S | med |
| layout_flip (10) | `layout_flip` config mirrors from the start; flip-layout toggles; per workspace | A now for the maths; B for config key to `Workspace.layout_flip` | same tests; proposed: `komorebi-settings` or core test that loads a workspace config with `layout_flip` and checks the field | S | med |
| padding (7) | workspace/container padding moves and shrinks tiles, setting back restores | A now | `e2e_layouts.rs` `the_workspace_padding_moves_the_tiles_in...`; existing `update_shared.rs` `the_padding_of_the_workspace_wins_over_the_monitors_even_when_it_is_zero` | S | med |
| tiling (6), closing (5), minimizing (4), floating (7) | windows share the area; a leaving container (closed/minimized/floated) gives the others room | A now (room) + A (floating/removal): `workspace_layers.rs` floating_* , `workspace_containers.rs` removing_*; C: the OS event | `e2e_layouts.rs` `removing_a_container_gives_the_others_more_room` | S | med |
| stacks (23) | stack/cycle-stack/unstack: one tile per stack, window in view has focus, others away | A: core `workspace_containers.rs` `stacking_*`, `unstacking_*`, `container_behaviour.rs` hide/restore; now `e2e_layouts.rs` `a_stack_is_one_tile`. B: `cycle-stack` has no shared method (daemon-side) | S | med (23 checks, 3 min) |
| tiling_off (6) | toggle-tiling leaves windows, tiling again tiles | A now | `e2e_layouts.rs` `a_workspace_with_tiling_off_places_nothing...`; `update_shared.rs` `a_workspace_that_is_not_tiling_prunes_and_places_nothing` | S | low |
| monocle (12) | monocle fills area, others away, toggle back; focus behaviours | A: `workspace_layers.rs` `toggling_monocle_*`, `reintegrating_*`, `cycling_monocle_*`; `update_shared.rs` `a_monocle_container_gets_the_work_area_less_every_padding`. Focus behaviour is daemon-side (B, needs seam) | - | - | med |
| maximizing (29) | toggle-maximize, unmaximize on float, monocle wins | A: `workspace_layers.rs` `floating_the_maximized_window_*`, `update_shared.rs` `the_monocle_container_wins_over_a_maximized_window`; user-maximize event C | - | - | med |
| promoting (4), moving (4), focus (7) | promote first tile, swap/cycle-move, focus order | A: `workspace_containers.rs` `promoting_*`, `swapping_containers_*`, `cycling_wraps_*`, `directions_follow_the_layout_and_the_flip`; focus OS agreement C | - | - | low |
| workspaces (8), workspace_moves (8) | send/move/focus workspace, last-workspace | A: `monitor_workspaces.rs` (focus, last focused, ensure, remove); the OS hide/show is C | - | - | med |
| commands (265, 7.6 min) | every komorebic command leaves the daemon alive, answers state, manages every window | A now for command line (`crates/komorebic-core/tests/e2e_sweep.rs`: every SWEEP/PRINTED line is a Windows command; macOS lacks exactly 8) and for the KDE model (`komorebi-linux/tests/command_sweep.rs`, about 185 messages, 0.2 s); B: same sweep for the Windows and macOS daemons (needs seam, below); C: stall-family (daemon busy 8 s), socket answering | S done / L for Win+mac | highest: biggest, flakiest scenario |
| unknown_monitor (12) | a window sent to a monitor that does not exist stays managed | B: shared `Monitor`/`Workspace::add_container` errors are covered (`monitor_workspaces.rs` `adding_a_container_to_a_workspace_that_is_not_there_is_an_error`); the daemon handler is per platform: needs seam | M | med (known flaky marker) |
| rules (12), rules_class (5), rules_exe (5), manage_rules (4), title_rule (5), startup_rules (5) | which window a rule catches (kind, strategy, composite, last wins, manage over ignore); the workspace it goes to | A now for matching, once for all three daemons: `crates/komorebi-config/tests/rule_matching.rs` (16 tests, KDE/macOS and Windows input shapes; the matching moved to `komorebi_config::rules`, see `docs/dev/rule-matching.md`); KDE keeps the manage-rule rescue test. Workspace of a rule: A in core: `workspace_containers.rs` `initial_placement_rules_choose_where_a_new_window_goes`. When-evaluated (open vs rename), hide/show: C | M to move the function into `komorebi-config` and test once for three daemons (recommended seam) | high |
| state_fidelity (5) | state names title/exe/class/path as opened | A: `state_roundtrip.rs` (kfw/kfde fixtures, `the_fixtures_are_the_ones_the_e2e_library_has`, `a_windows_tiled_state_is_written_as_it_was_read`); what the OS reports C | - | - | low |
| queries (3) | `komorebic query X` answers from the state, resize save/load round trip | B: state answers per query are `StateQuery` handling in the daemon; resize round trip needs `process_command` (KDE: `resize.rs` `sizes_saved_to_a_file_are_loaded_back_from_it` A) | M | low |
| config_reload (8) | file change reloads names, layout, rules | A for the merge/reset semantics: `crates/komorebi-settings/tests/reload.rs` (`a_reload_merges_what_the_file_gives_and_resets_what_it_resets`), `src/handle.rs`; C: the file watch and the reload message dropped while the lock is held (the NEW failing check in gate5 is that mechanism) | - | - | med |
| placement (1 x 5 cases) | initial_window_placement_rules first/last/Nth | A: `workspace_containers.rs` `a_one_based_placement_index_is_zero_based_and_out_of_range_falls_back_after_the_focused`, `initial_placement_rules_choose_where_a_new_window_goes`. The known Windows failure (number 2) is a daemon event-path matter | - | - | low |
| scrolling (46) | strip, edges, columns, two monitors | A: `komorebi-scrolling/tests` (rules 80, tiles 40, heights 20, pan 16) and `update_shared.rs` `a_scrolling_workspace_*`; the resting window at an edge on the OS is C | - | - | med |
| resize_commands (7), resize (26) | resize-axis widens and gives back; edge drag | A: `resize_constraints.rs`, `update_shared.rs` `the_resize_constraints_are_enforced_before_the_layout_is_calculated`; B (S): `DefaultLayout::resize` increase then decrease is the identity (no test in `komorebi-layouts`); the pointer drag and client rounding C | S | med |
| pausing (4) | paused daemon ignores new windows | A: `komorebi-transport/src/pause.rs` (the message filter); the event filter is per daemon (B needs seam) | - | - | low |
| hotplug (11), monitors (13) | unplug/plug a display, windows move, send-to-monitor | A for the model in `monitor_workspaces.rs`; the reconcile is per platform (Windows `monitor_reconciliator`, macOS own): B needs seam; the display itself C | L | med |
| lifecycle (7), lifecycle_hidden (13), locking (7), handover (6), fullscreen (6), display_change (4), focus_follows_mouse (7) | stop/start restores windows, screen lock, minimize/unmanage/manage, fullscreen, mode change, pointer focus | A for hide/restore: `container_behaviour.rs` `restoring_*`, `workspace_layers.rs` `hiding_a_workspace_*`; the process, session, OS events and pointer are C | - | - | - |
| Windows extras: bar (21), windows_padding, cloaking, config_file, tracking, second_display, display_change | bar process and window, 7 px border, cloaking, file watch, system focus setting, QXL display | bar schema: A (`crates/komorebi-bar-core/tests/schema_oracle.rs`); rest C | - | - | - |
## macOS-origin behaviour: what the shared tests give besides the e2e
The shared e2e scenarios run on macOS in a Tart guest, by hand on a Mac (`nix run .#e2e-macos`,
`e2e/platforms/tart_macos/README.md`; not in CI). What a macOS guest cannot do (a second display, a
pointer, a lock screen) is still never e2e-tested. The shared-crate tests that exist protect its code path as far as the hooks do:
`state_roundtrip.rs` `a_macos_shaped_state_is_written_as_it_was_read_with_the_corner_last`,
`a_macos_state_cannot_be_read_without_its_corner`, the `StubWindow` that is `Clone` but not `Copy`,
and now `e2e_layouts.rs` (the tiles every macOS tiling goes through). Proposed B tests, platform
dependent and meant for the Mini: the hide-spot corner per monitor (`hide_spot` hook is
`StubState { spot }`) with `hide`/`restore` on a stack, and `komorebic-core` macOS command set
(`e2e_sweep.rs` pins which sweep commands macOS lacks).
## KDE VM suites (33 suites with a test.py)
All of them need a headless or Plasma KWin: C. The model parts of them are already native through
the KDE tests (`komorebi-linux/tests`, formerly `komorebi-for-kde/komorebi/tests`, about 880): `core-e2e` and `hotplug`, `placement`,
`workspace-rules`, `scrolling`, `stacks`, `user-maximize`, `commands`, `core-lock`,
`focus-follows-mouse`, `wallpaper` have a native model twin; `kwin*`, `check`, `module`, `keys-*`,
`look`, `features`, `rdp`, `render-smoke`, `latency`, `client-geometry`, `drag-resize`,
`live-resize`, `lock-sleep`, `run-kde`, `restart-survival`, `kwin-restart` are the compositor, the
session, keys, pixels or timing (C). The `commands` suite's sweep now has a native twin
(`command_sweep.rs`), so the suite can run a shorter sweep.
## Counts
Scenario rows: Windows/shared 43 scenarios + 7 Windows extras = 50. Classified by what is left to do
after this work: A (fully native for the shared logic, e2e only needed as a smoke) 22; A now (added by
this work) 9 of those (layouts, flipping, layout_flip, padding, tiling, closing, minimizing, floating,
tiling_off, stacks in part); B open 7 (commands for Windows/macOS daemons, unknown_monitor, queries,
hotplug reconcile, pausing event path, rules for Windows/macOS copies, resize identity); C only 21
(lifecycle*, locking, display*, bar, extras, pointer, fullscreen, wallpaper rendering and the KDE
suites). KDE VM: 0 A fully, 33 C (about 10 with a native model twin).
## Time saved
The Windows gate is about 2.3 min per scenario for 35 scenarios (about 80 min), the KDE suites 15
x 1-13 min. Honest estimate:
- Today (what this work added): the arithmetic of layouts, flipping, layout_flip, padding, tiling
and the room checks is native. A guest still has to show the daemon hands the tiles to the OS, so
these scenarios shrink to one smoke each, or are merged into `tiling`: layouts (111 checks, about
3 min) to one layout and 3 windows saves about 2 min; flipping, layout_flip, padding,
tiling_off, minimizing, closing, floating merged in `tiling` save about 7 x 2 = 14 min. About 16
min of 80 (20%) on Windows, plus commands: the 7.6 min scenario keeps 20 commands as a smoke and
loses about 5 min once the Windows daemon is reachable by the sweep.
- If the B items existed (daemon model reachable natively for Windows and macOS, rule matching in
one shared function, config key tests): another 12 scenarios (rules, rules_class, rules_exe,
manage_rules, title_rule, startup_rules, state_fidelity, queries, config_reload, placement,
workspaces, workspace_moves) become one smoke each: about 25 min. Total about 45 min of the 80 min
gate (55%), and almost all the flaky checks (stall-family markers sit in commands, config_reload,
resize, maximizing, floating, which are the ones with timing).
- KDE VM: the model twins let `commands`, `placement`, `workspace-rules`, `hotplug`, `scrolling`
run with fewer cases; this is about 20-30 VM minutes of the 15 suites, not measured.
Not saved: boot and golden image (3 s from a state), the C scenarios.
## Seams that are missing (report only, nothing added)
1. The daemon's `process_command` and `process_event` are per platform. A model-level
`CommandHandler<P>` in `komorebi-core` (taking `SocketMessage`, mutating `Monitor`/`Workspace`,
calling hooks) would let one sweep (`command_sweep.rs`) protect Windows and macOS: `commands`,
`unknown_monitor`, `queries`, `pausing`, `handover`, `moving`, `focus`, `monocle` focus behaviour.
2. `should_act` / `should_act_individual` three times: move into `komorebi-config` (it already has
`MatchingRule`, `MatchingStrategy`): one test file would replace the rule matching logic of rules,
rules_class, rules_exe, manage_rules, title_rule, startup_rules for all daemons.
3. Monitor reconciliation (Windows `monitor_reconciliator`, macOS own, KDE `reconcile.rs`) is not in
`komorebi-core`: a pure `reconcile(old: &[MonitorSnapshot], new: &[MonitorSnapshot]) -> Plan`
would make `hotplug` and `unknown_monitor` native.
4. `cycle-stack`, `cycle-stack-index`, `focus-stack-window` are daemon methods, not `Container`
methods; moving them into `komorebi-core::Container` would make the stacks scenario native
beyond the existing stack/unstack tests.
5. Event filtering while paused (`allowed_while_paused` is shared, the event path is not).
## Tests added, with the e2e check each mirrors
| Test file | Tests | Mirrors |
|---|---|---|
| `komorebi-layouts/tests/layouts_everywhere.rs` | 8 | layouts, flipping, layout_flip, padding, tiling, closing |
| `crates/komorebi-core/tests/e2e_layouts.rs` | 6 | layouts, flipping, layout_flip, padding, closing, minimizing, floating, stacks, tiling, tiling_off |
| `komorebi-linux/tests/rule_matching.rs` | 13 | rules, rules_class, rules_exe, manage_rules, title_rule |
| `komorebi-linux/tests/command_sweep.rs` | 1 (185 messages) | commands |
| `crates/komorebic-core/tests/e2e_sweep.rs` | 3 | commands |
Each test says what stays e2e: the OS frame equals the tile, borders, event timing, the daemon's own
hooks.