Skip to content

Commit e6a7727

Browse files
committed
test(testing): add comprehensive test suite for core functionality
- Add mock implementation for VS Code API - Add tests for Anthropic API response handling and errors - Add tests for API key management and validation - Add tests for configuration edge cases and defaults - Add tests for Git integration and command handling - Add tests for commit message formatting and content types - Improve test organization by splitting into focused test files - Add error handling and edge case coverage - Remove outdated test file and setup - Add detailed test cases for message processing
1 parent 9cb4492 commit e6a7727

12 files changed

Lines changed: 2019 additions & 570 deletions

test/__mocks__/vscode.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export const window = {
2+
showInputBox: jest.fn(),
3+
showInformationMessage: jest.fn(),
4+
showErrorMessage: jest.fn(),
5+
showWarningMessage: jest.fn(),
6+
showTextDocument: jest.fn(),
7+
}
8+
9+
export const workspace = {
10+
getConfiguration: jest.fn(),
11+
openTextDocument: jest.fn(),
12+
workspaceFolders: [],
13+
}
14+
15+
export const commands = {
16+
registerCommand: jest.fn().mockReturnValue({ dispose: jest.fn() }),
17+
}
18+
19+
export const extensions = {
20+
getExtension: jest.fn(),
21+
}
22+
23+
export const ExtensionContext = jest.fn().mockImplementation(() => ({
24+
subscriptions: [],
25+
secrets: {
26+
get: jest.fn(),
27+
store: jest.fn(),
28+
delete: jest.fn(),
29+
},
30+
}))
31+
32+
export const Uri = {
33+
file: jest.fn(),
34+
}
35+
36+
// Add any other VSCode APIs that your tests need
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
// Mock Anthropic API at the top level
2+
const mockAnthropicCreate = jest.fn()
3+
4+
// Create mock APIError class
5+
class MockAPIError extends Error {
6+
constructor(
7+
public status: number,
8+
message: string,
9+
public type = "api_error",
10+
public headers = {},
11+
) {
12+
super(message)
13+
this.name = "APIError"
14+
}
15+
}
16+
17+
// Create mock Anthropic constructor
18+
function MockAnthropic() {
19+
return {
20+
messages: {
21+
create: mockAnthropicCreate,
22+
},
23+
}
24+
}
25+
MockAnthropic.APIError = MockAPIError
26+
27+
jest.mock("@anthropic-ai/sdk", () => ({
28+
__esModule: true,
29+
default: MockAnthropic,
30+
}))
31+
32+
import Anthropic from "@anthropic-ai/sdk"
33+
import { extensions, window, workspace, type ExtensionContext } from "vscode"
34+
import { activate } from "../src/extension"
35+
36+
// Store registered command callbacks
37+
const registeredCallbacks = new Map<string, Function>()
38+
39+
jest.mock("vscode", () => {
40+
const original = jest.requireActual("vscode")
41+
return {
42+
...original,
43+
window: {
44+
...original.window,
45+
showErrorMessage: jest.fn(),
46+
showWarningMessage: jest.fn(),
47+
},
48+
extensions: {
49+
...original.extensions,
50+
getExtension: jest.fn(),
51+
},
52+
commands: {
53+
registerCommand: jest.fn((id, callback) => {
54+
registeredCallbacks.set(id, callback)
55+
return { dispose: jest.fn() }
56+
}),
57+
},
58+
workspace: {
59+
...original.workspace,
60+
workspaceFolders: [
61+
{
62+
uri: {
63+
fsPath: "/test/workspace",
64+
},
65+
},
66+
],
67+
getConfiguration: jest.fn().mockReturnValue({
68+
get: jest.fn((key: string) => {
69+
switch (key) {
70+
case "model":
71+
return "claude-3-5-sonnet-latest"
72+
case "maxTokens":
73+
return 1024
74+
case "temperature":
75+
return 0.4
76+
case "allowedTypes":
77+
return ["feat", "fix", "refactor", "chore", "docs", "style", "test", "perf", "ci"]
78+
default:
79+
return undefined
80+
}
81+
}),
82+
}),
83+
},
84+
}
85+
})
86+
87+
describe("Anthropic API Response Handling", () => {
88+
let mockContext: ExtensionContext
89+
let mockGitRepo: any
90+
91+
beforeEach(() => {
92+
jest.clearAllMocks()
93+
registeredCallbacks.clear()
94+
95+
// Mock context with secrets
96+
mockContext = {
97+
subscriptions: [],
98+
secrets: {
99+
get: jest.fn().mockResolvedValue("sk-ant-api-valid-key"),
100+
store: jest.fn(),
101+
delete: jest.fn(),
102+
},
103+
} as unknown as ExtensionContext
104+
105+
// Setup basic mock Git repo
106+
mockGitRepo = {
107+
diff: jest.fn().mockResolvedValue("test diff"),
108+
inputBox: { value: "" },
109+
state: {
110+
workingTreeChanges: [],
111+
indexChanges: [],
112+
},
113+
}
114+
115+
// Setup default Git extension mock
116+
const mockGitExtension = {
117+
exports: {
118+
getAPI: jest.fn().mockReturnValue({
119+
repositories: [mockGitRepo],
120+
}),
121+
},
122+
}
123+
jest.spyOn(extensions, "getExtension").mockReturnValue(mockGitExtension as any)
124+
125+
// Activate the extension to register commands
126+
activate(mockContext)
127+
})
128+
129+
const getCommand = (commandId: string): Function => {
130+
const command = registeredCallbacks.get(commandId)
131+
if (!command) {
132+
throw new Error(`Command ${commandId} not registered`)
133+
}
134+
return command
135+
}
136+
137+
describe("API Error Handling", () => {
138+
it("should handle rate limit errors", async () => {
139+
const apiError = new Anthropic.APIError(429, "Rate limit exceeded", "api_error", {})
140+
mockAnthropicCreate.mockRejectedValue(apiError)
141+
142+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
143+
await generateCommitMessage()
144+
145+
expect(window.showErrorMessage).toHaveBeenCalledWith(
146+
"Rate limit exceeded. Please try again later: Rate limit exceeded",
147+
)
148+
expect(mockGitRepo.inputBox.value).toBe("")
149+
})
150+
151+
it("should handle authentication errors", async () => {
152+
const apiError = new Anthropic.APIError(401, "Invalid API key", "api_error", {})
153+
mockAnthropicCreate.mockRejectedValue(apiError)
154+
155+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
156+
await generateCommitMessage()
157+
158+
expect(window.showErrorMessage).toHaveBeenCalledWith("Invalid API key. Please update your API key and try again.")
159+
expect(mockGitRepo.inputBox.value).toBe("")
160+
})
161+
162+
it("should handle unknown API errors", async () => {
163+
const apiError = new Anthropic.APIError(418, "Unknown error", "api_error", {})
164+
mockAnthropicCreate.mockRejectedValue(apiError)
165+
166+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
167+
await generateCommitMessage()
168+
169+
expect(window.showErrorMessage).toHaveBeenCalledWith("Failed to generate commit message: Unknown error")
170+
expect(mockGitRepo.inputBox.value).toBe("")
171+
})
172+
})
173+
174+
describe("Response Content Handling", () => {
175+
it("should handle empty content array", async () => {
176+
mockAnthropicCreate.mockResolvedValue({
177+
content: [],
178+
stop_reason: "end_turn",
179+
usage: { input_tokens: 100, output_tokens: 0 },
180+
})
181+
182+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
183+
await generateCommitMessage()
184+
185+
expect(mockGitRepo.inputBox.value).toBe("")
186+
})
187+
188+
it("should handle non-text content", async () => {
189+
mockAnthropicCreate.mockResolvedValue({
190+
content: [{ type: "image", source: { type: "base64", media_type: "image/png", data: "some-image-data" } }],
191+
stop_reason: "end_turn",
192+
usage: { input_tokens: 100, output_tokens: 50 },
193+
})
194+
195+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
196+
await generateCommitMessage()
197+
198+
expect(mockGitRepo.inputBox.value).toBe("")
199+
})
200+
201+
it("should handle mixed content types", async () => {
202+
mockAnthropicCreate.mockResolvedValue({
203+
content: [
204+
{ type: "text", text: "feat(scope): add feature" },
205+
{ type: "image", source: { type: "base64", media_type: "image/png", data: "some-image" } },
206+
],
207+
stop_reason: "end_turn",
208+
usage: { input_tokens: 100, output_tokens: 50 },
209+
})
210+
211+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
212+
await generateCommitMessage()
213+
214+
expect(mockGitRepo.inputBox.value).toBe("feat(scope): add feature")
215+
})
216+
217+
it("should handle multiple text segments", async () => {
218+
mockAnthropicCreate.mockResolvedValue({
219+
content: [{ type: "text", text: "feat(scope): add feature\n\n* Change 1\n* Change 2" }],
220+
stop_reason: "end_turn",
221+
usage: { input_tokens: 100, output_tokens: 50 },
222+
})
223+
224+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
225+
await generateCommitMessage()
226+
227+
expect(mockGitRepo.inputBox.value).toBe("feat(scope): add feature\n\n- Change 1\n- Change 2")
228+
})
229+
230+
it("should handle excessive newlines", async () => {
231+
mockAnthropicCreate.mockResolvedValue({
232+
content: [
233+
{
234+
type: "text",
235+
text: "feat(scope): add feature\n\n\n* Change 1\n\n\n* Change 2\n\n\n",
236+
},
237+
],
238+
stop_reason: "end_turn",
239+
usage: { input_tokens: 100, output_tokens: 50 },
240+
})
241+
242+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
243+
await generateCommitMessage()
244+
245+
expect(mockGitRepo.inputBox.value).toBe("feat(scope): add feature\n\n- Change 1\n\n- Change 2")
246+
})
247+
})
248+
249+
describe("Response Metadata Handling", () => {
250+
it("should log stop reason", async () => {
251+
const consoleSpy = jest.spyOn(console, "log")
252+
mockAnthropicCreate.mockResolvedValue({
253+
content: [{ type: "text", text: "test message" }],
254+
stop_reason: "end_turn",
255+
usage: { input_tokens: 100, output_tokens: 50 },
256+
})
257+
258+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
259+
await generateCommitMessage()
260+
261+
expect(consoleSpy).toHaveBeenCalledWith("[DiffCommit] Stop Reason: ", "end_turn")
262+
})
263+
264+
it("should log usage data", async () => {
265+
const consoleSpy = jest.spyOn(console, "log")
266+
mockAnthropicCreate.mockResolvedValue({
267+
content: [{ type: "text", text: "test message" }],
268+
stop_reason: "end_turn",
269+
usage: { input_tokens: 100, output_tokens: 50 },
270+
})
271+
272+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
273+
await generateCommitMessage()
274+
275+
expect(consoleSpy).toHaveBeenCalledWith("[DiffCommit] Usage: ", { input_tokens: 100, output_tokens: 50 })
276+
})
277+
})
278+
279+
describe("XML Tag Handling", () => {
280+
it("should include task and instructions tags in prompt", async () => {
281+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
282+
await generateCommitMessage()
283+
284+
expect(mockAnthropicCreate).toHaveBeenCalledWith(
285+
expect.objectContaining({
286+
messages: expect.arrayContaining([
287+
expect.objectContaining({
288+
content: expect.stringContaining("<task>"),
289+
}),
290+
]),
291+
}),
292+
)
293+
294+
expect(mockAnthropicCreate).toHaveBeenCalledWith(
295+
expect.objectContaining({
296+
messages: expect.arrayContaining([
297+
expect.objectContaining({
298+
content: expect.stringContaining("<instructions>"),
299+
}),
300+
]),
301+
}),
302+
)
303+
})
304+
305+
it("should include custom instructions in XML tags when provided", async () => {
306+
jest.spyOn(workspace, "getConfiguration").mockReturnValue({
307+
get: jest.fn((key: string) => {
308+
if (key === "customInstructions") {
309+
return "Use emoji in commit messages"
310+
}
311+
return undefined
312+
}),
313+
} as any)
314+
315+
const generateCommitMessage = getCommand("diffCommit.generateCommitMessage")
316+
await generateCommitMessage()
317+
318+
expect(mockAnthropicCreate).toHaveBeenCalledWith(
319+
expect.objectContaining({
320+
messages: expect.arrayContaining([
321+
expect.objectContaining({
322+
content: expect.stringContaining(
323+
"<customInstructions>\nUse emoji in commit messages\n</customInstructions>",
324+
),
325+
}),
326+
]),
327+
}),
328+
)
329+
})
330+
})
331+
})

0 commit comments

Comments
 (0)