diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts deleted file mode 100644 index cce80476b89..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest" -import { AutocompleteModel } from "../AutocompleteModel" -import type { KiloConnectionService } from "../../cli-backend" - -const mockClient = { - kilo: { - fim: vi.fn(), - }, -} - -function createMockConnectionService(state: "connecting" | "connected" | "disconnected" | "error" = "connected") { - return { - getConnectionState: vi.fn().mockReturnValue(state), - getClient: vi.fn().mockReturnValue(mockClient), - getClientAsync: - state === "connected" - ? vi.fn().mockResolvedValue(mockClient) - : vi.fn().mockRejectedValue(new Error(`CLI backend is not connected (state: ${state})`)), - onStateChange: vi.fn().mockReturnValue(() => {}), - } as unknown as KiloConnectionService -} - -describe("AutocompleteModel", () => { - beforeEach(() => { - mockClient.kilo.fim.mockReset() - }) - - describe("constructor", () => { - it("defaults profileName and profileType to null", () => { - const model = new AutocompleteModel() - expect(model.profileName).toBeNull() - expect(model.profileType).toBeNull() - }) - }) - - describe("setConnectionService", () => { - it("sets the connection service after construction", () => { - const model = new AutocompleteModel() - expect(model.hasValidCredentials()).toBe(false) - - const connection = createMockConnectionService("connected") - model.setConnectionService(connection) - expect(model.hasValidCredentials()).toBe(true) - }) - }) - - describe("hasValidCredentials", () => { - it("returns true when connected", () => { - const connection = createMockConnectionService("connected") - const model = new AutocompleteModel(connection) - expect(model.hasValidCredentials()).toBe(true) - }) - - it("returns false when disconnected", () => { - const connection = createMockConnectionService("disconnected") - const model = new AutocompleteModel(connection) - expect(model.hasValidCredentials()).toBe(false) - }) - - it("returns false when connecting", () => { - const connection = createMockConnectionService("connecting") - const model = new AutocompleteModel(connection) - expect(model.hasValidCredentials()).toBe(false) - }) - - it("returns false when in error state", () => { - const connection = createMockConnectionService("error") - const model = new AutocompleteModel(connection) - expect(model.hasValidCredentials()).toBe(false) - }) - - it("returns false without connection service", () => { - const model = new AutocompleteModel() - expect(model.hasValidCredentials()).toBe(false) - }) - }) - - describe("getModelName", () => { - it("returns the default model", () => { - const model = new AutocompleteModel() - expect(model.getModelName()).toBe("mistralai/codestral-2508") - }) - }) - - describe("getProviderDisplayName", () => { - it("returns the default provider", () => { - const model = new AutocompleteModel() - expect(model.getProviderDisplayName()).toBe("Mistral AI") - }) - - it("returns the selected provider", () => { - const model = new AutocompleteModel() - model.setModel("inception/mercury-edit") - - expect(model.getProviderDisplayName()).toBe("Inception") - }) - }) - - describe("generateFimResponse", () => { - it("throws when connection service is not available", async () => { - const model = new AutocompleteModel() - await expect(model.generateFimResponse("prefix", "suffix", vi.fn())).rejects.toThrow( - "Connection service is not available", - ) - }) - - it("throws when not connected", async () => { - const connection = createMockConnectionService("disconnected") - const model = new AutocompleteModel(connection) - await expect(model.generateFimResponse("prefix", "suffix", vi.fn())).rejects.toThrow( - "CLI backend is not connected", - ) - }) - - it("streams chunks and returns metadata", async () => { - const chunks = [ - { choices: [{ delta: { content: "hello" } }] }, - { - choices: [{ delta: { content: " world" } }], - usage: { prompt_tokens: 10, completion_tokens: 5 }, - cost: 0.001, - }, - ] - - const connection = createMockConnectionService("connected") - mockClient.kilo.fim.mockResolvedValue({ - stream: (async function* () { - for (const chunk of chunks) yield chunk - })(), - }) - - const model = new AutocompleteModel(connection) - const received: string[] = [] - const result = await model.generateFimResponse("prefix", "suffix", (text) => received.push(text)) - - expect(received).toEqual(["hello", " world"]) - expect(result).toEqual({ - cost: 0.001, - inputTokens: 10, - outputTokens: 5, - cacheWriteTokens: 0, - cacheReadTokens: 0, - }) - }) - - it("streams text-completion chunks", async () => { - const chunks = [{ choices: [{ text: "hello" }] }, { choices: [{ text: " world" }] }] - - const connection = createMockConnectionService("connected") - mockClient.kilo.fim.mockResolvedValue({ - stream: (async function* () { - for (const chunk of chunks) yield chunk - })(), - }) - - const model = new AutocompleteModel(connection) - const received: string[] = [] - await model.generateFimResponse("prefix", "suffix", (text) => received.push(text)) - - expect(received).toEqual(["hello", " world"]) - }) - - it("passes model parameters to fim call", async () => { - const connection = createMockConnectionService("connected") - mockClient.kilo.fim.mockResolvedValue({ - stream: (async function* () {})(), - }) - - const model = new AutocompleteModel(connection) - const signal = new AbortController().signal - await model.generateFimResponse("pre", "suf", vi.fn(), signal) - - expect(mockClient.kilo.fim).toHaveBeenCalledWith( - { - prefix: "pre", - suffix: "suf", - model: "mistralai/codestral-2508", - maxTokens: 256, - temperature: 0.2, - }, - expect.objectContaining({ signal }), - ) - }) - - it("passes selected model parameters to fim call", async () => { - const connection = createMockConnectionService("connected") - mockClient.kilo.fim.mockResolvedValue({ - stream: (async function* () {})(), - }) - - const model = new AutocompleteModel(connection) - model.setModel("inception/mercury-edit") - await model.generateFimResponse("pre", "suf", vi.fn()) - - expect(mockClient.kilo.fim).toHaveBeenCalledWith( - expect.objectContaining({ - model: "inception/mercury-edit", - temperature: 0, - }), - expect.any(Object), - ) - }) - }) -}) diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts deleted file mode 100644 index cc1398cf57c..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" -import * as vscode from "vscode" -import { AutocompleteServiceManager } from "../AutocompleteServiceManager" - -vi.mock("vscode", () => { - class Position { - constructor( - public line: number, - public character: number, - ) {} - } - - class Range { - constructor( - public start: any, - public end: any, - ) {} - } - - class CancellationTokenSource { - public token = { - isCancellationRequested: false, - onCancellationRequested: vi.fn(), - } - - dispose = vi.fn() - } - - return { - Uri: { - parse: (uriString: string) => ({ - toString: () => uriString, - fsPath: uriString.replace("file://", ""), - scheme: "file", - path: uriString.replace("file://", ""), - }), - }, - Position, - Range, - CancellationTokenSource, - InlineCompletionTriggerKind: { - Invoke: 1, - }, - workspace: { - openTextDocument: vi.fn(), - applyEdit: vi.fn(), - asRelativePath: vi.fn().mockImplementation((uri) => { - if (typeof uri === "string") return uri.replace("file:///", "") - return uri.toString().replace("file:///", "") - }), - }, - window: { - activeTextEditor: null as any, - }, - languages: { - registerInlineCompletionItemProvider: vi.fn(), - }, - commands: { - executeCommand: vi.fn(), - }, - } -}) - -vi.mock("../AutocompleteModel", () => { - class AutocompleteModel { - public profileName = "test-profile" - - public getModelName(): string { - return "test-model" - } - - public getProviderDisplayName(): string { - return "test-provider" - } - - public hasValidCredentials(): boolean { - return true - } - } - - return { AutocompleteModel } -}) - -vi.mock("../AutocompleteStatusBar", () => { - class AutocompleteStatusBar { - public update = vi.fn() - public dispose = vi.fn() - constructor(_args: any) {} - } - return { AutocompleteStatusBar } -}) - -vi.mock("../AutocompleteCodeActionProvider", () => { - class AutocompleteCodeActionProvider {} - return { AutocompleteCodeActionProvider } -}) - -vi.mock("../classic-auto-complete/AutocompleteInlineCompletionProvider", () => { - class AutocompleteInlineCompletionProvider { - public provideInlineCompletionItems_Internal = vi.fn() - public dispose = vi.fn() - - constructor(..._args: any[]) {} - } - return { AutocompleteInlineCompletionProvider } -}) - -vi.mock("../classic-auto-complete/AutocompleteTelemetry", () => { - class AutocompleteTelemetry {} - return { AutocompleteTelemetry } -}) - -vi.mock("@roo-code/telemetry", () => ({ - TelemetryService: { - instance: { - captureEvent: vi.fn(), - }, - }, -})) - -vi.mock("../../../core/config/ContextProxy", () => { - const state: Record = {} - - const api = { - getGlobalState: (key: string) => state[key], - setValues: async (values: Record) => { - Object.assign(state, values) - }, - } - - class ContextProxy { - static instance = api - } - - const __resetState = () => { - for (const key of Object.keys(state)) delete state[key] - } - - const __setState = (values: Record) => { - Object.assign(state, values) - } - - return { ContextProxy, __resetState, __setState } -}) - -type TestCline = { - providerSettingsManager: { initialize: () => Promise } - postStateToWebview: () => Promise -} - -async function createManager(): Promise { - const { __setState } = (await import("../../../core/config/ContextProxy")) as any - - __setState({ - ghostServiceSettings: { - enableAutoTrigger: false, - enableSmartInlineTaskKeybinding: true, - }, - }) - - const context = { subscriptions: [] } as unknown as vscode.ExtensionContext - const cline: TestCline = { - providerSettingsManager: { initialize: vi.fn().mockResolvedValue(undefined) }, - postStateToWebview: vi.fn().mockResolvedValue(undefined), - } - - const connection = { onStateChange: vi.fn().mockReturnValue(() => {}), ...cline } - - const manager = new AutocompleteServiceManager(context, connection as any) - - await manager.load() - - return manager -} - -describe("AutocompleteServiceManager (less mocked logic)", () => { - beforeEach(async () => { - vi.clearAllMocks() - - const { __resetState } = (await import("../../../core/config/ContextProxy")) as any - __resetState() - ;(vscode.window as any).activeTextEditor = null - vi.mocked(vscode.languages.registerInlineCompletionItemProvider).mockReset() - - // Reset singleton instance before each test - AutocompleteServiceManager._resetInstance() - }) - - afterEach(() => { - ;(vscode.window as any).activeTextEditor = null - }) - - describe("codeSuggestion()", () => { - it("calls the provider and inserts the first completion into the editor", async () => { - const manager = await createManager() - - const document = { uri: vscode.Uri.parse("file:///test.ts") } - const position = new vscode.Position(0, 0) - const inserted: { position?: any; text?: string } = {} - - ;(vscode.window as any).activeTextEditor = { - document, - selection: { active: position }, - edit: vi.fn().mockImplementation(async (cb: any) => { - const editBuilder = { - insert: vi.fn((pos: any, text: string) => { - inserted.position = pos - inserted.text = text - }), - } - cb(editBuilder) - return true - }), - } - - const provider = manager.inlineCompletionProvider as any - provider.provideInlineCompletionItems_Internal.mockResolvedValueOnce([ - { - insertText: "// suggestion", - range: new vscode.Range(position, position), - }, - ]) - - await manager.codeSuggestion() - - expect(provider.provideInlineCompletionItems_Internal).toHaveBeenCalledWith( - document, - position, - expect.objectContaining({ - triggerKind: vscode.InlineCompletionTriggerKind.Invoke, - }), - expect.any(Object), - ) - - expect(inserted.position).toBe(position) - expect(inserted.text).toBe("// suggestion") - }) - - it("does nothing when there is no active editor", async () => { - const manager = await createManager() - - ;(vscode.window as any).activeTextEditor = null - - await manager.codeSuggestion() - - const provider = manager.inlineCompletionProvider as any - expect(provider.provideInlineCompletionItems_Internal).not.toHaveBeenCalled() - }) - }) - - describe("ensureInlineCompletionProviderRegistration()", () => { - it("registers the provider when enableAutoTrigger is true and not snoozed", async () => { - const manager = await createManager() - - const disposable = { dispose: vi.fn() } - vi.mocked(vscode.languages.registerInlineCompletionItemProvider).mockReturnValue(disposable as any) - ;(manager as any).settings = { - enableAutoTrigger: true, - enableSmartInlineTaskKeybinding: true, - } - - await (manager as any).ensureInlineCompletionProviderRegistration() - - expect(vscode.languages.registerInlineCompletionItemProvider).toHaveBeenCalledWith( - { scheme: "file" }, - manager.inlineCompletionProvider, - ) - expect((manager as any).inlineCompletionProviderDisposable).toBe(disposable) - }) - - it("does not register the provider when snoozed", async () => { - const manager = await createManager() - - vi.mocked(vscode.languages.registerInlineCompletionItemProvider).mockReturnValue({ - dispose: vi.fn(), - } as any) - ;(manager as any).settings = { - enableAutoTrigger: true, - snoozeUntil: Date.now() + 60_000, - enableSmartInlineTaskKeybinding: true, - } - - await (manager as any).ensureInlineCompletionProviderRegistration() - - expect(vscode.languages.registerInlineCompletionItemProvider).not.toHaveBeenCalled() - expect((manager as any).inlineCompletionProviderDisposable).toBeNull() - }) - - it("disposes an existing registration before applying the new registration decision", async () => { - const manager = await createManager() - - const existingDisposable = { dispose: vi.fn() } - ;(manager as any).inlineCompletionProviderDisposable = existingDisposable - ;(manager as any).settings = { - enableAutoTrigger: false, - enableSmartInlineTaskKeybinding: true, - } - - await (manager as any).ensureInlineCompletionProviderRegistration() - - expect(existingDisposable.dispose).toHaveBeenCalledTimes(1) - expect((manager as any).inlineCompletionProviderDisposable).toBeNull() - }) - }) - - describe("snooze state helpers", () => { - it("isSnoozed() returns false when snoozeUntil is not set", async () => { - const manager = await createManager() - ;(manager as any).settings = { enableAutoTrigger: true } - - expect(manager.isSnoozed()).toBe(false) - }) - - it("isSnoozed() returns false when snoozeUntil is in the past", async () => { - const manager = await createManager() - ;(manager as any).settings = { snoozeUntil: Date.now() - 1000 } - - expect(manager.isSnoozed()).toBe(false) - }) - - it("isSnoozed() returns true when snoozeUntil is in the future", async () => { - const manager = await createManager() - ;(manager as any).settings = { snoozeUntil: Date.now() + 60_000 } - - expect(manager.isSnoozed()).toBe(true) - }) - - it("getSnoozeRemainingSeconds() returns 0 when not snoozed", async () => { - const manager = await createManager() - ;(manager as any).settings = {} - - expect(manager.getSnoozeRemainingSeconds()).toBe(0) - }) - - it("getSnoozeRemainingSeconds() returns a positive number when snoozed", async () => { - const manager = await createManager() - ;(manager as any).settings = { snoozeUntil: Date.now() + 30_000 } - - const remaining = manager.getSnoozeRemainingSeconds() - expect(remaining).toBeGreaterThan(0) - expect(remaining).toBeLessThanOrEqual(30) - }) - }) -}) diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspace.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspace.spec.ts deleted file mode 100644 index c449dc410d3..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspace.spec.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest" -import * as vscode from "vscode" -import { MockWorkspace } from "./MockWorkspace" -import { createMockWorkspaceEdit } from "./MockWorkspaceEdit" - -// Mock VS Code API objects for test environment -export const mockVscode = { - Uri: { - parse: (uriString: string) => - ({ - toString: () => uriString, - fsPath: uriString.replace("file://", ""), - scheme: "file", - path: uriString.replace("file://", ""), - }) as vscode.Uri, - }, - Position: class { - constructor( - public line: number, - public character: number, - ) {} - } as any, - Range: class { - constructor( - public start: vscode.Position, - public end: vscode.Position, - ) {} - } as any, -} - -describe("MockWorkspace", () => { - let mockWorkspace: MockWorkspace - - beforeEach(() => { - mockWorkspace = new MockWorkspace() - }) - - describe("document management", () => { - it("should add and retrieve documents", () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - const content = "console.log('Hello, World!')" - - const document = mockWorkspace.addDocument(uri, content) - - expect(document.getText()).toBe(content) - expect(mockWorkspace.getDocumentContent(uri)).toBe(content) - }) - - it("should return empty string for non-existent document", () => { - const uri = mockVscode.Uri.parse("file:///nonexistent.ts") - - expect(mockWorkspace.getDocumentContent(uri)).toBe("") - }) - - it("should throw error when opening non-existent document", async () => { - const uri = mockVscode.Uri.parse("file:///nonexistent.ts") - - await expect(mockWorkspace.openTextDocument(uri)).rejects.toThrow("Document not found: file:///nonexistent.ts") - }) - - it("should open existing document", async () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - const content = "const x = 1" - - mockWorkspace.addDocument(uri, content) - const document = await mockWorkspace.openTextDocument(uri) - - expect(document.getText()).toBe(content) - }) - }) - - describe("workspace edit application", () => { - it("should apply insert operations", async () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - const originalContent = "Hello World" - mockWorkspace.addDocument(uri, originalContent) - - const edit = createMockWorkspaceEdit() - const position = new mockVscode.Position(0, 5) - edit.insert(uri, position as vscode.Position, " Beautiful") - - const success = await mockWorkspace.applyEdit(edit) - - expect(success).toBe(true) - expect(mockWorkspace.getDocumentContent(uri)).toBe("Hello Beautiful World") - }) - - it("should apply delete operations", async () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - const originalContent = "console.log('Hello')" - mockWorkspace.addDocument(uri, originalContent) - - const edit = createMockWorkspaceEdit() - const range = new mockVscode.Range(new mockVscode.Position(0, 12), new mockVscode.Position(0, 19)) - edit.delete(uri, range as vscode.Range) - - const success = await mockWorkspace.applyEdit(edit) - - expect(success).toBe(true) - expect(mockWorkspace.getDocumentContent(uri)).toBe("console.log()") - }) - - it("should apply replace operations", async () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - const originalContent = "console.log('Hello, World!')" - mockWorkspace.addDocument(uri, originalContent) - - const edit = createMockWorkspaceEdit() - const range = new mockVscode.Range(new mockVscode.Position(0, 12), new mockVscode.Position(0, 27)) - edit.replace(uri, range as vscode.Range, "'Goodbye'") - - const success = await mockWorkspace.applyEdit(edit) - - expect(success).toBe(true) - expect(mockWorkspace.getDocumentContent(uri)).toBe("console.log('Goodbye')") - }) - - it("should handle multi-line edits", async () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - const originalContent = "line1\nline2\nline3" - mockWorkspace.addDocument(uri, originalContent) - - const edit = createMockWorkspaceEdit() - const range = new mockVscode.Range(new mockVscode.Position(1, 0), new mockVscode.Position(1, 5)) - edit.replace(uri, range as vscode.Range, "modified_line2") - - const success = await mockWorkspace.applyEdit(edit) - - expect(success).toBe(true) - expect(mockWorkspace.getDocumentContent(uri)).toBe("line1\nmodified_line2\nline3") - }) - - it("should handle multiple edits in correct order", async () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - const originalContent = "ABCDEF" - mockWorkspace.addDocument(uri, originalContent) - - const edit = createMockWorkspaceEdit() - // Insert at position 2 (after 'AB') - edit.insert(uri, new mockVscode.Position(0, 2) as vscode.Position, "X") - // Insert at position 3 (after 'ABC' in original) - edit.insert(uri, new mockVscode.Position(0, 3) as vscode.Position, "Y") - - const success = await mockWorkspace.applyEdit(edit) - - expect(success).toBe(true) - // The actual result shows that edits are applied in reverse order by character position - // Position 3 edit (Y) is applied first: AB + Y + CDEF = ABYCDEF - // Position 2 edit (X) is applied second: AB + X + YCDEF = ABXYCDEF - expect(mockWorkspace.getDocumentContent(uri)).toBe("ABXCYDEF") - }) - - it("should return false for edits on non-existent documents", async () => { - const uri = mockVscode.Uri.parse("file:///nonexistent.ts") - const edit = createMockWorkspaceEdit() - edit.insert(uri, new mockVscode.Position(0, 0) as vscode.Position, "test") - - const success = await mockWorkspace.applyEdit(edit) - - expect(success).toBe(false) - }) - }) - - describe("edit tracking", () => { - it("should track applied edits", async () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - mockWorkspace.addDocument(uri, "original") - - const firstEdit = createMockWorkspaceEdit() - firstEdit.insert(uri, new mockVscode.Position(0, 0) as vscode.Position, "prefix ") - - const secondEdit = createMockWorkspaceEdit() - secondEdit.insert(uri, new mockVscode.Position(0, 8) as vscode.Position, " suffix") - - await mockWorkspace.applyEdit(firstEdit) - await mockWorkspace.applyEdit(secondEdit) - - const appliedEdits = mockWorkspace.getAppliedEdits() - expect(appliedEdits).toHaveLength(2) - }) - - it("should clear workspace state", async () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - mockWorkspace.addDocument(uri, "test") - - const edit = createMockWorkspaceEdit() - edit.insert(uri, new mockVscode.Position(0, 0) as vscode.Position, "prefix ") - - await mockWorkspace.applyEdit(edit) - expect(mockWorkspace.getAppliedEdits()).toHaveLength(1) - - mockWorkspace.clear() - expect(mockWorkspace.getAppliedEdits()).toHaveLength(0) - }) - }) - - describe("edge cases", () => { - it("should handle empty document edits", async () => { - const uri = mockVscode.Uri.parse("file:///empty.ts") - mockWorkspace.addDocument(uri, "") - - const edit = createMockWorkspaceEdit() - edit.insert(uri, new mockVscode.Position(0, 0) as vscode.Position, "first line") - - const success = await mockWorkspace.applyEdit(edit) - - expect(success).toBe(true) - expect(mockWorkspace.getDocumentContent(uri)).toBe("first line") - }) - - it("should handle edits at document boundaries", async () => { - const uri = mockVscode.Uri.parse("file:///test.ts") - const originalContent = "test" - mockWorkspace.addDocument(uri, originalContent) - - const edit = createMockWorkspaceEdit() - // Insert at the very end - edit.insert(uri, new mockVscode.Position(0, 4) as vscode.Position, " end") - - const success = await mockWorkspace.applyEdit(edit) - - expect(success).toBe(true) - expect(mockWorkspace.getDocumentContent(uri)).toBe("test end") - }) - }) -}) diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspace.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspace.ts deleted file mode 100644 index 8c1872682e0..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspace.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as vscode from "vscode" -import { MockTextDocument } from "../../mocking/MockTextDocument" - -/** - * Mock implementation of the key VSCode workspace APIs needed for testing AutocompleteWorkspaceEdit - */ -export class MockWorkspace { - private documents = new Map() - private appliedEdits: vscode.WorkspaceEdit[] = [] - - addDocument(uri: vscode.Uri, content: string): MockTextDocument { - const document = new MockTextDocument(uri, content) - this.documents.set(uri.toString(), document) - return document - } - - async openTextDocument(uri: vscode.Uri): Promise { - const document = this.documents.get(uri.toString()) - if (!document) { - throw new Error(`Document not found: ${uri.toString()}`) - } - return document - } - - async applyEdit(workspaceEdit: vscode.WorkspaceEdit): Promise { - this.appliedEdits.push(workspaceEdit) - - let allEditsSuccessful = true - - // Apply each text edit to the corresponding document - for (const [uri, textEdits] of workspaceEdit.entries()) { - const document = this.documents.get(uri.toString()) - if (!document) { - console.warn(`Document not found for edit: ${uri.toString()}`) - allEditsSuccessful = false - continue - } - - await this.applyTextEditsToDocument(document, textEdits) - } - - return allEditsSuccessful - } - - private async applyTextEditsToDocument( - document: MockTextDocument, - textEdits: readonly (vscode.TextEdit | vscode.SnippetTextEdit)[], - ): Promise { - // Sort edits by position in reverse order to avoid position shifting issues - const sortedEdits = [...textEdits] - .filter((edit): edit is vscode.TextEdit => "range" in edit && "newText" in edit) - .sort((a, b) => { - const startCompare = b.range.start.line - a.range.start.line - if (startCompare !== 0) return startCompare - return b.range.start.character - a.range.start.character - }) - - let currentContent = document.getText() - const lines = currentContent.split("\n") - - for (const edit of sortedEdits) { - const range = edit.range - const newText = edit.newText - - if (range.start.line === range.end.line) { - // Single line edit - const line = lines[range.start.line] || "" - const newLine = line.slice(0, range.start.character) + newText + line.slice(range.end.character) - lines[range.start.line] = newLine - } else { - // Multi-line edit - const startLine = lines[range.start.line] || "" - const endLine = lines[range.end.line] || "" - const newLine = startLine.slice(0, range.start.character) + newText + endLine.slice(range.end.character) - - // Remove the lines in between and replace with the new content - lines.splice(range.start.line, range.end.line - range.start.line + 1, newLine) - } - } - - // Update the document with the new content - const newContent = lines.join("\n") - document.updateContent(newContent) - } - - getDocumentContent(uri: vscode.Uri): string { - const document = this.documents.get(uri.toString()) - return document ? document.getText() : "" - } - - getAppliedEdits(): vscode.WorkspaceEdit[] { - return this.appliedEdits - } - - clear(): void { - this.documents.clear() - this.appliedEdits.length = 0 - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspaceEdit.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspaceEdit.ts deleted file mode 100644 index 57fd1bbd8f6..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspaceEdit.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as vscode from "vscode" -import { mockVscode } from "./MockWorkspace.spec" - -export function createMockWorkspaceEdit(): vscode.WorkspaceEdit { - const _edits = new Map() - - const createTextEdit = (range: vscode.Range, newText: string): vscode.TextEdit => - ({ range, newText }) as vscode.TextEdit - - return { - insert(uri: vscode.Uri, position: vscode.Position, newText: string) { - const key = uri.toString() - if (!_edits.has(key)) { - _edits.set(key, []) - } - const range = new mockVscode.Range(position, position) - _edits.get(key)!.push(createTextEdit(range as vscode.Range, newText)) - }, - - delete(uri: vscode.Uri, range: vscode.Range) { - const key = uri.toString() - if (!_edits.has(key)) { - _edits.set(key, []) - } - _edits.get(key)!.push(createTextEdit(range, "")) - }, - - replace(uri: vscode.Uri, range: vscode.Range, newText: string) { - const key = uri.toString() - if (!_edits.has(key)) { - _edits.set(key, []) - } - _edits.get(key)!.push(createTextEdit(range, newText)) - }, - - get(uri: vscode.Uri) { - return _edits.get(uri.toString()) || [] - }, - - entries() { - return Array.from(_edits.entries()).map( - ([uriString, edits]) => [mockVscode.Uri.parse(uriString), edits] as [vscode.Uri, vscode.TextEdit[]], - ) - }, - } as vscode.WorkspaceEdit -} diff --git a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts b/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts deleted file mode 100644 index f35427f1513..00000000000 --- a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { describe, it, expect, vi, beforeEach, type Mock } 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 mockClient: { commitMessage: { generate: Mock } } - - beforeEach(() => { - vi.clearAllMocks() - - mockContext = { - subscriptions: [], - } as any - - mockClient = { - commitMessage: { - generate: vi.fn().mockResolvedValue({ data: { message: "feat: add new feature" } }), - }, - } - - mockConnectionService = { - getClientAsync: vi.fn().mockResolvedValue(mockClient), - } 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 - - beforeEach(() => { - registerCommitMessageService(mockContext, mockConnectionService) - - // Extract the registered command callback - const registerCall = (vscode.commands.registerCommand as Mock).mock.calls[0]! - commandCallback = registerCall[1] as (...args: any[]) => Promise - }) - - it("shows error when git extension is not found", async () => { - ;(vscode.extensions.getExtension as Mock).mockReturnValue(undefined) - - await commandCallback() - - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Git extension not found") - }) - - it("shows error when no git repository is found", async () => { - ;(vscode.extensions.getExtension as Mock).mockReturnValue({ - isActive: true, - activate: vi.fn().mockResolvedValue(undefined), - exports: { - getAPI: () => ({ repositories: [] }), - }, - }) - - await commandCallback() - - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("No Git repository found") - }) - - it("shows error when backend fails to connect", async () => { - ;(vscode.extensions.getExtension as Mock).mockReturnValue({ - isActive: true, - activate: vi.fn().mockResolvedValue(undefined), - exports: { - getAPI: () => ({ - repositories: [{ inputBox: { value: "" }, rootUri: { fsPath: "/repo" } }], - }), - }, - }) - ;(mockConnectionService.getClientAsync as Mock).mockRejectedValue(new Error("Connect failed")) - - await commandCallback() - - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - "Failed to connect to Kilo backend. Please try again.", - ) - }) - - it("auto-connects backend and generates message when client not yet ready", async () => { - const mockInputBox = { value: "" } - ;(vscode.extensions.getExtension as Mock).mockReturnValue({ - isActive: true, - activate: vi.fn().mockResolvedValue(undefined), - exports: { - getAPI: () => ({ - repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/auto-connect-repo" } }], - }), - }, - }) - - const mockToken = { onCancellationRequested: vi.fn() } - ;(vscode.window.withProgress as Mock).mockImplementation(async (_options: any, task: any) => { - await task({}, mockToken) - }) - - await commandCallback() - - expect(mockConnectionService.getClientAsync).toHaveBeenCalled() - expect(mockInputBox.value).toBe("feat: add new feature") - }) - - it("calls commitMessage.generate on the SDK client with repository root path", async () => { - const mockInputBox = { value: "" } - ;(vscode.extensions.getExtension as Mock).mockReturnValue({ - isActive: true, - activate: vi.fn().mockResolvedValue(undefined), - exports: { - getAPI: () => ({ - repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }], - }), - }, - }) - - const mockToken = { onCancellationRequested: vi.fn() } - ;(vscode.window.withProgress as Mock).mockImplementation(async (_options: any, task: any) => { - await task({}, mockToken) - }) - - await commandCallback() - - expect(mockClient.commitMessage.generate).toHaveBeenCalledWith( - { path: "/repo", selectedFiles: undefined, previousMessage: undefined }, - expect.objectContaining({ throwOnError: true }), - ) - }) - - it("sets the generated message on the repository inputBox", async () => { - const mockInputBox = { value: "" } - ;(vscode.extensions.getExtension as Mock).mockReturnValue({ - isActive: true, - activate: vi.fn().mockResolvedValue(undefined), - exports: { - getAPI: () => ({ - repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }], - }), - }, - }) - - const mockToken = { onCancellationRequested: vi.fn() } - ;(vscode.window.withProgress as Mock).mockImplementation(async (_options: any, task: any) => { - await task({}, mockToken) - }) - - await commandCallback() - - expect(mockInputBox.value).toBe("feat: add new feature") - }) - - it("shows cancellable progress in SourceControl location", async () => { - const mockInputBox = { value: "" } - ;(vscode.extensions.getExtension as Mock).mockReturnValue({ - isActive: true, - activate: vi.fn().mockResolvedValue(undefined), - exports: { - getAPI: () => ({ - repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }], - }), - }, - }) - - const mockToken = { onCancellationRequested: vi.fn() } - ;(vscode.window.withProgress as Mock).mockImplementation(async (_options: any, task: any) => { - await task({}, mockToken) - }) - - await commandCallback() - - expect(vscode.window.withProgress).toHaveBeenCalledWith( - expect.objectContaining({ - location: vscode.ProgressLocation.SourceControl, - title: "Generating commit message...", - cancellable: true, - }), - expect.any(Function), - ) - }) - - it("uses the matching repository when SourceControl arg is provided", async () => { - const mainInputBox = { value: "" } - const worktreeInputBox = { value: "" } - vi.mocked(vscode.extensions.getExtension).mockReturnValue({ - isActive: true, - activate: vi.fn().mockResolvedValue(undefined), - exports: { - getAPI: () => ({ - repositories: [ - { inputBox: mainInputBox, rootUri: { fsPath: "/main-repo" } }, - { inputBox: worktreeInputBox, rootUri: { fsPath: "/worktree-repo" } }, - ], - }), - }, - } as any) - - vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { - await task({} as any, { onCancellationRequested: vi.fn() } as any) - }) - - // Simulate SCM title/input passing the SourceControl for the worktree repo - const scmArg = { rootUri: { fsPath: "/worktree-repo" } } as vscode.SourceControl - await commandCallback(scmArg) - - // The worktree repo's inputBox should be updated, not the main repo's - expect(worktreeInputBox.value).toBe("feat: add new feature") - expect(mainInputBox.value).toBe("") - }) - - it("falls back to first repository when SourceControl arg has no match", async () => { - const mainInputBox = { value: "" } - vi.mocked(vscode.extensions.getExtension).mockReturnValue({ - isActive: true, - activate: vi.fn().mockResolvedValue(undefined), - exports: { - getAPI: () => ({ - repositories: [{ inputBox: mainInputBox, rootUri: { fsPath: "/main-repo" } }], - }), - }, - } as any) - - vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { - await task({} as any, { onCancellationRequested: vi.fn() } as any) - }) - - const scmArg = { rootUri: { fsPath: "/nonexistent-repo" } } as vscode.SourceControl - await commandCallback(scmArg) - - expect(mainInputBox.value).toBe("feat: add new feature") - }) - }) -}) diff --git a/packages/kilo-vscode/tests/setup/vscode-mock.ts b/packages/kilo-vscode/tests/setup/vscode-mock.ts index 9ab5ff42032..082d4968c9e 100644 --- a/packages/kilo-vscode/tests/setup/vscode-mock.ts +++ b/packages/kilo-vscode/tests/setup/vscode-mock.ts @@ -70,6 +70,10 @@ const mockVscode = { tabGroups: { all: [] }, showTextDocument: async () => {}, showWarningMessage: async () => undefined, + showErrorMessage: async () => undefined, + showInformationMessage: async () => undefined, + withProgress: async (_options: unknown, task: (...args: unknown[]) => unknown) => + task({}, { onCancellationRequested: noop }), createTerminal: () => ({ show: noop, sendText: noop, dispose: noop }), createStatusBarItem: () => ({ text: "", @@ -85,6 +89,21 @@ const mockVscode = { registerCommand: () => ({ dispose: noop }), executeCommand: async () => {}, }, + languages: { + registerInlineCompletionItemProvider: () => ({ dispose: noop }), + registerCodeActionsProvider: () => ({ dispose: noop }), + }, + ProgressLocation: { + SourceControl: 1, + Window: 10, + Notification: 15, + }, + InlineCompletionTriggerKind: { Invoke: 0, Automatic: 1 }, + CancellationTokenSource: class { + token = { isCancellationRequested: false, onCancellationRequested: noop } + cancel = noop + dispose = noop + }, CodeAction: class { command?: { command: string; title: string } isPreferred?: boolean diff --git a/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts b/packages/kilo-vscode/tests/unit/agent-manager-provider-worktree.test.ts similarity index 78% rename from packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts rename to packages/kilo-vscode/tests/unit/agent-manager-provider-worktree.test.ts index 0c4dd458fcf..0e1cc8f4a6b 100644 --- a/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-provider-worktree.test.ts @@ -1,70 +1,17 @@ -import { describe, it, expect, vi } from "vitest" - -vi.mock("../WorktreeManager", () => ({ - WorktreeManager: class {}, -})) - -vi.mock("../WorktreeStateManager", () => ({ - WorktreeStateManager: class {}, -})) - -vi.mock("../GitStatsPoller", () => ({ - GitStatsPoller: class { - setEnabled() {} - stop() {} - }, -})) - -vi.mock("../GitOps", () => ({ - GitOps: class {}, -})) - -vi.mock("../SetupScriptService", () => ({ - SetupScriptService: class { - hasScript() { - return false - } - }, -})) - -vi.mock("../SetupScriptRunner", () => ({ - SetupScriptRunner: class { - async runIfConfigured() { - return false - } - }, -})) - -vi.mock("../SessionTerminalManager", () => ({ - SessionTerminalManager: class { - showTerminal() {} - showLocalTerminal() {} - syncLocalOnSessionSwitch() {} - syncOnSessionSwitch() { - return false - } - dispose() {} - }, -})) - -vi.mock("../terminal-host", () => ({ - createTerminalHost: () => ({}), -})) - -vi.mock("../format-keybinding", () => ({ - formatKeybinding: (value: string) => value, -})) - -vi.mock("../branch-name", () => ({ - versionedName: () => ({ branch: "branch", label: "label" }), -})) - -vi.mock("../git-import", () => ({ - normalizePath: (value: string) => value, -})) - -import { AgentManagerProvider } from "../AgentManagerProvider" -import type { Host, OutputHandle } from "../host" +import { describe, it, expect, mock } from "bun:test" + +// The tests build an AgentManagerProvider harness via Object.create, so the +// constructor never runs and none of its collaborators are instantiated. The +// real modules import cleanly under the shared vscode preload, so we skip the +// sprawling per-dep mock.module() dance the original vitest spec used — those +// mocks would collide with sibling test files that exercise the same modules +// for real (e.g. tests/unit/setup-script-service.test.ts). +const { AgentManagerProvider } = await import("../../src/agent-manager/AgentManagerProvider") +type Host = import("../../src/agent-manager/host").Host +type OutputHandle = import("../../src/agent-manager/host").OutputHandle + +// Local shim so the remaining test body reads naturally. +const vi = { fn: mock } function createMockHost(): Host { return { diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/contextualSkip.spec.ts b/packages/kilo-vscode/tests/unit/autocomplete-contextual-skip.test.ts similarity index 98% rename from packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/contextualSkip.spec.ts rename to packages/kilo-vscode/tests/unit/autocomplete-contextual-skip.test.ts index 8b6dde6497f..849fd97f675 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/contextualSkip.spec.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-contextual-skip.test.ts @@ -1,4 +1,8 @@ -import { getTerminatorsForLanguage, shouldSkipAutocomplete } from "../contextualSkip" +import { describe, it, expect } from "bun:test" +import { + getTerminatorsForLanguage, + shouldSkipAutocomplete, +} from "../../src/services/autocomplete/classic-auto-complete/contextualSkip" /** * Tests for shouldSkipAutocomplete behavior. diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/language-filters/__tests__/markdown.spec.ts b/packages/kilo-vscode/tests/unit/autocomplete-language-filter-markdown.test.ts similarity index 93% rename from packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/language-filters/__tests__/markdown.spec.ts rename to packages/kilo-vscode/tests/unit/autocomplete-language-filter-markdown.test.ts index 9c1d3f97ecb..5e139f6061c 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/language-filters/__tests__/markdown.spec.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-language-filter-markdown.test.ts @@ -1,4 +1,8 @@ -import { isInsideCodeBlock, removeSpuriousNewlinesBeforeCodeBlockClosingFences } from "../markdown" +import { describe, it, expect } from "bun:test" +import { + isInsideCodeBlock, + removeSpuriousNewlinesBeforeCodeBlockClosingFences, +} from "../../src/services/autocomplete/classic-auto-complete/language-filters/markdown" describe("isInsideCodeBlock", () => { it("returns false for normal markdown", () => { diff --git a/packages/kilo-vscode/tests/unit/autocomplete-model.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-model.test.ts new file mode 100644 index 00000000000..eea274559c8 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/autocomplete-model.test.ts @@ -0,0 +1,194 @@ +import { describe, it, expect, mock, beforeEach } from "bun:test" + +// vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts) +const { AutocompleteModel } = await import("../../src/services/autocomplete/AutocompleteModel") +type KiloConnectionService = import("../../src/services/cli-backend").KiloConnectionService + +const fim = mock() + +const client = { + kilo: { fim }, +} + +function makeConnection(state: "connecting" | "connected" | "disconnected" | "error" = "connected") { + return { + getConnectionState: () => state, + getClient: () => client, + getClientAsync: + state === "connected" + ? () => Promise.resolve(client) + : () => Promise.reject(new Error(`CLI backend is not connected (state: ${state})`)), + onStateChange: () => () => {}, + } as unknown as KiloConnectionService +} + +describe("AutocompleteModel", () => { + beforeEach(() => { + fim.mockReset() + }) + + describe("constructor", () => { + it("defaults profileName and profileType to null", () => { + const m = new AutocompleteModel() + expect(m.profileName).toBeNull() + expect(m.profileType).toBeNull() + }) + }) + + describe("setConnectionService", () => { + it("sets the connection service after construction", () => { + const m = new AutocompleteModel() + expect(m.hasValidCredentials()).toBe(false) + + m.setConnectionService(makeConnection("connected")) + expect(m.hasValidCredentials()).toBe(true) + }) + }) + + describe("hasValidCredentials", () => { + it("returns true when connected", () => { + const m = new AutocompleteModel(makeConnection("connected")) + expect(m.hasValidCredentials()).toBe(true) + }) + + it("returns false when disconnected", () => { + const m = new AutocompleteModel(makeConnection("disconnected")) + expect(m.hasValidCredentials()).toBe(false) + }) + + it("returns false when connecting", () => { + const m = new AutocompleteModel(makeConnection("connecting")) + expect(m.hasValidCredentials()).toBe(false) + }) + + it("returns false when in error state", () => { + const m = new AutocompleteModel(makeConnection("error")) + expect(m.hasValidCredentials()).toBe(false) + }) + + it("returns false without connection service", () => { + const m = new AutocompleteModel() + expect(m.hasValidCredentials()).toBe(false) + }) + }) + + describe("getModelName", () => { + it("returns the default model", () => { + const m = new AutocompleteModel() + expect(m.getModelName()).toBe("mistralai/codestral-2508") + }) + }) + + describe("getProviderDisplayName", () => { + it("returns the default provider", () => { + const m = new AutocompleteModel() + expect(m.getProviderDisplayName()).toBe("Mistral AI") + }) + + it("returns the selected provider", () => { + const m = new AutocompleteModel() + m.setModel("inception/mercury-edit") + + expect(m.getProviderDisplayName()).toBe("Inception") + }) + }) + + describe("generateFimResponse", () => { + it("throws when connection service is not available", async () => { + const m = new AutocompleteModel() + await expect(m.generateFimResponse("prefix", "suffix", mock())).rejects.toThrow( + "Connection service is not available", + ) + }) + + it("throws when not connected", async () => { + const m = new AutocompleteModel(makeConnection("disconnected")) + await expect(m.generateFimResponse("prefix", "suffix", mock())).rejects.toThrow("CLI backend is not connected") + }) + + it("streams chunks and returns metadata", async () => { + const chunks = [ + { choices: [{ delta: { content: "hello" } }] }, + { + choices: [{ delta: { content: " world" } }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + cost: 0.001, + }, + ] + + fim.mockResolvedValue({ + stream: (async function* () { + for (const chunk of chunks) yield chunk + })(), + }) + + const m = new AutocompleteModel(makeConnection("connected")) + const received: string[] = [] + const result = await m.generateFimResponse("prefix", "suffix", (text) => received.push(text)) + + expect(received).toEqual(["hello", " world"]) + expect(result).toEqual({ + cost: 0.001, + inputTokens: 10, + outputTokens: 5, + cacheWriteTokens: 0, + cacheReadTokens: 0, + }) + }) + + it("streams text-completion chunks", async () => { + const chunks = [{ choices: [{ text: "hello" }] }, { choices: [{ text: " world" }] }] + + fim.mockResolvedValue({ + stream: (async function* () { + for (const chunk of chunks) yield chunk + })(), + }) + + const m = new AutocompleteModel(makeConnection("connected")) + const received: string[] = [] + await m.generateFimResponse("prefix", "suffix", (text) => received.push(text)) + + expect(received).toEqual(["hello", " world"]) + }) + + it("passes model parameters to fim call", async () => { + fim.mockResolvedValue({ + stream: (async function* () {})(), + }) + + const m = new AutocompleteModel(makeConnection("connected")) + const signal = new AbortController().signal + await m.generateFimResponse("pre", "suf", mock(), signal) + + expect(fim).toHaveBeenCalledWith( + { + prefix: "pre", + suffix: "suf", + model: "mistralai/codestral-2508", + maxTokens: 256, + temperature: 0.2, + }, + expect.objectContaining({ signal }), + ) + }) + + it("passes selected model parameters to fim call", async () => { + fim.mockResolvedValue({ + stream: (async function* () {})(), + }) + + const m = new AutocompleteModel(makeConnection("connected")) + m.setModel("inception/mercury-edit") + await m.generateFimResponse("pre", "suf", mock()) + + expect(fim).toHaveBeenCalledWith( + expect.objectContaining({ + model: "inception/mercury-edit", + temperature: 0, + }), + expect.any(Object), + ) + }) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts new file mode 100644 index 00000000000..db99e1cc8f3 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts @@ -0,0 +1,240 @@ +import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" +import * as vscode from "vscode" + +// Keep the shared vscode preload in place; just instrument the bits we observe. +const registerInlineCompletionItemProvider = spyOn( + vscode.languages, + "registerInlineCompletionItemProvider", +).mockImplementation(() => ({ dispose: mock() })) + +function setActiveEditor(editor: unknown) { + ;(vscode.window as unknown as { activeTextEditor: unknown }).activeTextEditor = editor +} + +mock.module("../../src/services/autocomplete/AutocompleteModel", () => ({ + AutocompleteModel: class { + profileName = "test-profile" + setModel() {} + getModelName() { + return "test-model" + } + getProviderDisplayName() { + return "test-provider" + } + hasValidCredentials() { + return true + } + }, +})) + +mock.module("../../src/services/autocomplete/AutocompleteStatusBar", () => ({ + AutocompleteStatusBar: class { + update = mock() + dispose = mock() + }, +})) + +mock.module("../../src/services/autocomplete/AutocompleteCodeActionProvider", () => ({ + AutocompleteCodeActionProvider: class {}, +})) + +mock.module("../../src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider", () => ({ + AutocompleteInlineCompletionProvider: class { + provideInlineCompletionItems_Internal = mock() + resetBackoff = mock() + dispose = mock() + }, +})) + +mock.module("../../src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry", () => ({ + AutocompleteTelemetry: class {}, +})) + +mock.module("../../src/services/telemetry", () => ({ + TelemetryProxy: { capture: mock() }, + TelemetryEventName: { INLINE_ASSIST_AUTO_TASK: "inline_assist_auto_task", GHOST_SERVICE_DISABLED: "disabled" }, +})) + +const { AutocompleteServiceManager } = await import("../../src/services/autocomplete/AutocompleteServiceManager") + +function createManager() { + const context = { subscriptions: [] } as unknown as import("vscode").ExtensionContext + const connection = { + onStateChange: mock(() => () => {}), + onEventFiltered: mock(() => () => {}), + } + return new AutocompleteServiceManager(context, connection as never) +} + +describe("AutocompleteServiceManager", () => { + beforeEach(() => { + registerInlineCompletionItemProvider.mockClear() + setActiveEditor(null) + AutocompleteServiceManager._resetInstance() + }) + + afterEach(() => { + setActiveEditor(null) + }) + + describe("codeSuggestion()", () => { + it("calls the provider and inserts the first completion into the editor", async () => { + const manager = createManager() + + const document = { uri: vscode.Uri.parse("file:///test.ts") } + const position = new vscode.Position(0, 0) + const inserted: { position?: unknown; text?: string } = {} + + setActiveEditor({ + document, + selection: { active: position }, + edit: mock(async (cb: (builder: unknown) => void) => { + cb({ + insert: (pos: unknown, text: string) => { + inserted.position = pos + inserted.text = text + }, + }) + return true + }), + }) + + const provider = manager.inlineCompletionProvider as unknown as { + provideInlineCompletionItems_Internal: ReturnType + } + provider.provideInlineCompletionItems_Internal.mockResolvedValueOnce([ + { insertText: "// suggestion", range: new vscode.Range(position, position) }, + ]) + + await manager.codeSuggestion() + + expect(provider.provideInlineCompletionItems_Internal).toHaveBeenCalledWith( + document, + position, + expect.objectContaining({ triggerKind: vscode.InlineCompletionTriggerKind.Invoke }), + expect.any(Object), + ) + + expect(inserted.position).toBe(position) + expect(inserted.text).toBe("// suggestion") + }) + + it("does nothing when there is no active editor", async () => { + const manager = createManager() + + setActiveEditor(null) + + await manager.codeSuggestion() + + const provider = manager.inlineCompletionProvider as unknown as { + provideInlineCompletionItems_Internal: ReturnType + } + expect(provider.provideInlineCompletionItems_Internal).not.toHaveBeenCalled() + }) + }) + + describe("ensureInlineCompletionProviderRegistration()", () => { + it("registers the provider when enableAutoTrigger is true and not snoozed", async () => { + const manager = createManager() + + const disposable = { dispose: mock() } + registerInlineCompletionItemProvider.mockReturnValue(disposable) + ;(manager as unknown as { settings: unknown }).settings = { + enableAutoTrigger: true, + enableSmartInlineTaskKeybinding: true, + } + + await ( + manager as unknown as { ensureInlineCompletionProviderRegistration(): Promise } + ).ensureInlineCompletionProviderRegistration() + + expect(registerInlineCompletionItemProvider).toHaveBeenCalledWith( + { scheme: "file" }, + manager.inlineCompletionProvider, + ) + expect( + (manager as unknown as { inlineCompletionProviderDisposable: unknown }).inlineCompletionProviderDisposable, + ).toBe(disposable) + }) + + it("does not register the provider when snoozed", async () => { + const manager = createManager() + + registerInlineCompletionItemProvider.mockClear() + ;(manager as unknown as { settings: unknown }).settings = { + enableAutoTrigger: true, + snoozeUntil: Date.now() + 60_000, + enableSmartInlineTaskKeybinding: true, + } + + await ( + manager as unknown as { ensureInlineCompletionProviderRegistration(): Promise } + ).ensureInlineCompletionProviderRegistration() + + expect(registerInlineCompletionItemProvider).not.toHaveBeenCalled() + expect( + (manager as unknown as { inlineCompletionProviderDisposable: unknown }).inlineCompletionProviderDisposable, + ).toBeNull() + }) + + it("disposes an existing registration before applying the new registration decision", async () => { + const manager = createManager() + + const existing = { dispose: mock() } + ;(manager as unknown as { inlineCompletionProviderDisposable: unknown }).inlineCompletionProviderDisposable = + existing + ;(manager as unknown as { settings: unknown }).settings = { + enableAutoTrigger: false, + enableSmartInlineTaskKeybinding: true, + } + + await ( + manager as unknown as { ensureInlineCompletionProviderRegistration(): Promise } + ).ensureInlineCompletionProviderRegistration() + + expect(existing.dispose).toHaveBeenCalledTimes(1) + expect( + (manager as unknown as { inlineCompletionProviderDisposable: unknown }).inlineCompletionProviderDisposable, + ).toBeNull() + }) + }) + + describe("snooze state helpers", () => { + it("isSnoozed() returns false when snoozeUntil is not set", () => { + const manager = createManager() + ;(manager as unknown as { settings: unknown }).settings = { enableAutoTrigger: true } + + expect(manager.isSnoozed()).toBe(false) + }) + + it("isSnoozed() returns false when snoozeUntil is in the past", () => { + const manager = createManager() + ;(manager as unknown as { settings: unknown }).settings = { snoozeUntil: Date.now() - 1000 } + + expect(manager.isSnoozed()).toBe(false) + }) + + it("isSnoozed() returns true when snoozeUntil is in the future", () => { + const manager = createManager() + ;(manager as unknown as { settings: unknown }).settings = { snoozeUntil: Date.now() + 60_000 } + + expect(manager.isSnoozed()).toBe(true) + }) + + it("getSnoozeRemainingSeconds() returns 0 when not snoozed", () => { + const manager = createManager() + ;(manager as unknown as { settings: unknown }).settings = {} + + expect(manager.getSnoozeRemainingSeconds()).toBe(0) + }) + + it("getSnoozeRemainingSeconds() returns a positive number when snoozed", () => { + const manager = createManager() + ;(manager as unknown as { settings: unknown }).settings = { snoozeUntil: Date.now() + 30_000 } + + const remaining = manager.getSnoozeRemainingSeconds() + expect(remaining).toBeGreaterThan(0) + expect(remaining).toBeLessThanOrEqual(30) + }) + }) +}) diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts b/packages/kilo-vscode/tests/unit/autocomplete-settings.test.ts similarity index 57% rename from packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts rename to packages/kilo-vscode/tests/unit/autocomplete-settings.test.ts index 51eccf7f3f2..139ce22ee85 100644 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-settings.test.ts @@ -1,22 +1,25 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" +import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test" +import * as vscode from "vscode" const state = new Map() -const update = vi.fn(async (key: string, value: unknown) => { +const update = mock(async (key: string, value: unknown) => { state.set(key, value) }) -vi.mock("vscode", () => ({ - ConfigurationTarget: { - Global: 1, - }, - workspace: { - getConfiguration: vi.fn(() => ({ - get: vi.fn((key: string, fallback: unknown) => state.get(key) ?? fallback), +// Piggy-back on the shared vscode preload (tests/setup/vscode-mock.ts) instead of +// calling mock.module("vscode", ...) here — a whole-module override would leak +// into other test files that need the preload's richer surface. +spyOn(vscode.workspace, "getConfiguration").mockImplementation( + () => + ({ + get: (key: string, fallback: unknown) => state.get(key) ?? fallback, update, - })), - onDidChangeConfiguration: vi.fn(), - }, -})) + }) as unknown as vscode.WorkspaceConfiguration, +) + +const { buildAutocompleteSettingsMessage, routeAutocompleteMessage } = await import( + "../../src/services/autocomplete/settings" +) describe("autocomplete settings", () => { beforeEach(() => { @@ -24,29 +27,26 @@ describe("autocomplete settings", () => { update.mockClear() }) - it("includes the configured model in loaded settings", async () => { + it("includes the configured model in loaded settings", () => { state.set("model", "inception/mercury-edit") - const { buildAutocompleteSettingsMessage } = await import("../settings") expect(buildAutocompleteSettingsMessage().settings.model).toBe("inception/mercury-edit") }) it("persists supported model updates", async () => { - const post = vi.fn() - const { routeAutocompleteMessage } = await import("../settings") + const post = mock() await routeAutocompleteMessage( { type: "updateAutocompleteSetting", key: "model", value: "inception/mercury-edit" }, post, ) - expect(update).toHaveBeenCalledWith("model", "inception/mercury-edit", 1) + expect(update).toHaveBeenCalledWith("model", "inception/mercury-edit", vscode.ConfigurationTarget.Global) expect(post).toHaveBeenCalledWith(expect.objectContaining({ type: "autocompleteSettingsLoaded" })) }) it("rejects unsupported model updates", async () => { - const post = vi.fn() - const { routeAutocompleteMessage } = await import("../settings") + const post = mock() await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "model", value: "other/model" }, post) @@ -55,8 +55,7 @@ describe("autocomplete settings", () => { }) it("rejects non-boolean toggle updates", async () => { - const post = vi.fn() - const { routeAutocompleteMessage } = await import("../settings") + const post = mock() await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "enableAutoTrigger", value: "true" }, post) diff --git a/packages/kilo-vscode/src/services/autocomplete/context/__tests__/VisibleCodeTracker.spec.ts b/packages/kilo-vscode/tests/unit/autocomplete-visible-code-tracker.test.ts similarity index 86% rename from packages/kilo-vscode/src/services/autocomplete/context/__tests__/VisibleCodeTracker.spec.ts rename to packages/kilo-vscode/tests/unit/autocomplete-visible-code-tracker.test.ts index 1a19bdc7bd6..ee156bcd82e 100644 --- a/packages/kilo-vscode/src/services/autocomplete/context/__tests__/VisibleCodeTracker.spec.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-visible-code-tracker.test.ts @@ -1,21 +1,23 @@ -import { describe, it, expect, vi, beforeEach } from "vitest" +import { describe, it, expect, mock, beforeEach, spyOn } from "bun:test" import * as vscode from "vscode" -import { VisibleCodeTracker } from "../VisibleCodeTracker" - -// Mock vscode module -vi.mock("vscode", () => ({ - window: { - visibleTextEditors: [], - activeTextEditor: null, - }, -})) -vi.mock("../../../../services/autocomplete/continuedev/core/indexing/ignore", () => ({ - isSecurityConcern: vi.fn((filePath: string) => { - return filePath.includes(".env") || filePath.includes("credentials") - }), +// Piggy-back on the shared vscode preload rather than replacing the module — +// mock.module("vscode") would leak into other test files' expectations. +spyOn(vscode.workspace, "asRelativePath").mockImplementation((p: string | vscode.Uri) => { + const path = typeof p === "string" ? p : p.fsPath + return path.replace(/^\/workspace\//, "") +}) +;(vscode.window as unknown as { visibleTextEditors: unknown[] }).visibleTextEditors = [] +;(vscode.window as unknown as { activeTextEditor: unknown }).activeTextEditor = null + +mock.module("../../src/services/autocomplete/continuedev/core/indexing/ignore", () => ({ + isSecurityConcern: (filePath: string) => filePath.includes(".env") || filePath.includes("credentials"), })) +const { VisibleCodeTracker } = await import("../../src/services/autocomplete/context/VisibleCodeTracker") +// Alias vi to Bun's mock so tests below keep reading naturally. +const vi = { fn: mock, clearAllMocks: () => mock.clearAllMocks() } + describe("VisibleCodeTracker", () => { const mockWorkspacePath = "/workspace" diff --git a/packages/kilo-vscode/tests/unit/commit-message.test.ts b/packages/kilo-vscode/tests/unit/commit-message.test.ts new file mode 100644 index 00000000000..618e6d40e0c --- /dev/null +++ b/packages/kilo-vscode/tests/unit/commit-message.test.ts @@ -0,0 +1,194 @@ +import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test" +import * as vscode from "vscode" + +// Augment the shared vscode preload with the bits the service uses. Using spyOn +// keeps the override local — a whole mock.module("vscode", ...) would leak into +// every other test file. +let lastRegistration: { command: string; cb: (...args: unknown[]) => unknown } | null = null +const registerCommand = spyOn(vscode.commands, "registerCommand").mockImplementation((command: string, cb: unknown) => { + lastRegistration = { command, cb: cb as (...args: unknown[]) => unknown } + return { dispose: mock() } +}) +const showErrorMessage = spyOn(vscode.window, "showErrorMessage").mockImplementation( + () => Promise.resolve(undefined) as ReturnType, +) +const withProgress = spyOn(vscode.window, "withProgress").mockImplementation( + async (_options: unknown, task: (...args: unknown[]) => unknown) => { + await task({}, { onCancellationRequested: mock() }) + return undefined as never + }, +) +const getExtension = spyOn(vscode.extensions, "getExtension").mockImplementation(() => undefined) + +const { registerCommitMessageService } = await import("../../src/services/commit-message") + +type KiloConnectionService = import("../../src/services/cli-backend/connection-service").KiloConnectionService + +function makeExtension(repositories: Array<{ inputBox: { value: string }; rootUri: { fsPath: string } }>) { + return { + isActive: true, + activate: mock(() => Promise.resolve()), + exports: { + getAPI: () => ({ repositories }), + }, + } +} + +describe("commit-message service", () => { + let context: import("vscode").ExtensionContext + let connection: KiloConnectionService + let client: { commitMessage: { generate: ReturnType } } + + beforeEach(() => { + registerCommand.mockClear() + showErrorMessage.mockClear() + withProgress.mockClear() + getExtension.mockReset().mockImplementation(() => undefined) + lastRegistration = null + + context = { subscriptions: [] } as unknown as import("vscode").ExtensionContext + + client = { + commitMessage: { + generate: mock(() => Promise.resolve({ data: { message: "feat: add new feature" } })), + }, + } + + connection = { + getClientAsync: mock(() => Promise.resolve(client)), + } as unknown as KiloConnectionService + }) + + describe("registerCommitMessageService", () => { + it("returns an array of disposables", () => { + const disposables = registerCommitMessageService(context, connection) + + expect(Array.isArray(disposables)).toBe(true) + expect(disposables.length).toBeGreaterThan(0) + }) + + it("registers the kilo-code.new.generateCommitMessage command", () => { + registerCommitMessageService(context, connection) + + expect(registerCommand).toHaveBeenCalledWith("kilo-code.new.generateCommitMessage", expect.any(Function)) + }) + + it("pushes the command disposable to context.subscriptions", () => { + registerCommitMessageService(context, connection) + + expect(context.subscriptions.length).toBe(1) + }) + }) + + describe("command execution", () => { + function invoke(...args: unknown[]) { + registerCommitMessageService(context, connection) + if (!lastRegistration) throw new Error("command was not registered") + return lastRegistration.cb(...args) as Promise + } + + it("shows error when git extension is not found", async () => { + getExtension.mockImplementation(() => undefined) + + await invoke() + + expect(showErrorMessage).toHaveBeenCalledWith("Git extension not found") + }) + + it("shows error when no git repository is found", async () => { + getExtension.mockImplementation(() => makeExtension([]) as never) + + await invoke() + + expect(showErrorMessage).toHaveBeenCalledWith("No Git repository found") + }) + + it("shows error when backend fails to connect", async () => { + getExtension.mockImplementation( + () => makeExtension([{ inputBox: { value: "" }, rootUri: { fsPath: "/repo" } }]) as never, + ) + connection.getClientAsync = mock(() => Promise.reject(new Error("Connect failed"))) + + await invoke() + + expect(showErrorMessage).toHaveBeenCalledWith("Failed to connect to Kilo backend. Please try again.") + }) + + it("auto-connects backend and generates message when client not yet ready", async () => { + const inputBox = { value: "" } + getExtension.mockImplementation( + () => makeExtension([{ inputBox, rootUri: { fsPath: "/auto-connect-repo" } }]) as never, + ) + + await invoke() + + expect(connection.getClientAsync).toHaveBeenCalled() + expect(inputBox.value).toBe("feat: add new feature") + }) + + it("calls commitMessage.generate on the SDK client with repository root path", async () => { + const inputBox = { value: "" } + getExtension.mockImplementation(() => makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as never) + + await invoke() + + expect(client.commitMessage.generate).toHaveBeenCalledWith( + { path: "/repo", selectedFiles: undefined, previousMessage: undefined }, + expect.objectContaining({ throwOnError: true }), + ) + }) + + it("sets the generated message on the repository inputBox", async () => { + const inputBox = { value: "" } + getExtension.mockImplementation(() => makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as never) + + await invoke() + + expect(inputBox.value).toBe("feat: add new feature") + }) + + it("shows cancellable progress in SourceControl location", async () => { + const inputBox = { value: "" } + getExtension.mockImplementation(() => makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as never) + + await invoke() + + expect(withProgress).toHaveBeenCalledWith( + expect.objectContaining({ + location: vscode.ProgressLocation.SourceControl, + title: "Generating commit message...", + cancellable: true, + }), + expect.any(Function), + ) + }) + + it("uses the matching repository when SourceControl arg is provided", async () => { + const main = { value: "" } + const worktree = { value: "" } + getExtension.mockImplementation( + () => + makeExtension([ + { inputBox: main, rootUri: { fsPath: "/main-repo" } }, + { inputBox: worktree, rootUri: { fsPath: "/worktree-repo" } }, + ]) as never, + ) + + await invoke({ rootUri: { fsPath: "/worktree-repo" } }) + + expect(worktree.value).toBe("feat: add new feature") + expect(main.value).toBe("") + }) + + it("falls back to first repository when SourceControl arg has no match", async () => { + const main = { value: "" } + getExtension.mockImplementation( + () => makeExtension([{ inputBox: main, rootUri: { fsPath: "/main-repo" } }]) as never, + ) + + await invoke({ rootUri: { fsPath: "/nonexistent-repo" } }) + + expect(main.value).toBe("feat: add new feature") + }) + }) +}) diff --git a/packages/kilo-vscode/src/agent-manager/__tests__/SetupScriptService.spec.ts b/packages/kilo-vscode/tests/unit/setup-script-service.test.ts similarity index 96% rename from packages/kilo-vscode/src/agent-manager/__tests__/SetupScriptService.spec.ts rename to packages/kilo-vscode/tests/unit/setup-script-service.test.ts index 5714761bd7c..0972379852d 100644 --- a/packages/kilo-vscode/src/agent-manager/__tests__/SetupScriptService.spec.ts +++ b/packages/kilo-vscode/tests/unit/setup-script-service.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect } from "vitest" +import { describe, it, expect } from "bun:test" import * as fs from "node:fs" import * as os from "node:os" import * as path from "node:path" -import { SetupScriptService } from "../SetupScriptService" +import { SetupScriptService } from "../../src/agent-manager/SetupScriptService" function setupRoot(): string { return fs.mkdtempSync(path.join(os.tmpdir(), "kilo-setup-script-"))