aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
authordanix <danix@danix.xyz>2026-06-11 09:40:46 +0200
committerdanix <danix@danix.xyz>2026-06-11 09:40:46 +0200
commitdf190f46cda798fac6e7d5c5045c2c4cd2f5041e (patch)
tree4801e42439febd9d5169c856a5d15f73930921c6 /docs/superpowers
parentbb61e5cc1cd36a161d1382735bbf6dacab6b3206 (diff)
downloadwallp-df190f46cda798fac6e7d5c5045c2c4cd2f5041e.tar.gz
wallp-df190f46cda798fac6e7d5c5045c2c4cd2f5041e.zip
docs: switch plan test harness to bats
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/plans/2026-06-11-wallp-merge.md575
1 files changed, 294 insertions, 281 deletions
diff --git a/docs/superpowers/plans/2026-06-11-wallp-merge.md b/docs/superpowers/plans/2026-06-11-wallp-merge.md
index 3b17c88..90c2772 100644
--- a/docs/superpowers/plans/2026-06-11-wallp-merge.md
+++ b/docs/superpowers/plans/2026-06-11-wallp-merge.md
@@ -4,39 +4,38 @@
**Goal:** Merge `multiwal.sh` (interactive setter) and `qar-lastwall.sh` (restorer) into one flag-driven bash script `wallp` with per-output partial updates, theme persistence, and a config file.
-**Architecture:** Single bash script `wallp`. Pure logic (conf parse, arg parse, `~` expansion, output mapping, theme resolution) lives in sourceable functions. Side-effecting actions (swaybg, wal, qarma, notify-send, filesystem writes) are isolated in thin functions. The script runs `main "$@"` only when executed directly, so a plain-bash test harness can source it and unit-test the pure functions with PATH stubs and a temp `$HOME`. Persistence is wallp-owned under `~/.config/wallp/`; a `~/.cache/wal/wpaper` symlink is maintained for rofi.
+**Architecture:** Single bash script `wallp`. Pure logic (conf parse, arg parse, `~` expansion, output mapping, theme resolution) lives in functions. Side-effecting actions (swaybg, wal, qarma, notify-send, filesystem writes) are isolated in thin functions. The script runs `main "$@"` only when executed directly, so bats tests can source it and exercise the functions with PATH stubs and a temp `$HOME`. Persistence is wallp-owned under `~/.config/wallp/`; a `~/.cache/wal/wpaper` symlink is maintained for rofi.
-**Tech Stack:** bash, swaybg, pywal (`wal`), qarma, notify-send. Tests: plain bash harness (no bats/shellcheck — not installed on this Slackware host), PATH stubs, temp `$HOME`.
+**Tech Stack:** bash, swaybg, pywal (`wal`), qarma, notify-send. Tests: bats 1.5.0 (installed), PATH stubs, temp `$HOME`. No bats-support/-assert helper libs — use core `run`/`$status`/`$output` + `[ ]`.
---
## Conventions
-- All tests live in `tests/`. The harness `tests/run.sh` sources `wallp`, sets `HOME` to a temp dir, prepends a stub dir to `PATH`, and runs `test_*` functions, printing PASS/FAIL and exiting non-zero on any failure.
+- Tests live in `tests/wallp.bats`. A shared `setup()` creates a temp `$HOME`, a stub `PATH` dir with stub `swaybg/wal/qarma/notify-send/pkill/kill` (each logs its call to `$HOME/calls.log` and exits 0), then `source`s `wallp`. `teardown()` removes the temp dir.
- `wallp` ends with:
```bash
if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi
```
- so sourcing it for tests does not run `main`.
-- Every function reads paths via variables seeded from `$HOME` at call time (no hardcoded `/home/danix`), so tests relocating `$HOME` work.
-- Commit after each task.
+ so sourcing it in bats does not run `main`.
+- Functions reference paths via `$HOME` at call time (no hardcoded `/home/danix`), so relocating `$HOME` in tests works.
+- Run the suite with `bats tests/wallp.bats`. Commit after each task.
## File Structure
- Create: `wallp` — the merged script (all logic + `main`).
- Create: `wallp.conf.example` — shipped example config.
-- Create: `tests/run.sh` — bash test harness + assertions, sources `wallp`.
-- Create: `tests/stubs/` — stub executables (`swaybg`, `wal`, `qarma`, `notify-send`) created by the harness at runtime (not committed; harness writes them to a temp dir).
-- Modify: `/home/danix/bin/wal.sh` — remove wal/wal2 copy lines + wpaper symlink (done late, after wallp owns it). Out of repo; handled in final task with explicit diff.
-- Delete (after wallp verified): `multiwal.sh`, `qar-lastwall.sh` — final task.
+- Create: `tests/wallp.bats` — bats test file with shared setup/teardown.
+- Modify: `/home/danix/bin/wal.sh` — trim wal/wal2/wpaper logic (final task; outside repo).
+- Delete (after wallp verified): `multiwal.sh`, `qar-lastwall.sh` (final task).
---
-## Task 1: Test harness skeleton
+## Task 1: bats setup/teardown + sourceable wallp
**Files:**
-- Create: `tests/run.sh`
-- Create: `wallp` (stub: just a shebang + sourcing guard so harness can source it)
+- Create: `wallp` (minimal sourceable stub)
+- Create: `tests/wallp.bats`
- [ ] **Step 1: Create minimal sourceable `wallp`**
@@ -55,26 +54,21 @@ main() {
if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi
```
-- [ ] **Step 2: Write the harness with one trivial test**
+- [ ] **Step 2: Create `tests/wallp.bats` with shared setup + one sanity test**
-Create `tests/run.sh`:
+Create `tests/wallp.bats`:
```bash
-#!/bin/bash
-# Plain-bash test harness for wallp. No bats dependency.
-set -u
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
-PASS=0
-FAIL=0
+#!/usr/bin/env bats
setup() {
+ SCRIPT_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
TMP_HOME="$(mktemp -d)"
+ export HOME="$TMP_HOME"
STUB_DIR="$TMP_HOME/stubs"
mkdir -p "$STUB_DIR"
- export HOME="$TMP_HOME"
export PATH="$STUB_DIR:$PATH"
- # default stubs: record calls to $TMP_HOME/calls.log, succeed
+ local bin
for bin in swaybg wal qarma notify-send pkill kill; do
cat > "$STUB_DIR/$bin" <<EOF
#!/bin/bash
@@ -83,56 +77,28 @@ exit 0
EOF
chmod +x "$STUB_DIR/$bin"
done
+ source "$SCRIPT_DIR/wallp"
}
teardown() {
rm -rf "$TMP_HOME"
}
-assert_eq() { # expected actual msg
- if [ "$1" = "$2" ]; then PASS=$((PASS+1));
- else FAIL=$((FAIL+1)); echo "FAIL: $3 (expected '$1' got '$2')"; fi
-}
-assert_file() { # path msg
- if [ -f "$1" ]; then PASS=$((PASS+1));
- else FAIL=$((FAIL+1)); echo "FAIL: $2 (missing file $1)"; fi
-}
-assert_no_file() { # path msg
- if [ ! -e "$1" ]; then PASS=$((PASS+1));
- else FAIL=$((FAIL+1)); echo "FAIL: $2 (file exists $1)"; fi
-}
-assert_contains() { # haystack needle msg
- case "$1" in *"$2"*) PASS=$((PASS+1));; *) FAIL=$((FAIL+1)); echo "FAIL: $3 (no '$2' in '$1')";; esac
-}
-
-run_all() {
- for t in $(declare -F | awk '{print $3}' | grep '^test_'); do
- setup
- source "$SCRIPT_DIR/wallp"
- "$t"
- teardown
- done
- echo "PASS=$PASS FAIL=$FAIL"
- [ "$FAIL" -eq 0 ]
-}
-
-test_harness_self() {
- assert_eq "x" "x" "harness sanity"
+@test "harness sanity" {
+ [ "x" = "x" ]
}
-
-run_all
```
-- [ ] **Step 3: Run harness, verify it passes**
+- [ ] **Step 3: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `PASS=1 FAIL=0`, exit 0.
+Run: `bats tests/wallp.bats`
+Expected: `1 test, 0 failures`.
- [ ] **Step 4: Commit**
```bash
-git add wallp tests/run.sh
-git commit -m "test: bash harness skeleton + sourceable wallp stub"
+git add wallp tests/wallp.bats
+git commit -m "test: bats setup/teardown + sourceable wallp stub"
```
---
@@ -141,25 +107,29 @@ git commit -m "test: bash harness skeleton + sourceable wallp stub"
**Files:**
- Modify: `wallp` (add `expand_tilde`)
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
-- [ ] **Step 1: Write failing test**
+- [ ] **Step 1: Write failing tests**
-Add to `tests/run.sh` before `run_all`:
+Append to `tests/wallp.bats`:
```bash
-test_expand_tilde_home() {
- assert_eq "$HOME/pics/a.png" "$(expand_tilde '~/pics/a.png')" "leading tilde expands"
+@test "expand_tilde expands leading tilde" {
+ run expand_tilde '~/pics/a.png'
+ [ "$status" -eq 0 ]
+ [ "$output" = "$HOME/pics/a.png" ]
}
-test_expand_tilde_noop() {
- assert_eq "/abs/b.png" "$(expand_tilde '/abs/b.png')" "absolute path untouched"
+
+@test "expand_tilde leaves absolute path untouched" {
+ run expand_tilde '/abs/b.png'
+ [ "$output" = "/abs/b.png" ]
}
```
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
-Expected: FAIL — `expand_tilde: command not found` / failed asserts.
+Run: `bats tests/wallp.bats`
+Expected: FAIL — `expand_tilde: command not found`.
- [ ] **Step 3: Implement**
@@ -178,13 +148,13 @@ expand_tilde() {
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: PASS count up, `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: expand_tilde helper"
```
@@ -196,14 +166,14 @@ Parses `key=value` lines (ignore blanks + `#`), expands `~` in path values, sets
**Files:**
- Modify: `wallp` (add `parse_conf`)
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing test**
-Add to `tests/run.sh`:
+Append to `tests/wallp.bats`:
```bash
-test_parse_conf_reads_keys() {
+@test "parse_conf reads keys and expands tilde in paths" {
conf="$HOME/c.conf"
printf '%s\n' \
'# comment' '' \
@@ -213,17 +183,17 @@ test_parse_conf_reads_keys() {
'DEFAULT_H=~/p/h.png' \
'DEFAULT_V=/abs/v.png' > "$conf"
parse_conf "$conf"
- assert_eq "mytheme" "$CONF_THEME" "theme"
- assert_eq "DP-1" "$CONF_OUTPUT_H" "output_h"
- assert_eq "DP-3" "$CONF_OUTPUT_V" "output_v"
- assert_eq "$HOME/p/h.png" "$CONF_DEFAULT_H" "default_h tilde expanded"
- assert_eq "/abs/v.png" "$CONF_DEFAULT_V" "default_v abs"
+ [ "$CONF_THEME" = "mytheme" ]
+ [ "$CONF_OUTPUT_H" = "DP-1" ]
+ [ "$CONF_OUTPUT_V" = "DP-3" ]
+ [ "$CONF_DEFAULT_H" = "$HOME/p/h.png" ]
+ [ "$CONF_DEFAULT_V" = "/abs/v.png" ]
}
```
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
+Run: `bats tests/wallp.bats`
Expected: FAIL — `parse_conf: command not found`.
- [ ] **Step 3: Implement**
@@ -253,13 +223,13 @@ parse_conf() {
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: parse_conf key=value config loader"
```
@@ -267,45 +237,48 @@ git commit -m "feat: parse_conf key=value config loader"
## Task 4: Conf bootstrap + required-key validation
-`load_conf` orchestrates: if conf missing → write template, message, signal "bootstrapped" (caller exits 0); else parse + validate required path/output keys (hard error). THEME falls back to `sexy-splurge` if empty.
+`load_conf`: if conf missing → write template, message, return 10 (caller exits 0); else parse + validate required path/output keys (return 1 on miss). THEME falls back to `sexy-splurge` if empty.
**Files:**
- Modify: `wallp` (add `conf_path`, `write_conf_template`, `require_conf_keys`, `load_conf`)
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing tests**
-Add to `tests/run.sh`:
-
```bash
-test_load_conf_missing_bootstraps() {
- out="$(load_conf 2>&1)"; rc=$?
- assert_eq "10" "$rc" "missing conf returns rc 10 (bootstrapped)"
- assert_file "$HOME/.config/wallp/wallp.conf" "template written"
- assert_contains "$out" "fill it in" "user told to fill conf"
+@test "load_conf bootstraps template when conf missing" {
+ run load_conf
+ [ "$status" -eq 10 ]
+ [ -f "$HOME/.config/wallp/wallp.conf" ]
+ [[ "$output" == *"fill it in"* ]]
}
-test_load_conf_missing_key_hard_errors() {
+
+@test "load_conf hard-errors on missing required key" {
mkdir -p "$HOME/.config/wallp"
printf '%s\n' 'OUTPUT_H=DP-1' 'OUTPUT_V=DP-3' 'DEFAULT_H=/a.png' \
> "$HOME/.config/wallp/wallp.conf" # DEFAULT_V missing
- out="$(load_conf 2>&1)"; rc=$?
- assert_eq "1" "$rc" "missing key rc 1"
- assert_contains "$out" "DEFAULT_V" "names missing key"
+ run load_conf
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"DEFAULT_V"* ]]
}
-test_load_conf_ok_theme_fallback() {
+
+@test "load_conf ok with THEME fallback" {
mkdir -p "$HOME/.config/wallp"
printf '%s\n' 'OUTPUT_H=DP-1' 'OUTPUT_V=DP-3' \
'DEFAULT_H=/a.png' 'DEFAULT_V=/b.png' \
> "$HOME/.config/wallp/wallp.conf" # no THEME
- load_conf; rc=$?
- assert_eq "0" "$rc" "valid conf rc 0"
- assert_eq "sexy-splurge" "$CONF_THEME" "theme fallback"
+ load_conf
+ [ "$?" -eq 0 ]
+ [ "$CONF_THEME" = "sexy-splurge" ]
}
```
+Note: the third test calls `load_conf` directly (not via `run`) because it must
+assert on `CONF_THEME`, which `run` would not propagate (subshell).
+
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
+Run: `bats tests/wallp.bats`
Expected: FAIL — `load_conf: command not found`.
- [ ] **Step 3: Implement**
@@ -333,9 +306,9 @@ EOF
# Hard-error if any required path/output key is empty. Returns 1 on first miss.
require_conf_keys() {
- local k
+ local k var
for k in OUTPUT_H OUTPUT_V DEFAULT_H DEFAULT_V; do
- local var="CONF_$k"
+ var="CONF_$k"
if [ -z "${!var}" ]; then
echo "wallp: required config key '$k' is missing or empty in $(conf_path)" >&2
return 1
@@ -361,15 +334,18 @@ load_conf() {
}
```
+Note: `load_conf`'s message and the `notify-send` text both contain "fill it in"
+so the bootstrap test matches regardless of whether notify-send exists.
+
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: conf bootstrap + required-key validation"
```
@@ -381,34 +357,40 @@ git commit -m "feat: conf bootstrap + required-key validation"
**Files:**
- Modify: `wallp` (add `theme_file`, `persist_theme`, `resolve_theme`)
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing tests**
```bash
-test_resolve_theme_flag_wins() {
+@test "resolve_theme flag wins over all" {
CONF_THEME="conf"; mkdir -p "$HOME/.config/wallp"
echo persisted > "$HOME/.config/wallp/theme"
- assert_eq "flagged" "$(resolve_theme flagged)" "flag beats all"
+ run resolve_theme flagged
+ [ "$output" = "flagged" ]
}
-test_resolve_theme_persisted_beats_conf() {
+
+@test "resolve_theme persisted beats conf" {
CONF_THEME="conf"; mkdir -p "$HOME/.config/wallp"
echo persisted > "$HOME/.config/wallp/theme"
- assert_eq "persisted" "$(resolve_theme '')" "persisted beats conf"
+ run resolve_theme ''
+ [ "$output" = "persisted" ]
}
-test_resolve_theme_conf_when_no_persist() {
+
+@test "resolve_theme uses conf when no persisted" {
CONF_THEME="conf"
- assert_eq "conf" "$(resolve_theme '')" "conf used"
+ run resolve_theme ''
+ [ "$output" = "conf" ]
}
-test_persist_theme_writes() {
+
+@test "persist_theme writes the file" {
persist_theme "abc"
- assert_eq "abc" "$(cat "$HOME/.config/wallp/theme")" "theme persisted"
+ [ "$(cat "$HOME/.config/wallp/theme")" = "abc" ]
}
```
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
+Run: `bats tests/wallp.bats`
Expected: FAIL — functions not defined.
- [ ] **Step 3: Implement**
@@ -434,47 +416,55 @@ resolve_theme() {
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: theme resolution + persistence"
```
---
-## Task 6: Logical→physical output mapping + persistence paths
+## Task 6: Logical→physical output mapping + state paths
-Map `H`/`V` to `CONF_OUTPUT_H`/`CONF_OUTPUT_V`; provide persistence-path and pid-path helpers.
+Map `H`/`V` to `CONF_OUTPUT_H`/`CONF_OUTPUT_V`; provide wallpaper-persistence and pid path helpers.
**Files:**
- Modify: `wallp`
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing tests**
```bash
-test_output_for_logical() {
+@test "output_for maps logical to physical" {
CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"
- assert_eq "DP-1" "$(output_for H)" "H maps"
- assert_eq "DP-3" "$(output_for V)" "V maps"
+ run output_for H
+ [ "$output" = "DP-1" ]
+ run output_for V
+ [ "$output" = "DP-3" ]
}
-test_wall_file_for() {
- assert_eq "$HOME/.config/wallp/wall_h" "$(wall_file_for H)" "wall_h path"
- assert_eq "$HOME/.config/wallp/wall_v" "$(wall_file_for V)" "wall_v path"
+
+@test "wall_file_for returns config paths" {
+ run wall_file_for H
+ [ "$output" = "$HOME/.config/wallp/wall_h" ]
+ run wall_file_for V
+ [ "$output" = "$HOME/.config/wallp/wall_v" ]
}
-test_pid_file_for() {
- assert_eq "$HOME/.cache/wallp/H.pid" "$(pid_file_for H)" "H pid path"
- assert_eq "$HOME/.cache/wallp/V.pid" "$(pid_file_for V)" "V pid path"
+
+@test "pid_file_for returns cache paths" {
+ run pid_file_for H
+ [ "$output" = "$HOME/.cache/wallp/H.pid" ]
+ run pid_file_for V
+ [ "$output" = "$HOME/.cache/wallp/V.pid" ]
}
```
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
+Run: `bats tests/wallp.bats`
Expected: FAIL — functions not defined.
- [ ] **Step 3: Implement**
@@ -503,13 +493,13 @@ pid_file_for() {
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: output mapping + state path helpers"
```
@@ -517,43 +507,50 @@ git commit -m "feat: output mapping + state path helpers"
## Task 7: `--set H=/V=` argument parsing
-`parse_set_args` consumes `H=<file>` / `V=<file>` tokens into associative-style globals `SET_H`/`SET_V`; rejects unknown tokens.
+`parse_set_args` consumes `H=<file>` / `V=<file>` tokens into globals `SET_H`/`SET_V`; rejects unknown tokens.
**Files:**
- Modify: `wallp`
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing tests**
```bash
-test_parse_set_args_both() {
+@test "parse_set_args captures both" {
SET_H="" SET_V=""
parse_set_args "H=/a.png" "V=/b.png"
- assert_eq "/a.png" "$SET_H" "H captured"
- assert_eq "/b.png" "$SET_V" "V captured"
+ [ "$SET_H" = "/a.png" ]
+ [ "$SET_V" = "/b.png" ]
}
-test_parse_set_args_partial() {
+
+@test "parse_set_args partial leaves other empty" {
SET_H="" SET_V=""
parse_set_args "V=/only.png"
- assert_eq "" "$SET_H" "H empty"
- assert_eq "/only.png" "$SET_V" "V captured"
+ [ "$SET_H" = "" ]
+ [ "$SET_V" = "/only.png" ]
}
-test_parse_set_args_tilde() {
+
+@test "parse_set_args expands tilde" {
SET_H="" SET_V=""
parse_set_args "H=~/x.png"
- assert_eq "$HOME/x.png" "$SET_H" "H tilde expanded"
+ [ "$SET_H" = "$HOME/x.png" ]
}
-test_parse_set_args_unknown_rejected() {
+
+@test "parse_set_args rejects unknown token" {
SET_H="" SET_V=""
- out="$(parse_set_args "Z=/bad.png" 2>&1)"; rc=$?
- assert_eq "1" "$rc" "unknown token rc1"
- assert_contains "$out" "Z=" "names bad token"
+ run parse_set_args "Z=/bad.png"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Z="* ]]
}
```
+Note: the first three call `parse_set_args` directly (not `run`) so the `SET_*`
+globals survive into the assertions. The rejection test uses `run` (checks
+status/output, not globals).
+
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
+Run: `bats tests/wallp.bats`
Expected: FAIL — `parse_set_args: command not found`.
- [ ] **Step 3: Implement**
@@ -575,13 +572,13 @@ parse_set_args() {
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: parse --set H=/V= arguments"
```
@@ -589,44 +586,48 @@ git commit -m "feat: parse --set H=/V= arguments"
## Task 8: Apply a single wallpaper (selective kill + swaybg + persist)
-`apply_output <H|V> <file>`: validate file exists; selective-kill prior PID; launch swaybg; save new PID; persist path to `wall_h`/`wall_v`. Returns 1 (skip) on missing file without blanking.
+`apply_output <H|V> <file>`: validate file exists; selective-kill prior PID; launch swaybg; save new PID; persist path. Returns 1 (skip) on missing file without blanking.
**Files:**
- Modify: `wallp`
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing tests**
```bash
-test_apply_output_missing_file_skips() {
+@test "apply_output skips missing file without writing pid" {
CONF_OUTPUT_H="DP-1"
- out="$(apply_output H "$HOME/nope.png" 2>&1)"; rc=$?
- assert_eq "1" "$rc" "missing file rc1"
- assert_no_file "$(pid_file_for H)" "no pid written on skip"
+ run apply_output H "$HOME/nope.png"
+ [ "$status" -eq 1 ]
+ [ ! -e "$HOME/.cache/wallp/H.pid" ]
}
-test_apply_output_sets_and_persists() {
+
+@test "apply_output sets swaybg and persists path" {
CONF_OUTPUT_H="DP-1"
: > "$HOME/real.png"
- apply_output H "$HOME/real.png"; rc=$?
- assert_eq "0" "$rc" "apply ok"
- assert_eq "$HOME/real.png" "$(cat "$(wall_file_for H)")" "path persisted"
- assert_file "$(pid_file_for H)" "pid written"
- assert_contains "$(cat "$HOME/calls.log")" "swaybg -o DP-1 -i $HOME/real.png" "swaybg launched"
+ apply_output H "$HOME/real.png"
+ [ "$?" -eq 0 ]
+ [ "$(cat "$HOME/.config/wallp/wall_h")" = "$HOME/real.png" ]
+ [ -f "$HOME/.cache/wallp/H.pid" ]
+ grep -q "swaybg -o DP-1 -i $HOME/real.png" "$HOME/calls.log"
}
-test_apply_output_v_does_not_touch_h_pid() {
- CONF_OUTPUT_H="DP-1" CONF_OUTPUT_V="DP-3"
- mkdir -p "$HOME/.cache/wallp"; echo 99999 > "$(pid_file_for H)"
+
+@test "apply_output on V does not touch H pid" {
+ CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"
+ mkdir -p "$HOME/.cache/wallp"; echo 99999 > "$HOME/.cache/wallp/H.pid"
: > "$HOME/v.png"
apply_output V "$HOME/v.png"
- assert_eq "99999" "$(cat "$(pid_file_for H)")" "H pid untouched on V update"
+ [ "$(cat "$HOME/.cache/wallp/H.pid")" = "99999" ]
}
```
-Note: the harness `swaybg` stub exits 0 immediately, so `$!` is a real (already-exited) PID — fine for assertions. The `kill` stub records and exits 0.
+Note: tests 2 and 3 call `apply_output` directly (need `$?` / side effects in the
+same shell). The `swaybg` stub exits 0 immediately, so `$!` is a real
+already-exited PID — fine for the file-write assertions. `kill` stub exits 0.
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
+Run: `bats tests/wallp.bats`
Expected: FAIL — `apply_output: command not found`.
- [ ] **Step 3: Implement**
@@ -661,15 +662,19 @@ apply_output() {
}
```
+Note: in the bats stub environment `kill -0 99999` returns non-zero (no such
+process), so `kill_output` won't invoke the `kill` stub — the V-update test's H
+pid stays `99999` as asserted.
+
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: apply_output with selective kill + persistence"
```
@@ -677,32 +682,33 @@ git commit -m "feat: apply_output with selective kill + persistence"
## Task 9: wpaper symlink + theme application
-`update_wpaper`: symlink `~/.cache/wal/wpaper` → current H image (read from `wall_h`). `apply_theme <theme>`: persist theme, run wal, notify. `finalize <theme>`: wpaper + apply_theme.
+`update_wpaper`: symlink `~/.cache/wal/wpaper` → current H image (from `wall_h`). `apply_theme <theme>`: persist theme, run wal, notify. `finalize <theme>`: wpaper + apply_theme.
**Files:**
- Modify: `wallp`
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing tests**
```bash
-test_update_wpaper_symlinks_h() {
- : > "$HOME/h.png"; mkdir -p "$(dirname "$(wall_file_for H)")"
- echo "$HOME/h.png" > "$(wall_file_for H)"
+@test "update_wpaper symlinks to H image" {
+ : > "$HOME/h.png"; mkdir -p "$HOME/.config/wallp"
+ echo "$HOME/h.png" > "$HOME/.config/wallp/wall_h"
update_wpaper
- assert_eq "$HOME/h.png" "$(readlink "$HOME/.cache/wal/wpaper")" "wpaper -> H image"
+ [ "$(readlink "$HOME/.cache/wal/wpaper")" = "$HOME/h.png" ]
}
-test_apply_theme_runs_wal_and_persists() {
+
+@test "apply_theme runs wal and persists theme" {
apply_theme "mytheme"
- assert_eq "mytheme" "$(cat "$(theme_file)")" "theme persisted"
- assert_contains "$(cat "$HOME/calls.log")" "wal --backend colorz -nq --theme mytheme -o $HOME/bin/wal.sh" "wal invoked"
- assert_contains "$(cat "$HOME/calls.log")" "notify-send" "notify sent"
+ [ "$(cat "$HOME/.config/wallp/theme")" = "mytheme" ]
+ grep -q "wal --backend colorz -nq --theme mytheme -o $HOME/bin/wal.sh" "$HOME/calls.log"
+ grep -q "notify-send" "$HOME/calls.log"
}
```
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
+Run: `bats tests/wallp.bats`
Expected: FAIL — functions not defined.
- [ ] **Step 3: Implement**
@@ -733,13 +739,13 @@ finalize() {
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: wpaper symlink + theme application"
```
@@ -747,39 +753,40 @@ git commit -m "feat: wpaper symlink + theme application"
## Task 10: Restore action
-`do_restore <flag_theme>`: for H and V, use persisted `wall_h`/`wall_v` if present else `CONF_DEFAULT_*` (persisting the default); apply_output each; finalize with resolved theme.
+`do_restore <flag_theme>`: for H and V, use persisted `wall_h`/`wall_v` if present else `CONF_DEFAULT_*` (persisting the default via apply_output); finalize with resolved theme.
**Files:**
- Modify: `wallp`
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing tests**
```bash
-test_restore_uses_persisted() {
- CONF_OUTPUT_H="DP-1" CONF_OUTPUT_V="DP-3" CONF_THEME="conf"
- CONF_DEFAULT_H="$HOME/dh.png" CONF_DEFAULT_V="$HOME/dv.png"
+@test "restore uses persisted wallpapers" {
+ CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
+ CONF_DEFAULT_H="$HOME/dh.png"; CONF_DEFAULT_V="$HOME/dv.png"
: > "$HOME/saved_h.png"; : > "$HOME/saved_v.png"
mkdir -p "$HOME/.config/wallp"
- echo "$HOME/saved_h.png" > "$(wall_file_for H)"
- echo "$HOME/saved_v.png" > "$(wall_file_for V)"
+ echo "$HOME/saved_h.png" > "$HOME/.config/wallp/wall_h"
+ echo "$HOME/saved_v.png" > "$HOME/.config/wallp/wall_v"
do_restore ""
- assert_contains "$(cat "$HOME/calls.log")" "swaybg -o DP-1 -i $HOME/saved_h.png" "H restored from saved"
- assert_contains "$(cat "$HOME/calls.log")" "swaybg -o DP-3 -i $HOME/saved_v.png" "V restored from saved"
+ grep -q "swaybg -o DP-1 -i $HOME/saved_h.png" "$HOME/calls.log"
+ grep -q "swaybg -o DP-3 -i $HOME/saved_v.png" "$HOME/calls.log"
}
-test_restore_falls_back_to_defaults() {
- CONF_OUTPUT_H="DP-1" CONF_OUTPUT_V="DP-3" CONF_THEME="conf"
+
+@test "restore falls back to conf defaults" {
+ CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
: > "$HOME/dh.png"; : > "$HOME/dv.png"
- CONF_DEFAULT_H="$HOME/dh.png" CONF_DEFAULT_V="$HOME/dv.png"
+ CONF_DEFAULT_H="$HOME/dh.png"; CONF_DEFAULT_V="$HOME/dv.png"
do_restore ""
- assert_contains "$(cat "$HOME/calls.log")" "swaybg -o DP-1 -i $HOME/dh.png" "H default used"
- assert_eq "$HOME/dh.png" "$(cat "$(wall_file_for H)")" "default persisted"
+ grep -q "swaybg -o DP-1 -i $HOME/dh.png" "$HOME/calls.log"
+ [ "$(cat "$HOME/.config/wallp/wall_h")" = "$HOME/dh.png" ]
}
```
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
+Run: `bats tests/wallp.bats`
Expected: FAIL — `do_restore: command not found`.
- [ ] **Step 3: Implement**
@@ -807,13 +814,13 @@ do_restore() {
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: restore action (saved -> default fallback)"
```
@@ -821,40 +828,42 @@ git commit -m "feat: restore action (saved -> default fallback)"
## Task 11: Set action (CLI args path; qarma path isolated)
-`do_set <flag_theme> [H=.. V=..]`: parse args; if neither given and display present → `qarma_select` (separate fn, stubbed in tests); apply each provided; finalize. `qarma_select` populates `SET_H`/`SET_V` via the qarma menu+pickers — isolated so tests drive only the CLI path.
+`do_set <flag_theme> [H=.. V=..]`: parse args; if neither given and display present → `qarma_select`; apply each provided; finalize. `qarma_select` populates `SET_H`/`SET_V` via qarma — isolated so tests drive only the CLI path.
**Files:**
- Modify: `wallp`
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing tests**
```bash
-test_set_cli_partial_only_v() {
- CONF_OUTPUT_H="DP-1" CONF_OUTPUT_V="DP-3" CONF_THEME="conf"
+@test "set CLI partial sets only V" {
+ CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
: > "$HOME/v.png"
do_set "" "V=$HOME/v.png"
- assert_contains "$(cat "$HOME/calls.log")" "swaybg -o DP-3 -i $HOME/v.png" "V set"
- case "$(cat "$HOME/calls.log")" in *"swaybg -o DP-1"*) echo "FAIL: H should not be set"; FAIL=$((FAIL+1));; *) PASS=$((PASS+1));; esac
+ grep -q "swaybg -o DP-3 -i $HOME/v.png" "$HOME/calls.log"
+ ! grep -q "swaybg -o DP-1" "$HOME/calls.log"
}
-test_set_cli_theme_flag_persisted() {
- CONF_OUTPUT_H="DP-1" CONF_OUTPUT_V="DP-3" CONF_THEME="conf"
+
+@test "set CLI persists flag theme" {
+ CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
: > "$HOME/h.png"
do_set "flagtheme" "H=$HOME/h.png"
- assert_eq "flagtheme" "$(cat "$(theme_file)")" "flag theme persisted"
+ [ "$(cat "$HOME/.config/wallp/theme")" = "flagtheme" ]
}
-test_set_no_args_no_display_errors() {
- CONF_OUTPUT_H="DP-1" CONF_OUTPUT_V="DP-3" CONF_THEME="conf"
+
+@test "set with no args and no display errors" {
+ CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
unset WAYLAND_DISPLAY
- out="$(do_set "" 2>&1)"; rc=$?
- assert_eq "1" "$rc" "no display no args rc1"
- assert_contains "$out" "no display" "error mentions display"
+ run do_set ""
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"no display"* ]]
}
```
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
+Run: `bats tests/wallp.bats`
Expected: FAIL — `do_set: command not found`.
- [ ] **Step 3: Implement**
@@ -892,21 +901,21 @@ do_set() {
return 1
fi
fi
- [ -n "$SET_H" ] && apply_output H "$SET_H" || true
- [ -n "$SET_V" ] && apply_output V "$SET_V" || true
+ [ -n "$SET_H" ] && { apply_output H "$SET_H" || true; }
+ [ -n "$SET_V" ] && { apply_output V "$SET_V" || true; }
finalize "$(resolve_theme "$flag_theme")"
}
```
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: set action (CLI + qarma selection)"
```
@@ -914,50 +923,55 @@ git commit -m "feat: set action (CLI + qarma selection)"
## Task 12: Help screen + argument dispatch (`main`)
-`show_help` (qarma window if display, else stdout). `main` parses top-level flags `--set --restore --theme <name> --help/-h`, calls `load_conf` (honoring bootstrap rc 10 → exit 0), dispatches.
+`show_help` (qarma window if display, else stdout). `main` parses `--set --restore --theme <name> --help/-h` plus `H=`/`V=` tokens, calls `load_conf` (rc 10 → exit 0), dispatches.
**Files:**
- Modify: `wallp` (replace stub `main`, add `show_help`)
-- Test: `tests/run.sh`
+- Test: `tests/wallp.bats`
- [ ] **Step 1: Write failing tests**
```bash
-test_main_bare_shows_help() {
+@test "bare invocation shows help" {
unset WAYLAND_DISPLAY
- out="$(main 2>&1)"; rc=$?
- assert_eq "0" "$rc" "help rc0"
- assert_contains "$out" "Usage" "help text shown"
+ run main
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"Usage"* ]]
}
-test_main_unknown_flag_errors() {
- out="$(main --bogus 2>&1)"; rc=$?
- assert_eq "1" "$rc" "unknown flag rc1"
- assert_contains "$out" "Usage" "help shown on error"
+
+@test "unknown flag errors with help" {
+ unset WAYLAND_DISPLAY
+ run main --bogus
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Usage"* ]]
}
-test_main_bootstrap_exits_zero() {
- # no conf present -> bootstrap, no wallpaper change
- out="$(main --restore 2>&1)"; rc=$?
- assert_eq "0" "$rc" "bootstrap rc0"
- assert_no_file "$(pid_file_for H)" "no wallpaper set during bootstrap"
+
+@test "bootstrap on missing conf exits zero without setting wallpaper" {
+ unset WAYLAND_DISPLAY
+ run main --restore
+ [ "$status" -eq 0 ]
+ [ ! -e "$HOME/.cache/wallp/H.pid" ]
}
-test_main_theme_flag_parsed_with_restore() {
+
+@test "theme flag honored through main with restore" {
+ unset WAYLAND_DISPLAY
mkdir -p "$HOME/.config/wallp"
printf '%s\n' 'OUTPUT_H=DP-1' 'OUTPUT_V=DP-3' \
'DEFAULT_H=/dh.png' 'DEFAULT_V=/dv.png' > "$HOME/.config/wallp/wallp.conf"
- : > /dh.png 2>/dev/null || true # may be unwritable; rely on default-skip
- main --restore --theme tflag >/dev/null 2>&1
- assert_eq "tflag" "$(cat "$(theme_file)")" "theme flag honored via main"
+ main --restore --theme tflag
+ [ "$(cat "$HOME/.config/wallp/theme")" = "tflag" ]
}
```
-Note: `test_main_theme_flag_parsed_with_restore` uses `/dh.png` which likely
-does not exist; `apply_output` will skip (rc1, `|| true`) but `finalize` still
-runs and persists the theme — which is what the assertion checks.
+Note: the 4th test's defaults `/dh.png` `/dv.png` don't exist, so `apply_output`
+skips both (rc1, swallowed), but `finalize` still runs and persists `tflag` —
+which is what the assertion checks. It calls `main` directly (not `run`) so the
+persisted file is written in the test's shell.
- [ ] **Step 2: Run, verify fail**
-Run: `bash tests/run.sh`
-Expected: FAIL — `main` is still the stub returning 0 with no help.
+Run: `bats tests/wallp.bats`
+Expected: FAIL — stub `main` returns 0 with no "Usage" output; bootstrap/theme tests fail.
- [ ] **Step 3: Implement** (replace the stub `main`)
@@ -1011,15 +1025,20 @@ main() {
}
```
+Note: `show_help` writes to stdout when no display; under bats `WAYLAND_DISPLAY`
+is unset in these tests, so `run main` captures "Usage" in `$output`. `set_args`
+may be empty; `"${set_args[@]}"` under `set -u` is safe in bash ≥ 4.4 (and the
+stub `wallp` already uses `set -u`).
+
- [ ] **Step 4: Run, verify pass**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`.
+Run: `bats tests/wallp.bats`
+Expected: `0 failures`.
- [ ] **Step 5: Commit**
```bash
-git add wallp tests/run.sh
+git add wallp tests/wallp.bats
git commit -m "feat: help screen + main dispatch"
```
@@ -1029,7 +1048,7 @@ git commit -m "feat: help screen + main dispatch"
**Files:**
- Create: `wallp.conf.example`
-- Modify: `wallp` (ensure executable bit)
+- Modify: file mode of `wallp`
- [ ] **Step 1: Create `wallp.conf.example`**
@@ -1047,17 +1066,17 @@ DEFAULT_V=~/Pictures/wallpapers/SFW/vertical.png
- [ ] **Step 2: Make wallp executable**
-Run: `chmod +x wallp tests/run.sh`
+Run: `chmod +x wallp`
- [ ] **Step 3: Verify full suite green**
-Run: `bash tests/run.sh`
-Expected: `FAIL=0`, exit 0.
+Run: `bats tests/wallp.bats`
+Expected: all tests pass, `0 failures`.
- [ ] **Step 4: Commit**
```bash
-git add wallp wallp.conf.example tests/run.sh
+git add wallp wallp.conf.example
git commit -m "chore: ship example config, make wallp executable"
```
@@ -1065,29 +1084,26 @@ git commit -m "chore: ship example config, make wallp executable"
## Task 14: Manual smoke test on real two-screen setup
-No automated test — real hardware. Document the run; do not commit code unless a bug is found (then add a regression test first).
+No automated test — real hardware. If a bug is found, add a failing bats test first, then fix.
- [ ] **Step 1: Backup current state**
-Run:
-```bash
-cp -a ~/.config/wallp ~/.config/wallp.bak 2>/dev/null || true
-```
+Run: `cp -a ~/.config/wallp ~/.config/wallp.bak 2>/dev/null || true`
- [ ] **Step 2: First run bootstraps conf**
Run: `./wallp --restore`
-Expected: message "generated config at …"; conf file created; **no wallpaper change**. Edit the conf with real output names + default wallpaper paths.
+Expected: "generated config at …" message; conf created; **no wallpaper change**. Edit conf with real outputs + default wallpaper paths.
- [ ] **Step 3: Restore with defaults**
Run: `./wallp --restore`
-Expected: both screens show the configured defaults; theme applied; notification appears.
+Expected: both screens show configured defaults; theme applied; notification appears.
- [ ] **Step 4: Partial CLI set (V only) does not blank H**
-Run: `./wallp --set V=/path/to/some/vertical.png`
-Expected: vertical screen changes; **horizontal stays as-is** (not black).
+Run: `./wallp --set V=/path/to/vertical.png`
+Expected: vertical changes; **horizontal stays** (not black).
- [ ] **Step 5: qarma flow**
@@ -1101,7 +1117,7 @@ Run:
./wallp --set H=/some/h.png --theme some-other-theme
./wallp --restore
```
-Expected: after restore, `some-other-theme` is reapplied (persisted theme wins), not the conf default.
+Expected: after restore, `some-other-theme` reapplied (persisted wins), not conf default.
- [ ] **Step 7: Help**
@@ -1116,7 +1132,7 @@ Only after Task 14 passes. `wal.sh` is outside the repo (`/home/danix/bin/wal.sh
- [ ] **Step 1: Trim `wal.sh`**
-Edit `/home/danix/bin/wal.sh` — remove the wallpaper-pointer + wpaper logic (old lines 15-27), keeping only dunst + kitty glue. Resulting file:
+Edit `/home/danix/bin/wal.sh` — remove the wallpaper-pointer + wpaper logic (old lines 15-27), keeping only dunst + kitty glue. Result:
```sh
#!/bin/sh
@@ -1134,21 +1150,18 @@ dunst &
ln -sf ~/.cache/wal/colors-kitty.conf ~/.config/kitty/current-theme.conf
```
-- [ ] **Step 2: Verify wpaper still maintained**
+- [ ] **Step 2: Verify wpaper still maintained by wallp**
Run: `./wallp --restore && readlink ~/.cache/wal/wpaper`
-Expected: symlink points at current H image (wallp now owns it). Confirm rofi menus still themed.
+Expected: symlink points at current H image. Confirm rofi menus still themed.
- [ ] **Step 3: Remove superseded scripts**
-Run:
-```bash
-git rm multiwal.sh qar-lastwall.sh
-```
+Run: `git rm multiwal.sh qar-lastwall.sh`
-- [ ] **Step 4: Update any autostart references**
+- [ ] **Step 4: Repoint autostart**
-Manually check the wayland/hyprland autostart (e.g. hyprland config `exec-once`) that called `qar-lastwall.sh` — repoint to `wallp --restore`. Document the file edited in the commit body.
+Manually check wayland/hyprland autostart (e.g. hyprland `exec-once`) that called `qar-lastwall.sh` — repoint to `wallp --restore`. Note the edited file in the commit body.
- [ ] **Step 5: Commit**
@@ -1161,6 +1174,6 @@ git commit -m "chore: cut over to wallp, remove multiwal.sh + qar-lastwall.sh"
## Self-Review Notes
-- Spec coverage: CLI forms (T7,T11,T12), conf bootstrap + hard-error (T4), logical H/V mapping (T6), theme persistence + precedence (T5, asserted via T11/T12), selective per-output kill / no-blank partial (T8, asserted T11), wpaper kept for rofi (T9,T15), drop wal/wal2 (no writes anywhere in plan — verified), restore saved→default (T10), help (T12), error handling (T4,T7,T8,T11,T12). All covered.
-- Naming consistency: `apply_output`, `output_for`, `wall_file_for`, `pid_file_for`, `resolve_theme`, `persist_theme`, `do_set`, `do_restore`, `finalize`, `update_wpaper`, `apply_theme`, `parse_conf`, `load_conf`, `parse_set_args` — used consistently across tasks.
-- No placeholders: all steps carry real code/commands/expected output.
+- Spec coverage: CLI forms (T7,T11,T12), conf bootstrap + hard-error (T4), logical H/V mapping (T6), theme persistence + precedence (T5, asserted T11/T12), selective per-output kill / no-blank partial (T8, asserted T11), wpaper kept for rofi (T9,T15), drop wal/wal2 (no writes anywhere in plan — verified), restore saved→default (T10), help (T12), error handling (T4,T7,T8,T11,T12). All covered.
+- Naming consistency: `apply_output`, `output_for`, `wall_file_for`, `pid_file_for`, `resolve_theme`, `persist_theme`, `do_set`, `do_restore`, `finalize`, `update_wpaper`, `apply_theme`, `parse_conf`, `load_conf`, `parse_set_args`, `kill_output`, `restore_path_for`, `qarma_select`, `show_help` — consistent across tasks.
+- Test framework: bats 1.5.0; tests that assert on globals or in-shell side effects call functions directly, tests that assert on status/output use `run`. No bats-support/-assert helpers required.