bacfa4ae62
* refactor(opencode): migrate agent promise helpers to kilocode module and adopt Effect-based tests Remove legacy promise-based helpers (`get`, `list`, `defaultAgent`, `remove`) from the core agent module and update callsites to use the dedicated `@/kilocode/agent` module directly. Migrate kilocode-specific tests from `WithInstance.provide` patterns to the `testEffect` helper with Effect generators for cleaner, more idiomatic test code. - Remove `makeRuntime` import and exported promise helpers from agent.ts - Update HTTP API handlers to import from `@/kilocode/agent` instead of re-exported `Agent` namespace - Rewrite agent tests to use `load()` helper or Effect service access - Convert agent-global-config-dirs and agent-skill-permissions tests to `testEffect` pattern - Reorder SDK type definitions (BackgroundProcessLogs/WorkspaceWarpError) * chore(auth): remove legacy promise helpers and replace with direct Effect service access Eliminate the exported promise-based convenience functions (`get`, `all`, `set`, `remove`) from the Auth module and replace all callsites with explicit `AppRuntime.runPromise` or `makeRuntime` invocations that access `Auth.Service` directly through Effect's service pattern. - Delete `makeRuntime`-backed promise helpers from auth/index.ts - Update CLI entrypoint, kilo-sessions, indexing, and server instance to use `AppRuntime.runPromise(Auth.Service.use(...))` for auth access - Inject `Auth.Service` as a dependency into the ModelsDev layer and consume it via Effect generator instead of promise wrapper - Create a local `makeRuntime` instance in model-cache.ts for non-Effect callsites that still need promise-based auth access - Rewrite tests to manipulate auth.json directly on disk with proper save/restore semantics instead of relying on removed helpers * refactor(suggestion): convert suggest tool to Effect-native with injected Command dependency Transform the suggestion tool from async/promise-based implementation to idiomatic Effect generators with explicit dependency injection of the Command service rather than importing and calling module-level helpers. - Convert `resolvePrompt` from async function to Effect generator that accepts a `Command.Interface` parameter - Refactor `SuggestTool` definition to yield `Command.Service` from the Effect context and thread it through to `resolvePrompt` - Add `Command.Service` as a dependency to the tool registry layer and provide `Command.defaultLayer` in both production and test wiring - Remove unused `makeRuntime` import and exported `get` helper from command/index.ts - Add explicit type annotations to Auth delegate in server instance - Rewrite suggestion tests to use `testEffect` helper with a mock `Command.Service` layer instead of spying on module exports * feat(git): migrate WorktreeFamily to Effect service and wire Git.Service as dependency Convert WorktreeFamily.list from an async function using legacy promise helpers to an Effect generator that yields Git.Service from context, eliminating the need for the removed `run` promise wrapper in git/index. - Replace `WorktreeFamily.list()` async function with Effect.fn generator that obtains Git.Service and InstanceState from the Effect context - Remove legacy `makeRuntime`/`run`/`runPromise` exports from git module - Update RecallTool to thread Git.Service through to WorktreeFamily calls via EffectBridge - Add Git.Service as a required dependency in tool registry and HTTP server route layers - Update all test layers to provide Git.defaultLayer * chore(mcp): replace legacy promise helpers with Effect-native AppRuntime calls Remove exported promise-based `status`, `connect`, and `disconnect` helpers from MCP module and convert the network recovery callsite in SessionNetwork to use AppRuntime.runPromise with Effect.gen directly. * refactor(auth): adopt makeRuntime helper for Auth service resolution in kilo modules Replace AppRuntime.runPromise with locally scoped makeRuntime instances in kilo-sessions and kilocode/indexing modules, removing the dependency on the global AppRuntime singleton for Auth service access. * fix(test): simplify cleanup error handling in provider test Replace try-catch block with promise .catch() for file unlink operation during test teardown.
959 lines
29 KiB
TypeScript
959 lines
29 KiB
TypeScript
import { afterEach, test, expect } from "bun:test"
|
|
import { Effect } from "effect"
|
|
import path from "path"
|
|
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
|
import { WithInstance } from "../../src/project/with-instance"
|
|
import { Agent } from "../../src/agent/agent"
|
|
import { Permission } from "../../src/permission"
|
|
import { Global } from "@opencode-ai/core/global"
|
|
|
|
// Helper to evaluate permission for a tool with wildcard pattern
|
|
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
|
|
if (!agent) return undefined
|
|
return Permission.evaluate(permission, "*", agent.permission).action
|
|
}
|
|
|
|
function load<A>(dir: string, fn: (svc: Agent.Interface) => Effect.Effect<A>) {
|
|
return Effect.runPromise(provideInstance(dir)(Agent.Service.use(fn)).pipe(Effect.provide(Agent.defaultLayer)))
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await disposeAllInstances()
|
|
})
|
|
|
|
test("returns default native agents when no config", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const agents = await load(tmp.path, (svc) => svc.list())
|
|
const names = agents.map((a) => a.name)
|
|
expect(names).toContain("code") // kilocode_change
|
|
expect(names).toContain("plan")
|
|
expect(names).toContain("debug") // kilocode_change
|
|
expect(names).toContain("orchestrator") // kilocode_change
|
|
expect(names).toContain("ask") // kilocode_change
|
|
expect(names).toContain("general")
|
|
expect(names).toContain("explore")
|
|
expect(names).toContain("compaction")
|
|
expect(names).toContain("title")
|
|
expect(names).toContain("summary")
|
|
},
|
|
})
|
|
})
|
|
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
test("code agent has correct default properties", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code).toBeDefined()
|
|
expect(code?.mode).toBe("primary")
|
|
expect(code?.native).toBe(true)
|
|
expect(evalPerm(code, "edit")).toBe("allow")
|
|
expect(evalPerm(code, "bash")).toBe("ask") // kilocode_change - safe-bash default is ask
|
|
},
|
|
})
|
|
})
|
|
// kilocode_change end
|
|
|
|
// kilocode_change start - ask agent tests
|
|
test("ask agent has correct default properties", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const ask = await load(tmp.path, (svc) => svc.get("ask"))
|
|
expect(ask).toBeDefined()
|
|
expect(ask?.mode).toBe("primary")
|
|
expect(ask?.native).toBe(true)
|
|
// ask agent should allow read-only tools
|
|
expect(evalPerm(ask, "read")).toBe("allow")
|
|
expect(evalPerm(ask, "grep")).toBe("allow")
|
|
expect(evalPerm(ask, "glob")).toBe("allow")
|
|
expect(evalPerm(ask, "webfetch")).toBe("allow")
|
|
expect(evalPerm(ask, "websearch")).toBe("allow")
|
|
expect(evalPerm(ask, "codesearch")).toBe("allow")
|
|
// ask agent should deny edit and bash
|
|
expect(evalPerm(ask, "edit")).toBe("deny")
|
|
expect(evalPerm(ask, "bash")).toBe("deny")
|
|
expect(evalPerm(ask, "task")).toBe("deny")
|
|
// ask agent should gate .env files
|
|
expect(Permission.evaluate("read", ".env", ask!.permission).action).toBe("ask")
|
|
expect(Permission.evaluate("read", "config.env.local", ask!.permission).action).toBe("ask")
|
|
expect(Permission.evaluate("read", ".env.example", ask!.permission).action).toBe("allow")
|
|
expect(Permission.evaluate("read", "src/index.ts", ask!.permission).action).toBe("allow")
|
|
},
|
|
})
|
|
})
|
|
test("ask agent denies edit/write/bash even when user config adds a specific edit allow", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
permission: {
|
|
edit: { "src/output.log": "allow" },
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const ask = await load(tmp.path, (svc) => svc.get("ask"))
|
|
expect(ask).toBeDefined()
|
|
// user config must not leak edit capability into ask mode — even for the
|
|
// specific path the user allowed, ask mode must still deny it
|
|
expect(Permission.evaluate("edit", "src/output.log", ask!.permission).action).toBe("deny")
|
|
expect(evalPerm(ask, "bash")).toBe("deny")
|
|
expect(evalPerm(ask, "task")).toBe("deny")
|
|
// safe tools still work
|
|
expect(evalPerm(ask, "read")).toBe("allow")
|
|
expect(evalPerm(ask, "grep")).toBe("allow")
|
|
// disabled() hides tools entirely from LLM — bash is NOT disabled because it has specific allow rules
|
|
const disabled = Permission.disabled(["edit", "write", "bash"], ask!.permission)
|
|
expect(disabled.has("edit")).toBe(true)
|
|
expect(disabled.has("write")).toBe(true)
|
|
expect(disabled.has("bash")).toBe(false)
|
|
},
|
|
})
|
|
})
|
|
// kilocode_change end
|
|
|
|
// kilocode_change start
|
|
test("plan agent denies edits except .kilo/plans/* and .opencode/plans/*", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const plan = await load(tmp.path, (svc) => svc.get("plan"))
|
|
expect(plan).toBeDefined()
|
|
expect(evalPerm(plan, "edit")).toBe("deny")
|
|
expect(Permission.evaluate("edit", "src/index.ts", plan!.permission).action).toBe("deny")
|
|
expect(Permission.evaluate("edit", ".kilo/plans/foo.md", plan!.permission).action).toBe("allow")
|
|
expect(Permission.evaluate("edit", ".opencode/plans/foo.md", plan!.permission).action).toBe("allow")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("plan agent user config allows cannot re-enable non-plan edits", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
permission: {
|
|
edit: { "src/output.log": "allow" },
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const plan = await load(tmp.path, (svc) => svc.get("plan"))
|
|
expect(plan).toBeDefined()
|
|
expect(Permission.evaluate("edit", "src/output.log", plan!.permission).action).toBe("deny")
|
|
expect(Permission.evaluate("edit", ".kilo/plans/foo.md", plan!.permission).action).toBe("allow")
|
|
expect(Permission.evaluate("edit", ".opencode/plans/foo.md", plan!.permission).action).toBe("allow")
|
|
},
|
|
})
|
|
})
|
|
// kilocode_change end
|
|
|
|
test("explore agent denies edit and write", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
|
expect(explore).toBeDefined()
|
|
expect(explore?.mode).toBe("subagent")
|
|
expect(evalPerm(explore, "edit")).toBe("deny")
|
|
expect(evalPerm(explore, "write")).toBe("deny")
|
|
expect(evalPerm(explore, "todowrite")).toBe("deny")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("explore agent asks for external directories and allows whitelisted external paths", async () => {
|
|
const { Truncate } = await import("../../src/tool/truncate")
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
|
expect(explore).toBeDefined()
|
|
expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
|
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
|
|
expect(
|
|
Permission.evaluate("external_directory", path.join(Global.Path.tmp, "agent-work"), explore!.permission).action,
|
|
).toBe("allow")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("general agent denies todo tools", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const general = await load(tmp.path, (svc) => svc.get("general"))
|
|
expect(general).toBeDefined()
|
|
expect(general?.mode).toBe("subagent")
|
|
expect(general?.hidden).toBeUndefined()
|
|
expect(evalPerm(general, "todowrite")).toBe("deny")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("compaction agent denies all permissions", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const compaction = await load(tmp.path, (svc) => svc.get("compaction"))
|
|
expect(compaction).toBeDefined()
|
|
expect(compaction?.hidden).toBe(true)
|
|
expect(evalPerm(compaction, "bash")).toBe("deny")
|
|
expect(evalPerm(compaction, "edit")).toBe("deny")
|
|
expect(evalPerm(compaction, "read")).toBe("deny")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("custom agent from config creates new agent", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
my_custom_agent: {
|
|
model: "openai/gpt-4",
|
|
description: "My custom agent",
|
|
temperature: 0.5,
|
|
top_p: 0.9,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const custom = await load(tmp.path, (svc) => svc.get("my_custom_agent"))
|
|
expect(custom).toBeDefined()
|
|
expect(String(custom?.model?.providerID)).toBe("openai")
|
|
expect(String(custom?.model?.modelID)).toBe("gpt-4")
|
|
expect(custom?.description).toBe("My custom agent")
|
|
expect(custom?.temperature).toBe(0.5)
|
|
expect(custom?.topP).toBe(0.9)
|
|
expect(custom?.native).toBe(false)
|
|
expect(custom?.mode).toBe("all")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("custom agent config overrides native agent properties", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
// kilocode_change start
|
|
code: {
|
|
model: "anthropic/claude-3",
|
|
description: "Custom code agent",
|
|
temperature: 0.7,
|
|
color: "#FF0000",
|
|
},
|
|
// kilocode_change end
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code).toBeDefined()
|
|
expect(String(code?.model?.providerID)).toBe("anthropic")
|
|
expect(String(code?.model?.modelID)).toBe("claude-3")
|
|
expect(code?.description).toBe("Custom code agent")
|
|
expect(code?.temperature).toBe(0.7)
|
|
expect(code?.color).toBe("#FF0000")
|
|
expect(code?.native).toBe(true)
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("agent disable removes agent from list", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
explore: { disable: true },
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
|
expect(explore).toBeUndefined()
|
|
const agents = await load(tmp.path, (svc) => svc.list())
|
|
const names = agents.map((a) => a.name)
|
|
expect(names).not.toContain("explore")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("agent permission config merges with defaults", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
// kilocode_change start
|
|
code: {
|
|
// kilocode_change end
|
|
permission: {
|
|
bash: {
|
|
"rm -rf *": "deny",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code).toBeDefined()
|
|
// Specific pattern is denied
|
|
expect(Permission.evaluate("bash", "rm -rf *", code!.permission).action).toBe("deny")
|
|
// Edit still allowed
|
|
expect(evalPerm(code, "edit")).toBe("allow")
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("global permission config applies to all agents", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
permission: {
|
|
bash: "deny",
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code).toBeDefined()
|
|
expect(evalPerm(code, "bash")).toBe("deny")
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("agent steps/maxSteps config sets steps property", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
code: { steps: 50 },
|
|
// kilocode_change end
|
|
plan: { maxSteps: 100 },
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const code = await load(tmp.path, (svc) => svc.get("code")) // kilocode_change
|
|
const plan = await load(tmp.path, (svc) => svc.get("plan"))
|
|
expect(code?.steps).toBe(50) // kilocode_change
|
|
expect(plan?.steps).toBe(100)
|
|
},
|
|
})
|
|
})
|
|
|
|
test("agent mode can be overridden", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
explore: { mode: "primary" },
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
|
expect(explore?.mode).toBe("primary")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("agent name can be overridden", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
code: { name: "Coder" }, // kilocode_change
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code?.name).toBe("Coder")
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("agent prompt can be set from config", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
code: { prompt: "Custom system prompt" }, // kilocode_change
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code?.prompt).toBe("Custom system prompt")
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("unknown agent properties are placed into options", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
code: {
|
|
random_property: "hello",
|
|
another_random: 123,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code?.options.random_property).toBe("hello")
|
|
expect(code?.options.another_random).toBe(123)
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("agent options merge correctly", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
code: {
|
|
// kilocode_change end
|
|
options: {
|
|
custom_option: true,
|
|
another_option: "value",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code?.options.custom_option).toBe(true)
|
|
expect(code?.options.another_option).toBe("value")
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("multiple custom agents can be defined", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
agent_a: {
|
|
description: "Agent A",
|
|
mode: "subagent",
|
|
},
|
|
agent_b: {
|
|
description: "Agent B",
|
|
mode: "primary",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const agentA = await load(tmp.path, (svc) => svc.get("agent_a"))
|
|
const agentB = await load(tmp.path, (svc) => svc.get("agent_b"))
|
|
expect(agentA?.description).toBe("Agent A")
|
|
expect(agentA?.mode).toBe("subagent")
|
|
expect(agentB?.description).toBe("Agent B")
|
|
expect(agentB?.mode).toBe("primary")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Agent.list keeps the default agent first and sorts the rest by name", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
default_agent: "plan",
|
|
agent: {
|
|
zebra: {
|
|
description: "Zebra",
|
|
mode: "subagent",
|
|
},
|
|
alpha: {
|
|
description: "Alpha",
|
|
mode: "subagent",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const names = (await load(tmp.path, (svc) => svc.list())).map((a) => a.name)
|
|
expect(names[0]).toBe("plan")
|
|
expect(names.slice(1)).toEqual(names.slice(1).toSorted((a, b) => a.localeCompare(b)))
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Agent.get returns undefined for non-existent agent", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const nonExistent = await load(tmp.path, (svc) => svc.get("does_not_exist"))
|
|
expect(nonExistent).toBeUndefined()
|
|
},
|
|
})
|
|
})
|
|
|
|
test("default permission includes doom_loop and external_directory as ask", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(evalPerm(code, "doom_loop")).toBe("ask")
|
|
expect(evalPerm(code, "external_directory")).toBe("ask")
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("webfetch is allowed by default", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(evalPerm(code, "webfetch")).toBe("allow")
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("legacy tools config converts to permissions", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
code: {
|
|
// kilocode_change end
|
|
tools: {
|
|
bash: false,
|
|
read: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(evalPerm(code, "bash")).toBe("deny")
|
|
expect(evalPerm(code, "read")).toBe("deny")
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("legacy tools config maps write/edit/patch to edit permission", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
code: {
|
|
// kilocode_change end
|
|
tools: {
|
|
write: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(evalPerm(code, "edit")).toBe("deny")
|
|
// kilocode_change end
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Truncate.GLOB is allowed even when user denies external_directory globally", async () => {
|
|
const { Truncate } = await import("../../src/tool/truncate")
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
permission: {
|
|
external_directory: "deny",
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const build = await load(tmp.path, (svc) => svc.get("code"))
|
|
// kilocode_change end
|
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
|
|
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
|
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("global tmp directory children are allowed for external_directory", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
expect(
|
|
Permission.evaluate("external_directory", path.join(Global.Path.tmp, "scratch"), build!.permission).action,
|
|
).toBe("allow")
|
|
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("ask")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
|
|
const { Truncate } = await import("../../src/tool/truncate")
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
code: {
|
|
// kilocode_change end
|
|
permission: {
|
|
external_directory: "deny",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const build = await load(tmp.path, (svc) => svc.get("code"))
|
|
// kilocode_change end
|
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
|
|
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
|
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("explicit Truncate.GLOB deny is respected", async () => {
|
|
const { Truncate } = await import("../../src/tool/truncate")
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
permission: {
|
|
external_directory: {
|
|
"*": "deny",
|
|
[Truncate.GLOB]: "deny",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
const build = await load(tmp.path, (svc) => svc.get("code"))
|
|
// kilocode_change end
|
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny")
|
|
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("skill directories are allowed for external_directory", async () => {
|
|
await using tmp = await tmpdir({
|
|
git: true,
|
|
init: async (dir) => {
|
|
const skillDir = path.join(dir, ".kilo", "skill", "perm-skill") // kilocode_change: .kilo is primary
|
|
await Bun.write(
|
|
path.join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: perm-skill
|
|
description: Permission skill.
|
|
---
|
|
|
|
# Permission Skill
|
|
`,
|
|
)
|
|
},
|
|
})
|
|
|
|
const home = process.env.KILO_TEST_HOME
|
|
process.env.KILO_TEST_HOME = tmp.path
|
|
|
|
try {
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
const skillDir = path.join(tmp.path, ".kilo", "skill", "perm-skill") // kilocode_change: .kilo is primary
|
|
const target = path.join(skillDir, "reference", "notes.md")
|
|
expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")
|
|
},
|
|
})
|
|
} finally {
|
|
process.env.KILO_TEST_HOME = home
|
|
}
|
|
})
|
|
|
|
test("defaultAgent returns build when no default_agent config", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
|
expect(agent).toBe("code") // kilocode_change
|
|
},
|
|
})
|
|
})
|
|
|
|
test("defaultAgent respects default_agent config set to plan", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
default_agent: "plan",
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
|
expect(agent).toBe("plan")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("defaultAgent respects default_agent config set to custom agent with mode all", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
default_agent: "my_custom",
|
|
agent: {
|
|
my_custom: {
|
|
description: "My custom agent",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
|
expect(agent).toBe("my_custom")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("defaultAgent throws when default_agent points to subagent", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
default_agent: "explore",
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow('default agent "explore" is a subagent')
|
|
},
|
|
})
|
|
})
|
|
|
|
test("defaultAgent throws when default_agent points to hidden agent", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
default_agent: "compaction",
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow('default agent "compaction" is hidden')
|
|
},
|
|
})
|
|
})
|
|
|
|
test("defaultAgent throws when default_agent points to non-existent agent", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
default_agent: "does_not_exist",
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow(
|
|
'default agent "does_not_exist" not found',
|
|
)
|
|
},
|
|
})
|
|
})
|
|
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
test("defaultAgent returns plan when code is disabled and default_agent not set", async () => {
|
|
// kilocode_change end
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
// kilocode_change start - renamed from "build" to "code"
|
|
code: { disable: true },
|
|
// kilocode_change end
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
|
// kilocode_change - code is disabled, so it should return plan (next primary agent)
|
|
expect(agent).toBe("plan")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("defaultAgent throws when all primary agents are disabled", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
// kilocode_change start - disable all primary agents
|
|
code: { disable: true },
|
|
plan: { disable: true },
|
|
debug: { disable: true },
|
|
orchestrator: { disable: true },
|
|
ask: { disable: true },
|
|
// kilocode_change end
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
// kilocode_change - all primary agents are disabled
|
|
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow("no primary visible agent found")
|
|
},
|
|
})
|
|
})
|
|
|
|
// kilocode_change start - Backward compatibility tests for "build" -> "code" rename
|
|
test("Agent.get('build') returns code agent for backward compatibility", async () => {
|
|
await using tmp = await tmpdir()
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const [build, code] = await load(tmp.path, (svc) =>
|
|
Effect.gen(function* () {
|
|
const build = yield* svc.get("build")
|
|
const code = yield* svc.get("code")
|
|
return [build, code] as const
|
|
}),
|
|
)
|
|
expect(build).toBeDefined()
|
|
expect(build).toBe(code)
|
|
expect(build?.name).toBe("code")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("agent.build config applies to code agent for backward compatibility", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
build: {
|
|
temperature: 0.8,
|
|
color: "#00FF00",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code).toBeDefined()
|
|
expect(code?.temperature).toBe(0.8)
|
|
expect(code?.color).toBe("#00FF00")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("default_agent: 'build' returns code agent for backward compatibility", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
default_agent: "build",
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
|
expect(agent).toBe("code")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("agent.build disable removes code agent for backward compatibility", async () => {
|
|
await using tmp = await tmpdir({
|
|
config: {
|
|
agent: {
|
|
build: { disable: true },
|
|
},
|
|
},
|
|
})
|
|
await WithInstance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const code = await load(tmp.path, (svc) => svc.get("code"))
|
|
expect(code).toBeUndefined()
|
|
const agents = await load(tmp.path, (svc) => svc.list())
|
|
const names = agents.map((a) => a.name)
|
|
expect(names).not.toContain("code")
|
|
},
|
|
})
|
|
})
|
|
// kilocode_change end
|