@skyhook-io/radar-app 1.13.0 → 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 +55 -9
- package/src/RadarApp.tsx +3 -3
- package/src/api/diagnose.test.ts +268 -0
- package/src/api/diagnose.ts +117 -14
- package/src/components/diagnose/AISettings.tsx +10 -5
- package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
- package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
- package/src/components/diagnose/DiagnoseContext.tsx +228 -65
- package/src/components/diagnose/DiagnoseSurface.test.tsx +317 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +621 -132
- package/src/components/diagnose/Home.test.tsx +293 -0
- package/src/components/diagnose/Home.tsx +307 -108
- 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 +1911 -338
- 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 +1340 -536
- 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/execution/BatchExecutionView.render.test.tsx +35 -0
- package/src/components/execution/BatchExecutionView.tsx +2 -3
- package/src/components/execution/execution-definition.test.ts +19 -0
- package/src/components/execution/execution-definition.ts +2 -0
- package/src/components/gitops/GitOpsView.tsx +25 -3
- 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/nav/navigation.test.ts +26 -0
- package/src/components/nav/navigation.ts +10 -0
- 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
package/src/api/diagnose.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Client for
|
|
1
|
+
// Client for local AI investigations (OSS BYO-agent). The agent CLI runs
|
|
2
2
|
// on the user's own machine/subscription against Radar's MCP; this just starts
|
|
3
3
|
// the investigation and consumes its SSE event stream.
|
|
4
|
-
import { getApiBase, getCredentialsMode } from "./config";
|
|
4
|
+
import { getApiBase, getAuthHeaders, getCredentialsMode } from "./config";
|
|
5
5
|
|
|
6
6
|
export interface AgentInfo {
|
|
7
7
|
name: string;
|
|
@@ -18,7 +18,7 @@ export interface AgentInfo {
|
|
|
18
18
|
export interface AgentsResponse {
|
|
19
19
|
agents: AgentInfo[];
|
|
20
20
|
enabled: boolean;
|
|
21
|
-
// eligible: this run mode supports local BYO-agent
|
|
21
|
+
// eligible: this run mode supports local BYO-agent investigations (no proxy/OIDC
|
|
22
22
|
// auth, /mcp mounted) — true even when no agent is installed. Lets the UI tell
|
|
23
23
|
// "install an agent to enable this" (eligible && !enabled) apart from "not
|
|
24
24
|
// available here" (auth/cloud/--no-mcp). Absent on older servers / embed hosts.
|
|
@@ -37,13 +37,24 @@ export interface DiagnoseStep {
|
|
|
37
37
|
ms?: number;
|
|
38
38
|
summary?: string; // input args (on running)
|
|
39
39
|
result?: string; // result text (on done), capped
|
|
40
|
+
evidenceRef?: string; // server-issued reference used to bind a root cause to this exact result
|
|
41
|
+
// Server validated Radar's uncapped producer result and this retained result
|
|
42
|
+
// as its exact capped derivative.
|
|
43
|
+
radarEvidence?: boolean;
|
|
44
|
+
isError?: boolean; // authoritative agent-host result; absent means unknown
|
|
40
45
|
truncated?: boolean; // result was capped — payload shown/copied is partial
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
export interface RootCauseEvidence {
|
|
49
|
+
status: "linked" | "missing" | "invalid";
|
|
50
|
+
refs?: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
export interface Diagnosis {
|
|
44
54
|
healthy?: boolean;
|
|
45
55
|
inconclusive?: boolean; // investigated but couldn't determine — distinct from healthy
|
|
46
56
|
rootCause: string;
|
|
57
|
+
rootCauseEvidence?: RootCauseEvidence;
|
|
47
58
|
report: string;
|
|
48
59
|
remediation: string[];
|
|
49
60
|
recommendedIndex?: number; // 1-based index into remediation of the step Apply performs
|
|
@@ -72,8 +83,19 @@ export interface ResourceHealthSignal {
|
|
|
72
83
|
auditFindings?: HealthLine[];
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
export type ApplyMutationOutcome = "confirmed" | "failed" | "unknown";
|
|
87
|
+
|
|
75
88
|
export interface DiagnoseStreamEvent {
|
|
76
|
-
type:
|
|
89
|
+
type:
|
|
90
|
+
| "turn"
|
|
91
|
+
| "phase"
|
|
92
|
+
| "step"
|
|
93
|
+
| "thinking"
|
|
94
|
+
| "done"
|
|
95
|
+
| "error"
|
|
96
|
+
| "closed"
|
|
97
|
+
| "history_unavailable"
|
|
98
|
+
| "replay_complete";
|
|
77
99
|
phase?: string;
|
|
78
100
|
step?: DiagnoseStep;
|
|
79
101
|
token?: string;
|
|
@@ -81,6 +103,17 @@ export interface DiagnoseStreamEvent {
|
|
|
81
103
|
error?: string;
|
|
82
104
|
question?: string; // on "turn"
|
|
83
105
|
apply?: boolean; // on "turn"
|
|
106
|
+
verify?: boolean; // on "turn": explicit post-change re-check
|
|
107
|
+
explainAssessment?: number; // on "turn": originating assessment done-event sequence
|
|
108
|
+
// Evidence-backed mutation truth on an apply turn's terminal event. Only
|
|
109
|
+
// "confirmed" means a Radar write tool authoritatively reported success.
|
|
110
|
+
applyOutcome?: ApplyMutationOutcome;
|
|
111
|
+
// On an apply terminal event, the server durably queued the adjacent
|
|
112
|
+
// read-only verification turn. This prevents an idle control flash between
|
|
113
|
+
// the two SSE events without inferring lifecycle from copy.
|
|
114
|
+
verificationScheduled?: boolean;
|
|
115
|
+
retryable?: boolean; // on "history_unavailable": keep native EventSource reconnect alive
|
|
116
|
+
actor?: string; // human author on shared hosted transcripts
|
|
84
117
|
}
|
|
85
118
|
|
|
86
119
|
// A run is a durable, server-owned investigation. Its lifetime is independent of
|
|
@@ -89,6 +122,8 @@ export interface DiagnoseStreamEvent {
|
|
|
89
122
|
export interface RunSummary {
|
|
90
123
|
id: string;
|
|
91
124
|
kind: string;
|
|
125
|
+
/** Kubernetes API group; empty means the core API group. */
|
|
126
|
+
group: string;
|
|
92
127
|
namespace: string;
|
|
93
128
|
name: string;
|
|
94
129
|
/** The issue this session is for, on hosts that key sessions by issue. Always
|
|
@@ -101,11 +136,17 @@ export interface RunSummary {
|
|
|
101
136
|
effort?: string;
|
|
102
137
|
managedBy?: string; // GitOps/Helm owner of the target ("Argo CD"/"Flux"/"Helm"), for the Apply warning
|
|
103
138
|
health?: ResourceHealthSignal;
|
|
104
|
-
status: "running" | "done" | "error" | "stopped" | "stale";
|
|
139
|
+
status: "running" | "stopping" | "done" | "error" | "stopped" | "stale";
|
|
105
140
|
sessionId?: string;
|
|
106
141
|
preview?: string;
|
|
107
142
|
createdAt: string;
|
|
108
143
|
updatedAt: string;
|
|
144
|
+
visibility?: "private" | "organization";
|
|
145
|
+
ownedByMe?: boolean;
|
|
146
|
+
canManageVisibility?: boolean;
|
|
147
|
+
canContinue?: boolean;
|
|
148
|
+
trigger?: "interactive" | "background";
|
|
149
|
+
radarUrl?: string;
|
|
109
150
|
}
|
|
110
151
|
|
|
111
152
|
export async function fetchAgents(
|
|
@@ -113,6 +154,7 @@ export async function fetchAgents(
|
|
|
113
154
|
): Promise<AgentsResponse> {
|
|
114
155
|
const res = await fetch(`${getApiBase()}/agents`, {
|
|
115
156
|
credentials: getCredentialsMode(),
|
|
157
|
+
headers: getAuthHeaders(),
|
|
116
158
|
signal,
|
|
117
159
|
});
|
|
118
160
|
if (!res.ok) throw new Error(`agents: ${res.status}`);
|
|
@@ -145,6 +187,8 @@ const RUNS = () => `${getApiBase()}/diagnose/runs`;
|
|
|
145
187
|
export async function createRun(
|
|
146
188
|
target: {
|
|
147
189
|
kind: string;
|
|
190
|
+
/** Kubernetes API group; empty means the core API group. */
|
|
191
|
+
group: string;
|
|
148
192
|
namespace: string;
|
|
149
193
|
name: string;
|
|
150
194
|
// Associates the session with the issue it was started from, for hosts that
|
|
@@ -167,7 +211,7 @@ export async function createRun(
|
|
|
167
211
|
const res = await fetch(RUNS(), {
|
|
168
212
|
method: "POST",
|
|
169
213
|
credentials: getCredentialsMode(),
|
|
170
|
-
headers: { "Content-Type": "application/json" },
|
|
214
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
171
215
|
body: JSON.stringify({ ...target, ...opts }),
|
|
172
216
|
});
|
|
173
217
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
@@ -186,6 +230,7 @@ export interface RunsResponse {
|
|
|
186
230
|
export async function listRuns(signal?: AbortSignal): Promise<RunsResponse> {
|
|
187
231
|
const res = await fetch(RUNS(), {
|
|
188
232
|
credentials: getCredentialsMode(),
|
|
233
|
+
headers: getAuthHeaders(),
|
|
189
234
|
signal,
|
|
190
235
|
});
|
|
191
236
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
@@ -193,12 +238,41 @@ export async function listRuns(signal?: AbortSignal): Promise<RunsResponse> {
|
|
|
193
238
|
return { runs: d.runs ?? [], historyDegraded: !!d.historyDegraded };
|
|
194
239
|
}
|
|
195
240
|
|
|
241
|
+
// getRun resolves a stable run id directly. Deep links use this rather than
|
|
242
|
+
// relying on a bounded history page to happen to contain the target.
|
|
243
|
+
export async function getRun(
|
|
244
|
+
id: string,
|
|
245
|
+
signal?: AbortSignal,
|
|
246
|
+
): Promise<RunSummary> {
|
|
247
|
+
const res = await fetch(`${RUNS()}/${encodeURIComponent(id)}`, {
|
|
248
|
+
credentials: getCredentialsMode(),
|
|
249
|
+
headers: getAuthHeaders(),
|
|
250
|
+
signal,
|
|
251
|
+
});
|
|
252
|
+
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
253
|
+
return res.json();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function updateRunVisibility(
|
|
257
|
+
id: string,
|
|
258
|
+
visibility: "private" | "organization",
|
|
259
|
+
): Promise<RunSummary> {
|
|
260
|
+
const res = await fetch(`${RUNS()}/${encodeURIComponent(id)}`, {
|
|
261
|
+
method: "PATCH",
|
|
262
|
+
credentials: getCredentialsMode(),
|
|
263
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
264
|
+
body: JSON.stringify({ visibility }),
|
|
265
|
+
});
|
|
266
|
+
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
267
|
+
return res.json();
|
|
268
|
+
}
|
|
269
|
+
|
|
196
270
|
// recordConsent acknowledges the current disclosure for an execution profile, server-side.
|
|
197
271
|
export async function recordConsent(surface: string): Promise<void> {
|
|
198
272
|
const res = await fetch(`${getApiBase()}/diagnose/consent`, {
|
|
199
273
|
method: "POST",
|
|
200
274
|
credentials: getCredentialsMode(),
|
|
201
|
-
headers: { "Content-Type": "application/json" },
|
|
275
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
202
276
|
body: JSON.stringify({ surface }),
|
|
203
277
|
});
|
|
204
278
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
@@ -210,6 +284,7 @@ export async function clearHistory(): Promise<void> {
|
|
|
210
284
|
const res = await fetch(`${getApiBase()}/diagnose/history/clear`, {
|
|
211
285
|
method: "POST",
|
|
212
286
|
credentials: getCredentialsMode(),
|
|
287
|
+
headers: getAuthHeaders(),
|
|
213
288
|
});
|
|
214
289
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
215
290
|
}
|
|
@@ -217,12 +292,18 @@ export async function clearHistory(): Promise<void> {
|
|
|
217
292
|
// addTurn appends a follow-up (question) or an apply turn (apply + confirmed fix).
|
|
218
293
|
export async function addTurn(
|
|
219
294
|
id: string,
|
|
220
|
-
body: {
|
|
295
|
+
body: {
|
|
296
|
+
question?: string;
|
|
297
|
+
apply?: boolean;
|
|
298
|
+
fix?: string;
|
|
299
|
+
verify?: boolean;
|
|
300
|
+
explainAssessment?: number;
|
|
301
|
+
},
|
|
221
302
|
): Promise<void> {
|
|
222
303
|
const res = await fetch(`${RUNS()}/${id}/turns`, {
|
|
223
304
|
method: "POST",
|
|
224
305
|
credentials: getCredentialsMode(),
|
|
225
|
-
headers: { "Content-Type": "application/json" },
|
|
306
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
226
307
|
body: JSON.stringify(body),
|
|
227
308
|
});
|
|
228
309
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
@@ -233,12 +314,16 @@ export async function stopRun(id: string): Promise<void> {
|
|
|
233
314
|
await fetch(`${RUNS()}/${id}/stop`, {
|
|
234
315
|
method: "POST",
|
|
235
316
|
credentials: getCredentialsMode(),
|
|
317
|
+
headers: getAuthHeaders(),
|
|
236
318
|
}).catch(() => {});
|
|
237
319
|
}
|
|
238
320
|
|
|
239
321
|
export interface SubscribeHandlers {
|
|
240
|
-
onEvent: (ev: DiagnoseStreamEvent) => void;
|
|
241
|
-
|
|
322
|
+
onEvent: (ev: DiagnoseStreamEvent, sequence?: number) => void;
|
|
323
|
+
// EventSource fires open before each initial/reconnect replay. Together with
|
|
324
|
+
// replay_complete this brackets history so consumers can rebuild silently.
|
|
325
|
+
onReplayStart?: () => void;
|
|
326
|
+
onClosed?: (reason: "run_closed" | "unavailable") => void;
|
|
242
327
|
}
|
|
243
328
|
|
|
244
329
|
/**
|
|
@@ -262,6 +347,11 @@ export function subscribeRun(
|
|
|
262
347
|
es.close();
|
|
263
348
|
};
|
|
264
349
|
const dispatch = (e: MessageEvent) => {
|
|
350
|
+
// close() can run while an earlier SSE callback is being dispatched. Ignore
|
|
351
|
+
// any already-queued trailing frame (notably the server's `closed` sentinel
|
|
352
|
+
// after a permanent history_unavailable) so it cannot replace the specific
|
|
353
|
+
// failure explanation with a generic eviction state.
|
|
354
|
+
if (closed) return;
|
|
265
355
|
let ev: DiagnoseStreamEvent;
|
|
266
356
|
try {
|
|
267
357
|
ev = JSON.parse(e.data);
|
|
@@ -270,11 +360,21 @@ export function subscribeRun(
|
|
|
270
360
|
}
|
|
271
361
|
if (ev.type === "closed") {
|
|
272
362
|
close();
|
|
273
|
-
handlers.onClosed?.();
|
|
363
|
+
handlers.onClosed?.("run_closed");
|
|
274
364
|
return;
|
|
275
365
|
}
|
|
276
|
-
|
|
366
|
+
const sequence = Number(e.lastEventId);
|
|
367
|
+
handlers.onEvent(
|
|
368
|
+
ev,
|
|
369
|
+
Number.isSafeInteger(sequence) && sequence > 0 ? sequence : undefined,
|
|
370
|
+
);
|
|
371
|
+
// Hydration failures stay inside the SSE protocol. Retryable failures end
|
|
372
|
+
// this response and let native EventSource reconnect without advancing its
|
|
373
|
+
// cursor; a permanent failure must stop that reconnect loop after the UI
|
|
374
|
+
// receives the explanatory event.
|
|
375
|
+
if (ev.type === "history_unavailable" && ev.retryable !== true) close();
|
|
277
376
|
};
|
|
377
|
+
es.onopen = () => handlers.onReplayStart?.();
|
|
278
378
|
for (const t of [
|
|
279
379
|
"turn",
|
|
280
380
|
"phase",
|
|
@@ -283,10 +383,13 @@ export function subscribeRun(
|
|
|
283
383
|
"done",
|
|
284
384
|
"error",
|
|
285
385
|
"closed",
|
|
386
|
+
"history_unavailable",
|
|
387
|
+
"replay_complete",
|
|
286
388
|
] as const) {
|
|
287
389
|
es.addEventListener(t, dispatch);
|
|
288
390
|
}
|
|
289
391
|
es.onerror = () => {
|
|
392
|
+
if (closed) return;
|
|
290
393
|
// A permanent failure (readyState CLOSED) means the run is gone — a 404 because
|
|
291
394
|
// it was evicted (retention cap) or lost on a server restart. Surface it as
|
|
292
395
|
// closed so the view shows a "no longer available" state instead of a silent
|
|
@@ -294,7 +397,7 @@ export function subscribeRun(
|
|
|
294
397
|
// replays only what we missed), so leave those to EventSource.
|
|
295
398
|
if (es.readyState === EventSource.CLOSED) {
|
|
296
399
|
close();
|
|
297
|
-
handlers.onClosed?.();
|
|
400
|
+
handlers.onClosed?.("unavailable");
|
|
298
401
|
}
|
|
299
402
|
};
|
|
300
403
|
return close;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The agent/model/effort controls + a clear-history action for the Settings
|
|
2
|
-
// "AI
|
|
2
|
+
// "AI investigations" tab. Controlled by the dialog: it edits a STAGED draft committed
|
|
3
3
|
// on Save (like the rest of Settings), not on every keystroke. The heading,
|
|
4
4
|
// description, and Save button live in the dialog so this tab matches the other
|
|
5
5
|
// Settings tabs' layout — this renders only the controls (no card, no heading).
|
|
@@ -49,7 +49,7 @@ function ClearHistoryRow({
|
|
|
49
49
|
<div className="mt-3 flex items-center justify-between gap-2 border-t border-theme-border/60 pt-3">
|
|
50
50
|
<p className="text-[11px] leading-snug text-theme-text-tertiary">
|
|
51
51
|
{hosted ? (
|
|
52
|
-
|
|
52
|
+
`${agentLabel} stores private, organization-shared, and automatic investigation transcripts so history survives restarts.`
|
|
53
53
|
) : (
|
|
54
54
|
<>
|
|
55
55
|
Investigation transcripts are kept on this machine (
|
|
@@ -67,6 +67,12 @@ function ClearHistoryRow({
|
|
|
67
67
|
Couldn't clear history.
|
|
68
68
|
</span>
|
|
69
69
|
)}
|
|
70
|
+
{hosted && confirming && state === "idle" && (
|
|
71
|
+
<span className="ml-1 font-medium text-red-400">
|
|
72
|
+
This permanently deletes every member's investigations for this
|
|
73
|
+
cluster — private, organization-shared, and automatic.
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
70
76
|
</p>
|
|
71
77
|
{confirming ? (
|
|
72
78
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
@@ -133,9 +139,8 @@ export function AISettingsSection({
|
|
|
133
139
|
selectedAgent={draft.agent}
|
|
134
140
|
// Model + effort are agent-specific; reset them when the agent changes.
|
|
135
141
|
onSelectAgent={(a) => {
|
|
136
|
-
const nextProfile = agents.find(
|
|
137
|
-
|
|
138
|
-
)?.profiles?.[0];
|
|
142
|
+
const nextProfile = agents.find((agent) => agent.name === a)
|
|
143
|
+
?.profiles?.[0];
|
|
139
144
|
onChange({
|
|
140
145
|
agent: a,
|
|
141
146
|
profile: nextProfile ?? draft.profile,
|
|
@@ -52,7 +52,7 @@ function AgentRow({ agent }: { agent: AgentInstall }) {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// Shown in the AI surface's Home when
|
|
55
|
+
// Shown in the AI surface's Home when investigations are eligible in this deployment
|
|
56
56
|
// but not runnable yet — either no agent CLI is installed ("needs-install") or a
|
|
57
57
|
// supported one appeared after Radar booted ("needs-restart"). Radar decides the
|
|
58
58
|
// engine once at startup, so a fresh install needs a restart to take effect.
|
|
@@ -69,8 +69,8 @@ export function AgentSetupNotice({
|
|
|
69
69
|
</div>
|
|
70
70
|
<h3 className="text-base font-semibold text-theme-text-primary">
|
|
71
71
|
{needsRestart
|
|
72
|
-
? "Restart Radar to enable AI
|
|
73
|
-
: "Set up AI
|
|
72
|
+
? "Restart Radar to enable AI investigations"
|
|
73
|
+
: "Set up AI investigations"}
|
|
74
74
|
</h3>
|
|
75
75
|
<p className="mt-1 text-sm text-theme-text-secondary">
|
|
76
76
|
{needsRestart ? (
|
|
@@ -109,8 +109,8 @@ export function AgentSetupNotice({
|
|
|
109
109
|
</button>
|
|
110
110
|
|
|
111
111
|
<p className="mt-4 text-xs text-theme-text-tertiary">
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
Your agent runs locally. Resource details and logs are sent to its model
|
|
113
|
+
provider under your account, not to Radar.
|
|
114
114
|
</p>
|
|
115
115
|
</div>
|
|
116
116
|
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ApplyDialog } from "./parts";
|
|
5
|
+
import { AgentSetupNotice } from "./AgentSetupNotice";
|
|
6
|
+
|
|
7
|
+
vi.mock("@skyhook-io/k8s-ui/components/ui/DialogPortal", () => ({
|
|
8
|
+
DialogPortal: ({ open, children }: { open: boolean; children: ReactNode }) =>
|
|
9
|
+
open ? children : null,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe("Apply confirmation context", () => {
|
|
13
|
+
it.each([
|
|
14
|
+
"gke_project-a_us-east1-b_nonprod",
|
|
15
|
+
"gke_project-b_us-east1-b_nonprod",
|
|
16
|
+
])(
|
|
17
|
+
"identifies the exact destination %s even when display names collide",
|
|
18
|
+
(context) => {
|
|
19
|
+
const html = renderToStaticMarkup(
|
|
20
|
+
<ApplyDialog
|
|
21
|
+
open
|
|
22
|
+
onClose={() => {}}
|
|
23
|
+
onConfirm={() => {}}
|
|
24
|
+
agentLabel="Local agent"
|
|
25
|
+
resourceLabel="Deployment dev/api"
|
|
26
|
+
context={context}
|
|
27
|
+
fix="Only after verifying credentials, update the Secret reference."
|
|
28
|
+
/>,
|
|
29
|
+
);
|
|
30
|
+
expect(html).toContain("Cluster:");
|
|
31
|
+
expect(html).toContain("nonprod");
|
|
32
|
+
expect(html).toContain(context);
|
|
33
|
+
expect(html).toContain("Deployment dev/api");
|
|
34
|
+
expect(html).toContain("Proposed change");
|
|
35
|
+
expect(html).not.toContain("What will happen");
|
|
36
|
+
expect(html).not.toContain("max-h-48");
|
|
37
|
+
expect(html).toContain("Only after verifying credentials");
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
it("keeps managed-resource acknowledgement and low-confidence warning", () => {
|
|
42
|
+
const html = renderToStaticMarkup(
|
|
43
|
+
<ApplyDialog
|
|
44
|
+
open
|
|
45
|
+
onClose={() => {}}
|
|
46
|
+
onConfirm={() => {}}
|
|
47
|
+
agentLabel="Local agent"
|
|
48
|
+
resourceLabel="Deployment dev/api"
|
|
49
|
+
context="kind-dev"
|
|
50
|
+
fix="Update its configuration"
|
|
51
|
+
managedBy="Argo CD"
|
|
52
|
+
confidence={0.3}
|
|
53
|
+
/>,
|
|
54
|
+
);
|
|
55
|
+
expect(html).toContain("I understand Argo CD may revert this");
|
|
56
|
+
const applyButton = html
|
|
57
|
+
.match(/<button\b[^>]*>[\s\S]*?<\/button>/g)
|
|
58
|
+
?.find((button) => button.includes("Apply fix"));
|
|
59
|
+
expect(applyButton).toContain('disabled=""');
|
|
60
|
+
expect(html).toContain("low confidence");
|
|
61
|
+
expect(html.match(/kubeconfig/g)).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("distinguishes local execution from sending resource data to the model provider", () => {
|
|
66
|
+
const html = renderToStaticMarkup(
|
|
67
|
+
<AgentSetupNotice setupState="needs-install" />,
|
|
68
|
+
);
|
|
69
|
+
expect(html).toContain("Your agent runs locally");
|
|
70
|
+
expect(html).toContain("model provider under your account, not to Radar");
|
|
71
|
+
expect(html).not.toContain("never leaves your machine");
|
|
72
|
+
});
|