Merge branch 'main' into catrielmuller/fix-big-repos

This commit is contained in:
Catriel Müller
2026-05-04 09:57:27 -03:00
committed by GitHub
669 changed files with 13233 additions and 7237 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ import { Effect, Layer, Option } from "effect"
import { AccountRepo } from "../../src/account/repo"
import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema"
import { Database } from "../../src/storage"
import { Database } from "@/storage/db"
import { testEffect } from "../lib/effect"
const truncate = Layer.effectDiscard(
@@ -15,7 +15,7 @@ import {
RefreshToken,
UserCode,
} from "../../src/account/schema"
import { Database } from "../../src/storage"
import { Database } from "@/storage/db"
import { testEffect } from "../lib/effect"
const truncate = Layer.effectDiscard(
+4 -4
View File
@@ -171,7 +171,7 @@ test("explore agent denies edit and write", async () => {
})
test("explore agent asks for external directories and allows Truncate.GLOB", async () => {
const { Truncate } = await import("../../src/tool")
const { Truncate } = await import("../../src/tool/truncate")
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
@@ -617,7 +617,7 @@ test("legacy tools config maps write/edit/patch to edit permission", async () =>
})
test("Truncate.GLOB is allowed even when user denies external_directory globally", async () => {
const { Truncate } = await import("../../src/tool")
const { Truncate } = await import("../../src/tool/truncate")
await using tmp = await tmpdir({
config: {
permission: {
@@ -639,7 +639,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
})
test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
const { Truncate } = await import("../../src/tool")
const { Truncate } = await import("../../src/tool/truncate")
await using tmp = await tmpdir({
config: {
agent: {
@@ -667,7 +667,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory per-agen
})
test("explicit Truncate.GLOB deny is respected", async () => {
const { Truncate } = await import("../../src/tool")
const { Truncate } = await import("../../src/tool/truncate")
await using tmp = await tmpdir({
config: {
permission: {
+1 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Auth } from "../../src/auth"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -3,7 +3,7 @@ import { Deferred, Effect, Layer, Schema, Stream } from "effect"
import { Bus } from "../../src/bus"
import { BusEvent } from "../../src/bus/bus-event"
import { Instance } from "../../src/project/instance"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -0,0 +1,93 @@
import { Database } from "bun:sqlite"
import path from "node:path"
import { expect, test } from "bun:test"
import { offsetToPosition, resolveZedSelection } from "../../../src/cli/cmd/tui/context/editor-zed"
import { tmpdir } from "../../fixture/fixture"
type ZedFixtureOptions = {
workspacePaths?: string | null
itemKind?: string
editor?: boolean
selectionStart?: number | null
selectionEnd?: number | null
}
async function writeZedFixture(dir: string, options: ZedFixtureOptions = {}) {
const dbPath = path.join(dir, "zed.sqlite")
const filePath = path.join(dir, "file.ts")
await Bun.write(filePath, "one\ntwo\nthree")
const db = new Database(dbPath)
db.run("create table workspaces (workspace_id integer, paths text, timestamp text)")
db.run("create table panes (pane_id integer, workspace_id integer, active integer)")
db.run("create table items (item_id integer, workspace_id integer, pane_id integer, active integer, kind text)")
db.run("create table editors (item_id integer, workspace_id integer, buffer_path text, contents text)")
db.run("create table editor_selections (editor_id integer, workspace_id integer, start integer, end integer)")
db.run("insert into workspaces values (1, ?, ?)", [options.workspacePaths ?? JSON.stringify([dir]), "2026-04-27"])
db.run("insert into panes values (1, 1, 1)")
db.run("insert into items values (1, 1, 1, 1, ?)", [options.itemKind ?? "Editor"])
if (options.editor !== false) {
db.run("insert into editors values (1, 1, ?, ?)", [filePath, "one\ntwo\nthree"])
db.run("insert into editor_selections values (1, 1, ?, ?)", [
options.selectionStart === undefined ? 4 : options.selectionStart,
options.selectionEnd === undefined ? 7 : options.selectionEnd,
])
}
db.close()
return { dbPath, filePath }
}
test("offsetToPosition converts Zed offsets to 1-based editor positions", () => {
expect(offsetToPosition("one\ntwo\nthree", 0)).toEqual({ line: 1, character: 1 })
expect(offsetToPosition("one\ntwo\nthree", 4)).toEqual({ line: 2, character: 1 })
expect(offsetToPosition("one\ntwo\nthree", 6)).toEqual({ line: 2, character: 3 })
expect(offsetToPosition("one\ntwo\nthree", 100)).toEqual({ line: 3, character: 6 })
})
test("resolveZedSelection returns active editor selection", async () => {
await using tmp = await tmpdir()
const fixture = await writeZedFixture(tmp.path)
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({
type: "selection",
selection: {
text: "two",
filePath: fixture.filePath,
source: "zed",
selection: {
start: { line: 2, character: 1 },
end: { line: 2, character: 4 },
},
},
})
})
test("resolveZedSelection returns empty when no workspace matches", async () => {
await using tmp = await tmpdir()
const fixture = await writeZedFixture(tmp.path, {
workspacePaths: JSON.stringify([path.join(path.dirname(tmp.path), "other-workspace")]),
})
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({ type: "empty" })
})
test("resolveZedSelection returns unavailable when a Zed terminal is active", async () => {
await using tmp = await tmpdir()
const fixture = await writeZedFixture(tmp.path, { itemKind: "Terminal", editor: false })
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({ type: "unavailable" })
})
test("resolveZedSelection returns unavailable when the database cannot be queried", async () => {
await using tmp = await tmpdir()
expect(await resolveZedSelection(path.join(tmp.path, "missing.sqlite"), tmp.path)).toEqual({ type: "unavailable" })
})
test("resolveZedSelection returns unavailable when active selection is missing offsets", async () => {
await using tmp = await tmpdir()
const fixture = await writeZedFixture(tmp.path, { selectionStart: null, selectionEnd: null })
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({ type: "unavailable" })
})
@@ -5,7 +5,7 @@ import { pathToFileURL } from "url"
import { tmpdir } from "../../fixture/fixture"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
import { Npm } from "../../../src/npm"
import { Npm } from "@opencode-ai/core/npm"
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
@@ -4,9 +4,9 @@ import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../../fixture/fixture"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
import { Global } from "../../../src/global"
import { Global } from "@opencode-ai/core/global"
import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
import { Filesystem } from "../../../src/util/"
import { Filesystem } from "@/util/filesystem"
const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme")
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test"
import { Log } from "../../../src/util"
import * as Log from "@opencode-ai/core/util/log"
Log.init({ print: false })
@@ -3,7 +3,7 @@ import fs from "fs/promises"
import path from "path"
import { tmpdir } from "../../fixture/fixture"
import * as App from "../../../src/cli/cmd/tui/app"
import { Rpc } from "../../../src/util"
import { Rpc } from "@/util/rpc"
import { UI } from "../../../src/cli/ui"
import * as Timeout from "../../../src/util/timeout"
import * as Network from "../../../src/cli/network"
@@ -3,9 +3,9 @@ import { Effect } from "effect"
import path from "path"
import { provideInstance, tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Config } from "../../src/config"
import { Config } from "@/config/config"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Color } from "../../src/util"
import { Color } from "@/util/color"
import { AppRuntime } from "../../src/effect/app-runtime"
const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get()))
+174 -27
View File
@@ -1,19 +1,20 @@
import { test, expect, describe, mock, afterEach, beforeEach } from "bun:test"
import { Effect, Layer, Option } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config, ConfigManaged } from "../../src/config"
import { Config } from "@/config/config"
import { ConfigManaged } from "@/config/managed"
import { ConfigParse } from "../../src/config/parse"
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Instance } from "../../src/project/instance"
import { Auth } from "../../src/auth"
import { Account } from "../../src/account/account"
import { AccessToken, AccountID, OrgID } from "../../src/account/schema"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Env } from "../../src/env"
import { provideTmpdirInstance } from "../fixture/fixture"
import { tmpdir } from "../fixture/fixture"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
/** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */
@@ -23,11 +24,11 @@ const infra = CrossSpawnSpawner.defaultLayer.pipe(
import path from "path"
import fs from "fs/promises"
import { pathToFileURL } from "url"
import { Global } from "../../src/global"
import { Global } from "@opencode-ai/core/global"
import { ProjectID } from "../../src/project/schema"
import { Filesystem } from "../../src/util"
import { Filesystem } from "@/util/filesystem"
import { ConfigPlugin } from "@/config/plugin"
import { Npm } from "@/npm"
import { Npm } from "@opencode-ai/core/npm"
const emptyAccount = Layer.mock(Account.Service)({
active: () => Effect.succeed(Option.none()),
@@ -55,6 +56,8 @@ const it = testEffect(layer)
const load = () => Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(layer)))
const save = (config: Config.Info) =>
Effect.runPromise(Config.Service.use((svc) => svc.update(config)).pipe(Effect.scoped, Effect.provide(layer)))
const saveGlobal = (config: Config.Info) =>
Effect.runPromise(Config.Service.use((svc) => svc.updateGlobal(config)).pipe(Effect.scoped, Effect.provide(layer)))
const clear = (wait = false) =>
Effect.runPromise(Config.Service.use((svc) => svc.invalidate(wait)).pipe(Effect.scoped, Effect.provide(layer)))
const listDirs = () =>
@@ -142,6 +145,104 @@ test("loads JSON config file", async () => {
})
})
test("loads shell config field", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
shell: "bash",
})
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await load()
expect(config.shell).toBe("bash")
},
})
})
test("updates config and preserves empty shell sentinel", async () => {
// kilocode_change - upstream hardcodes project config to config.json; Kilo writes to kilo.json
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
shell: "bash",
})
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
await save({ shell: "" })
const writtenConfig = await Filesystem.readJson<{ shell?: string }>(path.join(tmp.path, "kilo.json"))
expect(writtenConfig.shell).toBe("")
},
})
})
test("updates global config and omits empty shell key in json", async () => {
// kilocode_change - globalConfigFile() prefers kilo.json over opencode.json
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
shell: "bash",
})
},
})
const prev = Global.Path.config
;(Global.Path as { config: string }).config = tmp.path
await clear(true)
try {
await saveGlobal({ shell: "" })
const writtenConfig = await Filesystem.readJson<{ shell?: string }>(path.join(tmp.path, "kilo.json"))
expect("shell" in writtenConfig).toBe(false)
} finally {
;(Global.Path as { config: string }).config = prev
await clear(true)
}
})
test("updates global config and omits empty shell key in jsonc", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Filesystem.write(
path.join(dir, "opencode.jsonc"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
shell: "bash",
model: "test/model",
}),
)
},
})
const prev = Global.Path.config
;(Global.Path as { config: string }).config = tmp.path
await clear(true)
try {
await saveGlobal({ shell: "" })
const file = path.join(tmp.path, "opencode.jsonc")
const writtenConfig = await Filesystem.readText(file)
const parsed = ConfigParse.schema(Config.Info.zod, ConfigParse.jsonc(writtenConfig, file), file)
expect(writtenConfig).not.toContain('"shell"')
expect(parsed.shell).toBeUndefined()
expect(parsed.model).toBe("test/model")
} finally {
;(Global.Path as { config: string }).config = prev
await clear(true)
}
})
test("loads formatter boolean config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
@@ -679,6 +780,33 @@ Test agent prompt`,
})
})
test("agent markdown permission config preserves user key order", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const agentDir = path.join(dir, ".opencode", "agent")
await fs.mkdir(agentDir, { recursive: true })
await Filesystem.write(
path.join(agentDir, "ordered.md"),
`---
permission:
bash: allow
"*": deny
edit: ask
---
Ordered permissions`,
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await load()
expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"])
},
})
})
test("loads agents from .kilo/agents (plural)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
@@ -912,7 +1040,6 @@ test("installs dependencies in writable KILO_CONFIG_DIR", async () => {
const noopNpm = Layer.mock(Npm.Service)({
install: () => Effect.void,
add: () => Effect.die("not implemented"),
outdated: () => Effect.succeed(false),
which: () => Effect.succeed(Option.none()),
})
const testLayer = Config.layer.pipe(
@@ -948,7 +1075,7 @@ test("installs dependencies in writable KILO_CONFIG_DIR", async () => {
})
// Note: deduplication and serialization of npm installs is now handled by the
// shared Npm.Service (via EffectFlock). Those behaviors are tested in the shared
// core Npm.Service (via EffectFlock). Those behaviors are tested in the core
// package's npm tests, not here.
test("resolves scoped npm plugins in config", async () => {
@@ -1548,7 +1675,7 @@ test("merges legacy tools with existing permission config", async () => {
})
})
test("permission config canonicalises known keys first, preserves rest-key insertion order", async () => {
test("permission config preserves user key order", async () => {
// ConfigPermission.Info is a StructWithRest schema — the decoder reorders
// keys into declaration-order for known permission names (edit, read,
// todowrite, external_directory are declared in `config/permission.ts`),
@@ -1594,15 +1721,12 @@ test("permission config canonicalises known keys first, preserves rest-key inser
fn: async () => {
const config = await load()
expect(Object.keys(config.permission!)).toEqual([
// known fields that the user provided, in declaration order from
// config/permission.ts (read, edit, ..., external_directory, todowrite)
"read",
"edit",
"external_directory",
"todowrite",
// rest keys (not in the known list), in user's insertion order
"*",
"edit",
"write",
"external_directory",
"read",
"todowrite",
"thoughts_*",
"reasoning_model_*",
"tools_*",
@@ -1618,6 +1742,29 @@ test("permission config canonicalises known keys first, preserves rest-key inser
// kilocode_change end
})
test("Effect config parser preserves permission order while rejecting unknown top-level keys", () => {
const config = ConfigParse.effectSchema(
Config.Info,
{
permission: {
bash: "allow",
"*": "deny",
edit: "ask",
},
},
"test",
)
expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"])
try {
ConfigParse.effectSchema(Config.Info, { invalid_field: true }, "test")
throw new Error("expected config parse to fail")
} catch (err) {
const error = err as { data?: { issues?: Array<{ code?: string; keys?: string[]; path?: string[] }> } }
expect(error.data?.issues?.[0]).toMatchObject({ code: "unrecognized_keys", keys: ["invalid_field"], path: [] })
}
})
// MCP config merging tests
test("project config can override MCP server enabled status", async () => {
@@ -2305,8 +2452,8 @@ describe("KILO_CONFIG_CONTENT token substitution", () => {
// parseManagedPlist unit tests — pure function, no OS interaction
test("parseManagedPlist strips MDM metadata keys", async () => {
const config = ConfigParse.schema(
Config.Info.zod,
const config = ConfigParse.effectSchema(
Config.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@@ -2333,8 +2480,8 @@ test("parseManagedPlist strips MDM metadata keys", async () => {
})
test("parseManagedPlist parses server settings", async () => {
const config = ConfigParse.schema(
Config.Info.zod,
const config = ConfigParse.effectSchema(
Config.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@@ -2353,8 +2500,8 @@ test("parseManagedPlist parses server settings", async () => {
})
test("parseManagedPlist parses permission rules", async () => {
const config = ConfigParse.schema(
Config.Info.zod,
const config = ConfigParse.effectSchema(
Config.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@@ -2383,8 +2530,8 @@ test("parseManagedPlist parses permission rules", async () => {
})
test("parseManagedPlist parses enabled_providers", async () => {
const config = ConfigParse.schema(
Config.Info.zod,
const config = ConfigParse.effectSchema(
Config.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@@ -2400,8 +2547,8 @@ test("parseManagedPlist parses enabled_providers", async () => {
})
test("parseManagedPlist handles empty config", async () => {
const config = ConfigParse.schema(
Config.Info.zod,
const config = ConfigParse.effectSchema(
Config.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://opencode.ai/config.json" })),
"test:mobileconfig",
@@ -1,5 +1,5 @@
import { expect, test, describe } from "bun:test"
import { ConfigMarkdown } from "../../src/config"
import { ConfigMarkdown } from "@/config/markdown"
describe("ConfigMarkdown: normal template", () => {
const template = `This is a @valid/path/to/a/file and it should also match at
+3 -3
View File
@@ -4,9 +4,9 @@ import fs from "fs/promises"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { TuiConfig } from "../../src/cli/cmd/tui/config/tui"
import { Config } from "../../src/config"
import { Global } from "../../src/global"
import { Filesystem } from "../../src/util"
import { Config } from "@/config/config"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem"
import { AppRuntime } from "../../src/effect/app-runtime"
import { Effect, Layer } from "effect"
import { CurrentWorkingDirectory } from "@/cli/cmd/tui/config/cwd"
@@ -1,9 +1,9 @@
import { expect, test } from "bun:test"
import { Context, Effect, Layer, Logger } from "effect"
import { AppRuntime } from "../../src/effect/app-runtime"
import { EffectBridge } from "../../src/effect"
import { EffectBridge } from "@/effect/bridge"
import { InstanceRef } from "../../src/effect/instance-ref"
import { EffectLogger } from "../../src/effect"
import * as EffectLogger from "@opencode-ai/core/effect/logger"
import { makeRuntime } from "../../src/effect/run-service"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
@@ -1,413 +0,0 @@
import { describe, expect } from "bun:test"
import fs from "node:fs/promises"
import path from "node:path"
import { Effect, Exit, Stream } from "effect"
import type * as PlatformError from "effect/PlatformError"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { tmpdir } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const live = CrossSpawnSpawner.defaultLayer
const fx = testEffect(live)
function js(code: string, opts?: ChildProcess.CommandOptions) {
return ChildProcess.make("node", ["-e", code], opts)
}
function decodeByteStream(stream: Stream.Stream<Uint8Array, PlatformError.PlatformError>) {
return Stream.runCollect(stream).pipe(
Effect.map((chunks) => {
const total = chunks.reduce((acc, x) => acc + x.length, 0)
const out = new Uint8Array(total)
let off = 0
for (const chunk of chunks) {
out.set(chunk, off)
off += chunk.length
}
return new TextDecoder("utf-8").decode(out).trim()
}),
)
}
function alive(pid: number) {
try {
process.kill(pid, 0)
return true
} catch {
return false
}
}
async function gone(pid: number, timeout = 5_000) {
const end = Date.now() + timeout
while (Date.now() < end) {
if (!alive(pid)) return true
await Bun.sleep(50)
}
return !alive(pid)
}
describe("cross-spawn spawner", () => {
describe("basic spawning", () => {
fx.effect(
"captures stdout",
Effect.gen(function* () {
const out = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) =>
svc.string(ChildProcess.make(process.execPath, ["-e", 'process.stdout.write("ok")'])),
)
expect(out).toBe("ok")
}),
)
fx.effect(
"captures multiple lines",
Effect.gen(function* () {
const handle = yield* js('console.log("line1"); console.log("line2"); console.log("line3")')
const out = yield* decodeByteStream(handle.stdout)
expect(out).toBe("line1\nline2\nline3")
}),
)
fx.effect(
"returns exit code",
Effect.gen(function* () {
const handle = yield* js("process.exit(0)")
const code = yield* handle.exitCode
expect(code).toBe(ChildProcessSpawner.ExitCode(0))
}),
)
fx.effect(
"returns non-zero exit code",
Effect.gen(function* () {
const handle = yield* js("process.exit(42)")
const code = yield* handle.exitCode
expect(code).toBe(ChildProcessSpawner.ExitCode(42))
}),
)
})
describe("cwd option", () => {
fx.effect(
"uses cwd when spawning commands",
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
const out = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) =>
svc.string(
ChildProcess.make(process.execPath, ["-e", "process.stdout.write(process.cwd())"], { cwd: tmp.path }),
),
)
expect(out).toBe(tmp.path)
}),
)
fx.effect(
"fails for invalid cwd",
Effect.gen(function* () {
const exit = yield* Effect.exit(
ChildProcess.make("echo", ["test"], { cwd: "/nonexistent/directory/path" }).asEffect(),
)
expect(Exit.isFailure(exit)).toBe(true)
}),
)
})
describe("env option", () => {
fx.effect(
"passes environment variables with extendEnv",
Effect.gen(function* () {
const handle = yield* js('process.stdout.write(process.env.TEST_VAR ?? "")', {
env: { TEST_VAR: "test_value" },
extendEnv: true,
})
const out = yield* decodeByteStream(handle.stdout)
expect(out).toBe("test_value")
}),
)
fx.effect(
"passes multiple environment variables",
Effect.gen(function* () {
const handle = yield* js(
"process.stdout.write(`${process.env.VAR1}-${process.env.VAR2}-${process.env.VAR3}`)",
{
env: { VAR1: "one", VAR2: "two", VAR3: "three" },
extendEnv: true,
},
)
const out = yield* decodeByteStream(handle.stdout)
expect(out).toBe("one-two-three")
}),
)
})
describe("stderr", () => {
fx.effect(
"captures stderr output",
Effect.gen(function* () {
const handle = yield* js('process.stderr.write("error message")')
const err = yield* decodeByteStream(handle.stderr)
expect(err).toBe("error message")
}),
)
fx.effect(
"captures both stdout and stderr",
Effect.gen(function* () {
const handle = yield* js(
[
"let pending = 2",
"const done = () => {",
" pending -= 1",
" if (pending === 0) setTimeout(() => process.exit(0), 0)",
"}",
'process.stdout.write("stdout\\n", done)',
'process.stderr.write("stderr\\n", done)',
].join("\n"),
)
const [stdout, stderr] = yield* Effect.all([decodeByteStream(handle.stdout), decodeByteStream(handle.stderr)], {
concurrency: 2,
})
expect(stdout).toBe("stdout")
expect(stderr).toBe("stderr")
}),
)
})
describe("combined output (all)", () => {
fx.effect(
"captures stdout via .all when no stderr",
Effect.gen(function* () {
const handle = yield* ChildProcess.make("echo", ["hello from stdout"])
const all = yield* decodeByteStream(handle.all)
expect(all).toBe("hello from stdout")
}),
)
fx.effect(
"captures stderr via .all when no stdout",
Effect.gen(function* () {
const handle = yield* js('process.stderr.write("hello from stderr")')
const all = yield* decodeByteStream(handle.all)
expect(all).toBe("hello from stderr")
}),
)
})
describe("stdin", () => {
fx.effect(
"allows providing standard input to a command",
Effect.gen(function* () {
const input = "a b c"
const stdin = Stream.make(Buffer.from(input, "utf-8"))
const handle = yield* js(
'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out))',
{ stdin },
)
const out = yield* decodeByteStream(handle.stdout)
yield* handle.exitCode
expect(out).toBe("a b c")
}),
)
})
describe("process control", () => {
fx.effect(
"kills a running process",
Effect.gen(function* () {
const exit = yield* Effect.exit(
Effect.gen(function* () {
const handle = yield* js("setTimeout(() => {}, 10_000)")
yield* handle.kill()
return yield* handle.exitCode
}),
)
expect(Exit.isFailure(exit) ? true : exit.value !== ChildProcessSpawner.ExitCode(0)).toBe(true)
}),
)
fx.effect(
"kills a child when scope exits",
Effect.gen(function* () {
const pid = yield* Effect.scoped(
Effect.gen(function* () {
const handle = yield* js("setInterval(() => {}, 10_000)")
return Number(handle.pid)
}),
)
const done = yield* Effect.promise(() => gone(pid))
expect(done).toBe(true)
}),
)
fx.effect(
"forceKillAfter escalates for stubborn processes",
Effect.gen(function* () {
if (process.platform === "win32") return
const started = Date.now()
const exit = yield* Effect.exit(
Effect.gen(function* () {
const handle = yield* js('process.on("SIGTERM", () => {}); setInterval(() => {}, 10_000)')
yield* handle.kill({ forceKillAfter: 100 })
return yield* handle.exitCode
}),
)
expect(Date.now() - started).toBeLessThan(1_000)
expect(Exit.isFailure(exit) ? true : exit.value !== ChildProcessSpawner.ExitCode(0)).toBe(true)
}),
)
fx.effect(
"isRunning reflects process state",
Effect.gen(function* () {
const handle = yield* js('process.stdout.write("done")')
yield* handle.exitCode
const running = yield* handle.isRunning
expect(running).toBe(false)
}),
)
})
describe("error handling", () => {
fx.effect(
"fails for invalid command",
Effect.gen(function* () {
const exit = yield* Effect.exit(
Effect.gen(function* () {
const handle = yield* ChildProcess.make("nonexistent-command-12345")
return yield* handle.exitCode
}),
)
expect(Exit.isFailure(exit) ? true : exit.value !== ChildProcessSpawner.ExitCode(0)).toBe(true)
}),
)
})
describe("pipeline", () => {
fx.effect(
"pipes stdout of one command to stdin of another",
Effect.gen(function* () {
const handle = yield* js('process.stdout.write("hello world")').pipe(
ChildProcess.pipeTo(
js(
'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out.toUpperCase()))',
),
),
)
const out = yield* decodeByteStream(handle.stdout)
yield* handle.exitCode
expect(out).toBe("HELLO WORLD")
}),
)
fx.effect(
"three-stage pipeline",
Effect.gen(function* () {
const handle = yield* js('process.stdout.write("hello world")').pipe(
ChildProcess.pipeTo(
js(
'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out.toUpperCase()))',
),
),
ChildProcess.pipeTo(
js(
'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out.replaceAll(" ", "-")))',
),
),
)
const out = yield* decodeByteStream(handle.stdout)
yield* handle.exitCode
expect(out).toBe("HELLO-WORLD")
}),
)
fx.effect(
"pipes stderr with { from: 'stderr' }",
Effect.gen(function* () {
const handle = yield* js('process.stderr.write("error")').pipe(
ChildProcess.pipeTo(
js(
'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out))',
),
{ from: "stderr" },
),
)
const out = yield* decodeByteStream(handle.stdout)
yield* handle.exitCode
expect(out).toBe("error")
}),
)
fx.effect(
"pipes combined output with { from: 'all' }",
Effect.gen(function* () {
const handle = yield* js('process.stdout.write("stdout\\n"); process.stderr.write("stderr\\n")').pipe(
ChildProcess.pipeTo(
js(
'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out))',
),
{ from: "all" },
),
)
const out = yield* decodeByteStream(handle.stdout)
yield* handle.exitCode
expect(out).toContain("stdout")
expect(out).toContain("stderr")
}),
)
})
describe("Windows-specific", () => {
fx.effect(
"uses shell routing on Windows",
Effect.gen(function* () {
if (process.platform !== "win32") return
const out = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) =>
svc.string(
ChildProcess.make("set", ["KILO_TEST_SHELL"], {
shell: true,
extendEnv: true,
env: { KILO_TEST_SHELL: "ok" },
}),
),
)
expect(out).toContain("KILO_TEST_SHELL=ok")
}),
)
fx.effect(
"runs cmd scripts with spaces on Windows without shell",
Effect.gen(function* () {
if (process.platform !== "win32") return
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
const dir = path.join(tmp.path, "with space")
const file = path.join(dir, "echo cmd.cmd")
yield* Effect.promise(() => fs.mkdir(dir, { recursive: true }))
yield* Effect.promise(() => Bun.write(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n"))
const code = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) =>
svc.exitCode(
ChildProcess.make(file, ["--stdio"], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
}),
),
)
expect(code).toBe(ChildProcessSpawner.ExitCode(0))
}),
)
})
})
@@ -1,6 +1,6 @@
import { afterEach, expect, test } from "bun:test"
import { Deferred, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Context } from "effect"
import { InstanceState } from "../../src/effect"
import { InstanceState } from "@/effect/instance-state"
import { InstanceRef } from "../../src/effect/instance-ref"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
@@ -1,46 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test"
import { resource } from "../../src/effect/observability"
const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES
const opencodeClient = process.env.KILO_CLIENT
afterEach(() => {
if (otelResourceAttributes === undefined) delete process.env.OTEL_RESOURCE_ATTRIBUTES
else process.env.OTEL_RESOURCE_ATTRIBUTES = otelResourceAttributes
if (opencodeClient === undefined) delete process.env.KILO_CLIENT
else process.env.KILO_CLIENT = opencodeClient
})
describe("resource", () => {
test("parses and decodes OTEL resource attributes", () => {
process.env.OTEL_RESOURCE_ATTRIBUTES =
"service.namespace=anomalyco,team=platform%2Cobservability,label=hello%3Dworld,key%2Fname=value%20here"
expect(resource().attributes).toMatchObject({
"service.namespace": "anomalyco",
team: "platform,observability",
label: "hello=world",
"key/name": "value here",
})
})
test("drops OTEL resource attributes when any entry is invalid", () => {
process.env.OTEL_RESOURCE_ATTRIBUTES = "service.namespace=anomalyco,broken"
expect(resource().attributes["service.namespace"]).toBeUndefined()
expect(resource().attributes["opencode.client"]).toBeDefined()
})
test("keeps built-in attributes when env values conflict", () => {
process.env.KILO_CLIENT = "cli"
process.env.OTEL_RESOURCE_ATTRIBUTES =
"opencode.client=web,service.instance.id=override,service.namespace=anomalyco"
expect(resource().attributes).toMatchObject({
"opencode.client": "cli",
"service.namespace": "anomalyco",
})
expect(resource().attributes["service.instance.id"]).not.toBe("override")
})
})
+17 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Deferred, Effect, Exit, Fiber, Ref, Scope } from "effect"
import { Runner } from "../../src/effect"
import { Runner } from "@/effect/runner"
import { it } from "../lib/effect"
describe("Runner", () => {
@@ -334,6 +334,22 @@ describe("Runner", () => {
}),
)
it.live(
"cancel does not mask shell defects",
Effect.gen(function* () {
const s = yield* Scope.Scope
const runner = Runner.make<string>(s, { onInterrupt: Effect.succeed("interrupted") })
const sh = yield* runner
.startShell(Effect.never.pipe(Effect.ensuring(Effect.die("boom")), Effect.as("ignored")))
.pipe(Effect.forkChild)
yield* Effect.sleep("10 millis")
yield* runner.cancel
expect(Exit.isFailure(yield* Fiber.await(sh))).toBe(true)
}),
)
// --- shell→run handoff ---
it.live(
+1 -1
View File
@@ -1,5 +1,5 @@
import { Effect, Layer } from "effect"
import { Provider } from "../../src/provider"
import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
export namespace ProviderTest {
+1 -1
View File
@@ -5,7 +5,7 @@ import path from "path"
import fs from "fs/promises"
import { File } from "../../src/file"
import { Instance } from "../../src/project/instance"
import { Filesystem } from "../../src/util"
import { Filesystem } from "@/util/filesystem"
import { provideInstance, tmpdir } from "../fixture/fixture"
afterEach(async () => {
@@ -2,7 +2,7 @@ import { test, expect, describe } from "bun:test"
import { Effect } from "effect"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "../../src/util"
import { Filesystem } from "@/util/filesystem"
import { File } from "../../src/file"
import { Instance } from "../../src/project/instance"
import { provideInstance, tmpdir } from "../fixture/fixture"
+1 -1
View File
@@ -5,7 +5,7 @@ import path from "path"
import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
import { tmpdir } from "../fixture/fixture"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config"
import { Config } from "@/config/config"
import { FileWatcher } from "../../src/file/watcher"
import { Git } from "../../src/git"
import { Instance } from "../../src/project/instance"
@@ -1,7 +1,7 @@
import { describe, test, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { testEffect } from "../lib/effect"
import path from "path"
+1 -1
View File
@@ -1,6 +1,6 @@
import { rm } from "fs/promises"
import { Instance } from "../../src/project/instance"
import { Database } from "../../src/storage"
import { Database } from "@/storage/db"
export async function resetDatabase() {
await Instance.disposeAll().catch(() => undefined)
+1 -1
View File
@@ -6,7 +6,7 @@ import { Effect, Context } from "effect"
import type * as PlatformError from "effect/PlatformError"
import type * as Scope from "effect/Scope"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import type { Config } from "../../src/config"
import type { Config } from "@/config/config"
import { InstanceRef } from "../../src/effect/instance-ref"
import { Instance } from "../../src/project/instance"
import { TestLLMServer } from "../lib/llm-server"
@@ -1,5 +1,5 @@
import fs from "fs/promises"
import { Flock } from "@opencode-ai/shared/util/flock"
import { Flock } from "@opencode-ai/core/util/flock"
type Msg = {
key: string
@@ -16,7 +16,7 @@ process.env.XDG_CONFIG_HOME = path.join(dir, "config")
process.env.XDG_STATE_HOME = path.join(dir, "state")
process.env.KILO_TEST_HOME = path.join(dir, "home")
const Log = await import("../../src/util/log")
const Log = await import("@opencode-ai/core/util/log")
async function bytes(file: string) {
return fs
@@ -1,7 +1,7 @@
import path from "path"
import { createPlugTask, type PlugCtx, type PlugDeps } from "../../src/cli/cmd/plug"
import { Filesystem } from "../../src/util"
import { Filesystem } from "@/util/filesystem"
type Msg = {
dir: string
+1 -1
View File
@@ -3,7 +3,7 @@ import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Format } from "../../src/format"
import * as Formatter from "../../src/format/formatter"
@@ -3,7 +3,7 @@ import { Effect, Layer, Stream } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { Installation } from "../../src/installation"
import { InstallationChannel } from "../../src/installation/version"
import { InstallationChannel } from "@opencode-ai/core/installation/version"
const encoder = new TextEncoder()
@@ -69,64 +69,46 @@ describe("installation", () => {
expect(result).toBe("4.0.0-beta.1")
})
test("reads npm versions via npm view", async () => {
const calls: string[][] = []
const layer = testLayer(
() => {
throw new Error("unexpected http request")
},
(cmd, args) => {
calls.push([cmd, ...args])
if (cmd === "npm" && args[0] === "view") return '"1.5.0"\n'
return ""
},
)
test("reads npm versions via registry", async () => {
const calls: string[] = []
const layer = testLayer((request) => {
calls.push(request.url)
return jsonResponse({ version: "1.5.0" })
})
const result = await Effect.runPromise(
Installation.Service.use((svc) => svc.latest("npm")).pipe(Effect.provide(layer)),
)
expect(result).toBe("1.5.0")
expect(calls).toContainEqual(["npm", "view", `@kilocode/cli@${InstallationChannel}`, "version", "--json"]) // kilocode_change
expect(calls).toContain(`https://registry.npmjs.org/@kilocode%2Fcli/${InstallationChannel}`) // kilocode_change
})
test("reads npm versions via bun pm view", async () => {
const calls: string[][] = []
const layer = testLayer(
() => {
throw new Error("unexpected http request")
},
(cmd, args) => {
calls.push([cmd, ...args])
if (cmd === "bun" && args[0] === "pm") return '"1.6.0"\n'
return ""
},
)
test("reads bun versions via registry", async () => {
const calls: string[] = []
const layer = testLayer((request) => {
calls.push(request.url)
return jsonResponse({ version: "1.6.0" })
})
const result = await Effect.runPromise(
Installation.Service.use((svc) => svc.latest("bun")).pipe(Effect.provide(layer)),
)
expect(result).toBe("1.6.0")
expect(calls).toContainEqual(["bun", "pm", "view", `@kilocode/cli@${InstallationChannel}`, "version", "--json"]) // kilocode_change
expect(calls).toContain(`https://registry.npmjs.org/@kilocode%2Fcli/${InstallationChannel}`) // kilocode_change
})
test("reads npm versions via pnpm view", async () => {
const calls: string[][] = []
const layer = testLayer(
() => {
throw new Error("unexpected http request")
},
(cmd, args) => {
calls.push([cmd, ...args])
if (cmd === "pnpm" && args[0] === "view") return '"1.7.0"\n'
return ""
},
)
test("reads pnpm versions via registry", async () => {
const calls: string[] = []
const layer = testLayer((request) => {
calls.push(request.url)
return jsonResponse({ version: "1.7.0" })
})
const result = await Effect.runPromise(
Installation.Service.use((svc) => svc.latest("pnpm")).pipe(Effect.provide(layer)),
)
expect(result).toBe("1.7.0")
expect(calls).toContainEqual(["pnpm", "view", `@kilocode/cli@${InstallationChannel}`, "version", "--json"]) // kilocode_change
expect(calls).toContain(`https://registry.npmjs.org/@kilocode%2Fcli/${InstallationChannel}`) // kilocode_change
})
test("reads scoop manifest versions", async () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, test, expect } from "bun:test"
import { Keybind } from "../src/util"
import { Keybind } from "@/util/keybind"
describe("Keybind.toString", () => {
test("should convert simple key to string", () => {
@@ -4,7 +4,7 @@ import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Agent } from "../../src/agent/agent"
import { Permission } from "../../src/permission"
import { Global } from "../../src/global"
import { Global } from "@opencode-ai/core/global"
afterEach(async () => {
await Instance.disposeAll()
@@ -2,11 +2,11 @@ import { describe, expect, test } from "bun:test"
import { Effect, Layer, ManagedRuntime } from "effect"
import { MessageID, SessionID } from "../../src/session/schema"
import { provideTmpdirInstance } from "../fixture/fixture"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AgentManagerTool } from "../../src/kilocode/tool/agent-manager"
import { Bus } from "../../src/bus"
import { Tool } from "../../src/tool"
import { Truncate } from "../../src/tool"
import { Tool } from "../../src/tool/tool"
import { Truncate } from "../../src/tool/truncate"
import { Agent } from "../../src/agent/agent"
const runtime = ManagedRuntime.make(
@@ -8,10 +8,11 @@ import { Shell } from "../../src/shell/shell"
import { SessionID, MessageID } from "../../src/session/schema"
import type { Permission } from "../../src/permission"
import { Agent } from "../../src/agent/agent"
import { Truncate } from "../../src/tool"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Truncate } from "../../src/tool/truncate"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Plugin } from "../../src/plugin"
import { Config } from "../../src/config/config"
const runtime = ManagedRuntime.make(
Layer.mergeAll(
@@ -20,6 +21,7 @@ const runtime = ManagedRuntime.make(
Plugin.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
Config.defaultLayer,
),
)
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { ProviderTransform } from "../../src/provider"
import { ProviderTransform } from "../../src/provider/transform"
describe("ProviderTransform.message - bedrock claude empty content filtering", () => {
const model = {
+6 -8
View File
@@ -17,14 +17,12 @@ export async function remove(dir: string) {
const cfg = opts()
const rm = async (left: number): Promise<void> => {
if (process.platform === "win32") Bun.gc(true)
return fs
.rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
.catch(async (error) => {
if (!locked(error)) throw error
if (left <= 1) throw error
await Bun.sleep(cfg.delay)
return rm(left - 1)
})
return fs.rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(async (error) => {
if (!locked(error)) throw error
if (left <= 1) throw error
await Bun.sleep(cfg.delay)
return rm(left - 1)
})
}
return rm(cfg.retries)
}
@@ -6,8 +6,8 @@ import type { GitContext } from "@/kilocode/commit-message/types"
// breaking other test files, we spread real exports and only override what
// this test needs.
const realLog = await import("@/util")
const realProvider = await import("@/provider")
const realLog = await import("@opencode-ai/core/util/log")
const realProvider = await import("@/provider/provider")
const realLLM = await import("@/session/llm")
const realAgent = await import("@/agent/agent")
const realGitContext = await import("@/kilocode/commit-message/git-context")
@@ -37,7 +37,7 @@ mock.module("@/kilocode/commit-message/git-context", () => ({
},
}))
mock.module("@/provider", () => ({
mock.module("@/provider/provider", () => ({
...realProvider,
Provider: {
...realProvider.Provider,
@@ -68,17 +68,14 @@ mock.module("@/agent/agent", () => ({
Agent: {},
}))
mock.module("@/util", () => ({
mock.module("@opencode-ai/core/util/log", () => ({
...realLog,
Log: {
...realLog.Log,
create: () => ({
info: () => {},
error: () => {},
warn: () => {},
debug: () => {},
}),
},
create: () => ({
info: () => {},
error: () => {},
warn: () => {},
debug: () => {},
}),
}))
import { generateCommitMessage } from "../../../src/kilocode/commit-message/generate"
@@ -11,16 +11,16 @@ import fs from "fs/promises"
import path from "path"
import { Effect, Layer, Option } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "../../src/config"
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
import { Npm } from "../../src/npm"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Config } from "../../src/config/config"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Npm } from "@opencode-ai/core/npm"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Env } from "../../src/env"
import { Auth } from "../../src/auth"
import { Account } from "../../src/account/account"
import { Instance } from "../../src/project/instance"
import { Filesystem } from "../../src/util"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Filesystem } from "../../src/util/filesystem"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { tmpdir } from "../fixture/fixture"
const infra = CrossSpawnSpawner.defaultLayer.pipe(
@@ -39,7 +39,6 @@ const emptyAuth = Layer.mock(Auth.Service)({
const noopNpm = Layer.mock(Npm.Service)({
install: () => Effect.void,
add: () => Effect.die("not implemented"),
outdated: () => Effect.succeed(false),
which: () => Effect.succeed(Option.none()),
})
@@ -1,8 +1,8 @@
import { afterEach, describe, expect, test } from "bun:test"
import path from "path"
import { Config } from "../../src/config"
import { Config } from "../../src/config/config"
import { Instance } from "../../src/project/instance"
import { Filesystem } from "../../src/util"
import { Filesystem } from "../../src/util/filesystem"
import { tmpdir } from "../fixture/fixture"
afterEach(async () => {
@@ -3,8 +3,8 @@ import { afterEach, describe, expect, test } from "bun:test"
import path from "path"
import { ConfigValidation } from "../../src/kilocode/config-validation"
import { Instance } from "../../src/project/instance"
import { Config } from "../../src/config"
import { Filesystem } from "../../src/util"
import { Config } from "../../src/config/config"
import { Filesystem } from "../../src/util/filesystem"
import { tmpdir } from "../fixture/fixture"
afterEach(async () => {
@@ -5,14 +5,14 @@ import path from "path"
import { hasIndexingPlugin } from "@kilocode/kilo-indexing/detect"
import { Account } from "../../../src/account/account"
import { Auth } from "../../../src/auth"
import { Config } from "../../../src/config"
import * as CrossSpawnSpawner from "../../../src/effect/cross-spawn-spawner"
import { Config } from "../../../src/config/config"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { Env } from "../../../src/env"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
import { Filesystem } from "../../../src/util"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Filesystem } from "../../../src/util/filesystem"
import { Instance } from "../../../src/project/instance"
import { Npm } from "../../../src/npm"
import { Npm } from "@opencode-ai/core/npm"
import { tmpdir } from "../../fixture/fixture"
const infra = CrossSpawnSpawner.defaultLayer.pipe(
@@ -28,7 +28,6 @@ const emptyAuth = Layer.mock(Auth.Service)({
const noopNpm = Layer.mock(Npm.Service)({
install: () => Effect.void,
add: () => Effect.die("not implemented"),
outdated: () => Effect.succeed(false),
which: () => Effect.succeed(Option.none()),
})
const layer = Config.layer.pipe(
@@ -1,6 +1,6 @@
// kilocode_change - new file
import { describe, expect, test } from "bun:test"
import { Config } from "../../../src/config"
import { Config } from "../../../src/config/config"
describe("Config.Info experimental.openTelemetry default", () => {
test("defaults to true when experimental is set without openTelemetry", () => {
@@ -5,14 +5,14 @@
import { afterEach, describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Bus } from "../../src/bus"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { KiloCostPropagation } from "../../src/kilocode/session/cost-propagation"
import { Instance } from "../../src/project/instance"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { MessageV2 } from "../../src/session/message-v2"
import { MessageID } from "../../src/session/schema"
import { Log } from "../../src/util"
import * as Log from "@opencode-ai/core/util/log"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -6,9 +6,10 @@ import { $ } from "bun"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import path from "path"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { DiffFull } from "../../src/kilocode/snapshot/diff-full"
import { Filesystem, Log } from "../../src/util"
import { Filesystem } from "../../src/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -10,12 +10,12 @@ import { Effect, Layer, ManagedRuntime } from "effect"
import { EditTool } from "../../src/tool/edit"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import { LSP } from "../../src/lsp"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { LSP } from "../../src/lsp/lsp"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Format } from "../../src/format"
import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { Truncate } from "../../src/tool"
import { Truncate } from "../../src/tool/truncate"
import { SessionID, MessageID } from "../../src/session/schema"
const runtime = ManagedRuntime.make(
@@ -5,9 +5,9 @@ import type { Permission } from "../../src/permission"
import { Instance } from "../../src/project/instance"
import { SessionID, MessageID } from "../../src/session/schema"
import { assertExternalDirectory } from "../../src/tool/external-directory"
import type { Tool } from "../../src/tool"
import { Filesystem } from "../../src/util"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import type { Tool } from "../../src/tool/tool"
import { Filesystem } from "../../src/util/filesystem"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { tmpdir } from "../fixture/fixture"
const base: Omit<Tool.Context, "ask"> = {
@@ -1,13 +1,13 @@
import { afterEach, describe, expect, spyOn, test } from "bun:test"
import { CodeIndexManager } from "@kilocode/kilo-indexing/engine"
import type { Config } from "../../src/config"
import type { Config } from "../../src/config/config"
import { GlobalBus } from "../../src/bus/global"
import { AppRuntime } from "../../src/effect/app-runtime"
import { KiloIndexing } from "../../src/kilocode/indexing"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { Instance } from "../../src/project/instance"
import { Server } from "../../src/server/server"
import { Log } from "../../src/util"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
@@ -144,7 +144,10 @@ describe("indexing startup degradation", () => {
const gate = Promise.withResolvers<{ requiresRestart: boolean }>()
const init = spyOn(CodeIndexManager.prototype, "initialize").mockImplementation(() => gate.promise)
const events: KiloIndexing.Status[] = []
const on = (data: { directory?: string; payload?: { type?: string; properties?: { status?: KiloIndexing.Status } } }) => {
const on = (data: {
directory?: string
payload?: { type?: string; properties?: { status?: KiloIndexing.Status } }
}) => {
if (data.directory !== tmp.path) return
if (data.payload?.type !== KiloIndexing.Event.type) return
if (data.payload.properties?.status) events.push(data.payload.properties.status)
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, test } from "bun:test"
import { mkdir } from "node:fs/promises"
import type { Config } from "../../src/config"
import type { Config } from "../../src/config/config"
import { AppRuntime } from "../../src/effect/app-runtime"
import { KiloIndexing } from "../../src/kilocode/indexing"
import { InstanceBootstrap } from "../../src/project/bootstrap"
@@ -2,7 +2,7 @@ import { describe, it, expect } from "bun:test"
import { MessageV2 } from "../../src/session/message-v2"
import { KILO_ERROR_CODES, isKiloError, parseKiloErrorCode } from "../../src/kilocode/kilo-errors"
import { SessionRetry } from "../../src/session/retry"
import { NamedError } from "@opencode-ai/shared/util/error"
import { NamedError } from "@opencode-ai/core/util/error"
/**
* Helper to create a mock APIError object (as returned by .toObject())
@@ -33,13 +33,13 @@ mock.module("@kilocode/kilo-gateway", () => ({
}))
import { tmpdir } from "../fixture/fixture"
import { Global } from "../../src/global"
import { Global } from "@opencode-ai/core/global"
import { Instance } from "../../src/project/instance"
import { Provider } from "../../src/provider"
import { Provider } from "../../src/provider/provider"
import { ProviderID } from "../../src/provider/schema"
import { Filesystem } from "../../src/util"
import { Filesystem } from "../../src/util/filesystem"
import { ModelCache } from "../../src/provider/model-cache"
import { ModelsDev } from "../../src/provider"
import { ModelsDev } from "../../src/provider/models"
import { Auth } from "../../src/auth"
function paid(providers: Awaited<ReturnType<typeof Provider.list>>) {
@@ -2,9 +2,9 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
const entry = "file:///tmp/kilo-cache/node_modules/@lancedb/lancedb/dist/index.js"
const add = mock(async () => ({ directory: "/tmp/kilo-cache", entrypoint: entry }))
const real = await import("../../src/npm/index")
const real = await import("@opencode-ai/core/npm")
mock.module("../../src/npm/index", () => ({
mock.module("@opencode-ai/core/npm", () => ({
...real,
Npm: {
...real.Npm,
@@ -157,7 +157,7 @@ mock.module("@tui/ui/toast", () => ({
await import("@tui/context/local")
// Import the real Global to get the state path (set by test preload via XDG_STATE_HOME)
const { Global } = await import("@/global")
const { Global } = await import("@opencode-ai/core/global")
const modelJsonPath = path.join(Global.Path.state, "model.json")
// ── Helpers ─────────────────────────────────────────────────────────────────
@@ -4,10 +4,10 @@
import { describe, test, expect, spyOn, afterEach } from "bun:test"
import path from "path"
import { LSPServer } from "../../src/lsp"
import * as LSPServer from "../../src/lsp/server"
import { TsClient } from "../../src/kilocode/ts-client"
import { TsCheck } from "../../src/kilocode/ts-check"
import { Flag } from "../../src/flag/flag"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Instance, type InstanceContext } from "../../src/project/instance"
afterEach(async () => {
@@ -0,0 +1,33 @@
import { describe, expect, test, afterEach } from "bun:test"
import { createServer } from "http"
import { McpOAuthCallback } from "../../src/mcp/oauth-callback"
describe("Kilo MCP OAuth callback", () => {
afterEach(async () => {
await McpOAuthCallback.stop()
})
test("fails fast when the callback port belongs to another process", async () => {
const blocker = createServer((_req, res) => {
res.writeHead(200)
res.end("occupied")
})
await new Promise<void>((resolve, reject) => {
blocker.once("error", reject)
blocker.listen(0, "127.0.0.1", resolve)
})
try {
const address = blocker.address()
if (!address || typeof address === "string") throw new Error("missing blocker address")
await expect(
McpOAuthCallback.ensureRunning(`http://127.0.0.1:${address.port}/mcp/oauth/callback`),
).rejects.toThrow("already in use")
expect(McpOAuthCallback.isRunning()).toBe(false)
} finally {
await new Promise<void>((resolve) => blocker.close(() => resolve()))
}
})
})
@@ -4,7 +4,7 @@
import { test, expect, mock } from "bun:test"
import path from "path"
import { Log } from "../../src/util"
import * as Log from "@opencode-ai/core/util/log"
Log.init({ print: false })
@@ -2,7 +2,7 @@
import path from "path"
import { describe, expect, test } from "bun:test"
import { ConfigProtection } from "../../../src/kilocode/permission/config-paths"
import { Global } from "../../../src/global"
import { Global } from "@opencode-ai/core/global"
import { KilocodePaths } from "../../../src/kilocode/paths"
describe("ConfigProtection.isRequest", () => {
@@ -1,16 +1,17 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Effect, Layer, ManagedRuntime } from "effect"
import path from "path"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import * as CrossSpawnSpawner from "../../../src/effect/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Global } from "@opencode-ai/core/global"
import { Agent } from "../../../src/agent/agent"
import { Global } from "../../../src/global"
import { Config } from "../../../src/config/config"
import { Permission } from "../../../src/permission"
import { PermissionID } from "../../../src/permission/schema"
import { Instance } from "../../../src/project/instance"
import { MessageID, SessionID } from "../../../src/session/schema"
import { Shell } from "../../../src/shell/shell"
import { Truncate } from "../../../src/tool"
import { Truncate } from "../../../src/tool/truncate"
import { BashTool } from "../../../src/tool/bash"
import { Plugin } from "../../../src/plugin"
import { tmpdir } from "../../fixture/fixture"
@@ -20,6 +21,7 @@ const runtime = ManagedRuntime.make(
Layer.mergeAll(
CrossSpawnSpawner.defaultLayer,
AppFileSystem.defaultLayer,
Config.defaultLayer,
Plugin.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
@@ -7,8 +7,8 @@ import { Permission } from "../../../src/permission"
import { PermissionID } from "../../../src/permission/schema"
import { SessionID } from "../../../src/session/schema"
import * as Config from "../../../src/config/config"
import { Global } from "../../../src/global"
import * as CrossSpawnSpawner from "../../../src/effect/cross-spawn-spawner"
import { Global } from "@opencode-ai/core/global"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirInstance } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"
import { Permission } from "../../../src/permission"
import { PermissionID } from "../../../src/permission/schema"
import { Instance } from "../../../src/project/instance"
import { Session } from "../../../src/session"
import { Session } from "../../../src/session/session"
import { tmpdir } from "../../fixture/fixture"
afterEach(() => {
@@ -202,5 +202,4 @@ describe("POST /permission/:requestID/always-rules", () => {
},
})
})
})
@@ -7,8 +7,8 @@ import { Permission } from "../../../src/permission"
import { PermissionID } from "../../../src/permission/schema"
import { SessionID } from "../../../src/session/schema"
import * as Config from "../../../src/config/config"
import { Global } from "../../../src/global"
import * as CrossSpawnSpawner from "../../../src/effect/cross-spawn-spawner"
import { Global } from "@opencode-ai/core/global"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
@@ -7,10 +7,10 @@ import { ModelID, ProviderID } from "../../src/provider/schema"
import { Instance } from "../../src/project/instance"
import { PlanFollowup } from "../../src/kilocode/plan-followup"
import { Question } from "../../src/question"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionPrompt } from "../../src/session/prompt"
import { Log } from "../../src/util"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
@@ -7,16 +7,16 @@ import { SessionID, MessageID, PartID } from "../../src/session/schema"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { formatTodos, generateHandover, PlanFollowup, PlanFollowupRuntime } from "../../src/kilocode/plan-followup"
import { Instance } from "../../src/project/instance"
import { Provider } from "../../src/provider"
import { Provider } from "../../src/provider/provider"
import { Question } from "../../src/question"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
import { AppRuntime } from "../../src/effect/app-runtime"
import { SessionStatus } from "../../src/session/status"
import { Todo } from "../../src/session/todo"
import { Global } from "../../src/global"
import { Log } from "../../src/util"
import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log"
import path from "path"
import fs from "fs/promises"
import { tmpdir } from "../fixture/fixture"
@@ -5,16 +5,16 @@ import fs from "fs/promises"
import path from "path"
import { Effect, Layer, Option } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
import { Config } from "../../src/config"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Config } from "../../src/config/config"
import { Auth } from "../../src/auth"
import { Account } from "../../src/account/account"
import { Env } from "../../src/env"
import { Npm } from "../../src/npm"
import { Npm } from "@opencode-ai/core/npm"
import { Instance } from "../../src/project/instance"
import { Filesystem } from "../../src/util"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Filesystem } from "../../src/util/filesystem"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { tmpdir } from "../fixture/fixture"
const infra = CrossSpawnSpawner.defaultLayer.pipe(
@@ -33,7 +33,6 @@ const emptyAuth = Layer.mock(Auth.Service)({
const noopNpm = Layer.mock(Npm.Service)({
install: () => Effect.void,
add: () => Effect.die("not implemented"),
outdated: () => Effect.succeed(false),
which: () => Effect.succeed(Option.none()),
})
@@ -3,14 +3,14 @@ import { Effect, Layer } from "effect"
import { symlink } from "fs/promises"
import path from "path"
import { Agent } from "../../src/agent/agent"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { LSP } from "../../src/lsp"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { LSP } from "../../src/lsp/lsp"
import { Instruction } from "../../src/session/instruction"
import { Truncate } from "../../src/tool"
import { Truncate } from "../../src/tool/truncate"
import { MessageID, SessionID } from "../../src/session/schema"
import { ReadTool } from "../../src/tool/read"
import { Tool } from "../../src/tool"
import { Tool } from "../../src/tool/tool"
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -8,7 +8,8 @@ import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import type { Permission } from "../../src/permission"
import { SessionID, MessageID } from "../../src/session/schema"
import { Tool, Truncate } from "../../src/tool"
import { Tool } from "../../src/tool/tool"
import { Truncate } from "../../src/tool/truncate"
const rt = ManagedRuntime.make(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer))
@@ -4,7 +4,7 @@ import { Permission } from "../../../src/permission"
import { PermissionID } from "../../../src/permission/schema"
import { Instance } from "../../../src/project/instance"
import { Server } from "../../../src/server/server"
import { Session } from "../../../src/session"
import { Session } from "../../../src/session/session"
import { tmpdir } from "../../fixture/fixture"
describe("permission.allowEverything endpoint", () => {
@@ -10,22 +10,22 @@ import { FetchHttpClient } from "effect/unstable/http"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { Command } from "../../src/command"
import { Config } from "../../src/config"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Config } from "../../src/config/config"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { Env } from "../../src/env"
import { Ripgrep } from "../../src/file/ripgrep"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Format } from "../../src/format"
import { KiloSession } from "../../src/kilocode/session"
import { KiloSessionPrompt } from "../../src/kilocode/session/prompt"
import { LSP } from "../../src/lsp"
import { LSP } from "../../src/lsp/lsp"
import { MCP } from "../../src/mcp"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import { Provider as ProviderSvc } from "../../src/provider"
import { Provider as ProviderSvc } from "../../src/provider/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Question } from "../../src/question"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
import { LLM } from "../../src/session/llm"
@@ -41,8 +41,9 @@ import { SessionSummary } from "../../src/session/summary"
import { Todo } from "../../src/session/todo"
import { Skill } from "../../src/skill"
import { Snapshot } from "../../src/snapshot"
import { ToolRegistry, Truncate } from "../../src/tool"
import { Log } from "../../src/util"
import { ToolRegistry } from "../../src/tool/registry"
import { Truncate } from "../../src/tool/truncate"
import * as Log from "@opencode-ai/core/util/log"
import { provideTmpdirServer } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { TestLLMServer } from "../lib/llm-server"
@@ -1,9 +1,9 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Instance } from "../../src/project/instance"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { MessageV2 } from "../../src/session/message-v2"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { Log } from "../../src/util"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
import { Database } from "../../src/storage"
import { Database } from "../../src/storage/db"
import { SessionImportService } from "../../src/kilocode/session-import/service"
let spy: ReturnType<typeof spyOn>
@@ -2,10 +2,10 @@ import { afterEach, describe, expect, test } from "bun:test"
import { Instance } from "../../src/project/instance"
import { ProjectTable } from "../../src/project/project.sql"
import { ProjectID } from "../../src/project/schema"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { SessionTable } from "../../src/session/session.sql"
import { Database, eq } from "../../src/storage"
import { Log } from "../../src/util"
import { Database, eq } from "../../src/storage/db"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
@@ -5,22 +5,22 @@ import * as Stream from "effect/Stream"
import path from "path"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config"
import { Config } from "../../src/config/config"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import type { Provider } from "../../src/provider"
import type { Provider } from "../../src/provider/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionProcessor } from "../../src/session/processor"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { MessageID } from "../../src/session/schema"
import { SessionStatus } from "../../src/session/status"
import { SessionSummary } from "../../src/session/summary"
import { Snapshot } from "../../src/snapshot"
import { KiloSessionProcessor } from "../../src/kilocode/session/processor"
import { Log } from "../../src/util"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import * as Log from "@opencode-ai/core/util/log"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -5,12 +5,12 @@ import * as Stream from "effect/Stream"
import path from "path"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config"
import { Config } from "../../src/config/config"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import type { Provider } from "../../src/provider"
import type { Provider } from "../../src/provider/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionNetwork } from "../../src/session/network"
@@ -19,8 +19,8 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { SessionStatus } from "../../src/session/status"
import { SessionSummary } from "../../src/session/summary"
import { Snapshot } from "../../src/snapshot"
import { Log } from "../../src/util"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import * as Log from "@opencode-ai/core/util/log"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -12,12 +12,12 @@ import * as Stream from "effect/Stream"
import path from "path"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config"
import { Config } from "../../src/config/config"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import type { Provider } from "../../src/provider"
import type { Provider } from "../../src/provider/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionProcessor } from "../../src/session/processor"
@@ -26,8 +26,8 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { SessionStatus } from "../../src/session/status"
import { SessionSummary } from "../../src/session/summary"
import { Snapshot } from "../../src/snapshot"
import { Log } from "../../src/util"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import * as Log from "@opencode-ai/core/util/log"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -206,7 +206,7 @@ describe("session processor retry limit", () => {
it.effect("only positive integers enable the limit", () =>
Effect.promise(async () => {
const { Flag } = await import("../../src/flag/flag")
const { Flag } = await import("@opencode-ai/core/flag/flag")
delete process.env.KILO_SESSION_RETRY_LIMIT
expect(Flag.KILO_SESSION_RETRY_LIMIT).toBeUndefined()
@@ -227,7 +227,7 @@ describe("session processor retry limit", () => {
it.effect("reads env at access time (dynamic getter)", () =>
Effect.promise(async () => {
const { Flag } = await import("../../src/flag/flag")
const { Flag } = await import("@opencode-ai/core/flag/flag")
delete process.env.KILO_SESSION_RETRY_LIMIT
expect(Flag.KILO_SESSION_RETRY_LIMIT).toBeUndefined()
process.env.KILO_SESSION_RETRY_LIMIT = "5"
@@ -9,20 +9,20 @@ import { FetchHttpClient } from "effect/unstable/http"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { Command } from "../../src/command"
import { Config } from "../../src/config"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Config } from "../../src/config/config"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { Env } from "../../src/env"
import { Ripgrep } from "../../src/file/ripgrep"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Format } from "../../src/format"
import { LSP } from "../../src/lsp"
import { LSP } from "../../src/lsp/lsp"
import { MCP } from "../../src/mcp"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import { Provider as ProviderSvc } from "../../src/provider"
import { Provider as ProviderSvc } from "../../src/provider/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Question } from "../../src/question"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
import { LLM } from "../../src/session/llm"
@@ -38,8 +38,9 @@ import { SessionSummary } from "../../src/session/summary"
import { Todo } from "../../src/session/todo"
import { Skill } from "../../src/skill"
import { Snapshot } from "../../src/snapshot"
import { ToolRegistry, Truncate } from "../../src/tool"
import { Log } from "../../src/util"
import { ToolRegistry } from "../../src/tool/registry"
import { Truncate } from "../../src/tool/truncate"
import * as Log from "@opencode-ai/core/util/log"
import { provideTmpdirServer } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { TestLLMServer } from "../lib/llm-server"
@@ -7,11 +7,11 @@ import { Suggestion } from "../../src/kilocode/suggestion"
import { Question } from "../../src/question"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Instance } from "../../src/project/instance"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionPrompt } from "../../src/session/prompt"
import { MessageID, SessionID } from "../../src/session/schema"
import { Log } from "../../src/util"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
@@ -2,8 +2,8 @@ import { test, expect } from "bun:test"
import { $ } from "bun"
import { Snapshot } from "../../src/snapshot"
import { Instance } from "../../src/project/instance"
import { Filesystem } from "../../src/util"
import { Log } from "../../src/util"
import { Filesystem } from "../../src/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
@@ -16,9 +16,10 @@ import { test, expect, afterEach, mock } from "bun:test"
import { $ } from "bun"
import { Instance } from "../../src/project/instance"
import { Server } from "../../src/server/server"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { Snapshot } from "../../src/snapshot"
import { Filesystem, Log } from "../../src/util"
import { Filesystem } from "../../src/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
@@ -8,9 +8,9 @@ import { aggregateSessionStats } from "../../src/cli/cmd/stats"
import { MessageV2 } from "../../src/session/message-v2"
import { Instance } from "../../src/project/instance"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { MessageID, PartID } from "../../src/session/schema"
import { Log } from "../../src/util"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
@@ -3,8 +3,8 @@ import { Effect, Layer, ManagedRuntime } from "effect"
import { Command } from "../../../src/command"
import { Suggestion } from "../../../src/kilocode/suggestion"
import { SuggestTool } from "../../../src/kilocode/suggestion/tool"
import { Tool } from "../../../src/tool"
import { Truncate } from "../../../src/tool"
import { Tool } from "../../../src/tool/tool"
import { Truncate } from "../../../src/tool/truncate"
import { Agent } from "../../../src/agent/agent"
import { SessionStatus } from "../../../src/session/status"
@@ -9,19 +9,19 @@ import path from "path"
import fs from "fs/promises"
import iconv from "iconv-lite"
import { Agent } from "../../src/agent/agent"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { ApplyPatchTool } from "../../src/tool/apply_patch"
import { Bus } from "../../src/bus"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { EditTool } from "../../src/tool/edit"
import { Format } from "../../src/format"
import { Instance } from "../../src/project/instance"
import { Instruction } from "../../src/session/instruction"
import { LSP } from "../../src/lsp"
import { LSP } from "../../src/lsp/lsp"
import { MessageID, SessionID } from "../../src/session/schema"
import { ReadTool } from "../../src/tool/read"
import * as Tool from "../../src/tool/tool"
import { Truncate } from "../../src/tool"
import { Truncate } from "../../src/tool/truncate"
import { WriteTool } from "../../src/tool/write"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -1,10 +1,10 @@
import { afterEach, describe, expect, spyOn } from "bun:test"
import { Effect, Layer } from "effect"
import { KiloIndexing } from "../../src/kilocode/indexing"
import { ToolRegistry } from "../../src/tool"
import { ToolRegistry } from "../../src/tool/registry"
import { Instance } from "../../src/project/instance"
import { provideTmpdirInstance } from "../fixture/fixture"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
const node = CrossSpawnSpawner.defaultLayer
@@ -3,18 +3,18 @@ import { Effect, Layer } from "effect"
import fs from "fs/promises"
import path from "path"
import { Agent } from "../../src/agent/agent"
import { Config } from "../../src/config"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Global } from "../../src/global"
import { Config } from "../../src/config/config"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { Global } from "@opencode-ai/core/global"
import { Instance } from "../../src/project/instance"
import { Session } from "../../src/session"
import { Session } from "../../src/session/session"
import { MessageV2 } from "../../src/session/message-v2"
import type { SessionPrompt } from "../../src/session/prompt"
import { MessageID, PartID } from "../../src/session/schema"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { TaskTool, type TaskPromptOps } from "../../src/tool/task"
import { Truncate } from "../../src/tool"
import { ToolRegistry } from "../../src/tool"
import { Truncate } from "../../src/tool/truncate"
import { ToolRegistry } from "../../src/tool/registry"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { ProviderTransform } from "../../src/provider"
import { ProviderTransform } from "../../src/provider/transform"
function mockModel(overrides: Partial<any> = {}): any {
return {
@@ -3,7 +3,7 @@ import { describe, expect } from "bun:test"
import * as fs from "fs/promises"
import path from "path"
import { Effect, Layer } from "effect"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { Worktree } from "../../src/worktree"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
+3 -3
View File
@@ -2,10 +2,10 @@ import { beforeEach, describe, expect, test } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../fixture/fixture"
import { LSPClient } from "../../src/lsp"
import { LSPServer } from "../../src/lsp"
import { LSPClient } from "@/lsp/client"
import * as LSPServer from "@/lsp/server"
import { Instance } from "../../src/project/instance"
import { Log } from "../../src/util"
import * as Log from "@opencode-ai/core/util/log"
function spawnFakeServer() {
const { spawn } = require("child_process")
+4 -4
View File
@@ -2,14 +2,14 @@ import { describe, expect, spyOn, test } from "bun:test"
import path from "path"
import fs from "fs/promises"
import { Effect, Layer } from "effect"
import { LSP } from "../../src/lsp"
import { LSPServer } from "../../src/lsp"
import { LSP } from "@/lsp/lsp"
import * as LSPServer from "@/lsp/server"
import * as launch from "../../src/lsp/launch" // kilocode_change - spy on spawn
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { Instance, type InstanceContext } from "../../src/project/instance"
import { Flag } from "../../src/flag/flag" // kilocode_change
import { Flag } from "@opencode-ai/core/flag/flag" // kilocode_change
import { TsCheck } from "../../src/kilocode/ts-check" // kilocode_change
// kilocode_change - Typescript.spawn ignores ctx, so a cast is fine here.
+3 -3
View File
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
import path from "path"
import { Effect, Layer } from "effect"
import { LSP } from "../../src/lsp"
import { LSPServer } from "../../src/lsp"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { LSP } from "@/lsp/lsp"
import * as LSPServer from "@/lsp/server"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
-143
View File
@@ -1,143 +0,0 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect, test } from "bun:test"
import { Effect, Layer, Stream } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Global } from "@opencode-ai/shared/global"
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { Npm } from "../src/npm"
import { tmpdir } from "./fixture/fixture"
const win = process.platform === "win32"
const encoder = new TextEncoder()
function mockSpawner(handler: (cmd: string, args: readonly string[]) => string = () => "") {
const spawner = ChildProcessSpawner.make((command) => {
const std = ChildProcess.isStandardCommand(command) ? command : undefined
const output = handler(std?.command ?? "", std?.args ?? [])
return Effect.succeed(
ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(0),
exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
isRunning: Effect.succeed(false),
kill: () => Effect.void,
stdin: { [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") } as any,
stdout: output ? Stream.make(encoder.encode(output)) : Stream.empty,
stderr: Stream.empty,
all: Stream.empty,
getInputFd: () => ({ [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") }) as any,
getOutputFd: () => Stream.empty,
unref: Effect.succeed(Effect.void),
}),
)
})
return Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)
}
function testLayer(spawnHandler?: (cmd: string, args: readonly string[]) => string) {
return Npm.layer.pipe(
Layer.provide(mockSpawner(spawnHandler)),
Layer.provide(EffectFlock.layer),
Layer.provide(AppFileSystem.layer),
Layer.provide(Global.layer),
Layer.provide(NodeFileSystem.layer),
)
}
const writePackage = (dir: string, pkg: Record<string, unknown>) =>
Bun.write(
path.join(dir, "package.json"),
JSON.stringify({
version: "1.0.0",
...pkg,
}),
)
describe("Npm.sanitize", () => {
test("keeps normal scoped package specs unchanged", () => {
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
expect(Npm.sanitize("@opencode/acme@1.0.0")).toBe("@opencode/acme@1.0.0")
expect(Npm.sanitize("prettier")).toBe("prettier")
})
test("handles git https specs", () => {
const spec = "acme@git+https://github.com/opencode/acme.git"
const expected = win ? "acme@git+https_//github.com/opencode/acme.git" : spec
expect(Npm.sanitize(spec)).toBe(expected)
})
})
describe("Npm.install", () => {
test("respects omit from project .npmrc", async () => {
await using tmp = await tmpdir()
await writePackage(tmp.path, {
name: "fixture",
dependencies: {
"prod-pkg": "file:./prod-pkg",
},
devDependencies: {
"dev-pkg": "file:./dev-pkg",
},
})
await Bun.write(path.join(tmp.path, ".npmrc"), "omit=dev\n")
await fs.mkdir(path.join(tmp.path, "prod-pkg"))
await fs.mkdir(path.join(tmp.path, "dev-pkg"))
await writePackage(path.join(tmp.path, "prod-pkg"), { name: "prod-pkg" })
await writePackage(path.join(tmp.path, "dev-pkg"), { name: "dev-pkg" })
await Npm.install(tmp.path)
await expect(fs.stat(path.join(tmp.path, "node_modules", "prod-pkg"))).resolves.toBeDefined()
await expect(fs.stat(path.join(tmp.path, "node_modules", "dev-pkg"))).rejects.toThrow()
})
})
describe("Npm.outdated", () => {
test("checks latest via npm view", async () => {
const calls: string[][] = []
const layer = testLayer((cmd, args) => {
calls.push([cmd, ...args])
if (cmd === "npm" && args[0] === "view") return '"2.0.0"\n'
return ""
})
const result = await Effect.runPromise(
Npm.Service.use((svc) => svc.outdated("example", "1.0.0")).pipe(Effect.provide(layer)),
)
expect(result).toBe(true)
expect(calls).toContainEqual(["npm", "view", "example", "dist-tags.latest", "--json"])
})
test("keeps range comparison behavior", async () => {
const layer = testLayer((cmd, args) => {
if (cmd === "npm" && args[0] === "view") return '"2.3.0"\n'
return ""
})
const result = await Effect.runPromise(
Npm.Service.use((svc) => svc.outdated("example", "^2.0.0")).pipe(Effect.provide(layer)),
)
expect(result).toBe(false)
})
test("falls back when npm view is unavailable", async () => {
const calls: string[][] = []
const layer = testLayer((cmd, args) => {
calls.push([cmd, ...args])
if (cmd === "pnpm" && args[0] === "view") return '"2.0.0"\n'
return ""
})
const result = await Effect.runPromise(
Npm.Service.use((svc) => svc.outdated("example", "1.0.0")).pipe(Effect.provide(layer)),
)
expect(result).toBe(true)
expect(calls).toContainEqual(["npm", "view", "example", "dist-tags.latest", "--json"])
expect(calls).toContainEqual(["pnpm", "view", "example", "dist-tags.latest", "--json"])
})
})
@@ -1,6 +1,6 @@
import { afterEach, describe, test, expect } from "bun:test"
import { Permission } from "../src/permission"
import { Config } from "../src/config"
import { Config } from "@/config/config"
import { Instance } from "../src/project/instance"
import { tmpdir } from "./fixture/fixture"
import { AppRuntime } from "../src/effect/app-runtime"
+17 -33
View File
@@ -4,9 +4,9 @@ import os from "os"
import path from "path" // kilocode_change
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config" // kilocode_change
import { Global } from "../../src/global" // kilocode_change
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Config } from "../../src/config/config" // kilocode_change
import { Global } from "@opencode-ai/core/global" // kilocode_change
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Permission } from "../../src/permission"
import { PermissionID } from "../../src/permission/schema"
import { Instance } from "../../src/project/instance"
@@ -142,61 +142,45 @@ test("fromConfig - does not expand tilde in middle of path", () => {
expect(result).toEqual([{ permission: "external_directory", pattern: "/some/~/path", action: "allow" }])
})
// Top-level wildcard-vs-specific precedence semantics.
//
// fromConfig sorts top-level keys so wildcard permissions (containing "*")
// come before specific permissions. Combined with `findLast` in evaluate(),
// this gives the intuitive semantic "specific tool rules override the `*`
// fallback", regardless of the order the user wrote the keys in their JSON.
//
// Sub-pattern order inside a single permission key (e.g. `bash: { "*": "allow", "rm": "deny" }`)
// still depends on insertion order — only top-level keys are sorted.
// Permission precedence follows config insertion order. `evaluate()` uses the
// last matching rule, so later config entries intentionally override earlier
// entries even when a wildcard appears after a specific permission.
test("fromConfig - specific key beats wildcard regardless of JSON key order", () => {
test("fromConfig - preserves top-level config key order", () => {
const wildcardFirst = Permission.fromConfig({ "*": "deny", bash: "allow" })
const specificFirst = Permission.fromConfig({ bash: "allow", "*": "deny" })
// Both orderings produce the same ruleset
expect(wildcardFirst).toEqual(specificFirst)
expect(wildcardFirst.map((r) => r.permission)).toEqual(["*", "bash"])
expect(specificFirst.map((r) => r.permission)).toEqual(["bash", "*"])
// And both evaluate bash → allow (bash rule wins over * fallback)
expect(Permission.evaluate("bash", "ls", wildcardFirst).action).toBe("allow")
expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("allow")
expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("deny")
})
test("fromConfig - wildcard acts as fallback for permissions with no specific rule", () => {
const ruleset = Permission.fromConfig({ bash: "allow", "*": "ask" })
test("fromConfig - wildcard acts as fallback when it appears before specifics", () => {
const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow" })
expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("ask")
expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow")
})
test("fromConfig - top-level ordering: wildcards first, specifics after", () => {
test("fromConfig - top-level ordering is not sorted by wildcard specificity", () => {
const ruleset = Permission.fromConfig({
bash: "allow",
"*": "ask",
edit: "deny",
"mcp_*": "allow",
})
// wildcards (* and mcp_*) come before specifics (bash, edit)
const permissions = ruleset.map((r) => r.permission)
expect(permissions.slice(0, 2).sort()).toEqual(["*", "mcp_*"])
expect(permissions.slice(2)).toEqual(["bash", "edit"])
expect(ruleset.map((r) => r.permission)).toEqual(["bash", "*", "edit", "mcp_*"])
})
test("fromConfig - sub-pattern insertion order inside a tool key is preserved (only top-level sorts)", () => {
// Sub-patterns within a single tool key use the documented "`*` first,
// specific patterns after" convention (findLast picks specifics). The
// top-level sort must not touch sub-pattern ordering.
test("fromConfig - sub-pattern insertion order inside a tool key is preserved", () => {
const ruleset = Permission.fromConfig({ bash: { "*": "deny", "git *": "allow" } })
expect(ruleset.map((r) => r.pattern)).toEqual(["*", "git *"])
// * fallback for unknown commands
expect(Permission.evaluate("bash", "rm foo", ruleset).action).toBe("deny")
// specific pattern wins for git commands (it's last, findLast picks it)
expect(Permission.evaluate("bash", "git status", ruleset).action).toBe("allow")
})
test("fromConfig - canonical documented example unchanged", () => {
// Regression guard for the example in docs/permissions.mdx
test("fromConfig - documented fallback-first example", () => {
const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow", edit: "deny" })
expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow")
expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("deny")
@@ -462,7 +446,7 @@ test("evaluate - wildcard permission fallback for unknown tool", () => {
expect(result.action).toBe("ask")
})
test("evaluate - permission patterns sorted by length regardless of object order", () => {
test("evaluate - later wildcard permission can override earlier specific permission", () => {
const result = Permission.evaluate("bash", "rm", [
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "*", pattern: "*", action: "deny" },
@@ -4,7 +4,7 @@ import fs from "fs/promises"
import { Effect } from "effect"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { ProviderAuth } from "../../src/provider"
import { ProviderAuth } from "@/provider/auth"
import { ProviderID } from "../../src/provider/schema"
describe("plugin.auth-override", () => {
@@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Process } from "../../src/util"
import { Filesystem } from "../../src/util"
import { Process } from "@/util/process"
import { Filesystem } from "@/util/filesystem"
import { tmpdir } from "../fixture/fixture"
const root = path.join(import.meta.dir, "../..")
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { parse as parseJsonc } from "jsonc-parser"
import { Filesystem } from "../../src/util"
import { Filesystem } from "@/util/filesystem"
import { createPlugTask, type PlugCtx, type PlugDeps } from "../../src/cli/cmd/plug"
import { tmpdir } from "../fixture/fixture"
@@ -4,7 +4,7 @@ import fs from "fs/promises"
import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../fixture/fixture"
import { Filesystem } from "../../src/util"
import { Filesystem } from "@/util/filesystem"
const disableDefault = process.env.KILO_DISABLE_DEFAULT_PLUGINS
process.env.KILO_DISABLE_DEFAULT_PLUGINS = "1"
@@ -13,7 +13,7 @@ const { Plugin } = await import("../../src/plugin/index")
const { PluginLoader } = await import("../../src/plugin/loader")
const { readPackageThemes } = await import("../../src/plugin/shared")
const { Instance } = await import("../../src/project/instance")
const { Npm } = await import("../../src/npm")
const { Npm } = await import("@opencode-ai/core/npm")
afterAll(() => {
if (disableDefault === undefined) {
+2 -2
View File
@@ -4,8 +4,8 @@ import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../fixture/fixture"
import { Process } from "../../src/util"
import { Filesystem } from "../../src/util"
import { Process } from "@/util/process"
import { Filesystem } from "@/util/filesystem"
const { PluginMeta } = await import("../../src/plugin/meta")
const root = path.join(import.meta.dir, "../..")
@@ -7,7 +7,7 @@ import { tmpdir } from "../fixture/fixture"
const disableDefault = process.env.KILO_DISABLE_DEFAULT_PLUGINS
process.env.KILO_DISABLE_DEFAULT_PLUGINS = "1"
const { Flag } = await import("../../src/flag/flag")
const { Flag } = await import("@opencode-ai/core/flag/flag")
const { Plugin } = await import("../../src/plugin/index")
const { Workspace } = await import("../../src/control-plane/workspace")
const { Instance } = await import("../../src/project/instance")
+2 -2
View File
@@ -10,7 +10,7 @@ import { remove as cleanup } from "./kilocode/cleanup" // kilocode_change
const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
await fs.mkdir(dir, { recursive: true })
afterAll(async () => {
const { Database } = await import("../src/storage")
const { Database } = await import("../src/storage/db")
Database.close()
await cleanup(dir) // kilocode_change
})
@@ -65,7 +65,7 @@ delete process.env["KILO_SERVER_USERNAME"]
process.env["KILO_DB"] = ":memory:"
// Now safe to import from src/
const { Log } = await import("../src/util")
const Log = await import("@opencode-ai/core/util/log")
const { initProjectors } = await import("../src/server/projectors")
void Log.init({
@@ -1,11 +1,12 @@
import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project"
import { Database, eq } from "../../src/storage"
import { Project } from "@/project/project"
import { Database } from "@/storage/db"
import { eq } from "drizzle-orm"
import { SessionTable } from "../../src/session/session.sql"
import { ProjectTable } from "../../src/project/project.sql"
import { ProjectID } from "../../src/project/schema"
import { SessionID } from "../../src/session/schema"
import { Log } from "../../src/util"
import * as Log from "@opencode-ai/core/util/log"
import { $ } from "bun"
import { tmpdir } from "../fixture/fixture"
import { Effect } from "effect"
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project"
import { Log } from "../../src/util"
import { Project } from "@/project/project"
import * as Log from "@opencode-ai/core/util/log"
import { $ } from "bun"
import path from "path"
import { tmpdir } from "../fixture/fixture"
@@ -9,8 +9,8 @@ import { ProjectID } from "../../src/project/schema"
import { Effect, Layer, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { NodePath } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
void Log.init({ print: false })

Some files were not shown because too many files have changed in this diff Show More