Repository navigation
Description
Problem
pm-stack/aggregator/src/alerts.ts mergeQuiet (alerts.ts:89-99) validates quiet-window times with a shape-only regex:
if (typeof q.start === "string" && /^\d{2}:\d{2}$/.test(q.start)) out.start = q.start;
if (typeof q.end === "string" && /^\d{2}:\d{2}$/.test(q.end)) out.end = q.end;Any two-digit pair passes: 99:99, 24:60, 23:60 are all stored as the subscription's quiet prefs. inQuietHours (alerts.ts:110-132) then converts with toMin — (h || 0) * 60 + (m || 0) — with no range check either, so the garbage silently becomes a different window instead of being rejected back to the default:
start: "99:99"→ 6039 minutes; withend: "07:00"the wrap branch (mins >= start || mins < end) makes the window 00:00–07:00 — the 23:00 start the user asked for (default) silently vanishes, and 23:00–24:00 is no longer quiet.end: "24:60"→ 1500 minutes, extending any window 60+ minutes past its stated end.
The existing test (test/alerts.test.ts, "quiet defaults…") pins only that a non-numeric spelling ("9pm") falls back to the default; digit-shaped nonsense is untested and unhandled. This is the same shape-only-validation class as the feed gates (#3458, #3427, #3428), in the one prefs parser that has an explicit reject-to-default contract for malformed input.
Evidence (verified this run)
Laptop probe importing the real alerts.ts at main 9be9c229 (sha256 868f4c78…, identical in both clones; probe dir ~/git/fleet/probe-2026-11-01-laneB-alerttick/, log probe.log):
mergeQuiet({ start: "99:99" })→{ start: "99:99", end: "07:00", tz: "America/Chicago" }— accepted verbatimmergeQuiet({ end: "24:60" })→{ start: "23:00", end: "24:60", … }— accepted verbatiminQuietHours({ start: "99:99", end: "07:00" }, 2026-01-15T06:00Z = 00:00 CST)→true; at18:00Z = 12:00 CST→false— i.e. the effective window is 00:00–07:00, not an error and not the default 23:00–07:00
Why it matters
Quiet prefs are user-supplied subscription input persisted via validateNewSubscription → upsertSubscription. A client bug or a hand-edited prefs payload that emits an out-of-range time is silently stored and then silently reinterpreted as a different suppression window for the dashboard classes that consult inQuietHours (#3317) — notifications suppressed at the wrong hours, with no error anywhere, and the stored value looks plausible (99:99 reads as a typo only if a human opens the row). Reject-to-default is the parser's own established behaviour for malformed strings; out-of-range digit strings are the gap in it.
Acceptance criteria
-
mergeQuietaccepts only real clock times: hour00–23, minute00–59; anything else (including99:99,24:00,23:60) falls back to the default for that field, like other malformed values do today. - Regression tests pin:
99:99and24:60fall back to defaults; boundary values00:00and23:59are accepted; the existing"9pm"fallback test still passes. - No other
mergeQuiet/inQuietHoursbehaviour changes (midnight-wrap and end-exclusive semantics unchanged).
Distinct from: #3685 (same module — the delivery engine's unconditional markDelivered; different function, different failure), #3317 (the dashboard classes that will consume quiet hours), #3458/#3427/#3428 (feed gates, same shape-only class in a different parser).
Activity
developing-today commentedon Nov 1, 2026
CODE-1502
dezren39 commentedon Nov 1, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 1, 2026
🤖 Agent note · adopted as Phase 2 of design #3702
source/design-architect. Existing queued triage packet stands.14 remaining items
dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 3, 2026
🤖 Agent note · parked (awaiting CI)
dezren39 commentedon Nov 3, 2026
🤖 Agent note · state: handed off
repo-health-code-pilot)ses_e5e5f853543ffeOs1fYm7RZxmsopencode-route/workhorse, agentbuildrepo-worker-codedezren39 commentedon Nov 3, 2026
🤖 Agent note · done
status/parked/queue/parkedwith no runnable fix leftOutcome: fixed and merged — PR #4786 landed as merge commit
550b611eb17099577cff1d2daaf7e9c824eaf844at 2026-11-03T10:52:31Z.What was blocking it: the parked task 6169 had a verified fix (
isClockTimerange check, per-field DEFAULT_QUIET fallback, 2 regression tests) but the branch sat ~3h withmergeStateStatus: UNKNOWNand no checks, because.github/workflows/pm-aggregator-tests.ymltriggers on push tomainonly — aggregator PRs never get an aggregator test check. Local verification is therefore the merge signal for this module.Verification on the branch tip before merging (base merged in twice: stale
1e3763faf, then realmain74064cbb3):bun test test/alerts.test.ts— 34 pass / 0 fail.bun test— 1296 pass / 0 fail (94 files).alerts.ts+13 −2 addingisClockTime(hours 00–23, minutes 00–59) used by bothstartandend;test/alerts.test.ts+25 −0 pinning99:99/24:60→ defaults,00:00/23:59accepted, existing"9pm"fallback untouched. NoinQuietHours, wrap, or tz changes.main:pm-stack/aggregator/src/alerts.ts:121isClockTime, used at:133-134.Post-merge signal: the push-to-main run of
pm-aggregator testscovers the aggregator suite; this PR adds no non-aggregator paths.