test(checks): pure-contract guards batch (#49)
All checks were successful
Check / eval (push) Successful in 3m6s
All checks were successful
Check / eval (push) Successful in 3m6s
Four new no-VM checks.* + CI py_compile expansion:
- hardware-db-modules: installer DB module names ∈ pinned
nixos-hardware.nixosModules (a lock bump can rename a module and break
profiled installs on just the matching DMI, invisible to any VM).
- installer-compose-lock: offline lock-composer contract on fixtures.
- installer-disko: pure Nix assert — swapSize "0"/"0G" → no @swap, "2G"
→ sized, withLuks wraps root (permanently guards the #46 install fix).
- windowrule-syntax: builds the generated hyprland.conf and fails on the
pre-0.53 grammar / windowrulev2 keyword (guards ed7fd93).
CI py_compile now covers all tracked *.py; docs/TESTING.md §1 synced.
Implemented by a worktree agent; diff reviewed. Verified V0 (flake check)
+ V1 (each checks.x86_64-linux.* built on main, exit 0); agent's negative
tests confirm each guard fails on a regression.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
73
tools/check-windowrule-syntax.py
Executable file
73
tools/check-windowrule-syntax.py
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Hyprland windowrule dead-syntax guard (flake check `windowrule-syntax`).
|
||||
|
||||
Usage: check-windowrule-syntax.py <hyprland.conf>
|
||||
|
||||
Hyprland 0.53 rewrote window rules (hyprlang `handleWindowrule`): the
|
||||
`windowrulev2` keyword is a hard error, and the old rule-first order
|
||||
(`windowrule = float, class:^…$`) no longer parses. Both surface as a red
|
||||
config-error banner on every session start and silently drop the rules
|
||||
(fixed in ed7fd93). The current, correct grammar is
|
||||
`windowrule = <effect> <value>, match:<prop> ^…$` — e.g.
|
||||
`windowrule = float 1, match:class ^…$`.
|
||||
|
||||
This guards the GENERATED hyprland.conf against a regression to the dead
|
||||
syntax, catching three markers:
|
||||
|
||||
1. the `windowrulev2` keyword anywhere;
|
||||
2. rule-first order — an effect keyword directly followed by a comma
|
||||
(`windowrule = float, …` / `center, …` / `size, …`), i.e. no value;
|
||||
3. a bare `<prop>:` matcher (`class:`, `title:`, …) instead of the
|
||||
`match:<prop>` form — this also catches an effect that DOES carry a
|
||||
value but kept the old matcher (`size 60% 65%, class:^…$`).
|
||||
|
||||
The current correct config must PASS; any of the above must FAIL.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
conf = sys.argv[1]
|
||||
lines = open(conf).read().splitlines()
|
||||
|
||||
# Effect keywords that in the dead grammar sat directly before the comma.
|
||||
EFFECTS = (
|
||||
"float", "center", "tile", "pin", "fullscreen", "maximize",
|
||||
"size", "move", "opacity", "workspace", "monitor", "fullscreenstate",
|
||||
"group", "stayfocused", "pseudo", "immediate",
|
||||
)
|
||||
rule_first = re.compile(
|
||||
r"windowrule\s*=\s*(?:" + "|".join(EFFECTS) + r")\s*,"
|
||||
)
|
||||
# A window-property matcher written the old (colon-suffixed) way. The new
|
||||
# grammar spells these `match:class …` — "class" is then followed by a
|
||||
# space, never a colon — so a bare `class:`/`title:` only ever appears in
|
||||
# the dead form.
|
||||
bare_matcher = re.compile(
|
||||
r"\b(?:initialclass|initialtitle|class|title|tag|xwayland|floating)\s*:",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
failures = []
|
||||
for n, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("#"):
|
||||
continue
|
||||
if "windowrulev2" in line:
|
||||
failures.append((n, "dead `windowrulev2` keyword", stripped))
|
||||
continue
|
||||
if not re.match(r"\s*windowrule\s*=", line):
|
||||
continue
|
||||
if rule_first.search(line):
|
||||
failures.append((n, "rule-first order (effect directly before comma)", stripped))
|
||||
if bare_matcher.search(line):
|
||||
failures.append((n, "bare `<prop>:` matcher — expected `match:<prop>`", stripped))
|
||||
|
||||
if failures:
|
||||
print(f"windowrule-syntax: {len(failures)} dead-syntax rule(s) in {conf}")
|
||||
for n, why, text in failures:
|
||||
print(f" line {n}: {why}\n {text}")
|
||||
sys.exit(1)
|
||||
|
||||
rules = sum(1 for l in lines if re.match(r"\s*windowrule\s*=", l))
|
||||
print(f"windowrule-syntax: {rules} windowrule line(s), all post-0.53 grammar")
|
||||
Reference in New Issue
Block a user