Files
Nomarchy/tools/doctor-float.nix
Bernardo Magri 073adf743d
Some checks failed
Check / eval (push) Has been cancelled
test(doctor): V2 float harness for System › Doctor
Headless softGL Hyprland harness (tools/doctor-float.nix): launches
nomarchy-menu doctor, asserts class/floating/center via hyprctl, dumps
before/after screenshots. Ran boreal + summer-night — both PASS
(floating centered sheet; Ghostty keeps ~800×600). MEMORY recipe.

Verified: V2 — THEME=boreal and THEME=summer-night doctor-float green;
screenshots viewed (desktop-before vs doctor-float under both themes).
2026-07-11 10:16:34 +01:00

145 lines
5.5 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# V2 harness for the floating Doctor terminal (System Doctor /
# nomarchy-menu doctor → ghostty --class=com.nomarchy.doctor).
#
# Boots headless softGL Hyprland (same stack as theme-shot), launches the
# real menu path, then asserts via hyprctl that the window is floating
# and sized (rules in modules/home/hyprland.nix). Screenshots: desktop
# (before) + doctor (after).
#
# nix build --impure -f tools/doctor-float.nix --no-link --print-out-paths
#
# Maintainer harness — not a checks.* gate (full desktop VM). Ghostty
# under softGL is best-effort (MEMORY); if the client never appears the
# scripted float asserts fail loudly rather than claiming success.
let
flake = builtins.getFlake ("git+file://" + toString ../.);
inherit (flake.inputs) nixpkgs home-manager;
pkgs = import nixpkgs {
system = "x86_64-linux";
overlays = [ flake.overlays.default ];
config.allowUnfree = true;
};
slug = let s = builtins.getEnv "THEME"; in if s == "" then "boreal" else s;
in
pkgs.testers.runNixOSTest {
name = "doctor-float-${slug}";
node.pkgsReadOnly = false;
node.specialArgs = { username = "nomarchy"; };
nodes.machine = { pkgs, lib, ... }: {
imports = [ flake.nixosModules.nomarchy home-manager.nixosModules.home-manager ];
virtualisation.memorySize = 4096;
virtualisation.cores = 4;
hardware.graphics.enable = true;
environment.variables.LIBGL_ALWAYS_SOFTWARE = "1";
boot.initrd.availableKernelModules = [ "virtio_gpu" ];
virtualisation.qemu.options = [ "-vga none" "-device virtio-gpu-pci,xres=1920,yres=1080" ];
users.users.nomarchy = {
isNormalUser = true;
password = "test";
extraGroups = [ "wheel" "video" ];
};
nomarchy.system.stateFile = flake + "/themes/${slug}.json";
systemd.tmpfiles.rules = [
"d /home/nomarchy/.nomarchy 0755 nomarchy users -"
"C /home/nomarchy/.nomarchy/theme-state.json 0644 nomarchy users - ${flake + "/themes/${slug}.json"}"
];
services.greetd.settings.initial_session = {
command = "start-hyprland";
user = "nomarchy";
};
home-manager.useGlobalPkgs = true;
home-manager.users.nomarchy = {
imports = [ flake.homeModules.nomarchy ];
nomarchy.stateFile = flake + "/themes/${slug}.json";
nomarchy.idle.enable = false;
};
};
testScript = ''
import json
machine.wait_for_unit("multi-user.target")
machine.wait_for_file("/run/user/1000/hypr", 180)
hy_env = "XDG_RUNTIME_DIR=/run/user/1000 HYPRLAND_INSTANCE_SIGNATURE=$(ls /run/user/1000/hypr | head -1)"
hy = f"su - nomarchy -c '{hy_env} hyprctl"
# Wallpaper + bar settle.
machine.sleep(25)
# Confirm rules are loaded (config path, not just eval).
conf = machine.succeed(
"su - nomarchy -c 'grep -n doctor /home/nomarchy/.config/hypr/hyprland.conf "
"|| grep -n doctor ~/.config/hypr/hyprland.conf || true'"
)
assert "com.nomarchy.doctor" in conf or r"com\.nomarchy\.doctor" in conf, (
f"doctor windowrules missing from live hyprland.conf:\n{conf}"
)
# Before shot tiled desktop only.
machine.execute(f"{hy} dispatch movecursor 1912 1075' >/dev/null 2>&1")
machine.sleep(1)
machine.screenshot("desktop-before")
# Real product path: Waybar / System Doctor both call this.
machine.execute(f"{hy} dispatch exec nomarchy-menu doctor' >/dev/null 2>&1")
# Wait for the classed Ghostty client (softGL may be slow).
def clients():
raw = machine.succeed(f"{hy} clients -j'").strip()
try:
return json.loads(raw) if raw else []
except Exception as e:
raise AssertionError(f"hyprctl clients -j not JSON: {e}\n{raw!r}") from e
doctor = None
for _ in range(40):
machine.sleep(1)
for c in clients():
cls = c.get("class") or c.get("initialClass") or ""
if cls == "com.nomarchy.doctor":
doctor = c
break
if doctor is not None:
break
dump = machine.succeed(f"{hy} clients -j'")
assert doctor is not None, (
"no client with class com.nomarchy.doctor after nomarchy-menu doctor "
f"(Ghostty under softGL may have failed clients dump):\n{dump}"
)
assert doctor.get("floating") is True, (
f"doctor window is not floating: {json.dumps(doctor, indent=2)}"
)
# Product contract: float + center. Ghostty often keeps its default
# 800×600 even when a size rule is present (client geometry wins);
# the size rule remains as a best-effort hint. Assert center from
# the actual size so a smaller Ghostty window still counts.
at = doctor.get("at") or [0, 0]
size = doctor.get("size") or [0, 0]
w, h = int(size[0]), int(size[1])
x, y = int(at[0]), int(at[1])
assert w >= 400 and h >= 300, f"doctor window too small: {doctor}"
mx, my = x + w / 2, y + h / 2
assert abs(mx - 960) < 250, f"doctor not horizontally centered (mid={mx}): {doctor}"
assert abs(my - 540) < 250, f"doctor not vertically centered (mid={my}): {doctor}"
machine.screenshot("doctor-float")
# Leave evidence in the guest for copy-out debugging.
machine.succeed(
"cat > /tmp/doctor-float-assert.txt <<'EOF'\n"
+ json.dumps(
{
"class": doctor.get("class") or doctor.get("initialClass"),
"floating": doctor.get("floating"),
"at": doctor.get("at"),
"size": doctor.get("size"),
"title": doctor.get("title"),
},
indent=2,
)
+ "\nEOF"
)
'';
}