Files
Nomarchy/tools/doctor-float.nix
Bernardo Magri d8e1a13d50
Some checks failed
Check / eval (push) Has been cancelled
refactor(#107): theme-state.json → state.json, theme-sync → state-sync
The machine flake's git-tracked settings file is system state, not
"theme" only — rename it to state.json. CLI becomes nomarchy-state-sync
with a nomarchy-theme-sync symlink for scripts and muscle memory.

Eval (mkFlake, doctor, lifecycle) still accepts theme-state.json; the
next write migrates to state.json and removes the legacy file.
Documented in MIGRATION.md; drop the CLI alias after release notes.
2026-07-15 11:26:59 +01:00

143 lines
5.4 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 → kitty --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). Kitty 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/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 Kitty 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"(Kitty 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. Size rule is best-effort if the
# client geometry wins; assert center from the actual size.
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"
)
'';
}