refactor(cli): simplify indexing safety changes

This commit is contained in:
marius-kilocode
2026-08-28 08:28:28 +02:00
parent 72dcd42218
commit 02a2f723bd
6 changed files with 308 additions and 206 deletions
@@ -72,6 +72,9 @@ describe("search target confinement", () => {
const results = yield* Effect.all([Fiber.join(first), Fiber.join(second)])
expect(calls).toBe(1)
expect(results.map((items) => String(items[0].path))).toEqual(["scan-1.ts", "scan-1.ts"])
const replaced = { ...target, ino: target.ino === 0 ? 1 : 0 }
expect(Exit.isFailure(yield* list(replaced).pipe(Effect.scoped, Effect.exit))).toBe(true)
expect(calls).toBe(1)
yield* Scope.close(one, Exit.void)
yield* Scope.close(two, Exit.void)
const fresh = yield* list(target).pipe(Effect.scoped)
@@ -109,37 +112,6 @@ describe("search target confinement", () => {
),
)
it.live("does not start a second listing for a replaced directory while one is active", () =>
withTmp((tmp) =>
Effect.gen(function* () {
const fsys = yield* FSUtil.Service
const ripgrep = yield* Ripgrep.Service
const target = yield* SearchTarget.inspect(fsys, tmp)
let calls = 0
const list = yield* SearchTarget.listing(
fsys,
{
...ripgrep,
find: () =>
Effect.sync(() => {
calls++
return []
}),
},
100_000,
)
const scope = yield* Scope.make()
yield* Scope.provide(scope)(list(target))
const replaced = { ...target, ino: target.ino === 0 ? 1 : 0 }
expect(replaced.ino).not.toBe(target.ino)
const changed = yield* list(replaced).pipe(Effect.scoped, Effect.exit)
expect(Exit.isFailure(changed)).toBe(true)
expect(calls).toBe(1)
yield* Scope.close(scope, Exit.void)
}),
),
)
it.live("recognizes only real managed output files", () =>
withTmp((tmp) =>
Effect.gen(function* () {
+7 -21
View File
@@ -19,11 +19,8 @@ import { MemoryEvents } from "@/kilocode/memory/events"
import { installMemoryRuntime } from "@/kilocode/memory/runtime"
import { KiloToolRegistry } from "@/kilocode/tool/registry"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { registerDisposer } from "@/effect/instance-registry"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { KilocodeWatcher } from "@/kilocode/watcher"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" // kilocode_change
const log = Log.create({ service: "kilocode-bootstrap" })
@@ -45,21 +42,10 @@ export namespace KilocodeBootstrap {
const summary = yield* SessionSummary.Service
const provider = yield* Provider.Service
const memory = yield* MemoryService.Service
const locations = yield* LocationServiceMap.Service
const off = registerDisposer((directory) =>
Effect.runPromise(
locations
.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))
.pipe(
Effect.catchCause((cause) =>
Effect.sync(() => log.warn("location cleanup failed", { directory, err: Cause.squash(cause) })),
),
),
),
)
yield* Effect.addFinalizer(() => Effect.sync(off))
const watcher = yield* KilocodeWatcher.Service
const init = Effect.fn("KilocodeBootstrap.init")(function* () {
yield* watcher.init()
yield* kilo.init()
yield* MemoryLifecycle.subscribe({ bus, sessions, summary, provider, memory })
// Invalidate enabled cache on every memory state mutation (properties.directory holds the memory root).
@@ -108,17 +94,17 @@ export namespace KilocodeBootstrap {
AppNodeBuilder.build(Provider.node),
MemoryService.layer,
Bus.defaultLayer,
locationServiceMapLayer,
KilocodeWatcher.defaultLayer,
]),
)
const memory = LayerNode.make({ service: MemoryService.Service, layer: MemoryService.layer, deps: [] })
const locations = LayerNode.make({ service: LocationServiceMap.Service, layer: locationServiceMapLayer, deps: [] })
const watcher = LayerNode.make({ service: KilocodeWatcher.Service, layer: KilocodeWatcher.defaultLayer, deps: [] })
export const node = LayerNode.suspend(() =>
LayerNode.make({
service: Service,
layer,
deps: [KiloSessions.node, Session.node, SessionSummary.node, Provider.node, memory, Bus.node, locations],
deps: [KiloSessions.node, Session.node, SessionSummary.node, Provider.node, memory, Bus.node, watcher],
}),
)
}
+83
View File
@@ -0,0 +1,83 @@
import { InstanceState } from "@/effect/instance-state"
import { registerDisposer } from "@/effect/instance-registry"
import type { InstanceContext } from "@/project/instance-context"
import * as Log from "@opencode-ai/core/util/log"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Cause, Context, Effect, Exit, Layer, Scope } from "effect"
const log = Log.create({ service: "kilocode-watcher" })
export namespace KilocodeWatcher {
export interface Interface {
readonly init: () => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@kilocode/Watcher") {}
// Embedded editor clients (VS Code, JetBrains) have their own file watching
// and git integration and do not consume the CLI's vcs.branch.updated event,
// so they must not eagerly warm the location stack. The standalone CLI/TUI
// keeps this subscription for live branch-label updates.
export function eager(client = Flag.KILO_CLIENT) {
return client !== "vscode" && client !== "jetbrains"
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const locations = yield* LocationServiceMap.Service
const scope = yield* Scope.Scope
const active = new Map<string, Scope.Closeable>()
const ref = (directory: string) => Location.Ref.make({ directory: AbsolutePath.make(directory) })
const off = registerDisposer((directory) =>
Effect.runPromise(
Effect.gen(function* () {
const child = active.get(directory)
if (child) {
active.delete(directory)
yield* Scope.close(child, Exit.void)
}
yield* locations.invalidate(ref(directory))
}).pipe(Effect.ignore),
),
)
yield* Effect.addFinalizer(() => Effect.sync(off))
yield* Effect.addFinalizer(() =>
Effect.forEach(active.values(), (child) => Scope.close(child, Exit.void), { discard: true }).pipe(
Effect.andThen(Effect.sync(() => active.clear())),
),
)
const warm = (ctx: InstanceContext, child: Scope.Closeable) =>
Scope.provide(child)(locations.contextEffect(ref(ctx.directory)))
return Service.of({
init: Effect.fn("KilocodeWatcher.init")(function* () {
if (!eager() || (yield* Flag.KILO_EXPERIMENTAL_DISABLE_FILEWATCHER.pipe(Effect.orElseSucceed(() => true))))
return
const ctx = yield* InstanceState.context
if (ctx.project.vcs !== "git" || active.has(ctx.directory)) return
const child = yield* Scope.make()
active.set(ctx.directory, child)
yield* warm(ctx, child).pipe(
Effect.catchCause((cause) =>
Effect.gen(function* () {
if (active.get(ctx.directory) === child) active.delete(ctx.directory)
yield* Scope.close(child, Exit.void).pipe(Effect.ignore)
yield* Effect.sync(() => log.warn("instance watcher init failed", { err: Cause.squash(cause) }))
}),
),
Effect.forkIn(scope, { startImmediately: true }),
)
}),
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(locationServiceMapLayer))
}
@@ -2,41 +2,31 @@ import { describe, expect, test } from "bun:test"
import type { VcsInfo } from "@kilocode/sdk/v2"
import { create } from "../../../../src/kilocode/cli/cmd/tui/branch-refresh"
function setup(input: {
workspace?: string
directory: string
project: string
branch?: string
bootstrap?: boolean
}) {
function setup(workspace?: string) {
const state = {
scope: { workspace: input.workspace, directory: input.directory, project: input.project },
vcs: input.bootstrap === false ? undefined : ({ branch: input.branch, default_branch: "main" } as VcsInfo),
scope: { workspace, directory: "/repo", project: "project" },
vcs: { branch: "main", default_branch: "main" } as VcsInfo | undefined,
}
const calls: Array<{ workspace?: string; directory?: string }> = []
const updates: VcsInfo[] = []
const pending = Promise.withResolvers<{ data?: VcsInfo }>()
const refresh = create({
get: async (route) => {
calls.push(route)
return pending.promise
},
apply: (data) => {
updates.push(data)
state.vcs = data
},
apply: (data) => (state.vcs = data),
scope: () => state.scope,
ready: () => state.vcs !== undefined,
})
return { state, calls, updates, pending, refresh }
return { state, calls, pending, refresh }
}
describe("TUI branch refresh", () => {
test("waits for bootstrap and then applies the complete VCS snapshot", async () => {
const value = setup({ directory: "/repo", project: "project", bootstrap: false })
const value = setup()
value.state.vcs = undefined
await value.refresh.refresh()
expect(value.calls).toEqual([])
expect(value.updates).toEqual([])
value.state.vcs = { branch: "main", default_branch: "main" }
const run = value.refresh.refresh()
@@ -45,64 +35,43 @@ describe("TUI branch refresh", () => {
expect(value.state.vcs).toEqual({ branch: "feature", default_branch: "main" })
})
test("routes workspace refreshes and preserves the default branch", async () => {
const value = setup({ workspace: "ws", directory: "/repo/ws", project: "project" })
const run = value.refresh.refresh()
expect(value.calls).toEqual([{ workspace: "ws" }])
value.pending.resolve({ data: { branch: "feature", default_branch: "main" } })
await run
expect(value.updates).toEqual([{ branch: "feature", default_branch: "main" }])
})
test("routes directory refreshes without a workspace", async () => {
const value = setup({ directory: "/repo", project: "project" })
const run = value.refresh.refresh()
expect(value.calls).toEqual([{ directory: "/repo" }])
value.pending.resolve({ data: { branch: "feature", default_branch: "main" } })
await run
expect(value.updates).toHaveLength(1)
})
test("updates default branch metadata even when the current branch is unchanged", async () => {
const value = setup({ directory: "/repo", project: "project", branch: "feature" })
const run = value.refresh.refresh()
value.pending.resolve({ data: { branch: "feature", default_branch: "develop" } })
await run
expect(value.state.vcs).toEqual({ branch: "feature", default_branch: "develop" })
})
test.each(["ws", undefined])(
"routes %s refreshes and updates metadata when the branch is unchanged",
async (workspace) => {
const value = setup(workspace)
const run = value.refresh.refresh()
expect(value.calls).toEqual([workspace ? { workspace } : { directory: "/repo" }])
value.pending.resolve({ data: { branch: "main", default_branch: "develop" } })
await run
expect(value.state.vcs).toEqual({ branch: "main", default_branch: "develop" })
},
)
test("ignores responses after the scope changes", async () => {
const value = setup({ workspace: "ws-a", directory: "/repo/a", project: "project" })
const value = setup("ws-a")
const run = value.refresh.refresh()
value.state.scope = { workspace: "ws-b", directory: "/repo/b", project: "project" }
value.pending.resolve({ data: { branch: "stale", default_branch: "main" } })
await run
expect(value.updates).toEqual([])
expect(value.state.vcs).toEqual({ branch: "main", default_branch: "main" })
})
test("ignores responses after disposal", async () => {
const value = setup({ directory: "/repo", project: "project" })
const value = setup()
const run = value.refresh.refresh()
value.refresh.dispose()
value.pending.resolve({ data: { branch: "stale", default_branch: "main" } })
await run
await value.refresh.refresh()
expect(value.calls).toHaveLength(1)
expect(value.updates).toEqual([])
expect(value.state.vcs).toEqual({ branch: "main", default_branch: "main" })
})
test("keeps the current VCS snapshot when the response has no data", async () => {
const value = setup({ directory: "/repo", project: "project", branch: "main" })
const value = setup()
const run = value.refresh.refresh()
value.pending.resolve({})
await run
expect(value.state.vcs).toEqual({ branch: "main", default_branch: "main" })
expect(value.updates).toEqual([])
})
})
@@ -1,85 +1,196 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { describe, expect } from "bun:test"
import { describe, expect, test } from "bun:test"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Deferred, Effect } from "effect"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { ConfigProvider, Context, Deferred, Effect, Fiber, Layer, LayerMap } from "effect"
import * as TestConsole from "effect/testing/TestConsole"
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
import { InstanceRef } from "../../src/effect/instance-ref"
import { disposeInstance } from "../../src/effect/instance-registry"
import { Git } from "../../src/git"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { Vcs } from "../../src/project/vcs"
import { TestInstance } from "../fixture/fixture"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { KilocodeWatcher } from "../../src/kilocode/watcher"
import type { InstanceContext } from "../../src/project/instance-context"
import { TestInstance, tmpdirScoped } from "../fixture/fixture"
import { awaitWithTimeout, testEffect } from "../lib/effect"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap, type LocationServices } from "@opencode-ai/core/location-services"
const layer = LayerNode.compile(LayerNode.group([Vcs.node, Git.node, EventV2Bridge.node, CrossSpawnSpawner.node]))
const it = testEffect(layer)
const layer = Layer.mergeAll(
AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
AppNodeBuilder.build(Git.node),
AppNodeBuilder.build(CrossSpawnSpawner.node),
)
const config = ConfigProvider.layerAdd(ConfigProvider.fromUnknown({ KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "false" }), {
asPrimary: true,
})
const it = testEffect(layer.pipe(Layer.provideMerge(config)))
const direct = testEffect(
LayerNode.compile(LayerNode.group([Vcs.node, Git.node, EventV2Bridge.node, CrossSpawnSpawner.node])),
)
const git = Effect.fn("VcsWatcherTest.git")(function* (cwd: string, args: string[]) {
const result = yield* Git.Service.use((git) => git.run(args, { cwd }))
if (result.exitCode !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr.toString("utf8")}`)
// The watcher is unreliable on Windows CI, so this test only runs on unix.
const live = process.platform === "win32" ? it.live.skip : it.live
describe("KilocodeWatcher.eager", () => {
test("skips eager location watchers for VS Code", () => {
expect(KilocodeWatcher.eager("vscode")).toBe(false)
})
test("skips eager location watchers for JetBrains", () => {
expect(KilocodeWatcher.eager("jetbrains")).toBe(false)
})
test("keeps eager location watchers for the standalone CLI", () => {
expect(KilocodeWatcher.eager("cli")).toBe(true)
expect(KilocodeWatcher.eager(undefined)).toBe(true)
})
})
describe("Vcs without native watchers", () => {
it.instance(
"branch reads the current git branch after a switch",
() =>
live(
"instances publish branch updates after git switch",
() =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const git = yield* Git.Service
const store = yield* InstanceStore.Service
const current = yield* git.branch(dir)
if (!current) return yield* Effect.die("missing initial branch")
const branch = `watch-${Math.random().toString(36).slice(2)}`
const created = yield* git.run(["branch", branch], { cwd: dir })
expect(created.exitCode).toBe(0)
yield* store.load({ directory: dir })
const pending = yield* Deferred.make<string | undefined>()
const handler = (event: GlobalEvent) => {
if (event.directory !== dir || event.payload.type !== "vcs.branch.updated") return
if (event.payload.properties.branch !== branch) return
Deferred.doneUnsafe(pending, Effect.succeed(event.payload.properties.branch))
}
GlobalBus.on("event", handler)
yield* Effect.addFinalizer(() => Effect.sync(() => GlobalBus.off("event", handler)))
// The watcher exposes no readiness signal (its .git subscription is forked
// during instance warm-up), so keep generating HEAD churn in the background
// and synchronize on the event itself with the full test budget.
const churn = yield* Effect.gen(function* () {
while (true) {
yield* git.run(["switch", current], { cwd: dir })
yield* Effect.sleep("50 millis")
yield* git.run(["switch", branch], { cwd: dir })
yield* Effect.sleep("100 millis")
}
}).pipe(Effect.forkScoped)
const updated = yield* awaitWithTimeout(
Deferred.await(pending),
"timed out waiting for vcs.branch.updated",
"15 seconds",
)
yield* Fiber.interrupt(churn)
expect(updated).toBe(branch)
}),
20_000,
)
direct.instance(
"refreshes branch reads, events, and diffs without native watchers",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const vcs = yield* Vcs.Service
const events = yield* EventV2Bridge.Service
const git = Effect.fn(function* (args: string[]) {
const result = yield* Git.Service.use((git) => git.run(args, { cwd: test.directory }))
expect(result.exitCode).toBe(0)
})
yield* git(["branch", "-M", "main"])
expect(yield* vcs.branch()).toBe("main")
const updated = yield* Deferred.make<string | undefined>()
const off = yield* events.listen((event) => {
if (event.type === Vcs.Event.BranchUpdated.type)
Deferred.doneUnsafe(updated, Effect.succeed((event.data as typeof Vcs.Event.BranchUpdated.data.Type).branch))
return Effect.void
})
yield* Effect.addFinalizer(() => off)
yield* git(["switch", "-c", "feature"])
expect(yield* vcs.branch()).toBe("feature")
expect(yield* awaitWithTimeout(Deferred.await(updated), "timed out waiting for branch update")).toBe("feature")
yield* git(["switch", "main"])
expect(yield* vcs.branch()).toBe("main")
yield* git(["switch", "feature"])
yield* Effect.promise(() => Bun.write(`${test.directory}/branch.txt`, "branch\n"))
yield* git(["add", "branch.txt"])
yield* git(["commit", "--no-gpg-sign", "-m", "branch change"])
const diff = yield* vcs.diff("branch")
expect(diff.find((item) => item.file === "branch.txt")).toMatchObject({ status: "added" })
}),
{ git: true },
)
test.serial(
"isolates location lifetimes between instances",
async () => {
await Effect.runPromise(
Effect.gen(function* () {
const test = yield* TestInstance
const vcs = yield* Vcs.Service
yield* vcs.branch()
const branch = `fresh-${Math.random().toString(36).slice(2)}`
yield* git(test.directory, ["branch", branch])
yield* git(test.directory, ["switch", branch])
const one = yield* tmpdirScoped()
const two = yield* tmpdirScoped()
const warmed = new Map<string, number>()
const invalidated: string[] = []
const map = yield* LayerMap.make((ref: Location.Ref) =>
Layer.effectContext(
Effect.acquireRelease(
Effect.sync(() => {
warmed.set(ref.directory, (warmed.get(ref.directory) ?? 0) + 1)
return Context.empty() as Context.Context<LocationServices>
}),
() => Effect.sync(() => invalidated.push(ref.directory)),
),
),
)
const watcher = KilocodeWatcher.layer.pipe(Layer.provide(Layer.succeed(LocationServiceMap.Service, map)))
const services = yield* Layer.build(watcher)
const init = (directory: string) =>
KilocodeWatcher.Service.use((service) => service.init()).pipe(
Effect.provide(services),
Effect.provideService(InstanceRef, {
directory,
worktree: directory,
project: { vcs: "git" },
} as InstanceContext),
)
expect(yield* vcs.branch()).toBe(branch)
}),
{ git: true },
)
it.instance(
"branch diff reads the current branch after a switch",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const vcs = yield* Vcs.Service
yield* git(test.directory, ["branch", "-M", "main"])
expect(yield* vcs.branch()).toBe("main")
const branch = `diff-${Math.random().toString(36).slice(2)}`
yield* git(test.directory, ["switch", "-c", branch])
yield* Effect.promise(() => Bun.write(`${test.directory}/branch.txt`, "branch\n"))
yield* git(test.directory, ["add", "branch.txt"])
yield* git(test.directory, ["commit", "--no-gpg-sign", "-m", "branch change"])
const diff = yield* vcs.diff("branch")
expect(diff.find((item) => item.file === "branch.txt")).toMatchObject({ status: "added" })
}),
{ git: true },
)
it.instance(
"publishes a branch update when a fresh branch read changes",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const vcs = yield* Vcs.Service
const events = yield* EventV2Bridge.Service
yield* vcs.branch()
const branch = `event-${Math.random().toString(36).slice(2)}`
yield* git(test.directory, ["branch", branch])
const updated = yield* Deferred.make<string | undefined>()
const off = yield* events.listen((event) => {
if (event.type === Vcs.Event.BranchUpdated.type)
Deferred.doneUnsafe(
updated,
Effect.succeed((event.data as typeof Vcs.Event.BranchUpdated.data.Type).branch),
)
return Effect.void
})
yield* Effect.addFinalizer(() => off)
yield* git(test.directory, ["switch", branch])
expect(yield* vcs.branch()).toBe(branch)
expect(yield* awaitWithTimeout(Deferred.await(updated), "timed out waiting for branch update")).toBe(branch)
}),
{ git: true },
)
})
yield* init(one).pipe(
Effect.provide(
ConfigProvider.layer(ConfigProvider.fromUnknown({ KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" })),
),
)
expect(warmed.size).toBe(0)
yield* init(one)
yield* init(one)
yield* init(two)
yield* Effect.yieldNow
expect(warmed).toEqual(
new Map([
[one, 1],
[two, 1],
]),
)
yield* Effect.promise(() => disposeInstance(one))
expect(invalidated).toEqual([one])
yield* Effect.promise(() => disposeInstance(two))
expect(invalidated).toEqual([one, two])
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(AppNodeBuilder.build(CrossSpawnSpawner.node), TestConsole.layer, config)),
),
)
},
20_000,
)
@@ -1,18 +1,16 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LocationServiceMap, type LocationServices } from "@opencode-ai/core/location-services"
import type { Location } from "@opencode-ai/core/location"
import { afterEach, describe, expect, spyOn, test } from "bun:test"
import { Effect, Layer, LayerMap, Schema, Stream } from "effect"
import { Effect, Layer, Schema, Stream } from "effect"
import * as Log from "@opencode-ai/core/util/log"
import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { KiloIndexing } from "../../src/kilocode/indexing"
import { KilocodeBootstrap } from "../../src/kilocode/bootstrap"
import { KilocodeWatcher } from "../../src/kilocode/watcher"
import { KiloSessions } from "../../src/kilo-sessions/kilo-sessions"
import { KiloMemory } from "@kilocode/kilo-memory/effect"
import { MemoryService } from "@kilocode/kilo-memory/effect/service"
import { InstanceState } from "../../src/effect/instance-state"
import { disposeInstance } from "../../src/effect/instance-registry"
import { KiloToolRegistry } from "../../src/kilocode/tool/registry"
import { Provider } from "../../src/provider/provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
@@ -21,7 +19,7 @@ import { Session } from "../../src/session/session"
import { SessionSummary } from "../../src/session/summary"
import { ToolRegistry } from "../../src/tool/registry"
import type * as Tool from "../../src/tool/tool"
import { disposeAllInstances, provideTmpdirInstance, tmpdir } from "../fixture/fixture"
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
@@ -456,8 +454,7 @@ describe("kilocode tool registry indexing", () => {
}
})
test("bootstraps sessions without indexing and preserves location cleanup", async () => {
await using tmp = await tmpdir()
test("does not start indexing during session bootstrap", async () => {
const platform = process.env["KILO_PLATFORM"]
process.env["KILO_PLATFORM"] = "cli"
const calls: string[] = []
@@ -483,38 +480,22 @@ describe("kilocode tool registry indexing", () => {
const session = Layer.succeed(Session.Service, {} as Session.Interface)
const summary = Layer.succeed(SessionSummary.Service, {} as SessionSummary.Interface)
const provider = Layer.succeed(Provider.Service, {} as Provider.Interface)
const invalidated: string[] = []
const locations = Layer.effect(
LocationServiceMap.Service,
LayerMap.make(
(_: Location.Ref): Layer.Layer<LocationServices> =>
Layer.effectContext(Effect.die("Unexpected location warmup")),
).pipe(
Effect.map((map) => ({
...map,
invalidate: (ref: Location.Ref) => Effect.sync(() => void invalidated.push(String(ref.directory))),
})),
),
)
const watcher = Layer.succeed(KilocodeWatcher.Service, KilocodeWatcher.Service.of({ init: () => Effect.void }))
const indexing = spyOn(KiloIndexing, "init").mockResolvedValue(undefined)
try {
await Effect.runPromise(
KilocodeBootstrap.Service.use((svc) => svc.init()).pipe(
Effect.andThen(Effect.promise(() => disposeInstance(tmp.path))),
Effect.provide(
KilocodeBootstrap.layer.pipe(Layer.provide([sessions, bus, memory, session, summary, provider, locations])),
KilocodeBootstrap.layer.pipe(Layer.provide([sessions, bus, memory, session, summary, provider, watcher])),
),
Effect.scoped,
),
)
await new Promise<void>((resolve) => setImmediate(resolve))
await new Promise((resolve) => setTimeout(resolve, 0))
expect(calls).toEqual(["sessions"])
expect(indexing).not.toHaveBeenCalled()
expect(invalidated).toEqual([tmp.path])
await disposeInstance(tmp.path)
expect(invalidated).toEqual([tmp.path])
} finally {
if (platform === undefined) delete process.env["KILO_PLATFORM"]
else process.env["KILO_PLATFORM"] = platform