@teamscala/service-boot 0.23.405 → 0.23.406
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -79,12 +79,19 @@ function isSustained(
|
|
|
79
79
|
aggregate: FaultAggregate,
|
|
80
80
|
policy: FaultEscalationPolicy,
|
|
81
81
|
override: KindThresholdOverride | undefined,
|
|
82
|
+
now: Date,
|
|
82
83
|
): boolean {
|
|
83
84
|
const minCount = override?.minCount ?? policy.minCount;
|
|
84
85
|
const minSpan = override?.minSpanMinutes ?? policy.minSpanMinutes;
|
|
86
|
+
// Recency, not merely presence in the lookback window. Count and span
|
|
87
|
+
// answer "did this happen"; a fault qualifies only while it is STILL
|
|
88
|
+
// happening. Without this the 720-minute lookback keeps a dead fault
|
|
89
|
+
// qualifying for hours, and since a resolved episode reads as no episode
|
|
90
|
+
// it re-escalates on every sweep until its events age out (2026-08-26).
|
|
85
91
|
return (
|
|
86
92
|
aggregate.count >= minCount &&
|
|
87
|
-
minutesBetween(aggregate.firstSeen, aggregate.lastSeen) >= minSpan
|
|
93
|
+
minutesBetween(aggregate.firstSeen, aggregate.lastSeen) >= minSpan &&
|
|
94
|
+
minutesBetween(aggregate.lastSeen, now) < policy.resolveAfterMinutes
|
|
88
95
|
);
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -107,7 +114,7 @@ export function decideSweep(
|
|
|
107
114
|
|
|
108
115
|
for (const aggregate of aggregates) {
|
|
109
116
|
const episode = byKind.get(aggregate.kind);
|
|
110
|
-
if (!isSustained(aggregate, policy, kindThresholds[aggregate.kind])) continue;
|
|
117
|
+
if (!isSustained(aggregate, policy, kindThresholds[aggregate.kind], now)) continue;
|
|
111
118
|
// No episode, or the previous episode closed (quiet past resolve window):
|
|
112
119
|
// a NEW episode begins and must be delivered.
|
|
113
120
|
if (!episode || episode.resolvedAt) {
|
|
@@ -202,3 +202,47 @@ describe("composeEscalationMessage", () => {
|
|
|
202
202
|
expect(message.split("\n")).toHaveLength(4);
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
|
+
|
|
206
|
+
describe("decideSweep — qualification asks 'is this happening', not 'did this happen'", () => {
|
|
207
|
+
// 2026-08-26: llm.provider.call.failed re-escalated every ~10 minutes with
|
|
208
|
+
// lastSeen frozen at 16:54 — escalated 19:40 on a fault three hours dead,
|
|
209
|
+
// resolved 19:45, repeat. The tell was the closure counter climbing
|
|
210
|
+
// (141/151/171 min): quiet measured from one frozen aggregate lastSeen.
|
|
211
|
+
// isSustained checked count and span but never recency, so a dead fault
|
|
212
|
+
// kept qualifying for the rest of the 720-minute lookback while a resolved
|
|
213
|
+
// episode read as "no episode" and re-escalated it.
|
|
214
|
+
const resolved = (kind: string) =>
|
|
215
|
+
episode({ kind, resolvedAt: new Date(NOW.getTime() - 5 * 60_000) });
|
|
216
|
+
|
|
217
|
+
test("a fault quiet past the resolve window does not re-escalate after resolution", () => {
|
|
218
|
+
const decision = decideSweep(
|
|
219
|
+
[agg("stale.error", 30, 400, 170)],
|
|
220
|
+
[resolved("stale.error")],
|
|
221
|
+
POLICY,
|
|
222
|
+
NOW,
|
|
223
|
+
);
|
|
224
|
+
expect(decision.escalate).toHaveLength(0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("a fault quiet past the resolve window does not escalate with no episode at all", () => {
|
|
228
|
+
const decision = decideSweep([agg("stale.error", 30, 400, 170)], [], POLICY, NOW);
|
|
229
|
+
expect(decision.escalate).toHaveLength(0);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// The other direction, and the one that protects live escalations: the fix
|
|
233
|
+
// must not silence a fault that is still emitting.
|
|
234
|
+
test("a fault still emitting escalates", () => {
|
|
235
|
+
const decision = decideSweep([agg("live.error", 30, 400, 2)], [], POLICY, NOW);
|
|
236
|
+
expect(decision.escalate).toHaveLength(1);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("a recurring fault opens a NEW episode after its previous one resolved", () => {
|
|
240
|
+
const decision = decideSweep(
|
|
241
|
+
[agg("live.error", 30, 400, 2)],
|
|
242
|
+
[resolved("live.error")],
|
|
243
|
+
POLICY,
|
|
244
|
+
NOW,
|
|
245
|
+
);
|
|
246
|
+
expect(decision.escalate).toHaveLength(1);
|
|
247
|
+
});
|
|
248
|
+
});
|