test(doctor): V2 float harness for System › Doctor
Some checks failed
Check / eval (push) Has been cancelled
Some checks failed
Check / eval (push) Has been cancelled
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).
This commit is contained in:
@@ -7,10 +7,17 @@ here the moment a debugging session teaches you something a future
|
|||||||
iteration would otherwise rediscover.
|
iteration would otherwise rediscover.
|
||||||
|
|
||||||
## Testing & VM recipes
|
## Testing & VM recipes
|
||||||
|
- **Doctor float V2:** `THEME=<slug> nix build --impure -f tools/doctor-float.nix`
|
||||||
|
— softGL Hyprland, `nomarchy-menu doctor`, asserts
|
||||||
|
`class=com.nomarchy.doctor` + `floating` + centered midpoints +
|
||||||
|
screenshots. Ghostty *does* start here under softGL (classed one-shot);
|
||||||
|
size often stays Ghostty’s default 800×600 (client geometry wins over
|
||||||
|
the percent `size` windowrule).
|
||||||
- **theme-shot softGL cannot start Ghostty** — `btop.png` is best-effort
|
- **theme-shot softGL cannot start Ghostty** — `btop.png` is best-effort
|
||||||
(usually identical to desktop). Guest asserts on
|
(usually identical to desktop). Guest asserts on
|
||||||
`~/.config/btop/themes/nomarchy.theme` prove baking; the TUI look is
|
`~/.config/btop/themes/nomarchy.theme` prove baking; the TUI look is
|
||||||
hardware/GL tier (HARDWARE-QUEUE).
|
hardware/GL tier (HARDWARE-QUEUE). Doctor float is the counter-example
|
||||||
|
that a *classed* one-shot Ghostty can still map under softGL.
|
||||||
- **Waybar `custom/doctor` tripwire:** status helper must invoke
|
- **Waybar `custom/doctor` tripwire:** status helper must invoke
|
||||||
`nomarchy-doctor` by **absolute store path** (waybar's env can miss
|
`nomarchy-doctor` by **absolute store path** (waybar's env can miss
|
||||||
system PATH → `command -v … || exit 0` self-hides forever); empty
|
system PATH → `command -v … || exit 0` self-hides forever); empty
|
||||||
|
|||||||
144
tools/doctor-float.nix
Normal file
144
tools/doctor-float.nix
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# 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"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user