Add Drive Health (gustav0ar/drive-health) (#58)

* add Drive Health

* declare optional package manager commands

* use live Drive Health screenshot

* hide redundant dependency subtitle

* fix drive error counter alerts

* stabilize transient SMART availability

* address collector packaging review

* refactor: use generic collector service name

* test: harden collector packaging checks
This commit is contained in:
Gustavo de Andrade Rosa
2026-07-21 11:14:38 -04:00
committed by GitHub
parent a55b9e6b8a
commit ddb054ae3c
26 changed files with 6222 additions and 0 deletions
+504
View File
@@ -0,0 +1,504 @@
-- Behavioral tests for the isolated alert service.
local state = {}
local watchers = {}
local notifications = {}
local stateWrites = {}
local stateRenames = {}
local successfulStateCommits = 0
local failNextStateWrite = false
local failNextStateRename = false
local fullSmartEnabled = false
local function translate(key, substitutions)
local value = key
for name, replacement in pairs(substitutions or {}) do
value = value:gsub("{" .. name .. "}", tostring(replacement))
end
if substitutions ~= nil and substitutions.count ~= nil then
value = value .. ":count=" .. tostring(substitutions.count)
end
return value
end
noctalia = {
getConfig = function(key)
local values = {
alerts_enabled = true,
notify_recovery = true,
warning_temperature = 65,
critical_temperature = 80,
life_warning_percent = 20,
show_hdd = false,
alert_hdd = true,
drive_missing_alerts = true,
missing_grace_scans = 3,
use_hotspot_temperature = true,
system_collector_enabled = fullSmartEnabled,
}
return values[key]
end,
pluginDataDir = function() return "/mock/plugin-data" end,
readFile = function(path)
return path:match("alert%-state%.json$") and "{}" or nil
end,
writeFile = function(path, _contents)
table.insert(stateWrites, path)
if failNextStateWrite then
failNextStateWrite = false
return false, "fixture write failure"
end
return true
end,
renameFile = function(from, to)
table.insert(stateRenames, { from = from, to = to })
if failNextStateRename then
failNextStateRename = false
return false, "fixture rename failure"
end
successfulStateCommits = successfulStateCommits + 1
return true
end,
log = function(_message) end,
notify = function(title, body)
table.insert(notifications, { severity = "warning", title = title, body = body })
end,
notifyError = function(title, body)
table.insert(notifications, { severity = "critical", title = title, body = body })
end,
tr = translate,
state = {
get = function(key) return state[key] end,
set = function(key, value) state[key] = value end,
watch = function(key, callback) watchers[key] = callback end,
},
json = {
decode = function(_raw)
return {
active = {
["SERIAL1:interface-crc"] = {
id = "SERIAL1:interface-crc", kind = "interface-crc",
drive = "Fixture SSD", message = "historical CRC total", severity = "warning",
},
},
counters = {}, inventory = {}, availability = {}, dismissed = {},
}
end,
encode = function(_value, _pretty) return "{}" end,
},
}
local handle = assert(io.open("service.luau", "rb"))
local source = handle:read("*a")
handle:close()
source = source:gsub("([%a_][%w_]*) %+%= ([^\n]+)", "%1 = %1 + %2")
assert(load(source, "@service.luau"))()
local publishSnapshot = assert(watchers.collector_snapshot, "alert service did not watch collector snapshots")
local function publish(value)
state.collector_snapshot = value
publishSnapshot(value)
end
local dismiss = assert(watchers.dismiss_alert_request, "alert service did not watch dismissal requests")
local function drive(temperature)
return {
id = "SERIAL1", device = "/dev/nvme0n1", model = "Fixture SSD",
kind = "ssd", health = "passed", smart_available = true,
temperature_c = temperature, hotspot_temperature_c = temperature, remaining_life_percent = 95,
available_spare_percent = 100, critical_warning = 0,
media_errors = 0, reallocated_sectors = 0, pending_sectors = 0,
uncorrectable_errors = 0, spin_retry_count = 0, command_timeout_count = 0,
interface_crc_errors = 0, unsafe_shutdowns = 12, error_log_entries = 5893,
smart_completeness = "full", alerts_enabled = true, presence_alert_enabled = true,
self_test_state = "passed",
}
end
local function snapshot(disk, collectionId)
return {
collection_id = collectionId,
disks = disk ~= nil and { disk } or {},
dependencies = { ready = true, missing = {}, missing_text = "", signature = "" },
summary = { ssd_count = disk ~= nil and 1 or 0 },
}
end
local function findIssue(kind)
for _, issue in ipairs(state.snapshot.issues or {}) do
if issue.kind == kind then
return issue
end
end
return nil
end
publish(snapshot(drive(45)))
assert(#state.snapshot.issues == 0, "healthy drive produced an alert")
assert(#notifications == 0, "healthy baseline or legacy counter cleanup produced a notification")
assert(#stateWrites == 1 and #stateRenames == 1 and successfulStateCommits == 1,
"first healthy baseline was not persisted as one atomic commit")
publish(snapshot(drive(45)))
onConfigChanged()
assert(#stateWrites == 1 and #stateRenames == 1,
"identical or config-only processing rewrote alert state")
local writesBeforeNewAlert = #stateWrites
publish(snapshot(drive(70)))
assert(#state.snapshot.issues == 1 and state.snapshot.issues[1].kind == "temperature", "warning temperature was missed")
assert(#notifications == 1 and notifications[1].severity == "warning", "warning notification was not sent")
assert(#stateWrites == writesBeforeNewAlert + 1 and #stateRenames == writesBeforeNewAlert + 1,
"new alert was not persisted as exactly one atomic commit")
publish(snapshot(drive(70)))
assert(#notifications == 1, "unchanged warning notification was duplicated")
assert(#stateWrites == writesBeforeNewAlert + 1,
"unchanged active alert rewrote alert state")
publish(snapshot(drive(63)))
assert(#state.snapshot.issues == 1 and state.snapshot.issues[1].message == "alerts.temperature_cooling",
"temperature hysteresis displayed a contradictory threshold message")
assert(#notifications == 1, "cooling hysteresis duplicated its warning notification")
publish(snapshot(drive(85)))
assert(state.snapshot.issues[1].severity == "critical", "critical escalation was missed")
assert(#notifications == 2 and notifications[2].severity == "critical", "critical escalation did not notify")
local writesBeforeRecovery = #stateWrites
publish(snapshot(drive(60)))
assert(#state.snapshot.issues == 0, "temperature recovery did not clear")
assert(#notifications == 3 and notifications[3].severity == "warning", "recovery did not notify")
assert(#stateWrites == writesBeforeRecovery + 1 and #stateRenames == writesBeforeRecovery + 1,
"alert recovery was not persisted as exactly one atomic commit")
publish(snapshot(drive(70)))
assert(#notifications == 4, "recurring temperature issue did not notify")
publish(snapshot(drive(45)))
assert(#notifications == 5, "temperature recovery did not notify")
local retiredFixture = drive(45)
retiredFixture.presence_alert_enabled = false
publish(snapshot(retiredFixture))
local hddIssue = drive(35)
hddIssue.id = "HDD-SERIAL"
hddIssue.kind = "hdd"
hddIssue.remaining_life_percent = nil
hddIssue.presence_alert_enabled = false
hddIssue.interface_crc_errors = 2
local notificationsBeforeHddBaseline = #notifications
publish(snapshot(hddIssue))
assert(#state.snapshot.issues == 0 and #notifications == notificationsBeforeHddBaseline,
"historical HDD interface CRC errors produced a false alert")
hddIssue.interface_crc_errors = 3
publish(snapshot(hddIssue))
assert(#state.snapshot.issues == 0, "new HDD interface CRC errors produced a persistent issue")
assert(#notifications == notificationsBeforeHddBaseline + 1
and notifications[#notifications].severity == "warning"
and notifications[#notifications].body:match("count=1"),
"new HDD interface CRC error did not produce one delta notification")
publish(snapshot(hddIssue))
assert(#notifications == notificationsBeforeHddBaseline + 1,
"unchanged HDD interface CRC total duplicated its notification")
hddIssue.interface_crc_errors = 0
publish(snapshot(hddIssue))
assert(#notifications == notificationsBeforeHddBaseline + 1,
"decreased HDD interface CRC total produced a notification")
local historicalErrors = drive(45)
historicalErrors.id = "HISTORICAL-SERIAL"
historicalErrors.presence_alert_enabled = false
historicalErrors.media_errors = 8
historicalErrors.reallocated_sectors = 4
historicalErrors.pending_sectors = 2
historicalErrors.uncorrectable_errors = 3
historicalErrors.spin_retry_count = 1
historicalErrors.command_timeout_count = 6
historicalErrors.interface_crc_errors = 9
historicalErrors.error_log_entries = 20
local notificationsBeforeHistoricalBaseline = #notifications
publish(snapshot(historicalErrors))
assert(#state.snapshot.issues == 0 and #notifications == notificationsBeforeHistoricalBaseline,
"first observation of historical SMART counters produced false alerts")
historicalErrors.uncorrectable_errors = 5
historicalErrors.error_log_entries = 21
historicalErrors.interface_crc_errors = 11
publish(snapshot(historicalErrors))
assert(#state.snapshot.issues == 0, "counter deltas produced persistent drive issues")
assert(#notifications == notificationsBeforeHistoricalBaseline + 3,
"new SMART counter values did not produce exactly one notification per increase")
assert(notifications[notificationsBeforeHistoricalBaseline + 1].severity == "critical"
and notifications[notificationsBeforeHistoricalBaseline + 1].body:match("count=2"),
"uncorrectable-error delta notification was incorrect")
assert(notifications[notificationsBeforeHistoricalBaseline + 2].severity == "warning"
and notifications[notificationsBeforeHistoricalBaseline + 2].body:match("count=2"),
"interface CRC delta notification was incorrect")
assert(notifications[notificationsBeforeHistoricalBaseline + 3].severity == "warning"
and notifications[notificationsBeforeHistoricalBaseline + 3].body:match("count=1"),
"error-log delta notification was incorrect")
publish(snapshot(historicalErrors))
local unchangedCounterNotifications = {}
for index = notificationsBeforeHistoricalBaseline + 1, #notifications do
table.insert(unchangedCounterNotifications, notifications[index].body)
end
assert(#notifications == notificationsBeforeHistoricalBaseline + 3,
"unchanged SMART counter values duplicated notifications: " .. table.concat(unchangedCounterNotifications, ", "))
publish(snapshot(drive(45)))
local hotspot = drive(45)
hotspot.hotspot_temperature_c = 70
publish(snapshot(hotspot))
assert(state.snapshot.issues[1].kind == "temperature", "NVMe hotspot warning was missed")
publish(snapshot(drive(45)))
local customThreshold = drive(55)
customThreshold.warning_temperature = 50
customThreshold.critical_temperature = 70
publish(snapshot(customThreshold))
assert(state.snapshot.issues[1].kind == "temperature" and state.snapshot.issues[1].severity == "warning",
"per-drive temperature threshold was ignored")
publish(snapshot(drive(45)))
local partial = drive(45)
partial.smart_completeness = "partial"
publish(snapshot(partial))
assert(#state.snapshot.issues == 0, "healthy partial SMART data produced an alert")
publish(snapshot(drive(45)))
local unavailable = drive(45)
unavailable.smart_available = false
publish(snapshot(unavailable))
assert(#state.snapshot.issues == 0,
"Basic mode produced a false SMART-unavailable warning")
fullSmartEnabled = true
local notificationsBeforeUnavailable = #notifications
local writesBeforeUnavailable = #stateWrites
publish(snapshot(unavailable, "smart-unavailable-transient"))
assert(#state.snapshot.issues == 0 and #notifications == notificationsBeforeUnavailable,
"a single transient SMART read failure produced an alert")
assert(#stateWrites == writesBeforeUnavailable + 1,
"the first unavailable SMART scan did not persist its pending state")
publish(snapshot(unavailable, "smart-unavailable-transient"))
onConfigChanged()
assert(#state.snapshot.issues == 0 and #notifications == notificationsBeforeUnavailable,
"reprocessing one unavailable SMART snapshot advanced its grace period")
assert(#stateWrites == writesBeforeUnavailable + 1,
"reprocessing one unavailable SMART snapshot rewrote pending state")
publish(snapshot(drive(45), "smart-available-reset"))
assert(#state.snapshot.issues == 0 and #notifications == notificationsBeforeUnavailable,
"transient SMART availability recovery produced a notification")
publish(snapshot(unavailable, "smart-unavailable-1"))
publish(snapshot(unavailable, "smart-unavailable-2"))
assert(#state.snapshot.issues == 0 and #notifications == notificationsBeforeUnavailable,
"SMART unavailability alerted before three completed scans")
publish(snapshot(unavailable, "smart-unavailable-3"))
assert(#state.snapshot.issues == 1 and state.snapshot.issues[1].kind == "smart-unavailable",
"sustained SMART unavailability did not alert after three completed scans")
assert(#notifications == notificationsBeforeUnavailable + 1,
"sustained SMART unavailability did not produce exactly one notification")
local writesAfterConfirmedUnavailable = #stateWrites
publish(snapshot(unavailable, "smart-unavailable-4"))
assert(#notifications == notificationsBeforeUnavailable + 1,
"continued SMART unavailability duplicated its notification")
assert(#stateWrites == writesAfterConfirmedUnavailable,
"confirmed SMART unavailability rewrote stable alert state")
local sleeping = drive(45)
sleeping.smart_available = false
sleeping.smart_sleeping = true
publish(snapshot(sleeping, "smart-sleeping"))
assert(#state.snapshot.issues == 0, "sleeping drive produced a SMART-unavailable warning")
assert(#notifications == notificationsBeforeUnavailable + 2,
"confirmed SMART-unavailable recovery did not notify exactly once")
fullSmartEnabled = false
publish(snapshot(drive(45)))
local selfTestFailure = drive(45)
selfTestFailure.self_test_state = "failed"
selfTestFailure.self_test_status = "Completed with read failure"
publish(snapshot(selfTestFailure))
assert(state.snapshot.issues[1].kind == "self-test" and state.snapshot.issues[1].severity == "critical",
"self-test failure was missed")
publish(snapshot(drive(45)))
local counterIncrease = drive(45)
counterIncrease.unsafe_shutdowns = 13
local writesBeforeCounterIncrease = #stateWrites
local notificationsBeforeCounterIncrease = #notifications
publish(snapshot(counterIncrease))
assert(#stateWrites == writesBeforeCounterIncrease + 1
and #stateRenames == writesBeforeCounterIncrease + 1,
"counter increase was not persisted as exactly one atomic commit")
assert(#notifications == notificationsBeforeCounterIncrease + 1,
"counter increase notification behavior changed")
publish(snapshot(counterIncrease))
assert(#stateWrites == writesBeforeCounterIncrease + 1,
"unchanged diagnostic counter rewrote alert state")
publish(snapshot(drive(45)))
publish(snapshot(drive(45), "scan-100"))
local firstMissingScan = snapshot(nil, "scan-101")
local writesBeforeMissingScan = #stateWrites
publish(firstMissingScan)
assert(findIssue("drive-missing") == nil, "missing drive alerted before the grace period")
assert(#stateWrites == writesBeforeMissingScan + 1
and #stateRenames == writesBeforeMissingScan + 1,
"missing-drive inventory change was not persisted as exactly one atomic commit")
publish(firstMissingScan)
assert(findIssue("drive-missing") == nil, "reprocessing one snapshot advanced the grace period")
assert(#stateWrites == writesBeforeMissingScan + 1,
"reprocessing one collection rewrote unchanged inventory state")
publish(snapshot(nil, "scan-101"))
assert(findIssue("drive-missing") == nil, "a repeated collection ID advanced the grace period")
assert(#stateWrites == writesBeforeMissingScan + 1,
"a repeated collection ID rewrote unchanged inventory state")
onConfigChanged()
assert(findIssue("drive-missing") == nil, "a config refresh advanced the grace period")
assert(#stateWrites == writesBeforeMissingScan + 1,
"config refresh rewrote unchanged missing-drive state")
local collectingSnapshot = snapshot(nil, "scan-collecting")
collectingSnapshot.collecting = true
publish(collectingSnapshot)
assert(findIssue("drive-missing") == nil, "an in-progress collection advanced the grace period")
local failedMissingScan = snapshot(nil, "scan-failed")
failedMissingScan.collector_error = "fixture failure"
publish(failedMissingScan)
assert(findIssue("drive-missing") == nil, "a failed collection advanced the grace period")
local blockedMissingScan = snapshot(nil, "scan-blocked")
blockedMissingScan.dependencies = {
ready = false, blocking = true,
missing = { "lsblk (lsblk)" }, missing_text = "lsblk (lsblk)", signature = "lsblk",
}
publish(blockedMissingScan)
assert(findIssue("drive-missing") == nil, "a blocked collection advanced the grace period")
publish(snapshot(nil, "scan-102"))
assert(findIssue("drive-missing") == nil, "missing drive alerted after only two completed scans")
publish(snapshot(nil, "scan-103"))
assert(findIssue("drive-missing") ~= nil, "three unique completed scans did not trigger a missing-drive alert")
publish(snapshot(nil, "scan-103"))
assert(findIssue("drive-missing") ~= nil, "reprocessing a snapshot removed an active missing-drive alert")
local writesBeforeReappearance = #stateWrites
publish(snapshot(drive(45), "scan-104"))
assert(findIssue("drive-missing") == nil, "drive reappearance did not clear its missing alert")
assert(#stateWrites == writesBeforeReappearance + 1
and #stateRenames == writesBeforeReappearance + 1,
"drive reappearance was not persisted as exactly one atomic commit")
publish(snapshot(nil, "scan-105"))
publish(snapshot(nil, "scan-106"))
assert(findIssue("drive-missing") == nil, "reappearance did not reset the missing-drive grace period")
publish(snapshot(nil, "scan-107"))
assert(findIssue("drive-missing") ~= nil, "three new scans after reappearance did not trigger an alert")
publish(snapshot(drive(45), "scan-108"))
local legacyMissingScan = snapshot(nil)
publish(legacyMissingScan)
publish(legacyMissingScan)
assert(findIssue("drive-missing") == nil, "a repeated legacy snapshot advanced the grace period")
publish(snapshot(nil))
assert(findIssue("drive-missing") == nil, "two legacy snapshot objects triggered an early alert")
publish(snapshot(nil))
assert(findIssue("drive-missing") ~= nil, "distinct legacy snapshot objects did not advance the grace period")
local missing = snapshot(nil)
missing.dependencies = {
ready = false, blocking = true,
missing = { "lsblk (lsblk)" }, missing_text = "lsblk (lsblk)", signature = "lsblk",
}
local notificationCountBeforeDependency = #notifications
publish(missing)
assert(findIssue("missing-dependencies") ~= nil, "dependency issue was missed")
assert(findIssue("drive-missing") ~= nil, "a blocking dependency removed an active missing-drive alert")
local dependencyCritical = false
for index = notificationCountBeforeDependency + 1, #notifications do
if notifications[index].severity == "critical" then dependencyCritical = true end
end
assert(dependencyCritical, "blocking dependency was not critical")
local failed = snapshot(nil)
failed.collector_error = "fixture failure"
local notificationCountBeforeFailure = #notifications
publish(failed)
assert(findIssue("collector-error") ~= nil, "collector issue was missed")
assert(findIssue("drive-missing") ~= nil, "a collector failure removed an active missing-drive alert")
local collectorCritical = false
for index = notificationCountBeforeFailure + 1, #notifications do
if notifications[index].severity == "critical" then
collectorCritical = true
end
end
assert(collectorCritical, "collector failure did not notify critically")
local writesBeforeWriteFailure = #stateWrites
local renamesBeforeWriteFailure = #stateRenames
local commitsBeforeWriteFailure = successfulStateCommits
failNextStateWrite = true
dismiss({ id = "collector:error", nonce = 8 })
assert(#stateWrites == writesBeforeWriteFailure + 1
and #stateRenames == renamesBeforeWriteFailure
and successfulStateCommits == commitsBeforeWriteFailure,
"failed temporary write attempted a rename or lost the pending state")
publish(failed)
assert(#stateWrites == writesBeforeWriteFailure + 2
and #stateRenames == renamesBeforeWriteFailure + 1
and successfulStateCommits == commitsBeforeWriteFailure + 1,
"unchanged snapshot did not retry a failed alert-state write")
publish(failed)
assert(#stateWrites == writesBeforeWriteFailure + 2,
"successful write retry did not clear dirty alert state")
local writesBeforeRenameFailure = #stateWrites
local renamesBeforeRenameFailure = #stateRenames
local commitsBeforeRenameFailure = successfulStateCommits
failNextStateRename = true
dismiss({ all = true, nonce = 9 })
assert(#state.snapshot.issues == 0, "dismiss all did not hide every active issue")
assert(#stateWrites == writesBeforeRenameFailure + 1
and #stateRenames == renamesBeforeRenameFailure + 1
and successfulStateCommits == commitsBeforeRenameFailure,
"failed atomic rename was treated as a successful commit")
onConfigChanged()
assert(#stateWrites == writesBeforeRenameFailure + 2
and #stateRenames == renamesBeforeRenameFailure + 2
and successfulStateCommits == commitsBeforeRenameFailure + 1,
"unchanged processing did not retry a failed atomic rename")
onConfigChanged()
assert(#stateWrites == writesBeforeRenameFailure + 2,
"successful rename retry did not clear dirty alert state")
publish(snapshot(drive(45), "scan-dismissal-baseline"))
publish(snapshot(drive(70), "scan-dismissal-warning"))
assert(findIssue("temperature") ~= nil, "permanent-dismissal fixture did not create an alert")
local notificationsBeforeDismissal = #notifications
local writesBeforeDismissal = #stateWrites
local renamesBeforeDismissal = #stateRenames
dismiss({ id = "SERIAL1:temperature", nonce = 10 })
assert(findIssue("temperature") == nil, "individual dismissal did not hide the active issue")
assert(#notifications == notificationsBeforeDismissal, "dismissing an issue emitted a notification")
assert(#stateWrites == writesBeforeDismissal + 1 and #stateRenames == renamesBeforeDismissal + 1,
"dismissal was not persisted as exactly one atomic commit")
publish(snapshot(drive(85), "scan-dismissal-critical"))
assert(findIssue("temperature") == nil and #notifications == notificationsBeforeDismissal,
"a dismissed alert returned or notified after escalating")
publish(snapshot(drive(45), "scan-dismissal-recovery"))
publish(snapshot(drive(70), "scan-dismissal-recurrence"))
assert(findIssue("temperature") == nil and #notifications == notificationsBeforeDismissal,
"a dismissed alert returned or notified after recurring")
for _, path in ipairs(stateWrites) do
assert(path:match("alert%-state%.json%.tmp$"),
"alert state bypassed its temporary file")
end
for _, rename in ipairs(stateRenames) do
assert(rename.from:match("alert%-state%.json%.tmp$")
and rename.to:match("alert%-state%.json$"),
"alert state was not committed with an atomic rename")
end
print("alert behavior tests passed")
+590
View File
@@ -0,0 +1,590 @@
-- Unit and initialization tests for collector.luau using a minimal Noctalia
-- host mock. This runs with stock Lua after lowering Luau compound assignment.
local mode = arg[1] or "ready"
local state = {}
local launchedCommand = nil
local launchedCommands = {}
local logs = {}
local notifications = {}
local files = {}
local directories = {}
local watchers = {}
local pendingProbeCallback = nil
local probeCalls = 0
local probeAction = nil
local collectorEnabled = mode == "raw-cache" or mode == "outdated-raw-cache"
local available = {
lsblk = mode ~= "missing-lsblk",
smartctl = mode ~= "missing-smartctl",
pacman = true,
sudo = true,
pkexec = true,
systemctl = true,
}
local rawFixture = {
schema = 2,
collector_version = mode == "outdated-raw-cache" and "0.6.0" or "2.0.0",
collection_id = "fixture-collection-id",
generated_at_epoch = 1700000000,
lsblk = { blockdevices = {} },
smart = {},
}
local function translate(key, substitutions)
local value = key
for name, replacement in pairs(substitutions or {}) do
value = value:gsub("{" .. name .. "}", tostring(replacement))
end
return value
end
noctalia = {
commandExists = function(command) return available[command] == true end,
getConfig = function(key)
if key == "system_collector_enabled" then
return collectorEnabled
end
return nil
end,
pluginDir = function() return "/mock/plugin" end,
pluginDataDir = function() return "/mock/plugin-data" end,
fileInfo = function(path)
if (mode == "raw-cache" or mode == "outdated-raw-cache" or mode == "collector-disabled")
and path:match("raw%.json$") then
return { isDir = false, mtime = os.time() }
end
return nil
end,
fileExists = function(path) return files[path] ~= nil end,
listDir = function(path) return directories[path] or {} end,
readFile = function(path)
if (mode == "raw-cache" or mode == "outdated-raw-cache" or mode == "collector-disabled")
and path:match("raw%.json$") then
return "raw-cache"
end
return files[path]
end,
writeFile = function(path, contents) files[path] = contents return true end,
log = function(message) table.insert(logs, message) end,
notify = function(title, body) table.insert(notifications, { title = title, body = body }) end,
tr = translate,
formatTime = function(_pattern, _epoch) return "22:13:20" end,
setUpdateInterval = function(_milliseconds) end,
state = {
get = function(key) return state[key] end,
set = function(key, value)
state[key] = value
if watchers[key] ~= nil then watchers[key](value) end
end,
watch = function(key, callback) watchers[key] = callback end,
},
json = {
decode = function(_raw) return rawFixture end,
encode = function(_value, _pretty) return "{}" end,
},
string = {
trim = function(value) return tostring(value):match("^%s*(.-)%s*$") end,
},
runAsync = function(command, callback, _timeout)
launchedCommand = command
table.insert(launchedCommands, command)
if command:match("lsblk=ok") then
probeCalls = probeCalls + 1
if probeAction == "pending" then
assert(pendingProbeCallback == nil, "dependency probes overlapped")
pendingProbeCallback = callback
return true
end
if probeAction == "launch-failure" then
probeAction = nil
return false
end
if type(probeAction) == "table" then
local response = probeAction
probeAction = nil
callback(response)
return true
end
if mode == "async-incompatible-lsblk" or mode == "probe-completes-during-collection" then
pendingProbeCallback = callback
return true
end
if mode == "probe-timeout" then
callback({ exitCode = 124, stdout = "", stderr = "", timedOut = true })
return true
end
local lsblk = mode == "incompatible-lsblk" and "bad" or "ok"
local smartctl = mode == "incompatible-smartctl" and "bad" or "ok"
callback({ exitCode = 0, stdout = "lsblk=" .. lsblk .. "\nsmartctl=" .. smartctl .. "\n",
stderr = "", timedOut = false })
else
if mode == "probe-completes-during-collection" and pendingProbeCallback ~= nil then
local probe = pendingProbeCallback
pendingProbeCallback = nil
probe({ exitCode = 0, stdout = "lsblk=bad\nsmartctl=ok\n", stderr = "", timedOut = false })
end
callback({ exitCode = 0, stdout = "{}", stderr = "", timedOut = false })
end
return true
end,
}
local handle = assert(io.open("collector.luau", "rb"))
local source = handle:read("*a")
handle:close()
source = source:gsub("local function mountedUsage", "function mountedUsage")
source = source:gsub("local function normalizeSmart", "function normalizeSmart")
source = source:gsub("local function normalizeRaw", "function normalizeRaw")
source = source:gsub("local function publishError", "function publishError")
source = source:gsub("([%a_][%w_]*) %+%= ([^\n]+)", "%1 = %1 + %2")
assert(load(source, "@collector.luau"))()
local snapshot = assert(state.collector_snapshot, "collector did not publish an initialization snapshot")
local dependencies = assert(snapshot.dependencies, "snapshot has no dependency state")
if mode == "probe-completes-during-collection" then
assert(dependencies.ready == false and dependencies.blocking == true,
"collector published stale dependency state when the probe completed during collection")
print("collector initialization test passed: " .. mode)
return
elseif mode == "async-incompatible-lsblk" then
assert(dependencies.ready == true and pendingProbeCallback ~= nil,
"asynchronous capability probe was not pending")
pendingProbeCallback({ exitCode = 0, stdout = "lsblk=bad\nsmartctl=ok\n", stderr = "", timedOut = false })
dependencies = assert(state.collector_snapshot.dependencies)
assert(dependencies.ready == false and dependencies.blocking == true,
"completed capability probe did not immediately refresh dependency state")
print("collector initialization test passed: " .. mode)
return
elseif mode == "probe-timeout" then
assert(dependencies.ready == false and dependencies.blocking == true,
"failed capability probe incorrectly reported dependencies ready")
print("collector initialization test passed: " .. mode)
return
elseif mode == "missing-lsblk" then
assert(dependencies.ready == false and dependencies.blocking == true, "missing lsblk was not blocking")
assert(dependencies.install_command == "sudo pacman -S --needed util-linux", "wrong lsblk install command")
assert(launchedCommand == nil, "collector launched without lsblk")
assert(snapshot.collector_error ~= nil, "blocking dependency did not publish an error")
print("collector initialization test passed: " .. mode)
return
elseif mode == "missing-smartctl" then
assert(dependencies.ready == false and dependencies.blocking == false, "missing smartctl blocked inventory")
assert(dependencies.install_command == "sudo pacman -S --needed smartmontools", "wrong smartctl install command")
assert(launchedCommand and launchedCommand:match("collect_raw%.sh"), "fallback collector did not launch")
print("collector initialization test passed: " .. mode)
return
elseif mode == "raw-cache" then
assert(snapshot.source == "system-cache", "raw cache was not normalized")
assert(snapshot.collection_id == "fixture-collection-id", "raw cache lost its collection ID")
assert(snapshot.system_collector.status == "healthy"
and snapshot.system_collector.version == "2.0.0"
and snapshot.system_collector.expected_version == "2.0.0",
"current system collector was not reported healthy")
assert(not (launchedCommand or ""):match("collect_raw%.sh"), "collector launched despite a fresh raw cache")
print("collector initialization test passed: " .. mode)
return
elseif mode == "outdated-raw-cache" then
assert(snapshot.source == "system-cache", "outdated raw cache was not normalized")
assert(snapshot.system_collector.status == "upgrade-required"
and snapshot.system_collector.version == "0.6.0"
and snapshot.system_collector.expected_version == "2.0.0",
"older system collector did not request an upgrade")
assert(not (launchedCommand or ""):match("collect_raw%.sh"), "collector launched despite a fresh raw cache")
assert(#notifications == 1 and notifications[1].title == "collector.update_title"
and notifications[1].body == "collector.update_body",
"enabled outdated collector did not produce one coordinated update notice")
print("collector initialization test passed: " .. mode)
return
elseif mode == "collector-disabled" then
assert(snapshot.source == "direct", "disabled collector still consumed the privileged cache")
assert(snapshot.system_collector.enabled == false and snapshot.system_collector.status == "disabled",
"disabled collector did not publish Basic-mode state")
assert((launchedCommand or ""):match("collect_raw%.sh"),
"disabled collector did not fall back to direct Basic collection")
assert(#notifications == 0, "disabled collector produced an update or installation notification")
print("collector initialization test passed: " .. mode)
return
elseif mode == "incompatible-lsblk" then
assert(dependencies.ready == false and dependencies.blocking == true, "incompatible lsblk was not blocking")
assert(dependencies.missing_text:match("incompatible"), "incompatible lsblk was not explained")
assert(not (launchedCommand or ""):match("collect_raw%.sh"), "collector launched with incompatible lsblk")
print("collector initialization test passed: " .. mode)
return
elseif mode == "incompatible-smartctl" then
assert(dependencies.ready == false and dependencies.blocking == false, "incompatible smartctl blocked inventory")
assert(dependencies.missing_text:match("incompatible"), "incompatible smartctl was not explained")
assert((launchedCommand or ""):match("collect_raw%.sh"), "fallback inventory did not launch")
print("collector initialization test passed: " .. mode)
return
end
assert(dependencies.ready == true, "available dependencies were reported missing")
assert(launchedCommand and launchedCommand:match("collect_raw%.sh"), "raw collector was not launched")
assert(snapshot.system_collector.authorization_available == true,
"available Polkit authorization was not exposed to the panel")
local nvme = normalizeSmart({
smart_status = { passed = true },
temperature = { current = 55 },
nvme_smart_health_information_log = {
available_spare = 100, available_spare_threshold = 25, percentage_used = 3,
temperature_sensors = { 74, 59, 55 },
data_units_read = 10, data_units_written = 20,
power_cycles = 7, power_on_hours = 100, unsafe_shutdowns = 2,
media_errors = 0, num_err_log_entries = 0, critical_warning = 0,
warning_temp_time = 4, critical_comp_time = 1,
},
nvme_self_test_log = {
current_self_test_operation = { value = 0, string = "No self-test in progress" },
table = { { self_test_code = { value = 1, string = "Short" },
self_test_result = { value = 0, string = "Completed without error" }, power_on_hours = 99 } },
},
})
assert(nvme.health == "passed" and nvme.temperature_c == 55, "NVMe health normalization failed")
assert(nvme.hotspot_temperature_c == 74 and #nvme.temperature_sensors_c == 3, "NVMe hotspot normalization failed")
assert(nvme.remaining_life_percent == 97, "NVMe endurance normalization failed")
assert(nvme.data_written_bytes == 20 * 512000, "NVMe data-unit conversion failed")
assert(nvme.available_spare_threshold_percent == 25, "NVMe spare threshold normalization failed")
assert(nvme.self_test_state == "passed" and nvme.self_test_supported, "NVMe self-test normalization failed")
local runningNvme = normalizeSmart({
smart_status = { passed = true },
nvme_smart_health_information_log = { percentage_used = 1 },
nvme_self_test_log = {
current_self_test_operation = { value = 1, string = "Short self-test in progress" },
current_self_test_completion_percent = 37,
},
})
assert(runningNvme.self_test_state == "running" and runningNvme.self_test_completion_percent == 37,
"NVMe self-test completion was not normalized")
local runningAta = normalizeSmart({
smart_status = { passed = true },
ata_smart_self_test_log = { standard = { table = { {
status = { value = 249, string = "Self-test routine in progress", remaining_percent = 80 },
} } } },
})
assert(runningAta.self_test_state == "running" and runningAta.self_test_completion_percent == 20,
"ATA remaining self-test percentage was not converted to completion")
local samsung = normalizeSmart({
smart_status = { passed = true },
power_on_time = { hours = 83062 },
ata_smart_error_log = { summary = { count = 5 } },
ata_smart_attributes = { table = {
{ name = "Wear_Leveling_Count", value = 90, raw = { value = 194 } },
{ name = "Total_LBAs_Written", value = 99, raw = { value = 210090409618 } },
{ name = "Airflow_Temperature_Cel", value = 61, raw = { value = 39 } },
{ name = "Reallocated_Sector_Ct", value = 100, raw = { value = 0 } },
} },
})
assert(samsung.remaining_life_percent == 90, "Samsung wear normalization failed")
assert(samsung.remaining_life_estimated == true, "vendor ATA life was not marked estimated")
assert(samsung.temperature_c == 39, "Samsung temperature normalization failed")
assert(samsung.data_written_bytes == 210090409618 * 512, "Samsung LBA conversion failed")
assert(samsung.reallocated_sectors == 0, "Samsung integrity counter normalization failed")
assert(samsung.media_errors == nil and samsung.error_log_entries == 5, "ATA error log was misclassified as media errors")
local hdd = normalizeSmart({
smart_status = { passed = true },
power_on_time = { hours = 113397 },
ata_smart_attributes = { table = {
{ name = "Start_Stop_Count", value = 98, raw = { value = 1423 } },
{ name = "Load_Cycle_Count", value = 92, raw = { value = 18421 } },
{ name = "Spin_Retry_Count", value = 100, raw = { value = 0 } },
{ name = "Command_Timeout", value = 100, raw = { value = 3 } },
{ name = "UDMA_CRC_Error_Count", value = 200, raw = { value = 2 } },
{ name = "Offline_Uncorrectable", value = 100, raw = { value = 0 } },
{ name = "Reported_Uncorrect", value = 100, raw = { value = 4 } },
} },
})
assert(hdd.start_stop_count == 1423 and hdd.load_cycle_count == 18421,
"HDD mechanical cycle counters were not normalized")
assert(hdd.spin_retry_count == 0 and hdd.command_timeout_count == 3 and hdd.interface_crc_errors == 2,
"HDD transport and spindle counters were not normalized")
assert(hdd.uncorrectable_errors == 4,
"HDD uncorrectable normalization ignored a nonzero counter after a zero counter")
local partial = normalizeSmart({
smart_status = { passed = true },
smartctl = { exit_status = 4, messages = { { severity = "error", string = "Optional log unavailable" } } },
nvme_smart_health_information_log = { percentage_used = 1 },
})
assert(partial.health == "passed" and partial.smart_completeness == "partial", "partial SMART result was not preserved")
assert(partial.smart_messages[1].message == "Optional log unavailable", "SMART diagnostic message was lost")
local currentPrefail = normalizeSmart({
smart_status = { passed = true }, smartctl = { exit_status = 16 },
})
assert(currentPrefail.health == "failed" and currentPrefail.smart_prefail_attribute_now == true,
"current pre-failure threshold bit did not fail health")
local historicalThreshold = normalizeSmart({
smart_status = { passed = true }, smartctl = { exit_status = 32 },
})
assert(historicalThreshold.health == "passed" and historicalThreshold.smart_past_threshold == true,
"historical threshold bit incorrectly failed current health")
local failedSelfTestLog = normalizeSmart({
smart_status = { passed = true }, smartctl = { exit_status = 128 },
})
assert(failedSelfTestLog.self_test_state == "failed" and failedSelfTestLog.smart_self_test_log_error == true,
"failed self-test log bit was ignored")
local sandisk = normalizeSmart({
smart_status = { passed = true },
ata_smart_attributes = { table = {
{ name = "Lifetime_Remaining%", value = 99, raw = { value = 99 } },
{ name = "Total_Writes_GiB", value = 253, raw = { value = 43210 } },
{ name = "Total_Reads_GiB", value = 253, raw = { value = 8765 } },
{ name = "Unexpect_Power_Loss_Ct", value = 100, raw = { value = 12 } },
} },
})
assert(sandisk.remaining_life_percent == 99, "SanDisk endurance normalization failed")
assert(sandisk.data_written_bytes == 43210 * 1024 ^ 3, "SanDisk write conversion failed")
assert(sandisk.data_read_bytes == 8765 * 1024 ^ 3, "SanDisk read conversion failed")
assert(sandisk.unsafe_shutdowns == 12, "SanDisk unsafe shutdown normalization failed")
local ambiguousHostWrites = normalizeSmart({
smart_status = { passed = true },
ata_smart_attributes = { table = {
{ name = "Host_Writes", value = 99, raw = { value = 123456 } },
{ name = "Host_Reads", value = 99, raw = { value = 654321 } },
} },
})
assert(ambiguousHostWrites.data_written_bytes == nil and ambiguousHostWrites.data_read_bytes == nil,
"unitless ATA host counters were incorrectly treated as LBAs")
local estimated = normalizeSmart({
smart_status = { passed = true },
ata_smart_attributes = { table = {
{ name = "Perc_Write/Erase_Count", value = 83, raw = { value = 590 } },
} },
})
assert(estimated.remaining_life_percent == 83 and estimated.remaining_life_estimated, "vendor life fallback failed")
assert(estimated.percentage_used == 17, "vendor life usage calculation failed")
local reserveOnly = normalizeSmart({
smart_status = { passed = true },
ata_smart_attributes = { table = {
{ name = "Perc_Avail_Resrvd_Space", value = 97, raw = { value = 97 } },
} },
})
assert(reserveOnly.remaining_life_percent == nil, "reserve space must not be treated as remaining life")
assert(reserveOnly.percentage_used == nil, "reserve space must not produce a used-life percentage")
assert(not reserveOnly.remaining_life_estimated, "reserve space must not be marked as estimated life")
assert(reserveOnly.available_spare_percent == 97, "vendor spare normalization failed")
local used, availableBytes, usage, mountPoints = mountedUsage({ children = {
{ kname = "nvme0n1p1", mountpoints = { "/home", "/", "/home" }, fsused = 25, fsavail = 75 },
} })
assert(used == 25 and availableBytes == 75 and usage == 25, "mounted usage aggregation failed")
assert(#mountPoints == 2 and mountPoints[1] == "/" and mountPoints[2] == "/home",
"mount points were not deduplicated and normalized")
files["/sys/class/nvme/nvme9/device/hwmon/hwmon9/temp1_input"] = "47000\n"
directories["/sys/class/nvme/nvme9/device/hwmon"] = { "hwmon9" }
local normalized, normalizeError = normalizeRaw({
schema = 2,
collection_id = "fixture-normalized-id",
generated_at_epoch = 1700000000,
lsblk = { blockdevices = {
{
name = "nvme9n1", kname = "nvme9n1", path = "/dev/nvme9n1", type = "disk",
tran = "nvme", rota = false, size = 2000000000, model = "Fixture NVMe", serial = "FIXTURE1",
mountpoints = {}, children = {
{ name = "nvme9n1p1", kname = "nvme9n1p1", path = "/dev/nvme9n1p1", type = "part",
mountpoints = { "/mnt/work" }, fsused = 250, fsavail = 750 },
},
},
} },
smart = {
{
requested_device = "/dev/nvme9",
payload = { smartctl = { messages = { { string = "Permission denied" } } } },
},
},
}, "test")
assert(normalized ~= nil and normalizeError == nil, "raw normalization failed")
assert(normalized.collection_id == "fixture-normalized-id", "raw normalization lost its collection ID")
assert(normalized.summary.ssd_count == 1 and normalized.disks[1].id == "FIXTURE1:n1", "drive discovery failed")
assert(normalized.disks[1].temperature_c == 47, "sysfs temperature fallback failed")
assert(normalized.disks[1].mount_points[1] == "/mnt/work",
"normalized drive omitted its mounted folder")
assert(normalized.disks[1].smart_available == false, "permission failure incorrectly marked SMART available")
assert(normalized.disks[1].smart_error:match("Permission denied"), "permission error was not preserved")
local mixed = assert(normalizeRaw({
schema = 2,
generated_at_epoch = 1700000000,
lsblk = { blockdevices = {
{ name = "sda", kname = "sda", path = "/dev/sda", type = "disk", tran = "sata",
rota = false, size = 1000000000, model = "Fixture SSD", serial = "SSD1", children = {} },
{ name = "sdb", kname = "sdb", path = "/dev/sdb", type = "disk", tran = "sata",
rota = true, size = 2000000000, model = "Fixture HDD", serial = "HDD1", children = {} },
} },
smart = {
{ requested_device = "/dev/sda", payload = {
smart_status = { passed = true }, temperature = { current = 42 },
nvme_smart_health_information_log = { percentage_used = 12 },
ata_smart_attributes = { table = {} },
} },
{ requested_device = "/dev/sdb", payload = {
smart_status = { passed = true }, temperature = { current = 36 },
ata_smart_attributes = { table = {} },
} },
},
}, "test"))
assert(mixed.summary.disk_count == 2 and mixed.summary.ssd_count == 1 and mixed.summary.hdd_count == 1,
"mixed SSD/HDD summary counts were incorrect")
assert(mixed.summary.smart_available_count == 2 and mixed.summary.hottest_drive_temperature_c == 42,
"mixed-drive SMART or temperature summary was incorrect")
assert(mixed.summary.hottest_drive_id == "SSD1" and mixed.summary.hottest_drive_name == "Fixture SSD"
and mixed.summary.hottest_ssd_drive_id == "SSD1",
"temperature summary omitted the responsible drive")
assert(mixed.summary.worst_ssd_remaining_life_percent == 88
and mixed.summary.worst_ssd_life_drive_id == "SSD1"
and mixed.summary.worst_ssd_life_drive_name == "Fixture SSD",
"SSD-life summary omitted the responsible drive")
local healthyRaw = assert(normalizeRaw({
schema = 2,
generated_at_epoch = 1700000000,
lsblk = { blockdevices = { {
name = "sda", kname = "sda", path = "/dev/sda", type = "disk", tran = "sata",
rota = false, size = 1000000000, model = "Healthy SSD", serial = "HEALTHY1",
mountpoints = {}, children = {},
} } },
smart = { {
requested_device = "/dev/sda",
payload = { smart_status = { passed = true }, ata_smart_attributes = { table = {} } },
} },
}, "test"))
assert(healthyRaw.disks[1].smart_available == true and healthyRaw.disks[1].smart_error == nil,
"healthy SMART data retained a contradictory error message")
local sleeping = assert(normalizeRaw({
schema = 2, generated_at_epoch = 1700000000,
lsblk = { blockdevices = { {
name = "sdb", kname = "sdb", path = "/dev/sdb", type = "disk", tran = "sata",
rota = true, size = 1000000000, model = "Sleeping HDD", serial = "SLEEP1",
mountpoints = {}, children = {},
} } },
smart = { { requested_device = "/dev/sdb", payload = {
power_mode = { value = 128, string = "STANDBY" },
smartctl = { exit_status = 2 },
} } },
}, "test"))
assert(sleeping.disks[1].smart_sleeping == true and sleeping.disks[1].smart_error == nil,
"sleeping HDD was reported as a SMART access failure")
assert(sleeping.summary.sleeping_count == 1 and sleeping.summary.smart_unavailable_count == 0,
"sleeping HDD was counted as unavailable")
local namespaces = assert(normalizeRaw({
schema = 2, generated_at_epoch = 1700000000,
lsblk = { blockdevices = {
{ name = "nvme0n1", kname = "nvme0n1", path = "/dev/nvme0n1", type = "disk",
tran = "nvme", rota = false, serial = "SHARED", children = {} },
{ name = "nvme0n2", kname = "nvme0n2", path = "/dev/nvme0n2", type = "disk",
tran = "nvme", rota = false, serial = "SHARED", children = {} },
} }, smart = {},
}, "test"))
assert(namespaces.disks[1].id ~= namespaces.disks[2].id
and namespaces.disks[1].id:match(":n%d+$") and namespaces.disks[2].id:match(":n%d+$"),
"NVMe namespaces sharing a controller serial did not receive unique IDs")
local empty = assert(normalizeRaw({
schema = 2, collection_id = " ", generated_at_epoch = 1700000000,
lsblk = { blockdevices = {} }, smart = {},
}, "test"))
assert(empty.access == "unavailable", "an empty drive inventory incorrectly reported full SMART access")
assert(empty.collection_id == nil, "invalid collection ID was preserved")
local oversizedId = assert(normalizeRaw({
schema = 2, collection_id = string.rep("x", 129), generated_at_epoch = 1700000000,
lsblk = { blockdevices = {} }, smart = {},
}, "test"))
assert(oversizedId.collection_id == nil, "oversized collection ID was preserved")
files["/usr/local/libexec/noctalia-drive-health/collect_raw.sh"] = "installed"
collectorEnabled = true
state.collector_snapshot = { summary = {}, system_collector = { status = "healthy" } }
publishError("fixture failure", { ready = true, blocking = false })
assert(state.collector_snapshot.system_collector.status == "stale",
"collector failure retained a stale healthy lifecycle status")
collectorEnabled = false
local compatibleProbe = {
exitCode = 0, stdout = "lsblk=ok\nsmartctl=ok\n", stderr = "", timedOut = false,
}
local incompatibleProbe = {
exitCode = 0, stdout = "lsblk=bad\nsmartctl=ok\n", stderr = "", timedOut = false,
}
probeAction = incompatibleProbe
watchers.refresh_nonce(1)
assert(state.collector_snapshot.dependencies.ready == false,
"manual dependency probe did not cache an incompatible result")
probeAction = "pending"
watchers.refresh_nonce(2)
assert(pendingProbeCallback ~= nil, "manual recheck did not launch a fresh dependency probe")
local manualRecheck = pendingProbeCallback
pendingProbeCallback = nil
manualRecheck(compatibleProbe)
assert(state.collector_snapshot.dependencies.ready == true,
"manual recheck did not recover a cached incompatible dependency")
local completedProbeCalls = probeCalls
update()
onIpc("refresh")
assert(probeCalls == completedProbeCalls,
"routine collection reran a completed dependency probe")
probeAction = incompatibleProbe
watchers.refresh_nonce(3)
assert(state.collector_snapshot.dependencies.ready == false,
"IPC recheck fixture did not cache an incompatible result")
probeAction = "pending"
onIpc("check-dependencies")
assert(pendingProbeCallback ~= nil, "dependency-check IPC did not launch a fresh probe")
local ipcRecheck = pendingProbeCallback
pendingProbeCallback = nil
ipcRecheck(compatibleProbe)
assert(state.collector_snapshot.dependencies.ready == true,
"dependency-check IPC did not recover a cached incompatible dependency")
probeAction = "pending"
local callsBeforeQueuedRecheck = probeCalls
watchers.refresh_nonce(4)
onIpc("check-dependencies")
assert(probeCalls == callsBeforeQueuedRecheck + 1 and pendingProbeCallback ~= nil,
"recheck during a running probe launched an overlapping probe")
local runningRecheck = pendingProbeCallback
pendingProbeCallback = nil
runningRecheck(compatibleProbe)
assert(probeCalls == callsBeforeQueuedRecheck + 2 and pendingProbeCallback ~= nil,
"pending rechecks were not coalesced into one follow-up probe")
local queuedRecheck = pendingProbeCallback
pendingProbeCallback = nil
probeAction = nil
queuedRecheck(compatibleProbe)
assert(probeCalls == callsBeforeQueuedRecheck + 2,
"queued recheck launched more than one follow-up probe")
probeAction = "launch-failure"
local callsBeforeLaunchFailure = probeCalls
watchers.refresh_nonce(5)
assert(probeCalls == callsBeforeLaunchFailure + 1
and state.collector_snapshot.dependencies.ready == false,
"probe launch failure did not publish an incompatible state")
update()
assert(probeCalls == callsBeforeLaunchFailure + 1,
"routine collection retried a failed probe launch")
probeAction = compatibleProbe
watchers.refresh_nonce(6)
assert(probeCalls == callsBeforeLaunchFailure + 2
and state.collector_snapshot.dependencies.ready == true,
"manual recheck did not recover after a probe launch failure")
print("collector normalization tests passed")
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
set -eu
case " $* " in
*" --nodeps "*)
printf '%s\n' "/dev/sda disk 1" "/dev/nvme0n1 disk 0"
;;
*" --json "*)
printf '%s\n' '{"blockdevices":[' \
'{"name":"sda","kname":"sda","path":"/dev/sda","type":"disk","tran":"sata","rota":true,"size":1000000000,"model":"Fixture SATA","serial":"SATA1","mountpoints":[]},' \
'{"name":"nvme0n1","kname":"nvme0n1","path":"/dev/nvme0n1","type":"disk","tran":"nvme","rota":false,"size":2000000000,"model":"Fixture NVMe","serial":"NVME1","mountpoints":[]}' \
']}'
;;
*)
echo "unexpected lsblk arguments: $*" >&2
exit 2
;;
esac
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
set -eu
device=""
standby=false
for argument in "$@"; do
device=$argument
if [ "$argument" = "--nocheck=standby,0" ]; then
standby=true
fi
done
case "$device" in
/dev/sda)
if [ "${SMARTCTL_EMPTY:-0}" = "1" ]; then
exit 2
fi
printf '%s\n' "{\"json_format_version\":[1,0],\"device\":{\"name\":\"/dev/sda\"},\"smart_status\":{\"passed\":true},\"temperature\":{\"current\":35},\"test_standby\":$standby}"
;;
/dev/nvme0)
printf '%s\n' "{\"json_format_version\":[1,0],\"device\":{\"name\":\"/dev/nvme0\"},\"smart_status\":{\"passed\":true},\"temperature\":{\"current\":42},\"nvme_smart_health_information_log\":{\"percentage_used\":2,\"available_spare\":100,\"critical_warning\":0},\"test_standby\":$standby}"
;;
*)
echo "unexpected smartctl device: $device" >&2
exit 2
;;
esac
+69
View File
@@ -0,0 +1,69 @@
-- Behavioral tests for bounded trend persistence.
local state = {}
local watchers = {}
local writes = 0
local renames = 0
local writesSucceed = true
noctalia = {
getConfig = function(key)
if key == "history_interval_minutes" then return 15 end
if key == "history_retention_days" then return 1 end
return nil
end,
pluginDataDir = function() return "/mock/plugin-data" end,
readFile = function(_path) return nil end,
writeFile = function(_path, _contents) writes = writes + 1 return writesSucceed end,
renameFile = function(_from, _to) renames = renames + 1 return true end,
log = function(_message) end,
state = {
get = function(key) return state[key] end,
set = function(key, value) state[key] = value end,
watch = function(key, callback) watchers[key] = callback end,
},
json = {
decode = function(_raw) return nil end,
encode = function(_value, _pretty) return "{}" end,
},
}
local handle = assert(io.open("history.luau", "rb"))
local source = handle:read("*a")
handle:close()
assert(load(source, "@history.luau"))()
local publish = assert(watchers.snapshot, "history service did not watch snapshots")
local function snapshot(epoch, hotspot, life)
return {
generated_at_epoch = epoch,
disks = { {
id = "SERIAL1", model = "Fixture SSD", display_name = "Fixture SSD", kind = "ssd",
temperature_c = hotspot - 5, hotspot_temperature_c = hotspot,
remaining_life_percent = life, storage_usage_percent = 25, data_written_bytes = 1000,
} },
}
end
publish(snapshot(100000, 60, 99))
local samples = state.drive_history.drives.SERIAL1.samples
assert(#samples == 1 and samples[1].hotspot_temperature_c == 60, "first history sample was not recorded")
assert(writes == 1 and renames == 1, "history was not committed atomically")
publish(snapshot(100100, 61, 99))
assert(#state.drive_history.drives.SERIAL1.samples == 1, "history ignored its sample interval")
assert(writes == 1, "history rewrote the file without a new sample")
publish(snapshot(100901, 62, 98))
samples = state.drive_history.drives.SERIAL1.samples
assert(#samples == 2 and samples[2].remaining_life_percent == 98, "scheduled history sample was missed")
assert(writes == 2 and renames == 2, "second history sample was not committed")
writesSucceed = false
publish(snapshot(101802, 63, 97))
assert(writes == 3 and renames == 2, "failed history write was not exercised")
writesSucceed = true
publish(snapshot(101900, 63, 97))
assert(writes == 4 and renames == 3, "dirty history was not retried after a transient write failure")
print("history behavior tests passed")
+404
View File
@@ -0,0 +1,404 @@
-- Declarative panel smoke tests with a minimal Noctalia/UI host.
local state = {}
local rendered = nil
local terminalCommand = nil
local asyncCommand = nil
local asyncCallback = nil
local notifications = {}
local errors = {}
local writesSucceed = true
local configValues = {
warning_temperature = 65,
critical_temperature = 80,
show_hdd = true,
use_hotspot_temperature = true,
system_collector_enabled = true,
}
local function translate(key, substitutions)
if key == "metrics.mounted_at" then
return key .. " " .. tostring(substitutions and substitutions.paths or "")
elseif key == "metrics.serial" then
return key .. " " .. tostring(substitutions and substitutions.value or "")
end
local value = key
for name, replacement in pairs(substitutions or {}) do
value = value:gsub("{" .. name .. "}", tostring(replacement))
end
return value
end
local function node(kind, props, children)
return { kind = kind, props = props or {}, children = children or {} }
end
ui = setmetatable({}, {
__index = function(_table, kind)
return function(props, children) return node(kind, props, children) end
end,
})
panel = {
render = function(tree) rendered = tree end,
close = function() end,
}
local watchers = {}
noctalia = {
getConfig = function(key)
return configValues[key]
end,
tr = translate,
pluginDataDir = function() return "/mock/plugin-data" end,
writeFile = function(_path, _contents) return writesSucceed end,
renameFile = function(_from, _to) return true end,
notify = function(title, body) table.insert(notifications, { title = title, body = body }) end,
notifyError = function(title, body) table.insert(errors, { title = title, body = body }) end,
runInTerminal = function(command) terminalCommand = command return true end,
runAsync = function(command, callback, _timeout)
asyncCommand = command
asyncCallback = callback
return true
end,
copyToClipboard = function(_text, _mime) return true end,
string = { trim = function(value) return tostring(value):match("^%s*(.-)%s*$") end },
json = { encode = function(_value, _pretty) return "{}" end },
state = {
get = function(key) return state[key] end,
set = function(key, value) state[key] = value end,
watch = function(key, callback) watchers[key] = callback end,
},
}
state.snapshot = {
generated_at_local = "12:00:00",
collector_error = nil,
dependencies = { ready = true },
system_collector = { enabled = true, installed = true, status = "healthy", version = "1.0.0",
expected_version = "1.0.0", helper_available = true, authorization_available = true,
enable_command = "sudo systemctl enable --now noctalia-drive-health.timer",
disable_command = "sudo systemctl disable --now noctalia-drive-health.timer",
install_command = "sudo '/mock/plugin/packaging/install-system-collector.sh'",
uninstall_command = "sudo '/mock/plugin/packaging/uninstall-system-collector.sh'" },
summary = { disk_count = 2, ssd_count = 1, hdd_count = 1, smart_available_count = 2,
ssd_smart_available_count = 1, hottest_drive_temperature_c = 70,
hottest_drive_name = "Fixture SSD", hottest_ssd_temperature_c = 70,
hottest_ssd_drive_name = "Fixture SSD", worst_ssd_remaining_life_percent = 95,
worst_ssd_life_drive_name = "Fixture SSD" },
issues = {},
disks = { {
id = "SERIAL1", serial = "SERIAL1", model = "Fixture SSD", display_name = "Fixture SSD", device = "/dev/nvme0n1",
smart_device = "/dev/nvme0", kind = "ssd", transport = "nvme", capacity_bytes = 2000000000,
health = "passed", smart_available = true, smart_completeness = "full",
temperature_c = 45, hotspot_temperature_c = 70, temperature_sensors_c = { 70, 45 },
remaining_life_percent = 95, percentage_used = 5, available_spare_percent = 100,
power_on_hours = 100, data_written_bytes = 1000, self_test_supported = true,
self_test_state = "running", self_test_status = "Short self-test in progress",
self_test_completion_percent = 37,
mount_points = { "/", "/home/example" },
alerts_enabled = true, presence_alert_enabled = true,
}, {
id = "HDD1", model = "Fixture HDD", display_name = "Fixture HDD", device = "/dev/sdb",
smart_device = "/dev/sdb", kind = "hdd", transport = "sata", capacity_bytes = 2000000000000,
health = "passed", smart_available = true, smart_completeness = "full",
temperature_c = 36, hotspot_temperature_c = 36, power_on_hours = 113397, power_cycles = 2200,
start_stop_count = 1423, load_cycle_count = 18421, reallocated_sectors = 0,
pending_sectors = 0, uncorrectable_errors = 0, spin_retry_count = 0,
command_timeout_count = 0, interface_crc_errors = 0, self_test_supported = true,
self_test_state = "passed", self_test_status = "Completed without error",
alerts_enabled = true, presence_alert_enabled = true,
} },
}
state.drive_history = { schema = 1, drives = { SERIAL1 = { samples = {
{ epoch = 1, hotspot_temperature_c = 65, remaining_life_percent = 96 },
{ epoch = 2, hotspot_temperature_c = 67, remaining_life_percent = 96 },
{ epoch = 3, hotspot_temperature_c = 69, remaining_life_percent = 95 },
{ epoch = 4, hotspot_temperature_c = 70, remaining_life_percent = 95 },
} } } }
state.drive_preferences = { schema = 1, order = {}, drives = {} }
local handle = assert(io.open("panel.luau", "rb"))
local source = handle:read("*a")
handle:close()
source = source:gsub("([%a_][%w_]*) %+%= ([^\n]+)", "%1 = %1 + %2")
source = source:gsub("([%a_][%w_]*) /%= ([^\n]+)", "%1 = %1 / %2")
assert(load(source, "@panel.luau"))()
local function containsText(value, target)
if type(value) ~= "table" then return false end
if type(value.props) == "table" and value.props.text == target then return true end
for _, child in pairs(value.children or {}) do
if containsText(child, target) then return true end
end
return false
end
local function countText(value, target)
if type(value) ~= "table" then return 0 end
local count = type(value.props) == "table" and value.props.text == target and 1 or 0
for _, child in pairs(value.children or {}) do
count = count + countText(child, target)
end
return count
end
local function findNode(value, kind)
if type(value) ~= "table" then return nil end
if value.kind == kind then return value end
for _, child in pairs(value.children or {}) do
local found = findNode(child, kind)
if found ~= nil then return found end
end
return nil
end
local function findNodeWithProp(value, kind, property, expected)
if type(value) ~= "table" then return nil end
if value.kind == kind and type(value.props) == "table" and value.props[property] == expected then
return value
end
for _, child in pairs(value.children or {}) do
local found = findNodeWithProp(child, kind, property, expected)
if found ~= nil then return found end
end
return nil
end
onOpen({})
assert(rendered ~= nil and not containsText(rendered, "collector.title"),
"healthy collector consumed panel space")
assert(findNodeWithProp(rendered, "glyph", "name", "server-2") ~= nil,
"panel header did not use the physical-storage icon")
assert(countText(rendered, "Fixture SSD") >= 3,
"summary cards did not identify the hottest and lowest-life drives")
assert(not containsText(rendered, "metrics.mounted_at / · /home/example"),
"collapsed drive card exposed mount paths")
onToggleCollectorSettingsClicked()
assert(containsText(rendered, "collector.settings_title")
and containsText(rendered, "collector.basic_features")
and containsText(rendered, "collector.full_features"),
"collector settings did not explain Basic and Full SMART capabilities")
onPauseCollectorClicked()
assert(terminalCommand == "sudo systemctl disable --now noctalia-drive-health.timer",
"collector settings did not expose the explicit service pause command")
terminalCommand = nil
onOpenPluginSettingsClicked()
assert(asyncCommand == "noctalia msg settings-open plugins",
"collector settings did not open Noctalia's Plugins section")
onToggleCollectorSettingsClicked()
state.snapshot.system_collector.status = "upgrade-required"
state.snapshot.system_collector.version = "0.6.0"
watchers.snapshot(state.snapshot)
assert(containsText(rendered, "collector.title"), "actionable collector state did not render")
onToggleCollectorSettingsClicked()
assert(not containsText(rendered, "collector.title") and containsText(rendered, "collector.settings_title"),
"collector settings duplicated the actionable lifecycle card")
onToggleCollectorSettingsClicked()
state.snapshot.system_collector.status = "healthy"
state.snapshot.system_collector.version = "1.0.0"
watchers.snapshot(state.snapshot)
configValues.system_collector_enabled = false
state.snapshot.system_collector.enabled = false
state.snapshot.system_collector.status = "disabled"
state.snapshot.system_collector.helper_available = false
watchers.snapshot(state.snapshot)
assert(not containsText(rendered, "collector.title"),
"disabled optional collector created a persistent main-panel warning")
onToggleCollectorSettingsClicked()
assert(containsText(rendered, "collector.status_disabled")
and containsText(rendered, "collector.open_settings"),
"disabled collector status or re-enable route was missing from collector settings")
onToggleCollectorSettingsClicked()
configValues.system_collector_enabled = true
state.snapshot.system_collector.enabled = true
state.snapshot.system_collector.status = "healthy"
state.snapshot.system_collector.helper_available = true
watchers.snapshot(state.snapshot)
onDrive1Clicked()
assert(containsText(rendered, "self_test.title"), "expanded self-test card did not render")
assert(containsText(rendered, "metrics.mounted_at / · /home/example"),
"expanded drive card omitted its mounted folders")
assert(containsText(rendered, "metrics.serial SERIAL1"),
"expanded drive card omitted its serial")
assert(containsText(rendered, "self_test.progress"), "running self-test progress did not render")
state.snapshot.disks[1].smart_completeness = "partial"
watchers.snapshot(state.snapshot)
assert(containsText(rendered, "smart.partial_details"), "partial SMART status lost its inline explanation")
state.snapshot.disks[1].smart_completeness = "full"
watchers.snapshot(state.snapshot)
local testProgress = assert(findNodeWithProp(rendered, "progress", "progress", 0.37),
"running self-test progress bar did not render")
assert(testProgress.props.progress == 0.37 and testProgress.props.value == nil,
"running self-test used an invalid progress property")
assert(containsText(rendered, "history.title"), "expanded history graph did not render")
assert(containsText(rendered, "preferences.edit"), "drive preference action did not render")
onDrive1Clicked()
assert(not containsText(rendered, "self_test.title"), "drive details did not collapse")
assert(not containsText(rendered, "history.title"), "drive history remained visible after collapse")
onDrive1Clicked()
state.snapshot.disks[1].self_test_state = "passed"
state.snapshot.disks[1].self_test_status = "Previous test passed"
state.snapshot.disks[1].self_test_completion_percent = nil
state.snapshot.generated_at_epoch = 100
watchers.snapshot(state.snapshot)
onStartShortSelfTestClicked()
assert(containsText(rendered, "self_test.confirm_action"), "self-test confirmation did not render")
assert(terminalCommand == nil, "self-test started before confirmation")
onConfirmSelfTestClicked()
assert(asyncCommand:match("^pkexec /usr/local/libexec/noctalia%-drive%-health/smart%-action%.sh 'short' '/dev/nvme0'$"),
"self-test did not use Polkit, the fixed helper, and normalized controller")
assert(terminalCommand == nil, "background self-test opened a terminal")
assert(containsText(rendered, "self_test.authorizing"), "authorization state did not render")
assert(asyncCallback ~= nil, "background self-test callback was not registered")
-- Bit 3 is an existing SMART health finding; it must not hide an accepted test request.
asyncCallback({ exitCode = 8, stdout = "accepted", stderr = "", timedOut = false })
assert(containsText(rendered, "self_test.starting"), "accepted self-test did not render startup state")
assert(state.refresh_nonce == 1, "accepted self-test did not request an immediate SMART refresh")
assert(notifications[#notifications].body == "self_test.started_background",
"accepted self-test did not notify that it is running in the background")
state.snapshot.generated_at_epoch = 101
state.snapshot.disks[1].self_test_state = "running"
state.snapshot.disks[1].self_test_status = "Short self-test in progress"
state.snapshot.disks[1].self_test_completion_percent = 52
watchers.snapshot(state.snapshot)
assert(containsText(rendered, "Short self-test in progress"), "firmware self-test state did not replace startup state")
assert(findNodeWithProp(rendered, "progress", "progress", 0.52) ~= nil,
"background self-test progress did not update")
state.snapshot.generated_at_epoch = 102
state.snapshot.disks[1].self_test_state = "passed"
state.snapshot.disks[1].self_test_status = "Completed without error"
state.snapshot.disks[1].self_test_completion_percent = nil
watchers.snapshot(state.snapshot)
assert(containsText(rendered, "Completed without error"), "completed background self-test result did not render")
asyncCommand = nil
asyncCallback = nil
onStartLongSelfTestClicked()
onConfirmSelfTestClicked()
assert(asyncCommand:match("smart%-action%.sh 'long' '/dev/nvme0'"),
"extended self-test did not use the long action")
asyncCallback({ exitCode = 126, stdout = "", stderr = "", timedOut = false })
assert(containsText(rendered, "self_test.authorization_cancelled"),
"cancelled authorization did not render a useful inline result")
assert(errors[#errors].body == "self_test.authorization_cancelled",
"cancelled authorization did not produce an error notification")
state.snapshot.system_collector.authorization_available = false
watchers.snapshot(state.snapshot)
assert(containsText(rendered, "self_test.authorization_required"),
"missing Polkit dependency was not explained")
state.snapshot.system_collector.authorization_available = true
watchers.snapshot(state.snapshot)
onEditExpandedDriveClicked()
assert(containsText(rendered, "preferences.title"), "drive preference editor did not render")
onDriveAlertsChanged(false)
onPresenceAlertsChanged(false)
onCancelDrivePreferencesClicked()
local cancelled = state.drive_preferences.drives.SERIAL1
assert(cancelled.alerts_enabled == nil and cancelled.presence_alert_enabled == nil,
"cancelled alert preference changes leaked into shared state")
onEditExpandedDriveClicked()
onAliasChanged("Workspace")
onWarningThresholdChanged("68")
onCriticalThresholdChanged("82")
onLifeThresholdChanged("15")
onSaveDrivePreferencesClicked()
local saved = state.drive_preferences.drives.SERIAL1
assert(saved.alias == "Workspace" and saved.warning_temperature == 68
and saved.critical_temperature == 82 and saved.life_warning_percent == 15,
"drive preferences were not persisted to shared state")
onEditExpandedDriveClicked()
onAliasChanged("Should not persist")
writesSucceed = false
onSaveDrivePreferencesClicked()
writesSucceed = true
assert(saved.alias == "Workspace", "failed preference write leaked changes into shared state")
onCancelDrivePreferencesClicked()
onEditExpandedDriveClicked()
writesSucceed = false
onHideDriveClicked()
writesSucceed = true
assert(saved.hidden == nil and containsText(rendered, "preferences.title"),
"failed hide write changed visibility or closed the editor")
onCancelDrivePreferencesClicked()
state.drive_history.drives.SERIAL1.samples = {
{ epoch = 1, hotspot_temperature_c = 65 },
{ epoch = 2, hotspot_temperature_c = 67 },
{ epoch = 3, hotspot_temperature_c = 69 },
}
watchers.drive_history(state.drive_history)
assert(findNode(rendered, "graph") == nil and not containsText(rendered, "history.title"),
"trend section rendered before a graph-compatible series had four samples")
state.drive_history.drives.SERIAL1.samples = {
{ epoch = 1, hotspot_temperature_c = 65 },
{ epoch = 2, hotspot_temperature_c = 67 },
{ epoch = 3, hotspot_temperature_c = 69 },
{ epoch = 4, hotspot_temperature_c = 70 },
}
watchers.drive_history(state.drive_history)
local graph = assert(findNode(rendered, "graph"), "temperature-only history graph did not render")
assert(graph.props.values2 == nil, "missing endurance history was rendered as a zero-percent series")
assert(graph.props.height == 44, "rendered trend graph did not use the compact height")
assert(not containsText(rendered, "● history.life"), "missing endurance history kept a misleading legend")
state.snapshot.issues = {
{ id = "SERIAL1:temperature", severity = "warning", message = "Fixture temperature warning" },
{ id = "SERIAL1:interface-crc", severity = "warning", message = "Fixture interface CRC warning" },
}
state.snapshot.summary.active_alert_count = 2
watchers.snapshot(state.snapshot)
assert(containsText(rendered, "alerts.dismiss_all"), "dismiss-all alert action did not render")
assert(findNodeWithProp(rendered, "button", "tooltip", "alerts.dismiss") ~= nil,
"per-alert dismiss action did not render")
onDismissAlert1Clicked()
assert(state.dismiss_alert_request.id == "SERIAL1:temperature",
"per-alert dismiss action targeted the wrong issue")
onDismissAllAlertsClicked()
assert(state.dismiss_alert_request.all == true, "dismiss-all action did not request all active issues")
state.snapshot.issues = {}
state.snapshot.summary.active_alert_count = 0
watchers.snapshot(state.snapshot)
assert(not containsText(rendered, "alerts.active_title")
and findNodeWithProp(rendered, "button", "tooltip", "alerts.dismiss") == nil,
"empty alert state kept an alert card or dismiss controls")
onDrive2Clicked()
assert(containsText(rendered, "metrics.start_stop_count")
and containsText(rendered, "metrics.load_cycle_count")
and containsText(rendered, "metrics.interface_crc_errors"),
"expanded HDD card omitted mechanical or interface health details")
assert(countText(rendered, "metrics.life_remaining") == 1,
"HDD card rendered a meaningless SSD endurance metric")
onEditExpandedDriveClicked()
assert(not containsText(rendered, "preferences.life_warning"),
"HDD preference editor exposed an SSD-only endurance threshold")
onCancelDrivePreferencesClicked()
state.snapshot.dependencies = {
ready = false, blocking = true, missing_text = "lsblk (util-linux)",
install_command = "sudo pacman -S --needed util-linux", package_manager = "pacman", can_install = true,
}
watchers.snapshot(state.snapshot)
assert(containsText(rendered, "dependencies.title"), "missing dependency card did not render")
state.snapshot.dependencies = { ready = true }
state.snapshot.disks = { state.snapshot.disks[2] }
state.snapshot.summary = {
disk_count = 1, ssd_count = 0, hdd_count = 1, smart_available_count = 1,
hdd_smart_available_count = 1, hottest_drive_temperature_c = 36,
hottest_drive_name = "Fixture HDD",
}
watchers.snapshot(state.snapshot)
assert(not containsText(rendered, "summary.lowest_ssd_life"),
"HDD-only system rendered the SSD-life summary card")
assert(countText(rendered, "Fixture HDD") >= 2,
"HDD-only temperature summary did not identify its drive")
print("panel rendering tests passed")
+47
View File
@@ -0,0 +1,47 @@
#!/bin/sh
set -eu
project_dir=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
fixture_bin="$project_dir/tests/fixtures/bin"
payload=$(PATH="$fixture_bin:$PATH" sh "$project_dir/scripts/collect_raw.sh")
printf '%s\n' "$payload" | jq -e '
.schema == 2
and .collector_version == "2.0.0"
and (.collection_id | type == "string" and length > 0)
and (.lsblk.blockdevices | length) == 2
and (.smart | length) == 2
and ([.smart[].requested_device] | sort) == ["/dev/nvme0", "/dev/sda"]
and (.smart[] | select(.requested_device == "/dev/sda") | .payload.test_standby) == true
and (.smart[] | select(.requested_device == "/dev/nvme0") | .payload.test_standby) == false
and ([.smart[].exit_code] | all(. == 0))
' >/dev/null
first_collection_id=$(printf '%s\n' "$payload" | jq -er '.collection_id')
second_payload=$(PATH="$fixture_bin:$PATH" sh "$project_dir/scripts/collect_raw.sh")
second_collection_id=$(printf '%s\n' "$second_payload" | jq -er '.collection_id')
if [ "$first_collection_id" = "$second_collection_id" ]; then
echo "raw collector reused a collection ID" >&2
exit 1
fi
empty_payload=$(SMARTCTL_EMPTY=1 PATH="$fixture_bin:$PATH" sh "$project_dir/scripts/collect_raw.sh")
printf '%s\n' "$empty_payload" | jq -e '
(.smart | length) == 2
and (.smart[] | select(.requested_device == "/dev/sda") | .exit_code) == 2
and (.smart[] | select(.requested_device == "/dev/sda")
| .payload.smartctl.messages[0].string) == "smartctl produced no JSON output"
' >/dev/null
output=$(mktemp "${TMPDIR:-/tmp}/noctalia-smart-raw-test.XXXXXX")
PATH="$fixture_bin:$PATH" sh "$project_dir/scripts/collect_raw.sh" --output "$output"
jq -e '.schema == 2 and (.collection_id | type == "string" and length > 0)
and (.smart | length) == 2' "$output" >/dev/null
mode=$(stat -c '%a' "$output")
if [ "$mode" != "640" ]; then
echo "raw collector output mode is $mode, expected 640" >&2
exit 1
fi
rm -f -- "$output"
echo "raw collector tests passed"
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
set -eu
project_dir=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
service_template="$project_dir/packaging/noctalia-drive-health.service.in"
timer="$project_dir/packaging/noctalia-drive-health.timer"
fixture=$(mktemp -d "${TMPDIR:-/tmp}/drive-health-packaging.XXXXXX")
trap 'rm -rf -- "$fixture"' EXIT HUP INT TERM
sed 's/@TARGET_GID@/1000/g' "$service_template" >"$fixture/noctalia-drive-health.service"
cp "$timer" "$fixture/noctalia-drive-health.timer"
grep -q '^Group=1000$' "$fixture/noctalia-drive-health.service"
grep -q '^RuntimeDirectoryMode=0750$' "$fixture/noctalia-drive-health.service"
grep -q '^UMask=0027$' "$fixture/noctalia-drive-health.service"
grep -q '^Unit=noctalia-drive-health.service$' "$fixture/noctalia-drive-health.timer"
if grep -R -q 'noctalia-smart-monito[r]' "$project_dir"; then
echo "generic legacy collector namespace must not be read, modified, or removed" >&2
exit 1
fi
if grep -R -q 'noctalia-gustav0ar-drive-healt[h]' "$project_dir"; then
echo "publisher-specific collector namespace must not be packaged" >&2
exit 1
fi
declared_dependencies=$(sed -n 's/^dependencies = \[\(.*\)\]$/\1/p' "$project_dir/plugin.toml")
for dependency in \
lsblk smartctl sh date dirname mkdir mktemp rm sed cat chmod mv sudo env bash \
install systemctl pkexec id tr pacman apt-get dnf zypper apk xbps-install emerge; do
case "$declared_dependencies" in
*\"$dependency\"*) ;;
*)
echo "runtime command is missing from plugin.toml dependencies: $dependency" >&2
exit 1
;;
esac
grep -q "\`$dependency\`" "$project_dir/README.md" || {
echo "runtime command is missing from README requirements: $dependency" >&2
exit 1
}
done
if command -v systemd-analyze >/dev/null 2>&1; then
if ! systemd-analyze verify \
"$fixture/noctalia-drive-health.service" \
"$fixture/noctalia-drive-health.timer" >"$fixture/verify.log" 2>&1; then
if grep -q 'Operation not permitted' "$fixture/verify.log"; then
echo "systemd unit verification unavailable in this sandbox; structural checks passed"
else
cat "$fixture/verify.log" >&2
exit 1
fi
fi
fi
echo "collector packaging tests passed"
+109
View File
@@ -0,0 +1,109 @@
-- Declarative bar-widget smoke tests with a minimal Noctalia host.
local state = {}
local watchers = {}
local rendered = nil
local tooltip = nil
local toggledPanel = nil
local function node(kind, props, children)
return { kind = kind, props = props or {}, children = children or {} }
end
ui = setmetatable({}, {
__index = function(_table, kind)
return function(props, children) return node(kind, props, children) end
end,
})
barWidget = {
render = function(tree) rendered = tree end,
setTooltip = function(value) tooltip = value end,
isVertical = function() return false end,
}
noctalia = {
getConfig = function(key)
local values = { warning_temperature = 65, critical_temperature = 50 }
return values[key]
end,
tr = function(key, substitutions)
local value = key
for name, replacement in pairs(substitutions or {}) do
value = value:gsub("{" .. name .. "}", tostring(replacement))
end
return value
end,
togglePanel = function(id) toggledPanel = id end,
state = {
get = function(key) return state[key] end,
watch = function(key, callback) watchers[key] = callback end,
},
}
state.snapshot = {
summary = {
disk_count = 3,
ssd_count = 2,
hdd_count = 1,
hottest_drive_temperature_c = 60,
hottest_ssd_temperature_c = 60,
worst_ssd_remaining_life_percent = 90,
ssd_smart_unavailable_count = 0,
ssd_unhealthy_count = 0,
smart_unavailable_count = 0,
unhealthy_count = 0,
active_alert_count = 0,
critical_alert_count = 0,
},
issues = {},
}
local handle = assert(io.open("widget.luau", "rb"))
local source = handle:read("*a")
handle:close()
source = source:gsub("([%a_][%w_]*) %.%.= ([^\n]+)", "%1 = %1 .. %2")
assert(load(source, "@widget.luau"))()
local function findNodeWithProp(value, kind, property, expected)
if type(value) ~= "table" then return nil end
if value.kind == kind and type(value.props) == "table" and value.props[property] == expected then
return value
end
for _, child in pairs(value.children or {}) do
local found = findNodeWithProp(child, kind, property, expected)
if found ~= nil then return found end
end
return nil
end
update()
assert(rendered ~= nil and tooltip:find("widget.no_alerts", 1, true), "healthy widget did not render")
assert(findNodeWithProp(rendered, "glyph", "name", "server-2") ~= nil,
"healthy widget did not use the physical-storage icon")
assert(findNodeWithProp(rendered, "glyph", "color", "primary") ~= nil,
"invalid cross-setting temperature thresholds produced a false critical state")
state.snapshot.summary.hottest_drive_temperature_c = 70
watchers.snapshot(state.snapshot)
assert(findNodeWithProp(rendered, "glyph", "color", "error") ~= nil,
"HDD temperature was excluded from the mixed-drive widget state")
state.snapshot.summary.hottest_drive_temperature_c = 60
state.snapshot.summary.unhealthy_count = 1
watchers.snapshot(state.snapshot)
assert(findNodeWithProp(rendered, "glyph", "color", "error") ~= nil,
"unhealthy HDD was excluded from the mixed-drive widget state")
state.snapshot.summary.unhealthy_count = 0
state.snapshot.summary.active_alert_count = 1
state.snapshot.summary.critical_alert_count = 1
state.snapshot.issues = { { message = "Fixture failure", severity = "critical" } }
watchers.snapshot(state.snapshot)
assert(findNodeWithProp(rendered, "glyph", "color", "error") ~= nil,
"critical widget alert did not render")
assert(tooltip:find("Fixture failure", 1, true), "widget tooltip omitted active alert details")
onClick()
assert(toggledPanel == "gustav0ar/drive-health:drives", "widget click did not toggle its panel")
print("widget rendering tests passed")