Files
Kilo-Org_kilocode/packages/opencode/test/kilocode/server/config-overlay.test.ts
T
Catriel Müller 93dd00b6bc Merge pull request #11262 from Kilo-Org/console-model-training-filter
feat(cli): add prompt-training model filter to Kilo Console
2026-06-16 16:01:21 -03:00

403 lines
15 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test"
import path from "path"
import * as Log from "@opencode-ai/core/util/log"
import { Global } from "@opencode-ai/core/global"
import { Server } from "../../../src/server/server"
import { Config } from "../../../src/config/config"
import { KilocodeConfigOverlay } from "../../../src/kilocode/config/overlay"
import { Permission } from "../../../src/permission"
import { PtyPaths } from "../../../src/server/routes/instance/httpapi/groups/pty"
import { resetDatabase } from "../../fixture/db"
import { disposeAllInstances, tmpdir } from "../../fixture/fixture"
void Log.init({ print: false })
const original = Global.Path.config
const terminal = process.platform === "win32" ? test.skip : test.serial
type Overlay = {
fields: Record<string, { source: string; inherited: boolean; overridden: boolean; value?: unknown }>
collections: Record<string, Array<{ key: string; source: string; inherited: boolean; local?: unknown }>>
targets: { project?: string; global?: string; active?: string }
}
type Agent = {
name: string
permission: Permission.Ruleset
}
afterEach(async () => {
;(Global.Path as { config: string }).config = original
await disposeAllInstances()
await resetDatabase()
})
function req(dir: string, input: string, init?: RequestInit) {
return Server.Default().app.request(input, {
...init,
headers: {
"x-kilo-directory": dir,
...init?.headers,
},
})
}
function app(_value: boolean) {
return Server.Default().app
}
function request(target: ReturnType<typeof app>, dir: string | undefined, input: string, init?: RequestInit) {
return target.request(input, {
...init,
headers: {
...(dir ? { "x-kilo-directory": dir } : {}),
...init?.headers,
},
})
}
async function json<T>(response: Response) {
expect(response.status).toBe(200)
return (await response.json()) as T
}
async function config(dir: string, value: unknown) {
await Bun.write(path.join(dir, "kilo.json"), JSON.stringify(value, null, 2))
}
async function setGlobal(dir: string, value: Config.Info) {
;(Global.Path as { config: string }).config = dir
await json(
await request(Server.Default().app, undefined, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ scope: "global", set: value }),
}),
)
}
describe("config overlay routes", () => {
test("ignores unsafe patch paths", () => {
const patched = KilocodeConfigOverlay.patch({
scope: "project",
unset: [
["__proto__", "polluted"],
["constructor", "prototype", "polluted"],
["prototype", "polluted"],
],
})
expect(Object.getPrototypeOf(patched)).toBe(Object.prototype)
expect(Object.hasOwn(patched, "constructor")).toBe(false)
expect(Object.hasOwn(patched, "prototype")).toBe(false)
})
test.serial("marks global values inherited in project scope", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
await setGlobal(global.path, {
model: "kilo/global-model",
permission: { bash: "ask" },
mcp: { shared: { type: "local", command: ["node", "shared.js"], enabled: true } },
})
const body = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
expect(body.fields.model).toMatchObject({ source: "global", inherited: true, overridden: false })
expect(body.collections.permission.find((item) => item.key === "bash")).toMatchObject({
source: "global",
inherited: true,
})
expect(body.collections.mcp.find((item) => item.key === "shared")).toMatchObject({
source: "global",
inherited: true,
})
})
test.serial("resolves prompt-training model visibility across scopes", async () => {
await using global = await tmpdir()
await using project = await tmpdir({ config: { hide_prompt_training_models: false } })
await setGlobal(global.path, { hide_prompt_training_models: true })
const body = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
expect(body.fields.hide_prompt_training_models).toMatchObject({
source: "project",
inherited: false,
overridden: true,
value: false,
})
})
test.serial("marks global indexing values inherited in project scope", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
await setGlobal(global.path, {
indexing: {
enabled: true,
provider: "ollama",
ollama: { baseUrl: "http://localhost:11434" },
},
})
const body = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
expect(body.fields["indexing.enabled"]).toMatchObject({ source: "global", inherited: true, value: true })
expect(body.fields["indexing.provider"]).toMatchObject({ source: "global", inherited: true, value: "ollama" })
expect(body.fields["indexing.ollama.baseUrl"]).toMatchObject({
source: "global",
inherited: true,
value: "http://localhost:11434",
})
})
test.serial("excludes project indexing values from global scope", async () => {
await using project = await tmpdir()
const global: Config.Info = {
indexing: {
enabled: true,
provider: "openai",
openai: { apiKey: "global-secret" },
},
}
const local: Config.Info = {
indexing: {
enabled: false,
provider: "ollama",
ollama: { baseUrl: "http://project:11434" },
},
}
await config(project.path, local)
const body = await KilocodeConfigOverlay.resolve({
directory: project.path,
scope: "global",
effective: local,
global,
sources: [],
})
expect(body.fields["indexing.enabled"]).toMatchObject({ source: "global", value: true })
expect(body.fields["indexing.provider"]).toMatchObject({ source: "global", value: "openai" })
expect(body.fields["indexing.openai.apiKey"]).toMatchObject({ source: "global", value: "global-secret" })
expect(body.fields["indexing.ollama.baseUrl"]).toMatchObject({ source: "default" })
expect(body.fields["indexing.ollama.baseUrl"].value).toBeUndefined()
})
test.serial("writes project indexing overrides to .kilo/kilo.jsonc", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
await setGlobal(global.path, { indexing: { enabled: true, provider: "openai" } })
await json(
await req(project.path, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({
scope: "project",
set: { indexing: { enabled: false, provider: "ollama", ollama: { baseUrl: "http://127.0.0.1:11434" } } },
}),
}),
)
const file = path.join(project.path, ".kilo", "kilo.jsonc")
const saved = (await Bun.file(file).json()) as { indexing: Record<string, unknown> }
const body = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
expect(await Bun.file(path.join(project.path, ".kilo", "kilo.json")).exists()).toBe(false)
expect(saved.indexing).toEqual({
enabled: false,
provider: "ollama",
ollama: { baseUrl: "http://127.0.0.1:11434" },
})
expect(body.fields["indexing.enabled"]).toMatchObject({ source: "project", value: false })
expect(body.fields["indexing.provider"]).toMatchObject({ source: "project", value: "ollama" })
})
test.serial("removes local scalar override and falls back to global", async () => {
await using global = await tmpdir()
await using project = await tmpdir({ config: { model: "kilo/project-model", username: "alice" } })
await setGlobal(global.path, { model: "kilo/global-model" })
await json(
await req(project.path, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ scope: "project", unset: [["model"]] }),
}),
)
const body = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
const saved = (await Bun.file(path.join(project.path, "opencode.json")).json()) as Record<string, unknown>
expect(body.fields.model).toMatchObject({ source: "global", inherited: true })
expect(saved.model).toBeUndefined()
expect(saved.username).toBe("alice")
})
test.serial("writes project mcp overrides without copying inherited servers", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
await setGlobal(global.path, {
mcp: { shared: { type: "local", command: ["node", "shared.js"], enabled: true } },
})
await json(
await req(project.path, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({
scope: "project",
set: { mcp: { local: { type: "local", command: ["node", "local.js"], enabled: true } } },
}),
}),
)
const saved = (await Bun.file(path.join(project.path, ".kilo", "kilo.jsonc")).json()) as {
mcp: Record<string, unknown>
}
expect(Object.keys(saved.mcp)).toEqual(["local"])
})
test.serial("disables inherited mcp server with a minimal local override", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
await setGlobal(global.path, {
mcp: { shared: { type: "local", command: ["node", "shared.js"], enabled: true } },
})
await json(
await req(project.path, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ scope: "project", set: { mcp: { shared: { enabled: false } } } }),
}),
)
const saved = (await Bun.file(path.join(project.path, ".kilo", "kilo.jsonc")).json()) as {
mcp: Record<string, unknown>
}
expect(saved.mcp).toEqual({ shared: { enabled: false } })
})
test.serial("refreshes effective config after project permission update", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
await setGlobal(global.path, { permission: { edit: "allow" } })
const before = await json<Agent[]>(await req(project.path, "/agent"))
expect(Permission.evaluate("edit", "*", before.find((item) => item.name === "code")?.permission ?? []).action).toBe(
"allow",
)
await json(
await req(project.path, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ scope: "project", set: { permission: { edit: { "*": "ask" } } } }),
}),
)
const body = await json<Overlay & { effective: { permission: Record<string, string | Record<string, string>> } }>(
await req(project.path, "/config/overlay?scope=project"),
)
const edit = body.effective.permission.edit
const after = await json<Agent[]>(await req(project.path, "/agent"))
expect(typeof edit === "string" ? edit : edit["*"]).toBe("ask")
expect(Permission.evaluate("edit", "*", after.find((item) => item.name === "code")?.permission ?? []).action).toBe(
"ask",
)
expect(body.collections.permission.find((item) => item.key === "edit")).toMatchObject({
source: "project",
overridden: true,
})
})
test.serial("refreshes agent permissions after global permission update", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
await setGlobal(global.path, { permission: { edit: "allow" } })
const before = await json<Agent[]>(await req(project.path, "/agent"))
expect(Permission.evaluate("edit", "*", before.find((item) => item.name === "code")?.permission ?? []).action).toBe(
"allow",
)
await json(
await req(project.path, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ scope: "global", set: { permission: { edit: { "*": "ask" } } } }),
}),
)
const body = await json<Overlay & { effective: { permission: Record<string, string | Record<string, string>> } }>(
await req(project.path, "/config/overlay?scope=global"),
)
const edit = body.effective.permission.edit
const after = await json<Agent[]>(await req(project.path, "/agent"))
expect(typeof edit === "string" ? edit : edit["*"]).toBe("ask")
expect(Permission.evaluate("edit", "*", after.find((item) => item.name === "code")?.permission ?? []).action).toBe(
"ask",
)
})
terminal("preserves active terminals after updating global console preferences", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
;(Global.Path as { config: string }).config = global.path
const headers = { "x-kilo-directory": project.path }
const created = await Server.Default().app.request(PtyPaths.create, {
method: "POST",
headers: { ...headers, "content-type": "application/json" },
body: JSON.stringify({ command: "/usr/bin/env", args: ["sh", "-c", "sleep 30"], title: "console" }),
})
const info = await json<{ id: string }>(created)
try {
await json(
await request(Server.Default().app, undefined, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ scope: "global", set: { console: { diff_style: "split" } } }),
}),
)
const found = await Server.Default().app.request(PtyPaths.get.replace(":ptyID", info.id), { headers })
expect(found.status).toBe(200)
expect(await found.json()).toMatchObject({ id: info.id, title: "console", status: "running" })
} finally {
await Server.Default().app.request(PtyPaths.remove.replace(":ptyID", info.id), { method: "DELETE", headers })
}
})
for (const value of [false, true]) {
test.serial(
`${value ? "httpapi" : "legacy"} global overlay update refreshes existing project instances without a project directory`,
async () => {
await using global = await tmpdir()
await using project = await tmpdir()
await setGlobal(global.path, { permission: { edit: "ask" } })
await disposeAllInstances()
const target = app(value)
const before = await json<Agent[]>(await request(target, project.path, "/agent"))
expect(
Permission.evaluate("edit", "*", before.find((item) => item.name === "code")?.permission ?? []).action,
).toBe("ask")
await json(
await request(target, undefined, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ scope: "global", set: { permission: { edit: { "*": "allow" } } } }),
}),
)
const after = await json<Agent[]>(await request(target, project.path, "/agent"))
expect(
Permission.evaluate("edit", "*", after.find((item) => item.name === "code")?.permission ?? []).action,
).toBe("allow")
},
)
}
})