@checkstack/ai-backend 0.2.0 → 0.3.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.
- package/CHANGELOG.md +73 -0
- package/package.json +6 -4
- package/src/chat/chat-service.streamturn.test.ts +16 -1
- package/src/chat/system-prompt.test.ts +11 -0
- package/src/chat/system-prompt.ts +34 -5
- package/src/extension-points.ts +89 -0
- package/src/generated/docs-index.ts +18 -3
- package/src/index.ts +46 -1
- package/src/registry-wiring.ts +24 -0
- package/src/system-signals-contributor.test.ts +162 -0
- package/src/system-signals-contributor.ts +129 -0
- package/src/tools/system-issues.test.ts +236 -0
- package/src/tools/system-issues.ts +209 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, test, expect, mock } from "bun:test";
|
|
2
|
+
import { access } from "@checkstack/common";
|
|
3
|
+
import type { AuthUser } from "@checkstack/backend-api";
|
|
4
|
+
import type { SystemSignal, SystemSignalsMap } from "@checkstack/catalog-common";
|
|
5
|
+
import {
|
|
6
|
+
createGatedSystemSignalsContributor,
|
|
7
|
+
type SystemAccessResolver,
|
|
8
|
+
} from "./system-signals-contributor";
|
|
9
|
+
|
|
10
|
+
// A read rule for a fictional "demo" plugin: id "thing.read", qualified
|
|
11
|
+
// "demo.thing.read", qualified resource type "demo.thing".
|
|
12
|
+
const rule = access("thing", "read", "View things", { pluginId: "demo" });
|
|
13
|
+
|
|
14
|
+
const signal = (source: string): SystemSignal => ({
|
|
15
|
+
source,
|
|
16
|
+
tone: "error",
|
|
17
|
+
label: "Problem",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const globalSignals: SystemSignalsMap = {
|
|
21
|
+
sysA: [signal("demo")],
|
|
22
|
+
sysB: [signal("demo")],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** A resolver that returns a fixed accessible subset and records its args. */
|
|
26
|
+
function stubResolver(accessible: string[]): {
|
|
27
|
+
resolver: SystemAccessResolver;
|
|
28
|
+
calls: Array<Parameters<SystemAccessResolver["accessibleSystemIds"]>[0]>;
|
|
29
|
+
} {
|
|
30
|
+
const calls: Array<
|
|
31
|
+
Parameters<SystemAccessResolver["accessibleSystemIds"]>[0]
|
|
32
|
+
> = [];
|
|
33
|
+
return {
|
|
34
|
+
calls,
|
|
35
|
+
resolver: {
|
|
36
|
+
accessibleSystemIds: async (args) => {
|
|
37
|
+
calls.push(args);
|
|
38
|
+
return accessible.filter((id) => args.systemIds.includes(id));
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const userWith = (accessRules: string[]): AuthUser => ({
|
|
45
|
+
type: "user",
|
|
46
|
+
id: "u1",
|
|
47
|
+
accessRules,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("createGatedSystemSignalsContributor", () => {
|
|
51
|
+
test("global-rule principal sees every system; resolver is not consulted", async () => {
|
|
52
|
+
const read = mock(async () => globalSignals);
|
|
53
|
+
const { resolver, calls } = stubResolver([]);
|
|
54
|
+
const contributor = createGatedSystemSignalsContributor({
|
|
55
|
+
sourceId: "demo",
|
|
56
|
+
accessRule: rule,
|
|
57
|
+
resolver,
|
|
58
|
+
readSignals: read,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = await contributor.read({
|
|
62
|
+
principal: userWith(["demo.thing.read"]),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result.accessible).toBe(true);
|
|
66
|
+
expect(Object.keys(result.signals).sort()).toEqual(["sysA", "sysB"]);
|
|
67
|
+
expect(read).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(calls).toHaveLength(0); // global access skips the team resolver
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("wildcard grant is treated as global", async () => {
|
|
72
|
+
const contributor = createGatedSystemSignalsContributor({
|
|
73
|
+
sourceId: "demo",
|
|
74
|
+
accessRule: rule,
|
|
75
|
+
resolver: stubResolver([]).resolver,
|
|
76
|
+
readSignals: async () => globalSignals,
|
|
77
|
+
});
|
|
78
|
+
const result = await contributor.read({ principal: userWith(["*"]) });
|
|
79
|
+
expect(Object.keys(result.signals).sort()).toEqual(["sysA", "sysB"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("service principals are trusted (treated as global)", async () => {
|
|
83
|
+
const contributor = createGatedSystemSignalsContributor({
|
|
84
|
+
sourceId: "demo",
|
|
85
|
+
accessRule: rule,
|
|
86
|
+
resolver: stubResolver([]).resolver,
|
|
87
|
+
readSignals: async () => globalSignals,
|
|
88
|
+
});
|
|
89
|
+
const principal: AuthUser = { type: "service", pluginId: "svc" };
|
|
90
|
+
const result = await contributor.read({ principal });
|
|
91
|
+
expect(result.accessible).toBe(true);
|
|
92
|
+
expect(Object.keys(result.signals).sort()).toEqual(["sysA", "sysB"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a manage grant satisfies the read rule (escalation) and sees all", async () => {
|
|
96
|
+
const contributor = createGatedSystemSignalsContributor({
|
|
97
|
+
sourceId: "demo",
|
|
98
|
+
accessRule: rule,
|
|
99
|
+
resolver: stubResolver([]).resolver,
|
|
100
|
+
readSignals: async () => globalSignals,
|
|
101
|
+
});
|
|
102
|
+
const result = await contributor.read({
|
|
103
|
+
principal: userWith(["demo.thing.manage"]),
|
|
104
|
+
});
|
|
105
|
+
expect(Object.keys(result.signals).sort()).toEqual(["sysA", "sysB"]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("non-global user is filtered to team-granted systems (correct resourceType)", async () => {
|
|
109
|
+
const read = mock(async () => globalSignals);
|
|
110
|
+
const { resolver, calls } = stubResolver(["sysB"]);
|
|
111
|
+
const contributor = createGatedSystemSignalsContributor({
|
|
112
|
+
sourceId: "demo",
|
|
113
|
+
accessRule: rule,
|
|
114
|
+
resolver,
|
|
115
|
+
readSignals: read,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const result = await contributor.read({ principal: userWith([]) });
|
|
119
|
+
|
|
120
|
+
expect(result.accessible).toBe(true);
|
|
121
|
+
expect(Object.keys(result.signals)).toEqual(["sysB"]); // sysA filtered out
|
|
122
|
+
expect(read).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(calls).toHaveLength(1);
|
|
124
|
+
expect(calls[0]).toMatchObject({
|
|
125
|
+
userId: "u1",
|
|
126
|
+
userType: "user",
|
|
127
|
+
resourceType: "demo.thing", // qualifyResourceType(pluginId, resource)
|
|
128
|
+
action: "read",
|
|
129
|
+
hasGlobalAccess: false,
|
|
130
|
+
});
|
|
131
|
+
expect(calls[0].systemIds.sort()).toEqual(["sysA", "sysB"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("non-global user with no team grants is reported inaccessible", async () => {
|
|
135
|
+
const { resolver } = stubResolver([]); // grants nothing
|
|
136
|
+
const contributor = createGatedSystemSignalsContributor({
|
|
137
|
+
sourceId: "demo",
|
|
138
|
+
accessRule: rule,
|
|
139
|
+
resolver,
|
|
140
|
+
readSignals: async () => globalSignals,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await contributor.read({ principal: userWith([]) });
|
|
144
|
+
|
|
145
|
+
expect(result).toEqual({ accessible: false, signals: {} });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("globally-clear source: accessible with no signals, resolver not called", async () => {
|
|
149
|
+
const { resolver, calls } = stubResolver([]);
|
|
150
|
+
const contributor = createGatedSystemSignalsContributor({
|
|
151
|
+
sourceId: "demo",
|
|
152
|
+
accessRule: rule,
|
|
153
|
+
resolver,
|
|
154
|
+
readSignals: async () => ({}),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const result = await contributor.read({ principal: userWith([]) });
|
|
158
|
+
|
|
159
|
+
expect(result).toEqual({ accessible: true, signals: {} });
|
|
160
|
+
expect(calls).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { AuthUser, RpcClient } from "@checkstack/backend-api";
|
|
2
|
+
import type { AccessRule } from "@checkstack/common";
|
|
3
|
+
import { isAccessRuleSatisfied, qualifyResourceType } from "@checkstack/common";
|
|
4
|
+
import { AuthApi } from "@checkstack/auth-common";
|
|
5
|
+
import type { SystemSignalsMap } from "@checkstack/catalog-common";
|
|
6
|
+
import {
|
|
7
|
+
principalGrantedRuleIds,
|
|
8
|
+
type SystemSignalsContributor,
|
|
9
|
+
} from "./extension-points";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the subset of `systemIds` a non-global principal may see for a
|
|
13
|
+
* resource type, applying the SAME team/instance grants the RPC middleware
|
|
14
|
+
* enforces for list/record endpoints (via auth's `getAccessibleResourceIds`).
|
|
15
|
+
*/
|
|
16
|
+
export interface SystemAccessResolver {
|
|
17
|
+
accessibleSystemIds(args: {
|
|
18
|
+
userId: string;
|
|
19
|
+
userType: "user" | "application";
|
|
20
|
+
resourceType: string;
|
|
21
|
+
systemIds: string[];
|
|
22
|
+
action: "read" | "manage";
|
|
23
|
+
hasGlobalAccess: boolean;
|
|
24
|
+
}): Promise<string[]>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a {@link SystemAccessResolver} backed by the auth plugin's
|
|
29
|
+
* `getAccessibleResourceIds` S2S query - the exact primitive the RPC middleware
|
|
30
|
+
* uses to filter list/record endpoints by team grants. A plugin gets its
|
|
31
|
+
* `rpcClient` from `coreServices.rpcClient` in `init`.
|
|
32
|
+
*/
|
|
33
|
+
export function createSystemAccessResolver(
|
|
34
|
+
rpcClient: RpcClient,
|
|
35
|
+
): SystemAccessResolver {
|
|
36
|
+
const authClient = rpcClient.forPlugin(AuthApi);
|
|
37
|
+
return {
|
|
38
|
+
accessibleSystemIds: ({
|
|
39
|
+
userId,
|
|
40
|
+
userType,
|
|
41
|
+
resourceType,
|
|
42
|
+
systemIds,
|
|
43
|
+
action,
|
|
44
|
+
hasGlobalAccess,
|
|
45
|
+
}) =>
|
|
46
|
+
authClient.getAccessibleResourceIds({
|
|
47
|
+
userId,
|
|
48
|
+
userType,
|
|
49
|
+
resourceType,
|
|
50
|
+
resourceIds: systemIds,
|
|
51
|
+
action,
|
|
52
|
+
hasGlobalAccess,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build a {@link SystemSignalsContributor} from a GLOBAL signal reader, applying
|
|
59
|
+
* the per-source access gate centrally so every source enforces it identically
|
|
60
|
+
* (and the same way the matching bulk RPC does):
|
|
61
|
+
*
|
|
62
|
+
* - A principal holding the global rule - including trusted service principals,
|
|
63
|
+
* which {@link principalGrantedRuleIds} maps to the wildcard - sees every
|
|
64
|
+
* system the source reports.
|
|
65
|
+
* - A real user / application WITHOUT the global rule sees only the systems its
|
|
66
|
+
* TEAM grants allow, via {@link SystemAccessResolver} (the same instance/team
|
|
67
|
+
* filtering the bulk RPC applies), so `system.issues` neither under- nor
|
|
68
|
+
* over-reports relative to the per-domain UI.
|
|
69
|
+
* - Any other principal without the global rule (anonymous, or a service lacking
|
|
70
|
+
* the wildcard) sees nothing.
|
|
71
|
+
*
|
|
72
|
+
* `readSignals` MUST return problem signals for ALL systems globally; the gate
|
|
73
|
+
* filters them. It is not called for a principal that can see nothing, so a
|
|
74
|
+
* no-access principal triggers no query.
|
|
75
|
+
*/
|
|
76
|
+
export function createGatedSystemSignalsContributor({
|
|
77
|
+
sourceId,
|
|
78
|
+
accessRule,
|
|
79
|
+
resolver,
|
|
80
|
+
readSignals,
|
|
81
|
+
}: {
|
|
82
|
+
sourceId: string;
|
|
83
|
+
accessRule: AccessRule;
|
|
84
|
+
resolver: SystemAccessResolver;
|
|
85
|
+
readSignals: () => Promise<SystemSignalsMap>;
|
|
86
|
+
}): SystemSignalsContributor {
|
|
87
|
+
const resourceType = qualifyResourceType(
|
|
88
|
+
accessRule.pluginId,
|
|
89
|
+
accessRule.resource,
|
|
90
|
+
);
|
|
91
|
+
return {
|
|
92
|
+
sourceId,
|
|
93
|
+
read: async ({ principal }: { principal: AuthUser }) => {
|
|
94
|
+
const hasGlobalAccess = isAccessRuleSatisfied(
|
|
95
|
+
principalGrantedRuleIds(principal),
|
|
96
|
+
accessRule,
|
|
97
|
+
);
|
|
98
|
+
if (hasGlobalAccess) {
|
|
99
|
+
return { accessible: true, signals: await readSignals() };
|
|
100
|
+
}
|
|
101
|
+
// Only real users / applications can carry per-team instance grants.
|
|
102
|
+
if (principal.type !== "user" && principal.type !== "application") {
|
|
103
|
+
return { accessible: false, signals: {} };
|
|
104
|
+
}
|
|
105
|
+
const signals = await readSignals();
|
|
106
|
+
const systemIds = Object.keys(signals);
|
|
107
|
+
if (systemIds.length === 0) {
|
|
108
|
+
// Source is globally clear: nothing to report and nothing to hide.
|
|
109
|
+
return { accessible: true, signals: {} };
|
|
110
|
+
}
|
|
111
|
+
const accessibleIds = new Set(
|
|
112
|
+
await resolver.accessibleSystemIds({
|
|
113
|
+
userId: principal.id,
|
|
114
|
+
userType: principal.type,
|
|
115
|
+
resourceType,
|
|
116
|
+
systemIds,
|
|
117
|
+
action: "read",
|
|
118
|
+
hasGlobalAccess: false,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
const filtered: SystemSignalsMap = {};
|
|
122
|
+
for (const [systemId, sigs] of Object.entries(signals)) {
|
|
123
|
+
if (accessibleIds.has(systemId)) filtered[systemId] = sigs;
|
|
124
|
+
}
|
|
125
|
+
// Accessible iff the principal may see at least one affected system.
|
|
126
|
+
return { accessible: accessibleIds.size > 0, signals: filtered };
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { AuthUser } from "@checkstack/backend-api";
|
|
3
|
+
import type { SystemSignal, SystemSignalsMap } from "@checkstack/catalog-common";
|
|
4
|
+
import type {
|
|
5
|
+
SystemSignalsContributor,
|
|
6
|
+
SystemSignalsContribution,
|
|
7
|
+
} from "../extension-points";
|
|
8
|
+
import {
|
|
9
|
+
mergeSystemSignalsMaps,
|
|
10
|
+
collectSystemSignals,
|
|
11
|
+
toSystemIssuesOutput,
|
|
12
|
+
type SystemSignalsCollection,
|
|
13
|
+
} from "./system-issues";
|
|
14
|
+
|
|
15
|
+
const signal = (over: Partial<SystemSignal> = {}): SystemSignal => ({
|
|
16
|
+
source: "incident",
|
|
17
|
+
tone: "error",
|
|
18
|
+
label: "Critical incident",
|
|
19
|
+
...over,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const principal: AuthUser = {
|
|
23
|
+
type: "user",
|
|
24
|
+
id: "u1",
|
|
25
|
+
accessRules: ["catalog.system.read"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** A contributor that is accessible and returns `signals`. */
|
|
29
|
+
const ok = (
|
|
30
|
+
sourceId: string,
|
|
31
|
+
signals: SystemSignalsMap,
|
|
32
|
+
): SystemSignalsContributor => ({
|
|
33
|
+
sourceId,
|
|
34
|
+
read: async (): Promise<SystemSignalsContribution> => ({
|
|
35
|
+
accessible: true,
|
|
36
|
+
signals,
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** A contributor the principal cannot access. */
|
|
41
|
+
const denied = (sourceId: string): SystemSignalsContributor => ({
|
|
42
|
+
sourceId,
|
|
43
|
+
read: async (): Promise<SystemSignalsContribution> => ({
|
|
44
|
+
accessible: false,
|
|
45
|
+
signals: {},
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** A contributor that throws while reading. */
|
|
50
|
+
const throwing = (sourceId: string): SystemSignalsContributor => ({
|
|
51
|
+
sourceId,
|
|
52
|
+
read: async (): Promise<SystemSignalsContribution> => {
|
|
53
|
+
throw new Error(`${sourceId} exploded`);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const emptyCollection = (
|
|
58
|
+
merged: SystemSignalsMap,
|
|
59
|
+
): SystemSignalsCollection => ({
|
|
60
|
+
merged,
|
|
61
|
+
checkedSources: [],
|
|
62
|
+
inaccessibleSources: [],
|
|
63
|
+
failedSources: [],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("mergeSystemSignalsMaps", () => {
|
|
67
|
+
test("merges two maps by systemId, concatenating signal arrays", () => {
|
|
68
|
+
const a: SystemSignalsMap = {
|
|
69
|
+
sysA: [signal({ source: "incident", label: "Incident" })],
|
|
70
|
+
sysB: [signal({ source: "incident", label: "B incident" })],
|
|
71
|
+
};
|
|
72
|
+
const b: SystemSignalsMap = {
|
|
73
|
+
sysA: [signal({ source: "slo", tone: "warn", label: "SLO at risk" })],
|
|
74
|
+
sysC: [signal({ source: "anomaly", tone: "warn", label: "Anomaly" })],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const merged = mergeSystemSignalsMaps([a, b]);
|
|
78
|
+
|
|
79
|
+
expect(Object.keys(merged).sort()).toEqual(["sysA", "sysB", "sysC"]);
|
|
80
|
+
// sysA gets both sources concatenated.
|
|
81
|
+
expect(merged.sysA).toHaveLength(2);
|
|
82
|
+
expect(merged.sysA.map((s) => s.source)).toEqual(["incident", "slo"]);
|
|
83
|
+
expect(merged.sysB).toHaveLength(1);
|
|
84
|
+
expect(merged.sysC).toHaveLength(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("does not mutate the input maps", () => {
|
|
88
|
+
const a: SystemSignalsMap = { sysA: [signal()] };
|
|
89
|
+
const b: SystemSignalsMap = { sysA: [signal({ source: "slo" })] };
|
|
90
|
+
|
|
91
|
+
mergeSystemSignalsMaps([a, b]);
|
|
92
|
+
|
|
93
|
+
expect(a.sysA).toHaveLength(1);
|
|
94
|
+
expect(b.sysA).toHaveLength(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("skips empty signal arrays", () => {
|
|
98
|
+
const merged = mergeSystemSignalsMaps([{ sysA: [] }, { sysB: [signal()] }]);
|
|
99
|
+
expect(Object.keys(merged)).toEqual(["sysB"]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("collectSystemSignals", () => {
|
|
104
|
+
test("merges signals from accessible contributors and lists them as checked", async () => {
|
|
105
|
+
const contributors = [
|
|
106
|
+
ok("incident", { sysA: [signal({ source: "incident" })] }),
|
|
107
|
+
ok("slo", {
|
|
108
|
+
sysA: [signal({ source: "slo", tone: "warn", label: "SLO" })],
|
|
109
|
+
sysB: [signal({ source: "slo", tone: "warn", label: "SLO B" })],
|
|
110
|
+
}),
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const result = await collectSystemSignals({ contributors, principal });
|
|
114
|
+
|
|
115
|
+
expect(result.merged.sysA.map((s) => s.source)).toEqual([
|
|
116
|
+
"incident",
|
|
117
|
+
"slo",
|
|
118
|
+
]);
|
|
119
|
+
expect(result.merged.sysB).toHaveLength(1);
|
|
120
|
+
expect(result.checkedSources.sort()).toEqual(["incident", "slo"]);
|
|
121
|
+
expect(result.inaccessibleSources).toEqual([]);
|
|
122
|
+
expect(result.failedSources).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("reports an inaccessible source distinctly (not as empty/clear)", async () => {
|
|
126
|
+
const contributors = [
|
|
127
|
+
denied("incident"),
|
|
128
|
+
ok("slo", { sysA: [signal({ source: "slo" })] }),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const result = await collectSystemSignals({ contributors, principal });
|
|
132
|
+
|
|
133
|
+
// The denied source contributes no signals but IS surfaced so the model
|
|
134
|
+
// can say "could not check incidents" instead of "no incidents".
|
|
135
|
+
expect(Object.keys(result.merged)).toEqual(["sysA"]);
|
|
136
|
+
expect(result.inaccessibleSources).toEqual(["incident"]);
|
|
137
|
+
expect(result.checkedSources).toEqual(["slo"]);
|
|
138
|
+
expect(result.failedSources).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("reports a throwing contributor as failed without breaking the call", async () => {
|
|
142
|
+
const contributors = [
|
|
143
|
+
throwing("boom"),
|
|
144
|
+
ok("incident", { sysA: [signal({ source: "incident" })] }),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const result = await collectSystemSignals({ contributors, principal });
|
|
148
|
+
|
|
149
|
+
expect(Object.keys(result.merged)).toEqual(["sysA"]);
|
|
150
|
+
expect(result.failedSources).toEqual(["boom"]);
|
|
151
|
+
expect(result.checkedSources).toEqual(["incident"]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("no contributors produces no entries and empty coverage", async () => {
|
|
155
|
+
const result = await collectSystemSignals({ contributors: [], principal });
|
|
156
|
+
expect(result.merged).toEqual({});
|
|
157
|
+
expect(result.checkedSources).toEqual([]);
|
|
158
|
+
expect(result.inaccessibleSources).toEqual([]);
|
|
159
|
+
expect(result.failedSources).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("toSystemIssuesOutput", () => {
|
|
164
|
+
test("groups signals by system and counts totals", () => {
|
|
165
|
+
const out = toSystemIssuesOutput({
|
|
166
|
+
collection: emptyCollection({
|
|
167
|
+
sysA: [signal({ source: "incident" }), signal({ source: "slo" })],
|
|
168
|
+
sysB: [signal({ source: "anomaly" })],
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(out.totalSystems).toBe(2);
|
|
173
|
+
expect(out.totalSignals).toBe(3);
|
|
174
|
+
const sysA = out.systems.find((s) => s.systemId === "sysA");
|
|
175
|
+
expect(sysA?.signals.map((s) => s.source)).toEqual(["incident", "slo"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("passes per-source coverage through to the output", () => {
|
|
179
|
+
const out = toSystemIssuesOutput({
|
|
180
|
+
collection: {
|
|
181
|
+
merged: { sysA: [signal()] },
|
|
182
|
+
checkedSources: ["incident", "slo"],
|
|
183
|
+
inaccessibleSources: ["healthcheck"],
|
|
184
|
+
failedSources: ["dependency"],
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(out.checkedSources).toEqual(["incident", "slo"]);
|
|
189
|
+
expect(out.inaccessibleSources).toEqual(["healthcheck"]);
|
|
190
|
+
expect(out.failedSources).toEqual(["dependency"]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("narrows to the requested systemIds", () => {
|
|
194
|
+
const out = toSystemIssuesOutput({
|
|
195
|
+
collection: emptyCollection({
|
|
196
|
+
sysA: [signal()],
|
|
197
|
+
sysB: [signal()],
|
|
198
|
+
}),
|
|
199
|
+
systemIds: ["sysB"],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(out.systems.map((s) => s.systemId)).toEqual(["sysB"]);
|
|
203
|
+
expect(out.totalSystems).toBe(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("drops href/accessRule/iconName, keeps source/tone/label/detail/since", () => {
|
|
207
|
+
const out = toSystemIssuesOutput({
|
|
208
|
+
collection: emptyCollection({
|
|
209
|
+
sysA: [
|
|
210
|
+
signal({
|
|
211
|
+
detail: "2 of 3 checks failing",
|
|
212
|
+
since: "2026-06-07T00:00:00Z",
|
|
213
|
+
href: "/checkstack/x",
|
|
214
|
+
accessRule: {
|
|
215
|
+
id: "x",
|
|
216
|
+
resource: "x",
|
|
217
|
+
level: "read",
|
|
218
|
+
pluginId: "p",
|
|
219
|
+
description: "view x",
|
|
220
|
+
},
|
|
221
|
+
iconName: "TriangleAlert",
|
|
222
|
+
}),
|
|
223
|
+
],
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const s = out.systems[0].signals[0];
|
|
228
|
+
expect(s).toEqual({
|
|
229
|
+
source: "incident",
|
|
230
|
+
tone: "error",
|
|
231
|
+
label: "Critical incident",
|
|
232
|
+
detail: "2 of 3 checks failing",
|
|
233
|
+
since: "2026-06-07T00:00:00Z",
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|