Public: anyone can read this; the crew can change it.

A pure step says when a clog starts and when it clears, once each, across readings #6527

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6518-a-pure-step-says-when-a-clog-starts-and → main 1 commit fork

  • Merged. scout/6518-a-pure-step-says-when-a-clog-starts-and was merged into main as 043d087.

Review

Sign in to review this change.
2 files changed +77 −0
src/flow/clog_watch.ts added +37
@@ -0,0 +1,37 @@
1 /**
2 * Clog watch (#6518): turns a stream of clog readings into one "started" and
3 * one "cleared" event per clog. A reading is the list of reasons the flow is
4 * clogged now (empty when it is not). A clog clears only after QUIET_TO_CLEAR
5 * quiet readings in a row, so a flapping reason does not notify twice.
6 */
7
8 export const QUIET_TO_CLEAR = 2;
9
10 export interface ClogState {
11 /** When the clog started, epoch ms. */
12 readonly since: number;
13 /** The reasons on the reading that started it. */
14 readonly firstReasons: readonly string[];
15 /** Quiet readings in a row so far; absent when zero. */
16 readonly quiet?: number;
17 }
18
19 export type ClogEvent = "started" | "cleared";
20
21 export interface ClogStep {
22 readonly state: ClogState | null;
23 readonly event: ClogEvent | null;
24 /** On "cleared": when the clog had started. */
25 readonly since?: number;
26 }
27
28 export function clogStep(prev: ClogState | null, reading: readonly string[], now: number): ClogStep {
29 if (!prev) {
30 if (reading.length === 0) return { state: null, event: null };
31 return { state: { since: now, firstReasons: [...reading] }, event: "started" };
32 }
33 if (reading.length > 0) return { state: { since: prev.since, firstReasons: prev.firstReasons }, event: null };
34 const quiet = (prev.quiet ?? 0) + 1;
35 if (quiet >= QUIET_TO_CLEAR) return { state: null, event: "cleared", since: prev.since };
36 return { state: { ...prev, quiet }, event: null };
37 }
test/flow-clog-watch.test.ts added +40
@@ -0,0 +1,40 @@
1 import { expect, test } from "bun:test";
2 import { clogStep } from "../src/flow/clog_watch.ts";
3
4 test("quiet with no clog stays null", () => {
5 expect(clogStep(null, [], 1)).toEqual({ state: null, event: null });
6 });
7
8 test("a reason starts a clog once and it stands without repeats", () => {
9 const a = clogStep(null, ["red main"], 10);
10 expect(a.event).toBe("started");
11 expect(a.state).toEqual({ since: 10, firstReasons: ["red main"] });
12 const b = clogStep(a.state, ["red main", "stuck"], 20);
13 expect(b.event).toBeNull();
14 expect(b.state).toEqual({ since: 10, firstReasons: ["red main"] });
15 });
16
17 test("one quiet reading is still standing; a reason resets the count", () => {
18 const a = clogStep(null, ["x"], 10);
19 const b = clogStep(a.state, [], 20);
20 expect(b.event).toBeNull();
21 expect(b.state?.since).toBe(10);
22 const c = clogStep(b.state, ["x"], 30);
23 const d = clogStep(c.state, [], 40);
24 expect(d.event).toBeNull();
25 expect(d.state).not.toBeNull();
26 });
27
28 test("two quiet readings in a row clear it with the start time", () => {
29 let s = clogStep(null, ["x"], 10).state;
30 s = clogStep(s, [], 20).state;
31 const c = clogStep(s, [], 30);
32 expect(c).toEqual({ state: null, event: "cleared", since: 10 });
33 });
34
35 test("a clog after a clear starts anew", () => {
36 const c = clogStep(clogStep(clogStep(null, ["x"], 10).state, [], 20).state, [], 30);
37 const r = clogStep(c.state, ["y"], 50);
38 expect(r.event).toBe("started");
39 expect(r.state).toEqual({ since: 50, firstReasons: ["y"] });
40 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs