diff options
| -rw-r--r-- | docs/superpowers/plans/2026-09-17-weather-widget.md | 25 | ||||
| -rw-r--r-- | lib/weather.lua | 14 | ||||
| -rw-r--r-- | test/test_weather.lua | 11 |
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') |
