From 6748528b9052eec1850a76658c7a16e78e337ff8 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Fri, 30 Oct 2026 12:12:04 -0700 Subject: [PATCH] feat(nix): the manage_rules and state_fidelity scenarios --- .../core/scenarios/manage_rules.py | 73 +++++++++++++++++++ .../core/scenarios/state_fidelity.py | 48 ++++++++++++ nix/tests/vm/lib/komorebi_vm/kde_platform.py | 7 +- nix/tests/vm/lib/tests/fake_platform.py | 20 ++++- nix/tests/vm/lib/tests/test_focus_moving.py | 41 +++++++++++ 5 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 nix/tests/vm/lib/komorebi_vm/core/scenarios/manage_rules.py create mode 100644 nix/tests/vm/lib/komorebi_vm/core/scenarios/state_fidelity.py diff --git a/nix/tests/vm/lib/komorebi_vm/core/scenarios/manage_rules.py b/nix/tests/vm/lib/komorebi_vm/core/scenarios/manage_rules.py new file mode 100644 index 00000000..7bcff874 --- /dev/null +++ b/nix/tests/vm/lib/komorebi_vm/core/scenarios/manage_rules.py @@ -0,0 +1,73 @@ +"""A manage rule has a window managed that an ignore rule leaves alone, by title, class and exe.""" + +from typing import Any + +from komorebi_vm.core.checks import Checks +from komorebi_vm.core.platform import Client, Platform + +# how long a window has to stay unmanaged for it to be left alone +QUIET = 3 + + +def _kinds(platform: Platform) -> list[tuple[str, str, dict[str, Any]]]: + """For each kind of rule: its name, the id of the rule, and what the window is opened with.""" + return [ + ("title", "e2e-manage-title", {}), + ("class", "e2e-manage-class", {"app_id": "e2e-manage-class"}), + ("exe", platform.exe_id("e2e-manage-exe"), {"exe": "e2e-manage-exe"}), + ] + + +def run(platform: Platform, checks: Checks) -> None: + """For each kind of rule, an ignore rule leaves a window alone and a manage rule for the same + application then has the next one managed and tiled. + + A window the daemon leaves alone does not make room for itself: the window that is tiled keeps + its frame. The daemon must be running, with no window, and the rules are for names no other + scenario uses, because `ignore-rule` drops a rule whose id an earlier rule has. + """ + base = platform.spawn("manage-base") + platform.wait_for(lambda: platform.state().workspace_of(base.title) == 0, timeout=20) + for kind, rule_id, how in _kinds(platform): + title = rule_id if kind == "title" else f"manage-{kind}" + platform.komorebic("ignore-rule", kind, rule_id) + before = platform.os_window(base) + ignored = platform.spawn(f"{title} ignored", **how) + checks.check( + f"a window an ignore rule by {kind} matches is not managed", + not platform.wait_for( + lambda c=ignored: platform.state().workspace_of(c.title) is not None, + timeout=QUIET, + ), + ) + after = platform.os_window(base) + checks.check( + f"... and the window that is tiled keeps its place ({kind})", + before is not None and after is not None and before.frame == after.frame, + ) + platform.close(ignored) + platform.wait_for(lambda c=ignored: platform.os_window(c) is None, timeout=20) + + platform.komorebic("manage-rule", kind, rule_id) + managed: Client = platform.spawn(f"{title} managed", **how) + checks.check( + f"with a manage rule by {kind} too, a window it matches is managed", + platform.wait_for( + lambda c=managed: platform.state().workspace_of(c.title) == 0, timeout=20 + ), + ) + checks.check( + f"... and tiled: {base.title} gives it room ({kind})", + platform.wait_for( + lambda b=before: ( + (a := platform.os_window(base)) is not None + and b is not None + and a.frame != b.frame + ), + timeout=20, + ), + ) + platform.close(managed) + platform.wait_for(lambda c=managed: platform.state().workspace_of(c.title) is None) + platform.close(base) + platform.wait_for(lambda: platform.state().managed() == 0, timeout=20) diff --git a/nix/tests/vm/lib/komorebi_vm/core/scenarios/state_fidelity.py b/nix/tests/vm/lib/komorebi_vm/core/scenarios/state_fidelity.py new file mode 100644 index 00000000..7c8be08d --- /dev/null +++ b/nix/tests/vm/lib/komorebi_vm/core/scenarios/state_fidelity.py @@ -0,0 +1,48 @@ +"""The daemon's state names each window as it was opened: its title, its program and its class.""" + +from komorebi_vm.core.checks import Checks +from komorebi_vm.core.platform import Platform + + +def _window(platform: Platform, title: str): # noqa: ANN202 + for workspace in platform.state().workspace().windows(): + if workspace.title == title: + return workspace + return None + + +def run(platform: Platform, checks: Checks) -> None: + """Opens a window with only a title, one with a class, and one made by a program of its own, + and checks what the state says of each: the title as given, the class that was set, and the + program (through `exe_id`, which is its file name on the platform). + + The rules scenarios check that a rule finds a window by these; this checks that what the state + shows of a window is what the window was given. The daemon must be running, with no window. + """ + plain = platform.spawn("fidelity plain") + classed = platform.spawn("fidelity class", app_id="e2e-fidelity-class") + made = platform.spawn("fidelity exe", exe="e2e-fidelity-exe") + platform.wait_for(lambda: platform.state().managed() == 3, timeout=30) + + found = _window(platform, plain.title) + checks.check("the state has the title of a window as it was opened", found is not None) + checks.check( + "... and says what program opened it", + found is not None and found.exe != "", + ) + found = _window(platform, classed.title) + checks.check( + "the state has the class a window was given", + found is not None and found.window_class == "e2e-fidelity-class", + ) + found = _window(platform, made.title) + checks.check( + "the state has the program that opened a window, as its file name", + found is not None and found.exe == platform.exe_id("e2e-fidelity-exe"), + ) + for client in (plain, classed, made): + platform.close(client) + checks.check( + "the three windows are closed", + platform.wait_for(lambda: platform.state().managed() == 0, timeout=20), + ) diff --git a/nix/tests/vm/lib/komorebi_vm/kde_platform.py b/nix/tests/vm/lib/komorebi_vm/kde_platform.py index c55f6595..868aa1c6 100644 --- a/nix/tests/vm/lib/komorebi_vm/kde_platform.py +++ b/nix/tests/vm/lib/komorebi_vm/kde_platform.py @@ -93,9 +93,12 @@ class KdePlatform: program = "foot" if exe: # the daemon reads the executable from /proc, where a link shows its target: a copy - # of the terminal is what has the name + # of the terminal is what has the name, made once: a copy that is running cannot be + # written over program = f"{BIN}/{exe}" - self.kde.sh(f"mkdir -p {BIN} && cp -L $(command -v foot) {program}") + self.kde.sh( + f"mkdir -p {BIN} && {{ test -x {program} || cp -L $(command -v foot) {program}; }}" + ) self.kde.spawn(f"{program} {name}-T {shlex.quote(title)} sleep 3000") self.kde.wait_for(lambda: self.kde.wid(title) != "", timeout=30) return Client(title) diff --git a/nix/tests/vm/lib/tests/fake_platform.py b/nix/tests/vm/lib/tests/fake_platform.py index 4a9f52ba..98e26c5f 100644 --- a/nix/tests/vm/lib/tests/fake_platform.py +++ b/nix/tests/vm/lib/tests/fake_platform.py @@ -87,6 +87,8 @@ class FakePlatform: self.exe_rules: dict[str, int] = {} self.ignored_exes: set[str] = set() self.exes: dict[str, str] = {} + self.classes: dict[str, str] = {} + self.manage: set[tuple[str, str]] = set() self.names: list[str | None] = [None, None, None] self.layout_name = "Columns" self.reloads = reloads @@ -171,7 +173,17 @@ class FakePlatform: return [stack[0] for stack in self.containers] def spawn(self, title: str, app_id: str | None = None, exe: str | None = None) -> Client: - self.exes[title] = exe or "" + self.exes[title] = exe or "foot" + self.classes[title] = app_id or "" + forced = ( + ("exe", exe) in self.manage + or ("class", app_id) in self.manage + or any(kind == "title" and title.startswith(i) for kind, i in self.manage) + ) + if forced: + self.containers.append([title]) + self._relayout() + return Client(title) if exe in self.ignored_exes: self.unmanaged.append(title) return Client(title) @@ -187,7 +199,7 @@ class FakePlatform: if self.paused: self.unmanaged.append(title) return Client(title) - if title in self.ignored: + if any(title.startswith(i) for i in self.ignored): self.unmanaged.append(title) elif title in self.initial_rules: self.spaces[self.initial_rules.pop(title)].append([title]) @@ -451,6 +463,8 @@ class FakePlatform: self.ignored_classes.add(name) case ("ignore-rule", "title", title): self.ignored.add(title) + case ("manage-rule", kind, name): + self.manage.add((kind, name)) case ("move-to-workspace", number): self.komorebic("send-to-workspace", number) self.last, self.current = self.current, int(number) @@ -528,7 +542,7 @@ class FakePlatform: "details": { "title": title, "exe": self.exes.get(title, ""), - "class": "", + "class": self.classes.get(title, ""), "path": "", }, } diff --git a/nix/tests/vm/lib/tests/test_focus_moving.py b/nix/tests/vm/lib/tests/test_focus_moving.py index ccb48b4f..f14fc691 100644 --- a/nix/tests/vm/lib/tests/test_focus_moving.py +++ b/nix/tests/vm/lib/tests/test_focus_moving.py @@ -16,6 +16,7 @@ from komorebi_vm.core.scenarios import ( lifecycle, lifecycle_hidden, locking, + manage_rules, maximizing, minimizing, monocle, @@ -32,6 +33,7 @@ from komorebi_vm.core.scenarios import ( scrolling, stacks, startup_rules, + state_fidelity, tiling, tiling_off, workspace_moves, @@ -507,6 +509,45 @@ def test_a_daemon_that_does_not_tile_again_on_unlock_fails_locking() -> None: assert checks.failed > 0 +def test_a_daemon_that_is_right_passes_manage_rules() -> None: + platform = FakePlatform() + checks = Checks() + manage_rules.run(platform, checks) + assert checks.failed == 0, checks.failures + assert checks.passed > 8 + + +def test_a_daemon_that_ignores_manage_rules_fails_manage_rules() -> None: + class Deaf(FakePlatform): + def komorebic(self, *args: str) -> str: + return "" if args[0] == "manage-rule" else super().komorebic(*args) + + platform = Deaf() + checks = Checks() + manage_rules.run(platform, checks) + assert checks.failed > 0 + + +def test_a_daemon_that_shows_what_a_window_was_given_passes_state_fidelity() -> None: + platform = FakePlatform() + checks = Checks() + state_fidelity.run(platform, checks) + assert checks.failed == 0, checks.failures + assert checks.passed > 4 + + +def test_a_daemon_that_loses_the_class_fails_state_fidelity() -> None: + class Forgetful(FakePlatform): + def spawn(self, title, app_id=None, exe=None): # noqa: ANN001, ANN202 + client = super().spawn(title, app_id=None, exe=exe) + return client + + platform = Forgetful() + checks = Checks() + state_fidelity.run(platform, checks) + assert checks.failed > 0 + + def test_a_command_the_port_lacks_is_skipped_by_name() -> None: platform = FakePlatform() checks = Checks()