mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
feat(vscode): multi-project Agent Manager with per-project state, sessions, and lifecycle
Add an experimental multi-project mode to the Agent Manager sidebar behind kilo-code.new.experimental.multiProject (default off). A persistent project registry catalogs additional git repositories across restarts, while the workspace repository stays the pinned default project. Extension: - Immutable per-project contexts own all repository-bound services (state, worktrees, setup scripts, stale tracking, pollers) with generation-based invalidation and fail-closed trust checks. - ProjectContexts manage activation, expansion, and fast switching; session routes resolve directories exactly per project via a shared route service. - pushProjectSessions caches each project's session list and re-posts it on fresh skips; session.created/updated/deleted SSE events upsert into the owning project's cache so externally created sessions appear immediately. - Selection restore persists the active target per project and falls back to the local context silently when the remembered target is gone. - Worktree lifecycle handlers extracted into provider-lifecycle.ts with an explicit deps object instead of ambient project scope. - initializeState and onRequestState always refresh sessions: with zero managed sessions the listing never ran and the sidebar skeletons forever. - Log instead of dropping silently when a state-gated message is not ready. Webview: - ProjectList accordion with per-project sidebar body, search, actions, and default-branch dialog; selecting a project header restores its target. - Local session tabs and terminal contexts are bucketed per project so open tabs never leak across projects sharing the LOCAL context. - The active project's session list overlays the live session store so new sessions show without waiting for a backend re-list. - SectionHeader requires a DragDropProvider ancestor; the multi-project body now provides one (its absence crashed the whole webview render). - SidebarBody and TabBar extracted out of AgentManagerApp (3215 to 2748 lines); AgentManagerProvider down to 1887 with caps lowered accordingly. Also includes the config write revision bindings and per-project indexing consent groundwork that rode along on this branch.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import { Effect } from "effect"
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
@@ -79,6 +80,7 @@ const staleKilo: Partial<Config.Info> = {
|
||||
}
|
||||
const configDir = process.env["KILO_CONFIG_DIR"]
|
||||
const disabled = process.env["KILO_DISABLE_CODEBASE_INDEXING"]
|
||||
const platform = process.env["KILO_PLATFORM"]
|
||||
const error = new Error("test indexing initialization failed")
|
||||
|
||||
function inline(directory: string, root: string, hooks: IndexingWorker.Hooks): IndexingWorker.Driver {
|
||||
@@ -118,6 +120,7 @@ async function called(init: ReturnType<typeof spyOn<CodeIndexManager, "initializ
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
process.env["KILO_PLATFORM"] = "cli"
|
||||
IndexingWorker.override(inline)
|
||||
})
|
||||
|
||||
@@ -127,6 +130,8 @@ afterEach(async () => {
|
||||
else process.env["KILO_CONFIG_DIR"] = configDir
|
||||
if (disabled === undefined) delete process.env["KILO_DISABLE_CODEBASE_INDEXING"]
|
||||
else process.env["KILO_DISABLE_CODEBASE_INDEXING"] = disabled
|
||||
if (platform === undefined) delete process.env["KILO_PLATFORM"]
|
||||
else process.env["KILO_PLATFORM"] = platform
|
||||
global.fetch = fetch
|
||||
await disposeAllInstances()
|
||||
})
|
||||
@@ -388,14 +393,17 @@ describe("indexing startup degradation", () => {
|
||||
},
|
||||
})
|
||||
expect(config.status).toBe(200)
|
||||
await called(init)
|
||||
|
||||
const status = await app.request("/indexing/status", {
|
||||
const status = await app.request("/indexing/consent", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-kilo-directory": tmp.path,
|
||||
},
|
||||
body: JSON.stringify({ enabled: true }),
|
||||
})
|
||||
expect(status.status).toBe(200)
|
||||
await called(init)
|
||||
|
||||
const body = await status.json()
|
||||
expect(body).toMatchObject({
|
||||
@@ -629,6 +637,99 @@ describe("indexing startup degradation", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("requires explicit VS Code consent even when repository config enables indexing", async () => {
|
||||
const created: string[] = []
|
||||
IndexingWorker.override((directory) => {
|
||||
created.push(directory)
|
||||
return inline(directory, "/index", {
|
||||
status() {},
|
||||
telemetry() {},
|
||||
warning() {},
|
||||
log() {},
|
||||
failure() {},
|
||||
})
|
||||
})
|
||||
|
||||
await using tmp = await tmpdir({ git: true, config: cfg })
|
||||
process.env["KILO_CONFIG_DIR"] = tmp.path
|
||||
process.env["KILO_PLATFORM"] = "vscode"
|
||||
|
||||
try {
|
||||
await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(() => KiloIndexing.setConsent(false)),
|
||||
fn: async () => {
|
||||
const status = await KiloIndexing.current()
|
||||
expect(status.state).toBe("Disabled")
|
||||
expect(status.message).toContain("enable it for this project")
|
||||
expect(created).toEqual([])
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
process.env["KILO_PLATFORM"] = "cli"
|
||||
}
|
||||
})
|
||||
|
||||
test("shares consent across linked worktrees and revokes every project worker", async () => {
|
||||
const created: string[] = []
|
||||
const disposed: string[] = []
|
||||
IndexingWorker.override((directory) => {
|
||||
created.push(directory)
|
||||
return {
|
||||
async init() {
|
||||
return {
|
||||
state: "Standby",
|
||||
message: "Indexing paused.",
|
||||
processedFiles: 0,
|
||||
totalFiles: 0,
|
||||
percent: 0,
|
||||
}
|
||||
},
|
||||
async search() {
|
||||
return []
|
||||
},
|
||||
async dispose() {
|
||||
disposed.push(directory)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
await using tmp = await tmpdir({ git: true, config: cfg })
|
||||
const worktree = path.join(path.dirname(tmp.path), `indexing-worktree-${Date.now()}`)
|
||||
await $`git worktree add --quiet -b indexing-consent-${Date.now()} ${worktree} HEAD`.cwd(tmp.path)
|
||||
process.env["KILO_CONFIG_DIR"] = tmp.path
|
||||
process.env["KILO_PLATFORM"] = "vscode"
|
||||
|
||||
try {
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await KiloIndexing.setConsent(true)
|
||||
await wait(() => KiloIndexing.current(), "Standby")
|
||||
},
|
||||
})
|
||||
await withTestInstance({
|
||||
directory: worktree,
|
||||
fn: async () => expect((await wait(() => KiloIndexing.current(), "Standby")).state).toBe("Standby"),
|
||||
})
|
||||
expect(new Set(created)).toEqual(new Set([tmp.path, worktree]))
|
||||
|
||||
await withTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: () => KiloIndexing.setConsent(false),
|
||||
})
|
||||
|
||||
expect(new Set(disposed)).toEqual(new Set([tmp.path, worktree]))
|
||||
await withTestInstance({
|
||||
directory: worktree,
|
||||
fn: async () => expect((await KiloIndexing.current()).state).toBe("Disabled"),
|
||||
})
|
||||
} finally {
|
||||
process.env["KILO_PLATFORM"] = "cli"
|
||||
await $`git worktree remove --force ${worktree}`.cwd(tmp.path).quiet()
|
||||
}
|
||||
}, 15_000)
|
||||
|
||||
test("enriches Kilo provider config from env auth", async () => {
|
||||
global.fetch = (() =>
|
||||
Promise.resolve(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { afterEach, describe, expect, setDefaultTimeout, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { chmod, rm, stat, symlink } from "fs/promises"
|
||||
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 { KilocodeConfigWriter } from "../../../src/kilocode/config/writer"
|
||||
import { Permission } from "../../../src/permission"
|
||||
import { PtyPaths } from "../../../src/server/routes/instance/httpapi/groups/pty"
|
||||
import { Filesystem } from "../../../src/util/filesystem"
|
||||
@@ -12,14 +14,16 @@ import { resetDatabase } from "../../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
setDefaultTimeout(30_000)
|
||||
|
||||
const original = Global.Path.config
|
||||
const terminal = process.platform === "win32" ? test.skip : test.serial
|
||||
|
||||
type Target = { path: string; revision: string; exists: boolean; writable: boolean; raw: Record<string, unknown> }
|
||||
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 }
|
||||
targets: { project: Target; global: Target; active: Target }
|
||||
}
|
||||
type Agent = {
|
||||
name: string
|
||||
@@ -30,34 +34,41 @@ afterEach(async () => {
|
||||
;(Global.Path as { config: string }).config = original
|
||||
await disposeAllInstances()
|
||||
await resetDatabase()
|
||||
})
|
||||
}, 15_000)
|
||||
|
||||
function req(dir: string, input: string, init?: RequestInit) {
|
||||
return Server.Default().app.request(input, {
|
||||
...init,
|
||||
headers: {
|
||||
"x-kilo-directory": dir,
|
||||
...init?.headers,
|
||||
},
|
||||
})
|
||||
return request(Server.Default().app, dir, input, init)
|
||||
}
|
||||
|
||||
function app(_value: boolean) {
|
||||
return Server.Default().app
|
||||
}
|
||||
|
||||
function request(target: ReturnType<typeof app>, dir: string | undefined, input: string, init?: RequestInit) {
|
||||
async function request(target: ReturnType<typeof app>, dir: string | undefined, input: string, init?: RequestInit) {
|
||||
const headers = {
|
||||
...(dir ? { "x-kilo-directory": dir } : {}),
|
||||
...init?.headers,
|
||||
}
|
||||
const body = init?.method === "PATCH" && input === "/config/overlay" ? JSON.parse(String(init.body)) : undefined
|
||||
const next =
|
||||
body && !body.expected
|
||||
? await (async () => {
|
||||
const scope = body.scope === "global" ? "global" : "project"
|
||||
const response = await target.request(`/config/overlay?scope=${scope}`, { headers })
|
||||
const overlay = (await response.json()) as Overlay
|
||||
const expected = overlay.targets[scope]
|
||||
return { ...body, expected: { path: expected.path, revision: expected.revision } }
|
||||
})()
|
||||
: body
|
||||
return target.request(input, {
|
||||
...init,
|
||||
headers: {
|
||||
...(dir ? { "x-kilo-directory": dir } : {}),
|
||||
...init?.headers,
|
||||
},
|
||||
headers,
|
||||
body: next ? JSON.stringify(next) : init?.body,
|
||||
})
|
||||
}
|
||||
|
||||
async function json<T>(response: Response) {
|
||||
expect(response.status).toBe(200)
|
||||
if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
@@ -77,6 +88,186 @@ async function setGlobal(dir: string, value: Config.Info) {
|
||||
}
|
||||
|
||||
describe("config overlay routes", () => {
|
||||
test("writes a missing project target atomically", async () => {
|
||||
await using project = await tmpdir()
|
||||
const target = await KilocodeConfigOverlay.target({ scope: "project", directory: project.path })
|
||||
|
||||
const result = await KilocodeConfigWriter.write({
|
||||
scope: "project",
|
||||
directory: project.path,
|
||||
expected: target,
|
||||
set: { model: "test/model" },
|
||||
})
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect(await Bun.file(target.path).text()).toContain('"model": "test/model"')
|
||||
})
|
||||
|
||||
test("returns exact raw target data and a stable missing-file revision", async () => {
|
||||
await using project = await tmpdir()
|
||||
const first = await KilocodeConfigOverlay.target({ scope: "project", directory: project.path })
|
||||
const second = await KilocodeConfigOverlay.target({ scope: "project", directory: project.path })
|
||||
|
||||
expect(first.exists).toBe(false)
|
||||
expect(first.raw).toEqual({})
|
||||
expect(first.revision).toBe(second.revision)
|
||||
|
||||
await Filesystem.write(first.path, '{\n // preserved\n "model": "test/model"\n}\n')
|
||||
const saved = await KilocodeConfigOverlay.target({ scope: "project", directory: project.path })
|
||||
expect(saved.raw).toEqual({ model: "test/model" })
|
||||
expect(saved.revision).not.toBe(first.revision)
|
||||
})
|
||||
|
||||
test("rejects a comment-only external edit with a typed revision conflict", async () => {
|
||||
await using project = await tmpdir()
|
||||
const before = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
|
||||
await Filesystem.write(before.targets.project.path, "{\n // external edit\n}\n")
|
||||
|
||||
const response = await Server.Default().app.request("/config/overlay", {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json", "x-kilo-directory": project.path },
|
||||
body: JSON.stringify({
|
||||
scope: "project",
|
||||
expected: {
|
||||
path: before.targets.project.path,
|
||||
revision: before.targets.project.revision,
|
||||
},
|
||||
set: { model: "test/model" },
|
||||
}),
|
||||
})
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
expect(await response.json()).toMatchObject({ code: "revision-conflict" })
|
||||
})
|
||||
|
||||
test("rejects a newly created higher-priority target", async () => {
|
||||
await using project = await tmpdir()
|
||||
const before = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
|
||||
await Filesystem.write(path.join(project.path, "kilo.json"), "{}")
|
||||
|
||||
const response = await Server.Default().app.request("/config/overlay", {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json", "x-kilo-directory": project.path },
|
||||
body: JSON.stringify({
|
||||
scope: "project",
|
||||
expected: {
|
||||
path: before.targets.project.path,
|
||||
revision: before.targets.project.revision,
|
||||
},
|
||||
set: { model: "test/model" },
|
||||
}),
|
||||
})
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
expect(await response.json()).toMatchObject({ code: "target-changed" })
|
||||
})
|
||||
|
||||
test("allows only one concurrent writer for a revision", async () => {
|
||||
await using project = await tmpdir()
|
||||
const before = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
|
||||
const update = (model: string) =>
|
||||
Server.Default().app.request("/config/overlay", {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json", "x-kilo-directory": project.path },
|
||||
body: JSON.stringify({
|
||||
scope: "project",
|
||||
expected: {
|
||||
path: before.targets.project.path,
|
||||
revision: before.targets.project.revision,
|
||||
},
|
||||
set: { model },
|
||||
}),
|
||||
})
|
||||
|
||||
const responses = await Promise.all([update("test/first"), update("test/second")])
|
||||
expect(responses.map((response) => response.status).sort()).toEqual([200, 409])
|
||||
})
|
||||
|
||||
test("rejects a project config target that escapes through a symlink", async () => {
|
||||
if (process.platform === "win32") return
|
||||
await using project = await tmpdir()
|
||||
await using outside = await tmpdir()
|
||||
await Filesystem.write(path.join(outside.path, "kilo.jsonc"), "{}")
|
||||
await symlink(outside.path, path.join(project.path, ".kilo"), "dir")
|
||||
const before = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
|
||||
|
||||
const response = await Server.Default().app.request("/config/overlay", {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json", "x-kilo-directory": project.path },
|
||||
body: JSON.stringify({
|
||||
scope: "project",
|
||||
expected: {
|
||||
path: before.targets.project.path,
|
||||
revision: before.targets.project.revision,
|
||||
},
|
||||
set: { model: "test/model" },
|
||||
}),
|
||||
})
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(await Bun.file(path.join(outside.path, "kilo.jsonc")).text()).not.toContain('"model"')
|
||||
})
|
||||
|
||||
test("does not expose partial content when an atomic replacement fails", async () => {
|
||||
await using project = await tmpdir()
|
||||
const file = path.join(project.path, "kilo.jsonc")
|
||||
await Filesystem.write(file, '{\n "model": "test/before"\n}\n')
|
||||
const target = await KilocodeConfigOverlay.target({ scope: "project", directory: project.path })
|
||||
|
||||
await expect(
|
||||
KilocodeConfigWriter.write({
|
||||
scope: "project",
|
||||
directory: project.path,
|
||||
expected: target,
|
||||
set: { model: "test/after" },
|
||||
write: async () => {
|
||||
throw new Error("simulated replacement failure")
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("simulated replacement failure")
|
||||
expect(await Bun.file(file).text()).toContain("test/before")
|
||||
})
|
||||
|
||||
test("rechecks missing target parents before replacement", async () => {
|
||||
if (process.platform === "win32") return
|
||||
await using project = await tmpdir()
|
||||
await using outside = await tmpdir()
|
||||
const target = await KilocodeConfigOverlay.target({ scope: "project", directory: project.path })
|
||||
|
||||
const result = await KilocodeConfigWriter.write({
|
||||
scope: "project",
|
||||
directory: project.path,
|
||||
expected: target,
|
||||
set: { model: "test/model" },
|
||||
beforeWrite: async () => {
|
||||
await rm(path.dirname(target.path), { recursive: true })
|
||||
await symlink(outside.path, path.dirname(target.path), "dir")
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ ok: false, code: "target-not-writable" })
|
||||
expect(await Bun.file(path.join(outside.path, "kilo.jsonc")).exists()).toBe(false)
|
||||
})
|
||||
|
||||
test("preserves restrictive config file permissions", async () => {
|
||||
if (process.platform === "win32") return
|
||||
await using project = await tmpdir()
|
||||
const file = path.join(project.path, "kilo.jsonc")
|
||||
await Filesystem.write(file, "{}", 0o600)
|
||||
await chmod(file, 0o600)
|
||||
const target = await KilocodeConfigOverlay.target({ scope: "project", directory: project.path })
|
||||
|
||||
const result = await KilocodeConfigWriter.write({
|
||||
scope: "project",
|
||||
directory: project.path,
|
||||
expected: target,
|
||||
set: { model: "test/model" },
|
||||
})
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect((await stat(file)).mode & 0o777).toBe(0o600)
|
||||
})
|
||||
|
||||
test("ignores unsafe patch paths", () => {
|
||||
const patched = KilocodeConfigOverlay.patch({
|
||||
scope: "project",
|
||||
@@ -141,7 +332,7 @@ describe("config overlay routes", () => {
|
||||
prompt: "kilo agent prompt",
|
||||
})
|
||||
expect(body.project.agent?.["opencode-only"]).toBeUndefined()
|
||||
expect(body.targets.project).toBe(path.join(project.path, ".kilo", "kilo.json"))
|
||||
expect(body.targets.project.path).toBe(path.join(project.path, ".kilo", "kilo.json"))
|
||||
})
|
||||
|
||||
test.serial("tolerates unsafe project config instead of failing the overlay", async () => {
|
||||
@@ -357,38 +548,42 @@ describe("config overlay routes", () => {
|
||||
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" } })
|
||||
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",
|
||||
)
|
||||
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"))
|
||||
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,
|
||||
})
|
||||
})
|
||||
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,
|
||||
})
|
||||
},
|
||||
15_000,
|
||||
)
|
||||
|
||||
test.serial("refreshes agent permissions after global permission update", async () => {
|
||||
await using global = await tmpdir()
|
||||
@@ -476,6 +671,7 @@ describe("config overlay routes", () => {
|
||||
Permission.evaluate("edit", "*", after.find((item) => item.name === "code")?.permission ?? []).action,
|
||||
).toBe("allow")
|
||||
},
|
||||
30_000,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -461,6 +461,8 @@ describe("kilocode tool registry indexing", () => {
|
||||
})
|
||||
|
||||
test("logs indexing bootstrap failures without blocking session bootstrap", async () => {
|
||||
const platform = process.env["KILO_PLATFORM"]
|
||||
process.env["KILO_PLATFORM"] = "cli"
|
||||
const logger = Log.create({ service: "kilocode-bootstrap" })
|
||||
const err = new Error("indexing init failed")
|
||||
const calls: string[] = []
|
||||
@@ -504,6 +506,8 @@ describe("kilocode tool registry indexing", () => {
|
||||
expect(indexing).toHaveBeenCalledTimes(1)
|
||||
expect(warn).toHaveBeenCalledWith("indexing bootstrap failed", { err })
|
||||
} finally {
|
||||
if (platform === undefined) delete process.env["KILO_PLATFORM"]
|
||||
else process.env["KILO_PLATFORM"] = platform
|
||||
indexing.mockRestore()
|
||||
warn.mockRestore()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user