Skip to content

Commit 1afefc5

Browse files
jaggederestclaude
andcommitted
test: add comprehensive tests for inbox.ts and workspaceMonitor.ts
- Add 14 test cases for inbox.ts covering WebSocket connection, event handling, and disposal - Add 19 test cases for workspaceMonitor.ts covering SSE monitoring, notifications, and status bar updates - Test WebSocket setup with proper URL construction and authentication headers - Test EventSource setup for workspace monitoring with data/error event handling - Test notification logic for autostop, deletion, outdated workspace, and non-running states - Test status bar updates and context management - Test proper cleanup and disposal patterns - Achieve comprehensive coverage for message handling and workspace monitoring functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 01246a1 commit 1afefc5

File tree

2 files changed

+773
-0
lines changed

2 files changed

+773
-0
lines changed

src/inbox.test.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import * as vscode from "vscode"
3+
import { Inbox } from "./inbox"
4+
import { Api } from "coder/site/src/api/api"
5+
import { Workspace } from "coder/site/src/api/typesGenerated"
6+
import { ProxyAgent } from "proxy-agent"
7+
import { WebSocket } from "ws"
8+
import { Storage } from "./storage"
9+
10+
// Mock external dependencies
11+
vi.mock("vscode", () => ({
12+
window: {
13+
showInformationMessage: vi.fn(),
14+
},
15+
}))
16+
17+
vi.mock("ws", () => ({
18+
WebSocket: vi.fn(),
19+
}))
20+
21+
vi.mock("proxy-agent", () => ({
22+
ProxyAgent: vi.fn(),
23+
}))
24+
25+
vi.mock("./api", () => ({
26+
coderSessionTokenHeader: "Coder-Session-Token",
27+
}))
28+
29+
vi.mock("./api-helper", () => ({
30+
errToStr: vi.fn(),
31+
}))
32+
33+
describe("Inbox", () => {
34+
let mockWorkspace: Workspace
35+
let mockHttpAgent: ProxyAgent
36+
let mockRestClient: Api
37+
let mockStorage: Storage
38+
let mockSocket: any
39+
let inbox: Inbox
40+
41+
beforeEach(async () => {
42+
vi.clearAllMocks()
43+
44+
// Setup mock workspace
45+
mockWorkspace = {
46+
id: "workspace-1",
47+
name: "test-workspace",
48+
owner_name: "testuser",
49+
} as Workspace
50+
51+
// Setup mock HTTP agent
52+
mockHttpAgent = {} as ProxyAgent
53+
54+
// Setup mock socket
55+
mockSocket = {
56+
on: vi.fn(),
57+
close: vi.fn(),
58+
}
59+
vi.mocked(WebSocket).mockReturnValue(mockSocket)
60+
61+
// Setup mock REST client
62+
mockRestClient = {
63+
getAxiosInstance: vi.fn(() => ({
64+
defaults: {
65+
baseURL: "https://coder.example.com",
66+
headers: {
67+
common: {
68+
"Coder-Session-Token": "test-token",
69+
},
70+
},
71+
},
72+
})),
73+
} as any
74+
75+
// Setup mock storage
76+
mockStorage = {
77+
writeToCoderOutputChannel: vi.fn(),
78+
} as any
79+
80+
// Setup errToStr mock
81+
const apiHelper = await import("./api-helper")
82+
vi.mocked(apiHelper.errToStr).mockReturnValue("Mock error message")
83+
})
84+
85+
afterEach(() => {
86+
if (inbox) {
87+
inbox.dispose()
88+
}
89+
})
90+
91+
describe("constructor", () => {
92+
it("should create WebSocket connection with correct URL and headers", () => {
93+
inbox = new Inbox(mockWorkspace, mockHttpAgent, mockRestClient, mockStorage)
94+
95+
expect(WebSocket).toHaveBeenCalledWith(
96+
expect.any(URL),
97+
{
98+
agent: mockHttpAgent,
99+
followRedirects: true,
100+
headers: {
101+
"Coder-Session-Token": "test-token",
102+
},
103+
}
104+
)
105+
106+
// Verify the WebSocket URL is constructed correctly
107+
const websocketCall = vi.mocked(WebSocket).mock.calls[0]
108+
const websocketUrl = websocketCall[0] as URL
109+
expect(websocketUrl.protocol).toBe("wss:")
110+
expect(websocketUrl.host).toBe("coder.example.com")
111+
expect(websocketUrl.pathname).toBe("/api/v2/notifications/inbox/watch")
112+
expect(websocketUrl.searchParams.get("format")).toBe("plaintext")
113+
expect(websocketUrl.searchParams.get("templates")).toContain("a9d027b4-ac49-4fb1-9f6d-45af15f64e7a")
114+
expect(websocketUrl.searchParams.get("templates")).toContain("f047f6a3-5713-40f7-85aa-0394cce9fa3a")
115+
expect(websocketUrl.searchParams.get("targets")).toBe("workspace-1")
116+
})
117+
118+
it("should use ws protocol for http base URL", () => {
119+
mockRestClient.getAxiosInstance = vi.fn(() => ({
120+
defaults: {
121+
baseURL: "http://coder.example.com",
122+
headers: {
123+
common: {
124+
"Coder-Session-Token": "test-token",
125+
},
126+
},
127+
},
128+
}))
129+
130+
inbox = new Inbox(mockWorkspace, mockHttpAgent, mockRestClient, mockStorage)
131+
132+
const websocketCall = vi.mocked(WebSocket).mock.calls[0]
133+
const websocketUrl = websocketCall[0] as URL
134+
expect(websocketUrl.protocol).toBe("ws:")
135+
})
136+
137+
it("should handle missing token in headers", () => {
138+
mockRestClient.getAxiosInstance = vi.fn(() => ({
139+
defaults: {
140+
baseURL: "https://coder.example.com",
141+
headers: {
142+
common: {},
143+
},
144+
},
145+
}))
146+
147+
inbox = new Inbox(mockWorkspace, mockHttpAgent, mockRestClient, mockStorage)
148+
149+
expect(WebSocket).toHaveBeenCalledWith(
150+
expect.any(URL),
151+
{
152+
agent: mockHttpAgent,
153+
followRedirects: true,
154+
headers: undefined,
155+
}
156+
)
157+
})
158+
159+
it("should throw error when no base URL is set", () => {
160+
mockRestClient.getAxiosInstance = vi.fn(() => ({
161+
defaults: {
162+
baseURL: undefined,
163+
headers: {
164+
common: {},
165+
},
166+
},
167+
}))
168+
169+
expect(() => {
170+
new Inbox(mockWorkspace, mockHttpAgent, mockRestClient, mockStorage)
171+
}).toThrow("No base URL set on REST client")
172+
})
173+
174+
it("should register socket event handlers", () => {
175+
inbox = new Inbox(mockWorkspace, mockHttpAgent, mockRestClient, mockStorage)
176+
177+
expect(mockSocket.on).toHaveBeenCalledWith("open", expect.any(Function))
178+
expect(mockSocket.on).toHaveBeenCalledWith("error", expect.any(Function))
179+
expect(mockSocket.on).toHaveBeenCalledWith("message", expect.any(Function))
180+
})
181+
})
182+
183+
describe("socket event handlers", () => {
184+
beforeEach(() => {
185+
inbox = new Inbox(mockWorkspace, mockHttpAgent, mockRestClient, mockStorage)
186+
})
187+
188+
it("should handle socket open event", () => {
189+
const openHandler = mockSocket.on.mock.calls.find(call => call[0] === "open")?.[1]
190+
expect(openHandler).toBeDefined()
191+
192+
openHandler()
193+
194+
expect(mockStorage.writeToCoderOutputChannel).toHaveBeenCalledWith(
195+
"Listening to Coder Inbox"
196+
)
197+
})
198+
199+
it("should handle socket error event", () => {
200+
const errorHandler = mockSocket.on.mock.calls.find(call => call[0] === "error")?.[1]
201+
expect(errorHandler).toBeDefined()
202+
203+
const mockError = new Error("Socket error")
204+
const disposeSpy = vi.spyOn(inbox, "dispose")
205+
206+
errorHandler(mockError)
207+
208+
expect(mockStorage.writeToCoderOutputChannel).toHaveBeenCalledWith("Mock error message")
209+
expect(disposeSpy).toHaveBeenCalled()
210+
})
211+
212+
it("should handle valid socket message", () => {
213+
const messageHandler = mockSocket.on.mock.calls.find(call => call[0] === "message")?.[1]
214+
expect(messageHandler).toBeDefined()
215+
216+
const mockMessage = {
217+
notification: {
218+
title: "Test notification",
219+
},
220+
}
221+
const messageData = Buffer.from(JSON.stringify(mockMessage))
222+
223+
messageHandler(messageData)
224+
225+
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("Test notification")
226+
})
227+
228+
it("should handle invalid JSON in socket message", () => {
229+
const messageHandler = mockSocket.on.mock.calls.find(call => call[0] === "message")?.[1]
230+
expect(messageHandler).toBeDefined()
231+
232+
const invalidData = Buffer.from("invalid json")
233+
234+
messageHandler(invalidData)
235+
236+
expect(mockStorage.writeToCoderOutputChannel).toHaveBeenCalledWith("Mock error message")
237+
})
238+
239+
it("should handle message parsing errors", () => {
240+
const messageHandler = mockSocket.on.mock.calls.find(call => call[0] === "message")?.[1]
241+
expect(messageHandler).toBeDefined()
242+
243+
const mockMessage = {
244+
// Missing required notification structure
245+
}
246+
const messageData = Buffer.from(JSON.stringify(mockMessage))
247+
248+
messageHandler(messageData)
249+
250+
// Should not throw, but may not show notification if structure is wrong
251+
// The test verifies that error handling doesn't crash the application
252+
})
253+
})
254+
255+
describe("dispose", () => {
256+
beforeEach(() => {
257+
inbox = new Inbox(mockWorkspace, mockHttpAgent, mockRestClient, mockStorage)
258+
})
259+
260+
it("should close socket and log when disposed", () => {
261+
inbox.dispose()
262+
263+
expect(mockStorage.writeToCoderOutputChannel).toHaveBeenCalledWith(
264+
"No longer listening to Coder Inbox"
265+
)
266+
expect(mockSocket.close).toHaveBeenCalled()
267+
})
268+
269+
it("should handle multiple dispose calls safely", () => {
270+
inbox.dispose()
271+
inbox.dispose()
272+
273+
// Should only log and close once
274+
expect(mockStorage.writeToCoderOutputChannel).toHaveBeenCalledTimes(1)
275+
expect(mockSocket.close).toHaveBeenCalledTimes(1)
276+
})
277+
})
278+
279+
describe("template constants", () => {
280+
it("should include workspace out of memory template", () => {
281+
inbox = new Inbox(mockWorkspace, mockHttpAgent, mockRestClient, mockStorage)
282+
283+
const websocketCall = vi.mocked(WebSocket).mock.calls[0]
284+
const websocketUrl = websocketCall[0] as URL
285+
const templates = websocketUrl.searchParams.get("templates")
286+
287+
expect(templates).toContain("a9d027b4-ac49-4fb1-9f6d-45af15f64e7a")
288+
})
289+
290+
it("should include workspace out of disk template", () => {
291+
inbox = new Inbox(mockWorkspace, mockHttpAgent, mockRestClient, mockStorage)
292+
293+
const websocketCall = vi.mocked(WebSocket).mock.calls[0]
294+
const websocketUrl = websocketCall[0] as URL
295+
const templates = websocketUrl.searchParams.get("templates")
296+
297+
expect(templates).toContain("f047f6a3-5713-40f7-85aa-0394cce9fa3a")
298+
})
299+
})
300+
})

0 commit comments

Comments
 (0)