fix(vscode): only clear autocomplete defaults when stored at user scope

config.get() returns the merged effective value, so a workspace-level pin
or the schema default would falsely look like a stored global default.
Switch to config.inspect().globalValue so the migration only acts on
values actually stored in user settings, and update the test stub to
model scoped values.
This commit is contained in:
Mark IJbema
2026-05-28 14:07:38 +02:00
parent a6e4aa9a7b
commit 8921a88d16
2 changed files with 76 additions and 26 deletions
@@ -21,8 +21,12 @@ export async function migrateDefaultAutocompleteSettings(context: vscode.Extensi
if (context.globalState.get<boolean>(FLAG)) return
const config = vscode.workspace.getConfiguration("kilo-code.new.autocomplete")
const provider = config.get<string>("provider")
const model = config.get<string>("model")
// Read the user/global scope specifically. `get()` returns the merged
// effective value (workspace > global > default), which would let a
// workspace-level pin or the schema default falsely look like a stored
// global default and cause us to no-op while still flipping the flag.
const provider = config.inspect<string>("provider")?.globalValue
const model = config.inspect<string>("model")?.globalValue
const matchesDefault =
provider === DEFAULT_AUTOCOMPLETE_MODEL.providerID && model === DEFAULT_AUTOCOMPLETE_MODEL.modelID
@@ -3,9 +3,13 @@ import * as vscode from "vscode"
import { migrateDefaultAutocompleteSettings } from "../../src/services/autocomplete/migrate-default"
import { DEFAULT_AUTOCOMPLETE_MODEL } from "../../src/shared/autocomplete-models"
type Scoped = { globalValue?: unknown; workspaceValue?: unknown }
type State = Map<string, Scoped>
type Stub = {
getConfiguration: (section?: string) => {
get: (key: string, fallback?: unknown) => unknown
inspect: (key: string) => Scoped | undefined
update: (key: string, value: unknown, target: unknown) => Promise<void>
}
}
@@ -27,78 +31,120 @@ function makeContext(initial: Record<string, unknown> = {}) {
}
}
function stubConfig(state: Map<string, unknown>) {
function stubConfig(state: State) {
function entry(key: string): Scoped {
const e = state.get(key)
if (e) return e
const fresh: Scoped = {}
state.set(key, fresh)
return fresh
}
;(vscode.workspace as unknown as Stub).getConfiguration = (section?: string) => {
if (section !== "kilo-code.new.autocomplete") {
return { get: () => undefined, update: async () => {} }
return {
get: () => undefined,
inspect: () => undefined,
update: async () => {},
}
}
return {
get: (key: string, fallback?: unknown) => state.get(key) ?? fallback,
update: async (key: string, value: unknown) => {
if (value === undefined) state.delete(key)
else state.set(key, value)
get: (key: string, fallback?: unknown) => {
const e = state.get(key)
return e?.workspaceValue ?? e?.globalValue ?? fallback
},
inspect: (key: string) => state.get(key) ?? {},
update: async (key: string, value: unknown, target: unknown) => {
// vscode.ConfigurationTarget.Global = 1, Workspace = 2
const scope: keyof Scoped = target === 2 ? "workspaceValue" : "globalValue"
const e = entry(key)
if (value === undefined) delete e[scope]
else e[scope] = value
},
}
}
}
function setGlobal(state: State, key: string, value: unknown) {
const e = state.get(key) ?? {}
e.globalValue = value
state.set(key, e)
}
function setWorkspace(state: State, key: string, value: unknown) {
const e = state.get(key) ?? {}
e.workspaceValue = value
state.set(key, e)
}
afterEach(() => {
;(vscode.workspace as unknown as Stub).getConfiguration = original as Stub["getConfiguration"]
})
describe("migrateDefaultAutocompleteSettings", () => {
let state: Map<string, unknown>
let state: State
beforeEach(() => {
state = new Map()
stubConfig(state)
})
it("clears provider/model when both equal the current default", async () => {
state.set("provider", DEFAULT_AUTOCOMPLETE_MODEL.providerID)
state.set("model", DEFAULT_AUTOCOMPLETE_MODEL.modelID)
it("clears provider/model when both equal the current default at global scope", async () => {
setGlobal(state, "provider", DEFAULT_AUTOCOMPLETE_MODEL.providerID)
setGlobal(state, "model", DEFAULT_AUTOCOMPLETE_MODEL.modelID)
const { context, flag } = makeContext()
await migrateDefaultAutocompleteSettings(context)
expect(state.has("provider")).toBe(false)
expect(state.has("model")).toBe(false)
expect(state.get("provider")?.globalValue).toBeUndefined()
expect(state.get("model")?.globalValue).toBeUndefined()
expect(flag.get("kilo.autocomplete.defaultClearMigrationV1")).toBe(true)
})
it("leaves an explicitly chosen non-default model untouched", async () => {
state.set("provider", "inception")
state.set("model", "mercury-edit-2")
setGlobal(state, "provider", "inception")
setGlobal(state, "model", "mercury-edit-2")
const { context, flag } = makeContext()
await migrateDefaultAutocompleteSettings(context)
expect(state.get("provider")).toBe("inception")
expect(state.get("model")).toBe("mercury-edit-2")
expect(state.get("provider")?.globalValue).toBe("inception")
expect(state.get("model")?.globalValue).toBe("mercury-edit-2")
expect(flag.get("kilo.autocomplete.defaultClearMigrationV1")).toBe(true)
})
it("leaves a partial match untouched", async () => {
state.set("provider", DEFAULT_AUTOCOMPLETE_MODEL.providerID)
state.set("model", "inception/mercury-edit-2")
setGlobal(state, "provider", DEFAULT_AUTOCOMPLETE_MODEL.providerID)
setGlobal(state, "model", "inception/mercury-edit-2")
const { context } = makeContext()
await migrateDefaultAutocompleteSettings(context)
expect(state.get("provider")).toBe(DEFAULT_AUTOCOMPLETE_MODEL.providerID)
expect(state.get("model")).toBe("inception/mercury-edit-2")
expect(state.get("provider")?.globalValue).toBe(DEFAULT_AUTOCOMPLETE_MODEL.providerID)
expect(state.get("model")?.globalValue).toBe("inception/mercury-edit-2")
})
it("ignores workspace-scoped pins so they aren't mistaken for global defaults", async () => {
setWorkspace(state, "provider", DEFAULT_AUTOCOMPLETE_MODEL.providerID)
setWorkspace(state, "model", DEFAULT_AUTOCOMPLETE_MODEL.modelID)
const { context } = makeContext()
await migrateDefaultAutocompleteSettings(context)
// Workspace value is intact — we only clear at global scope.
expect(state.get("provider")?.workspaceValue).toBe(DEFAULT_AUTOCOMPLETE_MODEL.providerID)
expect(state.get("model")?.workspaceValue).toBe(DEFAULT_AUTOCOMPLETE_MODEL.modelID)
})
it("only runs once per machine", async () => {
state.set("provider", DEFAULT_AUTOCOMPLETE_MODEL.providerID)
state.set("model", DEFAULT_AUTOCOMPLETE_MODEL.modelID)
setGlobal(state, "provider", DEFAULT_AUTOCOMPLETE_MODEL.providerID)
setGlobal(state, "model", DEFAULT_AUTOCOMPLETE_MODEL.modelID)
const { context } = makeContext({ "kilo.autocomplete.defaultClearMigrationV1": true })
await migrateDefaultAutocompleteSettings(context)
// Setting was preserved — second run is a no-op.
expect(state.get("provider")).toBe(DEFAULT_AUTOCOMPLETE_MODEL.providerID)
expect(state.get("model")).toBe(DEFAULT_AUTOCOMPLETE_MODEL.modelID)
expect(state.get("provider")?.globalValue).toBe(DEFAULT_AUTOCOMPLETE_MODEL.providerID)
expect(state.get("model")?.globalValue).toBe(DEFAULT_AUTOCOMPLETE_MODEL.modelID)
})
it("sets the flag even when nothing needed clearing", async () => {