@checkstack/healthcheck-backend 0.0.2

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.
@@ -0,0 +1,237 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { evaluateHealthStatus } from "./state-evaluator";
3
+ import type {
4
+ HealthCheckStatus,
5
+ ConsecutiveThresholds,
6
+ WindowThresholds,
7
+ } from "@checkstack/healthcheck-common";
8
+
9
+ // Helper to create runs with timestamps
10
+ function createRuns(
11
+ statuses: HealthCheckStatus[]
12
+ ): { status: HealthCheckStatus; timestamp: Date }[] {
13
+ const now = Date.now();
14
+ return statuses.map((status, i) => ({
15
+ status,
16
+ timestamp: new Date(now - i * 60000), // 1 minute apart, newest first
17
+ }));
18
+ }
19
+
20
+ describe("evaluateHealthStatus", () => {
21
+ describe("with no runs", () => {
22
+ test("returns healthy when no runs exist", () => {
23
+ const result = evaluateHealthStatus({ runs: [] });
24
+ expect(result).toBe("healthy");
25
+ });
26
+ });
27
+
28
+ describe("consecutive mode", () => {
29
+ const thresholds: ConsecutiveThresholds = {
30
+ mode: "consecutive",
31
+ healthy: { minSuccessCount: 2 },
32
+ degraded: { minFailureCount: 2 },
33
+ unhealthy: { minFailureCount: 4 },
34
+ };
35
+
36
+ test("returns healthy after minSuccessCount consecutive successes", () => {
37
+ const runs = createRuns(["healthy", "healthy", "unhealthy"]);
38
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
39
+ });
40
+
41
+ test("returns healthy with exactly minSuccessCount successes", () => {
42
+ const runs = createRuns(["healthy", "healthy"]);
43
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
44
+ });
45
+
46
+ test("returns degraded after minFailureCount consecutive failures", () => {
47
+ const runs = createRuns(["unhealthy", "degraded", "healthy"]);
48
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("degraded");
49
+ });
50
+
51
+ test("returns unhealthy after higher minFailureCount", () => {
52
+ const runs = createRuns([
53
+ "unhealthy",
54
+ "unhealthy",
55
+ "degraded",
56
+ "unhealthy",
57
+ "healthy",
58
+ ]);
59
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("unhealthy");
60
+ });
61
+
62
+ test("returns latest status when not enough history", () => {
63
+ const runs = createRuns(["healthy"]); // Only 1 run, needs 2 for healthy
64
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
65
+ });
66
+
67
+ test("handles mix of degraded and unhealthy as failures", () => {
68
+ const runs = createRuns(["degraded", "unhealthy"]);
69
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("degraded");
70
+ });
71
+
72
+ test("resets count when streak breaks", () => {
73
+ // Latest: 1 healthy, then failures - should use latest status
74
+ const runs = createRuns(["healthy", "unhealthy", "unhealthy"]);
75
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
76
+ });
77
+ });
78
+
79
+ describe("window mode", () => {
80
+ const thresholds: WindowThresholds = {
81
+ mode: "window",
82
+ windowSize: 5,
83
+ degraded: { minFailureCount: 2 },
84
+ unhealthy: { minFailureCount: 4 },
85
+ };
86
+
87
+ test("returns healthy when failures below threshold", () => {
88
+ const runs = createRuns([
89
+ "healthy",
90
+ "unhealthy",
91
+ "healthy",
92
+ "healthy",
93
+ "healthy",
94
+ ]);
95
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
96
+ });
97
+
98
+ test("returns degraded when failures at degraded threshold", () => {
99
+ const runs = createRuns([
100
+ "unhealthy",
101
+ "unhealthy",
102
+ "healthy",
103
+ "healthy",
104
+ "healthy",
105
+ ]);
106
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("degraded");
107
+ });
108
+
109
+ test("returns unhealthy when failures at unhealthy threshold", () => {
110
+ const runs = createRuns([
111
+ "unhealthy",
112
+ "degraded",
113
+ "unhealthy",
114
+ "unhealthy",
115
+ "healthy",
116
+ ]);
117
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("unhealthy");
118
+ });
119
+
120
+ test("only considers runs within window size", () => {
121
+ // Window is 5, so old failures outside window don't count
122
+ const runs = createRuns([
123
+ "healthy",
124
+ "healthy",
125
+ "healthy",
126
+ "healthy",
127
+ "healthy",
128
+ "unhealthy", // Outside window
129
+ "unhealthy",
130
+ "unhealthy",
131
+ "unhealthy",
132
+ ]);
133
+ expect(evaluateHealthStatus({ runs, thresholds })).toBe("healthy");
134
+ });
135
+
136
+ test("handles window smaller than run count", () => {
137
+ const smallWindowThresholds: WindowThresholds = {
138
+ mode: "window",
139
+ windowSize: 3,
140
+ degraded: { minFailureCount: 2 },
141
+ unhealthy: { minFailureCount: 3 },
142
+ };
143
+ const runs = createRuns([
144
+ "unhealthy",
145
+ "unhealthy",
146
+ "healthy",
147
+ "unhealthy",
148
+ ]);
149
+ expect(
150
+ evaluateHealthStatus({ runs, thresholds: smallWindowThresholds })
151
+ ).toBe("degraded");
152
+ });
153
+ });
154
+
155
+ describe("default thresholds", () => {
156
+ test("uses default consecutive mode when thresholds not provided", () => {
157
+ // Default: healthy after 1 success, degraded after 2 failures, unhealthy after 5
158
+ const runs = createRuns(["healthy"]);
159
+ expect(evaluateHealthStatus({ runs })).toBe("healthy");
160
+ });
161
+
162
+ test("default degraded after 2 consecutive failures", () => {
163
+ const runs = createRuns(["unhealthy", "unhealthy"]);
164
+ expect(evaluateHealthStatus({ runs })).toBe("degraded");
165
+ });
166
+
167
+ test("default unhealthy after 5 consecutive failures", () => {
168
+ const runs = createRuns([
169
+ "unhealthy",
170
+ "unhealthy",
171
+ "unhealthy",
172
+ "unhealthy",
173
+ "unhealthy",
174
+ ]);
175
+ expect(evaluateHealthStatus({ runs })).toBe("unhealthy");
176
+ });
177
+ });
178
+
179
+ describe("flickering scenarios", () => {
180
+ test("window mode handles flickering better than consecutive", () => {
181
+ // System that is mostly failing but occasionally succeeds
182
+ const runs = createRuns([
183
+ "unhealthy",
184
+ "healthy", // Flicker
185
+ "unhealthy",
186
+ "unhealthy",
187
+ "unhealthy",
188
+ ]);
189
+
190
+ const consecutiveThresholds: ConsecutiveThresholds = {
191
+ mode: "consecutive",
192
+ healthy: { minSuccessCount: 1 },
193
+ degraded: { minFailureCount: 2 },
194
+ unhealthy: { minFailureCount: 3 },
195
+ };
196
+
197
+ const windowThresholds: WindowThresholds = {
198
+ mode: "window",
199
+ windowSize: 5,
200
+ degraded: { minFailureCount: 2 },
201
+ unhealthy: { minFailureCount: 4 },
202
+ };
203
+
204
+ // Consecutive: sees only 1 failure at start, returns unhealthy (just the first)
205
+ expect(
206
+ evaluateHealthStatus({ runs, thresholds: consecutiveThresholds })
207
+ ).toBe("unhealthy");
208
+
209
+ // Window: sees 4 failures in window of 5, returns unhealthy
210
+ expect(evaluateHealthStatus({ runs, thresholds: windowThresholds })).toBe(
211
+ "unhealthy"
212
+ );
213
+ });
214
+
215
+ test("window mode shows recovery when mostly healthy", () => {
216
+ const runs = createRuns([
217
+ "healthy",
218
+ "unhealthy", // Flicker
219
+ "healthy",
220
+ "healthy",
221
+ "healthy",
222
+ ]);
223
+
224
+ const windowThresholds: WindowThresholds = {
225
+ mode: "window",
226
+ windowSize: 5,
227
+ degraded: { minFailureCount: 2 },
228
+ unhealthy: { minFailureCount: 4 },
229
+ };
230
+
231
+ // Only 1 failure in window - still healthy
232
+ expect(evaluateHealthStatus({ runs, thresholds: windowThresholds })).toBe(
233
+ "healthy"
234
+ );
235
+ });
236
+ });
237
+ });
@@ -0,0 +1,105 @@
1
+ import type {
2
+ StateThresholds,
3
+ ConsecutiveThresholds,
4
+ WindowThresholds,
5
+ HealthCheckStatus,
6
+ } from "@checkstack/healthcheck-common";
7
+ import { DEFAULT_STATE_THRESHOLDS } from "@checkstack/healthcheck-common";
8
+
9
+ interface RunForEvaluation {
10
+ status: HealthCheckStatus;
11
+ timestamp: Date;
12
+ }
13
+
14
+ /**
15
+ * Evaluates the current health status based on recent runs and configured thresholds.
16
+ * Returns the evaluated status based on the threshold mode.
17
+ *
18
+ * @param runs - Recent health check runs, sorted by timestamp descending (newest first)
19
+ * @param thresholds - State threshold configuration (uses defaults if not provided)
20
+ */
21
+ export function evaluateHealthStatus(props: {
22
+ runs: RunForEvaluation[];
23
+ thresholds?: StateThresholds;
24
+ }): HealthCheckStatus {
25
+ const { runs, thresholds = DEFAULT_STATE_THRESHOLDS } = props;
26
+
27
+ if (runs.length === 0) {
28
+ // No runs yet - assume healthy (matches current behavior)
29
+ return "healthy";
30
+ }
31
+
32
+ return thresholds.mode === "consecutive"
33
+ ? evaluateConsecutive({ runs, thresholds })
34
+ : evaluateWindow({ runs, thresholds });
35
+ }
36
+
37
+ /**
38
+ * Consecutive mode: evaluates based on sequential identical results from most recent.
39
+ */
40
+ function evaluateConsecutive(props: {
41
+ runs: RunForEvaluation[];
42
+ thresholds: ConsecutiveThresholds;
43
+ }): HealthCheckStatus {
44
+ const { runs, thresholds } = props;
45
+
46
+ // Count consecutive identical results from most recent
47
+ let consecutiveFailures = 0;
48
+ let consecutiveSuccesses = 0;
49
+
50
+ for (const run of runs) {
51
+ if (run.status === "healthy") {
52
+ if (consecutiveFailures === 0) {
53
+ consecutiveSuccesses++;
54
+ } else {
55
+ break; // Streak ended
56
+ }
57
+ } else {
58
+ // degraded or unhealthy both count as failures for threshold purposes
59
+ if (consecutiveSuccesses === 0) {
60
+ consecutiveFailures++;
61
+ } else {
62
+ break; // Streak ended
63
+ }
64
+ }
65
+ }
66
+
67
+ // Evaluate thresholds (unhealthy > degraded > healthy)
68
+ if (consecutiveFailures >= thresholds.unhealthy.minFailureCount) {
69
+ return "unhealthy";
70
+ }
71
+ if (consecutiveFailures >= thresholds.degraded.minFailureCount) {
72
+ return "degraded";
73
+ }
74
+ if (consecutiveSuccesses >= thresholds.healthy.minSuccessCount) {
75
+ return "healthy";
76
+ }
77
+
78
+ // Edge case: not enough history to determine - use latest individual status
79
+ return runs[0].status;
80
+ }
81
+
82
+ /**
83
+ * Window mode: evaluates based on failure count within a sliding window.
84
+ * Better for flickering systems where failures are intermittent.
85
+ */
86
+ function evaluateWindow(props: {
87
+ runs: RunForEvaluation[];
88
+ thresholds: WindowThresholds;
89
+ }): HealthCheckStatus {
90
+ const { runs, thresholds } = props;
91
+
92
+ // Take only the window size worth of runs
93
+ const windowRuns = runs.slice(0, thresholds.windowSize);
94
+ const failureCount = windowRuns.filter((r) => r.status !== "healthy").length;
95
+
96
+ // Evaluate thresholds (unhealthy > degraded > healthy)
97
+ if (failureCount >= thresholds.unhealthy.minFailureCount) {
98
+ return "unhealthy";
99
+ }
100
+ if (failureCount >= thresholds.degraded.minFailureCount) {
101
+ return "degraded";
102
+ }
103
+
104
+ return "healthy";
105
+ }
@@ -0,0 +1,15 @@
1
+ import { Versioned } from "@checkstack/backend-api";
2
+ import {
3
+ StateThresholdsSchema,
4
+ type StateThresholds,
5
+ } from "@checkstack/healthcheck-common";
6
+
7
+ /**
8
+ * Versioned handler for state thresholds.
9
+ * Provides parsing, validation, and migration capabilities.
10
+ */
11
+ export const stateThresholds = new Versioned<StateThresholds>({
12
+ version: 1,
13
+ schema: StateThresholdsSchema,
14
+ migrations: [],
15
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/backend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }