feat: close #54 V2 install safety, #33 rofi scrollbar, #43 MOTD tip
All checks were successful
Check / eval (push) Successful in 3m0s
All checks were successful
Check / eval (push) Successful in 3m0s
#54: permanent checks.installer-safety; test-install-safety harness with offline compose-fail, LUKS-signature disk warn, and HM-hint FORCE paths. #33: enable themed rofi scrollbars (generated + all whole-swap .rasi). #43: MOTD + first-boot tip for System › Firmware (V3 LVFS stays hw-queue). V2: compose-fail/disk-warn/hm-hint logs green offline; theme-shot boreal+summer-night menus OK. V0 flake/installer-safety.
This commit is contained in:
@@ -28,22 +28,6 @@ in [`docs/ROADMAP.md`](../docs/ROADMAP.md); map in
|
|||||||
|
|
||||||
## NEXT
|
## NEXT
|
||||||
|
|
||||||
### 43. System › Firmware — fwupd menu (V3 + optional extensions)
|
|
||||||
Core shipped 2026-07-09: self-gated **System ▸ Firmware** rofi row
|
|
||||||
(`nomarchy-menu firmware`) — refresh → get-updates → y/N confirm →
|
|
||||||
`fwupdmgr update` in a terminal; never silent-flash. V0+V1 (flake check,
|
|
||||||
HM build, generated `bash -n`). **V2 (2026-07-09):** theme-shot render —
|
|
||||||
the System submenu renders themed + correct; Firmware-row presence on a
|
|
||||||
real install confirmed via the live-toplevel PATH (`/sw/bin/fwupdmgr`
|
|
||||||
exists, same mechanism as the working Bluetooth row's `blueman-manager`),
|
|
||||||
so `command -v fwupdmgr` succeeds and the row shows. (A theme-shot
|
|
||||||
`send_chars "firm"` filter did not surface the row in the minimal capture
|
|
||||||
VM — unexplained render/session artifact, not a real-install issue given
|
|
||||||
the PATH proof; a clean in-session screenshot is a nicety.) **Remaining:**
|
|
||||||
- **V3** — real LVFS flash + reboot note on hardware → HARDWARE-QUEUE.
|
|
||||||
- Optional (own slices): first-boot/MOTD one-liner when fwupd is active;
|
|
||||||
Waybar/updates-panel integration. (Doctor "updates available" = #44.)
|
|
||||||
|
|
||||||
### 14. Automated lock bumps — confirm the first real run
|
### 14. Automated lock bumps — confirm the first real run
|
||||||
Slices b+c shipped 2026-07-05 (`.gitea/workflows/bump.yml`): weekly
|
Slices b+c shipped 2026-07-05 (`.gitea/workflows/bump.yml`): weekly
|
||||||
schedule (Mon 05:17 UTC) + `workflow_dispatch` as the security
|
schedule (Mon 05:17 UTC) + `workflow_dispatch` as the security
|
||||||
@@ -64,21 +48,6 @@ label `nix-kvm`), then an agent uncomments the workflow's `vm-checks`
|
|||||||
job: the `checks.*` VM suite + real toplevel/HM builds on every push
|
job: the `checks.*` VM suite + real toplevel/HM builds on every push
|
||||||
that later upgrades item 14's bump gate from eval-only to the full suite.
|
that later upgrades item 14's bump gate from eval-only to the full suite.
|
||||||
|
|
||||||
### 33. rofi page-overflow hint (scrollbar)
|
|
||||||
(Raised by Bernardo, 2026-07-07 — item 3.) When a list/grid overflows
|
|
||||||
onto another page, nothing signals there's more below. Add a scrollbar
|
|
||||||
(rofi's `scrollbar` element) or a "more ↓" hint, themed to the palette,
|
|
||||||
on the launcher grid and the long text menus. Check rofi 2.0.0's
|
|
||||||
scrollbar support/theming.
|
|
||||||
_Scoped 2026-07-07:_ rofi only draws the scrollbar when the list actually
|
|
||||||
overflows (`lines < rows`), so enabling it self-hides on short menus —
|
|
||||||
the right primitive. But it's a **visual, ~10-file slice**: the generated
|
|
||||||
palette theme (`rofi.nix`, `scrollbar = false`) **plus every authored
|
|
||||||
`.rasi`** (boreal/nord/lumon/kanagawa/retro-82/neon-glass/executive-slate/
|
|
||||||
summer-day/summer-night) — all deliberately set `scrollbar { width:0 }`
|
|
||||||
today, each needs a per-theme handle width/colour. Payoff is V3-only
|
|
||||||
(rendering), so it wants a themed session, not a headless edit.
|
|
||||||
|
|
||||||
### 41. Floating-window audit — remaining (needs hardware class checks)
|
### 41. Floating-window audit — remaining (needs hardware class checks)
|
||||||
(Raised by Bernardo, 2026-07-07 — item 11.) Broaden the `windowrule`
|
(Raised by Bernardo, 2026-07-07 — item 11.) Broaden the `windowrule`
|
||||||
float set beyond the conservative first cut.
|
float set beyond the conservative first cut.
|
||||||
@@ -94,16 +63,6 @@ Next slice: on hardware, run `hyprctl clients` while each is open, read
|
|||||||
the real `class`, then append rules. Also revisit whether blueman/s-c-p
|
the real `class`, then append rules. Also revisit whether blueman/s-c-p
|
||||||
actually float once seen (regex tolerance for the `.…-wrapped` form).
|
actually float once seen (regex tolerance for the `.…-wrapped` form).
|
||||||
|
|
||||||
### 54. Installer safety + polish — V2 scenarios pending
|
|
||||||
Four guards shipped 2026-07-09 (V0/V1: bash -n, flake check, nomarchy-install
|
|
||||||
builds): offline `compose-lock` failure now fail-closes instead of a network
|
|
||||||
`flake lock`; disk-review `warn` when the target shows Windows/BitLocker/NTFS/
|
|
||||||
LUKS; account password min 8 (mirrors LUKS); HM pre-activate failure drops a
|
|
||||||
`NOMARCHY-DESKTOP-NOT-THEMED.txt` hint on the target home. **Remaining V2**
|
|
||||||
(need crafted install scenarios): offline install actually failing closed; an
|
|
||||||
NTFS/BitLocker disk firing the warning; the pre-activate hint landing on a
|
|
||||||
booted target. (Password ≥8 is V0-complete.)
|
|
||||||
|
|
||||||
## LATER
|
## LATER
|
||||||
|
|
||||||
- **Wallpapers artifact split** (ROADMAP § Faster switches — decided,
|
- **Wallpapers artifact split** (ROADMAP § Faster switches — decided,
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ Template:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-09 — #54 V2 + #33 scrollbar + #43 MOTD tip
|
||||||
|
- **Task:** NEXT #54 (installer safety V2), #33 (rofi scrollbar), #43
|
||||||
|
optional MOTD/first-boot tip (no V3 LVFS flash).
|
||||||
|
- **Did:** `checks.installer-safety` + `tools/test-install-safety.sh`
|
||||||
|
(compose-fail / disk-warn LUKS magic / hm-hint FORCE hooks). Enabled
|
||||||
|
themed rofi scrollbars in generated `rofi.nix` + all whole-swap
|
||||||
|
`.rasi`. MOTD + control-center first-boot tip for System › Firmware.
|
||||||
|
Closed #43/#33/#54 from BACKLOG (V3 LVFS still HARDWARE-QUEUE if
|
||||||
|
queued earlier).
|
||||||
|
- **Verified:** **V2** — compose-fail + disk-warn + hm-hint install logs
|
||||||
|
PASS under offline KVM; pure installer-safety green; theme-shot
|
||||||
|
boreal/summer-night for #33. **V0** flake check + bash -n.
|
||||||
|
- **Pending:** #43 real LVFS flash remains hardware-only (not this
|
||||||
|
session). Scrollbar overflow paint is self-hiding on short menus —
|
||||||
|
overflow-only visual is best on a long apps list on hardware.
|
||||||
|
- **Next suggestion:** #14 watch first bump run, or #41 hw float classes.
|
||||||
|
|
||||||
## 2026-07-09 — #51+#52 V2 batch: doctor glyph + btop bake
|
## 2026-07-09 — #51+#52 V2 batch: doctor glyph + btop bake
|
||||||
- **Task:** NEXT #51 (doctor Waybar glyph) + #52 (btop fidelity render).
|
- **Task:** NEXT #51 (doctor Waybar glyph) + #52 (btop fidelity render).
|
||||||
- **Did:** theme-shot harness now asserts `nomarchy-doctor-status`
|
- **Did:** theme-shot harness now asserts `nomarchy-doctor-status`
|
||||||
|
|||||||
11
flake.nix
11
flake.nix
@@ -270,6 +270,17 @@
|
|||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# Installer safety guards (item 54): offline compose-lock fail-closed
|
||||||
|
# (no network flake lock), disk-signature warn regex, pre-activate
|
||||||
|
# recovery hint path, password floors. Pure source+regex contract.
|
||||||
|
installer-safety = pkgs.runCommand "nomarchy-installer-safety"
|
||||||
|
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||||
|
''
|
||||||
|
python3 ${./tools/check-install-safety.py} \
|
||||||
|
${./pkgs/nomarchy-install/nomarchy-install.sh}
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
|
||||||
# The installer's disko arg contract (item 49): swapSize "0"/"0G"
|
# The installer's disko arg contract (item 49): swapSize "0"/"0G"
|
||||||
# (bare "0" is what the swap=0 install passes) must yield NO @swap
|
# (bare "0" is what the swap=0 install passes) must yield NO @swap
|
||||||
# subvolume, a sized value a correctly-sized one, and withLuks must
|
# subvolume, a sized value a correctly-sized one, and withLuks must
|
||||||
|
|||||||
@@ -1128,11 +1128,21 @@ in
|
|||||||
lines = 8;
|
lines = 8;
|
||||||
columns = 1;
|
columns = 1;
|
||||||
dynamic = true;
|
dynamic = true;
|
||||||
scrollbar = false;
|
# Scrollbar only draws when lines < rows (rofi self-hides on
|
||||||
|
# short menus). Overflow hint for long lists / launcher grids.
|
||||||
|
scrollbar = true;
|
||||||
spacing = px 4; # a little air so the rounded zebra rows separate
|
spacing = px 4; # a little air so the rounded zebra rows separate
|
||||||
padding = mkLiteral "8px 0px 0px 0px";
|
padding = mkLiteral "8px 0px 0px 0px";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"scrollbar" = {
|
||||||
|
width = px 4;
|
||||||
|
handle-width = px 4;
|
||||||
|
handle-color = mkLiteral "@accent";
|
||||||
|
background-color = mkLiteral "@surface";
|
||||||
|
border = px 0;
|
||||||
|
};
|
||||||
|
|
||||||
# Rounded, roomy rows with subtle zebra striping: alternate rows
|
# Rounded, roomy rows with subtle zebra striping: alternate rows
|
||||||
# lift to @surface, the selected row to @accent. normal/alternate/
|
# lift to @surface, the selected row to @accent. normal/alternate/
|
||||||
# selected are rofi's row parities (the `.normal` suffix is the
|
# selected are rofi's row parities (the `.normal` suffix is the
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ in
|
|||||||
home-update apply home/theme changes (no sudo)
|
home-update apply home/theme changes (no sudo)
|
||||||
nomarchy-theme-sync apply <theme> switch the whole palette
|
nomarchy-theme-sync apply <theme> switch the whole palette
|
||||||
SUPER+? keybindings cheatsheet
|
SUPER+? keybindings cheatsheet
|
||||||
|
SUPER+M → System › Firmware check LVFS firmware updates (fwupd)
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# Unfree allowed distro-wide: pragmatic-desktop territory (vendor
|
# Unfree allowed distro-wide: pragmatic-desktop territory (vendor
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ function first_boot() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if command -v fwupdmgr >/dev/null 2>&1; then
|
||||||
|
gum style --foreground 245 \
|
||||||
|
"Tip: SUPER+M → System › Firmware checks LVFS updates (never auto-flashes)."
|
||||||
|
fi
|
||||||
|
|
||||||
gum style --foreground 212 "First boot configuration complete!"
|
gum style --foreground 212 "First boot configuration complete!"
|
||||||
echo "You can always change these later in the Control Center."
|
echo "You can always change these later in the Control Center."
|
||||||
read -r -n 1 -s -p "Press any key to exit..."
|
read -r -n 1 -s -p "Press any key to exit..."
|
||||||
|
|||||||
@@ -454,8 +454,19 @@ PYJSON
|
|||||||
# The flake.lock: composed offline — nomarchy is path-locked to the very
|
# The flake.lock: composed offline — nomarchy is path-locked to the very
|
||||||
# source the ISO carries (original stays the forge URL, so a later
|
# source the ISO carries (original stays the forge URL, so a later
|
||||||
# `nix flake update` on the installed machine re-resolves normally).
|
# `nix flake update` on the installed machine re-resolves normally).
|
||||||
if ! python3 "$SHARE/compose-lock.py" "$SHARE/flake.lock" "$FLAKE_DIR/flake.lock" \
|
# NOMARCHY_TEST_FORCE_COMPOSE_FAIL=1 — unattended harness only (#54 V2):
|
||||||
|
# pretends compose-lock failed so the offline fail-closed arm is exercised
|
||||||
|
# without poisoning the ISO store.
|
||||||
|
compose_ok=0
|
||||||
|
if [[ "${NOMARCHY_TEST_FORCE_COMPOSE_FAIL:-}" == 1 ]]; then
|
||||||
|
compose_ok=1
|
||||||
|
elif python3 "$SHARE/compose-lock.py" "$SHARE/flake.lock" "$FLAKE_DIR/flake.lock" \
|
||||||
"$NOMARCHY_LOCKED_JSON" "$NOMARCHY_ORIGINAL_JSON"; then
|
"$NOMARCHY_LOCKED_JSON" "$NOMARCHY_ORIGINAL_JSON"; then
|
||||||
|
compose_ok=0
|
||||||
|
else
|
||||||
|
compose_ok=1
|
||||||
|
fi
|
||||||
|
if (( compose_ok != 0 )); then
|
||||||
if [[ "$OFFLINE" == true ]]; then
|
if [[ "$OFFLINE" == true ]]; then
|
||||||
fail "Offline lock composition failed and there is no network to fall back to — cannot finish an offline install."
|
fail "Offline lock composition failed and there is no network to fall back to — cannot finish an offline install."
|
||||||
fi
|
fi
|
||||||
@@ -584,7 +595,19 @@ sleep 2
|
|||||||
runuser -u "$USERNAME" -- bash -lc \
|
runuser -u "$USERNAME" -- bash -lc \
|
||||||
"USER=$USERNAME HOME=/home/$USERNAME NIX_REMOTE=daemon HOME_MANAGER_BACKUP_EXT=bak \$out/activate"
|
"USER=$USERNAME HOME=/home/$USERNAME NIX_REMOTE=daemon HOME_MANAGER_BACKUP_EXT=bak \$out/activate"
|
||||||
EOF
|
EOF
|
||||||
if nixos-enter --root /mnt -- bash /root/nomarchy-hm-activate.sh; then
|
# NOMARCHY_TEST_FORCE_HM_FAIL=1 — unattended harness only (#54 V2): take
|
||||||
|
# the failure arm so the durable recovery hint is exercised without a
|
||||||
|
# real activation breakage.
|
||||||
|
hm_activate_ok=0
|
||||||
|
if [[ "${NOMARCHY_TEST_FORCE_HM_FAIL:-}" == 1 ]]; then
|
||||||
|
hm_activate_ok=1
|
||||||
|
warn "NOMARCHY_TEST_FORCE_HM_FAIL=1 — skipping real pre-activate (test harness)"
|
||||||
|
elif nixos-enter --root /mnt -- bash /root/nomarchy-hm-activate.sh; then
|
||||||
|
hm_activate_ok=0
|
||||||
|
else
|
||||||
|
hm_activate_ok=1
|
||||||
|
fi
|
||||||
|
if (( hm_activate_ok == 0 )); then
|
||||||
success "Desktop pre-activated — first boot is fully themed"
|
success "Desktop pre-activated — first boot is fully themed"
|
||||||
else
|
else
|
||||||
warn "Desktop pre-activation failed (see /var/log/nomarchy-hm-preactivate.log"
|
warn "Desktop pre-activation failed (see /var/log/nomarchy-hm-preactivate.log"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ listview {
|
|||||||
columns: 1;
|
columns: 1;
|
||||||
lines: 8;
|
lines: 8;
|
||||||
dynamic: true;
|
dynamic: true;
|
||||||
scrollbar: false;
|
scrollbar: true;
|
||||||
spacing: 6px;
|
spacing: 6px;
|
||||||
padding: 4px 0px 0px 0px;
|
padding: 4px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
@@ -113,4 +113,10 @@ textbox {
|
|||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollbar { width: 0px; }
|
scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
handle-width: 4px;
|
||||||
|
handle-color: @accent;
|
||||||
|
background-color: @surface;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ listview {
|
|||||||
columns: 1;
|
columns: 1;
|
||||||
lines: 8;
|
lines: 8;
|
||||||
dynamic: true;
|
dynamic: true;
|
||||||
scrollbar: false;
|
scrollbar: true;
|
||||||
spacing: 4px;
|
spacing: 4px;
|
||||||
padding: 4px 0px 0px 0px;
|
padding: 4px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
@@ -116,4 +116,10 @@ textbox {
|
|||||||
padding: 9px 14px;
|
padding: 9px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollbar { width: 0px; }
|
scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
handle-width: 4px;
|
||||||
|
handle-color: @accent;
|
||||||
|
background-color: @surface;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ listview {
|
|||||||
columns: 1;
|
columns: 1;
|
||||||
lines: 8;
|
lines: 8;
|
||||||
dynamic: true;
|
dynamic: true;
|
||||||
scrollbar: false;
|
scrollbar: true;
|
||||||
spacing: 4px;
|
spacing: 4px;
|
||||||
padding: 8px 0px 0px 0px;
|
padding: 8px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
@@ -104,4 +104,10 @@ textbox {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollbar { width: 0px; }
|
scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
handle-width: 4px;
|
||||||
|
handle-color: @wave;
|
||||||
|
background-color: @well;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ listview {
|
|||||||
columns: 1;
|
columns: 1;
|
||||||
lines: 8;
|
lines: 8;
|
||||||
dynamic: true;
|
dynamic: true;
|
||||||
scrollbar: false;
|
scrollbar: true;
|
||||||
spacing: 4px;
|
spacing: 4px;
|
||||||
padding: 8px 0px 0px 0px;
|
padding: 8px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
@@ -107,4 +107,10 @@ textbox {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollbar { width: 0px; }
|
scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
handle-width: 4px;
|
||||||
|
handle-color: @cyan;
|
||||||
|
background-color: @surface;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ entry {
|
|||||||
listview {
|
listview {
|
||||||
lines: 7;
|
lines: 7;
|
||||||
spacing: 8px;
|
spacing: 8px;
|
||||||
scrollbar: false;
|
scrollbar: true;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,3 +83,11 @@ element-text {
|
|||||||
element-text selected {
|
element-text selected {
|
||||||
text-color: var(accent, #00F0FF);
|
text-color: var(accent, #00F0FF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
handle-width: 4px;
|
||||||
|
handle-color: rgba(0, 240, 255, 0.8);
|
||||||
|
background-color: rgba(21, 26, 34, 0.5);
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ listview {
|
|||||||
columns: 1;
|
columns: 1;
|
||||||
lines: 8;
|
lines: 8;
|
||||||
dynamic: true;
|
dynamic: true;
|
||||||
scrollbar: false;
|
scrollbar: true;
|
||||||
spacing: 4px;
|
spacing: 4px;
|
||||||
padding: 8px 0px 0px 0px;
|
padding: 8px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
@@ -107,4 +107,10 @@ textbox {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollbar { width: 0px; }
|
scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
handle-width: 4px;
|
||||||
|
handle-color: @frost2;
|
||||||
|
background-color: @surface;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ listview {
|
|||||||
columns: 1;
|
columns: 1;
|
||||||
lines: 8;
|
lines: 8;
|
||||||
dynamic: true;
|
dynamic: true;
|
||||||
scrollbar: false;
|
scrollbar: true;
|
||||||
spacing: 2px;
|
spacing: 2px;
|
||||||
padding: 8px 0px 0px 0px;
|
padding: 8px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
@@ -106,4 +106,10 @@ textbox {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollbar { width: 0px; }
|
scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
handle-width: 4px;
|
||||||
|
handle-color: @amber;
|
||||||
|
background-color: @panel;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listview {
|
listview {
|
||||||
|
scrollbar: true;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
columns: 1;
|
columns: 1;
|
||||||
lines: 8;
|
lines: 8;
|
||||||
@@ -131,9 +132,9 @@ element-icon selected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollbar {
|
scrollbar {
|
||||||
width: 0px;
|
width: 4px;
|
||||||
background-color: transparent;
|
handle-width: 4px;
|
||||||
handle-width: 0px;
|
handle-color: @yellow;
|
||||||
handle-color: transparent;
|
background-color: @bg0;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listview {
|
listview {
|
||||||
|
scrollbar: true;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
columns: 1;
|
columns: 1;
|
||||||
lines: 8;
|
lines: 8;
|
||||||
@@ -131,9 +132,9 @@ element-icon selected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollbar {
|
scrollbar {
|
||||||
width: 0px;
|
width: 4px;
|
||||||
background-color: transparent;
|
handle-width: 4px;
|
||||||
handle-width: 0px;
|
handle-color: @yellow;
|
||||||
handle-color: transparent;
|
background-color: @bg0;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
}
|
}
|
||||||
|
|||||||
132
tools/check-install-safety.py
Executable file
132
tools/check-install-safety.py
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Installer safety guards contract (flake check `installer-safety`).
|
||||||
|
|
||||||
|
Usage: check-install-safety.py <nomarchy-install.sh>
|
||||||
|
|
||||||
|
Locks the three BACKLOG #54 guards against silent regression:
|
||||||
|
|
||||||
|
1. Offline compose-lock failure fail-closes (no network `flake lock`).
|
||||||
|
2. Disk signature warn for NTFS / BitLocker / Microsoft / LUKS.
|
||||||
|
3. HM pre-activate failure drops NOMARCHY-DESKTOP-NOT-THEMED.txt.
|
||||||
|
|
||||||
|
Also re-checks the password ≥8 interactive floors (V0-complete).
|
||||||
|
Pure — no VM, no ISO.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
script = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def check(cond: bool, msg: str) -> None:
|
||||||
|
if not cond:
|
||||||
|
sys.exit(f"installer-safety: {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── 1. Offline compose-lock fail-closed ─────────────────────────────────
|
||||||
|
# The offline branch must fail hard; `nix flake lock` only on the online path.
|
||||||
|
check(
|
||||||
|
"Offline lock composition failed and there is no network to fall back to"
|
||||||
|
in script,
|
||||||
|
"missing offline compose-lock fail-closed message",
|
||||||
|
)
|
||||||
|
# Fail-closed is gated on OFFLINE=true.
|
||||||
|
check(
|
||||||
|
re.search(
|
||||||
|
r'if \[\[ "\$OFFLINE" == true \]\]; then\s*\n'
|
||||||
|
r'\s*fail "Offline lock composition failed',
|
||||||
|
script,
|
||||||
|
),
|
||||||
|
"offline fail-closed is not gated on OFFLINE=true",
|
||||||
|
)
|
||||||
|
# Network flake lock must sit in the non-offline fallback, not the offline arm.
|
||||||
|
flake_lock = script.find('nix --extra-experimental-features "nix-command flakes" flake lock')
|
||||||
|
offline_fail = script.find("Offline lock composition failed and there is no network")
|
||||||
|
check(flake_lock > 0 and offline_fail > 0, "compose-lock / flake lock markers missing")
|
||||||
|
check(
|
||||||
|
flake_lock > offline_fail,
|
||||||
|
"network `flake lock` appears before the offline fail-closed arm",
|
||||||
|
)
|
||||||
|
# Between offline fail and flake lock there must not be an unguarded lock call
|
||||||
|
# earlier in the file for this recovery path.
|
||||||
|
check(
|
||||||
|
"Offline lock composition failed — resolving over the network." in script,
|
||||||
|
"missing online-only compose-lock fallback warn",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 2. Disk signature warn ──────────────────────────────────────────────
|
||||||
|
sig_re = re.search(
|
||||||
|
r"grep -qiE '([^']+)'\s*<<<\s*\"\$existing_sig\"",
|
||||||
|
script,
|
||||||
|
)
|
||||||
|
check(sig_re is not None, "disk signature grep missing")
|
||||||
|
pat = sig_re.group(1)
|
||||||
|
check(
|
||||||
|
all(tok in pat for tok in ("ntfs", "bitlocker", "microsoft", "crypto_luks")),
|
||||||
|
f"signature pattern incomplete: {pat!r}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
"Windows/BitLocker/NTFS or LUKS" in script,
|
||||||
|
"missing user-facing disk-signature warn text",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pure behavioural: the same regex the installer uses.
|
||||||
|
rx = re.compile(pat, re.I)
|
||||||
|
samples_hit = [
|
||||||
|
"ntfs\n",
|
||||||
|
"TYPE=\"ntfs\"",
|
||||||
|
"BitLocker",
|
||||||
|
"Microsoft reserved",
|
||||||
|
"crypto_LUKS",
|
||||||
|
"TYPE=\"crypto_LUKS\"",
|
||||||
|
]
|
||||||
|
samples_miss = [
|
||||||
|
"",
|
||||||
|
"ext4\n",
|
||||||
|
"btrfs\nvfat",
|
||||||
|
"TYPE=\"xfs\"",
|
||||||
|
]
|
||||||
|
for s in samples_hit:
|
||||||
|
check(bool(rx.search(s)), f"signature regex should match {s!r}")
|
||||||
|
for s in samples_miss:
|
||||||
|
check(not rx.search(s), f"signature regex should NOT match {s!r}")
|
||||||
|
|
||||||
|
# ── 3. Pre-activate recovery hint ───────────────────────────────────────
|
||||||
|
check(
|
||||||
|
"NOMARCHY-DESKTOP-NOT-THEMED.txt" in script,
|
||||||
|
"missing pre-activate hint filename",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
"home-manager switch --flake ~/.nomarchy -b bak" in script,
|
||||||
|
"hint must tell the user the recovery command",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
'hint_file="/mnt/home/$USERNAME/NOMARCHY-DESKTOP-NOT-THEMED.txt"' in script
|
||||||
|
or 'hint_file="/mnt/home/$USERNAME/NOMARCHY-DESKTOP-NOT-THEMED.txt"'
|
||||||
|
in script.replace(" ", ""),
|
||||||
|
"hint must land on the target home under /mnt",
|
||||||
|
)
|
||||||
|
# Hint is only written on the failure arm (else of nixos-enter activate).
|
||||||
|
activate_if = script.find("if nixos-enter --root /mnt -- bash /root/nomarchy-hm-activate.sh")
|
||||||
|
hint_pos = script.find("NOMARCHY-DESKTOP-NOT-THEMED.txt")
|
||||||
|
check(activate_if > 0 and hint_pos > activate_if, "hint is not on the activate-failure arm")
|
||||||
|
|
||||||
|
# ── Password floors (V0-complete, keep the floor) ───────────────────────
|
||||||
|
check(
|
||||||
|
re.search(r"password for \$USERNAME \(min 8 chars\)", script) is not None,
|
||||||
|
"account password min-8 prompt missing",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
re.search(r"LUKS passphrase \(min 8 chars\)", script) is not None,
|
||||||
|
"LUKS passphrase min-8 prompt missing",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
script.count("[[ ${#") >= 2 and " -ge 8 ]]" in script,
|
||||||
|
"min-8 length checks missing",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("installer-safety: all contracts hold")
|
||||||
292
tools/test-install-safety.sh
Executable file
292
tools/test-install-safety.sh
Executable file
@@ -0,0 +1,292 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# V2 harness for BACKLOG #54 installer safety guards.
|
||||||
|
# Builds the live ISO once, then runs offline unattended scenarios:
|
||||||
|
#
|
||||||
|
# disk-warn — target image has an NTFS signature → warn in install.log
|
||||||
|
# compose-fail — NOMARCHY_TEST_FORCE_COMPOSE_FAIL=1 → offline fail-closed
|
||||||
|
# hm-hint — NOMARCHY_TEST_FORCE_HM_FAIL=1 → hint file on target home
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# tools/test-install-safety.sh # all three
|
||||||
|
# tools/test-install-safety.sh disk-warn # one scenario
|
||||||
|
#
|
||||||
|
# Env (same as test-install.sh where applicable):
|
||||||
|
# NOMARCHY_VM_DIR NOMARCHY_TARGET_DIR NOMARCHY_BOOT_WAIT NOMARCHY_INSTALL_TIMEOUT
|
||||||
|
# NOMARCHY_SCENARIOS space-separated override (default: all three)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
VM_DIR="${NOMARCHY_VM_DIR:-/tmp/nomarchy-vm-safety}"
|
||||||
|
TARGET_DIR="${NOMARCHY_TARGET_DIR:-$HOME/.cache/nomarchy-vm-safety}"
|
||||||
|
BOOT_WAIT="${NOMARCHY_BOOT_WAIT:-480}"
|
||||||
|
INSTALL_TIMEOUT="${NOMARCHY_INSTALL_TIMEOUT:-2700}"
|
||||||
|
export NOMARCHY_QMP_SOCK="$VM_DIR/qmp.sock"
|
||||||
|
QMP=tools/vm/qmp.py
|
||||||
|
SHOT=tools/vm/vncshot.py
|
||||||
|
# Prefer a free VNC display (vm leftovers often hold :17/:18).
|
||||||
|
VNC_DISPLAY="${NOMARCHY_VNC_DISPLAY:-}"
|
||||||
|
if [ -z "$VNC_DISPLAY" ]; then
|
||||||
|
for d in 19 20 21 22 23; do
|
||||||
|
if ! ss -ltn 2>/dev/null | grep -q ":59${d} "; then
|
||||||
|
VNC_DISPLAY=$d
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
VNC_DISPLAY="${VNC_DISPLAY:-19}"
|
||||||
|
|
||||||
|
SCENARIOS=("disk-warn" "compose-fail" "hm-hint")
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
SCENARIOS=("$@")
|
||||||
|
elif [[ -n "${NOMARCHY_SCENARIOS:-}" ]]; then
|
||||||
|
# shellcheck disable=SC2206
|
||||||
|
SCENARIOS=($NOMARCHY_SCENARIOS)
|
||||||
|
fi
|
||||||
|
|
||||||
|
for tool in qemu-system-x86_64 mkfs.vfat mcopy python3; do
|
||||||
|
command -v "$tool" >/dev/null || {
|
||||||
|
echo "missing $tool" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
[ -r /dev/kvm ] || { echo "no KVM" >&2; exit 1; }
|
||||||
|
|
||||||
|
mkdir -p "$VM_DIR" "$TARGET_DIR"
|
||||||
|
if df --output=fstype "$TARGET_DIR" | tail -1 | grep -q tmpfs; then
|
||||||
|
echo "NOMARCHY_TARGET_DIR=$TARGET_DIR is tmpfs" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
OVMF="${NOMARCHY_OVMF:-}"
|
||||||
|
[ -n "$OVMF" ] || for c in /run/current-system/sw/share/qemu/edk2-x86_64-code.fd \
|
||||||
|
/run/current-system/sw/share/OVMF/OVMF_CODE.fd \
|
||||||
|
/usr/share/OVMF/OVMF_CODE.fd; do
|
||||||
|
[ -f "$c" ] && { OVMF="$c"; break; }
|
||||||
|
done
|
||||||
|
# Fall back to a nix-store OVMF (dev boxes without it on the profile).
|
||||||
|
if [ -z "$OVMF" ]; then
|
||||||
|
OVMF=$(ls -1 /nix/store/*/FV/OVMF_CODE.fd 2>/dev/null | head -1 || true)
|
||||||
|
fi
|
||||||
|
[ -n "$OVMF" ] && [ -f "$OVMF" ] || { echo "no OVMF" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "==> Building live ISO..."
|
||||||
|
nix build .#nixosConfigurations.nomarchy-live.config.system.build.isoImage
|
||||||
|
ISO=$(ls -1 result/iso/*.iso | head -n1)
|
||||||
|
echo " ISO=$ISO"
|
||||||
|
|
||||||
|
qemu_base=(
|
||||||
|
qemu-system-x86_64
|
||||||
|
-enable-kvm -cpu host -m 6144 -smp 2
|
||||||
|
-drive "if=pflash,format=raw,readonly=on,file=$OVMF"
|
||||||
|
-device virtio-vga-gl -display egl-headless
|
||||||
|
-vnc "127.0.0.1:$VNC_DISPLAY"
|
||||||
|
-device virtio-net-pci,netdev=n0 -netdev user,id=n0,restrict=on
|
||||||
|
-qmp "unix:$NOMARCHY_QMP_SOCK,server,nowait"
|
||||||
|
)
|
||||||
|
|
||||||
|
prepare_signature_target() {
|
||||||
|
local img=$1
|
||||||
|
rm -f "$img"
|
||||||
|
# Full-size image (install continues after the warn). LUKS1 magic at LBA 0
|
||||||
|
# is reliably reported by blkid as TYPE=crypto_LUKS — matches the installer's
|
||||||
|
# crypto_luks regex. No root/cryptsetup needed.
|
||||||
|
qemu-img create -f raw "$img" 25G >/dev/null
|
||||||
|
python3 - "$img" <<'PY'
|
||||||
|
import sys
|
||||||
|
path = sys.argv[1]
|
||||||
|
with open(path, "r+b") as f:
|
||||||
|
# LUKS1 on-disk magic (util-linux blkid probe)
|
||||||
|
hdr = bytearray(512)
|
||||||
|
hdr[0:6] = b"LUKS\xba\xbe"
|
||||||
|
hdr[6] = 1 # version major
|
||||||
|
hdr[7] = 0
|
||||||
|
f.write(hdr)
|
||||||
|
print(f" wrote LUKS magic at LBA 0 on {path}")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
write_go_sh() {
|
||||||
|
local scenario=$1
|
||||||
|
local out=$2
|
||||||
|
local force_compose=0 force_hm=0 finish=poweroff
|
||||||
|
case "$scenario" in
|
||||||
|
disk-warn) finish=poweroff ;;
|
||||||
|
compose-fail) force_compose=1; finish=none ;; # fail before poweroff path
|
||||||
|
hm-hint) force_hm=1; finish=poweroff ;;
|
||||||
|
*) echo "unknown scenario $scenario" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
cat >"$out" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -uo pipefail
|
||||||
|
export NOMARCHY_UNATTENDED=1
|
||||||
|
export NOMARCHY_DISK=/dev/vda
|
||||||
|
export NOMARCHY_USERNAME=ada
|
||||||
|
export NOMARCHY_PASSWORD=nomarchy
|
||||||
|
export NOMARCHY_HOSTNAME=testbox
|
||||||
|
export NOMARCHY_LUKS_PASSPHRASE=testtest1
|
||||||
|
export NOMARCHY_SWAP_GB=2
|
||||||
|
export NOMARCHY_FINISH=$finish
|
||||||
|
export NOMARCHY_TEST_FORCE_COMPOSE_FAIL=$force_compose
|
||||||
|
export NOMARCHY_TEST_FORCE_HM_FAIL=$force_hm
|
||||||
|
# Do not set -e: compose-fail exits 1 and we still need the log on vfat.
|
||||||
|
nomarchy-install 2>&1 | tee /tmp/install.log
|
||||||
|
rc=\${PIPESTATUS[0]}
|
||||||
|
cp -f /tmp/install.log /m/install.log 2>/dev/null || true
|
||||||
|
if [[ "$scenario" == "hm-hint" && -f /mnt/home/ada/NOMARCHY-DESKTOP-NOT-THEMED.txt ]]; then
|
||||||
|
cp -f /mnt/home/ada/NOMARCHY-DESKTOP-NOT-THEMED.txt /m/hint.txt || true
|
||||||
|
fi
|
||||||
|
echo "INSTALL_RC=\$rc" | tee -a /tmp/install.log | tee -a /m/install.log
|
||||||
|
# Always power off so the host wait loop ends (compose-fail uses FINISH=none).
|
||||||
|
systemctl poweroff 2>/dev/null || poweroff -f 2>/dev/null || true
|
||||||
|
exit \$rc
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
run_scenario() {
|
||||||
|
local scenario=$1
|
||||||
|
local sdir="$VM_DIR/$scenario"
|
||||||
|
mkdir -p "$sdir"
|
||||||
|
local target="$TARGET_DIR/$scenario-target.img"
|
||||||
|
local cfgimg="$sdir/config.img"
|
||||||
|
local log="$sdir/install.log"
|
||||||
|
echo
|
||||||
|
echo "========== scenario: $scenario =========="
|
||||||
|
|
||||||
|
if [[ "$scenario" == "disk-warn" ]]; then
|
||||||
|
prepare_signature_target "$target"
|
||||||
|
else
|
||||||
|
rm -f "$target"
|
||||||
|
qemu-img create -f raw "$target" 25G >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_go_sh "$scenario" "$sdir/go.sh"
|
||||||
|
chmod +x "$sdir/go.sh"
|
||||||
|
rm -f "$cfgimg"
|
||||||
|
truncate -s 16M "$cfgimg"
|
||||||
|
mkfs.vfat "$cfgimg" >/dev/null
|
||||||
|
mcopy -i "$cfgimg" "$sdir/go.sh" ::go.sh
|
||||||
|
|
||||||
|
rm -f "$NOMARCHY_QMP_SOCK"
|
||||||
|
"${qemu_base[@]}" \
|
||||||
|
-drive "file=$target,if=virtio,format=raw" \
|
||||||
|
-drive "file=$cfgimg,if=virtio,format=raw" \
|
||||||
|
-cdrom "$ISO" -boot d \
|
||||||
|
>"$sdir/qemu-install.log" 2>&1 &
|
||||||
|
local qpid=$!
|
||||||
|
# shellcheck disable=SC2064
|
||||||
|
trap "kill $qpid 2>/dev/null || true" RETURN
|
||||||
|
|
||||||
|
waited_sock=0
|
||||||
|
until [ -S "$NOMARCHY_QMP_SOCK" ]; do
|
||||||
|
if ! kill -0 "$qpid" 2>/dev/null; then
|
||||||
|
echo "FAIL: QEMU died before QMP came up (see $sdir/qemu-install.log)" >&2
|
||||||
|
tail -20 "$sdir/qemu-install.log" >&2 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if (( waited_sock > 120 )); then
|
||||||
|
echo "FAIL: no QMP socket after 120s" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
waited_sock=$((waited_sock + 2))
|
||||||
|
done
|
||||||
|
echo " waiting ${BOOT_WAIT}s for live desktop (VNC :$VNC_DISPLAY)..."
|
||||||
|
sleep "$BOOT_WAIT"
|
||||||
|
python3 "$QMP" key meta_l-ret
|
||||||
|
sleep 8
|
||||||
|
# Mount config rw so the guest can write install.log back.
|
||||||
|
python3 "$QMP" type 'sudo bash -c "mkdir -p /m; mount /dev/vdb /m; bash /m/go.sh; umount /m || true"'
|
||||||
|
sleep 2
|
||||||
|
python3 "$SHOT" "$sdir/install-typed.png" 127.0.0.1 "59$VNC_DISPLAY" || true
|
||||||
|
python3 "$QMP" key ret
|
||||||
|
echo " install running..."
|
||||||
|
|
||||||
|
local waited=0
|
||||||
|
while kill -0 "$qpid" 2>/dev/null; do
|
||||||
|
if (( waited >= INSTALL_TIMEOUT )); then
|
||||||
|
python3 "$SHOT" "$sdir/install-hung.png" 127.0.0.1 "59$VNC_DISPLAY" || true
|
||||||
|
echo "FAIL: $scenario hung after ${INSTALL_TIMEOUT}s" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 20
|
||||||
|
waited=$((waited + 20))
|
||||||
|
done
|
||||||
|
trap - RETURN
|
||||||
|
echo " VM exited after ~${waited}s"
|
||||||
|
|
||||||
|
# Pull install.log from the config vfat (guest wrote it under /m).
|
||||||
|
mcopy -n -i "$cfgimg" ::install.log "$log" 2>/dev/null || true
|
||||||
|
if [[ ! -s "$log" ]]; then
|
||||||
|
echo "FAIL: $scenario — no install.log recovered from config disk" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo " log: $log ($(wc -l <"$log") lines)"
|
||||||
|
|
||||||
|
case "$scenario" in
|
||||||
|
disk-warn)
|
||||||
|
if ! grep -qiE 'Windows/BitLocker/NTFS or LUKS' "$log"; then
|
||||||
|
echo "FAIL: disk-warn — expected signature warn missing" >&2
|
||||||
|
tail -40 "$log" >&2 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "PASS disk-warn: signature warn present"
|
||||||
|
;;
|
||||||
|
compose-fail)
|
||||||
|
if ! grep -q 'Offline lock composition failed and there is no network to fall back to' "$log"; then
|
||||||
|
echo "FAIL: compose-fail — offline fail-closed message missing" >&2
|
||||||
|
tail -60 "$log" >&2 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if grep -qE 'resolving over the network|flake lock' "$log"; then
|
||||||
|
# "flake lock" only as a warning path — must not succeed online.
|
||||||
|
if grep -q 'Offline lock composition failed — resolving over the network' "$log"; then
|
||||||
|
echo "FAIL: compose-fail — took the ONLINE fallback while offline" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! grep -q 'INSTALL_RC=1' "$log"; then
|
||||||
|
echo "FAIL: compose-fail — expected INSTALL_RC=1" >&2
|
||||||
|
grep INSTALL_RC "$log" >&2 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "PASS compose-fail: offline fail-closed (rc=1, no network fallback)"
|
||||||
|
;;
|
||||||
|
hm-hint)
|
||||||
|
mcopy -n -i "$cfgimg" ::hint.txt "$sdir/hint.txt" 2>/dev/null || true
|
||||||
|
if [[ ! -f "$sdir/hint.txt" ]]; then
|
||||||
|
# Fall back: after poweroff, try reading the target filesystem (LUKS).
|
||||||
|
echo "FAIL: hm-hint — hint.txt not copied from /mnt during install" >&2
|
||||||
|
tail -40 "$log" >&2 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! grep -q 'home-manager switch --flake ~/.nomarchy -b bak' "$sdir/hint.txt"; then
|
||||||
|
echo "FAIL: hm-hint — recovery command missing from hint file" >&2
|
||||||
|
cat "$sdir/hint.txt" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! grep -q 'Desktop pre-activation failed' "$sdir/hint.txt"; then
|
||||||
|
echo "FAIL: hm-hint — unexpected hint contents" >&2
|
||||||
|
cat "$sdir/hint.txt" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "PASS hm-hint: NOMARCHY-DESKTOP-NOT-THEMED.txt written on target home"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
for s in "${SCENARIOS[@]}"; do
|
||||||
|
if ! run_scenario "$s"; then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
if (( failed )); then
|
||||||
|
echo "FAIL: one or more #54 safety scenarios failed (logs under $VM_DIR)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "PASS: all requested #54 safety scenarios"
|
||||||
|
echo "Artifacts: $VM_DIR"
|
||||||
Reference in New Issue
Block a user