Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecc089c6ef | ||
|
|
7523ca5c19 | ||
|
|
3022277167 | ||
|
|
689ada428f |
+17
-6
@@ -80,10 +80,21 @@ wt/
|
||||
agent/memories/--home-blank-.omp-wt-*
|
||||
|
||||
# --- theme media: content the pipeline consumes, not configuration ---------
|
||||
# 23 wallpapers, 80 MiB. Tracked until 2026-10-20; every clone of a config repo
|
||||
# was carrying a library no diff can review. The files stay on disk because
|
||||
# ~/.local/share/noctalia/wallpapers symlinks into here, and every filename
|
||||
# carries its wallhaven ID, so a fresh box restores them with
|
||||
# `curl -L -o <name> https://wallhaven.cc/w/<id>` + deploy.sh.
|
||||
# Law: omp-docs/tools/dotfiles-deploy.md
|
||||
# 23 wallpapers, 80 MiB. Tracked until 2026-10-20: every clone of a config repo was
|
||||
# carrying a library no diff can review. The live library at
|
||||
# ~/.local/share/noctalia/wallpapers is a real directory of real files (measured
|
||||
# 2026-10-20), so this path in the repo was a duplicate of it, not its source — and
|
||||
# note that untracking deletes the copy in every other checkout regardless of
|
||||
# `git rm --cached` on the branch. A fresh box starts with an empty library; every
|
||||
# filename carries its wallhaven ID, so `curl -L -o <name> https://wallhaven.cc/w/<id>`
|
||||
# puts it back. Law: omp-docs/tools/dotfiles-deploy.md
|
||||
dotfiles/noctalia/.local/share/noctalia/wallpapers/
|
||||
|
||||
# --- generated theme output: machine-written, never reviewable, never deployed ---
|
||||
# noctalia/matugen rewrite these in place; a tracked copy is a stale snapshot that
|
||||
# deploy.sh would push back over a live palette. Patterns match the exclusion list in
|
||||
# scripts/deploy.sh. Law: omp-docs/inventory/home-config-census.md, Refs #42
|
||||
dotfiles/**/themes/noctalia*
|
||||
dotfiles/**/colors/noctalia*
|
||||
dotfiles/**/noctalia.*
|
||||
dotfiles/**/noctalia-*
|
||||
|
||||
@@ -9,8 +9,6 @@ window-size=(360, -1)
|
||||
|
||||
[org/gnome/desktop/background]
|
||||
picture-options='zoom'
|
||||
picture-uri='file:///home/dank/.config/background'
|
||||
picture-uri-dark='file:///home/dank/.config/background'
|
||||
|
||||
[org/gnome/desktop/interface]
|
||||
color-scheme='prefer-dark'
|
||||
@@ -26,14 +24,6 @@ default-visible-columns=['name', 'size', 'type', 'date_modified']
|
||||
default-folder-viewer='list-view'
|
||||
migrated-gtk-settings=true
|
||||
|
||||
[org/gnome/nautilus/window-state]
|
||||
initial-size=(890, 550)
|
||||
initial-size-file-chooser=(890, 550)
|
||||
maximized=false
|
||||
|
||||
[org/gnome/portal/filechooser/firefox]
|
||||
last-folder-path='/home/dank/Downloads'
|
||||
|
||||
[org/gtk/gtk4/settings/file-chooser]
|
||||
show-hidden=false
|
||||
sort-directories-first=false
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
layout {
|
||||
gaps 16 // Gap between windows
|
||||
center-focused-column "never" // Don’t auto-center focused column
|
||||
|
||||
background-color "transparent" // <- needed for noctalia-shell to set wallpaper
|
||||
|
||||
preset-column-widths {
|
||||
proportion 0.25 // 1/4
|
||||
proportion 0.3333 // 1/3
|
||||
proportion 0.5 // 1/2
|
||||
}
|
||||
|
||||
preset-window-heights {
|
||||
proportion 0.25 // 1/4
|
||||
proportion 0.5 // 1/2
|
||||
}
|
||||
|
||||
struts {
|
||||
}
|
||||
shadow
|
||||
focus-ring {
|
||||
width 2
|
||||
|
||||
active-color "#e01b24"
|
||||
}
|
||||
border {
|
||||
off
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
layout {
|
||||
|
||||
focus-ring {
|
||||
active-color "#ff8c00"
|
||||
inactive-color "#181818"
|
||||
urgent-color "#ff8c00"
|
||||
}
|
||||
|
||||
border {
|
||||
active-color "#f43841"
|
||||
inactive-color "#181818"
|
||||
urgent-color "#f43841"
|
||||
}
|
||||
|
||||
|
||||
tab-indicator {
|
||||
active-color "#f43841"
|
||||
inactive-color "#74050b"
|
||||
urgent-color "#f43841"
|
||||
}
|
||||
|
||||
insert-hint {
|
||||
color "#f4384180"
|
||||
}
|
||||
}
|
||||
|
||||
recent-windows {
|
||||
highlight {
|
||||
active-color "#f43841"
|
||||
urgent-color "#f43841"
|
||||
}
|
||||
}
|
||||
@@ -83,8 +83,8 @@ by category, not file-by-file.
|
||||
| micro | 0 | 154 | adopt: `settings.json`, `bindings.json`; generated: `colorschemes/noctalia.micro`; bundled/state: `syntax/*` (146), `buffers/history` |
|
||||
| mpv | 0 | 0 | — (empty) |
|
||||
| nautilus | 0 | 0 | — (empty) |
|
||||
| niri | 9 | 1 | 8 live KDL includes (5 from `dotfiles/niri`, 3 overridden by the `zz-blankpad` tier, whose own 9 niri files are counted in that tier) + 1 tracked relic, see *Never track*. Box-only: `noctalia.kdl`, a generated include — [tools/dotfiles-deploy](/tools/dotfiles-deploy.md) |
|
||||
| noctalia | 3 | 2672 | Tracked and live-linked: `config.toml`, `palettes/Gruber Red.json`, `state/noctalia/settings.toml`. Everything else is app state plus noctalia's downloaded catalog: `community-palettes/` 66, `community-templates/` 80, `wallhaven/thumbs`, `clipboard/`, `plugins/`, `state.toml`, `usage_counts.json`. The 23 wallpapers #41 untracks stay on disk — the theme reads them |
|
||||
| niri | 8 | 2 | 8 live KDL includes (5 from `dotfiles/niri`, 3 overridden by the `zz-blankpad` tier, whose own 7 niri files are counted in that tier — the stale `layout.kdl` duplicate and generated `noctalia.kdl` were removed by #43) + see *Never track* for the relic. Box-only: `noctalia.kdl` (generated include, refused by `deploy.sh`) and the retired copy |
|
||||
| noctalia | 3 | 2672 | Tracked and live-linked: `config.toml`, `palettes/Gruber Red.json`, `state/noctalia/settings.toml`. Everything else is app state plus noctalia's downloaded catalog: `community-palettes/` 66, `community-templates/` 80, `wallhaven/thumbs`, `clipboard/`, `plugins/`, `state.toml`, `usage_counts.json`. The app's wallpaper library is `~/.local/share/noctalia/wallpapers` — real files it owns; the repo's duplicate copy of 23 JPEGs was untracked by #41 and deleted from this checkout by the merge, which is what untracking does in every checkout |
|
||||
| nvim | 0 | 0 | — (empty) |
|
||||
| pywalfox | 0 | 1 | `config.json` — program absent, prune |
|
||||
| qt5ct | 0 | 1 | `colors/noctalia.conf` (generated) — program absent, prune |
|
||||
@@ -135,7 +135,7 @@ configs).
|
||||
| Reason | Contents |
|
||||
|---|---|
|
||||
| **Generated palette expansion** | Every `themes/noctalia*`, `*/noctalia.*`, `*/colors/noctalia*` / `Noctalia*` (24 files outside app-profile dirs). Machine-written by matugen/noctalia, not hand-authored: the `stat -c '%y %n'` cluster `2026-10-14 19:05:40.330…40.813 -0700` writes 24 files in one second (`alacritty/themes/noctalia.toml` at `.330212397`, `btop/themes/noctalia.theme` at `.334212456`, `tmux/themes/noctalia.conf` at `.813219586`, …); later re-runs re-stamp singles — `bat/themes/noctalia.tmTheme` `2026-10-18 17:11:22`, `niri/noctalia.kdl` `2026-10-18 17:36:27`, `mango/noctalia.conf` `2026-10-17 18:23:37`. A same-second mass-write is generation, never an edit. |
|
||||
| **App state / profiles** | `mozilla` (5471), `unity3d` RimWorld state (235), `nirimod` (116), `obsidian` (99), `FreeTube` (56), `google-chrome-for-testing` (5); plus per-program state inside tracked dirs: `dconf/user`, `fish/fish_variables`, `micro/syntax/*`+`micro/buffers/history`, `gtk-4.0/servers`, `cachyos/cachyos-hello.log`. |
|
||||
| **App state / profiles** | `mozilla` (5471), `unity3d` RimWorld state (235), `nirimod` (116), `obsidian` (99), `FreeTube` (56), `google-chrome-for-testing` (5); plus per-program state inside tracked dirs: `dconf/user`, `fish/fish_variables`, `micro/syntax/*`+`micro/buffers/history`, `gtk-4.0/servers`, `cachyos/cachyos-hello.log`. State hides inside a tracked file too: `dconf/dump.txt` carried `last-folder-path`, a nautilus `window-state` section and `picture-uri`/`picture-uri-dark` — all `/home/dank` values, and the wallpaper URIs were live on blankpad, i.e. already deployed here — dropped by #44, see [tools/dotfiles-deploy](/tools/dotfiles-deploy.md). |
|
||||
| **Skills-CLI installs** | `crush` (37), `devin` (37), `goose` (37), `kimchi/harness` (37) skill trees — provisioned by the skills CLI, owned by the store, not this repo. See [skills/store-wiring](/skills/store-wiring.md). |
|
||||
| **Secret tier** | `~/.config/secrets/` (`gitea`, `hf_token`, `hf_token_write`, `kagi_api_key` — names only), top-level `frank-job-hunt.env`, `containers/systemd/postgres.env`. Never tracked, values never printed. |
|
||||
| **Trash committed inside the tree** | `dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded` — a retired output config committed 2026-10-09, five days before `.gitignore:54` gained `.AGENTTRASH/` (2026-10-14). Tracked-then-ignored: an ignore rule never reaches an already-tracked path, so the only repair is `git rm --cached`, done by #41. The rule does match at depth — `git check-ignore --no-index -v dotfiles/niri/.config/niri/cfg/.AGENTTRASH/x` resolves to `.gitignore:54` — so a fresh in-tree trash dir is ignored from birth and this does not recur on its own. |
|
||||
|
||||
@@ -25,14 +25,16 @@ is stowed last and its files replace symlinks coming from base packages.
|
||||
| `deploy.sh` (sync) | adopt identicals, link new, quarantine divergents into `.quarantine/` and replace them |
|
||||
| `deploy.sh --adopt` | like sync, but divergents are reported and skipped rather than quarantined |
|
||||
|
||||
Two package shapes are not symlinks: `dconf/dump.txt` is loaded with `dconf load /`, and
|
||||
`systemd-user` units get `daemon-reload` plus `enable --now` only when their `ExecStart`
|
||||
binary resolves on the current machine, so a unit belonging to another box stays inert
|
||||
instead of crash-looping.
|
||||
Two package shapes are not symlinks. `dconf/dump.txt` is loaded with `dconf load /`, and
|
||||
`systemd-user` units get `daemon-reload` plus `enable`; a unit is **started** only when
|
||||
systemd reports it as nothing-yet (`""`/`disabled`), and never when its `ExecStart` binary
|
||||
is absent here, so a unit belonging to another box stays inert instead of crash-looping.
|
||||
Starting something already installed is what makes `deploy.sh` unsafe to run beside a live
|
||||
session — see *Never start a unit a session may already be running*.
|
||||
|
||||
**The tracked unit is the file, not the directory.** A live directory can be a real
|
||||
directory and still be fully tracked, because every file inside it is a symlink into the
|
||||
repo (`niri` is 9 tracked / 1 box-only; `gtk-3.0` is 3/0). Any audit that classifies by
|
||||
repo (`niri` is 8 tracked / 2 box-only; `gtk-3.0` is 3/0). Any audit that classifies by
|
||||
directory mislabels the tree — classify per file with `readlink -f`. The live census and
|
||||
its classifier command are in [home-config-census](/inventory/home-config-census.md).
|
||||
|
||||
@@ -42,14 +44,18 @@ Wallpapers live at `dotfiles/noctalia/.local/share/noctalia/wallpapers/` and are
|
||||
gitignored. They were tracked until 2026-10-20 — 23 files, 80 MiB — which meant every
|
||||
clone of a *configuration* repo carried a media library no diff can review, and the
|
||||
payload only ever grows because deleting a tracked blob from the tree does not delete it
|
||||
from history. [Noctalia](noctalia-shell.md) reads
|
||||
`~/.local/share/noctalia/wallpapers/`, which is a set of symlinks into that directory, so
|
||||
the files stay on disk and untracking them broke nothing on this box.
|
||||
from history. The topology is not what one would guess: [Noctalia](noctalia-shell.md)
|
||||
keeps its own library at `~/.local/share/noctalia/wallpapers/` as a **real directory of
|
||||
real files**, so the tracked tree was a duplicate of it and never its source — measured
|
||||
2026-10-20, when the merge of #41 deleted the repo copies and the desktop did not
|
||||
flicker. Two consequences to know before untracking media: the copy that survives belongs
|
||||
to the application, not to the repo; and untracking deletes the file in every other
|
||||
checkout, `git rm --cached` on the branch notwithstanding.
|
||||
|
||||
Restore on a fresh box: every filename is `wallhaven_<id>.<ext>` and the ID is the whole
|
||||
address — `curl -L -o <name> "https://wallhaven.cc/w/<id>"` puts the library back, then
|
||||
`deploy.sh` relinks it. Losing the set is an acceptable outcome; 80 MB per clone forever
|
||||
is not.
|
||||
Restore on a fresh box: the library lives in `~/.local/share/noctalia/wallpapers/`, a path
|
||||
`deploy.sh` does not manage. Every filename is `wallhaven_<id>.<ext>` and the ID is the
|
||||
whole address — `curl -L -o <name> "https://wallhaven.cc/w/<id>"` into that directory puts
|
||||
the set back. Losing it is an acceptable outcome; 80 MB per clone forever is not.
|
||||
|
||||
## Law: never `systemctl --user enable` a unit from a worktree
|
||||
|
||||
@@ -90,3 +96,91 @@ tracked in `dotfiles/zz-blankpad/`; the pointer is not, because which desk the b
|
||||
is not a fact about the repo. A fresh box therefore starts with no output configuration —
|
||||
`config.kdl` includes `display.kdl` with `optional=true`, so niri comes up usable but with
|
||||
default monitors until `desk.sh` is run once.
|
||||
|
||||
## Law: generated theme output is never deployed
|
||||
|
||||
noctalia rewrites palette output in place — `noctalia.*`, `themes/noctalia*`,
|
||||
`colors/noctalia*`, `noctalia-*` (the classes the [census](/inventory/home-config-census.md)
|
||||
proves by same-millisecond mtime). A tracked copy of one is a stale snapshot of a colour
|
||||
scheme, and the failure is quiet: `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl`
|
||||
(commit `b3d3df4`, 2026-10-18) sat byte-identical to the live file for two days, harmless,
|
||||
while `--check` cheerfully proposed `ADOPT … noctalia.kdl`. The moment the palette changed,
|
||||
the next sync would have quarantined the live file and restored 2026-10-18's colours —
|
||||
a tool silently reverting a user-visible theme choice.
|
||||
|
||||
`deploy.sh` now refuses the class (`SKIP … (generated theme output)`), `.gitignore` carries
|
||||
the matching patterns so it cannot be re-added, and `tests/deploy.bats` pins both
|
||||
directions: generated output is never adopted, and an ordinary identical config still is.
|
||||
Untracked in #43, Refs #42.
|
||||
|
||||
## Law: a `zz` override older than the file it overrides is a time bomb
|
||||
|
||||
Stow order means `zz-$HOST` wins, and when both packages carry the same path, sync relinks
|
||||
it *silently* — the script's own comment: "managed file changing owners between packages:
|
||||
silent relink, nothing to quarantine". `dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl`
|
||||
held focus ring `width 2` / `active-color "#e01b24"` from `7b7dbf5` (2026-10-17) while the
|
||||
base package absorbed `b3d3df4` (2026-10-18): `width 4`, `"#ff8c00"`. Live was linked to
|
||||
base, the newer intent, so `--check` printed `RELINK .config/niri/cfg/layout.kdl` on every
|
||||
run and one unattended `deploy.sh` would have reverted the amber focus ring with no
|
||||
quarantine and no warning. The duplicate is deleted in #43.
|
||||
|
||||
Rule: a `zz` file exists only to differ. When base absorbs the change, delete the `zz` copy
|
||||
in the same commit.
|
||||
|
||||
How to read `--check`, measured on blankpad: it prints `RELINK` for **every** path the `zz`
|
||||
tier carries, even when the live link already points at the override — `rules.kdl`,
|
||||
`keybinds.kdl`, `misc.kdl`, `autostart.kdl` and `.zshrc` all did — because the walk
|
||||
processes the base package first and reports the intermediate owner change. A `RELINK` line
|
||||
therefore proves a pending change only when `readlink` shows the live target is *not*
|
||||
already the `zz-$HOST` file. `actions=0` is the only honest summary.
|
||||
## Law: never start a unit a session may already be running
|
||||
|
||||
`sync` used to end with `systemctl --user enable --now` for every unit in `systemd-user/`
|
||||
whose `ExecStart` resolves. Measured on blankpad before one such run:
|
||||
|
||||
|unit|`UnitFileState`|`ActiveState`|
|
||||
|---|---|---|
|
||||
|`live-sync.service`|`linked`|`inactive`|
|
||||
|`shelly-notifications.service`|`enabled`|`active`|
|
||||
|`swayidle-lock.service`|`enabled`|`inactive`|
|
||||
|
||||
`swayidle-lock.service` was installed and **not running** — nothing started it, no swayidle
|
||||
process existed (`pgrep -c swayidle` = 0), and `autostart.kdl` does not launch one. A
|
||||
`deploy.sh` run would have armed it: monitors blanking and locking on the idle timer while
|
||||
the operator was mid-sentence. `live-sync.service` is `linked`, driven by its timer;
|
||||
`--now` forces a heartbeat commit at an arbitrary moment.
|
||||
|
||||
Two rules follow. Install state must be read from the **printed state**, not the exit
|
||||
status — `is-enabled` exits 1 for `linked` exactly as for `disabled`, so exit status cannot
|
||||
tell "installed, driven by a timer" apart from "never installed". And a unit is started only
|
||||
in the latter case: a machine with a session on it has already decided what runs, while a
|
||||
fresh box — where everything reports `disabled` — still gets its units up. Both directions
|
||||
are pinned in `tests/deploy.bats` over a stub `systemctl`, because the real one on this box
|
||||
is exactly what must not be invoked from a test. Refs #44.
|
||||
|
||||
## Law: the dconf dump holds preferences, not per-box state
|
||||
|
||||
`dconf load /` writes every key in `dotfiles/dconf/dump.txt`, so the dump carries whatever
|
||||
box it was taken from. Comparing key by key against live state on blankpad found **six of
|
||||
24** keys that were not this box's, and not one of them was a preference:
|
||||
|
||||
[org/gnome/portal/filechooser/firefox] last-folder-path
|
||||
repo: '/home/dank/Downloads' live: '/home/blank/Downloads'
|
||||
[org/gnome/nautilus/window-state] initial-size, initial-size-file-chooser, maximized
|
||||
[org/gnome/desktop/background] picture-uri, picture-uri-dark
|
||||
both: 'file:///home/dank/.config/background'
|
||||
|
||||
A file-chooser memory, a window geometry, a wallpaper URI: application state that arrived
|
||||
with the dotfiles import. The last five did not appear as conflicts at all, because
|
||||
blankpad's **live** tree already held the same `/home/dank` values — an earlier deploy had
|
||||
written them here. A foreign value that agrees with live is invisible to any diff and still
|
||||
wrong. All six come out in #44; the two wallpaper URIs are also cleared on this box
|
||||
(`gsettings set org.gnome.desktop.background picture-uri ''`, and its `-dark` twin), because
|
||||
niri draws the wallpaper, noctalia owns it, and nothing reads a dead path.
|
||||
|
||||
Compare **key by key**, not with a plain `diff`: `dconf load` writes only the keys present,
|
||||
so live-only keys survive and a diff overstates the damage in one direction and understates
|
||||
it in the other. Re-dump on the box whose settings are canonical (`dconf dump / >
|
||||
dotfiles/dconf/dump.txt`), then delete the state sections before committing — the census's
|
||||
*Never track: app state* class applies inside a tracked file, not just to whole directories.
|
||||
Refs #44.
|
||||
|
||||
+22
-2
@@ -44,6 +44,14 @@ actions=0; quarantined=0; skipped=0
|
||||
|
||||
deploy_file() { # $1 = repo file path, $2 = home-relative target path
|
||||
local src="$1" tgt="$HOME/$2"
|
||||
# Generated theme output is never deployment state: noctalia rewrites these files in
|
||||
# place, so a tracked copy is a stale snapshot and syncing one reverts a live palette to
|
||||
# the colours of the day it was committed. Classes are those the census measures by
|
||||
# mtime — see omp-docs/inventory/home-config-census.md, "Generated palette expansion".
|
||||
case "$2" in
|
||||
*/themes/noctalia*|*/colors/noctalia*|*/noctalia.*|*/noctalia-*|*/Noctalia*)
|
||||
echo "SKIP $2 (generated theme output)"; skipped=$((skipped+1)); return 0 ;;
|
||||
esac
|
||||
if [ -L "$tgt" ]; then
|
||||
if [ "$(realpath "$tgt")" = "$(realpath "$src")" ]; then return 0; fi # already deployed
|
||||
case "$MODE" in
|
||||
@@ -116,8 +124,20 @@ if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user"
|
||||
[ -e "$unit" ] || continue
|
||||
bin="$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$unit" | head -1)"
|
||||
if [ -n "$bin" ] && command -v "$bin" >/dev/null 2>&1; then
|
||||
systemctl --user enable --now "$(basename "$unit")" || true
|
||||
echo "ENABLE $(basename "$unit") (ExecStart resolves here)"
|
||||
name="$(basename "$unit")"
|
||||
# Read the state, not the exit status: `is-enabled` exits 1 for a `linked` unit exactly
|
||||
# as for a `disabled` one. Starting something already installed is how a deploy locks a
|
||||
# live session — swayidle-lock sits enabled/inactive on blankpad and `--now` arms it
|
||||
# mid-conversation. Only a unit nothing has installed yet gets started. Refs #44.
|
||||
state="$(systemctl --user is-enabled "$name" 2>/dev/null || true)"
|
||||
case "$state" in
|
||||
"" | disabled)
|
||||
systemctl --user enable --now "$name" || true
|
||||
echo "ENABLE $name (ExecStart resolves here; nothing running it yet)" ;;
|
||||
*)
|
||||
systemctl --user enable "$name" || true
|
||||
echo "ENABLE $name (already $state; not started — a live session decides what runs)" ;;
|
||||
esac
|
||||
else
|
||||
echo "SKIP $(basename "$unit") (ExecStart '$bin' absent on this host)"
|
||||
fi
|
||||
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bats
|
||||
# tests/deploy.bats — black-box tests for scripts/deploy.sh (Refs #42)
|
||||
#
|
||||
# Contract (see script header): the repo checkout IS the deployment; sync adopts
|
||||
# identicals, links new files, quarantines-and-replaces divergents, and stows the
|
||||
# zz-$(hostname) package last. One thing it must NEVER do is deploy generated theme
|
||||
# output: noctalia rewrites `noctalia.*` / `themes/noctalia*` in place, so a tracked
|
||||
# copy is a stale snapshot, and a sync would revert a live palette to whatever colour
|
||||
# scheme was current on the day it was committed.
|
||||
#
|
||||
# Nothing is observed except exit status, stdout, the file/symlink shape under $HOME,
|
||||
# and the quarantine directory.
|
||||
|
||||
setup() {
|
||||
TEST_TMP="$(mktemp -d)"
|
||||
REPO="$TEST_TMP/repo"
|
||||
DEPLOY="$BATS_TEST_DIRNAME/../scripts/deploy.sh"
|
||||
mkdir -p "$REPO/scripts" "$REPO/dotfiles"
|
||||
# A git repo, because the deployment checkout IS one and deploy.sh arms hooksPath.
|
||||
git init -q -b main "$REPO"
|
||||
cp "$DEPLOY" "$REPO/scripts/deploy.sh"
|
||||
chmod +x "$REPO/scripts/deploy.sh"
|
||||
export HOME="$TEST_TMP/home"
|
||||
mkdir -p "$HOME"
|
||||
export DEPLOY_REPO="$REPO" DEPLOY_HOST=box
|
||||
|
||||
# systemctl stub: records every call and answers is-enabled from a file. The real
|
||||
# systemctl must never run here — on this box it would start a screen lock.
|
||||
mkdir -p "$TEST_TMP/bin"
|
||||
export STUB_LOG="$TEST_TMP/systemctl.log" UNIT_STATE="$TEST_TMP/unit-state"
|
||||
: > "$STUB_LOG"
|
||||
cat > "$TEST_TMP/bin/systemctl" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
echo "systemctl $*" >> "$STUB_LOG"
|
||||
if [ "$2" = is-enabled ]; then
|
||||
state="$(cat "$UNIT_STATE" 2>/dev/null || echo disabled)"
|
||||
printf '%s\n' "$state"
|
||||
case "$state" in disabled | "") exit 1 ;; *) exit 0 ;; esac
|
||||
fi
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$TEST_TMP/bin/systemctl"
|
||||
export PATH="$TEST_TMP/bin:$PATH"
|
||||
}
|
||||
|
||||
teardown() { rm -rf "$TEST_TMP"; }
|
||||
|
||||
# repo_file <pkg-relative path> <content>
|
||||
repo_file() { mkdir -p "$(dirname "$REPO/dotfiles/$1")"; printf '%s\n' "$2" > "$REPO/dotfiles/$1"; }
|
||||
# live_file <absolute path under $HOME> <content>
|
||||
live_file() { mkdir -p "$(dirname "$1")"; printf '%s\n' "$2" > "$1"; }
|
||||
|
||||
# systemd_unit <name> <ExecStart> — a unit in the systemd-user package
|
||||
systemd_unit() {
|
||||
mkdir -p "$REPO/dotfiles/systemd-user/.config/systemd/user"
|
||||
printf '[Unit]\nDescription=%s\n[Service]\nExecStart=%s\n[Install]\nWantedBy=default.target\n' "$1" "$2" \
|
||||
> "$REPO/dotfiles/systemd-user/.config/systemd/user/$1"
|
||||
}
|
||||
|
||||
@test "generated theme output is never adopted into a symlink" {
|
||||
repo_file "ghostty/.config/ghostty/themes/noctalia" 'background = "#f43841"'
|
||||
live_file "$HOME/.config/ghostty/themes/noctalia" 'background = "#f43841"'
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"SKIP"*".config/ghostty/themes/noctalia"* ]]
|
||||
[ -f "$HOME/.config/ghostty/themes/noctalia" ]
|
||||
[ ! -L "$HOME/.config/ghostty/themes/noctalia" ]
|
||||
}
|
||||
|
||||
@test "a diverged live palette is never quarantined and reverted to a stale copy" {
|
||||
# The dangerous shape: the tracked file matches today's palette, the theme changes
|
||||
# tomorrow, and sync would quarantine the live file and restore the stale one.
|
||||
repo_file "niri/.config/niri/noctalia.kdl" 'active-color "#e01b24"'
|
||||
live_file "$HOME/.config/niri/noctalia.kdl" 'active-color "#ff8c00"'
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
grep -q 'active-color "#ff8c00"' "$HOME/.config/niri/noctalia.kdl"
|
||||
[ ! -L "$HOME/.config/niri/noctalia.kdl" ]
|
||||
[ ! -d "$REPO/.quarantine" ]
|
||||
}
|
||||
|
||||
@test "an ordinary identical config is still adopted" {
|
||||
# Control against an exclusion so broad the tool stops deploying anything.
|
||||
repo_file "ghostty/.config/ghostty/config.ghostty" 'theme = "gruber"'
|
||||
live_file "$HOME/.config/ghostty/config.ghostty" 'theme = "gruber"'
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -L "$HOME/.config/ghostty/config.ghostty" ]
|
||||
[ "$(realpath "$HOME/.config/ghostty/config.ghostty")" = "$(realpath "$REPO/dotfiles/ghostty/.config/ghostty/config.ghostty")" ]
|
||||
}
|
||||
|
||||
@test "--check reports and writes nothing" {
|
||||
repo_file "ghostty/.config/ghostty/config.ghostty" 'theme = "gruber"'
|
||||
live_file "$HOME/.config/ghostty/config.ghostty" 'theme = "tokyo-night"'
|
||||
run "$REPO/scripts/deploy.sh" --check
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"DRIFT"* || "$output" == *"QUAR"* ]]
|
||||
[ ! -L "$HOME/.config/ghostty/config.ghostty" ]
|
||||
grep -q 'theme = "tokyo-night"' "$HOME/.config/ghostty/config.ghostty"
|
||||
[ ! -d "$REPO/.quarantine" ]
|
||||
}
|
||||
|
||||
@test "a unit already enabled is enabled but not started" {
|
||||
systemd_unit lock.service /bin/sh
|
||||
printf 'enabled\n' > "$UNIT_STATE"
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"not started"* ]]
|
||||
grep -q 'systemctl --user enable lock.service' "$STUB_LOG"
|
||||
! grep -q -- '--now' "$STUB_LOG"
|
||||
}
|
||||
|
||||
@test "a linked unit is not started either" {
|
||||
# `is-enabled` exits 1 for a linked unit, exactly as it does for a disabled one, so the
|
||||
# printed state is the signal — exit status cannot tell them apart.
|
||||
systemd_unit timer-driven.service /bin/sh
|
||||
printf 'linked\n' > "$UNIT_STATE"
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
! grep -q -- '--now' "$STUB_LOG"
|
||||
}
|
||||
|
||||
@test "a disabled unit is enabled and started, so a fresh box still comes up" {
|
||||
systemd_unit fresh.service /bin/sh
|
||||
printf 'disabled\n' > "$UNIT_STATE"
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
grep -q 'systemctl --user enable --now fresh.service' "$STUB_LOG"
|
||||
}
|
||||
|
||||
@test "a unit whose ExecStart is absent here stays inert" {
|
||||
systemd_unit otherbox.service /nonexistent/daemon
|
||||
printf 'disabled\n' > "$UNIT_STATE"
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"SKIP"*otherbox.service* ]]
|
||||
! grep -q enable "$STUB_LOG"
|
||||
}
|
||||
Reference in New Issue
Block a user