test(vscode): share unit vscode mock for next edit

This commit is contained in:
Mark IJbema
2026-05-27 16:24:44 +02:00
parent 774b090905
commit b5aaa821e6
4 changed files with 31 additions and 65 deletions
+1 -1
View File
@@ -1009,7 +1009,7 @@
"check-kilocode-change": "! grep -rIn 'kilocode_change' . ../kilo-ui/ --exclude='package.json' --exclude='*.md' --exclude-dir='node_modules' --exclude-dir='dist' | grep -v '`kilocode_change`'",
"lint": "eslint src webview-ui",
"test": "vscode-test",
"test:unit": "bun test tests/unit/ && bun test tests/next-edit/",
"test:unit": "bun test tests/unit/",
"rebuild-sdk": "bun run --cwd ../sdk/js build",
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build -o storybook-static",
@@ -46,6 +46,10 @@ const mockVscode = {
version: "1.90.0",
workspace: {
workspaceFolders: [{ uri: { fsPath: "/repo" } }],
textDocuments: [] as Array<unknown>,
onDidOpenTextDocument: () => ({ dispose: noop }),
onDidChangeTextDocument: () => ({ dispose: noop }),
onDidCloseTextDocument: () => ({ dispose: noop }),
getConfiguration: () => ({
get: <T>(_key: string, value?: T) => value,
update: async () => {},
@@ -134,6 +138,13 @@ const mockVscode = {
public end: { line: number; character: number },
) {}
},
InlineCompletionItem: class {
constructor(
public insertText: string,
public range?: unknown,
public command?: unknown,
) {}
},
Disposable: class {
constructor(private callback: () => void = noop) {}
dispose() {
@@ -1,24 +1,7 @@
import { describe, expect, it, vi } from "vitest"
import { afterEach, describe, expect, it } from "bun:test"
import * as vscode from "vscode"
import { EditHistoryTracker } from "../../src/services/autocomplete/next-edit/editHistoryTracker"
vi.mock("vscode", () => {
const opens: Array<(doc: unknown) => void> = []
return {
workspace: {
textDocuments: [],
asRelativePath: (uri: { fsPath: string }) => uri.fsPath.replace("/workspace/", ""),
onDidOpenTextDocument: (cb: (doc: unknown) => void) => {
opens.push(cb)
return { dispose: vi.fn() }
},
onDidChangeTextDocument: () => ({ dispose: vi.fn() }),
onDidCloseTextDocument: () => ({ dispose: vi.fn() }),
open: (doc: unknown) => opens.forEach((cb) => cb(doc)),
},
}
})
type Doc = vscode.TextDocument & { setText(text: string): void }
function doc(path: string, initial: string): Doc {
@@ -32,19 +15,23 @@ function doc(path: string, initial: string): Doc {
} as unknown as Doc
}
function docs(...items: Doc[]): void {
;(vscode.workspace.textDocuments as unknown as Doc[]).splice(0, Infinity, ...items)
}
function settle(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0))
}
afterEach(() => docs())
describe("EditHistoryTracker", () => {
it("retains chronological edits across files for Mercury context", async () => {
const tracker = new EditHistoryTracker({ isFileAllowed: async () => true })
const a = doc("/workspace/a.ts", "const a = 1\n")
const b = doc("/workspace/b.ts", "const b = 1\n")
const open = (vscode.workspace as unknown as { open(doc: vscode.TextDocument): void }).open
docs(a, b)
const tracker = new EditHistoryTracker({ isFileAllowed: async () => true })
open(a)
open(b)
await settle()
a.setText("const a = 2\n")
await tracker.flush(a)
@@ -62,11 +49,10 @@ describe("EditHistoryTracker", () => {
})
it("does not retain edits when the access policy is missing at runtime", async () => {
const tracker = new EditHistoryTracker({} as { isFileAllowed: (path: string) => Promise<boolean> })
const a = doc("/workspace/a.ts", "const a = 1\n")
const open = (vscode.workspace as unknown as { open(doc: vscode.TextDocument): void }).open
docs(a)
const tracker = new EditHistoryTracker({} as { isFileAllowed: (path: string) => Promise<boolean> })
open(a)
await settle()
a.setText("const a = 2\n")
await tracker.flush(a)
@@ -77,13 +63,11 @@ describe("EditHistoryTracker", () => {
it("never returns edits from denied documents", async () => {
const denied = new Set(["/workspace/.env"])
const tracker = new EditHistoryTracker({ isFileAllowed: async (path) => !denied.has(path) })
const safe = doc("/workspace/app.ts", "const safe = 1\n")
const secret = doc("/workspace/.env", "TOKEN=old\n")
const open = (vscode.workspace as unknown as { open(doc: vscode.TextDocument): void }).open
docs(safe, secret)
const tracker = new EditHistoryTracker({ isFileAllowed: async (path) => !denied.has(path) })
open(safe)
open(secret)
await settle()
secret.setText("TOKEN=secret\n")
await tracker.flush(secret)
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest"
import { describe, expect, it, mock } from "bun:test"
import * as vscode from "vscode"
import type { KiloConnectionService } from "../../src/services/cli-backend"
import {
@@ -7,35 +7,6 @@ import {
} from "../../src/services/autocomplete/next-edit/NextEditInlineCompletionProvider"
import type { NextEditSuggestionManager } from "../../src/services/autocomplete/next-edit/NextEditSuggestionManager"
vi.mock("vscode", () => {
class Position {
constructor(
public line: number,
public character: number,
) {}
}
class Range {
constructor(
public start: Position,
public end: Position,
) {}
}
return {
Position,
Range,
InlineCompletionItem: class {},
workspace: {
textDocuments: [],
onDidOpenTextDocument: () => ({ dispose: vi.fn() }),
onDidChangeTextDocument: () => ({ dispose: vi.fn() }),
onDidCloseTextDocument: () => ({ dispose: vi.fn() }),
},
window: {
createOutputChannel: () => ({ appendLine: vi.fn(), dispose: vi.fn() }),
},
}
})
type Subject = {
toCompletionItems(
document: vscode.TextDocument,
@@ -64,7 +35,7 @@ function doc(text: string): vscode.TextDocument {
describe("NextEditInlineCompletionProvider", () => {
it("does not send a document when the access policy is missing at runtime", async () => {
const connection = { getClientAsync: vi.fn() }
const connection = { getClientAsync: mock() }
const provider = new NextEditInlineCompletionProvider({
connectionService: connection,
} as unknown as NextEditProviderDeps)
@@ -82,7 +53,7 @@ describe("NextEditInlineCompletionProvider", () => {
})
it("does not send a document when the access policy fails", async () => {
const connection = { getClientAsync: vi.fn() }
const connection = { getClientAsync: mock() }
const provider = new NextEditInlineCompletionProvider({
connectionService: connection as unknown as KiloConnectionService,
isFileAllowed: async () => Promise.reject(new Error("unavailable")),
@@ -101,7 +72,7 @@ describe("NextEditInlineCompletionProvider", () => {
})
it("stashes same-line rewrites before the cursor for decorated acceptance", () => {
const mgr = { clear: vi.fn(), setPending: vi.fn() }
const mgr = { clear: mock(), setPending: mock() }
const provider = new NextEditInlineCompletionProvider({
connectionService: {} as KiloConnectionService,
isFileAllowed: async () => true,
@@ -129,7 +100,7 @@ describe("NextEditInlineCompletionProvider", () => {
})
it("stashes complete-line deletion intent for acceptance", () => {
const mgr = { clear: vi.fn(), setPending: vi.fn() }
const mgr = { clear: mock(), setPending: mock() }
const provider = new NextEditInlineCompletionProvider({
connectionService: {} as KiloConnectionService,
isFileAllowed: async () => true,
@@ -155,7 +126,7 @@ describe("NextEditInlineCompletionProvider", () => {
})
it("does not classify a blank-line rewrite as deletion", () => {
const mgr = { clear: vi.fn(), setPending: vi.fn() }
const mgr = { clear: mock(), setPending: mock() }
const provider = new NextEditInlineCompletionProvider({
connectionService: {} as KiloConnectionService,
isFileAllowed: async () => true,