Skip to content

Commit 56c7b79

Browse files
committed
feat: improve diagnostics and settings surfaces
1 parent 26458c9 commit 56c7b79

26 files changed

Lines changed: 2146 additions & 254 deletions

File tree

e2e/fixtures/phase2-i18n.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Page } from "@playwright/test";
22
import { type E2ELocaleCode, translateForE2E } from "./i18n.js";
33

4-
type SettingsSection = "general" | "appearance" | "providers" | "shortcuts";
4+
type SettingsSection = "general" | "appearance" | "providers" | "shortcuts" | "analysis";
55
type ProviderSettingLabel =
66
| "base"
77
| "config_file"
@@ -15,6 +15,7 @@ const SETTINGS_SECTION_KEYS: Record<SettingsSection, Parameters<typeof translate
1515
general: "settings.general",
1616
appearance: "settings.appearance",
1717
providers: "settings.providers",
18+
analysis: "settings.analysis.title",
1819
shortcuts: "settings.shortcuts.title",
1920
};
2021

packages/server/src/__tests__/diagnostics-commands.test.ts

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mkdtemp, writeFile } from "node:fs/promises";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
4-
import type { ProviderDefinition } from "@coder-studio/core";
4+
import type { LspToolRuntimeStatusEntry, ProviderDefinition } from "@coder-studio/core";
55
import { providerRegistry } from "@coder-studio/providers";
66
import { describe, expect, it } from "vitest";
77
import type { EventBus } from "../bus/event-bus.js";
@@ -57,6 +57,36 @@ function createContext(overrides: Partial<CommandContext> = {}): CommandContext
5757
},
5858
...providerRuntimeDeps,
5959
},
60+
lspMgr: {
61+
getRuntimeMode: () => "auto",
62+
} as never,
63+
lspToolMgr: {
64+
runtimeStatus: async ({ serverKind }: { serverKind: string }) =>
65+
({
66+
serverKind,
67+
displayName: `${serverKind} language server`,
68+
available: serverKind === "typescript",
69+
autoInstallSupported: serverKind !== "typescript",
70+
installReadiness:
71+
serverKind === "python"
72+
? "missing_prerequisite"
73+
: serverKind === "rust"
74+
? "unsupported_platform"
75+
: "ready",
76+
missingCommands:
77+
serverKind === "python"
78+
? ["pylsp"]
79+
: serverKind === "go"
80+
? ["gopls"]
81+
: serverKind === "vue"
82+
? ["vue-language-server"]
83+
: [],
84+
missingPrerequisites: serverKind === "python" ? ["python3"] : [],
85+
}) satisfies LspToolRuntimeStatusEntry,
86+
} as never,
87+
lspToolInstallMgr: {
88+
getLatestFailure: () => undefined,
89+
} as never,
6090
...restOverrides,
6191
};
6292
}
@@ -103,6 +133,37 @@ describe("diagnostics commands", () => {
103133
}),
104134
])
105135
);
136+
expect(
137+
(
138+
result.data as {
139+
lspServices: Array<{ serverKind: string; status: string }>;
140+
metadata: {
141+
lspRuntimeContext?: {
142+
targetRuntime: "native" | "wsl";
143+
managedInstallSupported: boolean;
144+
};
145+
};
146+
}
147+
).lspServices
148+
).toEqual([
149+
expect.objectContaining({ serverKind: "typescript", status: "installed" }),
150+
expect.objectContaining({ serverKind: "python", status: "prerequisite_missing" }),
151+
expect.objectContaining({ serverKind: "go", status: "not_installed" }),
152+
expect.objectContaining({ serverKind: "rust", status: "not_installed" }),
153+
expect.objectContaining({ serverKind: "vue", status: "not_installed" }),
154+
]);
155+
expect(
156+
(
157+
result.data as {
158+
metadata: {
159+
lspRuntimeContext?: {
160+
targetRuntime: "native" | "wsl";
161+
managedInstallSupported: boolean;
162+
};
163+
};
164+
}
165+
).metadata.lspRuntimeContext
166+
).toBeUndefined();
106167
});
107168

108169
it("surfaces missing provider CLI checks for session start diagnostics", async () => {
@@ -146,6 +207,119 @@ describe("diagnostics commands", () => {
146207
);
147208
});
148209

210+
it("surfaces latest failed LSP install state without affecting canContinue", async () => {
211+
const workspaceDir = await mkdtemp(join(tmpdir(), "diagnostics-lsp-runtime-"));
212+
const result = await dispatch(
213+
{
214+
kind: "command",
215+
id: "diag-session-lsp-install-failed",
216+
op: "diagnostics.get",
217+
args: {
218+
context: "session_start",
219+
workspaceId: "ws-1",
220+
providerId: "claude",
221+
},
222+
},
223+
createContext({
224+
workspaceMgr: {
225+
get: (workspaceId: string) =>
226+
workspaceId === "ws-1" ? { id: "ws-1", path: workspaceDir } : undefined,
227+
list: () => [],
228+
} as unknown as WorkspaceManager,
229+
lspToolMgr: {
230+
runtimeStatus: async ({ serverKind }: { serverKind: string }) =>
231+
({
232+
serverKind,
233+
displayName: `${serverKind} language server`,
234+
available: false,
235+
autoInstallSupported: true,
236+
installReadiness: "ready",
237+
missingCommands: [serverKind],
238+
missingPrerequisites: [],
239+
}) satisfies LspToolRuntimeStatusEntry,
240+
} as never,
241+
lspToolInstallMgr: {
242+
getLatestFailure: (serverKind: string) =>
243+
serverKind === "go"
244+
? {
245+
jobId: "job-go-failed",
246+
serverKind: "go",
247+
status: "failed",
248+
steps: [],
249+
failure: {
250+
code: "command_failed",
251+
serverKind: "go",
252+
message: "install failed",
253+
failedStepId: "install-go-lsp",
254+
command: "go",
255+
args: ["install"],
256+
missingCommands: [],
257+
},
258+
}
259+
: undefined,
260+
} as never,
261+
})
262+
);
263+
264+
expect(result.ok).toBe(true);
265+
expect(result.data).toMatchObject({
266+
context: "session_start",
267+
canContinue: true,
268+
metadata: {
269+
workspaceId: "ws-1",
270+
workspacePath: workspaceDir,
271+
providerId: "claude",
272+
lspRuntimeContext: {
273+
targetRuntime: "native",
274+
managedInstallSupported: true,
275+
},
276+
},
277+
});
278+
expect(
279+
(result.data as { lspServices: Array<{ serverKind: string; status: string }> }).lspServices
280+
).toEqual(
281+
expect.arrayContaining([
282+
expect.objectContaining({ serverKind: "go", status: "install_failed" }),
283+
])
284+
);
285+
expect((result.data as { checks: Array<{ code: string }> }).checks).not.toEqual(
286+
expect.arrayContaining([expect.objectContaining({ code: "lsp_install_failed" })])
287+
);
288+
});
289+
290+
it("reports runtime_off only when the global LSP runtime mode is off", async () => {
291+
const result = await dispatch(
292+
{
293+
kind: "command",
294+
id: "diag-session-lsp-runtime-off",
295+
op: "diagnostics.get",
296+
args: {
297+
context: "session_start",
298+
workspaceId: "ws-1",
299+
providerId: "claude",
300+
},
301+
},
302+
createContext({
303+
lspMgr: {
304+
getRuntimeMode: () => "off",
305+
} as never,
306+
})
307+
);
308+
309+
expect(result.ok).toBe(true);
310+
expect(
311+
(result.data as { lspServices: Array<{ serverKind: string; status: string }> }).lspServices
312+
).toEqual(
313+
expect.arrayContaining([
314+
expect.objectContaining({ serverKind: "typescript", status: "runtime_off" }),
315+
expect.objectContaining({ serverKind: "python", status: "runtime_off" }),
316+
expect.objectContaining({ serverKind: "go", status: "runtime_off" }),
317+
expect.objectContaining({ serverKind: "rust", status: "runtime_off" }),
318+
expect.objectContaining({ serverKind: "vue", status: "runtime_off" }),
319+
])
320+
);
321+
});
322+
149323
it("blocks session start when node is missing but keeps workspace-open non-blocking", async () => {
150324
const workspaceDir = await mkdtemp(join(tmpdir(), "diagnostics-base-runtime-"));
151325
const nodeMissingContext = createContext({

packages/server/src/__tests__/monitoring/aggregation.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,64 @@ describe("buildMonitoringSnapshot", () => {
129129
expect(response.snapshot.workspaces[0]?.label).toBe("ws_1779980247607_u2lfvdjf");
130130
});
131131

132+
it("keeps workspace-scoped background roots under the workspace attribution tree", () => {
133+
const response = buildMonitoringSnapshot({
134+
settings: {
135+
...createDefaultMonitoringSettings(),
136+
enabled: true,
137+
},
138+
sampledAt: 100,
139+
host: null,
140+
roots: [
141+
{
142+
ownerId: "lsp:ws-1:typescript",
143+
rootPid: 200,
144+
kind: "lsp",
145+
label: "TypeScript language server",
146+
workspaceId: "ws-1",
147+
startedAt: 2,
148+
},
149+
],
150+
workspaceLabels: {
151+
"ws-1": "coder-studio",
152+
},
153+
processRows: [
154+
{
155+
pid: 200,
156+
ppid: 1,
157+
cpuPercent: 7,
158+
rssBytes: 150,
159+
elapsedSec: 45,
160+
command: "typescript-language-server",
161+
},
162+
],
163+
previousSnapshot: null,
164+
});
165+
166+
expect(response.snapshot.workspaces).toEqual([
167+
expect.objectContaining({
168+
id: "workspace:ws-1",
169+
kind: "workspace",
170+
label: "coder-studio",
171+
cpuPercent: 7,
172+
memoryBytes: 150,
173+
processCount: 1,
174+
}),
175+
]);
176+
expect(response.snapshot.backgroundGroups).toEqual([
177+
expect.objectContaining({
178+
id: "background:lsp:ws-1:typescript",
179+
kind: "background_group",
180+
parentId: "workspace:ws-1",
181+
workspaceId: "ws-1",
182+
label: "TypeScript language server",
183+
cpuPercent: 7,
184+
memoryBytes: 150,
185+
processCount: 1,
186+
}),
187+
]);
188+
});
189+
132190
it("keeps host data when process collection fails", () => {
133191
const response = buildMonitoringSnapshot({
134192
settings: {

packages/server/src/__tests__/monitoring/service.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,4 +520,77 @@ describe("MonitoringService", () => {
520520

521521
expect(registry.listRoots().map((root) => root.ownerId)).not.toContain("terminal:term-1");
522522
});
523+
524+
it("includes registered lsp roots in background groups", async () => {
525+
const registry = new ManagedProcessRegistry({ now: () => 10 });
526+
registry.registerBackgroundRoot({
527+
ownerId: "lsp:ws-1:typescript",
528+
rootPid: 200,
529+
kind: "lsp",
530+
label: "TypeScript language server",
531+
startedAt: 10,
532+
});
533+
534+
const service = new MonitoringService({
535+
broadcaster: { broadcast: vi.fn() },
536+
settingsRepo: {
537+
get: (key: string) => {
538+
const settings = {
539+
"monitoring.enabled": true,
540+
"monitoring.hostMetricsEnabled": true,
541+
"monitoring.runtimeSummaryEnabled": true,
542+
"monitoring.workspaceAttributionEnabled": true,
543+
"monitoring.subprocessDrilldownEnabled": false,
544+
"monitoring.sampleIntervalMs": 2000,
545+
} as Record<string, unknown>;
546+
return settings[key];
547+
},
548+
},
549+
registry,
550+
sessionMgr: { getAll: () => [], findSessionIdByTerminal: () => undefined },
551+
terminalMgr: { getAll: () => [] },
552+
hostCollector: {
553+
collect: () => ({
554+
cpuPercent: 30,
555+
memoryUsedBytes: 300,
556+
memoryTotalBytes: 1000,
557+
memoryAvailableBytes: 700,
558+
loadAverage: [0.3, 0.2, 0.1],
559+
uptimeSec: 10,
560+
pressure: "normal",
561+
}),
562+
},
563+
processCollector: {
564+
collect: async () => [
565+
{
566+
pid: 200,
567+
ppid: 1,
568+
cpuPercent: 7,
569+
rssBytes: 150,
570+
elapsedSec: 12,
571+
command: "typescript-language-server --stdio",
572+
},
573+
],
574+
},
575+
setInterval: vi.fn(() => ({ unref: vi.fn() })),
576+
clearInterval: vi.fn(),
577+
now: () => 10,
578+
});
579+
580+
service.start();
581+
const response = await service.recheck();
582+
583+
expect(response.snapshot.backgroundGroups).toEqual(
584+
expect.arrayContaining([
585+
expect.objectContaining({
586+
id: "background:lsp:ws-1:typescript",
587+
kind: "background_group",
588+
label: "TypeScript language server",
589+
cpuPercent: 7,
590+
memoryBytes: 150,
591+
processCount: 1,
592+
}),
593+
])
594+
);
595+
});
523596
});

0 commit comments

Comments
 (0)