From 70d1f494e50432be71ffb8e9854d6fdd5ccd4207 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:04:06 +0000 Subject: [PATCH 01/12] test(vscode): run AutocompleteModel tests in CI The file sat under src/**/__tests__/ and was not covered by `bun test tests/unit/`, so none of the AutocompleteModel tests were running in CI. Port to bun:test and move next to the rest of the unit suite. --- .../__tests__/AutocompleteModel.spec.ts | 204 ------------------ .../tests/unit/autocomplete-model.test.ts | 194 +++++++++++++++++ 2 files changed, 194 insertions(+), 204 deletions(-) delete mode 100644 packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts create mode 100644 packages/kilo-vscode/tests/unit/autocomplete-model.test.ts 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/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), + ) + }) + }) +}) From f59dccab55c7d0189507594bed5b46c67edb92b2 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:04:10 +0000 Subject: [PATCH 02/12] test(vscode): run autocomplete settings tests in CI Same story as the AutocompleteModel port: the spec under src/__tests__/ was not picked up by `bun test tests/unit/`. Move to tests/unit/ and swap vi.mock for mock.module so the suite exercises the new validation rules on every PR. --- .../unit/autocomplete-settings.test.ts} | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) rename packages/kilo-vscode/{src/services/autocomplete/__tests__/settings.spec.ts => tests/unit/autocomplete-settings.test.ts} (62%) 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 62% 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..de3e9b95db6 100644 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-settings.test.ts @@ -1,20 +1,22 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" +import { beforeEach, describe, expect, it, mock } from "bun:test" 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", () => ({ +// Overrides the shared vscode preload (tests/setup/vscode-mock.ts) so we can +// drive getConfiguration/update against a stateful backing store. +mock.module("vscode", () => ({ ConfigurationTarget: { Global: 1, }, workspace: { - getConfiguration: vi.fn(() => ({ - get: vi.fn((key: string, fallback: unknown) => state.get(key) ?? fallback), + getConfiguration: () => ({ + get: (key: string, fallback: unknown) => state.get(key) ?? fallback, update, - })), - onDidChangeConfiguration: vi.fn(), + }), + onDidChangeConfiguration: () => ({ dispose: () => {} }), }, })) @@ -26,14 +28,14 @@ describe("autocomplete settings", () => { it("includes the configured model in loaded settings", async () => { state.set("model", "inception/mercury-edit") - const { buildAutocompleteSettingsMessage } = await import("../settings") + const { buildAutocompleteSettingsMessage } = await import("../../src/services/autocomplete/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() + const { routeAutocompleteMessage } = await import("../../src/services/autocomplete/settings") await routeAutocompleteMessage( { type: "updateAutocompleteSetting", key: "model", value: "inception/mercury-edit" }, @@ -45,8 +47,8 @@ describe("autocomplete settings", () => { }) it("rejects unsupported model updates", async () => { - const post = vi.fn() - const { routeAutocompleteMessage } = await import("../settings") + const post = mock() + const { routeAutocompleteMessage } = await import("../../src/services/autocomplete/settings") await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "model", value: "other/model" }, post) @@ -55,8 +57,8 @@ describe("autocomplete settings", () => { }) it("rejects non-boolean toggle updates", async () => { - const post = vi.fn() - const { routeAutocompleteMessage } = await import("../settings") + const post = mock() + const { routeAutocompleteMessage } = await import("../../src/services/autocomplete/settings") await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "enableAutoTrigger", value: "true" }, post) From d7c82a30f6479e793c64a6ea2c5038d285ada0b8 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:51:46 +0000 Subject: [PATCH 03/12] test(vscode): run SetupScriptService tests in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the spec from src/agent-manager/__tests__/ to tests/unit/ so `bun run test:unit` picks it up. Mechanical port — only swap the vitest import for bun:test and re-anchor the source import. --- .../unit/setup-script-service.test.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename packages/kilo-vscode/{src/agent-manager/__tests__/SetupScriptService.spec.ts => tests/unit/setup-script-service.test.ts} (96%) 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-")) From 0d5db421bfa34eba4cde496874c11527a7cf758d Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:52:52 +0000 Subject: [PATCH 04/12] test(vscode): run autocomplete contextual-skip tests in CI Move the spec from src/__tests__/ to tests/unit/ so `bun run test:unit` picks it up. --- .../unit/autocomplete-contextual-skip.test.ts} | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) rename packages/kilo-vscode/{src/services/autocomplete/classic-auto-complete/__tests__/contextualSkip.spec.ts => tests/unit/autocomplete-contextual-skip.test.ts} (98%) 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. From 3c7875f6ccb29fa82e1344ea7577ef01875402d9 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:53:46 +0000 Subject: [PATCH 05/12] test(vscode): run autocomplete markdown filter tests in CI Move the spec from src/__tests__/ to tests/unit/ so `bun run test:unit` picks it up. --- .../unit/autocomplete-language-filter-markdown.test.ts} | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) rename packages/kilo-vscode/{src/services/autocomplete/classic-auto-complete/language-filters/__tests__/markdown.spec.ts => tests/unit/autocomplete-language-filter-markdown.test.ts} (93%) 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", () => { From c325827cbf86e70709774e0f0eb4e2eef3b45afe Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:55:41 +0000 Subject: [PATCH 06/12] test(vscode): run VisibleCodeTracker tests in CI Move the spec from src/__tests__/ to tests/unit/ so `bun run test:unit` picks it up. The original vitest mock omitted `workspace.asRelativePath` which the tracker calls on every editor; add it so the happy-path test actually exercises the relative-path fallback. --- ...autocomplete-visible-code-tracker.test.ts} | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) rename packages/kilo-vscode/{src/services/autocomplete/context/__tests__/VisibleCodeTracker.spec.ts => tests/unit/autocomplete-visible-code-tracker.test.ts} (88%) 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 88% 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..5696b3a38d0 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,25 @@ -import { describe, it, expect, vi, beforeEach } from "vitest" -import * as vscode from "vscode" -import { VisibleCodeTracker } from "../VisibleCodeTracker" +import { describe, it, expect, mock, beforeEach } from "bun:test" -// Mock vscode module -vi.mock("vscode", () => ({ +// Replace the shared vscode preload with a minimal shape this file drives directly. +mock.module("vscode", () => ({ window: { - visibleTextEditors: [], + visibleTextEditors: [] as unknown[], activeTextEditor: null, }, + workspace: { + asRelativePath: (p: string) => p.replace(/^\/workspace\//, ""), + }, })) -vi.mock("../../../../services/autocomplete/continuedev/core/indexing/ignore", () => ({ - isSecurityConcern: vi.fn((filePath: string) => { - return filePath.includes(".env") || filePath.includes("credentials") - }), +mock.module("../../src/services/autocomplete/continuedev/core/indexing/ignore", () => ({ + isSecurityConcern: (filePath: string) => filePath.includes(".env") || filePath.includes("credentials"), })) +const vscode = (await import("vscode")) as typeof import("vscode") +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" From 2fd1dc7d9527319d6dfe2fb97dc73c34788563c0 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:57:08 +0000 Subject: [PATCH 07/12] test(vscode): run commit-message service tests in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite the spec for bun:test: drive a hand-rolled vscode stub via mock.module (the shared preload omits ProgressLocation) and grab the command callback from the registerCommand spy instead of relying on vitest's hoisted vi.mock transform. Behavior coverage is preserved — all 12 original scenarios still run. --- .../commit-message/__tests__/index.spec.ts | 287 ------------------ .../tests/unit/commit-message.test.ts | 200 ++++++++++++ 2 files changed, 200 insertions(+), 287 deletions(-) delete mode 100644 packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts create mode 100644 packages/kilo-vscode/tests/unit/commit-message.test.ts 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/unit/commit-message.test.ts b/packages/kilo-vscode/tests/unit/commit-message.test.ts new file mode 100644 index 00000000000..e23eed1bfb0 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/commit-message.test.ts @@ -0,0 +1,200 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test" + +// Hand-rolled vscode shim so we can observe interactions without pulling in +// the shared preload's generic stub (it omits ProgressLocation etc.). +const registerCommand = mock((_command: string, cb: (...args: unknown[]) => unknown) => { + lastRegistration = { command: _command, cb } + return { dispose: mock() } +}) +const showErrorMessage = mock(() => Promise.resolve(undefined)) +const withProgress = mock(async (_options: unknown, task: (...args: unknown[]) => unknown) => { + await task({}, { onCancellationRequested: mock() }) +}) +const getExtension = mock(() => undefined) + +let lastRegistration: { command: string; cb: (...args: unknown[]) => unknown } | null = null + +mock.module("vscode", () => ({ + commands: { registerCommand }, + window: { showErrorMessage, withProgress }, + workspace: { workspaceFolders: [{ uri: { fsPath: "/test/workspace" } }] }, + extensions: { getExtension }, + ProgressLocation: { SourceControl: 1 }, + Uri: { parse: (s: string) => ({ fsPath: s }) }, +})) + +const vscode = (await import("vscode")) as typeof import("vscode") +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() + 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(vscode.commands.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.mockReturnValue(undefined) + + await invoke() + + expect(showErrorMessage).toHaveBeenCalledWith("Git extension not found") + }) + + it("shows error when no git repository is found", async () => { + getExtension.mockReturnValue(makeExtension([]) as unknown as undefined) + + await invoke() + + expect(showErrorMessage).toHaveBeenCalledWith("No Git repository found") + }) + + it("shows error when backend fails to connect", async () => { + getExtension.mockReturnValue( + makeExtension([{ inputBox: { value: "" }, rootUri: { fsPath: "/repo" } }]) as unknown as undefined, + ) + 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.mockReturnValue( + makeExtension([{ inputBox, rootUri: { fsPath: "/auto-connect-repo" } }]) as unknown as undefined, + ) + + 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.mockReturnValue(makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as unknown as undefined) + + 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.mockReturnValue(makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as unknown as undefined) + + await invoke() + + expect(inputBox.value).toBe("feat: add new feature") + }) + + it("shows cancellable progress in SourceControl location", async () => { + const inputBox = { value: "" } + getExtension.mockReturnValue(makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as unknown as undefined) + + 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.mockReturnValue( + makeExtension([ + { inputBox: main, rootUri: { fsPath: "/main-repo" } }, + { inputBox: worktree, rootUri: { fsPath: "/worktree-repo" } }, + ]) as unknown as undefined, + ) + + 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.mockReturnValue( + makeExtension([{ inputBox: main, rootUri: { fsPath: "/main-repo" } }]) as unknown as undefined, + ) + + await invoke({ rootUri: { fsPath: "/nonexistent-repo" } }) + + expect(main.value).toBe("feat: add new feature") + }) + }) +}) From f1464946ec3c0093c6ce59df46c487af39d16351 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:01:08 +0000 Subject: [PATCH 08/12] test(vscode): run AgentManagerProvider worktree tests in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the spec from src/__tests__/ to tests/unit/ using bun:test. Since mock.module isn't hoisted like vitest's vi.mock, the mocks are declared before the dynamic import of AgentManagerProvider. Had to flesh out the git-import, WorktreeStateManager, and format-keybinding stubs with the named exports the provider actually imports — the original mocks only covered a subset and relied on vitest leaving missing exports undefined. --- .../agent-manager-provider-worktree.test.ts} | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) rename packages/kilo-vscode/{src/agent-manager/__tests__/AgentManagerProvider.spec.ts => tests/unit/agent-manager-provider-worktree.test.ts} (81%) 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 81% 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..7b9a1e9da7f 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,41 +1,34 @@ -import { describe, it, expect, vi } from "vitest" +import { describe, it, expect, mock } from "bun:test" -vi.mock("../WorktreeManager", () => ({ - WorktreeManager: class {}, -})) +const base = "../../src/agent-manager" -vi.mock("../WorktreeStateManager", () => ({ +mock.module(`${base}/WorktreeManager`, () => ({ WorktreeManager: class {} })) +mock.module(`${base}/WorktreeStateManager`, () => ({ WorktreeStateManager: class {}, + remoteRef: (branch: string) => branch, })) - -vi.mock("../GitStatsPoller", () => ({ +mock.module(`${base}/GitStatsPoller`, () => ({ GitStatsPoller: class { setEnabled() {} stop() {} }, })) - -vi.mock("../GitOps", () => ({ - GitOps: class {}, -})) - -vi.mock("../SetupScriptService", () => ({ +mock.module(`${base}/GitOps`, () => ({ GitOps: class {} })) +mock.module(`${base}/SetupScriptService`, () => ({ SetupScriptService: class { hasScript() { return false } }, })) - -vi.mock("../SetupScriptRunner", () => ({ +mock.module(`${base}/SetupScriptRunner`, () => ({ SetupScriptRunner: class { async runIfConfigured() { return false } }, })) - -vi.mock("../SessionTerminalManager", () => ({ +mock.module(`${base}/SessionTerminalManager`, () => ({ SessionTerminalManager: class { showTerminal() {} showLocalTerminal() {} @@ -46,25 +39,24 @@ vi.mock("../SessionTerminalManager", () => ({ dispose() {} }, })) - -vi.mock("../terminal-host", () => ({ - createTerminalHost: () => ({}), -})) - -vi.mock("../format-keybinding", () => ({ +mock.module(`${base}/terminal-host`, () => ({ createTerminalHost: () => ({}) })) +mock.module(`${base}/format-keybinding`, () => ({ formatKeybinding: (value: string) => value, + buildKeybindingMap: () => ({}), })) - -vi.mock("../branch-name", () => ({ - versionedName: () => ({ branch: "branch", label: "label" }), -})) - -vi.mock("../git-import", () => ({ +mock.module(`${base}/branch-name`, () => ({ versionedName: () => ({ branch: "branch", label: "label" }) })) +mock.module(`${base}/git-import`, () => ({ normalizePath: (value: string) => value, + classifyWorktreeError: (err: unknown) => ({ kind: "unknown" as const, error: err }), + classifyPRError: () => "unknown" as const, })) -import { AgentManagerProvider } from "../AgentManagerProvider" -import type { Host, OutputHandle } from "../host" +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 { From b09bfa56c08a11c9c218f48539b805c6ebd58ada Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:02:26 +0000 Subject: [PATCH 09/12] test(vscode): run AutocompleteServiceManager tests in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite for bun:test. The original spec wired in a `__setState` / `__resetState` harness around a `core/config/ContextProxy` module that no longer exists in the tree — those mocks were vestigial since `AutocompleteServiceManager` reads settings from vscode.workspace directly. Drop the dead setup and keep the codeSuggestion, snooze, and inline-provider registration assertions. --- .../AutocompleteServiceManager.spec.ts | 344 ------------------ .../unit/autocomplete-service-manager.test.ts | 285 +++++++++++++++ 2 files changed, 285 insertions(+), 344 deletions(-) delete mode 100644 packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts create mode 100644 packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts 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/tests/unit/autocomplete-service-manager.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts new file mode 100644 index 00000000000..33d6e3a4201 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts @@ -0,0 +1,285 @@ +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" + +const registerInlineCompletionItemProvider = mock(() => ({ dispose: mock() })) + +let activeTextEditor: unknown = null + +mock.module("vscode", () => { + class Position { + constructor( + public line: number, + public character: number, + ) {} + } + class Range { + constructor( + public start: unknown, + public end: unknown, + ) {} + } + class CancellationTokenSource { + token = { isCancellationRequested: false, onCancellationRequested: mock() } + dispose = mock() + } + + return { + Uri: { + parse: (uri: string) => ({ + toString: () => uri, + fsPath: uri.replace("file://", ""), + scheme: "file", + path: uri.replace("file://", ""), + }), + }, + Position, + Range, + CancellationTokenSource, + InlineCompletionTriggerKind: { Invoke: 1 }, + ConfigurationTarget: { Global: 1 }, + window: { + get activeTextEditor() { + return activeTextEditor + }, + showWarningMessage: mock(() => Promise.resolve(undefined)), + }, + workspace: { + workspaceFolders: [{ uri: { fsPath: "/repo" } }], + getConfiguration: () => ({ + get: (_key: string, fallback?: unknown) => fallback, + update: mock(() => Promise.resolve()), + }), + }, + languages: { registerInlineCompletionItemProvider }, + commands: { executeCommand: mock(() => Promise.resolve()) }, + env: { openExternal: mock() }, + } +}) + +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 vscode = (await import("vscode")) as typeof import("vscode") +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() + activeTextEditor = null + AutocompleteServiceManager._resetInstance() + }) + + afterEach(() => { + activeTextEditor = 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 } = {} + + activeTextEditor = { + 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() + + activeTextEditor = 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) + }) + }) +}) From a172f9cabdf1c80265f19540584a041f575fbc1c Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:02:31 +0000 Subject: [PATCH 10/12] test(vscode): drop dead MockWorkspace spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The helper imports `../../mocking/MockTextDocument`, which was deleted long ago — the spec has been un-runnable in any runner since. The only consumer of MockWorkspace / MockWorkspaceEdit was this spec, so remove the whole shim along with it. --- .../__tests__/MockWorkspace.spec.ts | 226 ------------------ .../autocomplete/__tests__/MockWorkspace.ts | 99 -------- .../__tests__/MockWorkspaceEdit.ts | 46 ---- 3 files changed, 371 deletions(-) delete mode 100644 packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspace.spec.ts delete mode 100644 packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspace.ts delete mode 100644 packages/kilo-vscode/src/services/autocomplete/__tests__/MockWorkspaceEdit.ts 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 -} From da1901059e69ddc6a21b843341ddc9ab05029e18 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:12:53 +0000 Subject: [PATCH 11/12] test(vscode): isolate ported tests from each other's vscode mocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When several freshly-ported suites called `mock.module("vscode", ...)` at file scope, the last registered factory won globally — so running e.g. autocomplete-visible-code-tracker before autocomplete-settings erased `ConfigurationTarget`, and mocking agent-manager collaborators clobbered setup-script-service's own import. Fix: - Expand the shared preload (tests/setup/vscode-mock.ts) with the bits our suites need (`languages`, `ProgressLocation`, `InlineCompletionTriggerKind`, `showErrorMessage`, `withProgress`, `CancellationTokenSource`) so no one needs to replace the module. - Swap the per-file `mock.module("vscode", ...)` overrides for `spyOn` + direct property assignment on the preload shape. - Drop the agent-manager dependency mocks — the provider is stubbed out via `Object.create`, so real module imports don't run constructors and the mocks only existed to pre-empt deps that load fine. All 119 tests in the ported files now pass in any order, and `bun run test:unit` stays green at 2090 / 2090. --- .../kilo-vscode/tests/setup/vscode-mock.ts | 19 +++++ .../agent-manager-provider-worktree.test.ts | 57 ++----------- .../unit/autocomplete-service-manager.test.ts | 63 +++------------ .../tests/unit/autocomplete-settings.test.ts | 35 ++++---- .../autocomplete-visible-code-tracker.test.ts | 24 +++--- .../tests/unit/commit-message.test.ts | 80 +++++++++---------- 6 files changed, 99 insertions(+), 179 deletions(-) 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/tests/unit/agent-manager-provider-worktree.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-provider-worktree.test.ts index 7b9a1e9da7f..0e1cc8f4a6b 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-provider-worktree.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-provider-worktree.test.ts @@ -1,56 +1,11 @@ import { describe, it, expect, mock } from "bun:test" -const base = "../../src/agent-manager" - -mock.module(`${base}/WorktreeManager`, () => ({ WorktreeManager: class {} })) -mock.module(`${base}/WorktreeStateManager`, () => ({ - WorktreeStateManager: class {}, - remoteRef: (branch: string) => branch, -})) -mock.module(`${base}/GitStatsPoller`, () => ({ - GitStatsPoller: class { - setEnabled() {} - stop() {} - }, -})) -mock.module(`${base}/GitOps`, () => ({ GitOps: class {} })) -mock.module(`${base}/SetupScriptService`, () => ({ - SetupScriptService: class { - hasScript() { - return false - } - }, -})) -mock.module(`${base}/SetupScriptRunner`, () => ({ - SetupScriptRunner: class { - async runIfConfigured() { - return false - } - }, -})) -mock.module(`${base}/SessionTerminalManager`, () => ({ - SessionTerminalManager: class { - showTerminal() {} - showLocalTerminal() {} - syncLocalOnSessionSwitch() {} - syncOnSessionSwitch() { - return false - } - dispose() {} - }, -})) -mock.module(`${base}/terminal-host`, () => ({ createTerminalHost: () => ({}) })) -mock.module(`${base}/format-keybinding`, () => ({ - formatKeybinding: (value: string) => value, - buildKeybindingMap: () => ({}), -})) -mock.module(`${base}/branch-name`, () => ({ versionedName: () => ({ branch: "branch", label: "label" }) })) -mock.module(`${base}/git-import`, () => ({ - normalizePath: (value: string) => value, - classifyWorktreeError: (err: unknown) => ({ kind: "unknown" as const, error: err }), - classifyPRError: () => "unknown" as const, -})) - +// 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 diff --git a/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts index 33d6e3a4201..b1d15352409 100644 --- a/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts @@ -1,58 +1,16 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" +import * as vscode from "vscode" -const registerInlineCompletionItemProvider = mock(() => ({ dispose: mock() })) +// Keep the shared vscode preload in place; just instrument the bits we observe. +const registerInlineCompletionItemProvider = spyOn( + vscode.languages, + "registerInlineCompletionItemProvider", +).mockImplementation(() => ({ dispose: mock() })) let activeTextEditor: unknown = null - -mock.module("vscode", () => { - class Position { - constructor( - public line: number, - public character: number, - ) {} - } - class Range { - constructor( - public start: unknown, - public end: unknown, - ) {} - } - class CancellationTokenSource { - token = { isCancellationRequested: false, onCancellationRequested: mock() } - dispose = mock() - } - - return { - Uri: { - parse: (uri: string) => ({ - toString: () => uri, - fsPath: uri.replace("file://", ""), - scheme: "file", - path: uri.replace("file://", ""), - }), - }, - Position, - Range, - CancellationTokenSource, - InlineCompletionTriggerKind: { Invoke: 1 }, - ConfigurationTarget: { Global: 1 }, - window: { - get activeTextEditor() { - return activeTextEditor - }, - showWarningMessage: mock(() => Promise.resolve(undefined)), - }, - workspace: { - workspaceFolders: [{ uri: { fsPath: "/repo" } }], - getConfiguration: () => ({ - get: (_key: string, fallback?: unknown) => fallback, - update: mock(() => Promise.resolve()), - }), - }, - languages: { registerInlineCompletionItemProvider }, - commands: { executeCommand: mock(() => Promise.resolve()) }, - env: { openExternal: mock() }, - } +Object.defineProperty(vscode.window, "activeTextEditor", { + configurable: true, + get: () => activeTextEditor, }) mock.module("../../src/services/autocomplete/AutocompleteModel", () => ({ @@ -99,7 +57,6 @@ mock.module("../../src/services/telemetry", () => ({ TelemetryEventName: { INLINE_ASSIST_AUTO_TASK: "inline_assist_auto_task", GHOST_SERVICE_DISABLED: "disabled" }, })) -const vscode = (await import("vscode")) as typeof import("vscode") const { AutocompleteServiceManager } = await import("../../src/services/autocomplete/AutocompleteServiceManager") function createManager() { diff --git a/packages/kilo-vscode/tests/unit/autocomplete-settings.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-settings.test.ts index de3e9b95db6..139ce22ee85 100644 --- a/packages/kilo-vscode/tests/unit/autocomplete-settings.test.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-settings.test.ts @@ -1,24 +1,25 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test" +import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test" +import * as vscode from "vscode" const state = new Map() const update = mock(async (key: string, value: unknown) => { state.set(key, value) }) -// Overrides the shared vscode preload (tests/setup/vscode-mock.ts) so we can -// drive getConfiguration/update against a stateful backing store. -mock.module("vscode", () => ({ - ConfigurationTarget: { - Global: 1, - }, - workspace: { - getConfiguration: () => ({ +// 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: () => ({ dispose: () => {} }), - }, -})) + }) as unknown as vscode.WorkspaceConfiguration, +) + +const { buildAutocompleteSettingsMessage, routeAutocompleteMessage } = await import( + "../../src/services/autocomplete/settings" +) describe("autocomplete settings", () => { beforeEach(() => { @@ -26,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("../../src/services/autocomplete/settings") expect(buildAutocompleteSettingsMessage().settings.model).toBe("inception/mercury-edit") }) it("persists supported model updates", async () => { const post = mock() - const { routeAutocompleteMessage } = await import("../../src/services/autocomplete/settings") 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 = mock() - const { routeAutocompleteMessage } = await import("../../src/services/autocomplete/settings") await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "model", value: "other/model" }, post) @@ -58,7 +56,6 @@ describe("autocomplete settings", () => { it("rejects non-boolean toggle updates", async () => { const post = mock() - const { routeAutocompleteMessage } = await import("../../src/services/autocomplete/settings") await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "enableAutoTrigger", value: "true" }, post) diff --git a/packages/kilo-vscode/tests/unit/autocomplete-visible-code-tracker.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-visible-code-tracker.test.ts index 5696b3a38d0..ee156bcd82e 100644 --- a/packages/kilo-vscode/tests/unit/autocomplete-visible-code-tracker.test.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-visible-code-tracker.test.ts @@ -1,21 +1,19 @@ -import { describe, it, expect, mock, beforeEach } from "bun:test" - -// Replace the shared vscode preload with a minimal shape this file drives directly. -mock.module("vscode", () => ({ - window: { - visibleTextEditors: [] as unknown[], - activeTextEditor: null, - }, - workspace: { - asRelativePath: (p: string) => p.replace(/^\/workspace\//, ""), - }, -})) +import { describe, it, expect, mock, beforeEach, spyOn } from "bun:test" +import * as vscode from "vscode" + +// 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 vscode = (await import("vscode")) as typeof import("vscode") 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() } diff --git a/packages/kilo-vscode/tests/unit/commit-message.test.ts b/packages/kilo-vscode/tests/unit/commit-message.test.ts index e23eed1bfb0..618e6d40e0c 100644 --- a/packages/kilo-vscode/tests/unit/commit-message.test.ts +++ b/packages/kilo-vscode/tests/unit/commit-message.test.ts @@ -1,29 +1,25 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test" +import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test" +import * as vscode from "vscode" -// Hand-rolled vscode shim so we can observe interactions without pulling in -// the shared preload's generic stub (it omits ProgressLocation etc.). -const registerCommand = mock((_command: string, cb: (...args: unknown[]) => unknown) => { - lastRegistration = { command: _command, cb } +// 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 = mock(() => Promise.resolve(undefined)) -const withProgress = mock(async (_options: unknown, task: (...args: unknown[]) => unknown) => { - await task({}, { onCancellationRequested: mock() }) -}) -const getExtension = mock(() => undefined) - -let lastRegistration: { command: string; cb: (...args: unknown[]) => unknown } | null = null +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) -mock.module("vscode", () => ({ - commands: { registerCommand }, - window: { showErrorMessage, withProgress }, - workspace: { workspaceFolders: [{ uri: { fsPath: "/test/workspace" } }] }, - extensions: { getExtension }, - ProgressLocation: { SourceControl: 1 }, - Uri: { parse: (s: string) => ({ fsPath: s }) }, -})) - -const vscode = (await import("vscode")) as typeof import("vscode") const { registerCommitMessageService } = await import("../../src/services/commit-message") type KiloConnectionService = import("../../src/services/cli-backend/connection-service").KiloConnectionService @@ -47,7 +43,7 @@ describe("commit-message service", () => { registerCommand.mockClear() showErrorMessage.mockClear() withProgress.mockClear() - getExtension.mockReset() + getExtension.mockReset().mockImplementation(() => undefined) lastRegistration = null context = { subscriptions: [] } as unknown as import("vscode").ExtensionContext @@ -74,10 +70,7 @@ describe("commit-message service", () => { it("registers the kilo-code.new.generateCommitMessage command", () => { registerCommitMessageService(context, connection) - expect(vscode.commands.registerCommand).toHaveBeenCalledWith( - "kilo-code.new.generateCommitMessage", - expect.any(Function), - ) + expect(registerCommand).toHaveBeenCalledWith("kilo-code.new.generateCommitMessage", expect.any(Function)) }) it("pushes the command disposable to context.subscriptions", () => { @@ -95,7 +88,7 @@ describe("commit-message service", () => { } it("shows error when git extension is not found", async () => { - getExtension.mockReturnValue(undefined) + getExtension.mockImplementation(() => undefined) await invoke() @@ -103,7 +96,7 @@ describe("commit-message service", () => { }) it("shows error when no git repository is found", async () => { - getExtension.mockReturnValue(makeExtension([]) as unknown as undefined) + getExtension.mockImplementation(() => makeExtension([]) as never) await invoke() @@ -111,8 +104,8 @@ describe("commit-message service", () => { }) it("shows error when backend fails to connect", async () => { - getExtension.mockReturnValue( - makeExtension([{ inputBox: { value: "" }, rootUri: { fsPath: "/repo" } }]) as unknown as undefined, + getExtension.mockImplementation( + () => makeExtension([{ inputBox: { value: "" }, rootUri: { fsPath: "/repo" } }]) as never, ) connection.getClientAsync = mock(() => Promise.reject(new Error("Connect failed"))) @@ -123,8 +116,8 @@ describe("commit-message service", () => { it("auto-connects backend and generates message when client not yet ready", async () => { const inputBox = { value: "" } - getExtension.mockReturnValue( - makeExtension([{ inputBox, rootUri: { fsPath: "/auto-connect-repo" } }]) as unknown as undefined, + getExtension.mockImplementation( + () => makeExtension([{ inputBox, rootUri: { fsPath: "/auto-connect-repo" } }]) as never, ) await invoke() @@ -135,7 +128,7 @@ describe("commit-message service", () => { it("calls commitMessage.generate on the SDK client with repository root path", async () => { const inputBox = { value: "" } - getExtension.mockReturnValue(makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as unknown as undefined) + getExtension.mockImplementation(() => makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as never) await invoke() @@ -147,7 +140,7 @@ describe("commit-message service", () => { it("sets the generated message on the repository inputBox", async () => { const inputBox = { value: "" } - getExtension.mockReturnValue(makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as unknown as undefined) + getExtension.mockImplementation(() => makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as never) await invoke() @@ -156,7 +149,7 @@ describe("commit-message service", () => { it("shows cancellable progress in SourceControl location", async () => { const inputBox = { value: "" } - getExtension.mockReturnValue(makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as unknown as undefined) + getExtension.mockImplementation(() => makeExtension([{ inputBox, rootUri: { fsPath: "/repo" } }]) as never) await invoke() @@ -173,11 +166,12 @@ describe("commit-message service", () => { it("uses the matching repository when SourceControl arg is provided", async () => { const main = { value: "" } const worktree = { value: "" } - getExtension.mockReturnValue( - makeExtension([ - { inputBox: main, rootUri: { fsPath: "/main-repo" } }, - { inputBox: worktree, rootUri: { fsPath: "/worktree-repo" } }, - ]) as unknown as undefined, + getExtension.mockImplementation( + () => + makeExtension([ + { inputBox: main, rootUri: { fsPath: "/main-repo" } }, + { inputBox: worktree, rootUri: { fsPath: "/worktree-repo" } }, + ]) as never, ) await invoke({ rootUri: { fsPath: "/worktree-repo" } }) @@ -188,8 +182,8 @@ describe("commit-message service", () => { it("falls back to first repository when SourceControl arg has no match", async () => { const main = { value: "" } - getExtension.mockReturnValue( - makeExtension([{ inputBox: main, rootUri: { fsPath: "/main-repo" } }]) as unknown as undefined, + getExtension.mockImplementation( + () => makeExtension([{ inputBox: main, rootUri: { fsPath: "/main-repo" } }]) as never, ) await invoke({ rootUri: { fsPath: "/nonexistent-repo" } }) From f48858cce2e334dce29457c9bba0afc1d1ecdb28 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:18:11 +0000 Subject: [PATCH 12/12] test(vscode): don't redefine vscode.window.activeTextEditor as getter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Object.defineProperty(..., { get })` turned activeTextEditor into a read-only property, so when another ported file (sorted earlier alpha, e.g. autocomplete-visible-code-tracker) later tried to assign to it under the shared preload, Bun threw "Attempted to assign to readonly property" on CI. Swap it for a plain setter helper that writes through the preload's existing writable slot — keeps the tests working regardless of file load order. --- .../unit/autocomplete-service-manager.test.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts index b1d15352409..db99e1cc8f3 100644 --- a/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-service-manager.test.ts @@ -7,11 +7,9 @@ const registerInlineCompletionItemProvider = spyOn( "registerInlineCompletionItemProvider", ).mockImplementation(() => ({ dispose: mock() })) -let activeTextEditor: unknown = null -Object.defineProperty(vscode.window, "activeTextEditor", { - configurable: true, - get: () => activeTextEditor, -}) +function setActiveEditor(editor: unknown) { + ;(vscode.window as unknown as { activeTextEditor: unknown }).activeTextEditor = editor +} mock.module("../../src/services/autocomplete/AutocompleteModel", () => ({ AutocompleteModel: class { @@ -71,12 +69,12 @@ function createManager() { describe("AutocompleteServiceManager", () => { beforeEach(() => { registerInlineCompletionItemProvider.mockClear() - activeTextEditor = null + setActiveEditor(null) AutocompleteServiceManager._resetInstance() }) afterEach(() => { - activeTextEditor = null + setActiveEditor(null) }) describe("codeSuggestion()", () => { @@ -87,7 +85,7 @@ describe("AutocompleteServiceManager", () => { const position = new vscode.Position(0, 0) const inserted: { position?: unknown; text?: string } = {} - activeTextEditor = { + setActiveEditor({ document, selection: { active: position }, edit: mock(async (cb: (builder: unknown) => void) => { @@ -99,7 +97,7 @@ describe("AutocompleteServiceManager", () => { }) return true }), - } + }) const provider = manager.inlineCompletionProvider as unknown as { provideInlineCompletionItems_Internal: ReturnType @@ -124,7 +122,7 @@ describe("AutocompleteServiceManager", () => { it("does nothing when there is no active editor", async () => { const manager = createManager() - activeTextEditor = null + setActiveEditor(null) await manager.codeSuggestion()