Skip to content

aggregator(alerts): mergeQuiet accepts out-of-range times (99:99, 24:60) — a malformed quiet window is silently reinterpreted instead of falling back to the default #3686

Closed

Description

@dezren39

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; with end: "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 verbatim
  • mergeQuiet({ end: "24:60" }) → { start: "23:00", end: "24:60", … } — accepted verbatim
  • inQuietHours({ start: "99:99", end: "07:00" }, 2026-01-15T06:00Z = 00:00 CST) → true; at 18: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

  • mergeQuiet accepts only real clock times: hour 00–23, minute 00–59; anything else (including 99:99, 24:00, 23:60) falls back to the default for that field, like other malformed values do today.
  • Regression tests pin: 99:99 and 24:60 fall back to defaults; boundary values 00:00 and 23:59 are accepted; the existing "9pm" fallback test still passes.
  • No other mergeQuiet / inQuietHours behaviour 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

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    on Nov 1, 2026
  2. developing-today commented on Nov 1, 2026

    @developing-today
    Contributor
  3. added
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    effort/sSmall: expected under 2 hours of work
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Nov 1, 2026
  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · adopted as Phase 2 of design #3702

  6. 14 remaining items

  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor
  8. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 3, 2026
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked (awaiting CI)

  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: handed off

    • Agent: opencode-claimant
    • Task: 6169 (repo-health-code-pilot)
    • Session / ID: ses_e5e5f853543ffeOs1fYm7RZxms
    • Route: opencode-route/workhorse, agent build
    • Claiming worker: repo-worker-code
    • Status: session created, bound in the queue lane ledger/task row, checkpointed as launched, and task packet delivered.
  11. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: queue worker (repo-worker-code), OpenChamber scheduled lane 8ac61616-3815-4e7b-b33d-840a3829154a
    • Session / ID: task 7427, queue repo-health-code-pilot (triage task; took the parked fix the rest of the way)
    • Found via: queue claim — the issue carried status/parked/queue/parked with no runnable fix left

    Outcome: fixed and merged — PR #4786 landed as merge commit 550b611eb17099577cff1d2daaf7e9c824eaf844 at 2026-11-03T10:52:31Z.

    What was blocking it: the parked task 6169 had a verified fix (isClockTime range check, per-field DEFAULT_QUIET fallback, 2 regression tests) but the branch sat ~3h with mergeStateStatus: UNKNOWN and no checks, because .github/workflows/pm-aggregator-tests.yml triggers on push to main only — 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 real main 74064cbb3):

    • bun test test/alerts.test.ts — 34 pass / 0 fail.
    • Full aggregator suite bun test — 1296 pass / 0 fail (94 files).
    • Diff is exactly the acceptance criteria: alerts.ts +13 −2 adding isClockTime (hours 00–23, minutes 00–59) used by both start and end; test/alerts.test.ts +25 −0 pinning 99:99/24:60 → defaults, 00:00/23:59 accepted, existing "9pm" fallback untouched. No inQuietHours, wrap, or tz changes.
    • Landed on main: pm-stack/aggregator/src/alerts.ts:121 isClockTime, used at :133-134.

    Post-merge signal: the push-to-main run of pm-aggregator tests covers the aggregator suite; this PR adds no non-aggregator paths.

  12. removed
    task/triageQueue packet kind: verify, reproduce, and route this item
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect job

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions