Skip to content

Commit 72e01b2

Browse files
jaggederestclaude
andcommitted
test: add comprehensive tests for commands.ts
- Create 12 tests covering Commands class methods - Test workspace operations (openFromSidebar, open, openDevContainer) - Test basic functionality (login, logout, viewLogs) - Test error handling scenarios - Improve commands.ts coverage from ~30% to 56.01% - All 149 tests now passing across the test suite 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 2a166aa commit 72e01b2

File tree

1 file changed

+398
-0
lines changed

1 file changed

+398
-0
lines changed

src/commands.test.ts

Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import * as vscode from "vscode"
3+
import { Commands } from "./commands"
4+
import { Storage } from "./storage"
5+
import { Api } from "coder/site/src/api/api"
6+
import { User, Workspace } from "coder/site/src/api/typesGenerated"
7+
import * as apiModule from "./api"
8+
import { CertificateError } from "./error"
9+
import { getErrorMessage } from "coder/site/src/api/errors"
10+
11+
// Mock vscode module
12+
vi.mock("vscode", () => ({
13+
commands: {
14+
executeCommand: vi.fn(),
15+
},
16+
window: {
17+
showInputBox: vi.fn(),
18+
showErrorMessage: vi.fn(),
19+
showInformationMessage: vi.fn().mockResolvedValue(undefined),
20+
createQuickPick: vi.fn(),
21+
showQuickPick: vi.fn(),
22+
createTerminal: vi.fn(),
23+
withProgress: vi.fn(),
24+
showTextDocument: vi.fn(),
25+
},
26+
workspace: {
27+
getConfiguration: vi.fn(),
28+
openTextDocument: vi.fn(),
29+
workspaceFolders: [],
30+
},
31+
Uri: {
32+
parse: vi.fn().mockReturnValue({ toString: () => "parsed-uri" }),
33+
file: vi.fn().mockReturnValue({ toString: () => "file-uri" }),
34+
from: vi.fn().mockImplementation((options: any) => ({
35+
scheme: options.scheme,
36+
authority: options.authority,
37+
path: options.path,
38+
toString: () => `${options.scheme}://${options.authority}${options.path}`,
39+
})),
40+
},
41+
env: {
42+
openExternal: vi.fn().mockResolvedValue(undefined),
43+
},
44+
ProgressLocation: {
45+
Notification: 15,
46+
},
47+
InputBoxValidationSeverity: {
48+
Error: 3,
49+
},
50+
}))
51+
52+
// Mock dependencies
53+
vi.mock("./api", () => ({
54+
makeCoderSdk: vi.fn(),
55+
needToken: vi.fn(),
56+
}))
57+
58+
vi.mock("./error", () => ({
59+
CertificateError: vi.fn(),
60+
}))
61+
62+
vi.mock("coder/site/src/api/errors", () => ({
63+
getErrorMessage: vi.fn(),
64+
}))
65+
66+
vi.mock("./storage", () => ({
67+
Storage: vi.fn(),
68+
}))
69+
70+
vi.mock("./util", () => ({
71+
toRemoteAuthority: vi.fn((baseUrl: string, owner: string, name: string, agent?: string) => {
72+
const host = baseUrl.replace("https://", "").replace("http://", "")
73+
return `coder-${host}-${owner}-${name}${agent ? `-${agent}` : ""}`
74+
}),
75+
toSafeHost: vi.fn((url: string) => url.replace("https://", "").replace("http://", "")),
76+
}))
77+
78+
describe("Commands", () => {
79+
let commands: Commands
80+
let mockVscodeProposed: typeof vscode
81+
let mockRestClient: Api
82+
let mockStorage: Storage
83+
let mockQuickPick: any
84+
let mockTerminal: any
85+
86+
beforeEach(() => {
87+
vi.clearAllMocks()
88+
89+
mockVscodeProposed = vscode as any
90+
91+
mockRestClient = {
92+
setHost: vi.fn(),
93+
setSessionToken: vi.fn(),
94+
getAuthenticatedUser: vi.fn(),
95+
getWorkspaces: vi.fn(),
96+
updateWorkspaceVersion: vi.fn(),
97+
getAxiosInstance: vi.fn(() => ({
98+
defaults: {
99+
baseURL: "https://coder.example.com",
100+
},
101+
})),
102+
} as any
103+
104+
mockStorage = {
105+
getUrl: vi.fn(() => "https://coder.example.com"),
106+
setUrl: vi.fn(),
107+
getSessionToken: vi.fn(),
108+
setSessionToken: vi.fn(),
109+
configureCli: vi.fn(),
110+
withUrlHistory: vi.fn(() => ["https://coder.example.com"]),
111+
fetchBinary: vi.fn(),
112+
getSessionTokenPath: vi.fn(),
113+
writeToCoderOutputChannel: vi.fn(),
114+
} as any
115+
116+
mockQuickPick = {
117+
value: "",
118+
placeholder: "",
119+
title: "",
120+
items: [],
121+
busy: false,
122+
show: vi.fn(),
123+
dispose: vi.fn(),
124+
onDidHide: vi.fn(),
125+
onDidChangeValue: vi.fn(),
126+
onDidChangeSelection: vi.fn(),
127+
}
128+
129+
mockTerminal = {
130+
sendText: vi.fn(),
131+
show: vi.fn(),
132+
}
133+
134+
vi.mocked(vscode.window.createQuickPick).mockReturnValue(mockQuickPick)
135+
vi.mocked(vscode.window.createTerminal).mockReturnValue(mockTerminal)
136+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
137+
get: vi.fn(() => ""),
138+
} as any)
139+
140+
// Default mock for vscode.commands.executeCommand
141+
vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command: string) => {
142+
if (command === "_workbench.getRecentlyOpened") {
143+
return { workspaces: [] }
144+
}
145+
return undefined
146+
})
147+
148+
commands = new Commands(mockVscodeProposed, mockRestClient, mockStorage)
149+
})
150+
151+
describe("basic Commands functionality", () => {
152+
const mockUser: User = {
153+
id: "user-1",
154+
username: "testuser",
155+
roles: [{ name: "owner" }],
156+
} as User
157+
158+
beforeEach(() => {
159+
vi.mocked(apiModule.makeCoderSdk).mockResolvedValue(mockRestClient)
160+
vi.mocked(apiModule.needToken).mockReturnValue(true)
161+
vi.mocked(mockRestClient.getAuthenticatedUser).mockResolvedValue(mockUser)
162+
vi.mocked(getErrorMessage).mockReturnValue("Test error")
163+
})
164+
165+
it("should login with provided URL and token", async () => {
166+
vi.mocked(vscode.window.showInputBox).mockImplementation(async (options: any) => {
167+
if (options.validateInput) {
168+
await options.validateInput("test-token")
169+
}
170+
return "test-token"
171+
})
172+
vi.mocked(vscode.window.showInformationMessage).mockResolvedValue(undefined)
173+
vi.mocked(vscode.env.openExternal).mockResolvedValue(true)
174+
175+
await commands.login("https://coder.example.com", "test-token")
176+
177+
expect(mockRestClient.setHost).toHaveBeenCalledWith("https://coder.example.com")
178+
expect(mockRestClient.setSessionToken).toHaveBeenCalledWith("test-token")
179+
})
180+
181+
it("should logout successfully", async () => {
182+
vi.mocked(vscode.window.showInformationMessage).mockResolvedValue(undefined)
183+
184+
await commands.logout()
185+
186+
expect(mockRestClient.setHost).toHaveBeenCalledWith("")
187+
expect(mockRestClient.setSessionToken).toHaveBeenCalledWith("")
188+
})
189+
190+
it("should view logs when path is set", async () => {
191+
const logPath = "/tmp/workspace.log"
192+
const mockUri = { toString: () => `file://${logPath}` }
193+
const mockDoc = { fileName: logPath }
194+
195+
commands.workspaceLogPath = logPath
196+
vi.mocked(vscode.Uri.file).mockReturnValue(mockUri as any)
197+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDoc as any)
198+
199+
await commands.viewLogs()
200+
201+
expect(vscode.Uri.file).toHaveBeenCalledWith(logPath)
202+
expect(vscode.workspace.openTextDocument).toHaveBeenCalledWith(mockUri)
203+
})
204+
})
205+
206+
describe("workspace operations", () => {
207+
const mockTreeItem = {
208+
workspaceOwner: "testuser",
209+
workspaceName: "testworkspace",
210+
workspaceAgent: "main",
211+
workspaceFolderPath: "/workspace",
212+
}
213+
214+
it("should open workspace from sidebar", async () => {
215+
await commands.openFromSidebar(mockTreeItem as any)
216+
217+
// Should call _workbench.getRecentlyOpened first, then vscode.openFolder
218+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith("_workbench.getRecentlyOpened")
219+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
220+
"vscode.openFolder",
221+
expect.objectContaining({
222+
scheme: "vscode-remote",
223+
path: "/workspace",
224+
}),
225+
false // newWindow is false when no workspace folders exist
226+
)
227+
})
228+
229+
it("should open workspace with direct arguments", async () => {
230+
await commands.open("testuser", "testworkspace", undefined, "/custom/path", false)
231+
232+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
233+
"vscode.openFolder",
234+
expect.objectContaining({
235+
scheme: "vscode-remote",
236+
path: "/custom/path",
237+
}),
238+
false
239+
)
240+
})
241+
242+
it("should open dev container", async () => {
243+
await commands.openDevContainer("testuser", "testworkspace", undefined, "mycontainer", "/container/path")
244+
245+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
246+
"vscode.openFolder",
247+
expect.objectContaining({
248+
scheme: "vscode-remote",
249+
authority: expect.stringContaining("attached-container+"),
250+
path: "/container/path",
251+
}),
252+
false
253+
)
254+
})
255+
256+
it("should use first recent workspace when openRecent=true with multiple workspaces", async () => {
257+
const recentWorkspaces = {
258+
workspaces: [
259+
{
260+
folderUri: {
261+
authority: "coder-coder.example.com-testuser-testworkspace-main",
262+
path: "/recent/path1",
263+
},
264+
},
265+
{
266+
folderUri: {
267+
authority: "coder-coder.example.com-testuser-testworkspace-main",
268+
path: "/recent/path2",
269+
},
270+
},
271+
],
272+
}
273+
274+
vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command: string) => {
275+
if (command === "_workbench.getRecentlyOpened") {
276+
return recentWorkspaces
277+
}
278+
return undefined
279+
})
280+
281+
const treeItemWithoutPath = {
282+
...mockTreeItem,
283+
workspaceFolderPath: undefined,
284+
}
285+
286+
await commands.openFromSidebar(treeItemWithoutPath as any)
287+
288+
// openFromSidebar passes openRecent=true, so with multiple recent workspaces it should use the first one
289+
expect(vscode.window.showQuickPick).not.toHaveBeenCalled()
290+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
291+
"vscode.openFolder",
292+
expect.objectContaining({
293+
scheme: "vscode-remote",
294+
path: "/recent/path1",
295+
}),
296+
false
297+
)
298+
})
299+
300+
it("should use single recent workspace automatically", async () => {
301+
const recentWorkspaces = {
302+
workspaces: [
303+
{
304+
folderUri: {
305+
authority: "coder-coder.example.com-testuser-testworkspace-main",
306+
path: "/recent/single",
307+
},
308+
},
309+
],
310+
}
311+
312+
vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command: string) => {
313+
if (command === "_workbench.getRecentlyOpened") {
314+
return recentWorkspaces
315+
}
316+
return undefined
317+
})
318+
319+
const treeItemWithoutPath = {
320+
...mockTreeItem,
321+
workspaceFolderPath: undefined,
322+
}
323+
324+
await commands.openFromSidebar(treeItemWithoutPath as any)
325+
326+
expect(vscode.window.showQuickPick).not.toHaveBeenCalled()
327+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
328+
"vscode.openFolder",
329+
expect.objectContaining({
330+
path: "/recent/single",
331+
}),
332+
false
333+
)
334+
})
335+
336+
it("should open new window when no folder path available", async () => {
337+
const recentWorkspaces = { workspaces: [] }
338+
339+
vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command: string) => {
340+
if (command === "_workbench.getRecentlyOpened") {
341+
return recentWorkspaces
342+
}
343+
return undefined
344+
})
345+
346+
const treeItemWithoutPath = {
347+
...mockTreeItem,
348+
workspaceFolderPath: undefined,
349+
}
350+
351+
await commands.openFromSidebar(treeItemWithoutPath as any)
352+
353+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith("vscode.newWindow", {
354+
remoteAuthority: "coder-coder.example.com-testuser-testworkspace-main",
355+
reuseWindow: true,
356+
})
357+
})
358+
359+
it("should use new window when workspace folders exist", async () => {
360+
vi.mocked(vscode.workspace).workspaceFolders = [{ uri: { path: "/existing" } }] as any
361+
362+
await commands.openDevContainer("testuser", "testworkspace", undefined, "mycontainer", "/container/path")
363+
364+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
365+
"vscode.openFolder",
366+
expect.anything(),
367+
true
368+
)
369+
})
370+
371+
})
372+
373+
describe("error handling", () => {
374+
it("should throw error if not logged in for openFromSidebar", async () => {
375+
vi.mocked(mockRestClient.getAxiosInstance).mockReturnValue({
376+
defaults: { baseURL: undefined },
377+
} as any)
378+
379+
const mockTreeItem = {
380+
workspaceOwner: "testuser",
381+
workspaceName: "testworkspace",
382+
}
383+
384+
await expect(commands.openFromSidebar(mockTreeItem as any)).rejects.toThrow(
385+
"You are not logged in"
386+
)
387+
})
388+
389+
it("should call open() method when no tree item provided to openFromSidebar", async () => {
390+
const openSpy = vi.spyOn(commands, "open").mockResolvedValue()
391+
392+
await commands.openFromSidebar(null as any)
393+
394+
expect(openSpy).toHaveBeenCalled()
395+
openSpy.mockRestore()
396+
})
397+
})
398+
})

0 commit comments

Comments
 (0)