-
Notifications
You must be signed in to change notification settings - Fork 42
feat: add commit message generation #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a7cc035
0c21308
5612075
764f7b2
2ecb523
158f87f
51329c4
c09c77b
bc24a20
e906306
c071a6c
70df946
9faf4c3
bc27d89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest" | ||
|
|
||
| // Mock vscode following the pattern from AutocompleteServiceManager.spec.ts | ||
| vi.mock("vscode", () => { | ||
| const disposable = { dispose: vi.fn() } | ||
|
|
||
| return { | ||
| commands: { | ||
| registerCommand: vi.fn((_command: string, _callback: (...args: any[]) => any) => disposable), | ||
| }, | ||
| window: { | ||
| showErrorMessage: vi.fn(), | ||
| withProgress: vi.fn(), | ||
| }, | ||
| workspace: { | ||
| workspaceFolders: [ | ||
| { | ||
| uri: { fsPath: "/test/workspace" }, | ||
| }, | ||
| ], | ||
| }, | ||
| extensions: { | ||
| getExtension: vi.fn(), | ||
| }, | ||
| ProgressLocation: { | ||
| SourceControl: 1, | ||
| }, | ||
| Uri: { | ||
| parse: (s: string) => ({ fsPath: s }), | ||
| }, | ||
| } | ||
| }) | ||
|
|
||
| import * as vscode from "vscode" | ||
| import { registerCommitMessageService } from "../index" | ||
| import type { KiloConnectionService } from "../../cli-backend/connection-service" | ||
|
|
||
| describe("commit-message service", () => { | ||
| let mockContext: vscode.ExtensionContext | ||
| let mockConnectionService: KiloConnectionService | ||
| let mockHttpClient: { generateCommitMessage: ReturnType<typeof vi.fn> } | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks() | ||
|
|
||
| mockContext = { | ||
| subscriptions: [], | ||
| } as any | ||
|
|
||
| mockHttpClient = { | ||
| generateCommitMessage: vi.fn().mockResolvedValue("feat: add new feature"), | ||
| } | ||
|
|
||
| mockConnectionService = { | ||
| getHttpClient: vi.fn().mockReturnValue(mockHttpClient), | ||
| } as any | ||
| }) | ||
|
|
||
| describe("registerCommitMessageService", () => { | ||
| it("returns an array of disposables", () => { | ||
| const disposables = registerCommitMessageService(mockContext, mockConnectionService) | ||
|
|
||
| expect(Array.isArray(disposables)).toBe(true) | ||
| expect(disposables.length).toBeGreaterThan(0) | ||
| }) | ||
|
|
||
| it("registers the kilo-code.new.generateCommitMessage command", () => { | ||
| registerCommitMessageService(mockContext, mockConnectionService) | ||
|
|
||
| expect(vscode.commands.registerCommand).toHaveBeenCalledWith( | ||
| "kilo-code.new.generateCommitMessage", | ||
| expect.any(Function), | ||
| ) | ||
| }) | ||
|
|
||
| it("pushes the command disposable to context.subscriptions", () => { | ||
| registerCommitMessageService(mockContext, mockConnectionService) | ||
|
|
||
| expect(mockContext.subscriptions.length).toBe(1) | ||
| }) | ||
| }) | ||
|
|
||
| describe("command execution", () => { | ||
| let commandCallback: (...args: any[]) => Promise<void> | ||
|
|
||
| beforeEach(() => { | ||
| registerCommitMessageService(mockContext, mockConnectionService) | ||
|
|
||
| // Extract the registered command callback | ||
| const registerCall = vi.mocked(vscode.commands.registerCommand).mock.calls[0]! | ||
| commandCallback = registerCall[1] as (...args: any[]) => Promise<void> | ||
| }) | ||
|
|
||
| it("shows error when git extension is not found", async () => { | ||
| vi.mocked(vscode.extensions.getExtension).mockReturnValue(undefined) | ||
|
|
||
| await commandCallback() | ||
|
|
||
| expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Git extension not found") | ||
| }) | ||
|
|
||
| it("shows error when no git repository is found", async () => { | ||
| vi.mocked(vscode.extensions.getExtension).mockReturnValue({ | ||
| isActive: true, | ||
| activate: vi.fn().mockResolvedValue(undefined), | ||
| exports: { | ||
| getAPI: () => ({ repositories: [] }), | ||
| }, | ||
| } as any) | ||
|
|
||
| await commandCallback() | ||
|
|
||
| expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("No Git repository found") | ||
| }) | ||
|
|
||
| it("shows error when backend is not connected", async () => { | ||
| vi.mocked(vscode.extensions.getExtension).mockReturnValue({ | ||
| isActive: true, | ||
| activate: vi.fn().mockResolvedValue(undefined), | ||
| exports: { | ||
| getAPI: () => ({ | ||
| repositories: [{ inputBox: { value: "" }, rootUri: { fsPath: "/repo" } }], | ||
| }), | ||
| }, | ||
| } as any) | ||
| vi.mocked(mockConnectionService.getHttpClient as any).mockImplementation(() => { | ||
| throw new Error("Not connected") | ||
| }) | ||
|
|
||
| await commandCallback() | ||
|
|
||
| expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( | ||
| "Kilo backend is not connected. Please wait for the connection to establish.", | ||
| ) | ||
| }) | ||
|
|
||
| it("calls generateCommitMessage on the HTTP client with repository root path", async () => { | ||
| const mockInputBox = { value: "" } | ||
| vi.mocked(vscode.extensions.getExtension).mockReturnValue({ | ||
| isActive: true, | ||
| activate: vi.fn().mockResolvedValue(undefined), | ||
| exports: { | ||
| getAPI: () => ({ | ||
| repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }], | ||
| }), | ||
| }, | ||
| } as any) | ||
|
|
||
| // Make withProgress execute its callback | ||
| vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { | ||
| await task({} as any, {} as any) | ||
| }) | ||
|
|
||
| await commandCallback() | ||
|
|
||
| expect(mockHttpClient.generateCommitMessage).toHaveBeenCalledWith("/repo", undefined, undefined) | ||
| }) | ||
|
|
||
| it("sets the generated message on the repository inputBox", async () => { | ||
| const mockInputBox = { value: "" } | ||
| vi.mocked(vscode.extensions.getExtension).mockReturnValue({ | ||
| isActive: true, | ||
| activate: vi.fn().mockResolvedValue(undefined), | ||
| exports: { | ||
| getAPI: () => ({ | ||
| repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }], | ||
| }), | ||
| }, | ||
| } as any) | ||
|
|
||
| vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { | ||
| await task({} as any, {} as any) | ||
| }) | ||
|
|
||
| await commandCallback() | ||
|
|
||
| expect(mockInputBox.value).toBe("feat: add new feature") | ||
| }) | ||
|
|
||
| it("shows progress in SourceControl location", async () => { | ||
| const mockInputBox = { value: "" } | ||
| vi.mocked(vscode.extensions.getExtension).mockReturnValue({ | ||
| isActive: true, | ||
| activate: vi.fn().mockResolvedValue(undefined), | ||
| exports: { | ||
| getAPI: () => ({ | ||
| repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }], | ||
| }), | ||
| }, | ||
| } as any) | ||
|
|
||
| vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { | ||
| await task({} as any, {} as any) | ||
| }) | ||
|
|
||
| await commandCallback() | ||
|
|
||
| expect(vscode.window.withProgress).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| location: vscode.ProgressLocation.SourceControl, | ||
| title: "Generating commit message...", | ||
| }), | ||
| expect.any(Function), | ||
| ) | ||
| }) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import * as vscode from "vscode" | ||
| import type { KiloConnectionService } from "../cli-backend/connection-service" | ||
| import type { HttpClient } from "../cli-backend/http-client" | ||
|
|
||
| let lastGeneratedMessage: string | undefined | ||
| let lastWorkspacePath: string | undefined | ||
|
|
||
| interface GitRepository { | ||
| inputBox: { value: string } | ||
| rootUri: vscode.Uri | ||
| } | ||
|
|
||
| interface GitAPI { | ||
| repositories: GitRepository[] | ||
| } | ||
|
|
||
| interface GitExtensionExports { | ||
| getAPI(version: number): GitAPI | ||
| } | ||
|
|
||
| export function registerCommitMessageService( | ||
| context: vscode.ExtensionContext, | ||
| connectionService: KiloConnectionService, | ||
| ): vscode.Disposable[] { | ||
| const command = vscode.commands.registerCommand("kilo-code.new.generateCommitMessage", async () => { | ||
| const extension = vscode.extensions.getExtension<GitExtensionExports>("vscode.git") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SUGGESTION: Ensure the built-in Git extension is activated before accessing
|
||
| if (!extension) { | ||
| vscode.window.showErrorMessage("Git extension not found") | ||
| return | ||
| } | ||
|
|
||
| if (!extension.isActive) { | ||
| await extension.activate() | ||
| } | ||
|
|
||
| const git = extension.exports?.getAPI(1) | ||
| const repository = git?.repositories[0] | ||
| if (!repository) { | ||
| vscode.window.showErrorMessage("No Git repository found") | ||
| return | ||
| } | ||
|
|
||
| let client: HttpClient | undefined | ||
| try { | ||
| client = connectionService.getHttpClient() | ||
| } catch { | ||
| vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") | ||
| return | ||
| } | ||
| if (!client) { | ||
| vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") | ||
| return | ||
| } | ||
|
|
||
| const path = repository.rootUri.fsPath | ||
|
|
||
| const previousMessage = lastWorkspacePath === path ? lastGeneratedMessage : undefined | ||
|
|
||
| await vscode.window | ||
| .withProgress( | ||
| { location: vscode.ProgressLocation.SourceControl, title: "Generating commit message..." }, | ||
| async () => { | ||
| const message = await client.generateCommitMessage(path, undefined, previousMessage) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Possible runtime error if
|
||
| repository.inputBox.value = message | ||
| lastGeneratedMessage = message | ||
| lastWorkspacePath = path | ||
| console.log("[Kilo New] Commit message generated successfully") | ||
| }, | ||
| ) | ||
| .then(undefined, (error: unknown) => { | ||
| const msg = error instanceof Error ? error.message : String(error) | ||
| console.error("[Kilo New] Failed to generate commit message:", msg) | ||
| vscode.window.showErrorMessage(`Failed to generate commit message: ${msg}`) | ||
| }) | ||
| }) | ||
|
|
||
| context.subscriptions.push(command) | ||
| return [command] | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: Mocked
vscode.gitextension is missingisActive/activate, which can cause the command callback to throwregisterCommitMessageService()now checksif (!extension.isActive) await extension.activate(). In this test, the mocked extension object only providesexports, soextension.activatewill beundefinedand the callback will throw before reaching the"No Git repository found"branch.Add
isActive: true(oractivate: vi.fn().mockResolvedValue(undefined)+isActive: false) to the mocked extension return values so the test exercises the intended path.