Skip to content

Commit 51b26b4

Browse files
authored
Upgrade breaking dependencies (#709)
- Upgrade eventsource to v4 and update types (FetchLikeInit → EventSourceFetchInit) - Update ua-parser-js to 1.0.41 and pin to ^1.x to avoid AGPL license in v2 - Upgrade date-fns 3.6.0 → 4.1.0 - Upgrade openpgp 6.2.2 → 6.3.0 - Upgrade zod 4.1.12 → 4.3.2 - Update vitest and @vitest/coverage-v8 to 4.0.16 - Remove unused packages: glob, nyc, @vscode/test-electron, vscode-test
1 parent dde77f3 commit 51b26b4

File tree

9 files changed

+438
-1306
lines changed

9 files changed

+438
-1306
lines changed

CLAUDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ Comments explain what code does or why it exists:
2828
- Never mock behavior in end-to-end tests - use real data
2929
- Mock as little as possible in unit tests - try to use real data
3030
- Find root causes, not symptoms. Read error messages carefully before attempting fixes.
31+
- When mocking constructors (classes) with `vi.mocked(...).mockImplementation()`, use regular functions, not arrow functions. Arrow functions can't be called with `new`.
32+
```typescript
33+
// ✗ Wrong
34+
vi.mocked(SomeClass).mockImplementation(() => mock);
35+
// ✓ Correct
36+
vi.mocked(SomeClass).mockImplementation(function () {
37+
return mock;
38+
});
39+
```
3140

3241
## Version Control
3342

CONTRIBUTING.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,6 @@ Some dependencies are not directly used in the source but are required anyway.
122122

123123
- `bufferutil` and `utf-8-validate` are peer dependencies of `ws`.
124124
- `ua-parser-js` and `dayjs` are used by the Coder API client.
125-
- `glob`, `nyc`, `vscode-test`, and `@vscode/test-electron` are currently unused
126-
but we need to switch back to them from `vitest`.
127125

128126
The coder client is vendored from coder/coder. Every now and then, we should be running `yarn upgrade coder --latest`
129127
to make sure we're using up to date versions of the client.

package.json

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -404,33 +404,31 @@
404404
"dependencies": {
405405
"@peculiar/x509": "^1.14.2",
406406
"axios": "1.13.2",
407-
"date-fns": "^3.6.0",
408-
"eventsource": "^3.0.6",
407+
"date-fns": "^4.1.0",
408+
"eventsource": "^4.1.0",
409409
"find-process": "^2.0.0",
410410
"jsonc-parser": "^3.3.1",
411-
"openpgp": "^6.2.2",
411+
"openpgp": "^6.3.0",
412412
"pretty-bytes": "^7.1.0",
413413
"proper-lockfile": "^4.1.2",
414414
"proxy-agent": "^6.5.0",
415415
"semver": "^7.7.3",
416-
"ua-parser-js": "1.0.40",
416+
"ua-parser-js": "^1.0.41",
417417
"ws": "^8.18.3",
418-
"zod": "^4.1.12"
418+
"zod": "^4.3.2"
419419
},
420420
"devDependencies": {
421421
"@types/eventsource": "^3.0.0",
422-
"@types/glob": "^7.1.3",
423422
"@types/node": "^22.14.1",
424423
"@types/proper-lockfile": "^4.1.4",
425424
"@types/semver": "^7.7.1",
426-
"@types/ua-parser-js": "0.7.36",
425+
"@types/ua-parser-js": "0.7.39",
427426
"@types/vscode": "^1.73.0",
428427
"@types/ws": "^8.18.1",
429428
"@typescript-eslint/eslint-plugin": "^8.49.0",
430429
"@typescript-eslint/parser": "^8.50.1",
431-
"@vitest/coverage-v8": "^3.2.4",
430+
"@vitest/coverage-v8": "^4.0.16",
432431
"@vscode/test-cli": "^0.0.12",
433-
"@vscode/test-electron": "^2.5.2",
434432
"@vscode/vsce": "^3.7.1",
435433
"bufferutil": "^4.0.9",
436434
"coder": "https://github.com/coder/coder#main",
@@ -443,17 +441,14 @@
443441
"eslint-plugin-md": "^1.0.19",
444442
"eslint-plugin-package-json": "^0.85.0",
445443
"eslint-plugin-prettier": "^5.5.4",
446-
"glob": "^13.0.0",
447444
"jsonc-eslint-parser": "^2.4.2",
448445
"markdown-eslint-parser": "^1.2.1",
449446
"memfs": "^4.51.1",
450-
"nyc": "^17.1.0",
451447
"prettier": "^3.7.4",
452448
"ts-loader": "^9.5.4",
453449
"typescript": "^5.9.3",
454450
"utf-8-validate": "^6.0.6",
455-
"vitest": "^3.2.4",
456-
"vscode-test": "^1.5.0",
451+
"vitest": "^4.0.16",
457452
"webpack": "^5.104.1",
458453
"webpack-cli": "^6.0.1"
459454
},

src/api/coderApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export class CoderApi extends Api implements vscode.Disposable {
354354
} catch (error) {
355355
if (this.is404Error(error)) {
356356
this.output.warn(
357-
`WebSocket failed, using SSE fallback: ${socketConfigs.apiRoute}`,
357+
`WebSocket failed (${socketConfigs.apiRoute}), using SSE fallback: ${fallbackApiRoute}`,
358358
);
359359
const sse = this.createSseConnection(
360360
fallbackApiRoute,

src/api/streamingFetchAdapter.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type AxiosInstance } from "axios";
2-
import { type FetchLikeInit, type FetchLikeResponse } from "eventsource";
2+
import { type EventSourceFetchInit, type FetchLikeResponse } from "eventsource";
33
import { type IncomingMessage } from "node:http";
44

55
/**
@@ -9,10 +9,13 @@ import { type IncomingMessage } from "node:http";
99
export function createStreamingFetchAdapter(
1010
axiosInstance: AxiosInstance,
1111
configHeaders?: Record<string, string>,
12-
): (url: string | URL, init?: FetchLikeInit) => Promise<FetchLikeResponse> {
12+
): (
13+
url: string | URL,
14+
init?: EventSourceFetchInit,
15+
) => Promise<FetchLikeResponse> {
1316
return async (
1417
url: string | URL,
15-
init?: FetchLikeInit,
18+
init?: EventSourceFetchInit,
1619
): Promise<FetchLikeResponse> => {
1720
const urlStr = url.toString();
1821

test/unit/api/coderApi.test.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ describe("CoderApi", () => {
342342

343343
it("falls back to SSE when WebSocket creation fails with 404", async () => {
344344
// Only 404 errors trigger SSE fallback - other errors are thrown
345-
vi.mocked(Ws).mockImplementation(() => {
345+
vi.mocked(Ws).mockImplementation(function () {
346346
throw new Error("Unexpected server response: 404");
347347
});
348348

@@ -380,7 +380,7 @@ describe("CoderApi", () => {
380380
});
381381

382382
it("throws non-404 errors without SSE fallback", async () => {
383-
vi.mocked(Ws).mockImplementation(() => {
383+
vi.mocked(Ws).mockImplementation(function () {
384384
throw new Error("Network error");
385385
});
386386

@@ -398,7 +398,7 @@ describe("CoderApi", () => {
398398
let wsAttempts = 0;
399399
const mockEventSources: MockEventSource[] = [];
400400

401-
vi.mocked(Ws).mockImplementation(() => {
401+
vi.mocked(Ws).mockImplementation(function () {
402402
wsAttempts++;
403403
const mockWs = createMockWebSocket("wss://test", {
404404
on: vi.fn((event: string, handler: (e: unknown) => void) => {
@@ -413,7 +413,7 @@ describe("CoderApi", () => {
413413
return mockWs as Ws;
414414
});
415415

416-
vi.mocked(EventSource).mockImplementation(() => {
416+
vi.mocked(EventSource).mockImplementation(function () {
417417
const es = createMockEventSource(`${CODER_URL}/api/v2/test`);
418418
mockEventSources.push(es);
419419
return es as unknown as EventSource;
@@ -436,7 +436,7 @@ describe("CoderApi", () => {
436436

437437
const setupAutoOpeningWebSocket = () => {
438438
const sockets: Array<Partial<Ws>> = [];
439-
vi.mocked(Ws).mockImplementation((url: string | URL) => {
439+
vi.mocked(Ws).mockImplementation(function (url: string | URL) {
440440
const mockWs = createMockWebSocket(String(url), {
441441
on: vi.fn((event, handler) => {
442442
if (event === "open") {
@@ -575,7 +575,7 @@ describe("CoderApi", () => {
575575
describe("dispose", () => {
576576
it("disposes all tracked reconnecting sockets", async () => {
577577
const sockets: Array<Partial<Ws>> = [];
578-
vi.mocked(Ws).mockImplementation((url: string | URL) => {
578+
vi.mocked(Ws).mockImplementation(function (url: string | URL) {
579579
const mockWs = createMockWebSocket(String(url), {
580580
on: vi.fn((event, handler) => {
581581
if (event === "open") {
@@ -701,9 +701,13 @@ function createMockEventSource(url: string): MockEventSource {
701701
}
702702

703703
function setupWebSocketMock(ws: Partial<Ws>): void {
704-
vi.mocked(Ws).mockImplementation(() => ws as Ws);
704+
vi.mocked(Ws).mockImplementation(function () {
705+
return ws as Ws;
706+
});
705707
}
706708

707709
function setupEventSourceMock(es: Partial<EventSource>): void {
708-
vi.mocked(EventSource).mockImplementation(() => es as EventSource);
710+
vi.mocked(EventSource).mockImplementation(function () {
711+
return es as EventSource;
712+
});
709713
}

test/unit/api/streamingFetchAdapter.test.ts

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type AxiosInstance, type AxiosResponse } from "axios";
2-
import { type ReaderLike } from "eventsource";
2+
import { type EventSourceFetchInit, type ReaderLike } from "eventsource";
33
import { EventEmitter } from "node:events";
44
import { type IncomingMessage } from "node:http";
55
import { describe, it, expect, vi } from "vitest";
@@ -8,6 +8,23 @@ import { createStreamingFetchAdapter } from "@/api/streamingFetchAdapter";
88

99
const TEST_URL = "https://example.com/api";
1010

11+
/**
12+
* Creates a valid EventSourceFetchInit object for testing.
13+
* In production, EventSource always provides all required fields.
14+
*/
15+
function createFetchInit(
16+
overrides: Partial<EventSourceFetchInit> = {},
17+
): EventSourceFetchInit {
18+
return {
19+
signal: new AbortController().signal,
20+
headers: { Accept: "text/event-stream" },
21+
mode: "cors",
22+
cache: "no-store",
23+
redirect: "follow",
24+
...overrides,
25+
};
26+
}
27+
1128
describe("createStreamingFetchAdapter", () => {
1229
describe("Request Handling", () => {
1330
it("passes URL, signal, and responseType to axios", async () => {
@@ -18,12 +35,12 @@ describe("createStreamingFetchAdapter", () => {
1835
const adapter = createStreamingFetchAdapter(mockAxios);
1936
const signal = new AbortController().signal;
2037

21-
await adapter(TEST_URL, { signal });
38+
await adapter(TEST_URL, createFetchInit({ signal }));
2239

2340
expect(mockAxios.request).toHaveBeenCalledWith({
2441
url: TEST_URL,
2542
signal, // correctly passes signal
26-
headers: {},
43+
headers: { Accept: "text/event-stream" },
2744
responseType: "stream",
2845
validateStatus: expect.any(Function),
2946
});
@@ -36,27 +53,34 @@ describe("createStreamingFetchAdapter", () => {
3653

3754
// Test 1: No config headers, only init headers
3855
const adapter1 = createStreamingFetchAdapter(mockAxios);
39-
await adapter1(TEST_URL, {
40-
headers: { "X-Init": "init-value" },
41-
});
56+
await adapter1(
57+
TEST_URL,
58+
createFetchInit({
59+
headers: { Accept: "text/event-stream", "X-Init": "init-value" },
60+
}),
61+
);
4262

4363
expect(mockAxios.request).toHaveBeenCalledWith(
4464
expect.objectContaining({
45-
headers: { "X-Init": "init-value" },
65+
headers: { Accept: "text/event-stream", "X-Init": "init-value" },
4666
}),
4767
);
4868

4969
// Test 2: Config headers merge with init headers
5070
const adapter2 = createStreamingFetchAdapter(mockAxios, {
5171
"X-Config": "config-value",
5272
});
53-
await adapter2(TEST_URL, {
54-
headers: { "X-Init": "init-value" },
55-
});
73+
await adapter2(
74+
TEST_URL,
75+
createFetchInit({
76+
headers: { Accept: "text/event-stream", "X-Init": "init-value" },
77+
}),
78+
);
5679

5780
expect(mockAxios.request).toHaveBeenCalledWith(
5881
expect.objectContaining({
5982
headers: {
83+
Accept: "text/event-stream",
6084
"X-Init": "init-value",
6185
"X-Config": "config-value",
6286
},
@@ -67,13 +91,16 @@ describe("createStreamingFetchAdapter", () => {
6791
const adapter3 = createStreamingFetchAdapter(mockAxios, {
6892
"X-Header": "config-value",
6993
});
70-
await adapter3(TEST_URL, {
71-
headers: { "X-Header": "init-value" },
72-
});
94+
await adapter3(
95+
TEST_URL,
96+
createFetchInit({
97+
headers: { Accept: "text/event-stream", "X-Header": "init-value" },
98+
}),
99+
);
73100

74101
expect(mockAxios.request).toHaveBeenCalledWith(
75102
expect.objectContaining({
76-
headers: { "X-Header": "config-value" },
103+
headers: { Accept: "text/event-stream", "X-Header": "config-value" },
77104
}),
78105
);
79106
});

test/unit/websocket/sseConnection.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,9 @@ function createMockEventSource(
356356
}
357357

358358
function setupEventSourceMock(es: Partial<EventSource>): void {
359-
vi.mocked(EventSource).mockImplementation(() => es as EventSource);
359+
vi.mocked(EventSource).mockImplementation(function () {
360+
return es as EventSource;
361+
});
360362
}
361363

362364
function waitForNextTick(): Promise<void> {

0 commit comments

Comments
 (0)