Skip to content

Commit d84ee78

Browse files
committed
more test cleanup
1 parent 89ac9ee commit d84ee78

File tree

11 files changed

+1354
-595
lines changed

11 files changed

+1354
-595
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@
66
/coverage/
77
*.vsix
88
yarn-error.log
9+
/reports/
10+
/.stryker-tmp/
11+
stryker.log

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
yarn lint:fix # Lint with auto-fix
1111
yarn test:ci --coverage # Run ALL unit tests (ALWAYS use this)
1212
yarn pretest && yarn test:integration # Integration tests
13+
yarn mutate # Mutation testing (may take up to 180s - run occasionally)
1314
```
1415

1516
## Key Rules

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"fmt": "prettier --write .",
2323
"lint": "eslint . --ext ts,md,json",
2424
"lint:fix": "yarn lint --fix",
25+
"mutate": "npx stryker run",
2526
"package": "webpack --mode production --devtool hidden-source-map",
2627
"package:prerelease": "npx vsce package --pre-release",
2728
"pretest": "tsc -p . --outDir out && yarn run build && yarn run lint",
@@ -307,6 +308,8 @@
307308
"zod": "^3.25.65"
308309
},
309310
"devDependencies": {
311+
"@stryker-mutator/core": "^9.0.1",
312+
"@stryker-mutator/vitest-runner": "^9.0.1",
310313
"@types/eventsource": "^3.0.0",
311314
"@types/glob": "^7.1.3",
312315
"@types/node": "^22.14.1",
@@ -316,7 +319,7 @@
316319
"@types/ws": "^8.18.1",
317320
"@typescript-eslint/eslint-plugin": "^7.0.0",
318321
"@typescript-eslint/parser": "^6.21.0",
319-
"@vitest/coverage-v8": "0.34.6",
322+
"@vitest/coverage-v8": "^2.1.0",
320323
"@vscode/test-cli": "^0.0.10",
321324
"@vscode/test-electron": "^2.5.2",
322325
"@vscode/vsce": "^2.21.1",
@@ -338,7 +341,7 @@
338341
"tsc-watch": "^6.2.1",
339342
"typescript": "^5.4.5",
340343
"utf-8-validate": "^6.0.5",
341-
"vitest": "^0.34.6",
344+
"vitest": "^2.1.0",
342345
"vscode-test": "^1.5.0",
343346
"webpack": "^5.99.6",
344347
"webpack-cli": "^5.1.4"

src/api-helper.test.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ErrorEvent } from "eventsource";
22
import { describe, expect, it } from "vitest";
33
import {
44
AgentMetadataEventSchema,
5+
AgentMetadataEventSchemaArray,
56
errToStr,
67
extractAgents,
78
extractAllAgents,
@@ -70,6 +71,29 @@ describe("api-helper", () => {
7071
const errorEvent = new ErrorEvent("error", eventInit);
7172
expect(errToStr(errorEvent, "default")).toBe(expected);
7273
});
74+
75+
it("should handle API error response", () => {
76+
const apiError = {
77+
isAxiosError: true,
78+
response: {
79+
data: {
80+
message: "API request failed",
81+
detail: "API request failed",
82+
},
83+
},
84+
};
85+
expect(errToStr(apiError, "default")).toBe("API request failed");
86+
});
87+
88+
it("should handle API error response object", () => {
89+
const apiErrorResponse = {
90+
detail: "Invalid authentication",
91+
message: "Invalid authentication",
92+
};
93+
expect(errToStr(apiErrorResponse, "default")).toBe(
94+
"Invalid authentication",
95+
);
96+
});
7397
});
7498

7599
describe("extractAgents", () => {
@@ -148,16 +172,73 @@ describe("api-helper", () => {
148172

149173
describe("AgentMetadataEventSchema", () => {
150174
it("should validate correct event", () => {
151-
const result = AgentMetadataEventSchema.safeParse(
152-
createValidMetadataEvent(),
153-
);
175+
const validEvent = createValidMetadataEvent();
176+
const result = AgentMetadataEventSchema.safeParse(validEvent);
154177
expect(result.success).toBe(true);
178+
if (result.success) {
179+
expect(result.data.result.collected_at).toBe("2024-01-01T00:00:00Z");
180+
expect(result.data.result.age).toBe(60);
181+
expect(result.data.result.value).toBe("test-value");
182+
expect(result.data.result.error).toBe("");
183+
expect(result.data.description.display_name).toBe("Test Metric");
184+
expect(result.data.description.key).toBe("test_metric");
185+
expect(result.data.description.script).toBe("echo 'test'");
186+
expect(result.data.description.interval).toBe(30);
187+
expect(result.data.description.timeout).toBe(10);
188+
}
155189
});
156190

157191
it("should reject invalid event", () => {
158192
const event = createValidMetadataEvent({ age: "invalid" });
159193
const result = AgentMetadataEventSchema.safeParse(event);
160194
expect(result.success).toBe(false);
195+
if (!result.success) {
196+
expect(result.error.issues[0].code).toBe("invalid_type");
197+
expect(result.error.issues[0].path).toEqual(["result", "age"]);
198+
}
199+
});
200+
201+
it("should validate array of events", () => {
202+
const events = [
203+
createValidMetadataEvent(),
204+
createValidMetadataEvent({ value: "different-value" }),
205+
];
206+
const result = AgentMetadataEventSchemaArray.safeParse(events);
207+
expect(result.success).toBe(true);
208+
if (result.success) {
209+
expect(result.data).toHaveLength(2);
210+
expect(result.data[0].result.value).toBe("test-value");
211+
expect(result.data[1].result.value).toBe("different-value");
212+
}
213+
});
214+
215+
it("should reject array with invalid events", () => {
216+
const events = [createValidMetadataEvent(), { invalid: "structure" }];
217+
const result = AgentMetadataEventSchemaArray.safeParse(events);
218+
expect(result.success).toBe(false);
219+
});
220+
221+
it("should handle missing required fields", () => {
222+
const incompleteEvent = {
223+
result: {
224+
collected_at: "2024-01-01T00:00:00Z",
225+
// missing age, value, error
226+
},
227+
description: {
228+
display_name: "Test",
229+
// missing other fields
230+
},
231+
};
232+
const result = AgentMetadataEventSchema.safeParse(incompleteEvent);
233+
expect(result.success).toBe(false);
234+
if (!result.success) {
235+
const missingFields = result.error.issues.map(
236+
(issue) => issue.path[issue.path.length - 1],
237+
);
238+
expect(missingFields).toContain("age");
239+
expect(missingFields).toContain("value");
240+
expect(missingFields).toContain("error");
241+
}
161242
});
162243
});
163244
});

src/api.test.ts

Lines changed: 199 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,22 @@ describe("api", () => {
104104
false,
105105
],
106106
[
107-
"should handle null/undefined config values",
107+
"should handle null config values",
108108
{ "coder.tlsCertFile": null, "coder.tlsKeyFile": null },
109109
true,
110110
],
111+
[
112+
"should handle undefined config values",
113+
{ "coder.tlsCertFile": undefined, "coder.tlsKeyFile": undefined },
114+
true,
115+
],
116+
["should handle missing config entries", {}, true],
111117
])("%s", (_, configValues: Record<string, unknown>, expected) => {
112118
mockConfiguration.get.mockImplementation((key: string) => {
113-
return configValues[key] ?? "";
119+
if (key in configValues) {
120+
return configValues[key];
121+
}
122+
return undefined;
114123
});
115124

116125
// Mock expandPath to return the path as-is
@@ -173,12 +182,32 @@ describe("api", () => {
173182
rejectUnauthorized: true,
174183
},
175184
],
185+
[
186+
"undefined configuration values",
187+
{
188+
"coder.tlsCertFile": undefined,
189+
"coder.tlsKeyFile": undefined,
190+
"coder.tlsCaFile": undefined,
191+
"coder.tlsAltHost": undefined,
192+
"coder.insecure": undefined,
193+
},
194+
{
195+
cert: undefined,
196+
key: undefined,
197+
ca: undefined,
198+
servername: undefined,
199+
rejectUnauthorized: true,
200+
},
201+
],
176202
])(
177203
"should create ProxyAgent with %s",
178204
async (_, configValues: Record<string, unknown>, expectedAgentConfig) => {
179-
mockConfiguration.get.mockImplementation(
180-
(key: string) => configValues[key] ?? "",
181-
);
205+
mockConfiguration.get.mockImplementation((key: string) => {
206+
if (key in configValues) {
207+
return configValues[key];
208+
}
209+
return undefined;
210+
});
182211

183212
if (configValues["coder.tlsCertFile"]) {
184213
vi.mocked(fs.readFile)
@@ -374,6 +403,171 @@ describe("api", () => {
374403
expect.objectContaining({ url: "https://example.com/api" }),
375404
);
376405
});
406+
407+
it("should handle stream data events", async () => {
408+
let dataHandler: (chunk: Buffer) => void;
409+
const mockData = {
410+
on: vi.fn((event: string, handler: (chunk: Buffer) => void) => {
411+
if (event === "data") {
412+
dataHandler = handler;
413+
}
414+
}),
415+
destroy: vi.fn(),
416+
};
417+
418+
const mockAxiosInstance = {
419+
request: vi
420+
.fn()
421+
.mockResolvedValue(createMockAxiosResponse({ data: mockData })),
422+
};
423+
424+
const adapter = createStreamingFetchAdapter(mockAxiosInstance as never);
425+
426+
let enqueuedData: Buffer | undefined;
427+
global.ReadableStream = vi.fn().mockImplementation((options) => {
428+
const controller = {
429+
enqueue: vi.fn((chunk: Buffer) => {
430+
enqueuedData = chunk;
431+
}),
432+
close: vi.fn(),
433+
error: vi.fn(),
434+
};
435+
if (options.start) {
436+
options.start(controller);
437+
}
438+
return { getReader: vi.fn(() => ({ read: vi.fn() })) };
439+
}) as never;
440+
441+
await adapter("https://example.com/api");
442+
443+
// Simulate data event
444+
const testData = Buffer.from("test data");
445+
dataHandler!(testData);
446+
447+
expect(enqueuedData).toEqual(testData);
448+
expect(mockData.on).toHaveBeenCalledWith("data", expect.any(Function));
449+
});
450+
451+
it("should handle stream end event", async () => {
452+
let endHandler: () => void;
453+
const mockData = {
454+
on: vi.fn((event: string, handler: () => void) => {
455+
if (event === "end") {
456+
endHandler = handler;
457+
}
458+
}),
459+
destroy: vi.fn(),
460+
};
461+
462+
const mockAxiosInstance = {
463+
request: vi
464+
.fn()
465+
.mockResolvedValue(createMockAxiosResponse({ data: mockData })),
466+
};
467+
468+
const adapter = createStreamingFetchAdapter(mockAxiosInstance as never);
469+
470+
let streamClosed = false;
471+
global.ReadableStream = vi.fn().mockImplementation((options) => {
472+
const controller = {
473+
enqueue: vi.fn(),
474+
close: vi.fn(() => {
475+
streamClosed = true;
476+
}),
477+
error: vi.fn(),
478+
};
479+
if (options.start) {
480+
options.start(controller);
481+
}
482+
return { getReader: vi.fn(() => ({ read: vi.fn() })) };
483+
}) as never;
484+
485+
await adapter("https://example.com/api");
486+
487+
// Simulate end event
488+
endHandler!();
489+
490+
expect(streamClosed).toBe(true);
491+
expect(mockData.on).toHaveBeenCalledWith("end", expect.any(Function));
492+
});
493+
494+
it("should handle stream error event", async () => {
495+
let errorHandler: (err: Error) => void;
496+
const mockData = {
497+
on: vi.fn((event: string, handler: (err: Error) => void) => {
498+
if (event === "error") {
499+
errorHandler = handler;
500+
}
501+
}),
502+
destroy: vi.fn(),
503+
};
504+
505+
const mockAxiosInstance = {
506+
request: vi
507+
.fn()
508+
.mockResolvedValue(createMockAxiosResponse({ data: mockData })),
509+
};
510+
511+
const adapter = createStreamingFetchAdapter(mockAxiosInstance as never);
512+
513+
let streamError: Error | undefined;
514+
global.ReadableStream = vi.fn().mockImplementation((options) => {
515+
const controller = {
516+
enqueue: vi.fn(),
517+
close: vi.fn(),
518+
error: vi.fn((err: Error) => {
519+
streamError = err;
520+
}),
521+
};
522+
if (options.start) {
523+
options.start(controller);
524+
}
525+
return { getReader: vi.fn(() => ({ read: vi.fn() })) };
526+
}) as never;
527+
528+
await adapter("https://example.com/api");
529+
530+
// Simulate error event
531+
const testError = new Error("Stream error");
532+
errorHandler!(testError);
533+
534+
expect(streamError).toBe(testError);
535+
expect(mockData.on).toHaveBeenCalledWith("error", expect.any(Function));
536+
});
537+
538+
it("should handle stream cancel", async () => {
539+
const mockData = {
540+
on: vi.fn(),
541+
destroy: vi.fn(),
542+
};
543+
544+
const mockAxiosInstance = {
545+
request: vi
546+
.fn()
547+
.mockResolvedValue(createMockAxiosResponse({ data: mockData })),
548+
};
549+
550+
const adapter = createStreamingFetchAdapter(mockAxiosInstance as never);
551+
552+
let cancelFunction: (() => Promise<void>) | undefined;
553+
global.ReadableStream = vi.fn().mockImplementation((options) => {
554+
if (options.cancel) {
555+
cancelFunction = options.cancel;
556+
}
557+
if (options.start) {
558+
options.start({ enqueue: vi.fn(), close: vi.fn(), error: vi.fn() });
559+
}
560+
return { getReader: vi.fn(() => ({ read: vi.fn() })) };
561+
}) as never;
562+
563+
await adapter("https://example.com/api");
564+
565+
// Call cancel
566+
expect(cancelFunction).toBeDefined();
567+
await cancelFunction!();
568+
569+
expect(mockData.destroy).toHaveBeenCalled();
570+
});
377571
});
378572

379573
describe("startWorkspaceIfStoppedOrFailed", () => {

0 commit comments

Comments
 (0)