fb2db2e018
* fix(vscode): allow removing provider reasoning * style(vscode): format provider regression test --------- Co-authored-by: Christiaan Arnoldus <christiaan.arnoldus@outlook.com>
407 lines
13 KiB
TypeScript
407 lines
13 KiB
TypeScript
// kilocode_change - new file
|
|
import { afterEach, describe, expect, test } from "bun:test"
|
|
import { Effect, Layer, Option, Schema } from "effect"
|
|
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
|
import path from "path"
|
|
import { Global } from "@opencode-ai/core/global"
|
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
|
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
|
|
import { Npm } from "@opencode-ai/core/npm"
|
|
import { Account } from "../../../src/account/account"
|
|
import { Auth } from "../../../src/auth"
|
|
import { Config } from "../../../src/config/config"
|
|
import { ConfigMarkdown } from "../../../src/config/markdown"
|
|
import { Env } from "../../../src/env"
|
|
import { KiloIndexing } from "../../../src/kilocode/indexing"
|
|
import { KilocodeConfig } from "../../../src/kilocode/config/config"
|
|
import { provideTestInstance } from "../../fixture/fixture"
|
|
import { Filesystem } from "../../../src/util/filesystem"
|
|
import { disposeAllInstances, tmpdir } from "../../fixture/fixture"
|
|
|
|
const infra = CrossSpawnSpawner.defaultLayer.pipe(
|
|
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
|
|
)
|
|
const emptyAccount = Layer.mock(Account.Service)({
|
|
active: () => Effect.succeed(Option.none()),
|
|
activeOrg: () => Effect.succeed(Option.none()),
|
|
})
|
|
const emptyAuth = Layer.mock(Auth.Service)({
|
|
all: () => Effect.succeed({}),
|
|
})
|
|
const noopNpm = Layer.mock(Npm.Service)({
|
|
install: () => Effect.void,
|
|
add: () => Effect.die("not implemented"),
|
|
which: () => Effect.succeed(Option.none()),
|
|
})
|
|
const layer = Config.layer.pipe(
|
|
Layer.provide(EffectFlock.defaultLayer),
|
|
Layer.provide(AppFileSystem.defaultLayer),
|
|
Layer.provide(Env.defaultLayer),
|
|
Layer.provide(emptyAuth),
|
|
Layer.provide(emptyAccount),
|
|
Layer.provideMerge(infra),
|
|
Layer.provide(noopNpm),
|
|
)
|
|
|
|
const load = () => Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(layer)))
|
|
const clear = () =>
|
|
Effect.runPromise(Config.Service.use((svc) => svc.invalidate()).pipe(Effect.scoped, Effect.provide(layer)))
|
|
const saveGlobal = (config: Config.Info) =>
|
|
Effect.runPromise(Config.Service.use((svc) => svc.updateGlobal(config)).pipe(Effect.scoped, Effect.provide(layer)))
|
|
const saveProject = (config: Config.Info) =>
|
|
Effect.runPromise(Config.Service.use((svc) => svc.update(config)).pipe(Effect.scoped, Effect.provide(layer)))
|
|
|
|
async function writeConfig(dir: string, config: object, name = "kilo.json") {
|
|
await Filesystem.write(path.join(dir, name), JSON.stringify(config))
|
|
}
|
|
|
|
function decode(input: unknown): Config.Info {
|
|
const config = Schema.decodeUnknownSync(Config.Info)(input)
|
|
return {
|
|
...config,
|
|
skills: config.skills && {
|
|
paths: config.skills.paths && [...config.skills.paths],
|
|
urls: config.skills.urls && [...config.skills.urls],
|
|
},
|
|
}
|
|
}
|
|
|
|
const cfg: Partial<Config.Info> = {
|
|
plugin: ["@kilocode/kilo-indexing"],
|
|
indexing: {
|
|
provider: "ollama",
|
|
vectorStore: "qdrant",
|
|
ollama: {
|
|
baseUrl: "http://127.0.0.1:1",
|
|
},
|
|
},
|
|
}
|
|
|
|
afterEach(async () => {
|
|
delete process.env.KILO_MD_TEST
|
|
await clear()
|
|
await disposeAllInstances()
|
|
})
|
|
|
|
describe("markdown substitutions", () => {
|
|
test("applies file and env substitutions to parsed markdown body", async () => {
|
|
process.env.KILO_MD_TEST = "env content"
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Filesystem.write(path.join(dir, "body.md"), "file content")
|
|
await Filesystem.write(
|
|
path.join(dir, "SKILL.md"),
|
|
["---", "name: test", "description: Test", "---", "{file:body.md}", "{env:KILO_MD_TEST}"].join("\n"),
|
|
)
|
|
},
|
|
})
|
|
|
|
const md = await ConfigMarkdown.parse(path.join(tmp.path, "SKILL.md"))
|
|
|
|
expect(md.content).toContain("file content")
|
|
expect(md.content).toContain("env content")
|
|
})
|
|
})
|
|
|
|
describe("kilocode indexing config", () => {
|
|
test("ignores retired semantic indexing flags in existing configs", async () => {
|
|
await using tmp = await tmpdir({ git: true })
|
|
await writeConfig(tmp.path, {
|
|
experimental: { semantic_indexing: true, batch_tool: true },
|
|
})
|
|
|
|
await provideTestInstance({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const config = await load()
|
|
expect(config.experimental?.batch_tool).toBe(true)
|
|
expect(config.experimental).not.toHaveProperty("semantic_indexing")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("keeps global indexing enabled in global config", async () => {
|
|
await using globalTmp = await tmpdir()
|
|
await using tmp = await tmpdir()
|
|
|
|
const prev = Global.Path.config
|
|
;(Global.Path as { config: string }).config = globalTmp.path
|
|
await clear()
|
|
await disposeAllInstances()
|
|
|
|
try {
|
|
await writeConfig(globalTmp.path, {
|
|
$schema: "https://app.kilo.ai/config.json",
|
|
indexing: {
|
|
enabled: true,
|
|
provider: "ollama",
|
|
},
|
|
})
|
|
|
|
await provideTestInstance({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const config = await load()
|
|
const global = await Effect.runPromise(
|
|
Config.Service.use((svc) => svc.getGlobal()).pipe(Effect.scoped, Effect.provide(layer)),
|
|
)
|
|
expect(config.indexing?.provider).toBe("ollama")
|
|
expect(config.indexing?.enabled).toBeUndefined()
|
|
expect(global.indexing?.enabled).toBe(true)
|
|
},
|
|
})
|
|
} finally {
|
|
;(Global.Path as { config: string }).config = prev
|
|
await clear()
|
|
await disposeAllInstances()
|
|
}
|
|
})
|
|
|
|
test("uses global indexing enabled when project enablement is unset", async () => {
|
|
await using globalTmp = await tmpdir()
|
|
await using tmp = await tmpdir({ git: true, config: cfg })
|
|
|
|
const prev = Global.Path.config
|
|
;(Global.Path as { config: string }).config = globalTmp.path
|
|
await clear()
|
|
await disposeAllInstances()
|
|
|
|
try {
|
|
await writeConfig(globalTmp.path, {
|
|
$schema: "https://app.kilo.ai/config.json",
|
|
indexing: {
|
|
enabled: true,
|
|
},
|
|
})
|
|
|
|
await provideTestInstance({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const global = await Effect.runPromise(
|
|
Config.Service.use((svc) => svc.getGlobal()).pipe(Effect.scoped, Effect.provide(layer)),
|
|
)
|
|
const config = await load()
|
|
const input = KiloIndexing.input(config.indexing, global.indexing)
|
|
expect(input.enabled).toBe(true)
|
|
},
|
|
})
|
|
} finally {
|
|
;(Global.Path as { config: string }).config = prev
|
|
await clear()
|
|
await disposeAllInstances()
|
|
}
|
|
})
|
|
|
|
test("project indexing enabled overrides global enablement", async () => {
|
|
const input = KiloIndexing.input({ enabled: false }, { enabled: true })
|
|
expect(input.enabled).toBe(false)
|
|
expect(KiloIndexing.input(undefined, { enabled: true }).enabled).toBe(true)
|
|
expect(KiloIndexing.input({ enabled: true }, { enabled: false }).enabled).toBe(true)
|
|
})
|
|
|
|
test("creates missing project config as .kilo/kilo.jsonc", async () => {
|
|
await using tmp = await tmpdir({ git: true })
|
|
|
|
await provideTestInstance({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
await saveProject({ indexing: { enabled: true } })
|
|
},
|
|
})
|
|
|
|
expect(await Bun.file(path.join(tmp.path, ".kilo", "kilo.jsonc")).exists()).toBe(true)
|
|
expect(await Bun.file(path.join(tmp.path, ".kilo", "kilo.json")).exists()).toBe(false)
|
|
})
|
|
|
|
test("accepts delete sentinels for indexing model overrides", () => {
|
|
const patch = decode({ indexing: { model: null, dimension: null } })
|
|
const merged = KilocodeConfig.mergeConfig(
|
|
{
|
|
indexing: {
|
|
provider: "openai",
|
|
model: "text-embedding-3-large",
|
|
dimension: 3072,
|
|
},
|
|
},
|
|
patch,
|
|
)
|
|
const input = KiloIndexing.input(patch.indexing)
|
|
|
|
expect(merged.indexing).toEqual({ provider: "openai" })
|
|
expect(input.modelId).toBeUndefined()
|
|
expect(input.modelDimension).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("custom provider model config", () => {
|
|
test("persists and removes reasoning across a global config reload", async () => {
|
|
await using globalTmp = await tmpdir()
|
|
const file = path.join(globalTmp.path, "kilo.json")
|
|
const prev = Global.Path.config
|
|
;(Global.Path as { config: string }).config = globalTmp.path
|
|
await clear()
|
|
await disposeAllInstances()
|
|
|
|
try {
|
|
await writeConfig(globalTmp.path, {
|
|
provider: {
|
|
custom: {
|
|
name: "Custom",
|
|
models: { model: { name: "Model" } },
|
|
},
|
|
},
|
|
})
|
|
await saveGlobal(
|
|
decode({
|
|
provider: {
|
|
custom: {
|
|
models: { model: { reasoning: true } },
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
const added = JSON.parse(await Bun.file(file).text())
|
|
expect(added.provider.custom.models.model.reasoning).toBe(true)
|
|
|
|
await saveGlobal(
|
|
decode({
|
|
provider: {
|
|
custom: {
|
|
models: { model: { reasoning: null } },
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
const written = JSON.parse(await Bun.file(file).text())
|
|
expect(written.provider.custom.models.model).not.toHaveProperty("reasoning")
|
|
|
|
await clear()
|
|
const reloaded = await Effect.runPromise(
|
|
Config.Service.use((svc) => svc.getGlobal()).pipe(Effect.scoped, Effect.provide(layer)),
|
|
)
|
|
expect(reloaded.provider?.custom?.models?.model?.reasoning).toBeUndefined()
|
|
} finally {
|
|
;(Global.Path as { config: string }).config = prev
|
|
await clear()
|
|
await disposeAllInstances()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("subagent variant overrides", () => {
|
|
test("removes one model override without removing sibling models", () => {
|
|
const patch = decode({
|
|
subagent_variant_overrides: {
|
|
"anthropic/claude-sonnet-4-6": null,
|
|
},
|
|
})
|
|
const merged = KilocodeConfig.mergeConfig(
|
|
{
|
|
subagent_variant_overrides: {
|
|
"anthropic/claude-sonnet-4-6": "high",
|
|
"openai/gpt-5": "xhigh",
|
|
},
|
|
},
|
|
patch,
|
|
)
|
|
|
|
expect(patch.subagent_variant_overrides?.["anthropic/claude-sonnet-4-6"]).toBeNull()
|
|
expect(merged.subagent_variant_overrides).toEqual({ "openai/gpt-5": "xhigh" })
|
|
})
|
|
|
|
test("accepts a delete sentinel for the complete override map", () => {
|
|
const patch = decode({ subagent_variant_overrides: null })
|
|
const merged = KilocodeConfig.mergeConfig(
|
|
{
|
|
subagent_variant_overrides: {
|
|
"anthropic/claude-sonnet-4-6": "high",
|
|
},
|
|
},
|
|
patch,
|
|
)
|
|
|
|
expect(patch.subagent_variant_overrides).toBeNull()
|
|
expect(merged.subagent_variant_overrides).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("agent config", () => {
|
|
test("accepts delete sentinels for agent model and variant overrides", () => {
|
|
const patch = decode({ agent: { explore: { model: null, variant: null } } })
|
|
const merged = KilocodeConfig.mergeConfig(
|
|
{
|
|
agent: {
|
|
explore: {
|
|
model: "kilo/anthropic/claude-sonnet-4-6",
|
|
variant: "high",
|
|
},
|
|
},
|
|
},
|
|
patch,
|
|
)
|
|
|
|
expect(patch.agent?.explore?.model).toBeNull()
|
|
expect(patch.agent?.explore?.variant).toBeNull()
|
|
expect(merged.agent).toBeUndefined()
|
|
})
|
|
|
|
test("removes an agent variant override without removing its model", () => {
|
|
const patch = decode({ agent: { explore: { variant: null } } })
|
|
const merged = KilocodeConfig.mergeConfig(
|
|
{
|
|
agent: {
|
|
explore: {
|
|
model: "kilo/anthropic/claude-sonnet-4-6",
|
|
variant: "high",
|
|
},
|
|
},
|
|
},
|
|
patch,
|
|
)
|
|
|
|
expect(patch.agent?.explore?.variant).toBeNull()
|
|
expect(merged.agent?.explore).toEqual({ model: "kilo/anthropic/claude-sonnet-4-6" })
|
|
})
|
|
|
|
test("removes agent model and variant overrides from global JSONC config", async () => {
|
|
await using globalTmp = await tmpdir()
|
|
const file = path.join(globalTmp.path, "kilo.jsonc")
|
|
const prev = Global.Path.config
|
|
;(Global.Path as { config: string }).config = globalTmp.path
|
|
await clear()
|
|
await disposeAllInstances()
|
|
|
|
try {
|
|
await Filesystem.write(
|
|
file,
|
|
[
|
|
"{",
|
|
" // Preserve this comment while clearing overrides.",
|
|
' "agent": {',
|
|
' "explore": {',
|
|
' "model": "kilo/anthropic/claude-sonnet-4-6",',
|
|
' "variant": "high",',
|
|
' "description": "Keep me"',
|
|
" }",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
)
|
|
const patch = decode({ agent: { explore: { model: null, variant: null } } })
|
|
|
|
await saveGlobal(patch)
|
|
|
|
const written = await Bun.file(file).text()
|
|
expect(written).toContain("// Preserve this comment while clearing overrides.")
|
|
expect(written).not.toContain('"model"')
|
|
expect(written).not.toContain('"variant"')
|
|
expect(written).toContain('"description": "Keep me"')
|
|
} finally {
|
|
;(Global.Path as { config: string }).config = prev
|
|
await clear()
|
|
await disposeAllInstances()
|
|
}
|
|
})
|
|
})
|