diff --git a/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.dark.toml b/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.dark.toml new file mode 100644 index 00000000..1bea860a --- /dev/null +++ b/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.dark.toml @@ -0,0 +1,34 @@ +# Managed by Dreamcoder; repository variant only. +[theme] +name = "catppuccin" + +[theme.custom] +accent = "#A5B4FC" +panel_bg = "#000000" +surface0 = "#0B0B0B" +surface1 = "#0D0D0F" +surface_dim = "#0B0B0B" +overlay0 = "#767676" +overlay1 = "#A7A7A7" +text = "#E6E6E6" +subtext0 = "#C7C7C7" +mauve = "#D8B4FE" +green = "#34D399" +yellow = "#FBBF24" +red = "#FB8585" +blue = "#7DD3FC" +teal = "#3B82F6" +peach = "#D4B5FD" +sidebar_bg = "#000000" +active_row_bg = "#0B0B0B" +selection_bg = "#3A3A3A" + +[ui] +accent = "#6FA0AF" +pane_scrollbars = false + +[keys] +prefix = "ctrl+a" +previous_agent = "prefix+alt+k" +next_agent = "prefix+alt+j" +focus_agent = "prefix+ctrl+1..9" diff --git a/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.light.toml b/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.light.toml new file mode 100644 index 00000000..7a07ccc5 --- /dev/null +++ b/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.light.toml @@ -0,0 +1,34 @@ +# Managed by Dreamcoder; repository variant only. +[theme] +name = "catppuccin-latte" + +[theme.custom] +accent = "#824f16" +panel_bg = "#f3eadc" +surface0 = "#fff7ea" +surface1 = "#decbb1" +surface_dim = "#e6d7c4" +overlay0 = "#66513b" +overlay1 = "#554638" +text = "#17120d" +subtext0 = "#352e22" +mauve = "#7d3e64" +green = "#315b31" +yellow = "#654300" +red = "#842f24" +blue = "#0d4a68" +teal = "#0f6570" +peach = "#a7471c" +sidebar_bg = "#f3eadc" +active_row_bg = "#fff7ea" +selection_bg = "#decbb1" + +[ui] +accent = "#6FA0AF" +pane_scrollbars = false + +[keys] +prefix = "ctrl+a" +previous_agent = "prefix+alt+k" +next_agent = "prefix+alt+j" +focus_agent = "prefix+ctrl+1..9" diff --git a/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.night.toml b/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.night.toml new file mode 100644 index 00000000..57e50f0b --- /dev/null +++ b/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.night.toml @@ -0,0 +1,34 @@ +# Managed by Dreamcoder; repository variant only. +[theme] +name = "catppuccin" + +[theme.custom] +accent = "#a4afee" +panel_bg = "#000000" +surface0 = "#090909" +surface1 = "#0b0b0d" +surface_dim = "#090909" +overlay0 = "#797979" +overlay1 = "#959595" +text = "#c6c6c6" +subtext0 = "#ababab" +mauve = "#b98beb" +green = "#43b48b" +yellow = "#d1a126" +red = "#e88484" +blue = "#62b9e2" +teal = "#3a76d6" +peach = "#b48ce9" +sidebar_bg = "#000000" +active_row_bg = "#090909" +selection_bg = "#323232" + +[ui] +accent = "#6FA0AF" +pane_scrollbars = false + +[keys] +prefix = "ctrl+a" +previous_agent = "prefix+alt+k" +next_agent = "prefix+alt+j" +focus_agent = "prefix+ctrl+1..9" diff --git a/DreamcoderShell/.config/fish/config.fish b/DreamcoderShell/.config/fish/config.fish index 51fda963..35951116 100644 --- a/DreamcoderShell/.config/fish/config.fish +++ b/DreamcoderShell/.config/fish/config.fish @@ -49,7 +49,9 @@ alias zf="zen-browser -P Founder" set -gx BROWSER "zen-browser -P Personal" # Start selected terminal multiplexer (Herdr) if status is-interactive; and command -q herdr; and not set -q HERDR_ENV; and not set -q TMUX; and not set -q ZELLIJ; and not set -q ORCA_PANE_KEY - herdr; or echo "⚠️ Herdr failed to start; continuing in Fish." + # Herdr spawns panes with $SHELL; pin it to this fish so panes never inherit + # a foreign login shell (e.g. zsh from the session environment). + env SHELL=(status fish-path) herdr; or echo "⚠️ Herdr failed to start; continuing in Fish." end # ── SDD Profile Aliases ─────────────────────────────────────────── diff --git a/DreamcoderShell/.config/fish/functions/dev-dots.fish b/DreamcoderShell/.config/fish/functions/dev-dots.fish index b80f171a..1299d7d6 100644 --- a/DreamcoderShell/.config/fish/functions/dev-dots.fish +++ b/DreamcoderShell/.config/fish/functions/dev-dots.fish @@ -1,26 +1,33 @@ # Launch Dreamcoder-dots dev workspace with Herdr function dev-dots set -l project_dir "$HOME/Documents/PROYECTOS/dreamcoder-dots" + set -l herdr_lib "$project_dir/scripts/herdr-lib.sh" if not test -d "$project_dir" echo "❌ Project not found: $project_dir" return 1 end - # Start Herdr if not running - if not set -q HERDR_SOCKET - herdr --session dreamcoder-dots & - sleep 0.5 + # Herdr control commands only work from inside a Herdr pane; the fish + # autostart in config.fish launches or attaches the session. + if not set -q HERDR_ENV + echo "❌ Run dev-dots from a Herdr pane (start one with: herdr)" + return 1 end cd "$project_dir" echo "🚀 Dreamcoder-dots workspace ready at $project_dir" - # Open lazygit in current dir - herdr tab open -- lazygit - - # Open nvim in project - herdr tab open -- nvim + # Herdr 0.9 has no `tab open`: create each tab, wait for its shell, then run. + for tool in lazygit nvim + set -l pane_id (herdr tab create --cwd "$project_dir" --label $tool --no-focus | jq -r '.result.root_pane.pane_id') + if test -z "$pane_id"; or test "$pane_id" = null + echo "❌ herdr tab create failed for $tool" + return 1 + end + bash -c 'source "$1"; herdr_wait_shell "$2"' _ "$herdr_lib" "$pane_id" + and herdr pane run "$pane_id" $tool >/dev/null + end # Status line echo " 📂 $(pwd)" diff --git a/docs/README.md b/docs/README.md index bb5aebf1..a9573038 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,7 +25,7 @@ High-level documentation on how the system is built and how it works. | [Monorepo Structure](architecture/monorepo-structure.mmd) | Visual map of the repository: `src/`, `scripts/`, `tests/`, and every top-level directory | | [Data Flow](architecture/data-flow.mmd) | Full flow of `dreamcoder-theme sync`: palette → tokens → renderers → writers → disk | | [Source Manifest](sources.md) | Upstream inputs (ML4W, Gentleman.Dots), ownership boundaries, and prohibitions on secrets/runtime state | -| [Herdr Integration](herdr.md) | Herdr 0.7.3/0.8.0 contracts, versioned generated variants, and deployment profiles | +| [Herdr Integration](herdr.md) | Herdr 0.7.3/0.8.0/0.8.2/0.9.1 contracts, versioned generated variants, and deployment profiles | | [Design System](DREAMCODER_DESIGN_SYSTEM.md) | Design principles, color philosophy, and visual identity guidelines | | [Control Center](DREAMCODER_CONTROL_CENTER.md) | Central control dashboard for the Dreamcoder Workbench ecosystem | diff --git a/docs/herdr.md b/docs/herdr.md index fa3c572b..92ca36fd 100644 --- a/docs/herdr.md +++ b/docs/herdr.md @@ -26,6 +26,7 @@ configuration. | `herdr-0.7.3` | 0.7.3 | `herdr config check` | `herdr server reload-config` | `~/.config/herdr/config.toml` | Pre-existing supported profile | | `herdr-0.8.0` | 0.8.0 | `herdr config check` | `herdr server reload-config` | `~/.config/herdr/config.toml` (overridable by `HERDR_CONFIG_PATH`) | Installed-binary evidence | | `herdr-0.8.2` | 0.8.2 | Source-derived | `herdr server reload-config` | XDG config path (overridable by `HERDR_CONFIG_PATH`) | Public upstream source; no local runtime observation | +| `herdr-0.9.1` | 0.9.1 | `herdr config check` | `herdr server reload-config` | `~/.config/herdr/config.toml` (overridable by `HERDR_CONFIG_PATH`) | Installed-binary evidence | ### Herdr 0.8.0 installed-binary evidence @@ -52,6 +53,30 @@ base and the custom `sidebar_bg`, `active_row_bg`, and `selection_bg` tokens. See the complete hashes and procedural boundaries in [`herdr-contract-evidence.md`](../src/dreamcoder_theme/herdr-contract-evidence.md). +### Herdr 0.9.1 installed-binary evidence + +Observed from the executable installed by `herdr update` (0.9.0 → 0.9.1): + +- Executable: `~/.cargo/bin/herdr`, version `0.9.1` +- Binary SHA-256: + `2a02fed16beb651ef006e1d43f048f652ca4dc58ad053cd2d44450563d5c54b7` +- Config validation: `herdr config check`. In 0.9.1 it is strict: unknown keys + (`theme.custom.*`, `ui.*`) and unknown theme names are reported as issues and + exit non-zero, so a passing check proves every emitted field is recognized. +- Reload command: `herdr server reload-config` +- Config path: `~/.config/herdr/config.toml`, overridable via `HERDR_CONFIG_PATH` +- Default-config deltas relevant to the Dreamcoder variant (`herdr --default-config`): + `[ui] pane_scrollbars` now defaults to `true` (the variant keeps `false`); + `[ui] accent` defaults to the named color `"cyan"` (the variant pins a hex); + `[keys] prefix` defaults to `"ctrl+b"` and `previous_agent` / `next_agent` / + `focus_agent` are unset by default (the variant keeps `ctrl+a` and its agent + bindings); `catppuccin-latte` is a valid built-in theme name. + +The 0.9.1 variants carry the same field set as 0.8.2 (catppuccin base, +`catppuccin-latte` for Light, sidebar/active-row/selection tokens). Each +generated variant passed `HERDR_CONFIG_PATH= herdr config check`. A +live reload was not observed (the server was not running when captured). + ## Generated repository variants Versioned variants are generated from the Dreamcoder Workbench canonical tokens @@ -69,13 +94,17 @@ DreamcoderHerdr/.config/herdr/dreamcoder/ 0.8.2/config.dark.toml 0.8.2/config.light.toml 0.8.2/config.night.toml + 0.9.1/config.dark.toml + 0.9.1/config.light.toml + 0.9.1/config.night.toml ``` - Each variant carries the header `# Managed by Dreamcoder; repository variant only.` - Light renders Dreamcoder Light; dark renders Dreamcoder dark. - The 0.8.0 variants include `pane_scrollbars = false`. -- The 0.8.2 Light variant uses `catppuccin-latte`; all 0.8.2 variants explicitly - map sidebar, active-row, and navigation-selection backgrounds. +- The 0.8.2 and 0.9.1 Light variants use `catppuccin-latte`; all 0.8.2 and + 0.9.1 variants explicitly map sidebar, active-row, and navigation-selection + backgrounds. - Active/live configuration (`~/.config/herdr/config.toml`, or whatever `HERDR_CONFIG_PATH` points to) stays out of git. The repository only ever ships static, versioned variants. @@ -103,7 +132,8 @@ that deployment profiles validate against their schema, that the mobile profile selects Light with pane scrollbars disabled, that the source manifest is present, and that no sensitive material exists in the synchronization surface. When `herdr` is installed, the verifier additionally runs `herdr config check` -against a temporary copy of the 0.8.0 light variant using `HERDR_CONFIG_PATH` +against a temporary copy of the light variant for the installed version (the +0.8.0 variant when that version has no profile) using `HERDR_CONFIG_PATH` (never the live configuration). When `herdr` is absent, that step is skipped safely. diff --git a/docs/installation/linux.md b/docs/installation/linux.md index 9e6215cb..43181723 100644 --- a/docs/installation/linux.md +++ b/docs/installation/linux.md @@ -133,3 +133,21 @@ systemctl --user disable --now dreamcoder-theme-auto.timer | Btop theme not found | Run `dreamcoder-theme sync` to regenerate | | Auto-timer not switching | Check `systemctl --user status dreamcoder-theme-auto.timer` | | Doctor reports missing files | Run `./scripts/dreamcoder repair` | + +### Upstream upgrades that re-point Dreamcoder targets + +An ML4W upgrade can re-point `~/.config/fish`, `~/.config/kitty`, +`~/.config/fastfetch`, `~/.bashrc`, or `~/.zshrc` to its own tree under +`~/.mydotfiles`. `./scripts/dreamcoder repair` (and `install`) relinks the +Dreamcoder stow modules (`DreamcoderShell`, `DreamcoderKitty`, +`DreamcoderGhostty`, `DreamcoderFastfetch`, `DreamcoderWarp`, `DreamcoderBat`, +`DreamcoderSystemd`) before applying hooks: + +- a stow dry run lists every blocking target: regular files, and symlinks stow + does not own (foreign trees or absolute links into the repo); +- each one is moved, never deleted, to + `~/.local/share/dreamcoder/install-conflicts//`; + symlinks move as links, so the tree they point to stays untouched; +- stow then links the modules. Anything the upstream tree held that you still + need (for example fish universal variables in its `fish_variables`) must be + copied back by hand. diff --git a/odd/tasks/ml4w-2.16-gentleman-sync.md b/odd/tasks/ml4w-2.16-gentleman-sync.md index e26772d5..798b3b10 100644 --- a/odd/tasks/ml4w-2.16-gentleman-sync.md +++ b/odd/tasks/ml4w-2.16-gentleman-sync.md @@ -91,6 +91,57 @@ injected into ML4W-owned files. - Pre-existing, out of scope: `tests/shell/test_apply_theme.bats` "kanagawa bridge carries night-derived colors" also fails on clean `main`. +- [x] T5 — Restore the Dreamcoder stow layer: `scripts/dreamcoder-lib.sh` stows modules + `Shell Kitty Ghostty Fastfetch Warp Bat Systemd`, which no longer exist (renamed to + `Dreamcoder*`), so `dreamcoder install|repair` cannot relink anything. Fix the module + list and let install/repair replace a Dreamcoder-owned target that ML4W re-pointed + to its own tree (backed up, never deleted). Route: delegated writer. + - Trigger: the ML4W 2.16 upgrade (00:04) re-pointed `~/.config/fish` to ML4W's empty + `config.fish`, dropping the Dreamcoder fish layer and the Herdr autostart + (`DreamcoderShell/.config/fish/config.fish:51`); `kitty` and `fastfetch` also point + to ML4W. + - Evidence: commit `fix(install): relink Dreamcoder stow modules over foreign targets`; + `bats tests/ml4w/ tests/shell/` 126/127 (only the known kanagawa failure); + `tests/shell/test_stow_layer.bats` 8/8; `stow -n -v` of the 7 modules into an empty + temp HOME exits 0; pytest green on a clean `HEAD` export plus this change (the live + working tree fails `test_active_mirror_identity_consistency` only because of the + uncommitted 07:00 auto-theme rewrite); shellcheck clean. + - Rationale: modules renamed 1:1 (`a9dd5b5` prefix rename; `installer.py` was + already updated, the shell list was missed) — no new packages, a bats test pins + the list to `installer_plan()`. Conflicts come from a stow dry run instead of a + hand-kept list, so foreign symlinks (ML4W), regular files, and absolute links into + the repo are all moved (never deleted, symlinks moved as links) to + `~/.local/share/dreamcoder/install-conflicts//` — not under + `~/.config/dreamcoder`, which is itself a stow target. Install and repair now stow + before hooks, so hooks never write through ML4W links. The fastfetch hook keeps + stow's relative link instead of re-creating an absolute one each run. + - Live dry run (read-only) conflicts T6 will move: `.config/{fish,kitty,fastfetch}` + and `.bashrc`/`.zshrc` (ML4W links), `.config/dreamcoder`, `.config/starship.toml`, + systemd unit links (absolute repo links), `.config/systemd/user/dreamcoder-env.conf` + (identical regular file), warp settings/theme links. ML4W's `fish_variables` + (universal vars incl. `fish_user_paths`) is not migrated — report to the user. +- [ ] T6 — Relink the live system through `dreamcoder repair`, confirm a new + interactive fish starts Herdr. Route: inline. +- [x] T7 — Herdr 0.9.1: live binary updated with `herdr update` (0.9.0 → 0.9.1), + integrations reinstalled, `herdr config check` ok. Onboard a `herdr-0.9.1` profile + per `docs/herdr.md` (installed-binary evidence, generated variants, verify-repo-sync). + Route: delegated writer. + - Evidence: commit `feat(herdr): onboard the installed Herdr 0.9.1 profile`; binary + SHA-256 `2a02fed1…5c54b7`; `HERDR_CONFIG_PATH= herdr config check` ok for + 0.9.1 dark/light/night; pytest green on a clean `HEAD` export plus this change; + `verify-repo-sync.py` ok (host check now validates the installed 0.9.1 variant). + - Rationale: 0.9.1's `config check` is strict (unknown keys and theme names fail), + so passing proves the 0.8.2 field set is accepted; the profile reuses it with no new + fields and the variants are byte-identical to 0.8.2. Variants generated with a + scoped `sync_herdr_repo_variants(profiles=(HERDR_091_PROFILE,))` call, touching no + other file. Consequence: a version between profiles (0.9.0) now fails closed; only + versions newer than 0.9.1 fall back. `herdr-theme-switch.sh` needed no change + (Python detection). Herdr scripts (`herdr-lib`, `herdr-review`, + `herdr-workspace-dev`) and the fish autostart (plain `herdr`) match the 0.9.1 CLI; + `dev-dots.fish` used `herdr tab open`, absent in 0.9.1, now `tab create` + + `pane run` from inside a Herdr pane (`HERDR_ENV`). + - Not observed: live `server reload-config` (server not running at capture). + ## Acceptance criteria - `bats tests/ml4w/` and `python -m pytest tests/` pass. @@ -106,5 +157,5 @@ injected into ML4W-owned files. ## Next step -Feature complete. Pending user decisions: SUPER+SHIFT+arrows overlap (resize vs -move), the pre-existing kanagawa bridge test, push/PR of the branch. +T6 (inline live relink). Pending user decisions: SUPER+SHIFT+arrows +overlap (resize vs move), the pre-existing kanagawa bridge test, push/PR of the branch. diff --git a/scripts/apply-fastfetch-assets.sh b/scripts/apply-fastfetch-assets.sh index 56e460a2..7cbdf505 100755 --- a/scripts/apply-fastfetch-assets.sh +++ b/scripts/apply-fastfetch-assets.sh @@ -15,6 +15,12 @@ BACKUP_DIR="${CONFIG_HOME}/dreamcoder-backup-$(date +%Y%m%d-%H%M%S)" } mkdir -p "${CONFIG_HOME}" +# Already linked (e.g. by stow's relative link): keep it, so the next +# `dreamcoder repair` does not see an absolute link stow does not own. +if [[ -L "${TARGET_DIR}" && "$(cd -P "${TARGET_DIR}" 2>/dev/null && pwd)" == "$(cd -P "${SOURCE_DIR}" && pwd)" ]]; then + printf '✓ Fastfetch assets linked: %s\n' "${TARGET_DIR}" + exit 0 +fi if [[ -e "${TARGET_DIR}" && ! -L "${TARGET_DIR}" ]]; then mkdir -p "${BACKUP_DIR}" mv "${TARGET_DIR}" "${BACKUP_DIR}/" diff --git a/scripts/dreamcoder-lib.sh b/scripts/dreamcoder-lib.sh index 94d82085..45311ad0 100644 --- a/scripts/dreamcoder-lib.sh +++ b/scripts/dreamcoder-lib.sh @@ -1,10 +1,61 @@ #!/usr/bin/env bash set -euo pipefail # used by sourcing scripts (dreamcoder.sh, dreamcoder-maintenance.sh) -export DREAMCODER_MODULES=(Shell Kitty Ghostty Fastfetch Warp Bat Systemd) -DREAMCODER_TARGETS=("${CONFIG_HOME}/kitty" "${CONFIG_HOME}/ghostty" "${CONFIG_HOME}/fastfetch" "${CONFIG_HOME}/dreamcoder" "${CONFIG_HOME}/fish" "${CONFIG_HOME}/starship.toml" "${CONFIG_HOME}/bat" "${DATA_HOME}/warp-terminal/themes") +# Stow packages owned by install/repair; keep in sync with +# src/dreamcoder_theme/installer.py (installer_plan()["modules"]). +DREAMCODER_MODULES=(DreamcoderShell DreamcoderKitty DreamcoderGhostty DreamcoderFastfetch DreamcoderWarp DreamcoderBat DreamcoderSystemd) +DREAMCODER_TARGETS=("${CONFIG_HOME}/kitty" "${CONFIG_HOME}/ghostty" "${CONFIG_HOME}/fastfetch" "${CONFIG_HOME}/dreamcoder" "${CONFIG_HOME}/fish" "${CONFIG_HOME}/starship.toml" "${CONFIG_HOME}/bat" "${DATA_HOME}/warp-terminal/themes" "${HOME}/.zshrc" "${HOME}/.bashrc" "${HOME}/.inputrc" "${CONFIG_HOME}/systemd/user/dreamcoder-theme-auto.service" "${CONFIG_HOME}/systemd/user/dreamcoder-theme-auto.timer") dreamcoder_control() { PYTHONPATH="${DREAMCODER_DOTS_DIR}/src${PYTHONPATH:+:${PYTHONPATH}}" python3 -m dreamcoder_theme.control "$@"; } dreamcoder_json_get() { python3 -c 'import json,sys; print(json.load(sys.stdin)[sys.argv[1]])' "$1"; } dreamcoder_backup() { dreamcoder_control backup create "${DREAMCODER_TARGETS[@]}" --reason "${1}" --json; } dreamcoder_apply_hooks() { "${DREAMCODER_DOTS_DIR}/scripts/apply-ml4w-hooks.sh"; "${DREAMCODER_DOTS_DIR}/scripts/apply-cli-env-hooks.sh"; "${DREAMCODER_DOTS_DIR}/scripts/apply-fastfetch-assets.sh"; } dreamcoder_enable_timer() { command -v systemctl >/dev/null || return 0; systemctl --user daemon-reload || true; systemctl --user enable --now dreamcoder-theme-auto.timer || true; } + +# Print every target (relative to $HOME) that blocks stowing the modules, +# as reported by a stow dry run: regular files/directories where stow wants a +# link, and symlinks stow does not own (foreign trees such as ML4W's +# ~/.mydotfiles, or absolute links into the repo). +dreamcoder_stow_conflicts() { + local report + report="$(stow -n -v -d "${DREAMCODER_DOTS_DIR}" -t "${HOME}" "${DREAMCODER_MODULES[@]}" 2>&1)" || true + printf '%s\n' "${report}" | sed -n \ + -e 's/^ \* existing target is not owned by stow: //p' \ + -e 's/^ \* cannot stow .* over existing target \(.*\) since neither a link nor a directory.*$/\1/p' +} + +# Move one conflicting target into CONFLICT_DIR, keeping its $HOME-relative +# path. Symlinks are moved as links (never followed), so the tree they point +# to is left untouched. Nothing is ever deleted. +dreamcoder_move_conflict() { + local rel="${1}" conflict_dir="${2}" + local src="${HOME}/${rel}" dest="${conflict_dir}/${rel}" + [[ -e "${src}" || -L "${src}" ]] || return 0 + if [[ -e "${dest}" || -L "${dest}" ]]; then + printf '✗ Refusing to overwrite %s while moving stow conflict %s\n' "${dest}" "${src}" >&2 + return 1 + fi + mkdir -p "$(dirname "${dest}")" + if [[ -L "${src}" ]]; then + printf '→ Moved foreign symlink %s (-> %s) to %s\n' "${src}" "$(readlink "${src}")" "${dest}" + else + printf '→ Moved stow conflict %s to %s\n' "${src}" "${dest}" + fi + mv "${src}" "${dest}" +} + +# Clear stow conflicts into CONFLICT_DIR, then stow every module into $HOME. +# Bounded passes: moving a target never creates a new conflict, but a second +# dry run proves the tree is clean before the real stow runs. +dreamcoder_stow_modules() { + local conflict_dir="${1}" rel moved _ + for _ in 1 2 3; do + moved=0 + while IFS= read -r rel; do + [[ -n "${rel}" ]] || continue + dreamcoder_move_conflict "${rel}" "${conflict_dir}" + moved=1 + done < <(dreamcoder_stow_conflicts) + (( moved )) || break + done + stow -d "${DREAMCODER_DOTS_DIR}" -t "${HOME}" "${DREAMCODER_MODULES[@]}" +} diff --git a/scripts/dreamcoder-maintenance.sh b/scripts/dreamcoder-maintenance.sh index 3c81511c..3280bbef 100755 --- a/scripts/dreamcoder-maintenance.sh +++ b/scripts/dreamcoder-maintenance.sh @@ -8,15 +8,22 @@ LIB_FILE="${DREAMCODER_DOTS_DIR}/scripts/dreamcoder-lib.sh" # shellcheck source=/dev/null [[ -f "${LIB_FILE}" ]] && source "${LIB_FILE}" fail() { printf '✗ %s\n' "${*}" >&2; exit 1; } -backup_path() { local path="${1}" legacy_dir="${CONFIG_HOME}/dreamcoder/install-conflicts/${BACKUP_ID}"; [[ -e "${path}" && ! -L "${path}" ]] || return 0; mkdir -p "${legacy_dir}"; mv "${path}" "${legacy_dir}/"; printf '→ Moved stow conflict %s to %s\n' "${path}" "${legacy_dir}"; } command -v python3 >/dev/null || fail 'Missing dependency: python3' +[[ "${MODE}" == install ]] && command -v stow >/dev/null || [[ "${MODE}" == repair ]] || fail 'Usage: dreamcoder-maintenance.sh {install|repair}' BACKUP_JSON="$(dreamcoder_backup "${MODE}-preflight")"; BACKUP_ID="$(printf '%s' "${BACKUP_JSON}" | dreamcoder_json_get backup_id)" printf '→ Backup manifest: %s\n rollback: ./scripts/dreamcoder backup restore %s --json\n' "${BACKUP_ID}" "${BACKUP_ID}" -[[ "${MODE}" == install ]] && command -v stow >/dev/null || [[ "${MODE}" == repair ]] || fail 'Usage: dreamcoder-maintenance.sh {install|repair}' +# Outside every stowed tree: ~/.config/dreamcoder is itself a stow target. +CONFLICT_DIR="${DATA_HOME}/dreamcoder/install-conflicts/${BACKUP_ID}" cd "${DREAMCODER_DOTS_DIR}" -if [[ "${MODE}" == install ]]; then for target in "${DREAMCODER_TARGETS[@]}"; do backup_path "${target}"; done; stow -t "${HOME}" "${DREAMCODER_MODULES[@]}"; fi +# Stow before the hooks so they write through Dreamcoder links, not through a +# target an upstream (e.g. an ML4W upgrade) re-pointed to its own tree. +if command -v stow >/dev/null; then + dreamcoder_stow_modules "${CONFLICT_DIR}" + [[ -d "${CONFLICT_DIR}" ]] && printf '→ Stow conflicts preserved in %s\n' "${CONFLICT_DIR}" +else + printf '! stow not found: skipping relink (%s)\n' "${MODE}" >&2 +fi dreamcoder_apply_hooks -if [[ "${MODE}" == repair ]]; then command -v stow >/dev/null && stow -t "${HOME}" "${DREAMCODER_MODULES[@]}"; fi dreamcoder_enable_timer "${DREAMCODER_DOTS_DIR}/scripts/theme-auto.sh" [[ "${MODE}" == repair ]] && "${DREAMCODER_DOTS_DIR}/scripts/verify.sh" diff --git a/scripts/verify-repo-sync.py b/scripts/verify-repo-sync.py index ae791423..e1532198 100644 --- a/scripts/verify-repo-sync.py +++ b/scripts/verify-repo-sync.py @@ -21,7 +21,7 @@ import jsonschema -from dreamcoder_theme.herdr_contract import SUPPORTED_PROFILES +from dreamcoder_theme.herdr_contract import SUPPORTED_PROFILES, detect_profile from dreamcoder_theme.palette_tokens import VARIANTS from dreamcoder_theme.renderers_herdr import herdr_content @@ -343,10 +343,16 @@ def optional_host_tool_problems() -> list[str]: if herdr is None: print("herdr: not installed — optional config validation skipped (safe)") return problems - profile = next(p for p in SUPPORTED_PROFILES if p.evidence.version == "0.8.0") - variant = HERDR_VARIANT_ROOT / "0.8.0" / "config.light.toml" + # Validate the installed version's own variant; an unprofiled version + # falls back to the 0.8.0 baseline. + version_result = subprocess.run( + [herdr, "--version"], capture_output=True, text=True, check=False + ) + selection = detect_profile(str(version_result.stdout or "")) + version = selection.profile.evidence.version if selection.profile else "0.8.0" + variant = HERDR_VARIANT_ROOT / version / "config.light.toml" if not variant.is_file(): - problems.append("0.8.0 light variant missing; cannot run optional herdr config check") + problems.append(f"{version} light variant missing; cannot run optional herdr config check") return problems with tempfile.TemporaryDirectory() as tmp: candidate = Path(tmp) / "config.toml" @@ -357,7 +363,7 @@ def optional_host_tool_problems() -> list[str]: ) if result.returncode != 0: problems.append( - "herdr config check failed for the 0.8.0 light variant: " + f"herdr config check failed for the {version} light variant: " f"{result.stdout.strip()} {result.stderr.strip()}".strip() ) return problems diff --git a/src/dreamcoder_theme/herdr-contract-evidence.md b/src/dreamcoder_theme/herdr-contract-evidence.md index 372fdb10..a1a4ce31 100644 --- a/src/dreamcoder_theme/herdr-contract-evidence.md +++ b/src/dreamcoder_theme/herdr-contract-evidence.md @@ -146,6 +146,30 @@ silently accepted" behavior already documented above for 0.7.3) is a design decision, not a patch, and is left open here rather than decided unilaterally. +## Herdr 0.9.1 installed-binary profile + +`herdr-0.9.1` is bound to local `herdr 0.9.1` (installed by `herdr update` +from 0.9.0), binary SHA-256 +`2a02fed16beb651ef006e1d43f048f652ca4dc58ad053cd2d44450563d5c54b7` at +`~/.cargo/bin/herdr`. Unlike the 0.9.0 note above, 0.9.1's +`herdr config check` rejects unknown keys (`unknown config key +theme.custom.; ignoring key`) and unknown theme names with a non-zero +exit, which closes the open question of whether the 0.8.2 field set is still +accepted: each generated `0.9.1/config.{dark,light,night}.toml` passed +`HERDR_CONFIG_PATH= herdr config check` with `config: ok`. The profile +therefore reuses the 0.8.2 theme, custom, and ui field sets exactly and adds no +new fields. + +Observed default-config deltas (`herdr --default-config`, 374 lines, SHA-256 +`a62a4a2fc4746dd392976916d3333b89598cc30407819de9544cc8f4b4502fa8`): +`[ui] pane_scrollbars` defaults to `true`, `[ui] accent` to `"cyan"`, +`[keys] prefix` to `"ctrl+b"`, and the agent navigation keys are unset. The +Dreamcoder variant sets all of them explicitly, so those defaults do not leak +into it. Reload remains `herdr server reload-config`; no live reload was +observed for this profile (server not running at capture time). A version +between two profiles (for example 0.9.0) fails closed; only versions newer than +the newest profile fall back to it. + ## Fail-closed boundary Unknown or malformed version output remains `unsupported-contract`; an absent diff --git a/src/dreamcoder_theme/herdr_contract.py b/src/dreamcoder_theme/herdr_contract.py index 2a43a6b2..22ebeba3 100644 --- a/src/dreamcoder_theme/herdr_contract.py +++ b/src/dreamcoder_theme/herdr_contract.py @@ -296,7 +296,39 @@ def profile_from_evidence(evidence: ContractEvidence) -> HerdrProfile: restoration=ProcedureEvidence(available=True, unambiguous=True), ) HERDR_082_PROFILE = HerdrProfile(evidence=HERDR_082_EVIDENCE) -SUPPORTED_PROFILES = (HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE) + +# Herdr v0.9.1 installed-binary evidence (`herdr update` from 0.9.0). Observed +# exactly: executable `herdr` at ~/.cargo/bin/herdr, version `0.9.1`, binary +# SHA-256 2a02fed16beb651ef006e1d43f048f652ca4dc58ad053cd2d44450563d5c54b7. +# `herdr config check` rejects unknown keys and theme names in 0.9.1, so the +# field set below is the 0.8.2 set proven accepted by running `config check` +# against each generated variant through HERDR_CONFIG_PATH. Reload command is +# `herdr server reload-config`; see herdr-contract-evidence.md. +HERDR_091_EVIDENCE = ContractEvidence( + profile_id="herdr-0.9.1", + executable="herdr", + version="0.9.1", + source_identity=( + "Herdr v0.9.1 installed-binary evidence: `herdr config check` (strict on " + "unknown keys) accepts every generated field; reload " + "`herdr server reload-config`; HERDR_CONFIG_PATH override" + ), + source_sha256="2a02fed16beb651ef006e1d43f048f652ca4dc58ad053cd2d44450563d5c54b7", + default_config_path="/.config/herdr/config.toml", + config_path_environment="HERDR_CONFIG_PATH", + color_representation="hex (#RRGGBB)", + base_theme_name="catppuccin", + light_base_theme_name="catppuccin-latte", + allowed_theme_fields=HERDR_082_EVIDENCE.allowed_theme_fields, + allowed_custom_fields=HERDR_082_EVIDENCE.allowed_custom_fields, + allowed_ui_fields=HERDR_082_EVIDENCE.allowed_ui_fields, + candidate_validation=ProcedureEvidence(available=True, unambiguous=True), + server_applicability=ProcedureEvidence(available=True, unambiguous=True), + reload=ProcedureEvidence(available=True, unambiguous=True, observable=True), + restoration=ProcedureEvidence(available=True, unambiguous=True), +) +HERDR_091_PROFILE = HerdrProfile(evidence=HERDR_091_EVIDENCE) +SUPPORTED_PROFILES = (HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE, HERDR_091_PROFILE) def detect_profile( diff --git a/src/dreamcoder_theme/installer.py b/src/dreamcoder_theme/installer.py index 5a37ad8b..66be3feb 100644 --- a/src/dreamcoder_theme/installer.py +++ b/src/dreamcoder_theme/installer.py @@ -30,7 +30,10 @@ def managed_targets() -> list[dict[str, Any]]: "module": "DreamcoderSystemd", "path": str(ch / "systemd" / "user" / "dreamcoder-theme-auto.service"), }, - {"module": "Systemd", "path": str(ch / "systemd" / "user" / "dreamcoder-theme-auto.timer")}, + { + "module": "DreamcoderSystemd", + "path": str(ch / "systemd" / "user" / "dreamcoder-theme-auto.timer"), + }, ] diff --git a/tests/shell/test_fish_herdr_autostart.bats b/tests/shell/test_fish_herdr_autostart.bats new file mode 100644 index 00000000..e6905263 --- /dev/null +++ b/tests/shell/test_fish_herdr_autostart.bats @@ -0,0 +1,14 @@ +#!/usr/bin/env bats +# ============================================================================ +# Fish autostart must launch Herdr with SHELL pinned to fish, because Herdr +# spawns every pane with $SHELL and the session may export another shell. +# ============================================================================ + +@test "fish config has valid syntax" { + run fish -n DreamcoderShell/.config/fish/config.fish + [ "$status" -eq 0 ] +} + +@test "fish autostart pins SHELL to the running fish when launching Herdr" { + grep -qF 'env SHELL=(status fish-path) herdr' DreamcoderShell/.config/fish/config.fish +} diff --git a/tests/shell/test_stow_layer.bats b/tests/shell/test_stow_layer.bats new file mode 100644 index 00000000..f0bb6cf9 --- /dev/null +++ b/tests/shell/test_stow_layer.bats @@ -0,0 +1,103 @@ +#!/usr/bin/env bats +# ============================================================================ +# Tests for the Dreamcoder stow layer (scripts/dreamcoder-lib.sh) +# ============================================================================ + +setup() { + REPO_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME}")/../.." && pwd)" + TEST_DIR="$(mktemp -d)" + export HOME="${TEST_DIR}/home" + export DREAMCODER_DOTS_DIR="${REPO_DIR}" + export CONFIG_HOME="${HOME}/.config" + export DATA_HOME="${HOME}/.local/share" + CONFLICT_DIR="${TEST_DIR}/conflicts" + FOREIGN="${TEST_DIR}/mydotfiles/.config" + mkdir -p "${HOME}/.config" "${FOREIGN}/fish" + printf '# foreign fish\n' >"${FOREIGN}/fish/config.fish" + printf 'SETUVAR foo:bar\n' >"${FOREIGN}/fish/fish_variables" + # shellcheck source=/dev/null + source "${REPO_DIR}/scripts/dreamcoder-lib.sh" +} + +teardown() { + rm -rf "${TEST_DIR}" +} + +resolved() { (cd -P "$1" 2>/dev/null && pwd) || readlink -f "$1"; } + +@test "every stow module exists as a package directory" { + [ "${#DREAMCODER_MODULES[@]}" -gt 0 ] + for module in "${DREAMCODER_MODULES[@]}"; do + [ -d "${REPO_DIR}/${module}" ] || { echo "missing module: ${module}"; false; } + done +} + +@test "stow modules match the installer plan" { + expected="$(PYTHONPATH="${REPO_DIR}/src" python3 -c 'from dreamcoder_theme.installer import installer_plan; print(" ".join(installer_plan()["modules"]))')" + [ "${expected}" = "${DREAMCODER_MODULES[*]}" ] +} + +@test "stow dry run resolves every module into an empty HOME" { + command -v stow >/dev/null || skip "stow not installed" + run stow -n -v -d "${REPO_DIR}" -t "${HOME}" "${DREAMCODER_MODULES[@]}" + [ "$status" -eq 0 ] + [[ "$output" != *"conflict"* ]] +} + +@test "a foreign directory symlink is moved aside as a link and relinked into the repo" { + command -v stow >/dev/null || skip "stow not installed" + ln -s "${FOREIGN}/fish" "${HOME}/.config/fish" + + run dreamcoder_stow_modules "${CONFLICT_DIR}" + [ "$status" -eq 0 ] + + [ -L "${HOME}/.config/fish" ] + [ "$(resolved "${HOME}/.config/fish")" = "$(resolved "${REPO_DIR}/DreamcoderShell/.config/fish")" ] + # The foreign link is preserved as a link; its tree is untouched. + [ -L "${CONFLICT_DIR}/.config/fish" ] + [ "$(readlink "${CONFLICT_DIR}/.config/fish")" = "${FOREIGN}/fish" ] + [ "$(cat "${FOREIGN}/fish/fish_variables")" = "SETUVAR foo:bar" ] +} + +@test "regular-file conflicts are moved with their contents and never deleted" { + command -v stow >/dev/null || skip "stow not installed" + printf 'user zshrc\n' >"${HOME}/.zshrc" + + run dreamcoder_stow_modules "${CONFLICT_DIR}" + [ "$status" -eq 0 ] + + [ -L "${HOME}/.zshrc" ] + [ "$(cat "${CONFLICT_DIR}/.zshrc")" = "user zshrc" ] +} + +@test "an absolute link into the repo is replaced by stow's own link" { + command -v stow >/dev/null || skip "stow not installed" + ln -s "${REPO_DIR}/DreamcoderShell/.config/starship.toml" "${HOME}/.config/starship.toml" + + run dreamcoder_stow_modules "${CONFLICT_DIR}" + [ "$status" -eq 0 ] + + [ "$(readlink -f "${HOME}/.config/starship.toml")" = "${REPO_DIR}/DreamcoderShell/.config/starship.toml" ] + [[ "$(readlink "${HOME}/.config/starship.toml")" != /* ]] + [ -L "${CONFLICT_DIR}/.config/starship.toml" ] +} + +@test "restowing an already linked HOME is a no-op" { + command -v stow >/dev/null || skip "stow not installed" + ln -s "${FOREIGN}/fish" "${HOME}/.config/fish" + dreamcoder_stow_modules "${CONFLICT_DIR}" >/dev/null + + run dreamcoder_stow_modules "${TEST_DIR}/second" + [ "$status" -eq 0 ] + [ ! -e "${TEST_DIR}/second" ] +} + +@test "maintenance relinks before hooks and keeps conflicts outside stowed trees" { + maintenance="${REPO_DIR}/scripts/dreamcoder-maintenance.sh" + stow_line="$(grep -n 'dreamcoder_stow_modules' "${maintenance}" | head -1 | cut -d: -f1)" + hooks_line="$(grep -n '^dreamcoder_apply_hooks' "${maintenance}" | cut -d: -f1)" + [ -n "${stow_line}" ] && [ -n "${hooks_line}" ] + [ "${stow_line}" -lt "${hooks_line}" ] + # shellcheck disable=SC2016 # literal match of the script source + grep -qF 'CONFLICT_DIR="${DATA_HOME}/dreamcoder/install-conflicts/' "${maintenance}" +} diff --git a/tests/test_herdr_contract.py b/tests/test_herdr_contract.py index bec60994..16b8c5f7 100644 --- a/tests/test_herdr_contract.py +++ b/tests/test_herdr_contract.py @@ -11,6 +11,7 @@ HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE, + HERDR_091_PROFILE, SUPPORTED_PROFILES, ContractEvidence, ContractStatus, @@ -65,6 +66,17 @@ def test_source_derived_082_profile_is_complete_and_version_bound() -> None: assert detect_profile("herdr 0.8.3").status is ContractStatus.UNSUPPORTED_CONTRACT +def test_installed_binary_091_profile_is_complete_and_version_bound() -> None: + assert HERDR_091_PROFILE.is_complete + assert HERDR_091_PROFILE.evidence.source_sha256 == ( + "2a02fed16beb651ef006e1d43f048f652ca4dc58ad053cd2d44450563d5c54b7" + ) + assert HERDR_091_PROFILE.evidence.light_base_theme_name == "catppuccin-latte" + assert detect_profile("herdr 0.9.1").profile is HERDR_091_PROFILE + assert detect_profile("herdr 0.9.0").status is ContractStatus.UNSUPPORTED_CONTRACT + assert detect_profile("herdr 0.9.2").status is ContractStatus.UNSUPPORTED_CONTRACT + + def test_073_and_080_profiles_select_their_own_exact_versions() -> None: assert detect_profile("herdr 0.7.3").profile is HERDR_073_PROFILE assert detect_profile("herdr 0.8.0").profile is HERDR_080_PROFILE diff --git a/tests/test_herdr_switch.py b/tests/test_herdr_switch.py index 1d1eca3e..ac014b47 100644 --- a/tests/test_herdr_switch.py +++ b/tests/test_herdr_switch.py @@ -37,7 +37,7 @@ def _variant_root( tmp_path: Path, *, - versions: tuple[str, ...] = ("0.7.3", "0.8.0", "0.8.2"), + versions: tuple[str, ...] = ("0.7.3", "0.8.0", "0.8.2", "0.9.1"), modes: tuple[str, ...] = ("dark", "light", "night"), ) -> Path: root = tmp_path / "variants" @@ -57,7 +57,7 @@ class FakeRun: def __init__( self, *, - version: str = "herdr 0.9.0\n", + version: str = "herdr 0.9.2\n", config_check_returncode: int = 0, reload_stdout: str = APPLIED, reload_stderr: str = "", @@ -142,13 +142,28 @@ def test_exact_profile_selects_its_own_variant(tmp_path: Path) -> None: def test_newer_unprofiled_version_falls_back_to_newest_variant(tmp_path: Path) -> None: root = _variant_root(tmp_path) - choice = choose_variant("herdr 0.9.0\n", "dark", root) - assert choice.path == root / "0.8.2/config.dark.toml" - assert choice.profile_version == "0.8.2" + choice = choose_variant("herdr 0.9.2\n", "dark", root) + assert choice.path == root / "0.9.1/config.dark.toml" + assert choice.profile_version == "0.9.1" assert choice.fallback is True assert "no checked-in profile" in choice.reason +def test_installed_091_selects_its_own_variant(tmp_path: Path) -> None: + root = _variant_root(tmp_path) + choice = choose_variant("herdr 0.9.1\n", "night", root) + assert choice.path == root / "0.9.1/config.night.toml" + assert choice.profile_version == "0.9.1" + assert choice.fallback is False + + +def test_version_between_profiles_is_rejected(tmp_path: Path) -> None: + root = _variant_root(tmp_path) + choice = choose_variant("herdr 0.9.0\n", "dark", root) + assert choice.path is None + assert "unsupported" in choice.reason + + def test_older_unprofiled_version_is_rejected(tmp_path: Path) -> None: root = _variant_root(tmp_path) choice = choose_variant("herdr 0.7.2\n", "dark", root) @@ -221,22 +236,22 @@ def test_absent_selector_is_created(tmp_path: Path) -> None: assert selector.is_symlink() assert selector.readlink() == Path("config.light.toml") deployed = selector.parent / "config.light.toml" - assert deployed.read_text() == (root / "0.8.2/config.light.toml").read_text() + assert deployed.read_text() == (root / "0.9.1/config.light.toml").read_text() def test_existing_symlink_selector_is_repointed(tmp_path: Path) -> None: root = _variant_root(tmp_path) selector = tmp_path / "config.toml" - selector.symlink_to(root / "0.8.2/config.dark.toml") + selector.symlink_to(root / "0.9.1/config.dark.toml") outcome = switch_herdr("light", run=FakeRun(), selector=selector, variant_root=root) assert outcome.status == "applied" assert selector.readlink() == Path("config.light.toml") - assert outcome.previous_target == str(root / "0.8.2/config.dark.toml") + assert outcome.previous_target == str(root / "0.9.1/config.dark.toml") def test_repo_variant_is_never_modified(tmp_path: Path) -> None: root = _variant_root(tmp_path) - variant = root / "0.8.2/config.light.toml" + variant = root / "0.9.1/config.light.toml" before = variant.read_text() switch_herdr("light", run=FakeRun(), selector=tmp_path / "config.toml", variant_root=root) assert variant.read_text() == before @@ -315,7 +330,7 @@ def test_reload_server_not_running_keeps_the_selector(tmp_path: Path) -> None: def test_reload_failure_restores_the_previous_selector(tmp_path: Path) -> None: root = _variant_root(tmp_path) selector = tmp_path / "config.toml" - original = root / "0.8.2/config.dark.toml" + original = root / "0.9.1/config.dark.toml" selector.symlink_to(original) run = FakeRun(reload_stdout=RELOAD_FAILED, reload_returncode=1) outcome = switch_herdr("light", run=run, selector=selector, variant_root=root) @@ -337,7 +352,7 @@ def test_reload_failure_removes_a_newly_created_selector(tmp_path: Path) -> None def test_reload_launch_failure_restores_the_previous_selector(tmp_path: Path) -> None: root = _variant_root(tmp_path) selector = tmp_path / "config.toml" - original = root / "0.8.2/config.night.toml" + original = root / "0.9.1/config.night.toml" selector.symlink_to(original) run = FakeRun(reload_error=FileNotFoundError("herdr")) outcome = switch_herdr("light", run=run, selector=selector, variant_root=root) diff --git a/tests/test_herdr_theme_generation.py b/tests/test_herdr_theme_generation.py index cf0e3284..d1bd0ac5 100644 --- a/tests/test_herdr_theme_generation.py +++ b/tests/test_herdr_theme_generation.py @@ -12,6 +12,7 @@ HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE, + HERDR_091_PROFILE, ) from dreamcoder_theme.palette_tokens import VARIANTS from dreamcoder_theme.renderers_herdr import ( @@ -159,31 +160,28 @@ def test_repository_sync_writes_only_versioned_variants( base_073 = tmp_path / "DreamcoderHerdr/.config/herdr/dreamcoder/0.7.3" base_080 = tmp_path / "DreamcoderHerdr/.config/herdr/dreamcoder/0.8.0" base_082 = tmp_path / "DreamcoderHerdr/.config/herdr/dreamcoder/0.8.2" + base_091 = tmp_path / "DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1" - assert changes == [True, True, True, True, True, True] + assert changes == [True] * 8 assert (base_073 / "config.dark.toml").is_file() assert (base_073 / "config.light.toml").is_file() assert (base_080 / "config.dark.toml").is_file() assert (base_080 / "config.light.toml").is_file() assert (base_082 / "config.dark.toml").is_file() assert (base_082 / "config.light.toml").is_file() + assert (base_091 / "config.dark.toml").is_file() + assert (base_091 / "config.light.toml").is_file() assert selector.read_text() == "onboarding = false\n" - assert sync.sync_herdr_repo_variants( - {"dark": VARIANTS["dark"], "light": VARIANTS["light"]} - ) == [ - False, - False, - False, - False, - False, - False, - ] + assert ( + sync.sync_herdr_repo_variants({"dark": VARIANTS["dark"], "light": VARIANTS["light"]}) + == [False] * 8 + ) def test_checked_in_repository_variants_match_the_renderer() -> None: repo = Path(__file__).parents[1] night = sync.prepare("dark", "standard").variants["night"] - for profile in (HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE): + for profile in (HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE, HERDR_091_PROFILE): base = repo / "DreamcoderHerdr/.config/herdr/dreamcoder" / profile.evidence.version assert (base / "config.dark.toml").read_text() == herdr_content( diff --git a/tests/test_verify_repo_sync.py b/tests/test_verify_repo_sync.py index 4663df9b..d757d524 100644 --- a/tests/test_verify_repo_sync.py +++ b/tests/test_verify_repo_sync.py @@ -13,6 +13,7 @@ HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE, + HERDR_091_PROFILE, ) from dreamcoder_theme.palette_tokens import VARIANTS from dreamcoder_theme.renderers_herdr import herdr_content @@ -30,7 +31,7 @@ def _load_verifier() -> ModuleType: def _install_layout(module: ModuleType, root: Path) -> None: - for profile in (HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE): + for profile in (HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE, HERDR_091_PROFILE): base = root / "DreamcoderHerdr/.config/herdr/dreamcoder" / profile.evidence.version base.mkdir(parents=True) (base / "config.dark.toml").write_text(herdr_content(profile, "dark", VARIANTS["dark"])) @@ -166,6 +167,39 @@ class FakeResult: assert "herdr config check failed" in output +def test_host_check_validates_the_installed_version_variant( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + module = _load_verifier() + _install_layout(module, tmp_path) + _point_module_at(module, monkeypatch, tmp_path) + monkeypatch.setattr(module.shutil, "which", lambda _: "/bin/herdr") + checked: list[str] = [] + + class FakeResult: + def __init__(self, returncode: int, stdout: str) -> None: + self.returncode = returncode + self.stdout = stdout + self.stderr = "" + + def fake_run(args: list[str], **kwargs: object) -> FakeResult: + if args[1:] == ["--version"]: + return FakeResult(0, "herdr 0.9.1\n") + env = kwargs["env"] + assert isinstance(env, dict) + checked.append(Path(env["HERDR_CONFIG_PATH"]).read_text()) + return FakeResult(1, "invalid config") + + monkeypatch.setattr(module.subprocess, "run", fake_run) + + code, output = _run(module, capsys) + + variant = tmp_path / "DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.light.toml" + assert code == 1 + assert checked == [variant.read_text()] + assert "herdr config check failed for the 0.9.1 light variant" in output + + def test_missing_source_manifest_fails_safely( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: