@skyhook-io/radar-app 1.13.1 → 1.13.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.
- package/package.json +2 -2
- package/src/App.tsx +19 -1
- package/src/RadarApp.tsx +2 -2
- package/src/api/diagnose.test.ts +268 -0
- package/src/api/diagnose.ts +72 -9
- package/src/components/diagnose/AISettings.tsx +1 -1
- package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
- package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
- package/src/components/diagnose/DiagnoseContext.tsx +12 -17
- package/src/components/diagnose/DiagnoseSurface.test.tsx +211 -16
- package/src/components/diagnose/DiagnoseSurface.tsx +464 -133
- package/src/components/diagnose/Home.test.tsx +293 -0
- package/src/components/diagnose/Home.tsx +289 -119
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
- package/src/components/diagnose/InvestigationView.test.ts +17 -0
- package/src/components/diagnose/InvestigationView.tsx +1900 -393
- package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
- package/src/components/diagnose/agentCatalog.ts +1 -1
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
- package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
- package/src/components/diagnose/investigationEvidence.ts +3492 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
- package/src/components/diagnose/investigationExplanation.test.ts +63 -0
- package/src/components/diagnose/investigationExplanation.ts +22 -0
- package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
- package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
- package/src/components/diagnose/investigationSourceFocus.ts +98 -0
- package/src/components/diagnose/investigationState.test.ts +695 -0
- package/src/components/diagnose/investigationState.ts +451 -0
- package/src/components/diagnose/parts.test.tsx +864 -3
- package/src/components/diagnose/parts.tsx +1337 -541
- package/src/components/diagnose/target.test.ts +39 -0
- package/src/components/diagnose/target.ts +36 -0
- package/src/components/diagnose/useDisclosureReveal.ts +117 -0
- package/src/components/home/MCPSetupDialog.tsx +2 -2
- package/src/components/home/mcpToolCatalog.test.ts +22 -0
- package/src/components/home/mcpToolCatalog.ts +3 -2
- package/src/components/issues/IssuesPane.tsx +5 -1
- package/src/components/settings/SettingsDialog.tsx +11 -13
- package/src/components/workload/WorkloadView.tsx +1 -1
- package/src/context/DiagnoseCustomization.tsx +11 -8
- package/src/index.css +63 -79
- package/src/index.ts +1 -1
|
@@ -0,0 +1,2170 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
INVESTIGATION_DISCLOSURE_SETTLE_MS,
|
|
6
|
+
InvestigationEvidencePane,
|
|
7
|
+
VISIBLE_LOG_EVIDENCE_LINES,
|
|
8
|
+
partitionInvestigationEvidence,
|
|
9
|
+
investigationDisclosureSettleDelay,
|
|
10
|
+
investigationDisclosureScrollTop,
|
|
11
|
+
investigationEvidenceFullRowFlags,
|
|
12
|
+
investigationEvidenceRevealCollection,
|
|
13
|
+
investigationEvidenceShouldRevealHistory,
|
|
14
|
+
} from "./InvestigationEvidencePane";
|
|
15
|
+
import {
|
|
16
|
+
investigationEvidenceSourceDomId,
|
|
17
|
+
investigationEvidenceStepIdsByTurn,
|
|
18
|
+
projectInvestigationEvidence,
|
|
19
|
+
resolveInvestigationRootCauseEvidence,
|
|
20
|
+
type InvestigationEvidenceProjection,
|
|
21
|
+
type InvestigationRootCauseEvidenceResolution,
|
|
22
|
+
type InvestigationEvidenceTimelineItem,
|
|
23
|
+
} from "./investigationEvidence";
|
|
24
|
+
import type { DiagnosisResourceRef } from "./diagnoseEvidenceTypes";
|
|
25
|
+
import type { Diagnosis } from "../../api/diagnose";
|
|
26
|
+
import { AssessmentSources, ResultCard } from "./parts";
|
|
27
|
+
import { investigationEvidenceCoverageLimited } from "./investigationState";
|
|
28
|
+
|
|
29
|
+
const onViewSource = vi.fn();
|
|
30
|
+
const target = {
|
|
31
|
+
kind: "Deployment",
|
|
32
|
+
group: "apps",
|
|
33
|
+
namespace: "shop",
|
|
34
|
+
name: "api",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function tool(
|
|
38
|
+
id: string,
|
|
39
|
+
name: string,
|
|
40
|
+
result: unknown,
|
|
41
|
+
patch: Partial<
|
|
42
|
+
Extract<InvestigationEvidenceTimelineItem, { kind: "tool" }>
|
|
43
|
+
> = {},
|
|
44
|
+
): Extract<InvestigationEvidenceTimelineItem, { kind: "tool" }> {
|
|
45
|
+
return {
|
|
46
|
+
kind: "tool",
|
|
47
|
+
id,
|
|
48
|
+
tool: name,
|
|
49
|
+
status: "done",
|
|
50
|
+
summary: JSON.stringify({ namespace: "shop", name: "api" }),
|
|
51
|
+
result: JSON.stringify(result),
|
|
52
|
+
isError: false,
|
|
53
|
+
radarEvidence: true,
|
|
54
|
+
...patch,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function project(
|
|
59
|
+
...timeline: InvestigationEvidenceTimelineItem[]
|
|
60
|
+
): InvestigationEvidenceProjection {
|
|
61
|
+
return projectInvestigationEvidence([{ timeline }], target);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function render(
|
|
65
|
+
projection: InvestigationEvidenceProjection,
|
|
66
|
+
collecting = false,
|
|
67
|
+
afterEvidence?: string,
|
|
68
|
+
rootCauseEvidence?: InvestigationRootCauseEvidenceResolution,
|
|
69
|
+
onOpenResource?: (ref: DiagnosisResourceRef) => void,
|
|
70
|
+
): string {
|
|
71
|
+
return renderToStaticMarkup(
|
|
72
|
+
<InvestigationEvidencePane
|
|
73
|
+
projection={projection}
|
|
74
|
+
rootCauseEvidence={rootCauseEvidence}
|
|
75
|
+
collecting={collecting}
|
|
76
|
+
animateGroupIds={new Set()}
|
|
77
|
+
onViewSource={onViewSource}
|
|
78
|
+
onViewActivity={() => {}}
|
|
79
|
+
onOpenResource={onOpenResource}
|
|
80
|
+
afterEvidence={afterEvidence}
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function evidenceRef(scope: string, nonce: string): string {
|
|
86
|
+
return `ev_${scope.repeat(26)}_${nonce.repeat(26)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const criticalIssue = {
|
|
90
|
+
id: "issue-api-crash",
|
|
91
|
+
severity: "critical",
|
|
92
|
+
source: "problem",
|
|
93
|
+
category: "crashloop",
|
|
94
|
+
category_group: "runtime",
|
|
95
|
+
grouping_scope: "workload",
|
|
96
|
+
kind: "Deployment",
|
|
97
|
+
group: "apps",
|
|
98
|
+
namespace: "shop",
|
|
99
|
+
name: "api",
|
|
100
|
+
reason: "CrashLoopBackOff",
|
|
101
|
+
message: "The API container keeps restarting.",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
describe("InvestigationEvidencePane hierarchy and provenance", () => {
|
|
105
|
+
it("keeps the target issue from a cluster-wide response, not neighboring failures", () => {
|
|
106
|
+
const projection = project(
|
|
107
|
+
tool(
|
|
108
|
+
"cluster-issues",
|
|
109
|
+
"issues",
|
|
110
|
+
{
|
|
111
|
+
issues: [
|
|
112
|
+
criticalIssue,
|
|
113
|
+
{
|
|
114
|
+
...criticalIssue,
|
|
115
|
+
id: "unrelated",
|
|
116
|
+
name: "unrelated",
|
|
117
|
+
reason: "UnrelatedFailure",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
total: 2,
|
|
121
|
+
total_matched: 2,
|
|
122
|
+
},
|
|
123
|
+
{ summary: "{}" },
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
const partition = partitionInvestigationEvidence(projection.groups);
|
|
127
|
+
expect(partition.main).toHaveLength(1);
|
|
128
|
+
const html = render(projection);
|
|
129
|
+
expect(html).toContain("CrashLoopBackOff");
|
|
130
|
+
expect(html).not.toContain("UnrelatedFailure");
|
|
131
|
+
expect(projection.groups).toHaveLength(2);
|
|
132
|
+
expect(
|
|
133
|
+
investigationEvidenceStepIdsByTurn(
|
|
134
|
+
projection,
|
|
135
|
+
new Set(partition.collectionByGroup.keys()),
|
|
136
|
+
)
|
|
137
|
+
.get(0)
|
|
138
|
+
?.has("cluster-issues"),
|
|
139
|
+
).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("keeps coverage navigation when its primary broad card is omitted", () => {
|
|
143
|
+
const projection = project(
|
|
144
|
+
tool(
|
|
145
|
+
"limited-broad",
|
|
146
|
+
"issues",
|
|
147
|
+
{
|
|
148
|
+
issues: [{ ...criticalIssue, id: "other", name: "other" }],
|
|
149
|
+
total: 1,
|
|
150
|
+
total_matched: 2,
|
|
151
|
+
},
|
|
152
|
+
{ summary: "{}" },
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
const partition = partitionInvestigationEvidence(projection.groups);
|
|
156
|
+
const source = projection.sources[0];
|
|
157
|
+
expect(source.primaryGroupId).toBeDefined();
|
|
158
|
+
expect(partition.collectionByGroup.size).toBe(0);
|
|
159
|
+
expect(projection.limitations.length).toBeGreaterThan(0);
|
|
160
|
+
expect(
|
|
161
|
+
investigationEvidenceRevealCollection(projection, source.id, partition),
|
|
162
|
+
).toBe("coverage");
|
|
163
|
+
const html = render(projection);
|
|
164
|
+
expect(
|
|
165
|
+
html.split(`id="${investigationEvidenceSourceDomId(source.id)}"`),
|
|
166
|
+
).toHaveLength(2);
|
|
167
|
+
expect(html).not.toContain("<article");
|
|
168
|
+
expect(
|
|
169
|
+
investigationEvidenceStepIdsByTurn(projection, new Set())
|
|
170
|
+
.get(0)
|
|
171
|
+
?.has("limited-broad"),
|
|
172
|
+
).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("does not promote namespace-wide change lists even when cited", () => {
|
|
176
|
+
const ref = evidenceRef("a", "b");
|
|
177
|
+
const projection = project(
|
|
178
|
+
tool(
|
|
179
|
+
"broad-changes",
|
|
180
|
+
"get_changes",
|
|
181
|
+
{
|
|
182
|
+
changes: [
|
|
183
|
+
{
|
|
184
|
+
kind: "Deployment",
|
|
185
|
+
namespace: "shop",
|
|
186
|
+
name: "other",
|
|
187
|
+
summary: "Unrelated rollout",
|
|
188
|
+
changeType: "update",
|
|
189
|
+
timestamp: "2026-09-07T00:00:00Z",
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
{ summary: JSON.stringify({ namespace: "shop" }), evidenceRef: ref },
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
197
|
+
projection,
|
|
198
|
+
{ status: "linked", refs: [ref] },
|
|
199
|
+
0,
|
|
200
|
+
);
|
|
201
|
+
expect(resolution.status).toBe("linked");
|
|
202
|
+
expect(projection.groups.length).toBeGreaterThan(0);
|
|
203
|
+
expect(render(projection, false, undefined, resolution)).not.toContain(
|
|
204
|
+
"Unrelated rollout",
|
|
205
|
+
);
|
|
206
|
+
expect(
|
|
207
|
+
partitionInvestigationEvidence(projection.groups, resolution)
|
|
208
|
+
.collectionByGroup.size,
|
|
209
|
+
).toBe(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("does not promote an arbitrary issue from a cited broad result", () => {
|
|
213
|
+
const ref = evidenceRef("a", "b");
|
|
214
|
+
const projection = project(
|
|
215
|
+
tool(
|
|
216
|
+
"broad",
|
|
217
|
+
"issues",
|
|
218
|
+
{
|
|
219
|
+
issues: ["db", "queue"].map((name) => ({
|
|
220
|
+
...criticalIssue,
|
|
221
|
+
id: name,
|
|
222
|
+
name,
|
|
223
|
+
namespace: "other",
|
|
224
|
+
})),
|
|
225
|
+
total: 2,
|
|
226
|
+
total_matched: 2,
|
|
227
|
+
},
|
|
228
|
+
{ summary: JSON.stringify({ namespace: "other" }), evidenceRef: ref },
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
232
|
+
projection,
|
|
233
|
+
{ status: "linked", refs: [ref] },
|
|
234
|
+
0,
|
|
235
|
+
);
|
|
236
|
+
const partition = partitionInvestigationEvidence(
|
|
237
|
+
projection.groups,
|
|
238
|
+
resolution,
|
|
239
|
+
);
|
|
240
|
+
expect(partition.main).toHaveLength(0);
|
|
241
|
+
expect(partition.collectionByGroup.size).toBe(0);
|
|
242
|
+
expect(
|
|
243
|
+
investigationEvidenceStepIdsByTurn(
|
|
244
|
+
projection,
|
|
245
|
+
new Set(partition.collectionByGroup.keys()),
|
|
246
|
+
).size,
|
|
247
|
+
).toBe(0);
|
|
248
|
+
expect(
|
|
249
|
+
investigationEvidenceRevealCollection(
|
|
250
|
+
projection,
|
|
251
|
+
projection.sources[0].id,
|
|
252
|
+
partition,
|
|
253
|
+
),
|
|
254
|
+
).toBeUndefined();
|
|
255
|
+
const html = render(projection, false, undefined, resolution);
|
|
256
|
+
expect(html).not.toContain("<article");
|
|
257
|
+
expect(html).not.toContain("other/");
|
|
258
|
+
expect(html).toContain("No relevant evidence to show yet");
|
|
259
|
+
expect(
|
|
260
|
+
html.split(
|
|
261
|
+
`id="${investigationEvidenceSourceDomId(projection.sources[0].id)}"`,
|
|
262
|
+
),
|
|
263
|
+
).toHaveLength(1);
|
|
264
|
+
projection.groups.forEach((group) => {
|
|
265
|
+
group.historical = true;
|
|
266
|
+
});
|
|
267
|
+
const history = partitionInvestigationEvidence(
|
|
268
|
+
projection.groups,
|
|
269
|
+
resolution,
|
|
270
|
+
);
|
|
271
|
+
expect(history.main).toHaveLength(0);
|
|
272
|
+
expect(history.collectionByGroup.size).toBe(0);
|
|
273
|
+
expect(history.earlier).toHaveLength(0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("keeps a selected focused fact but leaves unselected broader resources in Activity", () => {
|
|
277
|
+
const ref = evidenceRef("a", "b");
|
|
278
|
+
const projection = project(
|
|
279
|
+
tool(
|
|
280
|
+
"config",
|
|
281
|
+
"get_resource",
|
|
282
|
+
{
|
|
283
|
+
resource: {
|
|
284
|
+
apiVersion: "v1",
|
|
285
|
+
kind: "ConfigMap",
|
|
286
|
+
metadata: { namespace: "shop", name: "api" },
|
|
287
|
+
data: { HOST: "mongo" },
|
|
288
|
+
},
|
|
289
|
+
recentChanges: [],
|
|
290
|
+
recentChangesSaturated: false,
|
|
291
|
+
recentChangesCoverageLimited: false,
|
|
292
|
+
},
|
|
293
|
+
{ evidenceRef: ref },
|
|
294
|
+
),
|
|
295
|
+
tool("other", "get_resource", {
|
|
296
|
+
kind: "Secret",
|
|
297
|
+
namespace: "shop",
|
|
298
|
+
name: "api",
|
|
299
|
+
keys: ["PASSWORD"],
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
303
|
+
projection,
|
|
304
|
+
{ status: "linked", refs: [ref] },
|
|
305
|
+
0,
|
|
306
|
+
);
|
|
307
|
+
const partition = partitionInvestigationEvidence(
|
|
308
|
+
projection.groups,
|
|
309
|
+
resolution,
|
|
310
|
+
);
|
|
311
|
+
expect(partition.main.map((group) => group.id)).toEqual([
|
|
312
|
+
resolution.links[0].originalGroupId,
|
|
313
|
+
]);
|
|
314
|
+
expect(partition.main[0].latest.tone).toBe("neutral");
|
|
315
|
+
expect(partition.workload).toHaveLength(0);
|
|
316
|
+
expect(partition.collectionByGroup.size).toBe(1);
|
|
317
|
+
expect(projection.groups).toHaveLength(3);
|
|
318
|
+
expect(render(projection, false, undefined, resolution)).not.toContain(
|
|
319
|
+
'aria-expanded="true"',
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("does not present collection timestamps as changed evidence", () => {
|
|
324
|
+
const ref = evidenceRef("a", "b");
|
|
325
|
+
const projection = project(
|
|
326
|
+
tool(
|
|
327
|
+
"first",
|
|
328
|
+
"issues",
|
|
329
|
+
{
|
|
330
|
+
issues: [{ ...criticalIssue, last_seen: "2026-09-07T08:00:00Z" }],
|
|
331
|
+
total: 1,
|
|
332
|
+
total_matched: 1,
|
|
333
|
+
},
|
|
334
|
+
{ evidenceRef: ref },
|
|
335
|
+
),
|
|
336
|
+
tool("again", "issues", {
|
|
337
|
+
issues: [{ ...criticalIssue, last_seen: "2026-09-07T08:05:00Z" }],
|
|
338
|
+
total: 1,
|
|
339
|
+
total_matched: 1,
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
343
|
+
projection,
|
|
344
|
+
{ status: "linked", refs: [ref] },
|
|
345
|
+
0,
|
|
346
|
+
);
|
|
347
|
+
const html = render(projection, false, undefined, resolution);
|
|
348
|
+
expect(html.match(/aria-label="CrashLoopBackOff evidence"/g)).toHaveLength(
|
|
349
|
+
1,
|
|
350
|
+
);
|
|
351
|
+
expect(html).not.toContain("Previous observations");
|
|
352
|
+
expect(projection.groups[0].observations).toHaveLength(2);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("keeps different details behind a collapsed history even when summaries match", () => {
|
|
356
|
+
const projection = project(
|
|
357
|
+
tool("first", "issues", {
|
|
358
|
+
issues: [
|
|
359
|
+
{
|
|
360
|
+
...criticalIssue,
|
|
361
|
+
cause: "The container failed",
|
|
362
|
+
message: "Exit code 1",
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
total: 1,
|
|
366
|
+
total_matched: 1,
|
|
367
|
+
}),
|
|
368
|
+
tool("again", "issues", {
|
|
369
|
+
issues: [
|
|
370
|
+
{
|
|
371
|
+
...criticalIssue,
|
|
372
|
+
cause: "The container failed",
|
|
373
|
+
message: "Exit code 2",
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
total: 1,
|
|
377
|
+
total_matched: 1,
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
const html = render(projection);
|
|
381
|
+
expect(html).toContain("Previous observations · 1");
|
|
382
|
+
expect(html).toContain("Exit code 1");
|
|
383
|
+
expect(html).toContain("Exit code 2");
|
|
384
|
+
expect(html).not.toContain("Changed since the previous observation");
|
|
385
|
+
expect(
|
|
386
|
+
investigationEvidenceShouldRevealHistory(
|
|
387
|
+
projection.groups[0],
|
|
388
|
+
projection.sources[0].id,
|
|
389
|
+
),
|
|
390
|
+
).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("folds unchanged uncited rechecks into the cited fact with every source accessible", () => {
|
|
394
|
+
const ref = evidenceRef("a", "b");
|
|
395
|
+
const projection = project(
|
|
396
|
+
tool(
|
|
397
|
+
"cited",
|
|
398
|
+
"issues",
|
|
399
|
+
{ issues: [criticalIssue], total: 1, total_matched: 1 },
|
|
400
|
+
{ evidenceRef: ref },
|
|
401
|
+
),
|
|
402
|
+
tool("recheck", "issues", {
|
|
403
|
+
issues: [criticalIssue],
|
|
404
|
+
total: 1,
|
|
405
|
+
total_matched: 1,
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
409
|
+
projection,
|
|
410
|
+
{ status: "linked", refs: [ref] },
|
|
411
|
+
0,
|
|
412
|
+
);
|
|
413
|
+
const group = projection.groups.find(
|
|
414
|
+
(item) => item.id === resolution.links[0].originalGroupId,
|
|
415
|
+
)!;
|
|
416
|
+
expect(resolution.links[0].source.stepId).toBe("cited");
|
|
417
|
+
expect(group.observations.map((item) => item.source.stepId)).toEqual([
|
|
418
|
+
"cited",
|
|
419
|
+
"recheck",
|
|
420
|
+
]);
|
|
421
|
+
const html = render(projection, false, undefined, resolution);
|
|
422
|
+
expect(html.match(/aria-label="CrashLoopBackOff evidence"/g)).toHaveLength(
|
|
423
|
+
1,
|
|
424
|
+
);
|
|
425
|
+
expect(html).not.toContain("Previous observations");
|
|
426
|
+
expect(html).not.toContain("Changed since the previous observation");
|
|
427
|
+
expect(html).not.toContain("Critical evidence");
|
|
428
|
+
for (const source of projection.sources) {
|
|
429
|
+
expect(
|
|
430
|
+
html.split(`id="${investigationEvidenceSourceDomId(source.id)}"`),
|
|
431
|
+
).toHaveLength(2);
|
|
432
|
+
}
|
|
433
|
+
expect(
|
|
434
|
+
investigationEvidenceShouldRevealHistory(group, projection.sources[1].id),
|
|
435
|
+
).toBe(false);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("does not fold a changed intervening observation into an unchanged cited snapshot", () => {
|
|
439
|
+
const ref = evidenceRef("a", "b");
|
|
440
|
+
const projection = project(
|
|
441
|
+
tool(
|
|
442
|
+
"cited",
|
|
443
|
+
"issues",
|
|
444
|
+
{ issues: [criticalIssue], total: 1, total_matched: 1 },
|
|
445
|
+
{ evidenceRef: ref },
|
|
446
|
+
),
|
|
447
|
+
tool("changed", "issues", {
|
|
448
|
+
issues: [{ ...criticalIssue, message: "A different failure detail" }],
|
|
449
|
+
total: 1,
|
|
450
|
+
total_matched: 1,
|
|
451
|
+
}),
|
|
452
|
+
tool("again", "issues", {
|
|
453
|
+
issues: [criticalIssue],
|
|
454
|
+
total: 1,
|
|
455
|
+
total_matched: 1,
|
|
456
|
+
}),
|
|
457
|
+
);
|
|
458
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
459
|
+
projection,
|
|
460
|
+
{ status: "linked", refs: [ref] },
|
|
461
|
+
0,
|
|
462
|
+
);
|
|
463
|
+
expect(projection.groups[0].observations).toHaveLength(3);
|
|
464
|
+
const html = render(projection, false, undefined, resolution);
|
|
465
|
+
expect(html).not.toContain("Observed after the assessment’s source");
|
|
466
|
+
expect(html).toContain("A different failure detail");
|
|
467
|
+
for (const source of projection.sources) {
|
|
468
|
+
expect(
|
|
469
|
+
html.split(`id="${investigationEvidenceSourceDomId(source.id)}"`),
|
|
470
|
+
).toHaveLength(2);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
it("keeps detector guidance in the evidence data, not the Findings card or its disclosure", () => {
|
|
474
|
+
const action = "Restore configuration or mark the reference optional.";
|
|
475
|
+
const withGuidance = project(
|
|
476
|
+
tool("issues-1", "issues", {
|
|
477
|
+
issues: [{ ...criticalIssue, action }],
|
|
478
|
+
total: 1,
|
|
479
|
+
total_matched: 1,
|
|
480
|
+
}),
|
|
481
|
+
);
|
|
482
|
+
const withoutGuidance = project(
|
|
483
|
+
tool("issues-1", "issues", {
|
|
484
|
+
issues: [criticalIssue],
|
|
485
|
+
total: 1,
|
|
486
|
+
total_matched: 1,
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
expect(render(withGuidance)).toEqual(render(withoutGuidance));
|
|
490
|
+
expect(render(withGuidance)).not.toContain("Suggested check");
|
|
491
|
+
const issue = withGuidance.groups.find((group) => group.kind === "issue")!;
|
|
492
|
+
expect(issue.chronologicalLatest.data).toMatchObject({
|
|
493
|
+
type: "issue",
|
|
494
|
+
issue: { action },
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("shows a small Secret key set directly, without redundant expansion or metadata", () => {
|
|
499
|
+
const ref = evidenceRef("a", "b");
|
|
500
|
+
const projection = project(
|
|
501
|
+
tool(
|
|
502
|
+
"secret-keys",
|
|
503
|
+
"get_resource",
|
|
504
|
+
{
|
|
505
|
+
kind: "Secret",
|
|
506
|
+
namespace: "dev",
|
|
507
|
+
name: "skyhook-agent",
|
|
508
|
+
type: "Opaque",
|
|
509
|
+
keys: ["QUALIFIRE_API_KEY"],
|
|
510
|
+
},
|
|
511
|
+
{ evidenceRef: ref },
|
|
512
|
+
),
|
|
513
|
+
);
|
|
514
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
515
|
+
projection,
|
|
516
|
+
{ status: "linked", refs: [ref] },
|
|
517
|
+
0,
|
|
518
|
+
);
|
|
519
|
+
const html = render(projection, false, undefined, resolution);
|
|
520
|
+
expect(html).toContain("Keys: QUALIFIRE_API_KEY");
|
|
521
|
+
expect(html.match(/QUALIFIRE_API_KEY/g)).toHaveLength(1);
|
|
522
|
+
expect(html).not.toContain("Opaque");
|
|
523
|
+
expect(html).not.toContain("values hidden");
|
|
524
|
+
expect(html).not.toContain("Secret values are never shown");
|
|
525
|
+
expect(html).not.toContain("aria-expanded=");
|
|
526
|
+
expect(html).toContain(
|
|
527
|
+
'aria-label="View source for Secret dev/skyhook-agent"',
|
|
528
|
+
);
|
|
529
|
+
expect(html).not.toContain("Relationship to target not established");
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("preserves already-plural resource kinds in inventory titles", () => {
|
|
533
|
+
const projection = project(
|
|
534
|
+
tool("endpoints", "list_resources", [
|
|
535
|
+
{ kind: "Endpoints", namespace: "shop", name: "api" },
|
|
536
|
+
]),
|
|
537
|
+
);
|
|
538
|
+
expect(projection.groups[0].latest.title).toContain("Endpoints");
|
|
539
|
+
expect(projection.groups[0].latest.title).not.toContain("Endpointses");
|
|
540
|
+
expect(render(projection)).not.toContain("<article");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("keeps even cited inventories in Activity rather than promoting the whole list", () => {
|
|
544
|
+
const ref = evidenceRef("a", "b");
|
|
545
|
+
const projection = project(
|
|
546
|
+
tool(
|
|
547
|
+
"secrets",
|
|
548
|
+
"list_resources",
|
|
549
|
+
[{ kind: "Secret", namespace: "autopush", name: "app" }],
|
|
550
|
+
{
|
|
551
|
+
summary: JSON.stringify({
|
|
552
|
+
kind: "secrets",
|
|
553
|
+
namespace: "autopush",
|
|
554
|
+
}),
|
|
555
|
+
evidenceRef: ref,
|
|
556
|
+
},
|
|
557
|
+
),
|
|
558
|
+
);
|
|
559
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
560
|
+
projection,
|
|
561
|
+
{ status: "linked", refs: [ref] },
|
|
562
|
+
0,
|
|
563
|
+
);
|
|
564
|
+
const html = render(projection, false, undefined, resolution);
|
|
565
|
+
expect(projection.groups[0].latest.title).toContain("Secrets in autopush");
|
|
566
|
+
expect(html).not.toContain("Secrets in autopush");
|
|
567
|
+
const sources = renderToStaticMarkup(
|
|
568
|
+
<AssessmentSources resolution={resolution} onViewSource={onViewSource} />,
|
|
569
|
+
);
|
|
570
|
+
expect(sources).toContain("List Resources");
|
|
571
|
+
expect(html).not.toContain("Resource inventory");
|
|
572
|
+
expect(html).not.toContain("found in a broader search");
|
|
573
|
+
expect(html).not.toContain('aria-expanded="true"');
|
|
574
|
+
expect(html).not.toContain("<article");
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("preserves the actual query under collapsed cited sources and puts actions after evidence", () => {
|
|
578
|
+
const ref = evidenceRef("a", "b");
|
|
579
|
+
const projection = project(
|
|
580
|
+
tool(
|
|
581
|
+
"search",
|
|
582
|
+
"search",
|
|
583
|
+
{ hits: [] },
|
|
584
|
+
{
|
|
585
|
+
summary: JSON.stringify({
|
|
586
|
+
query: "kind:Secret project-infra",
|
|
587
|
+
limit: 20,
|
|
588
|
+
}),
|
|
589
|
+
evidenceRef: ref,
|
|
590
|
+
},
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
594
|
+
projection,
|
|
595
|
+
{ status: "linked", refs: [ref] },
|
|
596
|
+
0,
|
|
597
|
+
);
|
|
598
|
+
const html = render(projection, false, "NEXT-STEPS-SENTINEL", resolution);
|
|
599
|
+
const sources = renderToStaticMarkup(
|
|
600
|
+
<AssessmentSources resolution={resolution} onViewSource={onViewSource} />,
|
|
601
|
+
);
|
|
602
|
+
expect(sources).toContain("kind:Secret project-infra");
|
|
603
|
+
expect(sources).toContain("Sources used for this assessment");
|
|
604
|
+
expect(sources).not.toContain('id="investigation-evidence-');
|
|
605
|
+
expect(html).not.toContain("Cited sources in Activity");
|
|
606
|
+
expect(html).not.toContain(
|
|
607
|
+
"The assessment cites a result that could not be summarized",
|
|
608
|
+
);
|
|
609
|
+
expect(html).not.toContain('aria-controls="investigation-cited-sources"');
|
|
610
|
+
expect(html.indexOf("NEXT-STEPS-SENTINEL")).toBeGreaterThan(
|
|
611
|
+
html.lastIndexOf("</section>"),
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("fills supporting-evidence rows instead of leaving orphan half-width cards", () => {
|
|
616
|
+
expect(
|
|
617
|
+
investigationEvidenceFullRowFlags(["resource", "events", "changes"]),
|
|
618
|
+
).toEqual([true, true, true]);
|
|
619
|
+
expect(
|
|
620
|
+
investigationEvidenceFullRowFlags([
|
|
621
|
+
"resource",
|
|
622
|
+
"changes",
|
|
623
|
+
"logs",
|
|
624
|
+
"resource",
|
|
625
|
+
"inventory",
|
|
626
|
+
]),
|
|
627
|
+
).toEqual([false, false, true, false, false]);
|
|
628
|
+
expect(
|
|
629
|
+
investigationEvidenceFullRowFlags(["resource", "changes", "resource"]),
|
|
630
|
+
).toEqual([false, false, true]);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("promotes cited evidence ahead of other Radar observations without exposing tool-result wrappers", () => {
|
|
634
|
+
const ref = evidenceRef("a", "b");
|
|
635
|
+
const projection = project(
|
|
636
|
+
tool(
|
|
637
|
+
"diagnose-cited",
|
|
638
|
+
"diagnose",
|
|
639
|
+
{
|
|
640
|
+
resource: {
|
|
641
|
+
apiVersion: "apps/v1",
|
|
642
|
+
kind: "Deployment",
|
|
643
|
+
metadata: { namespace: "shop", name: "api" },
|
|
644
|
+
},
|
|
645
|
+
resourceContext: { tier: "basic" },
|
|
646
|
+
relatedIssues: [criticalIssue],
|
|
647
|
+
},
|
|
648
|
+
{ evidenceRef: ref },
|
|
649
|
+
),
|
|
650
|
+
tool(
|
|
651
|
+
"events-other",
|
|
652
|
+
"get_events",
|
|
653
|
+
{
|
|
654
|
+
events: [
|
|
655
|
+
{
|
|
656
|
+
type: "Warning",
|
|
657
|
+
reason: "BackOff",
|
|
658
|
+
message: "Back-off restarting failed container",
|
|
659
|
+
count: 1,
|
|
660
|
+
lastTimestamp: "2026-09-02T10:00:00Z",
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
summary: JSON.stringify({
|
|
666
|
+
kind: "Deployment",
|
|
667
|
+
namespace: "shop",
|
|
668
|
+
name: "api",
|
|
669
|
+
}),
|
|
670
|
+
},
|
|
671
|
+
),
|
|
672
|
+
);
|
|
673
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
674
|
+
projection,
|
|
675
|
+
{ status: "linked", refs: [ref] },
|
|
676
|
+
0,
|
|
677
|
+
);
|
|
678
|
+
const originalGroupId = resolution.links[0].originalGroupId!;
|
|
679
|
+
const before = render(projection);
|
|
680
|
+
const html = render(projection, false, undefined, resolution);
|
|
681
|
+
const anchor = `id="${investigationEvidenceSourceDomId(
|
|
682
|
+
resolution.links[0].source.id,
|
|
683
|
+
)}"`;
|
|
684
|
+
|
|
685
|
+
expect(html).not.toContain("Cited by the agent");
|
|
686
|
+
expect(html).not.toContain("validated against this run");
|
|
687
|
+
expect(html).not.toContain("Agent-selected check");
|
|
688
|
+
expect(html).not.toContain("from this check below");
|
|
689
|
+
expect(html).not.toContain("Additional Radar observations");
|
|
690
|
+
expect(html.indexOf("CrashLoopBackOff")).toBeLessThan(
|
|
691
|
+
html.indexOf("Kubernetes events"),
|
|
692
|
+
);
|
|
693
|
+
expect(html.match(new RegExp(anchor, "g"))).toHaveLength(1);
|
|
694
|
+
expect(html).toContain('aria-label="CrashLoopBackOff evidence"');
|
|
695
|
+
// The terminal assessment moves an existing card without adding an
|
|
696
|
+
// evidence revision. Keep its old DOM id so the workspace's layout anchor
|
|
697
|
+
// can compensate the relocation instead of jumping the reader's scroll.
|
|
698
|
+
expect(before).toContain(`id="${originalGroupId}"`);
|
|
699
|
+
expect(html.match(new RegExp(`id="${originalGroupId}"`, "g"))).toHaveLength(
|
|
700
|
+
1,
|
|
701
|
+
);
|
|
702
|
+
expect(html).not.toContain("animate-transcript-enter");
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("keeps uncited revisions visible when cited evidence shares their semantic card", () => {
|
|
706
|
+
const ref = evidenceRef("a", "b");
|
|
707
|
+
const citedMessage = "The first check saw one crashing replica.";
|
|
708
|
+
const uncitedMessage = "A later uncited check saw every replica crashing.";
|
|
709
|
+
const projection = project(
|
|
710
|
+
tool(
|
|
711
|
+
"issues-cited",
|
|
712
|
+
"issues",
|
|
713
|
+
{
|
|
714
|
+
issues: [{ ...criticalIssue, message: citedMessage }],
|
|
715
|
+
total: 1,
|
|
716
|
+
total_matched: 1,
|
|
717
|
+
},
|
|
718
|
+
{ evidenceRef: ref },
|
|
719
|
+
),
|
|
720
|
+
tool("issues-uncited", "issues", {
|
|
721
|
+
issues: [{ ...criticalIssue, message: uncitedMessage }],
|
|
722
|
+
total: 1,
|
|
723
|
+
total_matched: 1,
|
|
724
|
+
}),
|
|
725
|
+
);
|
|
726
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
727
|
+
projection,
|
|
728
|
+
{ status: "linked", refs: [ref] },
|
|
729
|
+
0,
|
|
730
|
+
);
|
|
731
|
+
const html = render(projection, false, undefined, resolution);
|
|
732
|
+
const citedSource = projection.sources.find(
|
|
733
|
+
(source) => source.stepId === "issues-cited",
|
|
734
|
+
)!;
|
|
735
|
+
const uncitedSource = projection.sources.find(
|
|
736
|
+
(source) => source.stepId === "issues-uncited",
|
|
737
|
+
)!;
|
|
738
|
+
|
|
739
|
+
expect(projection.groups).toHaveLength(1);
|
|
740
|
+
expect(html).toContain(citedMessage);
|
|
741
|
+
expect(html).toContain(uncitedMessage);
|
|
742
|
+
expect(html.indexOf(uncitedMessage)).toBeLessThan(
|
|
743
|
+
html.indexOf(citedMessage),
|
|
744
|
+
);
|
|
745
|
+
expect(html).toContain("Used for assessment");
|
|
746
|
+
expect(
|
|
747
|
+
html.match(new RegExp(`id="${projection.groups[0].id}"`, "g")),
|
|
748
|
+
).toHaveLength(1);
|
|
749
|
+
for (const source of [citedSource, uncitedSource]) {
|
|
750
|
+
expect(
|
|
751
|
+
html.match(
|
|
752
|
+
new RegExp(
|
|
753
|
+
`id="${investigationEvidenceSourceDomId(source.id)}"`,
|
|
754
|
+
"g",
|
|
755
|
+
),
|
|
756
|
+
),
|
|
757
|
+
).toHaveLength(1);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("keeps an unadapted cited result in Activity instead of presenting it as evidence", () => {
|
|
762
|
+
const ref = evidenceRef("a", "b");
|
|
763
|
+
const projection = project(
|
|
764
|
+
tool(
|
|
765
|
+
"metrics-cited",
|
|
766
|
+
"query_prometheus",
|
|
767
|
+
{ result: [1] },
|
|
768
|
+
{ evidenceRef: ref },
|
|
769
|
+
),
|
|
770
|
+
);
|
|
771
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
772
|
+
projection,
|
|
773
|
+
{ status: "linked", refs: [ref] },
|
|
774
|
+
0,
|
|
775
|
+
);
|
|
776
|
+
const html = render(projection, false, undefined, resolution);
|
|
777
|
+
|
|
778
|
+
const sources = renderToStaticMarkup(
|
|
779
|
+
<AssessmentSources resolution={resolution} onViewSource={onViewSource} />,
|
|
780
|
+
);
|
|
781
|
+
expect(sources).toContain("Query Prometheus");
|
|
782
|
+
expect(sources).toContain("Sources used for this assessment");
|
|
783
|
+
expect(html).not.toContain("Cited sources in Activity");
|
|
784
|
+
expect(html).not.toContain("Agent-selected check");
|
|
785
|
+
expect(html).not.toContain("View source");
|
|
786
|
+
expect(html).toContain(">Evidence</h2>");
|
|
787
|
+
expect(html).not.toContain("Evidence from cited results");
|
|
788
|
+
expect(html).not.toContain("Additional Radar observations");
|
|
789
|
+
expect(html).toContain("No relevant evidence to show yet");
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it("does not frame unrelated structured evidence as other when the cited source is Activity-only", () => {
|
|
793
|
+
const ref = evidenceRef("a", "c");
|
|
794
|
+
const projection = project(
|
|
795
|
+
tool(
|
|
796
|
+
"search-cited",
|
|
797
|
+
"search",
|
|
798
|
+
{ results: [{ kind: "Pod", namespace: "shop", name: "api-123" }] },
|
|
799
|
+
{ evidenceRef: ref },
|
|
800
|
+
),
|
|
801
|
+
tool(
|
|
802
|
+
"events-uncited",
|
|
803
|
+
"get_events",
|
|
804
|
+
{
|
|
805
|
+
events: [
|
|
806
|
+
{
|
|
807
|
+
type: "Warning",
|
|
808
|
+
reason: "BackOff",
|
|
809
|
+
message: "Back-off restarting failed container",
|
|
810
|
+
count: 1,
|
|
811
|
+
lastTimestamp: "2026-09-02T10:00:00Z",
|
|
812
|
+
},
|
|
813
|
+
],
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
summary: JSON.stringify({
|
|
817
|
+
kind: "Deployment",
|
|
818
|
+
namespace: "shop",
|
|
819
|
+
name: "api",
|
|
820
|
+
}),
|
|
821
|
+
},
|
|
822
|
+
),
|
|
823
|
+
);
|
|
824
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
825
|
+
projection,
|
|
826
|
+
{ status: "linked", refs: [ref] },
|
|
827
|
+
0,
|
|
828
|
+
);
|
|
829
|
+
const html = render(projection, false, undefined, resolution);
|
|
830
|
+
|
|
831
|
+
expect(resolution.links[0].originalGroupId).toBeUndefined();
|
|
832
|
+
expect(html).toContain(">Evidence</h2>");
|
|
833
|
+
expect(html).toContain("Kubernetes events");
|
|
834
|
+
expect(html).not.toContain("Evidence from cited results");
|
|
835
|
+
expect(html).not.toContain("Additional Radar observations");
|
|
836
|
+
expect(html).not.toContain("No relevant evidence to show yet");
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it("gives a promoted fallback check one source anchor while retaining its coverage limit", () => {
|
|
840
|
+
const ref = evidenceRef("a", "b");
|
|
841
|
+
const projection = project(
|
|
842
|
+
tool(
|
|
843
|
+
"resource-invalid-cited",
|
|
844
|
+
"get_resource",
|
|
845
|
+
{ unexpected: true },
|
|
846
|
+
{ evidenceRef: ref },
|
|
847
|
+
),
|
|
848
|
+
);
|
|
849
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
850
|
+
projection,
|
|
851
|
+
{ status: "linked", refs: [ref] },
|
|
852
|
+
0,
|
|
853
|
+
);
|
|
854
|
+
const source = resolution.links[0].source;
|
|
855
|
+
const html = render(projection, false, undefined, resolution);
|
|
856
|
+
const anchor = `id="${investigationEvidenceSourceDomId(source.id)}"`;
|
|
857
|
+
|
|
858
|
+
expect(resolution.links[0].originalGroupId).toBeUndefined();
|
|
859
|
+
expect(html).toContain("Resource details");
|
|
860
|
+
expect(html).not.toContain("Agent-selected check");
|
|
861
|
+
expect(
|
|
862
|
+
renderToStaticMarkup(
|
|
863
|
+
<AssessmentSources
|
|
864
|
+
resolution={resolution}
|
|
865
|
+
onViewSource={onViewSource}
|
|
866
|
+
/>,
|
|
867
|
+
),
|
|
868
|
+
).toContain("View Get Resource source used for this assessment");
|
|
869
|
+
expect(html).toContain("Evidence coverage is incomplete");
|
|
870
|
+
expect(html).toContain("couldn't summarize this investigation step");
|
|
871
|
+
expect(html.match(new RegExp(anchor, "g"))).toHaveLength(1);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("keeps inline log evidence compact and strips terminal color codes", () => {
|
|
875
|
+
const ref = evidenceRef("a", "b");
|
|
876
|
+
const lines = Array.from(
|
|
877
|
+
{ length: VISIBLE_LOG_EVIDENCE_LINES + 3 },
|
|
878
|
+
(_, index) =>
|
|
879
|
+
`\u001b[31mentry-${String(index + 1).padStart(2, "0")}\u001b[0m`,
|
|
880
|
+
);
|
|
881
|
+
const projection = project(
|
|
882
|
+
tool(
|
|
883
|
+
"logs-compact",
|
|
884
|
+
"get_pod_logs",
|
|
885
|
+
{
|
|
886
|
+
lines,
|
|
887
|
+
totalLines: lines.length,
|
|
888
|
+
matchedLines: lines.length,
|
|
889
|
+
fallback: false,
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
evidenceRef: ref,
|
|
893
|
+
summary: JSON.stringify({
|
|
894
|
+
namespace: "shop",
|
|
895
|
+
name: "api-pod",
|
|
896
|
+
container: "api",
|
|
897
|
+
}),
|
|
898
|
+
},
|
|
899
|
+
),
|
|
900
|
+
);
|
|
901
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
902
|
+
projection,
|
|
903
|
+
{ status: "linked", refs: [ref] },
|
|
904
|
+
0,
|
|
905
|
+
);
|
|
906
|
+
const html = render(projection, false, undefined, resolution);
|
|
907
|
+
|
|
908
|
+
expect(html).toContain(
|
|
909
|
+
`Selected log excerpt · last ${VISIBLE_LOG_EVIDENCE_LINES} of ${lines.length} lines`,
|
|
910
|
+
);
|
|
911
|
+
expect(html).not.toContain("entry-01");
|
|
912
|
+
expect(html).not.toContain("\u001b[31m");
|
|
913
|
+
expect(html).toContain(`entry-${lines.length}`);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it("shows compact honest boundaries for missing or invalid assessment links", () => {
|
|
917
|
+
const projection = project(
|
|
918
|
+
tool("resource-1", "get_resource", {
|
|
919
|
+
apiVersion: "apps/v1",
|
|
920
|
+
kind: "Deployment",
|
|
921
|
+
metadata: { namespace: "shop", name: "api" },
|
|
922
|
+
}),
|
|
923
|
+
);
|
|
924
|
+
const missing = render(projection, false, undefined, {
|
|
925
|
+
status: "missing",
|
|
926
|
+
links: [],
|
|
927
|
+
});
|
|
928
|
+
const invalid = render(projection, false, undefined, {
|
|
929
|
+
status: "invalid",
|
|
930
|
+
links: [],
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
expect(missing).toContain(
|
|
934
|
+
"Assessment does not cite specific Radar evidence",
|
|
935
|
+
);
|
|
936
|
+
expect(invalid).toContain("Assessment references could not be matched");
|
|
937
|
+
expect(invalid).toContain(
|
|
938
|
+
"could not match the assessment’s references to this investigation",
|
|
939
|
+
);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it("offers a quiet Radar link only for a host-wired, unambiguous subject", () => {
|
|
943
|
+
const projection = project(
|
|
944
|
+
tool("issues-nav", "issues", {
|
|
945
|
+
issues: [criticalIssue],
|
|
946
|
+
total: 1,
|
|
947
|
+
total_matched: 1,
|
|
948
|
+
}),
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
expect(render(projection)).not.toContain(
|
|
952
|
+
"Open current Deployment shop/api in Radar",
|
|
953
|
+
);
|
|
954
|
+
expect(render(projection, false, undefined, undefined, () => {})).toContain(
|
|
955
|
+
"Open current Deployment shop/api in Radar",
|
|
956
|
+
);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it("encloses evidence items in cards while keeping replica details unboxed", () => {
|
|
960
|
+
const html = render(
|
|
961
|
+
project(
|
|
962
|
+
tool("workload", "diagnose", {
|
|
963
|
+
resource: {
|
|
964
|
+
apiVersion: "apps/v1",
|
|
965
|
+
kind: "Deployment",
|
|
966
|
+
metadata: { namespace: "shop", name: "api" },
|
|
967
|
+
},
|
|
968
|
+
resourceContext: {
|
|
969
|
+
tier: "basic",
|
|
970
|
+
workloadSummary: {
|
|
971
|
+
replicas: { desired: 1, ready: 0, updated: 1, unavailable: 1 },
|
|
972
|
+
},
|
|
973
|
+
statusSummary: {
|
|
974
|
+
conditions: [
|
|
975
|
+
{
|
|
976
|
+
type: "Available",
|
|
977
|
+
status: "False",
|
|
978
|
+
reason: "MinimumReplicasUnavailable",
|
|
979
|
+
},
|
|
980
|
+
],
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
relatedIssues: [criticalIssue],
|
|
984
|
+
}),
|
|
985
|
+
tool("events", "get_events", {
|
|
986
|
+
events: [
|
|
987
|
+
{
|
|
988
|
+
type: "Warning",
|
|
989
|
+
reason: "BackOff",
|
|
990
|
+
message: "Back-off restarting failed container",
|
|
991
|
+
count: 2,
|
|
992
|
+
lastTimestamp: "2026-09-02T10:00:00Z",
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
}),
|
|
996
|
+
),
|
|
997
|
+
);
|
|
998
|
+
const articles = html.match(/<article\b[^>]*>/g) ?? [];
|
|
999
|
+
expect(articles.length).toBeGreaterThanOrEqual(3);
|
|
1000
|
+
for (const article of articles) {
|
|
1001
|
+
expect(article).toContain("rounded-lg border bg-theme-surface");
|
|
1002
|
+
expect(article).not.toContain("border-b ");
|
|
1003
|
+
}
|
|
1004
|
+
expect(html).toContain("border-l-red-500");
|
|
1005
|
+
expect(html).toContain("border-theme-border/70");
|
|
1006
|
+
const replicaFacts = html.match(
|
|
1007
|
+
/<dl[^>]*>\s*<div><dt[^>]*>Ready replicas[\s\S]*?<\/dl>/,
|
|
1008
|
+
)?.[0];
|
|
1009
|
+
expect(replicaFacts).toBeDefined();
|
|
1010
|
+
expect(replicaFacts).not.toContain("border");
|
|
1011
|
+
expect(replicaFacts).toContain("0/1");
|
|
1012
|
+
expect(replicaFacts).toContain("Updated");
|
|
1013
|
+
expect(replicaFacts).toContain("Unavailable");
|
|
1014
|
+
expect(html).toContain("MinimumReplicasUnavailable");
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it("links exact resources named inside change and DNS evidence", () => {
|
|
1018
|
+
const projection = project(
|
|
1019
|
+
tool("diagnose-related-resources", "diagnose", {
|
|
1020
|
+
resource: {
|
|
1021
|
+
apiVersion: "apps/v1",
|
|
1022
|
+
kind: "Deployment",
|
|
1023
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1024
|
+
},
|
|
1025
|
+
resourceContext: { tier: "basic" },
|
|
1026
|
+
pods: 0,
|
|
1027
|
+
events: [],
|
|
1028
|
+
recentChanges: [
|
|
1029
|
+
{
|
|
1030
|
+
apiVersion: "v1",
|
|
1031
|
+
kind: "ConfigMap",
|
|
1032
|
+
namespace: "shop",
|
|
1033
|
+
name: "api-settings",
|
|
1034
|
+
changeType: "update",
|
|
1035
|
+
timestamp: "2026-09-02T09:55:00Z",
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
kind: "ExternalRecord",
|
|
1039
|
+
namespace: "shop",
|
|
1040
|
+
name: "api.example.test",
|
|
1041
|
+
changeType: "update",
|
|
1042
|
+
timestamp: "2026-09-02T09:56:00Z",
|
|
1043
|
+
},
|
|
1044
|
+
],
|
|
1045
|
+
dnsContext: {
|
|
1046
|
+
coreDNSFindings: [
|
|
1047
|
+
{
|
|
1048
|
+
kind: "ConfigMap",
|
|
1049
|
+
namespace: "kube-system",
|
|
1050
|
+
name: "coredns",
|
|
1051
|
+
severity: "warning",
|
|
1052
|
+
reason: "Suspicious forwarding rule",
|
|
1053
|
+
},
|
|
1054
|
+
],
|
|
1055
|
+
},
|
|
1056
|
+
}),
|
|
1057
|
+
);
|
|
1058
|
+
const html = render(projection, false, undefined, undefined, () => {});
|
|
1059
|
+
|
|
1060
|
+
expect(html).toMatch(/<button[^>]*>shop\/api-settings<\/button>/);
|
|
1061
|
+
expect(html).toMatch(/<button[^>]*>kube-system\/coredns<\/button>/);
|
|
1062
|
+
expect(html).not.toMatch(/<button[^>]*>shop\/api\.example\.test<\/button>/);
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
it("waits for disclosure motion only when reduced motion is not requested", () => {
|
|
1066
|
+
expect(investigationDisclosureSettleDelay(false)).toBe(
|
|
1067
|
+
INVESTIGATION_DISCLOSURE_SETTLE_MS,
|
|
1068
|
+
);
|
|
1069
|
+
expect(INVESTIGATION_DISCLOSURE_SETTLE_MS).toBeGreaterThan(200);
|
|
1070
|
+
expect(investigationDisclosureSettleDelay(true)).toBe(0);
|
|
1071
|
+
|
|
1072
|
+
const html = render(
|
|
1073
|
+
project(
|
|
1074
|
+
tool("resource-motion", "get_resource", {
|
|
1075
|
+
apiVersion: "apps/v1",
|
|
1076
|
+
kind: "Deployment",
|
|
1077
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1078
|
+
}),
|
|
1079
|
+
),
|
|
1080
|
+
);
|
|
1081
|
+
expect(
|
|
1082
|
+
html.match(/motion-reduce:transition-none/g)?.length ?? 0,
|
|
1083
|
+
).toBeGreaterThanOrEqual(2);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it("reveals a newly expanded disclosure without skipping the start of tall content", () => {
|
|
1087
|
+
expect(
|
|
1088
|
+
investigationDisclosureScrollTop({
|
|
1089
|
+
scrollTop: 500,
|
|
1090
|
+
viewportTop: 100,
|
|
1091
|
+
viewportBottom: 1000,
|
|
1092
|
+
disclosureTop: 780,
|
|
1093
|
+
disclosureBottom: 1180,
|
|
1094
|
+
}),
|
|
1095
|
+
).toBe(688);
|
|
1096
|
+
expect(
|
|
1097
|
+
investigationDisclosureScrollTop({
|
|
1098
|
+
scrollTop: 500,
|
|
1099
|
+
viewportTop: 100,
|
|
1100
|
+
viewportBottom: 1000,
|
|
1101
|
+
disclosureTop: 780,
|
|
1102
|
+
disclosureBottom: 1900,
|
|
1103
|
+
}),
|
|
1104
|
+
).toBe(1172);
|
|
1105
|
+
expect(
|
|
1106
|
+
investigationDisclosureScrollTop({
|
|
1107
|
+
scrollTop: 500,
|
|
1108
|
+
viewportTop: 100,
|
|
1109
|
+
viewportBottom: 1000,
|
|
1110
|
+
disclosureTop: 240,
|
|
1111
|
+
disclosureBottom: 900,
|
|
1112
|
+
}),
|
|
1113
|
+
).toBeUndefined();
|
|
1114
|
+
expect(
|
|
1115
|
+
investigationDisclosureScrollTop({
|
|
1116
|
+
scrollTop: 500,
|
|
1117
|
+
viewportTop: 100,
|
|
1118
|
+
viewportBottom: 1000,
|
|
1119
|
+
disclosureTop: 90,
|
|
1120
|
+
disclosureBottom: 500,
|
|
1121
|
+
}),
|
|
1122
|
+
).toBe(482);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it("renders the first producer-classified failure without claiming it was ranked", () => {
|
|
1126
|
+
const projection = project(
|
|
1127
|
+
tool("issues-1", "issues", {
|
|
1128
|
+
issues: [criticalIssue],
|
|
1129
|
+
total: 1,
|
|
1130
|
+
total_matched: 1,
|
|
1131
|
+
}),
|
|
1132
|
+
);
|
|
1133
|
+
const html = render(projection);
|
|
1134
|
+
const source = projection.sources[0];
|
|
1135
|
+
|
|
1136
|
+
expect(html).not.toContain("Critical evidence");
|
|
1137
|
+
expect(html).not.toContain("strongest");
|
|
1138
|
+
expect(html).not.toContain("main proof");
|
|
1139
|
+
expect(html).toContain("CrashLoopBackOff");
|
|
1140
|
+
expect(html).toContain('aria-label="View source for CrashLoopBackOff"');
|
|
1141
|
+
expect(html).toContain(
|
|
1142
|
+
`id="${investigationEvidenceSourceDomId(source.id)}"`,
|
|
1143
|
+
);
|
|
1144
|
+
expect(source.primaryGroupId).toBe(
|
|
1145
|
+
projection.groups.find((group) => group.kind === "issue")?.id,
|
|
1146
|
+
);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it("omits an unmatched broad issue without deleting the raw evidence", () => {
|
|
1150
|
+
const projection = project(
|
|
1151
|
+
tool(
|
|
1152
|
+
"issues-broad",
|
|
1153
|
+
"issues",
|
|
1154
|
+
{
|
|
1155
|
+
issues: [
|
|
1156
|
+
{
|
|
1157
|
+
...criticalIssue,
|
|
1158
|
+
id: "issue-db-crash",
|
|
1159
|
+
name: "db",
|
|
1160
|
+
reason: "DatabaseCrashLoop",
|
|
1161
|
+
},
|
|
1162
|
+
],
|
|
1163
|
+
total: 1,
|
|
1164
|
+
total_matched: 1,
|
|
1165
|
+
},
|
|
1166
|
+
{ summary: JSON.stringify({ namespace: "shop" }) },
|
|
1167
|
+
),
|
|
1168
|
+
);
|
|
1169
|
+
const html = render(projection);
|
|
1170
|
+
|
|
1171
|
+
expect(html).not.toContain("Critical evidence");
|
|
1172
|
+
expect(html).not.toContain("Related resources and broader checks");
|
|
1173
|
+
expect(html).not.toContain("DatabaseCrashLoop");
|
|
1174
|
+
expect(projection.groups).toHaveLength(1);
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it("keeps proof-scope provenance without surfacing identical evidence as history", () => {
|
|
1178
|
+
const podIssue = {
|
|
1179
|
+
...criticalIssue,
|
|
1180
|
+
id: "issue-api-pod-crash",
|
|
1181
|
+
kind: "Pod",
|
|
1182
|
+
group: "",
|
|
1183
|
+
name: "api-abc",
|
|
1184
|
+
};
|
|
1185
|
+
const projection = project(
|
|
1186
|
+
tool("diagnose-target", "diagnose", {
|
|
1187
|
+
resource: {
|
|
1188
|
+
apiVersion: "apps/v1",
|
|
1189
|
+
kind: "Deployment",
|
|
1190
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1191
|
+
},
|
|
1192
|
+
resourceContext: { tier: "basic" },
|
|
1193
|
+
relatedIssues: [podIssue],
|
|
1194
|
+
}),
|
|
1195
|
+
tool("issues-broad", "issues", {
|
|
1196
|
+
issues: [podIssue],
|
|
1197
|
+
total: 1,
|
|
1198
|
+
total_matched: 1,
|
|
1199
|
+
}),
|
|
1200
|
+
);
|
|
1201
|
+
const group = projection.groups.find((item) => item.kind === "issue")!;
|
|
1202
|
+
const html = render(projection);
|
|
1203
|
+
|
|
1204
|
+
expect(group.latest.source.stepId).toBe("diagnose-target");
|
|
1205
|
+
expect(group.chronologicalLatest.source.stepId).toBe("issues-broad");
|
|
1206
|
+
expect(
|
|
1207
|
+
investigationEvidenceShouldRevealHistory(
|
|
1208
|
+
group,
|
|
1209
|
+
group.chronologicalLatest.source.id,
|
|
1210
|
+
),
|
|
1211
|
+
).toBe(false);
|
|
1212
|
+
expect(
|
|
1213
|
+
investigationEvidenceShouldRevealHistory(group, group.latest.source.id),
|
|
1214
|
+
).toBe(false);
|
|
1215
|
+
expect(html).not.toContain("Previous observations");
|
|
1216
|
+
expect(group.latest.relevance).toBe("producer-related");
|
|
1217
|
+
expect(html).not.toContain("Broader context");
|
|
1218
|
+
expect(html).not.toContain("later broader observation retained");
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it.each(["critical", "warning"])(
|
|
1222
|
+
"shows all distinct current %s headers, with bodies collapsed",
|
|
1223
|
+
(severity) => {
|
|
1224
|
+
const projection = project(
|
|
1225
|
+
...Array.from({ length: 7 }, (_, index) =>
|
|
1226
|
+
tool(`failure-${index}`, "issues", {
|
|
1227
|
+
issues: [
|
|
1228
|
+
{
|
|
1229
|
+
...criticalIssue,
|
|
1230
|
+
id: `failure-${index}`,
|
|
1231
|
+
reason: `Failure${index}`,
|
|
1232
|
+
severity,
|
|
1233
|
+
},
|
|
1234
|
+
],
|
|
1235
|
+
total: 1,
|
|
1236
|
+
total_matched: 1,
|
|
1237
|
+
}),
|
|
1238
|
+
),
|
|
1239
|
+
);
|
|
1240
|
+
const partition = partitionInvestigationEvidence(projection.groups);
|
|
1241
|
+
expect(partition.main).toHaveLength(7);
|
|
1242
|
+
expect(partition.workload).toHaveLength(0);
|
|
1243
|
+
const html = render(projection);
|
|
1244
|
+
expect(html.match(/<article\b/g)).toHaveLength(7);
|
|
1245
|
+
expect(html).not.toContain('aria-expanded="true"');
|
|
1246
|
+
expect(html).not.toContain("More critical evidence");
|
|
1247
|
+
for (const source of projection.sources)
|
|
1248
|
+
expect(
|
|
1249
|
+
investigationEvidenceRevealCollection(projection, source.id),
|
|
1250
|
+
).toBeUndefined();
|
|
1251
|
+
},
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
it("keeps neutral supporting observations reachable without an adverse preview", () => {
|
|
1255
|
+
const projection = project(
|
|
1256
|
+
tool("neutral", "issues", {
|
|
1257
|
+
issues: [{ ...criticalIssue, severity: "warning" }],
|
|
1258
|
+
total: 1,
|
|
1259
|
+
total_matched: 1,
|
|
1260
|
+
}),
|
|
1261
|
+
);
|
|
1262
|
+
projection.groups[0].latest.tone = "neutral";
|
|
1263
|
+
const html = render(projection);
|
|
1264
|
+
expect(html).toContain("More evidence about this workload");
|
|
1265
|
+
expect(html).not.toContain("with warnings or errors");
|
|
1266
|
+
expect(
|
|
1267
|
+
investigationEvidenceRevealCollection(
|
|
1268
|
+
projection,
|
|
1269
|
+
projection.sources[0].id,
|
|
1270
|
+
),
|
|
1271
|
+
).toBe("workload");
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it("keeps critical cards and overflow ahead of noncritical cited results", () => {
|
|
1275
|
+
const ref = evidenceRef("a", "b");
|
|
1276
|
+
const projection = project(
|
|
1277
|
+
tool(
|
|
1278
|
+
"cited-warning",
|
|
1279
|
+
"issues",
|
|
1280
|
+
{
|
|
1281
|
+
issues: [
|
|
1282
|
+
{
|
|
1283
|
+
...criticalIssue,
|
|
1284
|
+
id: "warning",
|
|
1285
|
+
severity: "warning",
|
|
1286
|
+
reason: "CitedWarning",
|
|
1287
|
+
},
|
|
1288
|
+
],
|
|
1289
|
+
total: 1,
|
|
1290
|
+
total_matched: 1,
|
|
1291
|
+
},
|
|
1292
|
+
{ evidenceRef: ref },
|
|
1293
|
+
),
|
|
1294
|
+
...Array.from({ length: 4 }, (_, index) =>
|
|
1295
|
+
tool(`critical-${index}`, "issues", {
|
|
1296
|
+
issues: [
|
|
1297
|
+
{
|
|
1298
|
+
...criticalIssue,
|
|
1299
|
+
id: `critical-${index}`,
|
|
1300
|
+
reason: `Critical${index}`,
|
|
1301
|
+
message: `Critical detail ${index}`,
|
|
1302
|
+
cause: "Brief critical summary",
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1305
|
+
total: 1,
|
|
1306
|
+
total_matched: 1,
|
|
1307
|
+
}),
|
|
1308
|
+
),
|
|
1309
|
+
);
|
|
1310
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
1311
|
+
projection,
|
|
1312
|
+
{ status: "linked", refs: [ref] },
|
|
1313
|
+
0,
|
|
1314
|
+
);
|
|
1315
|
+
const html = render(projection, false, undefined, resolution);
|
|
1316
|
+
expect(html).not.toContain("Critical evidence");
|
|
1317
|
+
expect(html).not.toContain("Cited by the agent");
|
|
1318
|
+
expect(html.indexOf('aria-label="Critical3 evidence"')).toBeLessThan(
|
|
1319
|
+
html.indexOf('aria-label="CitedWarning evidence"'),
|
|
1320
|
+
);
|
|
1321
|
+
const firstCard = html.match(
|
|
1322
|
+
/<article[^>]*aria-label="Critical0 evidence"[\s\S]*?<\/article>/,
|
|
1323
|
+
)?.[0];
|
|
1324
|
+
expect(firstCard).toContain('aria-expanded="false"');
|
|
1325
|
+
for (const source of projection.sources) {
|
|
1326
|
+
expect(
|
|
1327
|
+
html.split(`id="${investigationEvidenceSourceDomId(source.id)}"`),
|
|
1328
|
+
).toHaveLength(2);
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
it("orders critical cited cards first without mutating citation order", () => {
|
|
1333
|
+
const refs = [evidenceRef("a", "b"), evidenceRef("a", "c")];
|
|
1334
|
+
const projection = project(
|
|
1335
|
+
...["warning", "critical", "critical"].map((severity, index) =>
|
|
1336
|
+
tool(
|
|
1337
|
+
`ordered-${index}`,
|
|
1338
|
+
"issues",
|
|
1339
|
+
{
|
|
1340
|
+
issues: [
|
|
1341
|
+
{
|
|
1342
|
+
...criticalIssue,
|
|
1343
|
+
id: `ordered-${index}`,
|
|
1344
|
+
reason: `Ordered${index}`,
|
|
1345
|
+
severity,
|
|
1346
|
+
message: `Detail ${index}`,
|
|
1347
|
+
cause: "Brief summary",
|
|
1348
|
+
},
|
|
1349
|
+
],
|
|
1350
|
+
total: 1,
|
|
1351
|
+
total_matched: 1,
|
|
1352
|
+
},
|
|
1353
|
+
index < 2 ? { evidenceRef: refs[index] } : {},
|
|
1354
|
+
),
|
|
1355
|
+
),
|
|
1356
|
+
);
|
|
1357
|
+
const resolution = resolveInvestigationRootCauseEvidence(
|
|
1358
|
+
projection,
|
|
1359
|
+
{ status: "linked", refs },
|
|
1360
|
+
0,
|
|
1361
|
+
);
|
|
1362
|
+
const html = render(projection, false, undefined, resolution);
|
|
1363
|
+
expect(html).not.toContain("Cited by the agent");
|
|
1364
|
+
expect(html.indexOf('aria-label="Ordered2 evidence"')).toBeLessThan(
|
|
1365
|
+
html.indexOf('aria-label="Ordered0 evidence"'),
|
|
1366
|
+
);
|
|
1367
|
+
expect(html.indexOf('aria-label="Ordered1 evidence"')).toBeLessThan(
|
|
1368
|
+
html.indexOf('aria-label="Ordered0 evidence"'),
|
|
1369
|
+
);
|
|
1370
|
+
expect(resolution.links[0].source.stepId).toBe("ordered-0");
|
|
1371
|
+
expect(
|
|
1372
|
+
html.match(
|
|
1373
|
+
/<article[^>]*aria-label="Ordered1 evidence"[\s\S]*?<\/article>/,
|
|
1374
|
+
)?.[0],
|
|
1375
|
+
).toContain('aria-expanded="false"');
|
|
1376
|
+
expect(
|
|
1377
|
+
html.match(
|
|
1378
|
+
/<article[^>]*aria-label="Ordered2 evidence"[\s\S]*?<\/article>/,
|
|
1379
|
+
)?.[0],
|
|
1380
|
+
).toContain('aria-expanded="false"');
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
it.each(["missing", "invalid"] as const)(
|
|
1384
|
+
"keeps %s citation notice ahead of critical facts",
|
|
1385
|
+
(status) => {
|
|
1386
|
+
const projection = project(
|
|
1387
|
+
tool("critical", "issues", {
|
|
1388
|
+
issues: [criticalIssue],
|
|
1389
|
+
total: 1,
|
|
1390
|
+
total_matched: 1,
|
|
1391
|
+
}),
|
|
1392
|
+
);
|
|
1393
|
+
const html = render(projection, false, undefined, { status, links: [] });
|
|
1394
|
+
const notice =
|
|
1395
|
+
status === "missing"
|
|
1396
|
+
? "Assessment does not cite specific Radar evidence"
|
|
1397
|
+
: "Assessment references could not be matched";
|
|
1398
|
+
expect(html.indexOf(notice)).toBeGreaterThan(-1);
|
|
1399
|
+
expect(html.indexOf(notice)).toBeLessThan(
|
|
1400
|
+
html.indexOf("CrashLoopBackOff"),
|
|
1401
|
+
);
|
|
1402
|
+
},
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
it("routes source navigation through collapsed collections", () => {
|
|
1406
|
+
const projection = project(
|
|
1407
|
+
tool("issues-old", "issues", {
|
|
1408
|
+
issues: [criticalIssue],
|
|
1409
|
+
total: 1,
|
|
1410
|
+
total_matched: 1,
|
|
1411
|
+
}),
|
|
1412
|
+
);
|
|
1413
|
+
projection.groups[0].historical = true;
|
|
1414
|
+
const sourceId = projection.sources[0].id;
|
|
1415
|
+
expect(investigationEvidenceRevealCollection(projection, sourceId)).toBe(
|
|
1416
|
+
"earlier",
|
|
1417
|
+
);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
it("routes a fan-out source to its primary Key card before secondary Context", () => {
|
|
1421
|
+
const projection = project(
|
|
1422
|
+
tool("diagnose-fan-out", "diagnose", {
|
|
1423
|
+
resource: {
|
|
1424
|
+
apiVersion: "apps/v1",
|
|
1425
|
+
kind: "Deployment",
|
|
1426
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1427
|
+
},
|
|
1428
|
+
resourceContext: { tier: "basic" },
|
|
1429
|
+
pods: 1,
|
|
1430
|
+
relatedIssues: [criticalIssue],
|
|
1431
|
+
events: [],
|
|
1432
|
+
recentChanges: [],
|
|
1433
|
+
}),
|
|
1434
|
+
);
|
|
1435
|
+
const source = projection.sources[0];
|
|
1436
|
+
const primary = projection.groups.find(
|
|
1437
|
+
(group) => group.id === source.primaryGroupId,
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1440
|
+
expect(primary?.latest.tier).toBe("key");
|
|
1441
|
+
expect(
|
|
1442
|
+
projection.groups.some(
|
|
1443
|
+
(group) =>
|
|
1444
|
+
group.latest.tier === "context" &&
|
|
1445
|
+
group.observations.some(
|
|
1446
|
+
(observation) => observation.source.id === source.id,
|
|
1447
|
+
),
|
|
1448
|
+
),
|
|
1449
|
+
).toBe(true);
|
|
1450
|
+
expect(
|
|
1451
|
+
investigationEvidenceRevealCollection(projection, source.id),
|
|
1452
|
+
).toBeUndefined();
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
it("routes limitation-only sources through coverage", () => {
|
|
1456
|
+
const projection = project(
|
|
1457
|
+
tool(
|
|
1458
|
+
"issues-cut",
|
|
1459
|
+
"issues",
|
|
1460
|
+
{ issues: [criticalIssue], total: 1, total_matched: 1 },
|
|
1461
|
+
{ truncated: true },
|
|
1462
|
+
),
|
|
1463
|
+
);
|
|
1464
|
+
const sourceId = projection.sources[0].id;
|
|
1465
|
+
const html = render(projection);
|
|
1466
|
+
|
|
1467
|
+
expect(investigationEvidenceRevealCollection(projection, sourceId)).toBe(
|
|
1468
|
+
"coverage",
|
|
1469
|
+
);
|
|
1470
|
+
expect(html).toContain(
|
|
1471
|
+
`id="${investigationEvidenceSourceDomId(sourceId)}"`,
|
|
1472
|
+
);
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
it("emits exactly one Evidence anchor when one bundled source fans out into cards", () => {
|
|
1476
|
+
const projection = project(
|
|
1477
|
+
tool("diagnose-1", "diagnose", {
|
|
1478
|
+
resource: {
|
|
1479
|
+
apiVersion: "apps/v1",
|
|
1480
|
+
kind: "Deployment",
|
|
1481
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1482
|
+
},
|
|
1483
|
+
resourceContext: { tier: "basic" },
|
|
1484
|
+
pods: 1,
|
|
1485
|
+
relatedIssues: [criticalIssue],
|
|
1486
|
+
startupBlockers: [
|
|
1487
|
+
{
|
|
1488
|
+
kind: "Pod",
|
|
1489
|
+
name: "api-abc",
|
|
1490
|
+
reason: "ImagePullBackOff",
|
|
1491
|
+
severity: "critical",
|
|
1492
|
+
message: "The image tag does not exist.",
|
|
1493
|
+
},
|
|
1494
|
+
],
|
|
1495
|
+
logsCurrent: [
|
|
1496
|
+
{
|
|
1497
|
+
pod: "api-abc",
|
|
1498
|
+
container: "api",
|
|
1499
|
+
logs: {
|
|
1500
|
+
lines: ["ERROR missing DATABASE_URL"],
|
|
1501
|
+
totalLines: 1,
|
|
1502
|
+
matchedLines: 1,
|
|
1503
|
+
fallback: false,
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
],
|
|
1507
|
+
events: [
|
|
1508
|
+
{
|
|
1509
|
+
reason: "BackOff",
|
|
1510
|
+
message: "Back-off restarting failed container",
|
|
1511
|
+
type: "Warning",
|
|
1512
|
+
count: 3,
|
|
1513
|
+
lastTimestamp: "2026-09-02T10:00:00Z",
|
|
1514
|
+
},
|
|
1515
|
+
],
|
|
1516
|
+
eventsError: "The warning-event result was incomplete.",
|
|
1517
|
+
recentChanges: [],
|
|
1518
|
+
}),
|
|
1519
|
+
);
|
|
1520
|
+
expect(projection.groups.length).toBeGreaterThan(3);
|
|
1521
|
+
|
|
1522
|
+
const html = render(projection);
|
|
1523
|
+
const anchor = `id="${investigationEvidenceSourceDomId(projection.sources[0].id)}"`;
|
|
1524
|
+
expect(html.split(anchor)).toHaveLength(2);
|
|
1525
|
+
expect(
|
|
1526
|
+
projection.groups.filter((group) => group.latest.tier === "key").length,
|
|
1527
|
+
).toBeGreaterThan(1);
|
|
1528
|
+
expect(html).not.toContain('id="investigation-key-evidence-heading"');
|
|
1529
|
+
expect(html).not.toContain('id="investigation-evidence-tier-key"');
|
|
1530
|
+
expect(html.match(/<article\b/g)).toHaveLength(projection.groups.length);
|
|
1531
|
+
expect(html).not.toContain("related to the investigated resource");
|
|
1532
|
+
|
|
1533
|
+
const ordered = render(projection, false, "NEXT_STEP_MARKER");
|
|
1534
|
+
expect(ordered.indexOf("CrashLoopBackOff")).toBeGreaterThan(
|
|
1535
|
+
ordered.indexOf("Evidence coverage is incomplete"),
|
|
1536
|
+
);
|
|
1537
|
+
expect(ordered.indexOf("Evidence coverage is incomplete")).toBeLessThan(
|
|
1538
|
+
ordered.indexOf("NEXT_STEP_MARKER"),
|
|
1539
|
+
);
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
it("deduplicates source anchors when one call records repeated observations in a card", () => {
|
|
1543
|
+
const projection = project(
|
|
1544
|
+
tool("issues-duplicate", "issues", {
|
|
1545
|
+
issues: [criticalIssue],
|
|
1546
|
+
total: 1,
|
|
1547
|
+
total_matched: 1,
|
|
1548
|
+
}),
|
|
1549
|
+
);
|
|
1550
|
+
const group = projection.groups[0];
|
|
1551
|
+
group.observations.push({
|
|
1552
|
+
...group.observations[0],
|
|
1553
|
+
revision: group.observations.length + 1,
|
|
1554
|
+
});
|
|
1555
|
+
const sourceDomId = investigationEvidenceSourceDomId(
|
|
1556
|
+
group.observations[0].source.id,
|
|
1557
|
+
);
|
|
1558
|
+
|
|
1559
|
+
const html = render(projection);
|
|
1560
|
+
expect(html.split(`id="${sourceDomId}"`)).toHaveLength(2);
|
|
1561
|
+
expect(html).not.toContain("confirmed by");
|
|
1562
|
+
expect(html).not.toContain("2 observations");
|
|
1563
|
+
expect(html).not.toContain("Observation history");
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
it("does not expose repeated tool-result counts as corroboration", () => {
|
|
1567
|
+
const projection = project(
|
|
1568
|
+
tool("issues-first", "issues", {
|
|
1569
|
+
issues: [criticalIssue],
|
|
1570
|
+
total: 1,
|
|
1571
|
+
total_matched: 1,
|
|
1572
|
+
}),
|
|
1573
|
+
tool("issues-second", "issues", {
|
|
1574
|
+
issues: [criticalIssue],
|
|
1575
|
+
total: 1,
|
|
1576
|
+
total_matched: 1,
|
|
1577
|
+
}),
|
|
1578
|
+
);
|
|
1579
|
+
|
|
1580
|
+
expect(render(projection)).not.toContain("seen in 2 results");
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
it("deduplicates source anchors in compact Checked receipts", () => {
|
|
1584
|
+
const projection = project(
|
|
1585
|
+
tool("diagnose-checked", "diagnose", {
|
|
1586
|
+
resource: {
|
|
1587
|
+
apiVersion: "apps/v1",
|
|
1588
|
+
kind: "Deployment",
|
|
1589
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1590
|
+
},
|
|
1591
|
+
resourceContext: { tier: "basic" },
|
|
1592
|
+
pods: 0,
|
|
1593
|
+
events: [],
|
|
1594
|
+
recentChanges: [],
|
|
1595
|
+
}),
|
|
1596
|
+
);
|
|
1597
|
+
const group = projection.groups.find((item) => item.kind === "receipt")!;
|
|
1598
|
+
const source = group.observations[0].source;
|
|
1599
|
+
source.primaryGroupId = group.id;
|
|
1600
|
+
group.observations.push({
|
|
1601
|
+
...group.observations[0],
|
|
1602
|
+
revision: group.observations.length + 1,
|
|
1603
|
+
});
|
|
1604
|
+
projection.groups = [group];
|
|
1605
|
+
const sourceDomId = investigationEvidenceSourceDomId(source.id);
|
|
1606
|
+
|
|
1607
|
+
expect(render(projection).split(`id="${sourceDomId}"`)).toHaveLength(2);
|
|
1608
|
+
});
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
describe("InvestigationEvidencePane honest result states", () => {
|
|
1612
|
+
it("renders evidence without details as static content, not a disabled control", () => {
|
|
1613
|
+
const projection = project(
|
|
1614
|
+
tool("resource-static", "get_resource", {
|
|
1615
|
+
apiVersion: "apps/v1",
|
|
1616
|
+
kind: "Deployment",
|
|
1617
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1618
|
+
}),
|
|
1619
|
+
);
|
|
1620
|
+
const resource = projection.groups.find(
|
|
1621
|
+
(group) => group.kind === "resource",
|
|
1622
|
+
);
|
|
1623
|
+
expect(resource).toBeDefined();
|
|
1624
|
+
|
|
1625
|
+
const html = render(projection);
|
|
1626
|
+
expect(html).toContain("Deployment shop/api");
|
|
1627
|
+
expect(html).not.toContain("disabled");
|
|
1628
|
+
expect(html).not.toContain(`${resource!.id}-body`);
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
it("keeps empty specialized resources static unless another real detail exists", () => {
|
|
1632
|
+
const projection = project(
|
|
1633
|
+
tool("config-empty", "get_resource", {
|
|
1634
|
+
apiVersion: "v1",
|
|
1635
|
+
kind: "ConfigMap",
|
|
1636
|
+
metadata: { namespace: "shop", name: "empty-config" },
|
|
1637
|
+
data: {},
|
|
1638
|
+
}),
|
|
1639
|
+
tool("secret-empty", "get_resource", {
|
|
1640
|
+
kind: "Secret",
|
|
1641
|
+
name: "empty-secret",
|
|
1642
|
+
namespace: "shop",
|
|
1643
|
+
type: "Opaque",
|
|
1644
|
+
keys: [],
|
|
1645
|
+
}),
|
|
1646
|
+
tool("config-empty-context", "get_resource", {
|
|
1647
|
+
resource: {
|
|
1648
|
+
apiVersion: "v1",
|
|
1649
|
+
kind: "ConfigMap",
|
|
1650
|
+
metadata: { namespace: "shop", name: "empty-context" },
|
|
1651
|
+
data: {},
|
|
1652
|
+
},
|
|
1653
|
+
resourceContext: {
|
|
1654
|
+
tier: "basic",
|
|
1655
|
+
workloadSummary: { replicas: {} },
|
|
1656
|
+
},
|
|
1657
|
+
}),
|
|
1658
|
+
);
|
|
1659
|
+
const resources = projection.groups.filter(
|
|
1660
|
+
(group) => group.kind === "resource",
|
|
1661
|
+
);
|
|
1662
|
+
// Body-rendering fixture: relationship eligibility is tested separately.
|
|
1663
|
+
resources.forEach((group) => {
|
|
1664
|
+
group.latest.relevance = "producer-related";
|
|
1665
|
+
});
|
|
1666
|
+
const html = render(projection);
|
|
1667
|
+
|
|
1668
|
+
expect(html).toContain("No key names in this result");
|
|
1669
|
+
expect(html).not.toContain("values hidden");
|
|
1670
|
+
expect(html).not.toContain("0/0 replicas ready");
|
|
1671
|
+
for (const resource of resources) {
|
|
1672
|
+
expect(html).not.toContain(`${resource.id}-body`);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
const withWarning = project(
|
|
1676
|
+
tool("config-warning", "get_resource", {
|
|
1677
|
+
resource: {
|
|
1678
|
+
apiVersion: "v1",
|
|
1679
|
+
kind: "ConfigMap",
|
|
1680
|
+
metadata: { namespace: "shop", name: "empty-config" },
|
|
1681
|
+
data: {},
|
|
1682
|
+
},
|
|
1683
|
+
warnings: ["The captured ConfigMap result is incomplete."],
|
|
1684
|
+
}),
|
|
1685
|
+
);
|
|
1686
|
+
const warningResource = withWarning.groups.find(
|
|
1687
|
+
(group) => group.kind === "resource",
|
|
1688
|
+
)!;
|
|
1689
|
+
warningResource.latest.relevance = "producer-related";
|
|
1690
|
+
const warningHtml = render(withWarning);
|
|
1691
|
+
expect(warningHtml).toContain(`${warningResource.id}-body`);
|
|
1692
|
+
expect(warningHtml).toContain(
|
|
1693
|
+
"The captured ConfigMap result is incomplete.",
|
|
1694
|
+
);
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
it("keeps revision history expandable when the latest resource is empty", () => {
|
|
1698
|
+
const projection = project(
|
|
1699
|
+
tool("config-before", "get_resource", {
|
|
1700
|
+
apiVersion: "v1",
|
|
1701
|
+
kind: "ConfigMap",
|
|
1702
|
+
metadata: { namespace: "shop", name: "changing-config" },
|
|
1703
|
+
data: { API_ENDPOINT: "https://api.example.test" },
|
|
1704
|
+
}),
|
|
1705
|
+
tool("config-after", "get_resource", {
|
|
1706
|
+
apiVersion: "v1",
|
|
1707
|
+
kind: "ConfigMap",
|
|
1708
|
+
metadata: { namespace: "shop", name: "changing-config" },
|
|
1709
|
+
data: {},
|
|
1710
|
+
}),
|
|
1711
|
+
);
|
|
1712
|
+
const resource = projection.groups.find(
|
|
1713
|
+
(group) => group.kind === "resource",
|
|
1714
|
+
)!;
|
|
1715
|
+
resource.latest.relevance = "producer-related";
|
|
1716
|
+
const html = render(projection);
|
|
1717
|
+
|
|
1718
|
+
expect(resource.observations).toHaveLength(2);
|
|
1719
|
+
expect(html).toContain(`${resource.id}-body`);
|
|
1720
|
+
expect(html).toContain("Previous observations · 1");
|
|
1721
|
+
expect(html).toMatch(
|
|
1722
|
+
/aria-expanded="false"[^>]*>[^]*?Previous observations/,
|
|
1723
|
+
);
|
|
1724
|
+
expect(html).not.toContain("Changed since the previous observation");
|
|
1725
|
+
expect(html).not.toContain("2 observations");
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
it("renders a confirmed empty check as a compact, non-expandable receipt", () => {
|
|
1729
|
+
const projection = project(
|
|
1730
|
+
tool("diagnose-empty", "diagnose", {
|
|
1731
|
+
resource: {
|
|
1732
|
+
apiVersion: "apps/v1",
|
|
1733
|
+
kind: "Deployment",
|
|
1734
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1735
|
+
},
|
|
1736
|
+
resourceContext: { tier: "basic" },
|
|
1737
|
+
pods: 0,
|
|
1738
|
+
events: [],
|
|
1739
|
+
recentChanges: [],
|
|
1740
|
+
}),
|
|
1741
|
+
);
|
|
1742
|
+
const receipt = projection.groups.find((group) => group.kind === "receipt");
|
|
1743
|
+
expect(receipt).toBeDefined();
|
|
1744
|
+
|
|
1745
|
+
const html = render(projection);
|
|
1746
|
+
expect(html).toContain("More evidence about this workload");
|
|
1747
|
+
expect(html).toContain("No matching warning events");
|
|
1748
|
+
expect(html).not.toContain("What Radar did not find");
|
|
1749
|
+
expect(html).not.toContain(`${receipt!.id}-body`);
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
it("does not offer an expander when the body would only repeat the summary", () => {
|
|
1753
|
+
const issueProjection = project(
|
|
1754
|
+
tool("issues", "issues", {
|
|
1755
|
+
issues: [criticalIssue],
|
|
1756
|
+
total: 1,
|
|
1757
|
+
total_matched: 1,
|
|
1758
|
+
}),
|
|
1759
|
+
);
|
|
1760
|
+
const startupProjection = project(
|
|
1761
|
+
tool("startup", "diagnose", {
|
|
1762
|
+
resource: {
|
|
1763
|
+
apiVersion: "apps/v1",
|
|
1764
|
+
kind: "Deployment",
|
|
1765
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1766
|
+
},
|
|
1767
|
+
resourceContext: { tier: "basic" },
|
|
1768
|
+
startupBlockers: [
|
|
1769
|
+
{
|
|
1770
|
+
kind: "Pod",
|
|
1771
|
+
name: "api-123",
|
|
1772
|
+
reason: "ImagePullBackOff",
|
|
1773
|
+
severity: "critical",
|
|
1774
|
+
message: "The image could not be pulled.",
|
|
1775
|
+
},
|
|
1776
|
+
],
|
|
1777
|
+
events: [],
|
|
1778
|
+
recentChanges: [],
|
|
1779
|
+
}),
|
|
1780
|
+
);
|
|
1781
|
+
const issue = issueProjection.groups.find(
|
|
1782
|
+
(group) => group.kind === "issue",
|
|
1783
|
+
)!;
|
|
1784
|
+
const startup = startupProjection.groups.find(
|
|
1785
|
+
(group) => group.kind === "startup",
|
|
1786
|
+
)!;
|
|
1787
|
+
|
|
1788
|
+
expect(render(issueProjection)).not.toContain(issue.id + "-body");
|
|
1789
|
+
expect(render(startupProjection)).not.toContain(startup.id + "-body");
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
it("renders SealedSecret conditions only once in a resource card", () => {
|
|
1793
|
+
const projection = project(
|
|
1794
|
+
tool("sealed-secret", "get_resource", {
|
|
1795
|
+
resource: {
|
|
1796
|
+
apiVersion: "bitnami.com/v1alpha1",
|
|
1797
|
+
kind: "SealedSecret",
|
|
1798
|
+
metadata: { namespace: "shop", name: "db-password" },
|
|
1799
|
+
spec: { encryptedData: { password: "ciphertext" } },
|
|
1800
|
+
status: {
|
|
1801
|
+
conditions: [
|
|
1802
|
+
{
|
|
1803
|
+
type: "Synced",
|
|
1804
|
+
status: "False",
|
|
1805
|
+
reason: "ControllerError",
|
|
1806
|
+
message: "The key could not be decrypted.",
|
|
1807
|
+
},
|
|
1808
|
+
],
|
|
1809
|
+
},
|
|
1810
|
+
},
|
|
1811
|
+
resourceContext: {
|
|
1812
|
+
tier: "basic",
|
|
1813
|
+
statusSummary: {
|
|
1814
|
+
conditions: [
|
|
1815
|
+
{
|
|
1816
|
+
type: "Synced",
|
|
1817
|
+
status: "False",
|
|
1818
|
+
reason: "ControllerError",
|
|
1819
|
+
message: "The key could not be decrypted.",
|
|
1820
|
+
},
|
|
1821
|
+
],
|
|
1822
|
+
},
|
|
1823
|
+
},
|
|
1824
|
+
}),
|
|
1825
|
+
);
|
|
1826
|
+
projection.groups.forEach((group) => {
|
|
1827
|
+
group.latest.relevance = "producer-related";
|
|
1828
|
+
});
|
|
1829
|
+
const html = render(projection);
|
|
1830
|
+
|
|
1831
|
+
expect(html.match(/ControllerError/g)).toHaveLength(1);
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
it("does not render Checked when the same empty result lacks explicit success", () => {
|
|
1835
|
+
const projection = project(
|
|
1836
|
+
tool("events-old", "get_events", { events: [] }, { isError: undefined }),
|
|
1837
|
+
);
|
|
1838
|
+
const html = render(projection);
|
|
1839
|
+
|
|
1840
|
+
expect(projection.coverage.checked).toBe(0);
|
|
1841
|
+
expect(projection.groups).toHaveLength(0);
|
|
1842
|
+
expect(html).not.toContain('id="investigation-checked-heading"');
|
|
1843
|
+
expect(html).not.toContain("No matching warning events");
|
|
1844
|
+
expect(html).toContain("Evidence coverage is incomplete");
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
it("puts the change age in the header and only raw evidence in the details", () => {
|
|
1848
|
+
const evidence = "generation=2, observedGeneration=2, 2 owned ReplicaSets";
|
|
1849
|
+
const projection = project(
|
|
1850
|
+
tool("rollout", "diagnose", {
|
|
1851
|
+
resource: {
|
|
1852
|
+
apiVersion: "apps/v1",
|
|
1853
|
+
kind: "Deployment",
|
|
1854
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1855
|
+
},
|
|
1856
|
+
changeContext: {
|
|
1857
|
+
changed: true,
|
|
1858
|
+
what: "pod_template",
|
|
1859
|
+
when: "29d",
|
|
1860
|
+
evidence,
|
|
1861
|
+
},
|
|
1862
|
+
}),
|
|
1863
|
+
);
|
|
1864
|
+
const group = projection.groups.find(
|
|
1865
|
+
(item) => item.latest.data.type === "changes",
|
|
1866
|
+
)!;
|
|
1867
|
+
expect(group.latest.summary).toBe(
|
|
1868
|
+
"Pod template changed; newest ReplicaSet created 29d ago",
|
|
1869
|
+
);
|
|
1870
|
+
const html = render(projection);
|
|
1871
|
+
expect(html).toContain(evidence);
|
|
1872
|
+
expect(html).toContain('aria-label="About change evidence"');
|
|
1873
|
+
expect(html).not.toContain("Why this may matter");
|
|
1874
|
+
expect(html).not.toContain("A recent change is context");
|
|
1875
|
+
const body = html.split(`id="${group.id}-body"`)[1]?.split("</article>")[0];
|
|
1876
|
+
expect(body).toBeDefined();
|
|
1877
|
+
expect(body).not.toContain("Pod template changed");
|
|
1878
|
+
expect(body).toContain(evidence);
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
it("does not offer an empty change disclosure when only the summary is available", () => {
|
|
1882
|
+
const projection = project(
|
|
1883
|
+
tool("rollout", "diagnose", {
|
|
1884
|
+
resource: {
|
|
1885
|
+
apiVersion: "apps/v1",
|
|
1886
|
+
kind: "Deployment",
|
|
1887
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1888
|
+
},
|
|
1889
|
+
changeContext: { changed: true, what: "pod_template" },
|
|
1890
|
+
}),
|
|
1891
|
+
);
|
|
1892
|
+
const group = projection.groups.find(
|
|
1893
|
+
(item) => item.latest.data.type === "changes",
|
|
1894
|
+
)!;
|
|
1895
|
+
const html = render(projection);
|
|
1896
|
+
expect(group.latest.summary).toBe("The workload's Pod template changed");
|
|
1897
|
+
expect(html).not.toContain(`aria-controls="${group.id}-body"`);
|
|
1898
|
+
expect(html).toContain('aria-label="About change evidence"');
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
it("presents qualified change history as a neutral note without repeating its details", () => {
|
|
1902
|
+
const projection = project(
|
|
1903
|
+
tool("empty-changes", "get_changes", { changes: [] }),
|
|
1904
|
+
tool(
|
|
1905
|
+
"limited-history",
|
|
1906
|
+
"get_resource",
|
|
1907
|
+
{
|
|
1908
|
+
resource: {
|
|
1909
|
+
apiVersion: "v1",
|
|
1910
|
+
kind: "Secret",
|
|
1911
|
+
metadata: { namespace: "shop", name: "api" },
|
|
1912
|
+
},
|
|
1913
|
+
recentChanges: [],
|
|
1914
|
+
recentChangesSaturated: false,
|
|
1915
|
+
recentChangesCoverageLimited: true,
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
summary: JSON.stringify({
|
|
1919
|
+
kind: "secret",
|
|
1920
|
+
namespace: "shop",
|
|
1921
|
+
name: "api",
|
|
1922
|
+
}),
|
|
1923
|
+
},
|
|
1924
|
+
),
|
|
1925
|
+
);
|
|
1926
|
+
expect(projection.limitations).toHaveLength(2);
|
|
1927
|
+
expect(
|
|
1928
|
+
projection.limitations.every((item) => item.presentation === "history"),
|
|
1929
|
+
).toBe(true);
|
|
1930
|
+
const html = render(projection);
|
|
1931
|
+
expect(html).toContain("Change history is limited");
|
|
1932
|
+
expect(html).not.toContain("Evidence coverage is incomplete");
|
|
1933
|
+
expect(html).not.toContain("access restrictions");
|
|
1934
|
+
const header = html.match(
|
|
1935
|
+
/<button[^>]*aria-controls="investigation-evidence-coverage"[\s\S]*?<\/button>/,
|
|
1936
|
+
)?.[0];
|
|
1937
|
+
expect(header).toBeDefined();
|
|
1938
|
+
expect(header).not.toContain("No changes were returned");
|
|
1939
|
+
expect(header).not.toContain("text-amber");
|
|
1940
|
+
expect(html).toContain("Change history for secret shop/api is incomplete.");
|
|
1941
|
+
expect(investigationEvidenceCoverageLimited(projection)).toBe(true);
|
|
1942
|
+
for (const source of projection.sources) {
|
|
1943
|
+
expect(
|
|
1944
|
+
html.split(`id="${investigationEvidenceSourceDomId(source.id)}"`),
|
|
1945
|
+
).toHaveLength(2);
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
it.each([
|
|
1950
|
+
[
|
|
1951
|
+
"failed read",
|
|
1952
|
+
tool(
|
|
1953
|
+
"failure",
|
|
1954
|
+
"get_pod_logs",
|
|
1955
|
+
{ error: "pods/log is forbidden" },
|
|
1956
|
+
{ isError: true },
|
|
1957
|
+
),
|
|
1958
|
+
],
|
|
1959
|
+
["truncated result", tool("truncated", "issues", {}, { truncated: true })],
|
|
1960
|
+
[
|
|
1961
|
+
"malformed history",
|
|
1962
|
+
tool("malformed", "get_resource", {
|
|
1963
|
+
resource: {
|
|
1964
|
+
apiVersion: "v1",
|
|
1965
|
+
kind: "Secret",
|
|
1966
|
+
metadata: { name: "api", namespace: "shop" },
|
|
1967
|
+
},
|
|
1968
|
+
recentChangesCoverageLimited: "yes",
|
|
1969
|
+
}),
|
|
1970
|
+
],
|
|
1971
|
+
])("keeps %s prominent alongside a history note", (_label, failedTool) => {
|
|
1972
|
+
const projection = project(
|
|
1973
|
+
tool("empty-history", "get_changes", { changes: [] }),
|
|
1974
|
+
failedTool,
|
|
1975
|
+
);
|
|
1976
|
+
const html = render(projection);
|
|
1977
|
+
expect(html).toContain("Evidence coverage is incomplete");
|
|
1978
|
+
expect(html).not.toContain("Change history is limited");
|
|
1979
|
+
expect(investigationEvidenceCoverageLimited(projection)).toBe(true);
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
it("summarizes incomplete coverage and points to Activity for review", () => {
|
|
1983
|
+
const projection = project(
|
|
1984
|
+
tool(
|
|
1985
|
+
"issues-cut",
|
|
1986
|
+
"issues",
|
|
1987
|
+
{ issues: [criticalIssue], total: 1, total_matched: 1 },
|
|
1988
|
+
{ truncated: true },
|
|
1989
|
+
),
|
|
1990
|
+
);
|
|
1991
|
+
const html = render(projection);
|
|
1992
|
+
const source = projection.sources[0];
|
|
1993
|
+
|
|
1994
|
+
expect(html).toContain("Evidence coverage is incomplete");
|
|
1995
|
+
expect(html).not.toContain("1 investigation result needs review");
|
|
1996
|
+
expect(html).toContain("Evidence coverage update: Issue scan:");
|
|
1997
|
+
expect(html).toContain(
|
|
1998
|
+
"Only part of this investigation result was saved, so Radar could not summarize it here.",
|
|
1999
|
+
);
|
|
2000
|
+
expect(html).toContain('aria-label="View source for Issue scan"');
|
|
2001
|
+
expect(source.primaryGroupId).toBeUndefined();
|
|
2002
|
+
expect(html).toContain(
|
|
2003
|
+
`id="${investigationEvidenceSourceDomId(source.id)}"`,
|
|
2004
|
+
);
|
|
2005
|
+
expect(html).toContain("data-evidence-source-container");
|
|
2006
|
+
expect(html).toContain('aria-label="Evidence limitation for Issue scan:');
|
|
2007
|
+
expect(html).toContain("focus:ring-2");
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
it("prioritizes failed reads in the summary without reordering detailed provenance", () => {
|
|
2011
|
+
const projection = project(
|
|
2012
|
+
tool("cut-issues", "issues", {}, { truncated: true }),
|
|
2013
|
+
tool("cut-events", "get_events", {}, { truncated: true }),
|
|
2014
|
+
tool(
|
|
2015
|
+
"denied-logs",
|
|
2016
|
+
"get_pod_logs",
|
|
2017
|
+
{ error: "pods/log is forbidden" },
|
|
2018
|
+
{ isError: true },
|
|
2019
|
+
),
|
|
2020
|
+
);
|
|
2021
|
+
expect(projection.limitations).toHaveLength(3);
|
|
2022
|
+
const originalOrder = projection.limitations.map((item) => item.source);
|
|
2023
|
+
const failure = projection.limitations.find(
|
|
2024
|
+
(item) => item.kind === "error",
|
|
2025
|
+
)!;
|
|
2026
|
+
const html = render(projection);
|
|
2027
|
+
expect(html).toContain(
|
|
2028
|
+
renderToStaticMarkup(
|
|
2029
|
+
<>
|
|
2030
|
+
Evidence coverage update: {failure.source}: {failure.message}
|
|
2031
|
+
</>,
|
|
2032
|
+
),
|
|
2033
|
+
);
|
|
2034
|
+
expect(projection.limitations.map((item) => item.source)).toEqual(
|
|
2035
|
+
originalOrder,
|
|
2036
|
+
);
|
|
2037
|
+
expect(html).toContain(
|
|
2038
|
+
'aria-label="Evidence limitation for Container logs:',
|
|
2039
|
+
);
|
|
2040
|
+
expect(html).toContain('aria-label="Evidence limitation for Issue scan:');
|
|
2041
|
+
expect(html).toContain(
|
|
2042
|
+
'aria-label="Evidence limitation for Kubernetes events:',
|
|
2043
|
+
);
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
it.each([
|
|
2047
|
+
["denied logs", { logsError: "pods/log is forbidden" }],
|
|
2048
|
+
[
|
|
2049
|
+
"sampled pods",
|
|
2050
|
+
{
|
|
2051
|
+
logCoverage: {
|
|
2052
|
+
selectedPods: 2,
|
|
2053
|
+
resolvedPods: 5,
|
|
2054
|
+
selectionTruncated: true,
|
|
2055
|
+
},
|
|
2056
|
+
},
|
|
2057
|
+
],
|
|
2058
|
+
["unavailable changes", { recentChangesError: "timeline unavailable" }],
|
|
2059
|
+
[
|
|
2060
|
+
"denied previous logs",
|
|
2061
|
+
{
|
|
2062
|
+
logsPrevious: [
|
|
2063
|
+
{ pod: "api-0", container: "api", error: "pods/log is forbidden" },
|
|
2064
|
+
],
|
|
2065
|
+
},
|
|
2066
|
+
],
|
|
2067
|
+
])("retains %s when the agent reports healthy", (_label, limits) => {
|
|
2068
|
+
const projection = project(
|
|
2069
|
+
tool("diagnose", "diagnose", {
|
|
2070
|
+
resource: {
|
|
2071
|
+
kind: "Deployment",
|
|
2072
|
+
metadata: { name: "api", namespace: "shop" },
|
|
2073
|
+
},
|
|
2074
|
+
pods: 0,
|
|
2075
|
+
events: [],
|
|
2076
|
+
recentChanges: [],
|
|
2077
|
+
...limits,
|
|
2078
|
+
}),
|
|
2079
|
+
);
|
|
2080
|
+
const assessment = renderToStaticMarkup(
|
|
2081
|
+
<ResultCard
|
|
2082
|
+
diagnosis={
|
|
2083
|
+
{
|
|
2084
|
+
healthy: true,
|
|
2085
|
+
rootCause: "",
|
|
2086
|
+
report: "The workload appears ready.",
|
|
2087
|
+
remediation: [],
|
|
2088
|
+
} as Diagnosis
|
|
2089
|
+
}
|
|
2090
|
+
coverageLimited={investigationEvidenceCoverageLimited(projection)}
|
|
2091
|
+
/>,
|
|
2092
|
+
);
|
|
2093
|
+
expect(assessment).toContain("No problem identified in available evidence");
|
|
2094
|
+
expect(assessment).not.toContain("border-emerald-500/30");
|
|
2095
|
+
expect(render(projection)).toContain("Evidence coverage is incomplete");
|
|
2096
|
+
expect(projection.limitations.length).toBeGreaterThan(0);
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
it("distinguishes a live empty collection from a finished inconclusive one", () => {
|
|
2100
|
+
const empty: InvestigationEvidenceProjection = {
|
|
2101
|
+
groups: [],
|
|
2102
|
+
limitations: [],
|
|
2103
|
+
sources: [],
|
|
2104
|
+
evidenceRefSources: [],
|
|
2105
|
+
citableSources: [],
|
|
2106
|
+
coverage: { attempted: 0, projected: 0, limited: 0, checked: 0 },
|
|
2107
|
+
};
|
|
2108
|
+
const collecting = render(empty, true);
|
|
2109
|
+
const finished = render(empty, false);
|
|
2110
|
+
|
|
2111
|
+
expect(collecting).toContain("collecting");
|
|
2112
|
+
expect(collecting).toContain("Evidence will appear here");
|
|
2113
|
+
expect(collecting).toContain(
|
|
2114
|
+
"The Activity pane remains the live record while the agent investigates.",
|
|
2115
|
+
);
|
|
2116
|
+
expect(collecting).not.toContain("No relevant evidence to show yet");
|
|
2117
|
+
|
|
2118
|
+
expect(finished).not.toContain(">collecting<");
|
|
2119
|
+
expect(finished).toContain("No relevant evidence to show yet");
|
|
2120
|
+
expect(finished).toContain("This does not mean the resource is healthy.");
|
|
2121
|
+
expect(finished).not.toContain("Evidence will appear here");
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
it("keeps pre-verification smoking guns inspectable as Earlier evidence", () => {
|
|
2125
|
+
const resource = {
|
|
2126
|
+
apiVersion: "apps/v1",
|
|
2127
|
+
kind: "Deployment",
|
|
2128
|
+
metadata: { namespace: "shop", name: "api" },
|
|
2129
|
+
};
|
|
2130
|
+
const projection = projectInvestigationEvidence(
|
|
2131
|
+
[
|
|
2132
|
+
{
|
|
2133
|
+
status: "done",
|
|
2134
|
+
timeline: [
|
|
2135
|
+
tool("initial", "diagnose", {
|
|
2136
|
+
resource,
|
|
2137
|
+
resourceContext: { tier: "basic" },
|
|
2138
|
+
pods: 1,
|
|
2139
|
+
relatedIssues: [criticalIssue],
|
|
2140
|
+
events: [],
|
|
2141
|
+
recentChanges: [],
|
|
2142
|
+
}),
|
|
2143
|
+
],
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
status: "done",
|
|
2147
|
+
verify: true,
|
|
2148
|
+
timeline: [
|
|
2149
|
+
tool("verification", "diagnose", {
|
|
2150
|
+
resource,
|
|
2151
|
+
resourceContext: { tier: "basic" },
|
|
2152
|
+
pods: 1,
|
|
2153
|
+
events: [],
|
|
2154
|
+
recentChanges: [],
|
|
2155
|
+
}),
|
|
2156
|
+
],
|
|
2157
|
+
},
|
|
2158
|
+
],
|
|
2159
|
+
target,
|
|
2160
|
+
);
|
|
2161
|
+
|
|
2162
|
+
const html = render(projection);
|
|
2163
|
+
expect(
|
|
2164
|
+
projection.groups.find((group) => group.kind === "issue")?.historical,
|
|
2165
|
+
).toBe(true);
|
|
2166
|
+
expect(html).toContain("Previous observations");
|
|
2167
|
+
expect(html).toContain("Earlier does not mean resolved.");
|
|
2168
|
+
expect(html).toContain("CrashLoopBackOff");
|
|
2169
|
+
});
|
|
2170
|
+
});
|