@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.
Files changed (47) hide show
  1. package/package.json +2 -2
  2. package/src/App.tsx +19 -1
  3. package/src/RadarApp.tsx +2 -2
  4. package/src/api/diagnose.test.ts +268 -0
  5. package/src/api/diagnose.ts +72 -9
  6. package/src/components/diagnose/AISettings.tsx +1 -1
  7. package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
  8. package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
  9. package/src/components/diagnose/DiagnoseContext.tsx +12 -17
  10. package/src/components/diagnose/DiagnoseSurface.test.tsx +211 -16
  11. package/src/components/diagnose/DiagnoseSurface.tsx +464 -133
  12. package/src/components/diagnose/Home.test.tsx +293 -0
  13. package/src/components/diagnose/Home.tsx +289 -119
  14. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
  15. package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
  16. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
  17. package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
  18. package/src/components/diagnose/InvestigationView.test.ts +17 -0
  19. package/src/components/diagnose/InvestigationView.tsx +1900 -393
  20. package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
  21. package/src/components/diagnose/agentCatalog.ts +1 -1
  22. package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
  23. package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
  24. package/src/components/diagnose/investigationEvidence.ts +3492 -0
  25. package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
  26. package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
  27. package/src/components/diagnose/investigationExplanation.test.ts +63 -0
  28. package/src/components/diagnose/investigationExplanation.ts +22 -0
  29. package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
  30. package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
  31. package/src/components/diagnose/investigationSourceFocus.ts +98 -0
  32. package/src/components/diagnose/investigationState.test.ts +695 -0
  33. package/src/components/diagnose/investigationState.ts +451 -0
  34. package/src/components/diagnose/parts.test.tsx +864 -3
  35. package/src/components/diagnose/parts.tsx +1337 -541
  36. package/src/components/diagnose/target.test.ts +39 -0
  37. package/src/components/diagnose/target.ts +36 -0
  38. package/src/components/diagnose/useDisclosureReveal.ts +117 -0
  39. package/src/components/home/MCPSetupDialog.tsx +2 -2
  40. package/src/components/home/mcpToolCatalog.test.ts +22 -0
  41. package/src/components/home/mcpToolCatalog.ts +3 -2
  42. package/src/components/issues/IssuesPane.tsx +5 -1
  43. package/src/components/settings/SettingsDialog.tsx +11 -13
  44. package/src/components/workload/WorkloadView.tsx +1 -1
  45. package/src/context/DiagnoseCustomization.tsx +11 -8
  46. package/src/index.css +63 -79
  47. package/src/index.ts +1 -1
@@ -0,0 +1,293 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import type { RunSummary } from "../../api/diagnose";
5
+ import { RecentList } from "./Home";
6
+
7
+ const NOW = new Date(2026, 8, 2, 14, 30);
8
+ const time = (day: number, hour: number) =>
9
+ new Date(2026, 8, day, hour).toISOString();
10
+ const visible = (html: string) => html.replace(/<[^>]*>/g, "");
11
+
12
+ function run(patch: Partial<RunSummary> = {}): RunSummary {
13
+ return {
14
+ id: "run-1",
15
+ kind: "Deployment",
16
+ group: "apps",
17
+ namespace: "shop",
18
+ name: "checkout",
19
+ context: "gke_project-one_us-east1-b_nonprod",
20
+ status: "done",
21
+ createdAt: time(2, 13),
22
+ updatedAt: time(2, 14),
23
+ ...patch,
24
+ };
25
+ }
26
+
27
+ function render(runs: RunSummary[], selectedId?: string): string {
28
+ vi.useFakeTimers();
29
+ vi.setSystemTime(NOW);
30
+ return renderToStaticMarkup(
31
+ <RecentList
32
+ agentLabel="Codex"
33
+ runs={runs}
34
+ selectedId={selectedId}
35
+ onSelect={() => {}}
36
+ />,
37
+ );
38
+ }
39
+
40
+ afterEach(() => vi.useRealTimers());
41
+
42
+ describe("RecentList", () => {
43
+ it("keeps hosted ownership and visibility cues in the compact resource-first list", () => {
44
+ const html = visible(
45
+ render([
46
+ run({
47
+ id: "mine",
48
+ name: "my-service",
49
+ ownedByMe: true,
50
+ visibility: "private",
51
+ }),
52
+ run({
53
+ id: "shared",
54
+ name: "team-service",
55
+ ownedByMe: false,
56
+ visibility: "organization",
57
+ }),
58
+ run({
59
+ id: "auto",
60
+ name: "automatic-service",
61
+ trigger: "background",
62
+ status: "stopping",
63
+ }),
64
+ ]),
65
+ );
66
+ expect(html).toContain("Your investigations");
67
+ expect(html).toContain("Organization");
68
+ expect(html).toContain("Private");
69
+ expect(html).toContain("Shared");
70
+ expect(html).toContain("Automatic");
71
+ expect(html).toContain("Stopping");
72
+ expect(html.indexOf("my-service")).toBeLessThan(
73
+ html.indexOf("Organization"),
74
+ );
75
+ expect(html.indexOf("Organization")).toBeLessThan(
76
+ html.indexOf("team-service"),
77
+ );
78
+ });
79
+
80
+ it("keeps the whole row as one button and labels individual identity fields without native tooltips", () => {
81
+ const html = render([run()]);
82
+ expect(html.match(/<button\b/g)).toHaveLength(1);
83
+ expect(html).not.toContain('title="');
84
+ expect(html).toContain('aria-label="Deployment.apps shop/checkout');
85
+ expect(html).toContain('aria-label="Cluster: nonprod"');
86
+ });
87
+ it("uses cluster identity instead of lifecycle locks and deterministic initial-issue text", () => {
88
+ const current = run({ health: { topReason: "CrashLoopBackOff" } });
89
+ const html = renderToStaticMarkup(
90
+ <RecentList
91
+ agentLabel="Codex"
92
+ runs={[
93
+ current,
94
+ run({ id: "stale", status: "stale", context: "kind-demo" }),
95
+ ]}
96
+ currentContext={current.context}
97
+ onSelect={() => {}}
98
+ />,
99
+ );
100
+ expect(html).toContain('aria-label="Current cluster: nonprod"');
101
+ expect(html).toContain('aria-label="Cluster: kind-demo"');
102
+ expect(html).toContain("Started with CrashLoopBackOff");
103
+ expect(html).not.toContain("lucide-lock");
104
+ expect(html).not.toContain("lucide-check");
105
+ });
106
+ it("uses one stable start time per run, sorted into local date groups without previews", () => {
107
+ const recentTime = time(2, 13);
108
+ const olderTime = time(1, 12);
109
+ const html = render([
110
+ run({ id: "older", name: "older", createdAt: olderTime }),
111
+ run({
112
+ id: "recent",
113
+ name: "recent",
114
+ createdAt: recentTime,
115
+ preview: "MONGO_PASSWORD **broken**",
116
+ }),
117
+ ]);
118
+ expect(html.match(/<time /g)).toHaveLength(2);
119
+ expect(html).toContain(`dateTime="${recentTime}"`);
120
+ expect(html).toContain(`dateTime="${olderTime}"`);
121
+ expect(html).not.toContain(`dateTime="${time(2, 14)}"`);
122
+ expect(html).not.toContain("ago");
123
+ expect(html).not.toContain("MONGO_PASSWORD");
124
+ expect(html).toContain(">Today</h3>");
125
+ expect(html).toContain(">Yesterday</h3>");
126
+ expect(visible(html).indexOf("recent")).toBeLessThan(
127
+ visible(html).indexOf("older"),
128
+ );
129
+ });
130
+
131
+ it("states every outcome in text and retains foreign-cluster identity", () => {
132
+ const html = render([
133
+ run({ id: "running", status: "running" }),
134
+ run({ id: "done", status: "done" }),
135
+ run({ id: "failed", status: "error" }),
136
+ run({ id: "stopped", status: "stopped" }),
137
+ run({
138
+ id: "foreign",
139
+ status: "stale",
140
+ context: "gke_prod-us-east1",
141
+ }),
142
+ ]);
143
+
144
+ expect(html).toContain("Running");
145
+ expect(html).toContain("Failed");
146
+ expect(html).toContain("Stopped");
147
+ expect(html).not.toContain("Different cluster");
148
+ expect(html).toContain("Read-only investigation");
149
+ expect(html).toContain("Investigation failed");
150
+ expect(html).toContain("text-semantic-error");
151
+ expect(html).toContain("Completed");
152
+ expect(visible(html)).not.toContain("Completed");
153
+ expect(html).not.toContain("text-amber");
154
+ expect(html).toContain("gke_prod-us-east1");
155
+ });
156
+
157
+ it("marks the selected history row without adding another visual label", () => {
158
+ const html = render([run({ id: "selected" })], "selected");
159
+
160
+ expect(html).toContain('aria-current="true"');
161
+ expect(html).toContain("border-accent bg-accent-muted");
162
+ });
163
+
164
+ it("leads with resource name and keeps full identity accessible", () => {
165
+ const html = render([run()]);
166
+ const text = visible(html);
167
+ expect(text).toContain("shop · Deployment");
168
+ expect(text).toContain("nonprod");
169
+ expect(text).not.toContain("Deployment.apps");
170
+ expect(text).not.toContain("gke_");
171
+ expect(text.indexOf("checkout")).toBeLessThan(
172
+ text.indexOf("shop · Deployment"),
173
+ );
174
+ expect(html).toContain("Deployment.apps shop/checkout");
175
+ });
176
+
177
+ it("does not change displayed time when cluster-switch bookkeeping updates runs", () => {
178
+ const before = visible(render([run()]));
179
+ const after = visible(
180
+ render([run({ status: "stale", updatedAt: time(2, 16) })]),
181
+ );
182
+ expect(after).toBe(before);
183
+ });
184
+
185
+ it("disambiguates matching cluster names by project or region", () => {
186
+ const text = visible(
187
+ render([
188
+ run(),
189
+ run({ id: "two", context: "gke_project-two_us-east1-b_nonprod" }),
190
+ ]),
191
+ );
192
+ expect(text).toContain("project-one");
193
+ expect(text).toContain("project-two");
194
+ expect(text).not.toContain("gke_");
195
+ const regions = visible(
196
+ render([
197
+ run(),
198
+ run({ id: "two", context: "gke_project-one_us-west1-b_nonprod" }),
199
+ ]),
200
+ );
201
+ expect(regions).toContain("project-one · us-east1-b");
202
+ expect(regions).toContain("project-one · us-west1-b");
203
+ });
204
+
205
+ it("retains raw identity when parsed qualifiers still collide", () => {
206
+ const text = visible(
207
+ render([
208
+ run({ context: "clusterUser_rg_nonprod" }),
209
+ run({ id: "admin", context: "clusterAdmin_rg_nonprod" }),
210
+ ]),
211
+ );
212
+ expect(text).toContain("clusterUser_rg_nonprod");
213
+ expect(text).toContain("clusterAdmin_rg_nonprod");
214
+ });
215
+
216
+ it("qualifies ambiguous kinds and handles cluster-scoped resources", () => {
217
+ const text = visible(
218
+ render([
219
+ run({ kind: "Service", group: "" }),
220
+ run({ id: "knative", kind: "Service", group: "serving.knative.dev" }),
221
+ run({
222
+ id: "node",
223
+ kind: "Node",
224
+ group: "",
225
+ namespace: "",
226
+ name: "worker",
227
+ }),
228
+ ]),
229
+ );
230
+ expect(text).toContain("Service · core");
231
+ expect(text).toContain("Service · serving.knative.dev");
232
+ expect(text).toContain("worker");
233
+ expect(text).toContain("Node");
234
+ expect(text).not.toContain("shop · Node");
235
+ });
236
+
237
+ it("retains full long names and custom contexts", () => {
238
+ const name =
239
+ "a-very-long-workload-name-that-needs-two-lines-to-be-recognizable";
240
+ const html = render([run({ name, context: "my-custom-context" })]);
241
+ expect(html).toContain(name);
242
+ expect(html).toContain("line-clamp-2 break-words");
243
+ expect(visible(html)).toContain("my-custom-context");
244
+ });
245
+
246
+ it("uses Radar's readable kind labels and omits built-in group display noise", () => {
247
+ const text = visible(
248
+ render([run(), run({ id: "plural", kind: "deployments", group: "" })]),
249
+ );
250
+ expect(text.match(/shop · Deployment/g)).toHaveLength(2);
251
+ expect(text).not.toContain("Deployment · apps");
252
+ expect(text).not.toContain("Deployment · core");
253
+ });
254
+
255
+ it("does not duplicate an unparsed cluster name when it collides with a parsed name", () => {
256
+ const html = render([run(), run({ id: "alias", context: "nonprod" })]);
257
+ const aliasButton = html.match(
258
+ /<button[^>]*aria-label="[^"]* · nonprod ·[^>]*>[\s\S]*?<\/button>/,
259
+ )?.[0];
260
+ expect(aliasButton).toBeDefined();
261
+ expect(visible(aliasButton!).match(/nonprod/g)).toHaveLength(1);
262
+ });
263
+
264
+ it("distinguishes prior years and handles yesterday across month boundaries", () => {
265
+ expect(
266
+ visible(
267
+ render([run({ createdAt: new Date(2025, 8, 2, 9).toISOString() })]),
268
+ ),
269
+ ).toContain("2025");
270
+ vi.setSystemTime(new Date(2026, 8, 1, 0, 30));
271
+ const html = renderToStaticMarkup(
272
+ <RecentList
273
+ agentLabel="Codex"
274
+ runs={[run({ createdAt: new Date(2026, 7, 31, 23).toISOString() })]}
275
+ onSelect={() => {}}
276
+ />,
277
+ );
278
+ expect(html).toContain(">Yesterday</h3>");
279
+ });
280
+
281
+ it("retains empty-state guidance and persistence warnings", () => {
282
+ expect(visible(render([]))).toContain("No investigations yet");
283
+ const html = renderToStaticMarkup(
284
+ <RecentList
285
+ agentLabel="Codex"
286
+ runs={[]}
287
+ onSelect={() => {}}
288
+ historyDegraded
289
+ />,
290
+ );
291
+ expect(visible(html)).toContain("disk error");
292
+ });
293
+ });