mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
* refactor(opencode): migrate ModelCache and Config to effect-native services Remove legacy async wrapper functions from Config module and convert ModelCache from a stateful namespace with module-level Maps into a proper Effect service with Context/Layer semantics. Key changes: - Delete Config's `makeRuntime`-based async wrappers (get, getGlobal, update, warnings, etc.) — all callsites now use `Config.Service.use(...)` through AppRuntime - Rewrite ModelCache as an Effect service with HttpClient dependency injection, replacing imperative Map-based caching with Effect-native Ref cells and TTL logic - Convert KiloSessions.init and KilocodeBootstrap.init into proper Effect services with Layer-based dependency injection - Wire ModelCache.Service into AppLayer, ProviderAuth, ModelsDev, and HTTP API handler layers - Update Permission.layer to depend on Config.Service directly instead of calling Config async wrappers - Add new test files for KiloSessions and ModelCache Effect integration - Remove stale Config.get spyOn mocks from tests that no longer need them (experimental-session-list, recall) - Fix indexing-auth to use typed IndexingConfig parameter instead of untyped record access * fix(model-cache): resolve race conditions in concurrent fetch and cache invalidation Introduce versioned cache cells with proper key derivation to prevent stale responses from overwriting fresher data during concurrent fetches. - Add version tracking to detect and discard outdated fetch results - Derive cache keys from provider-specific options (baseURL, token, apiKey) to isolate concurrent requests with different credentials - Make ModelCache.clear async to properly await invalidation across layers - Update OrganizationDeps.clear signature to allow Promise<void> return - Add concurrency and ordering tests for fetch/refresh race scenarios - Rename local variable from `state` to `entry` in kilo-sessions sync loop * chore(opencode): remove duplicate imports and fix test layer composition Remove duplicate `AppRuntime` imports introduced during merge and update kilo-sessions tests to use Effect-native Auth service instead of static module calls. - Remove duplicate `AppRuntime` import in index.ts and instance.ts - Add Auth.defaultLayer to test layer helper - Refactor test to yield Auth.Service and use instance methods - Reorder Effect.provide/Effect.ensuring for correct resource cleanup * style(opencode): normalize kilocode_change marker comments to block format Standardize inline `// kilocode_change` annotations across source and test files to use consistent `// kilocode_change start` / `// kilocode_change end` block delimiters, improving readability and grep-ability of custom modifications.
142 lines
5.0 KiB
TypeScript
142 lines
5.0 KiB
TypeScript
// kilocode_change - new file
|
|
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
|
|
import { $ } from "bun"
|
|
import { Effect } from "effect"
|
|
import path from "path"
|
|
import { WithInstance } from "../../src/project/with-instance"
|
|
import { RecallTool } from "../../src/tool/recall"
|
|
import { AppRuntime } from "../../src/effect/app-runtime"
|
|
import { resetDatabase } from "../fixture/db"
|
|
import { tmpdir } from "../fixture/fixture"
|
|
import type { Tool } from "../../src/tool/tool"
|
|
import { SessionID, MessageID } from "../../src/session/schema"
|
|
import { RemoteSender } from "../../src/kilo-sessions/remote-sender"
|
|
|
|
beforeEach(() => {
|
|
spyOn(RemoteSender, "create").mockReturnValue({ handle() {}, dispose() {} })
|
|
})
|
|
|
|
const ctx: Tool.Context = {
|
|
sessionID: SessionID.make("ses_test"),
|
|
messageID: MessageID.make("msg_test"),
|
|
callID: "call_test",
|
|
agent: "code",
|
|
abort: AbortSignal.any([]),
|
|
messages: [],
|
|
metadata: () => Effect.void,
|
|
ask: () => Effect.void,
|
|
}
|
|
|
|
afterEach(async () => {
|
|
mock.restore()
|
|
await resetDatabase()
|
|
})
|
|
|
|
describe("tool.recall", () => {
|
|
test("search is limited to the current project worktrees", async () => {
|
|
await using first = await tmpdir({ git: true })
|
|
await using second = await tmpdir({ git: true })
|
|
const worktree = path.join(first.path, "..", path.basename(first.path) + "-worktree")
|
|
|
|
try {
|
|
await $`git worktree add ${worktree} -b test-branch-${Date.now()}`.cwd(first.path).quiet()
|
|
await Bun.write(path.join(first.path, ".git", "opencode"), "stale-project-id")
|
|
|
|
try {
|
|
const { Session } = await import("../../src/session/session")
|
|
await WithInstance.provide({
|
|
directory: first.path,
|
|
fn: async () => Session.create({ title: "search-target root" }),
|
|
})
|
|
await WithInstance.provide({
|
|
directory: worktree,
|
|
fn: async () => Session.create({ title: "search-target worktree" }),
|
|
})
|
|
await WithInstance.provide({
|
|
directory: second.path,
|
|
fn: async () => Session.create({ title: "search-target other" }),
|
|
})
|
|
|
|
const result = await WithInstance.provide({
|
|
directory: first.path,
|
|
fn: async () => {
|
|
const info = await AppRuntime.runPromise(RecallTool)
|
|
const tool = await AppRuntime.runPromise(info.init())
|
|
return AppRuntime.runPromise(tool.execute({ mode: "search", query: "search-target" }, ctx))
|
|
},
|
|
})
|
|
|
|
expect(result.output).toContain("search-target root")
|
|
expect(result.output).toContain("search-target worktree")
|
|
expect(result.output).not.toContain("search-target other")
|
|
} finally {
|
|
mock.restore()
|
|
}
|
|
} finally {
|
|
await $`git worktree remove ${worktree}`.cwd(first.path).quiet().nothrow()
|
|
}
|
|
})
|
|
|
|
test("read rejects sessions from another project", async () => {
|
|
await using first = await tmpdir({ git: true })
|
|
await using second = await tmpdir({ git: true })
|
|
|
|
try {
|
|
const { Session } = await import("../../src/session/session")
|
|
const session = await WithInstance.provide({
|
|
directory: second.path,
|
|
fn: async () => Session.create({ title: "other-project-session" }),
|
|
})
|
|
|
|
const err = await WithInstance.provide({
|
|
directory: first.path,
|
|
fn: async () => {
|
|
const info = await AppRuntime.runPromise(RecallTool)
|
|
const tool = await AppRuntime.runPromise(info.init())
|
|
return AppRuntime.runPromise(tool.execute({ mode: "read", sessionID: session.id }, ctx)).catch(
|
|
(error: unknown) => error as Error,
|
|
)
|
|
},
|
|
})
|
|
|
|
expect(err).toBeInstanceOf(Error)
|
|
expect((err as Error).message).toContain("belongs to a different workspace")
|
|
} finally {
|
|
mock.restore()
|
|
}
|
|
})
|
|
|
|
test("read allows sessions from sibling worktrees when project IDs drift", async () => {
|
|
await using first = await tmpdir({ git: true })
|
|
const worktree = path.join(first.path, "..", path.basename(first.path) + "-worktree")
|
|
|
|
try {
|
|
await $`git worktree add ${worktree} -b test-branch-${Date.now()}`.cwd(first.path).quiet()
|
|
await Bun.write(path.join(first.path, ".git", "opencode"), "stale-project-id")
|
|
|
|
try {
|
|
const { Session } = await import("../../src/session/session")
|
|
const session = await WithInstance.provide({
|
|
directory: worktree,
|
|
fn: async () => Session.create({ title: "worktree readable" }),
|
|
})
|
|
|
|
const result = await WithInstance.provide({
|
|
directory: first.path,
|
|
fn: async () => {
|
|
const info = await AppRuntime.runPromise(RecallTool)
|
|
const tool = await AppRuntime.runPromise(info.init())
|
|
return AppRuntime.runPromise(tool.execute({ mode: "read", sessionID: session.id }, ctx))
|
|
},
|
|
})
|
|
|
|
expect(result.output).toContain("# Session: worktree readable")
|
|
} finally {
|
|
mock.restore()
|
|
}
|
|
} finally {
|
|
await $`git worktree remove ${worktree}`.cwd(first.path).quiet().nothrow()
|
|
}
|
|
})
|
|
})
|