aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 09:04:46 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 09:04:46 +0200
commit53d39b7212ffe0753d2b10ea39348bc033f06bef (patch)
treebe756f5cf2e3d4e1b303b05637924e1224ff2d96
parent752b58b3992d1a9cfb51c891deb77007286415ee (diff)
downloadconky-theme-udt-53d39b7212ffe0753d2b10ea39348bc033f06bef.tar.gz
conky-theme-udt-53d39b7212ffe0753d2b10ea39348bc033f06bef.zip
fix: guard now, and clamp a negative age, in the staleness pair
is_stale and age_str were the only functions in the module doing arithmetic on an unchecked argument: a nil `now` raised, and a raise in this project is a blank dashboard with no message. Every caller passes os.time(), which cannot be nil, so this was unreachable, but it is the same bug already fixed once in icon() and the module header promises nothing raises. age_str also printed a negative age when the observation timestamp was in the future, which a clock skew can produce. "stale -17m" on the card reads as a broken widget, so the age now floors at zero and such a reading counts as fresh: weather from the future is not old. With this the whole module is raise-free, verified across every function against nil, string, table, boolean, NaN, negative and zero arguments in all three positions. Both cases were found by the Task 5 implementer, which was asked to probe beyond its given tests and reported them without changing the code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--docs/superpowers/plans/2026-09-17-weather-widget.md25
-rw-r--r--lib/weather.lua14
-rw-r--r--test/test_weather.lua11
3 files changed, 46 insertions, 4 deletions
diff --git a/docs/superpowers/plans/2026-09-17-weather-widget.md b/docs/superpowers/plans/2026-09-17-weather-widget.md
index c19b253..0b78b83 100644
--- a/docs/superpowers/plans/2026-09-17-weather-widget.md
+++ b/docs/superpowers/plans/2026-09-17-weather-widget.md
@@ -643,6 +643,17 @@ assert(weather.age_str(1000, 1000 + 90) == '1m', 'ninety seconds reads as 1m')
assert(weather.age_str(1000, 1000 + 3600) == '1h', 'an hour reads as 1h')
assert(weather.age_str(1000, 1000 + 7200) == '2h', 'two hours')
assert(weather.age_str(1000, 1000 + 86400 * 2) == '2d', 'days, once it gets that bad')
+
+-- Neither takes `now` on faith: a nil there used to raise, and a raise is a
+-- blank dashboard rather than a message.
+assert(weather.is_stale(1000, nil) == true, 'a nil now must not raise')
+assert(weather.age_str(1000, nil) == '?', 'a nil now must not raise')
+
+-- A dt in the future means a clock skew. "stale -17m" reads as a broken
+-- widget, so the age floors at zero and the reading counts as fresh.
+assert(weather.age_str(2000, 1000) == '0m', 'a future dt reads as 0m, got ' ..
+ tostring(weather.age_str(2000, 1000)))
+assert(weather.is_stale(2000, 1000) == false, 'a future dt is not stale')
```
- [ ] **Step 2: Run test to verify it fails**
@@ -658,15 +669,25 @@ Add to `lib/weather.lua`, before the final `return M`:
-- Three missed fetches at the 15-minute interval.
M.STALE_AFTER = 45 * 60
+-- `now` is guarded alongside `dt` although every caller passes os.time(),
+-- which cannot be nil: these were the only two functions in the module that
+-- did arithmetic on an unchecked argument, and the module's promise is that
+-- nothing raises. A raise here is a blank dashboard.
function M.is_stale(dt, now)
- if type(dt) ~= 'number' then return true end
+ if type(dt) ~= 'number' or type(now) ~= 'number' then return true end
return (now - dt) > M.STALE_AFTER
end
-- Short age for the marker beside the city: "12m", "3h", "2d".
+--
+-- A negative age is clamped to zero rather than printed. An observation
+-- timestamped in the future means a clock skew somewhere, and "stale -17m" on
+-- the card reads as a broken widget; treating it as fresh is both tidier and
+-- truer, since weather from the future is not old.
function M.age_str(dt, now)
- if type(dt) ~= 'number' then return '?' end
+ if type(dt) ~= 'number' or type(now) ~= 'number' then return '?' end
local s = now - dt
+ if s < 0 then s = 0 end
if s < 3600 then return math.floor(s / 60) .. 'm' end
if s < 86400 then return math.floor(s / 3600) .. 'h' end
return math.floor(s / 86400) .. 'd'
diff --git a/lib/weather.lua b/lib/weather.lua
index b1fda4d..8ecd273 100644
--- a/lib/weather.lua
+++ b/lib/weather.lua
@@ -189,15 +189,25 @@ end
-- Three missed fetches at the 15-minute interval.
M.STALE_AFTER = 45 * 60
+-- `now` is guarded alongside `dt` although every caller passes os.time(),
+-- which cannot be nil: these were the only two functions in the module that
+-- did arithmetic on an unchecked argument, and the module's promise is that
+-- nothing raises. A raise here is a blank dashboard.
function M.is_stale(dt, now)
- if type(dt) ~= 'number' then return true end
+ if type(dt) ~= 'number' or type(now) ~= 'number' then return true end
return (now - dt) > M.STALE_AFTER
end
-- Short age for the marker beside the city: "12m", "3h", "2d".
+--
+-- A negative age is clamped to zero rather than printed. An observation
+-- timestamped in the future means a clock skew somewhere, and "stale -17m" on
+-- the card reads as a broken widget; treating it as fresh is both tidier and
+-- truer, since weather from the future is not old.
function M.age_str(dt, now)
- if type(dt) ~= 'number' then return '?' end
+ if type(dt) ~= 'number' or type(now) ~= 'number' then return '?' end
local s = now - dt
+ if s < 0 then s = 0 end
if s < 3600 then return math.floor(s / 60) .. 'm' end
if s < 86400 then return math.floor(s / 3600) .. 'h' end
return math.floor(s / 86400) .. 'd'
diff --git a/test/test_weather.lua b/test/test_weather.lua
index 95908ce..cdcdc2d 100644
--- a/test/test_weather.lua
+++ b/test/test_weather.lua
@@ -222,4 +222,15 @@ assert(weather.age_str(1000, 1000 + 3600) == '1h', 'an hour reads as 1h')
assert(weather.age_str(1000, 1000 + 7200) == '2h', 'two hours')
assert(weather.age_str(1000, 1000 + 86400 * 2) == '2d', 'days, once it gets that bad')
+-- Neither takes `now` on faith: a nil there used to raise, and a raise is a
+-- blank dashboard rather than a message.
+assert(weather.is_stale(1000, nil) == true, 'a nil now must not raise')
+assert(weather.age_str(1000, nil) == '?', 'a nil now must not raise')
+
+-- A dt in the future means a clock skew. "stale -17m" reads as a broken
+-- widget, so the age floors at zero and the reading counts as fresh.
+assert(weather.age_str(2000, 1000) == '0m', 'a future dt reads as 0m, got ' ..
+ tostring(weather.age_str(2000, 1000)))
+assert(weather.is_stale(2000, 1000) == false, 'a future dt is not stale')
+
print('test_weather: all assertions passed')