39a7305c97
* 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.
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import path from "path"
|
|
import fs from "fs/promises"
|
|
import { pathToFileURL } from "url"
|
|
import { Effect, Layer } from "effect"
|
|
import { provideTestInstance, tmpdir } from "../fixture/fixture"
|
|
import { ProviderAuth } from "@/provider/auth"
|
|
import { ProviderID } from "../../src/provider/schema"
|
|
import { Plugin } from "@/plugin"
|
|
import { Auth } from "@/auth"
|
|
import { ModelCache } from "@/provider/model-cache" // kilocode_change
|
|
import { Bus } from "@/bus"
|
|
import { TestConfig } from "../fixture/config"
|
|
|
|
function layer(directory: string, plugins: string[]) {
|
|
return ProviderAuth.layer.pipe(
|
|
Layer.provide(Auth.defaultLayer),
|
|
Layer.provide(ModelCache.defaultLayer), // kilocode_change
|
|
Layer.provide(
|
|
Plugin.layer.pipe(
|
|
Layer.provide(Bus.layer),
|
|
Layer.provide(
|
|
TestConfig.layer({
|
|
get: () =>
|
|
Effect.succeed({
|
|
plugin: plugins,
|
|
plugin_origins: plugins.map((plugin) => ({
|
|
spec: plugin,
|
|
source: path.join(directory, "opencode.json"),
|
|
scope: "local" as const,
|
|
})),
|
|
}),
|
|
directories: () => Effect.succeed([directory]),
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
describe("plugin.auth-override", () => {
|
|
test("user plugin overrides built-in github-copilot auth", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
const pluginDir = path.join(dir, ".opencode", "plugin")
|
|
await fs.mkdir(pluginDir, { recursive: true })
|
|
|
|
await Bun.write(
|
|
path.join(pluginDir, "custom-copilot-auth.ts"),
|
|
[
|
|
"export default {",
|
|
' id: "demo.custom-copilot-auth",',
|
|
" server: async () => ({",
|
|
" auth: {",
|
|
' provider: "github-copilot",',
|
|
" methods: [",
|
|
' { type: "api", label: "Test Override Auth" },',
|
|
" ],",
|
|
" loader: async () => ({ access: 'test-token' }),",
|
|
" },",
|
|
" }),",
|
|
"}",
|
|
"",
|
|
].join("\n"),
|
|
)
|
|
},
|
|
})
|
|
|
|
await using plain = await tmpdir()
|
|
|
|
const plugin = pathToFileURL(path.join(tmp.path, ".opencode", "plugin", "custom-copilot-auth.ts")).href
|
|
const [methods, plainMethods] = await Promise.all([
|
|
provideTestInstance({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
return Effect.runPromise(
|
|
ProviderAuth.Service.use((svc) => svc.methods()).pipe(Effect.provide(layer(tmp.path, [plugin]))),
|
|
)
|
|
},
|
|
}),
|
|
provideTestInstance({
|
|
directory: plain.path,
|
|
fn: async () => {
|
|
return Effect.runPromise(
|
|
ProviderAuth.Service.use((svc) => svc.methods()).pipe(Effect.provide(layer(plain.path, []))),
|
|
)
|
|
},
|
|
}),
|
|
])
|
|
|
|
const copilot = methods[ProviderID.make("github-copilot")]
|
|
expect(copilot).toBeDefined()
|
|
expect(copilot.length).toBe(1)
|
|
expect(copilot[0].label).toBe("Test Override Auth")
|
|
expect(plainMethods[ProviderID.make("github-copilot")][0].label).not.toBe("Test Override Auth")
|
|
}, 30000)
|
|
})
|
|
|
|
const file = path.join(import.meta.dir, "../../src/plugin/index.ts")
|
|
|
|
describe("plugin.config-hook-error-isolation", () => {
|
|
test("config hooks are individually error-isolated in the layer factory", async () => {
|
|
const src = await Bun.file(file).text()
|
|
|
|
// Each hook's config call is wrapped in Effect.tryPromise with error logging + Effect.ignore
|
|
expect(src).toContain("plugin config hook failed")
|
|
|
|
const pattern =
|
|
/for\s*\(const hook of hooks\)\s*\{[\s\S]*?Effect\.tryPromise[\s\S]*?\.config\?\.\([\s\S]*?plugin config hook failed[\s\S]*?Effect\.ignore/
|
|
expect(pattern.test(src)).toBe(true)
|
|
})
|
|
})
|