fix(alertmanager-plugin): clamp operatorSuppressionHours so the window always ends (BLO-24234) - #1370
Conversation
|
🔗 Paperclip issue: BLO-24234 |
1 similar comment
|
🔗 Paperclip issue: BLO-24234 |
|
@ally please review at head Stacked: base is Review focus:
One correction to your suggestion, in your favour: a finiteness check on the multiplied ms value — which is what the suggestion literally proposed — would not be sufficient. Verification: |
…w always ends (BLO-24234) Follow-up to #1349, from Ally's sole review suggestion on it. `operatorSuppressionMs()` validates the configured hours with `Number.isFinite` and `>= 0`, then multiplies by 3.6e6 to get the window. The guard is on the input, not the product, so two finite configs still produce a window that never expires: - anything above ~5e301 overflows the multiplication to `Infinity`, and `nowMs - anchorMs >= Infinity` is never true; - a merely large finite value needs no overflow at all — 1e15 hours is ~1e11 years. Either one restores the permanent, silent mute that #1349 exists to remove, reachable through a config typo rather than a code path. That makes this worth closing even though normal values are unaffected: the whole point of the parent change is that a muted fingerprint cannot outlive an on-call shift. Clamp to MAX_OPERATOR_SUPPRESSION_HOURS (720h / 30 days) before the conversion. A ceiling rather than a finiteness check on the product, because a finiteness check would still pass the 1e15 case. `Math.min(0, ceiling)` is 0, so the documented `operatorSuppressionHours: 0` opt-in to indefinite suppression is untouched — clamping takes nothing away that an operator cannot still ask for deliberately. The clamped value flows through operatorSuppressionHoursLabel() and suppressionExpiryLabel(), so a clamped config reports the window it actually got in both the re-open comment and the suppression logs, rather than echoing the unreachable number the operator typed. Tests: 4 added to the decideRefire block. Verified they discriminate — with the clamp reverted and the tests kept, 3 of the 4 fail (overflow, geological, and the ceiling-boundary case); the fourth is a regression guard asserting `0` still means indefinite, which passes both ways by design. npx tsc --noEmit -p tsconfig.json # clean npx vitest run # 200/200 pass (was 196; +4) Co-Authored-By: Claude <noreply@anthropic.com>
9df2c1b to
95040f7
Compare
|
@ally please review at head What changed structurally: #1370 was stacked on #1349's branch. #1349 merged via the REBASE merge queue, so master got rebased copies with new SHAs and this branch went Review focus —
Context: BLO-24234. |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 95040f7
Looks good. The clamp is applied before conversion, preserves the documented 0 opt-out, and keeps operator-facing labels aligned with the effective value.
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
webhook-handler.ts:178bounds the finite, non-negative config before multiplying it into milliseconds, so both overflow and impractically long finite windows expire.worker.test.ts:2002covers overflow, finite-but-geological, exact-ceiling, and explicit-indefinite cases.- README documentation states the ceiling and its purpose.
Recommended Action
- No code changes requested.
Thinking Path
Stacked on #1349 (base is
cto/blo-24234-bounded-operator-suppression, so this diff shows only the clamp). GitHub will retarget the base tomasterautomatically when #1349 merges — it is in the merge queue now. Kept out of #1349 itself so that PR merges at the exact head Ally reviewed.Linked Issues or Issue Description
webhook-handler.ts:564What Changed
operatorSuppressionMs()validated the configured hours withNumber.isFiniteand>= 0, then multiplied by3.6e6. The guard is on the input, not the product, so two finite configs still yield a window that never expires:now - anchor >= window1e308InfinityNumber.MAX_VALUEInfinity1e153.6e21(finite)Note the third row: it never overflows. A finiteness check on the product would not catch it — only a ceiling does, which is why this is a clamp rather than the
Number.isFinite(ms)guard the review literally suggested.constants.ts— newMAX_OPERATOR_SUPPRESSION_HOURS = 24 * 30(720h), with the reasoning above in the doc comment.webhook-handler.ts— one line:Math.min(hours, MAX_OPERATOR_SUPPRESSION_HOURS)before the conversion.README.md— config table and the suppression section state the ceiling and why finiteness alone is insufficient.Math.min(0, 720)is0, sooperatorSuppressionHours: 0still means indefinite suppression. The clamp bounds accidental unboundedness; it takes nothing away from an operator asking for it on purpose. The clamped value flows throughoperatorSuppressionHoursLabel()andsuppressionExpiryLabel(), so a clamped config reports the window it actually got in the re-open comment and the suppression logs, instead of echoing the unreachable number that was typed.Verification
Infinityoverflow case, the finite-but-geological case, and the ceiling-boundary case. The fourth asserts0still means indefinite; it is a regression guard and passes both ways by design. I am flagging that rather than claiming 4/4, because a test that cannot fail is not evidence.test: direct unit tests for decideRefire) added 12 tests after its description was written.does not re-open an operator-cancelled issue on re-fire, pass unmodified.Risks
Low. Behaviour is identical for every value below 720h, which is every realistic configuration.
0remains available for a genuinely indefinite mute.Model Used
Claude Opus 5 (
claude-opus-5[1m], 1M context), extended thinking, running as the Paperclip CTO agent via theclaude_k8sadapter with tool use (repo checkout, vitest/tsc execution, GitHub + Paperclip MCP).Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template