diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 7c5766b95..fc5f31fe6 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -131,6 +131,9 @@ jobs: - name: Run bats (oem-brew hook) run: bats tests/test_oem_brew.bats + - name: Run bats (damask-setup hook) + run: bats tests/test_damask_setup.bats + - name: Run bats (bonedigger report) run: bats tests/test_bonedigger_report.bats diff --git a/Justfile b/Justfile index 68cf9a6dc..082b4907d 100644 --- a/Justfile +++ b/Justfile @@ -36,6 +36,7 @@ test: bats tests/test_hardware_hooks.bats bats tests/test_theming_hook.bats bats tests/test_dynamic_wallpaper_hook.bats + bats tests/test_damask_setup.bats bats tests/test_nvidia_flatpak_sync.bats bats tests/test_system_just.bats bats tests/test_brew_tap_trust.bats diff --git a/docs/skills/dconf-consistency.md b/docs/skills/dconf-consistency.md index ee2fdf079..c24d11498 100644 --- a/docs/skills/dconf-consistency.md +++ b/docs/skills/dconf-consistency.md @@ -1,7 +1,7 @@ --- name: dconf-consistency -version: "1.0" -last_updated: "2026-06-23" +version: "1.1" +last_updated: "2026-09-23" id: dconf-consistency one_line_purpose: Keep GSettings overrides and dconf lock files in parity. entry_point: docs/skills/dconf-consistency.md @@ -126,3 +126,13 @@ This means any `gsettings get org.gnome.shell enabled-extensions` call in a test `enabled-extensions` for `org.gnome.shell` is set in `system_files/bluefin/usr/share/glib-2.0/schemas/zz0-bluefin-modifications.gschema.override` — this sets the schema DEFAULT. It is NOT in `distro.d/` and is not a locked key, so users can override it. The CI's `local.d/00-ci-testing` write overrides it in every test VM. Tests must use `get_default_value()` to validate this config, not `gsettings get`. + +## Flatpak application defaults via GKeyfileSettingsBackend + +Flatpak applications run sandboxed and typically use `GKeyfileSettingsBackend` rather than the host system's dconf database. + +Key differences from host GSettings: +- Config is stored in a keyfile at `~/.var/app//config/glib-2.0/settings/keyfile`. +- Schema separation: root application preferences are separated from plugin sources (e.g., `[app/drey/Damask]` vs `[app/drey/Damask/sources/slideshow]`). +- Pre-seeding defaults cannot be done via `/etc/dconf/db` or `gschema.override`. Instead, pre-seed defaults in `system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/` using the `version-script` contract. +- Non-destructive activation: check `[[ ! -f "${KEYFILE}" ]]` before writing so existing user configurations are never overwritten, and set inactive or dormant defaults (e.g., `active-source='none'`) when activating by default would override host user desktop settings or timed wallpaper slideshows. diff --git a/docs/skills/index.json b/docs/skills/index.json index 879b511b4..f6a940d47 100644 --- a/docs/skills/index.json +++ b/docs/skills/index.json @@ -154,8 +154,8 @@ "configuration" ], "description": "GSettings override and dconf lock file parity rules \u2014 must edit both files together for locked settings. Use when changing any GSettings key, dconf override, or lock file in system_files/.", - "version": "1.0", - "last_updated": "2026-06-23", + "version": "1.1", + "last_updated": "2026-09-23", "doc_type": "reference" }, { diff --git a/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh new file mode 100755 index 000000000..33958633a --- /dev/null +++ b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +# shellcheck disable=SC1091 +source /usr/lib/ublue/setup-services/libsetup.sh + +flatpak info app.drey.Damask &>/dev/null || exit 0 + +version-script damask-setup user 1 || exit 0 + +SETTINGS_DIR="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings" +KEYFILE="${SETTINGS_DIR}/keyfile" + +mkdir -p "${SETTINGS_DIR}" + +if [[ ! -f "${KEYFILE}" ]]; then + cat > "${KEYFILE}" << 'EOF' +[app/drey/Damask] +refresh-interval='86400' +enable-automatic-refresh=true +run-in-background=true +active-source='none' + +[app/drey/Damask/sources/slideshow] +folder-uri='file:///run/host/usr/share/backgrounds/bluefin' +sort-by='random' +EOF + chmod 0644 "${KEYFILE}" +fi + +if systemctl --user list-unit-files damask.service &>/dev/null; then + systemctl --user enable damask.service +fi diff --git a/tests/test_damask_setup.bats b/tests/test_damask_setup.bats new file mode 100644 index 000000000..ec5fd5611 --- /dev/null +++ b/tests/test_damask_setup.bats @@ -0,0 +1,160 @@ +#!/usr/bin/env bats +# Tests for system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh +# +# Run: bats tests/test_damask_setup.bats + +REPO_ROOT="$BATS_TEST_DIRNAME/.." +DAMASK_HOOK="${REPO_ROOT}/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh" +LIBSETUP_REAL="${REPO_ROOT}/system_files/shared/usr/lib/ublue/setup-services/libsetup.sh" + +WORKDIR="" +PATCHED_HOOK="" + +setup() { + WORKDIR="$(mktemp -d)" + mkdir -p "${WORKDIR}/bin" "${WORKDIR}/home" + + echo 0 > "${WORKDIR}/systemctl.rc" + echo 0 > "${WORKDIR}/systemctl-list.rc" + + cat > "${WORKDIR}/bin/systemctl" << MOCK +#!/bin/bash +echo "\$*" >> "${WORKDIR}/systemctl.log" +if [[ "\$*" == *"list-unit-files damask.service"* ]]; then + exit "\$(cat "${WORKDIR}/systemctl-list.rc" 2>/dev/null || echo 0)" +fi +exit "\$(cat "${WORKDIR}/systemctl.rc")" +MOCK + chmod +x "${WORKDIR}/bin/systemctl" + echo 0 > "${WORKDIR}/flatpak.rc" + cat > "${WORKDIR}/bin/flatpak" << MOCK +#!/bin/bash +echo "\$*" >> "${WORKDIR}/flatpak.log" +exit "\$(cat "${WORKDIR}/flatpak.rc")" +MOCK + chmod +x "${WORKDIR}/bin/flatpak" + + # Patch the absolute source path so hook runs against the real libsetup.sh in the repo. + PATCHED_HOOK="${WORKDIR}/25-damask-setup.sh" + sed \ + -e "s|source /usr/lib/ublue/setup-services/libsetup.sh|source ${LIBSETUP_REAL}|g" \ + "${DAMASK_HOOK}" > "${PATCHED_HOOK}" + chmod +x "${PATCHED_HOOK}" + + export PATH="${WORKDIR}/bin:${PATH}" + export HOME="${WORKDIR}/home" + unset XDG_CONFIG_HOME + export SETUP_CHECKER_FILE="${WORKDIR}/setup_versioning.json" +} + +teardown() { + rm -rf "${WORKDIR}" +} + +@test "25-damask-setup: first run creates keyfile with expected content and permissions" { + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + local keyfile="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings/keyfile" + [ -f "${keyfile}" ] + + # Verify file permissions are 0644 + local perms + perms="$(stat -c "%a" "${keyfile}")" + [ "${perms}" = "644" ] + + # Verify root preferences section + grep -qx "\[app/drey/Damask\]" "${keyfile}" + grep -qx "refresh-interval='86400'" "${keyfile}" + grep -qx "enable-automatic-refresh=true" "${keyfile}" + grep -qx "run-in-background=true" "${keyfile}" + grep -qx "active-source='none'" "${keyfile}" + + # Verify slideshow plugin source section + grep -qx "\[app/drey/Damask/sources/slideshow\]" "${keyfile}" + grep -qx "folder-uri='file:///run/host/usr/share/backgrounds/bluefin'" "${keyfile}" + grep -qx "sort-by='random'" "${keyfile}" +} + +@test "25-damask-setup: first run enables damask.service for current user" { + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + grep -qx -- "--user enable damask.service" "${WORKDIR}/systemctl.log" + ! grep -q -- "--system" "${WORKDIR}/systemctl.log" +} + +@test "25-damask-setup: records version 1 under user service namespace" { + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + [ "$(jq -r '.version.user."damask-setup"' "${SETUP_CHECKER_FILE}")" = "1" ] +} + +@test "25-damask-setup: version-script gate makes a second run a no-op" { + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + # Modify the keyfile to verify second run doesn't overwrite it + local keyfile="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings/keyfile" + echo "# user modified" >> "${keyfile}" + + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + # The hook body must not run again: exactly one enable, and no second + # unit-file probe. Counting every logged call would also count the + # list-unit-files probe the first run makes. + [ "$(grep -c -x -- "--user enable damask.service" "${WORKDIR}/systemctl.log")" -eq 1 ] + [ "$(grep -c -x -- "--user list-unit-files damask.service" "${WORKDIR}/systemctl.log")" -eq 1 ] + + # user modification should still be present + grep -qx "# user modified" "${keyfile}" +} + +@test "25-damask-setup: non-destructive when keyfile already exists before first run" { + local keyfile_dir="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings" + mkdir -p "${keyfile_dir}" + cat > "${keyfile_dir}/keyfile" << 'EXISTING' +[app/drey/Damask] +active-source='wallhaven' +EXISTING + + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + # Existing content preserved + grep -qx "active-source='wallhaven'" "${keyfile_dir}/keyfile" + run grep -qx "active-source='none'" "${keyfile_dir}/keyfile" + [ "${status}" -ne 0 ] + + # systemctl was still called + grep -qx -- "--user enable damask.service" "${WORKDIR}/systemctl.log" +} + +@test "25-damask-setup: a failing systemctl causes the hook to exit with failure" { + echo 1 > "${WORKDIR}/systemctl.rc" + + run bash "${PATCHED_HOOK}" + [ "${status}" -ne 0 ] +} + +@test "25-damask-setup: gracefully skips enable if damask.service does not exist" { + echo 1 > "${WORKDIR}/systemctl-list.rc" + + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + grep -q -- "list-unit-files damask.service" "${WORKDIR}/systemctl.log" + ! grep -q -- "--user enable damask.service" "${WORKDIR}/systemctl.log" +} + +@test "25-damask-setup: exits cleanly without creating keyfile or stamping when Damask is not installed" { + echo 1 > "${WORKDIR}/flatpak.rc" + + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + local keyfile="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings/keyfile" + [ ! -f "${keyfile}" ] + [ ! -f "${SETUP_CHECKER_FILE}" ] +}