@checkstack/healthcheck-backend 1.21.3 → 1.23.0

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,136 @@
1
+ import { describe, it, expect, mock } from "bun:test";
2
+ import { call } from "@orpc/server";
3
+ import { createMockRpcContext } from "@checkstack/backend-api";
4
+ import { createHealthCheckRouter } from "./router";
5
+ import { createStubHealthCheckCache } from "./cache-test-stub";
6
+ import type { HealthRunReaction } from "./queue-executor";
7
+
8
+ /**
9
+ * A satellite result MUST drive the SAME core post-run path a local run does:
10
+ * the reactive `health` entity write, the notification, the automation hooks,
11
+ * etc. The router routes it there via the injected `reactToSatelliteRun`
12
+ * reactor. These tests pin that wiring so ingest can never silently regress to
13
+ * an insert-only path (which is what left satellite-detected outages silent).
14
+ */
15
+
16
+ // A service principal - ingestSatelliteResult is a backend-to-backend proc.
17
+ const serviceContext = () =>
18
+ createMockRpcContext({
19
+ user: { type: "service", pluginId: "satellite-backend" } as never,
20
+ });
21
+
22
+ function buildRouter(opts: {
23
+ reactToSatelliteRun?: (run: HealthRunReaction) => Promise<void>;
24
+ transaction?: ReturnType<typeof mock>;
25
+ }) {
26
+ // db.select(...).from(...).where(...) resolves the config row (name +
27
+ // collectors) that processSatelliteResult reads.
28
+ const database = {
29
+ select: mock(() => ({
30
+ from: mock(() => ({
31
+ where: mock(() =>
32
+ Promise.resolve([{ name: "Ping", collectors: [] }]),
33
+ ),
34
+ })),
35
+ })),
36
+ transaction:
37
+ opts.transaction ??
38
+ mock(async () => {
39
+ /* not expected in the reactor path */
40
+ }),
41
+ };
42
+
43
+ const collectorRegistry = {
44
+ register: mock(() => {}),
45
+ getCollector: mock(() => undefined),
46
+ getCollectors: mock(() => []),
47
+ };
48
+
49
+ const catalogClient = {
50
+ getSystem: mock(async () => ({ id: "sys-1", name: "API Server" })),
51
+ };
52
+
53
+ return createHealthCheckRouter({
54
+ database: database as never,
55
+ registry: { getStrategy: mock(() => undefined) } as never,
56
+ collectorRegistry: collectorRegistry as never,
57
+ gitOpsClient: {} as never,
58
+ getEmitHook: () => undefined,
59
+ cache: createStubHealthCheckCache(),
60
+ configService: {} as never,
61
+ catalogClient: catalogClient as never,
62
+ maintenanceClient: {} as never,
63
+ logger: {
64
+ debug: mock(() => {}),
65
+ info: mock(() => {}),
66
+ warn: mock(() => {}),
67
+ error: mock(() => {}),
68
+ } as never,
69
+ ...(opts.reactToSatelliteRun
70
+ ? { reactToSatelliteRun: opts.reactToSatelliteRun }
71
+ : {}),
72
+ });
73
+ }
74
+
75
+ const ingestInput = {
76
+ configId: "config-1",
77
+ systemId: "sys-1",
78
+ status: "unhealthy" as const,
79
+ latencyMs: 123,
80
+ result: {
81
+ status: "unhealthy" as const,
82
+ latencyMs: 123,
83
+ message: "Check failed",
84
+ metadata: { connected: true, collectors: {} },
85
+ },
86
+ executedAt: "2026-07-03T10:00:00.000Z",
87
+ sourceId: "sat-eu",
88
+ sourceLabel: "EU West",
89
+ environmentId: "env-prod",
90
+ };
91
+
92
+ describe("ingestSatelliteResult - drives the shared post-run path", () => {
93
+ it("routes a satellite result through reactToSatelliteRun with its processed payload", async () => {
94
+ const runs: HealthRunReaction[] = [];
95
+ const router = buildRouter({
96
+ reactToSatelliteRun: async (run) => {
97
+ runs.push(run);
98
+ },
99
+ });
100
+
101
+ await call(router.ingestSatelliteResult, ingestInput, {
102
+ context: serviceContext(),
103
+ });
104
+
105
+ expect(runs).toHaveLength(1);
106
+ const run = runs[0];
107
+ // The run carries the satellite's SOURCE + environment, and its display
108
+ // name was resolved for the notification the shared path will send.
109
+ expect(run.systemId).toBe("sys-1");
110
+ expect(run.systemName).toBe("API Server");
111
+ expect(run.configId).toBe("config-1");
112
+ expect(run.configName).toBe("Ping");
113
+ expect(run.environmentId).toBe("env-prod");
114
+ expect(run.status).toBe("unhealthy");
115
+ expect(run.sourceId).toBe("sat-eu");
116
+ expect(run.sourceLabel).toBe("EU West");
117
+ expect(run.runTimestamp).toEqual(new Date("2026-07-03T10:00:00.000Z"));
118
+ });
119
+
120
+ it("falls back to an insert-only persist when no reactor is wired", async () => {
121
+ // Record that the insert-only fallback opened a transaction, without
122
+ // executing its body (the aggregate internals need a full db mock and are
123
+ // covered elsewhere); the point is that ingest still records the run.
124
+ const transaction = mock(async () => {});
125
+ const router = buildRouter({ transaction });
126
+
127
+ await call(router.ingestSatelliteResult, ingestInput, {
128
+ context: serviceContext(),
129
+ });
130
+
131
+ // The insert-only fallback ran (a run is still recorded), but there is no
132
+ // reactor path to drive notifications/automations - which is exactly why
133
+ // the real host always wires `reactToSatelliteRun`.
134
+ expect(transaction).toHaveBeenCalledTimes(1);
135
+ });
136
+ });
package/src/router.ts CHANGED
@@ -46,6 +46,7 @@ import { CatalogApi } from "@checkstack/catalog-common";
46
46
  import { MaintenanceApi } from "@checkstack/maintenance-common";
47
47
  import type { Logger } from "@checkstack/backend-api";
48
48
  import type { HealthCheckCache } from "./cache";
49
+ import type { HealthRunReaction } from "./queue-executor";
49
50
  import {
50
51
  applySystemHealthOverrides,
51
52
  type SystemHealthOverrideReader,
@@ -104,6 +105,17 @@ export const createHealthCheckRouter = (opts: {
104
105
  * Optional so tests / no-incident deployments simply skip the fold.
105
106
  */
106
107
  incidentHealthOverrideReader?: SystemHealthOverrideReader;
108
+ /**
109
+ * Drives the SHARED core post-run path for an ingested satellite result -
110
+ * the reactive `health` entity write, cache reconcile, realtime signal,
111
+ * automation hooks, transition record, and subscriber notification - so a
112
+ * satellite-detected change reacts exactly like a local run. The host binds
113
+ * the service dependencies once (via a `persistRunAndReact` closure) and
114
+ * passes this narrowed reactor, so the local and satellite callers cannot
115
+ * pass different deps and drift. Optional so tests may omit it, in which case
116
+ * ingest falls back to an insert-only persist.
117
+ */
118
+ reactToSatelliteRun?: (run: HealthRunReaction) => Promise<void>;
107
119
  }) => {
108
120
  const {
109
121
  database,
@@ -118,6 +130,7 @@ export const createHealthCheckRouter = (opts: {
118
130
  signalService,
119
131
  recomputeSystemRollupHealth,
120
132
  incidentHealthOverrideReader,
133
+ reactToSatelliteRun,
121
134
  } = opts;
122
135
  // Create service instance once - shared across all handlers
123
136
  const service = new HealthCheckService(
@@ -612,6 +625,7 @@ export const createHealthCheckRouter = (opts: {
612
625
  enabled: input.body.enabled,
613
626
  stateThresholds: input.body.stateThresholds,
614
627
  satelliteIds: input.body.satelliteIds,
628
+ satelliteEnvironmentIds: input.body.satelliteEnvironmentIds,
615
629
  environmentIds: input.body.environmentIds,
616
630
  includeLocal: input.body.includeLocal,
617
631
  notificationPolicy: input.body.notificationPolicy,
@@ -634,6 +648,7 @@ export const createHealthCheckRouter = (opts: {
634
648
  enabled: input.enabled,
635
649
  stateThresholds: input.stateThresholds,
636
650
  satelliteIds: input.satelliteIds,
651
+ satelliteEnvironmentIds: input.satelliteEnvironmentIds,
637
652
  environmentIds: input.environmentIds,
638
653
  includeLocal: input.includeLocal,
639
654
  notificationPolicy: input.notificationPolicy,
@@ -899,10 +914,56 @@ export const createHealthCheckRouter = (opts: {
899
914
 
900
915
  ingestSatelliteResult: os.ingestSatelliteResult.handler(
901
916
  async ({ input }) => {
902
- await service.ingestSatelliteResult(input);
903
- // A satellite result writes a new run for this system, so the
904
- // cached aggregate status is now stale.
905
- await cache.invalidateSystem(input.systemId);
917
+ // Process the satellite's raw result (evaluate assertions, strip
918
+ // ephemeral fields, resolve the check name) WITHOUT persisting...
919
+ const { status, resultRecord, configName } =
920
+ await service.processSatelliteResult({
921
+ configId: input.configId,
922
+ status: input.status,
923
+ result: input.result,
924
+ });
925
+
926
+ if (reactToSatelliteRun) {
927
+ // ...then persist + REACT through the SAME core post-run path a local
928
+ // run uses (reactive entity, cache reconcile, signals, automation
929
+ // hooks, transition, notification). Previously ingest only inserted
930
+ // the row, so a satellite-detected outage fired no notifications or
931
+ // automations - routing it here is what keeps the two from drifting.
932
+ const system = await catalogClient
933
+ .getSystem({ systemId: input.systemId })
934
+ .catch(() => null);
935
+ await reactToSatelliteRun({
936
+ systemId: input.systemId,
937
+ systemName: system?.name ?? input.systemId,
938
+ configId: input.configId,
939
+ configName,
940
+ environmentId: input.environmentId ?? null,
941
+ // The env NAME is intentionally omitted (no extra catalog
942
+ // round-trip): the notifier falls back to the system name.
943
+ status,
944
+ latencyMs: input.latencyMs,
945
+ result: resultRecord,
946
+ sourceId: input.sourceId,
947
+ sourceLabel: input.sourceLabel,
948
+ runTimestamp: new Date(input.executedAt),
949
+ });
950
+ } else {
951
+ // No reactor wired (older host / test harness): fall back to a bare
952
+ // persist + cache refresh so the run is still recorded. The real host
953
+ // always wires the reactor above.
954
+ await service.insertSatelliteRun({
955
+ configId: input.configId,
956
+ systemId: input.systemId,
957
+ environmentId: input.environmentId ?? null,
958
+ status,
959
+ latencyMs: input.latencyMs,
960
+ result: resultRecord,
961
+ sourceId: input.sourceId,
962
+ sourceLabel: input.sourceLabel,
963
+ executedAt: input.executedAt,
964
+ });
965
+ await cache.invalidateSystem(input.systemId);
966
+ }
906
967
  },
907
968
  ),
908
969
 
@@ -0,0 +1,199 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ buildUnobservableResult,
4
+ buildUnobservableRun,
5
+ resolveSatelliteOnlyOutcome,
6
+ } from "./satellite-liveness";
7
+
8
+ describe("resolveSatelliteOnlyOutcome", () => {
9
+ test("satellites execute the check when any assigned one is online", () => {
10
+ expect(
11
+ resolveSatelliteOnlyOutcome({
12
+ satelliteIds: ["a", "b"],
13
+ onlineSatelliteIds: ["b"],
14
+ }),
15
+ ).toBe("satellites-executing");
16
+ });
17
+
18
+ test("records an unobservable run when NO assigned satellite is online", () => {
19
+ // The bug: this used to return silently, so the check kept displaying its
20
+ // last status forever and a dead probe read exactly like a passing one.
21
+ expect(
22
+ resolveSatelliteOnlyOutcome({
23
+ satelliteIds: ["a", "b"],
24
+ onlineSatelliteIds: [],
25
+ }),
26
+ ).toBe("record-unobservable");
27
+ });
28
+
29
+ test("ignores online satellites this check is not assigned to", () => {
30
+ expect(
31
+ resolveSatelliteOnlyOutcome({
32
+ satelliteIds: ["a"],
33
+ onlineSatelliteIds: ["someone-else"],
34
+ }),
35
+ ).toBe("record-unobservable");
36
+ });
37
+
38
+ test("stays silent when liveness is UNKNOWN", () => {
39
+ // A transient failure to reach the satellite service must never mark every
40
+ // satellite-only check in the fleet degraded at once. Unknown is not the
41
+ // same as offline, and silence is the pre-existing, safe direction.
42
+ expect(
43
+ resolveSatelliteOnlyOutcome({ satelliteIds: ["a"] }),
44
+ ).toBe("satellites-executing");
45
+ });
46
+
47
+ test("a check with no assigned satellites is treated as executing", () => {
48
+ // Not reachable from the caller (the branch requires assignments), but the
49
+ // function must not claim an empty assignment set is unobservable.
50
+ expect(
51
+ resolveSatelliteOnlyOutcome({
52
+ satelliteIds: [],
53
+ onlineSatelliteIds: [],
54
+ }),
55
+ ).toBe("satellites-executing");
56
+ });
57
+
58
+ test("one online satellite out of many is enough", () => {
59
+ expect(
60
+ resolveSatelliteOnlyOutcome({
61
+ satelliteIds: ["a", "b", "c"],
62
+ onlineSatelliteIds: ["c"],
63
+ }),
64
+ ).toBe("satellites-executing");
65
+ });
66
+ });
67
+
68
+ describe("buildUnobservableResult", () => {
69
+ test("says plainly that health is UNKNOWN, not that the target is down", () => {
70
+ // The run is degraded because we could not observe, not because the service
71
+ // failed. The message must not let an operator conclude otherwise.
72
+ const result = buildUnobservableResult({ satelliteIds: ["a", "b"] });
73
+
74
+ expect(String(result.error)).toContain("unknown");
75
+ expect(String(result.error)).toContain("monitoring gap");
76
+ expect(String(result.error)).toContain("not a confirmed outage");
77
+ });
78
+
79
+ test("carries a machine-readable marker and the assignment count", () => {
80
+ const result = buildUnobservableResult({ satelliteIds: ["a", "b"] });
81
+
82
+ expect(result.satelliteOffline).toBe(true);
83
+ expect(result.assignedSatelliteCount).toBe(2);
84
+ });
85
+ });
86
+
87
+ describe("buildUnobservableRun", () => {
88
+ test("records DEGRADED, never unhealthy", () => {
89
+ // Unhealthy would raise incident-grade alarms about services that may be
90
+ // perfectly healthy, every time a satellite host reboots.
91
+ expect(
92
+ buildUnobservableRun({ environmentId: null, satelliteIds: ["a"] }).status,
93
+ ).toBe("degraded");
94
+ });
95
+
96
+ test("lands on the slice the job owns, including a concrete environment", () => {
97
+ // The satellites would have reported for this exact slice, so the gap has
98
+ // to be recorded there and not on the rollup.
99
+ expect(
100
+ buildUnobservableRun({ environmentId: "env-1", satelliteIds: ["a"] })
101
+ .environmentId,
102
+ ).toBe("env-1");
103
+ expect(
104
+ buildUnobservableRun({ environmentId: null, satelliteIds: ["a"] })
105
+ .environmentId,
106
+ ).toBeNull();
107
+ });
108
+
109
+ test("attributes the run to the core, not to a satellite", () => {
110
+ // A satellite reported nothing - the core is what noticed the gap.
111
+ expect(
112
+ buildUnobservableRun({ environmentId: null, satelliteIds: ["a"] })
113
+ .sourceLabel,
114
+ ).toBe("Local");
115
+ });
116
+
117
+ test("carries the explanatory result payload", () => {
118
+ const run = buildUnobservableRun({
119
+ environmentId: null,
120
+ satelliteIds: ["a", "b"],
121
+ });
122
+
123
+ expect(run.result.satelliteOffline).toBe(true);
124
+ expect(run.result.assignedSatelliteCount).toBe(2);
125
+ expect(String(run.result.error)).toContain("unknown");
126
+ });
127
+ });
128
+
129
+ describe("resolveSatelliteOnlyOutcome - assignment changes must not fabricate a gap", () => {
130
+ /**
131
+ * The class of bug these guard: an operator changes an assignment and the
132
+ * platform reacts as though something FAILED. Removing a satellite, adding
133
+ * local execution back, or swapping one satellite for another are all
134
+ * deliberate acts - none of them means "nobody is checking this".
135
+ */
136
+ test("emptying the satellite list is not an outage", () => {
137
+ // The executor's branch requires a non-empty list, but the predicate must
138
+ // agree independently - a future caller must not be able to turn a cleared
139
+ // assignment into a degraded run.
140
+ expect(
141
+ resolveSatelliteOnlyOutcome({
142
+ satelliteIds: [],
143
+ onlineSatelliteIds: [],
144
+ }),
145
+ ).toBe("satellites-executing");
146
+ });
147
+
148
+ test("swapping to a different, online satellite is not an outage", () => {
149
+ expect(
150
+ resolveSatelliteOnlyOutcome({
151
+ satelliteIds: ["new-sat"],
152
+ onlineSatelliteIds: ["new-sat"],
153
+ }),
154
+ ).toBe("satellites-executing");
155
+ });
156
+
157
+ test("one online satellite is enough even when the others were deleted", () => {
158
+ // A deleted satellite simply stops appearing in the online set. As long as
159
+ // ONE assigned satellite is still online, the check is being executed.
160
+ expect(
161
+ resolveSatelliteOnlyOutcome({
162
+ satelliteIds: ["deleted-sat", "live-sat"],
163
+ onlineSatelliteIds: ["live-sat", "unrelated-sat"],
164
+ }),
165
+ ).toBe("satellites-executing");
166
+ });
167
+
168
+ test("an entirely unrelated fleet being online is NOT enough", () => {
169
+ // Guards the inverse mistake: "some satellite somewhere is up" must not be
170
+ // read as "this check is being executed".
171
+ expect(
172
+ resolveSatelliteOnlyOutcome({
173
+ satelliteIds: ["mine"],
174
+ onlineSatelliteIds: ["someone-elses", "another"],
175
+ }),
176
+ ).toBe("record-unobservable");
177
+ });
178
+
179
+ test("an empty online set with no assignment is not an outage", () => {
180
+ expect(
181
+ resolveSatelliteOnlyOutcome({ satelliteIds: [], onlineSatelliteIds: [] }),
182
+ ).toBe("satellites-executing");
183
+ });
184
+
185
+ test("duplicate ids in an assignment do not change the verdict", () => {
186
+ expect(
187
+ resolveSatelliteOnlyOutcome({
188
+ satelliteIds: ["a", "a"],
189
+ onlineSatelliteIds: ["a"],
190
+ }),
191
+ ).toBe("satellites-executing");
192
+ expect(
193
+ resolveSatelliteOnlyOutcome({
194
+ satelliteIds: ["a", "a"],
195
+ onlineSatelliteIds: [],
196
+ }),
197
+ ).toBe("record-unobservable");
198
+ });
199
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * What a satellite-only check should do when the core reaches its tick.
3
+ *
4
+ * ## The bug this exists to close
5
+ *
6
+ * A check with `includeLocal: false` and assigned satellites is executed BY
7
+ * those satellites; the core's own tick has nothing to run and returned
8
+ * immediately. If every assigned satellite is offline, nobody executes it - and
9
+ * because the core recorded nothing at all, the check kept displaying whatever
10
+ * status it last had, indefinitely. A dead probe was indistinguishable from a
11
+ * passing one, which is the single worst failure mode a monitoring tool can
12
+ * have.
13
+ *
14
+ * So the core now records a `degraded` run instead of staying silent.
15
+ * `degraded` rather than `unhealthy` because the target may be perfectly
16
+ * healthy - what failed is our ability to observe it. Marking it unhealthy
17
+ * would raise incident-grade alarms about services that are fine, every time a
18
+ * satellite host reboots.
19
+ */
20
+ export type SatelliteOnlyOutcome =
21
+ /** Satellites are executing this check; the core has nothing to do. */
22
+ | "satellites-executing"
23
+ /** No assigned satellite is online - record a stale run so the gap is visible. */
24
+ | "record-unobservable";
25
+
26
+ export function resolveSatelliteOnlyOutcome({
27
+ satelliteIds,
28
+ onlineSatelliteIds,
29
+ }: {
30
+ /** Satellites assigned to this check. */
31
+ satelliteIds: readonly string[];
32
+ /**
33
+ * Currently-online satellite ids. `undefined` when liveness could not be
34
+ * determined at all (no resolver wired, or the lookup failed).
35
+ */
36
+ onlineSatelliteIds?: readonly string[];
37
+ }): SatelliteOnlyOutcome {
38
+ // Unknown liveness must NEVER manufacture a degraded run: a transient failure
39
+ // to reach the satellite service would otherwise mark every satellite-only
40
+ // check degraded across the fleet at once. Staying silent is the pre-existing
41
+ // behaviour and the safe direction for an unknown.
42
+ if (onlineSatelliteIds === undefined) return "satellites-executing";
43
+
44
+ // An empty assignment set has nothing to be offline. `[].some()` is false, so
45
+ // without this guard a check with no satellites would be reported as
46
+ // unobservable - a degraded run for a configuration that cannot produce one.
47
+ if (satelliteIds.length === 0) return "satellites-executing";
48
+
49
+ const online = new Set(onlineSatelliteIds);
50
+ const anyOnline = satelliteIds.some((id) => online.has(id));
51
+
52
+ return anyOnline ? "satellites-executing" : "record-unobservable";
53
+ }
54
+
55
+ /** The result payload recorded for an unobservable run. */
56
+ export function buildUnobservableResult({
57
+ satelliteIds,
58
+ }: {
59
+ satelliteIds: readonly string[];
60
+ }): Record<string, unknown> {
61
+ const count = satelliteIds.length;
62
+ return {
63
+ error:
64
+ `No assigned satellite is online (${count} assigned), so this check could not be executed. ` +
65
+ "The target's actual health is unknown - this is a monitoring gap, not a confirmed outage.",
66
+ satelliteOffline: true,
67
+ assignedSatelliteCount: count,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * The `persistRunAndReact` arguments for an unobservable run.
73
+ *
74
+ * Extracted so the RECORDED VALUES - degraded, the payload's environment slice,
75
+ * the local source label - are pinned by a test. The executor's mock database
76
+ * cannot service `persistRunAndReact`'s insert/aggregate chain, so the only way
77
+ * to assert what gets written is to make the decision about what to write a
78
+ * separate, pure step.
79
+ */
80
+ export function buildUnobservableRun({
81
+ environmentId,
82
+ satelliteIds,
83
+ }: {
84
+ /** The single (config, system, env) slice this job owns. */
85
+ environmentId: string | null;
86
+ satelliteIds: readonly string[];
87
+ }): {
88
+ status: "degraded";
89
+ environmentId: string | null;
90
+ sourceLabel: string;
91
+ result: Record<string, unknown>;
92
+ } {
93
+ return {
94
+ // Degraded, NOT unhealthy: the target may be perfectly healthy and what
95
+ // failed is our ability to observe it. Unhealthy would raise
96
+ // incident-grade alarms about healthy services on every satellite reboot.
97
+ status: "degraded",
98
+ // The job payload already names the slice the satellites would have
99
+ // reported for, so the gap lands exactly where the missing runs would have.
100
+ environmentId,
101
+ // Recorded by the CORE, which is what noticed the gap - not by a satellite,
102
+ // which by definition reported nothing.
103
+ sourceLabel: "Local",
104
+ result: buildUnobservableResult({ satelliteIds }),
105
+ };
106
+ }
package/src/schema.ts CHANGED
@@ -111,6 +111,27 @@ export const systemHealthChecks = pgTable(
111
111
  * `length === 0`; jsonb stores both faithfully.
112
112
  */
113
113
  environmentIds: jsonb("environment_ids").$type<string[]>(),
114
+ /**
115
+ * Per-SATELLITE environment scoping, keyed by satellite id.
116
+ *
117
+ * Additive on top of `satelliteIds`, which still answers "which satellites
118
+ * run this". This answers "and for which environments", so a prod satellite
119
+ * can run only the prod environment instead of probing every environment
120
+ * from a network it may have no route to.
121
+ *
122
+ * Semantics per entry, mirroring `environmentIds`:
123
+ * - key ABSENT (or `null`) => that satellite runs every environment the
124
+ * assignment resolves to. This is the backfilled default, so existing
125
+ * rows keep behaving as they did.
126
+ * - `[]` => opt out: that satellite runs ONCE with no environment.
127
+ * - non-empty => exactly those ids, INTERSECTED with the assignment's own
128
+ * effective set - a satellite can never widen the assignment's scope,
129
+ * only narrow it.
130
+ */
131
+ satelliteEnvironmentIds:
132
+ jsonb("satellite_environment_ids").$type<
133
+ Record<string, string[] | null>
134
+ >(),
114
135
  /**
115
136
  * Whether to also run this check locally on the core instance.
116
137
  * Defaults to true. Only relevant when satelliteIds is set.
@@ -97,7 +97,9 @@ describe("HealthCheckService.getSystemHealthStatus - read batching", () => {
97
97
 
98
98
  const result = await service.getSystemHealthStatus("system-1");
99
99
 
100
- expect(result.status).toBe("healthy");
100
+ // `unknown`, not `healthy`: a system with no checks is unmeasured, and this
101
+ // test is about transaction batching, not about inventing a status.
102
+ expect(result.status).toBe("unknown");
101
103
  expect(result.checkStatuses).toHaveLength(0);
102
104
  const transaction = (mockDb as unknown as { transaction: ReturnType<typeof mock> })
103
105
  .transaction;