Sync config from arch
- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
// Standalone harness for the background auto-check logic added to
|
||||
// Panel.qml (autoCheckEnabled, autoCheckIntervalHours, and the
|
||||
// pendingUpdateCount filter). Panel.qml itself only loads inside the full
|
||||
// Omarchy shell (qs.Ui, qs.Commons, the live PluginRegistry), so this copies
|
||||
// the pure expressions to exercise them in isolation. Keep the three
|
||||
// `readonly property` blocks below byte-for-byte in sync with Panel.qml if
|
||||
// that logic changes.
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
property var settings: {
|
||||
var raw = Quickshell.env("OMAPLUG_TEST_SETTINGS")
|
||||
return raw ? JSON.parse(raw) : {}
|
||||
}
|
||||
|
||||
// Mirrors qs.Ui/Panel.qml's setting(name, fallback).
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
// Kept byte-identical to Panel.qml by auto-check-test.sh's sync guard;
|
||||
// edit both or the build fails.
|
||||
// AUTOCHECK-SETTINGS-BEGIN
|
||||
readonly property bool autoCheckEnabled: root.setting("autoCheckUpdates", false) === true
|
||||
// real, not int: an int property truncates any fractional hours value
|
||||
// (e.g. 0.5) towards zero, which would silently turn into a zero-interval
|
||||
// Timer below and spin checkUpdates() in a tight loop.
|
||||
readonly property real autoCheckIntervalHours: {
|
||||
var hours = Number(root.setting("autoCheckIntervalHours", 6))
|
||||
return (isFinite(hours) && hours > 0) ? hours : 6
|
||||
}
|
||||
// AUTOCHECK-SETTINGS-END
|
||||
|
||||
property var updateStates: JSON.parse(Quickshell.env("OMAPLUG_TEST_UPDATE_STATES") || "{}")
|
||||
property var pluginRepos: JSON.parse(Quickshell.env("OMAPLUG_TEST_PLUGIN_REPOS") || "{}")
|
||||
|
||||
// Kept byte-identical to Panel.qml by auto-check-test.sh's sync guard;
|
||||
// edit both or the build fails.
|
||||
// PENDING-UPDATE-COUNT-BEGIN
|
||||
readonly property int pendingUpdateCount: {
|
||||
var n = 0
|
||||
for (var k in root.updateStates) {
|
||||
if (root.pluginRepos[k] === undefined) continue
|
||||
if (root.updateStates[k] === "UPDATE") n++
|
||||
}
|
||||
n
|
||||
}
|
||||
// PENDING-UPDATE-COUNT-END
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("RESOLVED_ENABLED", root.autoCheckEnabled)
|
||||
console.log("RESOLVED_HOURS", root.autoCheckIntervalHours)
|
||||
console.log("RESOLVED_PENDING", root.pendingUpdateCount)
|
||||
}
|
||||
|
||||
// The property under test: same shape as Panel.qml's autoUpdateCheckTimer.
|
||||
// running/interval stay bound to autoCheckEnabled/autoCheckIntervalHours so
|
||||
// a live settings change (see toggleTimer below) reaches this Timer the
|
||||
// same way persistAutoCheckSetting()'s write to root.settings does in the
|
||||
// real Panel.qml - no restart, just the binding re-evaluating.
|
||||
Timer {
|
||||
interval: root.autoCheckIntervalHours * 3600000
|
||||
running: root.autoCheckEnabled
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: console.log("TIMER_FIRED")
|
||||
}
|
||||
|
||||
// Simulates the user flipping the panel's toggle (or picking a new
|
||||
// interval) while the shell is already running: OMAPLUG_TEST_TOGGLE_AFTER_MS
|
||||
// schedules a one-shot settings mutation partway through the run, proving
|
||||
// the Timer above reacts live rather than only at startup.
|
||||
property real toggleAfterMs: Number(Quickshell.env("OMAPLUG_TEST_TOGGLE_AFTER_MS") || "0")
|
||||
Timer {
|
||||
interval: root.toggleAfterMs
|
||||
running: root.toggleAfterMs > 0
|
||||
onTriggered: {
|
||||
var next = {}
|
||||
for (var k in root.settings) next[k] = root.settings[k]
|
||||
next.autoCheckUpdates = Quickshell.env("OMAPLUG_TEST_TOGGLE_TO") === "true"
|
||||
root.settings = next
|
||||
console.log("TOGGLED_ENABLED", root.autoCheckEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds how long the process waits for triggers before exiting; must be
|
||||
// well under any interval used above so a firing timer wins the race, and
|
||||
// comfortably after toggleAfterMs so the post-toggle behavior is observed.
|
||||
Timer {
|
||||
interval: 1200
|
||||
running: true
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
ShellRoot {
|
||||
Component.onCompleted: {
|
||||
Quickshell.execDetached([
|
||||
Quickshell.env("OMAPLUG_TEST_HELPER"),
|
||||
Quickshell.env("OMAPLUG_TEST_STATUS"),
|
||||
"job-quickshell",
|
||||
"slow",
|
||||
"good"
|
||||
])
|
||||
exitTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: exitTimer
|
||||
interval: 50
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf -- "$TMP"' EXIT
|
||||
|
||||
SCRIPTDIR="$TMP/scriptdir"
|
||||
mkdir -p -- "$SCRIPTDIR"
|
||||
cp -- "$ROOT/auto-check-coordinator.sh" "$SCRIPTDIR/"
|
||||
cp -- "$ROOT/runtime-state.py" "$SCRIPTDIR/"
|
||||
chmod +x "$SCRIPTDIR/auto-check-coordinator.sh"
|
||||
|
||||
CALLS="$TMP/calls.log"
|
||||
: > "$CALLS"
|
||||
|
||||
# Stands in for plugin-state.sh: records that it ran, takes long enough that
|
||||
# a second, concurrent coordinator invocation is guaranteed to still find
|
||||
# the lock held, and prints one fixed, recognizable result line.
|
||||
cat > "$SCRIPTDIR/plugin-state.sh" <<EOF
|
||||
#!/bin/bash
|
||||
printf '%s\n' "\$\$" >> "$CALLS"
|
||||
sleep 0.4
|
||||
printf 'UPDATE\tfixture\thttps://example.invalid/fixture.git\tbehind\n'
|
||||
EOF
|
||||
chmod +x "$SCRIPTDIR/plugin-state.sh"
|
||||
|
||||
export XDG_RUNTIME_DIR="$TMP/xdg"
|
||||
mkdir -p -- "$XDG_RUNTIME_DIR"
|
||||
|
||||
PLUGINS_DIR="$TMP/plugins"
|
||||
mkdir -p -- "$PLUGINS_DIR"
|
||||
|
||||
EXPECTED=$'UPDATE\tfixture\thttps://example.invalid/fixture.git\tbehind'
|
||||
|
||||
# Two "monitors" ticking their auto-check timer at effectively the same
|
||||
# moment. The second is staggered by 50ms - well under the stub's 400ms
|
||||
# hold time - so it deterministically finds the lock already taken instead
|
||||
# of racing for it, making this a real leader/follower run rather than a
|
||||
# coin flip.
|
||||
"$SCRIPTDIR/auto-check-coordinator.sh" "$PLUGINS_DIR" > "$TMP/out1" 2>"$TMP/err1" &
|
||||
pid1=$!
|
||||
sleep 0.05
|
||||
"$SCRIPTDIR/auto-check-coordinator.sh" "$PLUGINS_DIR" > "$TMP/out2" 2>"$TMP/err2" &
|
||||
pid2=$!
|
||||
wait "$pid1"
|
||||
wait "$pid2"
|
||||
|
||||
call_count=$(wc -l < "$CALLS")
|
||||
if [[ $call_count -ne 1 ]]; then
|
||||
printf 'FAIL: expected plugin-state.sh to run exactly once for 2 concurrent monitors, ran %s times\n' "$call_count" >&2
|
||||
cat "$CALLS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for f in out1 out2; do
|
||||
if [[ $(cat "$TMP/$f") != "$EXPECTED" ]]; then
|
||||
printf 'FAIL: %s did not receive the leader'"'"'s result\n' "$f" >&2
|
||||
printf 'got: %q\n' "$(cat "$TMP/$f")" >&2
|
||||
cat "$TMP/err1" "$TMP/err2" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# A lock directory left behind by a process that no longer exists (e.g.
|
||||
# Panel.qml's checkWatchdog SIGKILLing a coordinator mid-run, which skips
|
||||
# any EXIT trap) must not wedge every future auto-check.
|
||||
: > "$CALLS"
|
||||
LOCK="$XDG_RUNTIME_DIR/omaplug/auto-check.flock"
|
||||
printf 'stale\n' > "$LOCK"
|
||||
|
||||
OUT=$("$SCRIPTDIR/auto-check-coordinator.sh" "$PLUGINS_DIR")
|
||||
if [[ $OUT != "$EXPECTED" ]]; then
|
||||
printf 'FAIL: stale lock (dead pid) blocked a fresh check instead of being reclaimed\n' >&2
|
||||
printf 'got: %q\n' "$OUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ $(wc -l < "$CALLS") -eq 1 ]] || {
|
||||
printf 'FAIL: stale-lock recovery did not actually run plugin-state.sh\n' >&2
|
||||
exit 1
|
||||
}
|
||||
# The flock inode is intentionally retained; the kernel releases ownership
|
||||
# when the process exits, avoiding stale-PID deletion races.
|
||||
|
||||
printf 'auto-check-coordinator-test: ok\n'
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
printf 'auto-check-test: skipped (quickshell not installed)\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
run() {
|
||||
QT_QPA_PLATFORM=offscreen \
|
||||
OMAPLUG_TEST_SETTINGS="${1:-{\}}" \
|
||||
OMAPLUG_TEST_UPDATE_STATES="${2:-{\}}" \
|
||||
OMAPLUG_TEST_PLUGIN_REPOS="${3:-{\}}" \
|
||||
OMAPLUG_TEST_TOGGLE_AFTER_MS="${4:-0}" \
|
||||
OMAPLUG_TEST_TOGGLE_TO="${5:-false}" \
|
||||
quickshell --no-color -p "$ROOT/tests/AutoCheckLogic.qml" 2>&1
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local needle="$1"
|
||||
local haystack="$2"
|
||||
local label="$3"
|
||||
if [[ $haystack != *"$needle"* ]]; then
|
||||
printf 'FAIL: %s: expected output to contain %q\n' "$label" "$needle" >&2
|
||||
printf '%s\n' "$haystack" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local needle="$1"
|
||||
local haystack="$2"
|
||||
local label="$3"
|
||||
if [[ $haystack == *"$needle"* ]]; then
|
||||
printf 'FAIL: %s: expected output to NOT contain %q\n' "$label" "$needle" >&2
|
||||
printf '%s\n' "$haystack" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# True if a TIMER_FIRED line appears strictly after the first line matching
|
||||
# marker, i.e. after the point in the run's timeline where the setting changed.
|
||||
found_fire_after() {
|
||||
local marker="$1"
|
||||
local haystack="$2"
|
||||
awk -v m="$marker" '
|
||||
$0 ~ m { seen=1; next }
|
||||
seen && /TIMER_FIRED/ { found=1 }
|
||||
END { exit !found }
|
||||
' <<<"$haystack"
|
||||
}
|
||||
|
||||
assert_no_fire_after() {
|
||||
local marker="$1" haystack="$2" label="$3"
|
||||
if found_fire_after "$marker" "$haystack"; then
|
||||
printf 'FAIL: %s: TIMER_FIRED appeared after %q\n' "$label" "$marker" >&2
|
||||
printf '%s\n' "$haystack" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_fire_after() {
|
||||
local marker="$1" haystack="$2" label="$3"
|
||||
if ! found_fire_after "$marker" "$haystack"; then
|
||||
printf 'FAIL: %s: expected TIMER_FIRED after %q but none seen\n' "$label" "$marker" >&2
|
||||
printf '%s\n' "$haystack" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Pulls the text between "<marker>-BEGIN" and "<marker>-END" comment lines.
|
||||
extract_block() {
|
||||
local file="$1" marker="$2"
|
||||
awk -v b="${marker}-BEGIN" -v e="${marker}-END" '
|
||||
$0 ~ b { flag=1; next }
|
||||
$0 ~ e { flag=0 }
|
||||
flag
|
||||
' "$file"
|
||||
}
|
||||
|
||||
# Panel.qml only loads inside the full Omarchy shell, so AutoCheckLogic.qml
|
||||
# duplicates its auto-check expressions to test them standalone (see the
|
||||
# header comment in each file). A copy that's never re-checked against the
|
||||
# original is worse than no test: it would keep passing while the real
|
||||
# Panel.qml logic silently drifts. This fails loudly the moment they disagree.
|
||||
assert_blocks_match() {
|
||||
local marker="$1" label="$2"
|
||||
local a b
|
||||
a=$(extract_block "$ROOT/Panel.qml" "$marker")
|
||||
b=$(extract_block "$ROOT/tests/AutoCheckLogic.qml" "$marker")
|
||||
if [[ -z $a || -z $b ]]; then
|
||||
printf 'FAIL: %s: marker %q missing from Panel.qml or AutoCheckLogic.qml\n' "$label" "$marker" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ $a != "$b" ]]; then
|
||||
printf 'FAIL: %s: Panel.qml and tests/AutoCheckLogic.qml have drifted apart\n' "$label" >&2
|
||||
diff <(printf '%s\n' "$a") <(printf '%s\n' "$b") >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_blocks_match AUTOCHECK-SETTINGS auto-check-settings-in-sync
|
||||
assert_blocks_match PENDING-UPDATE-COUNT pending-update-count-in-sync
|
||||
|
||||
# The sync guard above only proves the settings math agrees; it says nothing
|
||||
# about what the real Timer's onTriggered calls. Pin that down directly so a
|
||||
# future edit can't quietly swap in a new/duplicate check function instead of
|
||||
# the one the manual "Check for updates" button already uses.
|
||||
grep -Fq 'onTriggered: root.checkUpdates(root.autoCheckCoordinatorPath)' "$ROOT/Panel.qml" || {
|
||||
printf 'FAIL: Panel.qml autoUpdateCheckTimer is not wired to the existing checkUpdates()\n' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Defaults: disabled, 6h interval, stays quiet until enabled.
|
||||
OUT=$(run '{}')
|
||||
assert_contains 'RESOLVED_ENABLED false' "$OUT" default-disabled
|
||||
assert_contains 'RESOLVED_HOURS 6' "$OUT" default-hours
|
||||
assert_not_contains 'TIMER_FIRED' "$OUT" default-does-not-fire
|
||||
|
||||
# Explicitly disabled: never fires, even once, within the test window.
|
||||
OUT=$(run '{"autoCheckUpdates":false}')
|
||||
assert_contains 'RESOLVED_ENABLED false' "$OUT" disabled-flag
|
||||
assert_not_contains 'TIMER_FIRED' "$OUT" disabled-does-not-fire
|
||||
|
||||
# Custom interval passes through unchanged.
|
||||
OUT=$(run '{"autoCheckIntervalHours":3}')
|
||||
assert_contains 'RESOLVED_HOURS 3' "$OUT" custom-hours
|
||||
|
||||
# A fractional value must survive as-is, not truncate to 0 (which an `int`
|
||||
# property would silently do, turning into a zero-interval, tight-looping
|
||||
# Timer - not reachable through the shipped 1/3/6/12/24h picker, but settings
|
||||
# are hand-editable in shell.json).
|
||||
OUT=$(run '{"autoCheckIntervalHours":0.5}')
|
||||
assert_contains 'RESOLVED_HOURS 0.5' "$OUT" fractional-hours-not-truncated
|
||||
|
||||
# Zero, negative, and non-numeric intervals all fall back to 6h rather than
|
||||
# producing a zero/negative-interval Timer (which would fire in a tight loop).
|
||||
OUT=$(run '{"autoCheckIntervalHours":0}')
|
||||
assert_contains 'RESOLVED_HOURS 6' "$OUT" zero-hours-fallback
|
||||
|
||||
OUT=$(run '{"autoCheckIntervalHours":-2}')
|
||||
assert_contains 'RESOLVED_HOURS 6' "$OUT" negative-hours-fallback
|
||||
|
||||
OUT=$(run '{"autoCheckIntervalHours":"banana"}')
|
||||
assert_contains 'RESOLVED_HOURS 6' "$OUT" non-numeric-hours-fallback
|
||||
|
||||
# pendingUpdateCount only counts UPDATE states for keys plugin-state.sh also
|
||||
# reported a repo URL for (mirrors the real property's join against
|
||||
# pluginRepos, which is what keeps a stale/removed plugin out of the count).
|
||||
OUT=$(run '{}' '{"a":"UPDATE","b":"CURRENT","c":"UPDATE","d":"UPDATE"}' '{"a":"url","b":"url","d":"url"}')
|
||||
assert_contains 'RESOLVED_PENDING 2' "$OUT" pending-count-filters-unknown-repo
|
||||
|
||||
# Live toggle off: this is what happens when the user flips the panel's
|
||||
# switch off while the shell keeps running - no restart, so the same process
|
||||
# must stop firing right away. A sub-second interval (0.0001h) guarantees a
|
||||
# refire would land inside the test window if disabling didn't take effect.
|
||||
OUT=$(run '{"autoCheckIntervalHours":0.0001}' '{}' '{}' 500 false)
|
||||
assert_contains 'TOGGLED_ENABLED false' "$OUT" live-disable-toggled
|
||||
assert_no_fire_after 'TOGGLED_ENABLED false' "$OUT" live-disable-stops-firing
|
||||
|
||||
# Live toggle on: starts disabled (proven quiet up to the toggle line), then
|
||||
# flips on mid-run - the resumed Timer's triggeredOnStart must fire again
|
||||
# immediately rather than waiting for the next shell restart.
|
||||
OUT=$(run '{"autoCheckUpdates":false}' '{}' '{}' 300 true)
|
||||
assert_not_contains 'TIMER_FIRED' "${OUT%%TOGGLED_ENABLED*}" live-enable-quiet-before-toggle
|
||||
assert_contains 'TOGGLED_ENABLED true' "$OUT" live-enable-toggled
|
||||
assert_fire_after 'TOGGLED_ENABLED true' "$OUT" live-enable-resumes-firing
|
||||
|
||||
printf 'auto-check-test: ok\n'
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
PANEL="$ROOT/Panel.qml"
|
||||
HELPER="$ROOT/marketplace-catalog.sh"
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf -- "$TMP"' EXIT
|
||||
|
||||
grep -Fq 'Qt.resolvedUrl("marketplace-catalog.sh")' "$PANEL"
|
||||
grep -Fq 'marketplaceProcess.command = [root.marketplaceHelperPath]' "$PANEL"
|
||||
grep -Fq 'if (exitCode !== 0)' "$PANEL"
|
||||
grep -Fq '!Array.isArray(catalog.plugins)' "$PANEL"
|
||||
grep -Fq -- '--max-filesize 16777216' "$HELPER"
|
||||
|
||||
if grep -Fq 'head -c 4194304' "$PANEL"; then
|
||||
printf 'FAIL: marketplace fetch must reject oversized catalogs, not truncate them\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FIXTURE="$TMP/catalog.json"
|
||||
jq -n '{
|
||||
ignored: ("x" * 4500000),
|
||||
plugins: [
|
||||
{
|
||||
id: "fixture",
|
||||
verificationStatus: "verified",
|
||||
verificationCommit: "abc",
|
||||
upstreamObservedCommit: "def",
|
||||
repositoryRelease: {url: "https://example.invalid/release"},
|
||||
extra: ("y" * 10000)
|
||||
}
|
||||
]
|
||||
}' > "$FIXTURE"
|
||||
|
||||
test "$(wc -c < "$FIXTURE")" -gt 4194304
|
||||
SUMMARY=$("$HELPER" "file://$FIXTURE")
|
||||
jq -e '.plugins | length == 1' <<< "$SUMMARY" >/dev/null
|
||||
jq -e '.plugins[0] == {
|
||||
id: "fixture",
|
||||
verificationStatus: "verified",
|
||||
verificationCommit: "abc",
|
||||
verificationSnapshotStatus: null,
|
||||
verificationCoverage: null,
|
||||
upstreamObservedCommit: "def",
|
||||
repositoryRelease: {url: "https://example.invalid/release"}
|
||||
}' <<< "$SUMMARY" >/dev/null
|
||||
test "$(wc -c <<< "$SUMMARY")" -lt 4096
|
||||
|
||||
printf 'catalog-fetch-test: ok\n'
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
PANEL="$ROOT/Panel.qml"
|
||||
|
||||
assert_contains() {
|
||||
local needle="$1"
|
||||
grep -Fq -- "$needle" "$PANEL" || {
|
||||
printf 'FAIL: missing %q in %s\n' "$needle" "$PANEL" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
assert_contains 'function leadingIconGlyph(text)'
|
||||
assert_contains 'var first = s.charCodeAt(0)'
|
||||
assert_contains 'return codePoint >= 0xF0000 && codePoint <= 0x10FFFD ? s.substring(0, 2) : ""'
|
||||
assert_contains 'var live = root.leadingIconGlyph(root.liveGlyphFor(id))'
|
||||
|
||||
printf 'icon-glyph-test: ok\n'
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
HELPER="$ROOT/nested-widget-toggle.sh"
|
||||
|
||||
TASK_DIR=$(mktemp -d)
|
||||
trap 'rm -rf -- "$TASK_DIR"' EXIT
|
||||
export CALL_LOG="$TASK_DIR/calls"
|
||||
export CONFIG BAR_FAIL=0
|
||||
|
||||
# Executable stubs also intercept the helper's final exec command.
|
||||
omarchy-shell() {
|
||||
[[ $* == 'shell listShellConfig' ]] || return 90
|
||||
printf '%s\n' "$CONFIG"
|
||||
}
|
||||
omarchy() {
|
||||
jq -cn --args '$ARGS.positional' -- "$@" >> "$CALL_LOG"
|
||||
if [[ ${1:-} == bar && $BAR_FAIL == 1 ]]; then return 42; fi
|
||||
}
|
||||
mkdir "$TASK_DIR/bin"
|
||||
printf '#!/bin/bash\n%s\nomarchy-shell "$@"\n' "$(declare -f omarchy-shell)" > "$TASK_DIR/bin/omarchy-shell"
|
||||
printf '#!/bin/bash\n%s\nomarchy "$@"\n' "$(declare -f omarchy)" > "$TASK_DIR/bin/omarchy"
|
||||
chmod +x "$TASK_DIR/bin/omarchy-shell" "$TASK_DIR/bin/omarchy"
|
||||
export PATH="$TASK_DIR/bin:$PATH"
|
||||
|
||||
failures=0
|
||||
check() {
|
||||
local label=$1
|
||||
shift
|
||||
if ! "$@" > /dev/null; then
|
||||
printf 'FAIL: %s\n' "$label" >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
}
|
||||
run_helper() {
|
||||
: > "$CALL_LOG"
|
||||
result=0
|
||||
bash "$HELPER" "$@" > "$TASK_DIR/stdout" 2> "$TASK_DIR/stderr" || result=$?
|
||||
}
|
||||
|
||||
CONFIG='{"bar":{"layout":{"left":[{"id":"host","widgets":["before","child","after","child"]}],"right":[{"id":"other","widgets":["untouched"]}]}}}'
|
||||
run_helper child disable
|
||||
check 'nested disable succeeds' test "$result" -eq 0
|
||||
check 'remove only requested child before disabling it' jq -se '
|
||||
. == [["bar","set","host","widgets","[\"before\",\"after\"]","--json"],
|
||||
["plugin","disable","child"]]
|
||||
' "$CALL_LOG"
|
||||
|
||||
CONFIG='{"bar":{"layout":{"left":[{"id":"host","widgets":["other"]}]}}}'
|
||||
run_helper standalone disable
|
||||
check 'non-nested disable succeeds' test "$result" -eq 0
|
||||
check 'non-nested disable does not modify hosts' jq -se '. == [["plugin","disable","standalone"]]' "$CALL_LOG"
|
||||
|
||||
CONFIG='{not valid json'
|
||||
run_helper child disable
|
||||
check 'invalid JSON fails' test "$result" -ne 0
|
||||
check 'invalid JSON never disables a plugin' test ! -s "$CALL_LOG"
|
||||
|
||||
CONFIG='{"bar":{"layout":42}}'
|
||||
run_helper child disable
|
||||
check 'invalid layout fails' test "$result" -ne 0
|
||||
check 'jq failure never disables a plugin' test ! -s "$CALL_LOG"
|
||||
|
||||
CONFIG='{"bar":{"layout":{"left":[{"id":"host","widgets":["child","other"]}]}}}'
|
||||
BAR_FAIL=1
|
||||
run_helper child disable
|
||||
check 'host update error propagates' test "$result" -eq 42
|
||||
check 'failed host update prevents plugin disable' jq -se '
|
||||
. == [["bar","set","host","widgets","[\"other\"]","--json"]]
|
||||
' "$CALL_LOG"
|
||||
|
||||
(( failures == 0 )) || exit 1
|
||||
|
||||
printf 'nested-widget-toggle-test: ok\n'
|
||||
@@ -0,0 +1,115 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const source = fs.readFileSync(path.join(__dirname, '../Panel.qml'), 'utf8');
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'omaplug-metadata-'));
|
||||
try {
|
||||
const context = vm.createContext({
|
||||
console, pluginRows: [], marketplaceMap: {},
|
||||
pluginManifestProcess: {}, scanPluginRepos() {},
|
||||
Quickshell: { env: () => temporary }
|
||||
});
|
||||
context.root = context;
|
||||
for (const name of ['applyPluginList', 'applyPluginMetadata', 'mergeMarketplaceMetadata', 'applyMarketplaceCatalog']) {
|
||||
const start = source.indexOf(' function ' + name + '(');
|
||||
assert.notEqual(start, -1);
|
||||
const end = source.indexOf('\n }', start) + 4;
|
||||
vm.runInContext(source.slice(start, end), context);
|
||||
}
|
||||
|
||||
const manifests = [
|
||||
['shell/plugins/panels/clock/clock.manifest.json', 'omarchy.clock'],
|
||||
['.config/omarchy/plugins/test plugin/manifest.json', 'test.plugin']
|
||||
];
|
||||
for (const [relative, id] of manifests) {
|
||||
const file = path.join(temporary, relative);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, JSON.stringify({ id, description: 'Installed ' + id }));
|
||||
}
|
||||
const catalog = JSON.stringify([
|
||||
{ id: 'omarchy.clock', firstParty: true, enabled: true, kinds: ['bar-widget'] },
|
||||
{ id: 'test.plugin', enabled: false, kinds: ['panel'] },
|
||||
{ id: 'catalog.only', enabled: true, kinds: ['panel'] }
|
||||
]);
|
||||
context.marketplaceMap = {
|
||||
'test.plugin': { description: 'Marketplace description' },
|
||||
'catalog.only': { description: 'Cached description' }
|
||||
};
|
||||
|
||||
// Repeat to cover a refresh after the marketplace has already loaded.
|
||||
for (let refresh = 0; refresh < 2; refresh++) {
|
||||
context.applyPluginList(catalog);
|
||||
const [command, ...args] = context.pluginManifestProcess.command;
|
||||
const output = execFileSync(command, args, {
|
||||
env: { ...process.env, HOME: temporary }, encoding: 'utf8'
|
||||
});
|
||||
context.applyPluginMetadata(output);
|
||||
const rows = Object.fromEntries(context.pluginRows.map(row => [row.id, row]));
|
||||
assert.equal(rows['omarchy.clock'].description, 'Installed omarchy.clock');
|
||||
assert.equal(rows['test.plugin'].description, 'Installed test.plugin');
|
||||
assert.equal(rows['catalog.only'].description, 'Cached description');
|
||||
assert.equal(rows['test.plugin'].enabled, false);
|
||||
assert.equal(rows['omarchy.clock'].enabled, true);
|
||||
// A later marketplace response must not replace installed metadata.
|
||||
context.mergeMarketplaceMetadata(context.marketplaceMap);
|
||||
assert.equal(context.pluginRows.find(row => row.id === 'test.plugin').description,
|
||||
'Installed test.plugin');
|
||||
}
|
||||
console.log('plugin-metadata-test: ok');
|
||||
const page = fs.readFileSync(path.join(__dirname, '../panel/updates/Page.qml'), 'utf8');
|
||||
const start = page.indexOf(' function verificationText(');
|
||||
vm.runInContext(page.slice(start, page.indexOf('\n }', start) + 4), context);
|
||||
context.applyMarketplaceCatalog(JSON.stringify({ plugins: [{
|
||||
id: 'listed', verificationStatus: 'unverified',
|
||||
verificationSnapshotStatus: 'verified', verificationCoverage: 'update-unverified'
|
||||
}] }));
|
||||
assert.equal(context.marketplaceMap.listed.snapshotStatus, 'update-unverified');
|
||||
context.updateStates = { folder: 'UPDATE' };
|
||||
context.marketplaceFetching = false;
|
||||
context.marketplaceFetchFailed = false;
|
||||
assert.equal(context.verificationText('listed', 'folder'), 'Update Unverified');
|
||||
assert.equal(context.verificationText('unlisted', 'folder'), 'Not listed');
|
||||
context.updateStates.folder = 'CURRENT';
|
||||
assert.equal(context.verificationText('listed', 'folder'), 'Update Unverified');
|
||||
context.marketplaceMap.listed.verified = true;
|
||||
assert.equal(context.verificationText('listed', 'folder'), 'Verified');
|
||||
context.marketplaceMap.listed.verified = false;
|
||||
context.marketplaceMap.listed.snapshotStatus = '';
|
||||
assert.equal(context.verificationText('listed', 'folder'), 'Unverified');
|
||||
context.updateStates.folder = 'UPDATE';
|
||||
assert.equal(context.verificationText('listed', 'folder'), 'Unverified');
|
||||
console.log('update-verification-test: ok');
|
||||
const reopenStart = source.indexOf(' function keepOpenAcrossRebuild(');
|
||||
vm.runInContext(source.slice(reopenStart, source.indexOf('\n }', reopenStart) + 4), context);
|
||||
context.keepOpenFlagWrite = {};
|
||||
context.layoutPageOpen = true;
|
||||
context.keepOpenAcrossRebuild();
|
||||
assert.equal(context.keepOpenFlagWrite.command.at(-1), 'layout');
|
||||
assert.equal(context.keepOpenFlagWrite.running, true);
|
||||
context.layoutPageOpen = false;
|
||||
context.keepOpenAcrossRebuild();
|
||||
assert.equal(context.keepOpenFlagWrite.command.at(-1), 'main');
|
||||
assert.match(source, /^ keepOpenFlagRead\.running = true$/m);
|
||||
console.log('panel-reopen-test: ok');
|
||||
const settingsStart = source.indexOf(' function persistAutoCheckSetting(');
|
||||
vm.runInContext(source.slice(settingsStart, source.indexOf('\n }', settingsStart) + 4), context);
|
||||
context.autoCheckSettingsProcess = { running: false };
|
||||
context.moduleName = 'omaplug';
|
||||
context.persistAutoCheckSetting({ autoCheckUpdates: false });
|
||||
assert.deepEqual(Array.from(context.autoCheckSettingsProcess.command),
|
||||
['omarchy', 'bar', 'set', 'omaplug', 'autoCheckUpdates', 'false', '--json']);
|
||||
assert.equal(context.autoCheckSettingsProcess.running, true);
|
||||
context.persistAutoCheckSetting({ autoCheckIntervalHours: 3 });
|
||||
assert.equal(context.autoCheckSettingsProcess.command[4], 'autoCheckUpdates');
|
||||
context.autoCheckSettingsProcess.running = false;
|
||||
context.persistAutoCheckSetting({ autoCheckIntervalHours: 3 });
|
||||
assert.deepEqual(Array.from(context.autoCheckSettingsProcess.command),
|
||||
['omarchy', 'bar', 'set', 'omaplug', 'autoCheckIntervalHours', '3', '--json']);
|
||||
console.log('auto-check-settings-cli-test: ok');
|
||||
} finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf -- "$TMP"' EXIT
|
||||
|
||||
PLUGINS="$TMP/plugins"
|
||||
SEED="$TMP/seed"
|
||||
REMOTE="$TMP/origin.git"
|
||||
mkdir -p -- "$PLUGINS"
|
||||
|
||||
git init --bare -q "$REMOTE"
|
||||
git -C "$REMOTE" symbolic-ref HEAD refs/heads/main
|
||||
git init -q -b main "$SEED"
|
||||
git -C "$SEED" config user.name "Omaplug Tests"
|
||||
git -C "$SEED" config user.email "omaplug-tests@example.invalid"
|
||||
printf '{"id":"fixture"}\n' > "$SEED/manifest.json"
|
||||
git -C "$SEED" add manifest.json
|
||||
git -C "$SEED" commit -qm "initial"
|
||||
git -C "$SEED" remote add origin "$REMOTE"
|
||||
# Fixture pushes must not inherit the user's global hooks. Those hooks may
|
||||
# intentionally reject non-GitHub remotes, while this test uses a local bare
|
||||
# repository by design.
|
||||
git -C "$SEED" -c core.hooksPath=/dev/null push -q -u origin main
|
||||
|
||||
# Keep one checkout at the initial commit. It becomes behind after the second
|
||||
# upstream commit, and a local commit on a copy of it creates a divergence.
|
||||
git clone -q "$REMOTE" "$PLUGINS/behind"
|
||||
git clone -q "$REMOTE" "$PLUGINS/diverged"
|
||||
|
||||
printf '{"id":"fixture","version":2}\n' > "$SEED/manifest.json"
|
||||
git -C "$SEED" add manifest.json
|
||||
git -C "$SEED" commit -qm "upstream update"
|
||||
git -C "$SEED" -c core.hooksPath=/dev/null push -q
|
||||
|
||||
git clone -q "$REMOTE" "$PLUGINS/current"
|
||||
git clone -q "$REMOTE" "$PLUGINS/dirty"
|
||||
git clone -q "$REMOTE" "$PLUGINS/ahead"
|
||||
git clone -q "$REMOTE" "$PLUGINS/fetch-error"
|
||||
git clone -q "$REMOTE" "$PLUGINS/detached"
|
||||
|
||||
printf '\n' >> "$PLUGINS/dirty/manifest.json"
|
||||
git -C "$PLUGINS/detached" checkout -q --detach
|
||||
|
||||
git -C "$PLUGINS/ahead" config user.name "Omaplug Tests"
|
||||
git -C "$PLUGINS/ahead" config user.email "omaplug-tests@example.invalid"
|
||||
printf '{"id":"fixture","version":3}\n' > "$PLUGINS/ahead/manifest.json"
|
||||
git -C "$PLUGINS/ahead" add manifest.json
|
||||
git -C "$PLUGINS/ahead" commit -qm "local commit"
|
||||
|
||||
git -C "$PLUGINS/diverged" config user.name "Omaplug Tests"
|
||||
git -C "$PLUGINS/diverged" config user.email "omaplug-tests@example.invalid"
|
||||
printf '{"id":"fixture","local":true}\n' > "$PLUGINS/diverged/manifest.json"
|
||||
git -C "$PLUGINS/diverged" add manifest.json
|
||||
git -C "$PLUGINS/diverged" commit -qm "divergent local commit"
|
||||
|
||||
git -C "$PLUGINS/fetch-error" remote set-url origin "$TMP/missing.git"
|
||||
|
||||
mkdir -p -- "$PLUGINS/plain" "$PLUGINS/no-origin"
|
||||
printf '{"id":"plain"}\n' > "$PLUGINS/plain/manifest.json"
|
||||
printf '{"id":"no-origin"}\n' > "$PLUGINS/no-origin/manifest.json"
|
||||
git init -q "$PLUGINS/no-origin"
|
||||
git -C "$PLUGINS/no-origin" config user.name "Omaplug Tests"
|
||||
git -C "$PLUGINS/no-origin" config user.email "omaplug-tests@example.invalid"
|
||||
git -C "$PLUGINS/no-origin" add manifest.json
|
||||
git -C "$PLUGINS/no-origin" commit -qm "local plugin"
|
||||
ln -s -- "$PLUGINS/current" "$PLUGINS/symlinked"
|
||||
|
||||
OUTPUT=$($ROOT/plugin-state.sh "$PLUGINS")
|
||||
|
||||
state_for() {
|
||||
local id="$1"
|
||||
awk -F '\t' -v id="$id" '$1 != "CHECK" && $2 == id { print $1 }' <<< "$OUTPUT"
|
||||
}
|
||||
|
||||
detail_for() {
|
||||
local id="$1"
|
||||
awk -F '\t' -v id="$id" '$1 != "CHECK" && $2 == id { print $4 }' <<< "$OUTPUT"
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local expected="$1"
|
||||
local actual="$2"
|
||||
local label="$3"
|
||||
if [[ $actual != "$expected" ]]; then
|
||||
printf 'FAIL: %s: expected %q, got %q\n' "$label" "$expected" "$actual" >&2
|
||||
printf '%s\n' "$OUTPUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_eq CURRENT "$(state_for current)" current
|
||||
assert_eq UPDATE "$(state_for behind)" behind
|
||||
assert_eq LOCAL_CHANGES "$(state_for dirty)" dirty
|
||||
assert_eq modified "$(detail_for dirty)" dirty-detail
|
||||
assert_eq LOCAL_CHANGES "$(state_for detached)" detached
|
||||
assert_eq detached "$(detail_for detached)" detached-detail
|
||||
assert_eq LOCAL_CHANGES "$(state_for ahead)" ahead
|
||||
assert_eq ahead "$(detail_for ahead)" ahead-detail
|
||||
assert_eq LOCAL_CHANGES "$(state_for diverged)" diverged
|
||||
assert_eq diverged "$(detail_for diverged)" diverged-detail
|
||||
assert_eq LOCAL "$(state_for plain)" plain
|
||||
assert_eq LOCAL "$(state_for no-origin)" no-origin
|
||||
assert_eq LOCAL "$(state_for symlinked)" symlinked
|
||||
assert_eq ERROR "$(state_for fetch-error)" fetch-error
|
||||
|
||||
current_url=$(awk -F '\t' '$1 == "CURRENT" && $2 == "current" { print $3 }' <<< "$OUTPUT")
|
||||
assert_eq "$REMOTE" "$current_url" origin-url-without-trailing-space
|
||||
|
||||
printf 'plugin-state-test: ok\n'
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
printf 'quickshell-detached-test: skipped (quickshell not installed)\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf -- "$TMP"' EXIT
|
||||
|
||||
BIN="$TMP/bin"
|
||||
TEST_HOME="$TMP/home"
|
||||
PLUGINS="$TEST_HOME/.config/omarchy/plugins"
|
||||
STATUS="$TMP/update.status"
|
||||
mkdir -p -- "$BIN" "$PLUGINS"
|
||||
|
||||
for id in slow good; do
|
||||
dir="$PLUGINS/$id"
|
||||
git init -q -b main "$dir"
|
||||
git -C "$dir" config user.name "Omaplug Tests"
|
||||
git -C "$dir" config user.email "omaplug-tests@example.invalid"
|
||||
printf '{"id":"%s"}\n' "$id" > "$dir/manifest.json"
|
||||
git -C "$dir" add manifest.json
|
||||
git -C "$dir" commit -qm "fixture"
|
||||
done
|
||||
|
||||
cat > "$BIN/omarchy" <<'EOF'
|
||||
#!/bin/bash
|
||||
[[ ${3:-} == slow ]] && sleep 0.4
|
||||
printf 'updated %s\n' "${3:-}"
|
||||
EOF
|
||||
chmod +x "$BIN/omarchy"
|
||||
|
||||
PATH="$BIN:$PATH" \
|
||||
HOME="$TEST_HOME" \
|
||||
OMAPLUG_TEST_HELPER="$ROOT/update-helper.sh" \
|
||||
OMAPLUG_TEST_STATUS="$STATUS" \
|
||||
QT_QPA_PLATFORM=offscreen \
|
||||
quickshell --no-color -p "$ROOT/tests/DetachedLaunch.qml" >/dev/null 2>&1
|
||||
|
||||
for _ in {1..200}; do
|
||||
grep -Fqx $'done\t2\t0' "$STATUS" 2>/dev/null && break
|
||||
sleep 0.01
|
||||
done
|
||||
|
||||
grep -Fqx $'job\tjob-quickshell' "$STATUS"
|
||||
grep -Fqx $'done\t2\t0' "$STATUS"
|
||||
printf 'quickshell-detached-test: ok\n'
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
"$ROOT/plugin-state-test.sh"
|
||||
"$ROOT/update-helper-test.sh"
|
||||
"$ROOT/quickshell-detached-test.sh"
|
||||
bash "$ROOT/verification-status-test.sh"
|
||||
node "$ROOT/plugin-metadata-test.cjs"
|
||||
"$ROOT/auto-check-test.sh"
|
||||
"$ROOT/auto-check-coordinator-test.sh"
|
||||
"$ROOT/nested-widget-toggle-test.sh"
|
||||
python3 "$ROOT/security-regression-test.py"
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Isolated filesystem regressions; never touches installed plugins."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class StateSafety(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory(prefix="omaplug-security-")
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.path = Path(self.tmp.name)
|
||||
self.env = dict(os.environ, XDG_RUNTIME_DIR=str(self.path))
|
||||
(self.path / "omaplug").mkdir(mode=0o700)
|
||||
(self.path / "plugins").mkdir()
|
||||
self.victim = self.path / "victim"
|
||||
self.victim.write_text("keep me")
|
||||
|
||||
def run_helper(self, *args):
|
||||
return subprocess.run(args, env=self.env, capture_output=True, timeout=10)
|
||||
|
||||
def test_cache_symlink_never_truncates_target(self):
|
||||
(self.path / "omaplug/auto-check.cache.tmp").symlink_to(self.victim)
|
||||
self.run_helper("bash", str(ROOT / "auto-check-coordinator.sh"), str(self.path / "plugins"))
|
||||
self.assertEqual(self.victim.read_text(), "keep me")
|
||||
|
||||
def test_update_status_symlink_is_rejected(self):
|
||||
status = self.path / "update.status"
|
||||
status.symlink_to(self.victim)
|
||||
result = self.run_helper("bash", str(ROOT / "update-helper.sh"), str(status), "test", "invalid/id")
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(self.victim.read_text(), "keep me")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf -- "$TMP"' EXIT
|
||||
|
||||
BIN="$TMP/bin"
|
||||
LOG="$TMP/calls.log"
|
||||
STATUS="$TMP/update.status"
|
||||
TEST_HOME="$TMP/home"
|
||||
PLUGINS="$TEST_HOME/.config/omarchy/plugins"
|
||||
mkdir -p -- "$BIN" "$PLUGINS"
|
||||
|
||||
make_plugin() {
|
||||
local id="$1"
|
||||
local dir="$PLUGINS/$id"
|
||||
git init -q -b main "$dir"
|
||||
git -C "$dir" config user.name "Omaplug Tests"
|
||||
git -C "$dir" config user.email "omaplug-tests@example.invalid"
|
||||
printf '{"id":"%s"}\n' "$id" > "$dir/manifest.json"
|
||||
git -C "$dir" add manifest.json
|
||||
git -C "$dir" commit -qm "fixture"
|
||||
}
|
||||
|
||||
for id in good fail slow dirty detached; do make_plugin "$id"; done
|
||||
printf '\n' >> "$PLUGINS/dirty/manifest.json"
|
||||
git -C "$PLUGINS/detached" checkout -q --detach
|
||||
ln -s -- "$PLUGINS/good" "$PLUGINS/linked"
|
||||
|
||||
cat > "$BIN/omarchy" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >> "$OMAPLUG_TEST_LOG"
|
||||
case "${3:-}" in
|
||||
slow) sleep 0.4 ;;
|
||||
esac
|
||||
if [[ ${3:-} == fail ]]; then
|
||||
printf 'could not update %s\n' "$3" >&2
|
||||
exit 7
|
||||
fi
|
||||
printf 'updated %s\n' "${3:-}"
|
||||
EOF
|
||||
chmod +x "$BIN/omarchy"
|
||||
|
||||
export PATH="$BIN:$PATH"
|
||||
export OMAPLUG_TEST_LOG="$LOG"
|
||||
export HOME="$TEST_HOME"
|
||||
|
||||
# A prior run may have left a status file created under a permissive umask.
|
||||
# The helper must tighten it before writing any job details.
|
||||
: > "$STATUS"
|
||||
chmod 0644 "$STATUS"
|
||||
|
||||
assert_line() {
|
||||
local line="$1"
|
||||
local file="$2"
|
||||
grep -Fqx -- "$line" "$file" || {
|
||||
printf 'FAIL: missing line %q in %s\n' "$line" "$file" >&2
|
||||
cat "$file" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
if "$ROOT/update-helper.sh" "$STATUS" job-direct good fail dirty detached linked 'bad/id'; then
|
||||
printf 'FAIL: a partially failed update job returned success\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
assert_line $'job\tjob-direct' "$STATUS"
|
||||
assert_line $'start\tgood' "$STATUS"
|
||||
assert_line $'ok\tgood\tupdated good' "$STATUS"
|
||||
assert_line $'start\tfail' "$STATUS"
|
||||
assert_line $'failed\tfail\tcould not update fail' "$STATUS"
|
||||
assert_line $'failed\tdirty\tplugin gained local changes after the update check' "$STATUS"
|
||||
assert_line $'failed\tdetached\tplugin is on a detached HEAD' "$STATUS"
|
||||
assert_line $'failed\tlinked\tplugin became a symlink after the update check' "$STATUS"
|
||||
assert_line $'failed\tbad/id\tinvalid plugin id' "$STATUS"
|
||||
assert_line $'done\t1\t5' "$STATUS"
|
||||
[[ $(stat -c '%a' "$STATUS") == 600 ]] || {
|
||||
printf 'FAIL: status file mode is %s instead of 600\n' "$(stat -c '%a' "$STATUS")" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $(tail -n 1 "$STATUS") == $'done\t1\t5' ]] || {
|
||||
printf 'FAIL: done is not the final status record\n' >&2
|
||||
cat "$STATUS" >&2
|
||||
exit 1
|
||||
}
|
||||
assert_line 'plugin update good --yes' "$LOG"
|
||||
assert_line 'plugin update fail --yes' "$LOG"
|
||||
[[ $(wc -l < "$LOG") -eq 2 ]] || {
|
||||
printf 'FAIL: invalid plugin id reached omarchy\n' >&2
|
||||
cat "$LOG" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# A live runner owns the stable status path. A second runner must fail without
|
||||
# replacing or appending to that job's status stream.
|
||||
: > "$LOG"
|
||||
"$ROOT/update-helper.sh" "$STATUS" job-first slow &
|
||||
first_pid=$!
|
||||
for _ in {1..100}; do
|
||||
[[ -f $STATUS.flock ]] && break
|
||||
sleep 0.01
|
||||
done
|
||||
sleep 0.05
|
||||
if "$ROOT/update-helper.sh" "$STATUS" job-second good; then
|
||||
printf 'FAIL: a concurrent update runner acquired the same lock\n' >&2
|
||||
exit 1
|
||||
else
|
||||
second_rc=$?
|
||||
fi
|
||||
[[ $second_rc -eq 3 ]] || {
|
||||
printf 'FAIL: concurrent runner returned %s instead of 3\n' "$second_rc" >&2
|
||||
exit 1
|
||||
}
|
||||
wait "$first_pid"
|
||||
assert_line $'job\tjob-first' "$STATUS"
|
||||
if grep -Fq $'job\tjob-second' "$STATUS"; then
|
||||
printf 'FAIL: concurrent runner clobbered the active status\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'update-helper-test: ok\n'
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
PAGE="$ROOT/panel/updates/Page.qml"
|
||||
PANEL="$ROOT/Panel.qml"
|
||||
|
||||
assert_contains() {
|
||||
local needle="$1"
|
||||
local file="$2"
|
||||
grep -Fq -- "$needle" "$file" || {
|
||||
printf 'FAIL: missing %q in %s\n' "$needle" "$file" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
assert_contains 'required property var marketplaceMap' "$PAGE"
|
||||
assert_contains 'required property bool marketplaceFetchFailed' "$PAGE"
|
||||
assert_contains 'function verificationText(id, sourceKey)' "$PAGE"
|
||||
assert_contains 'page.verificationText(updateRow.modelData.id, updateRow.modelData.sourceKey)' "$PAGE"
|
||||
assert_contains 'marketplaceMap: root.marketplaceMap' "$PANEL"
|
||||
assert_contains 'marketplaceFetching: root.marketplaceFetching' "$PANEL"
|
||||
assert_contains 'marketplaceFetchFailed: root.marketplaceFetchFailed' "$PANEL"
|
||||
assert_contains 'marketplaceProcess.command = [root.marketplaceHelperPath]' "$PANEL"
|
||||
assert_contains '--max-filesize 16777216' "$ROOT/marketplace-catalog.sh"
|
||||
|
||||
printf 'verification-status-test: ok\n'
|
||||
Reference in New Issue
Block a user