Merge branch 'main' into feature/charts

This commit is contained in:
cmanu
2026-07-24 12:43:38 -07:00
681 changed files with 37505 additions and 55642 deletions
@@ -1,6 +1,6 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { RGBA, type BoxRenderable } from "@opentui/core"
import { BoxRenderable, RGBA, type RootRenderable } from "@opentui/core"
import { testRender, useRenderer } from "@opentui/solid"
import { createSignal } from "solid-js"
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
@@ -251,6 +251,45 @@ function expectPaletteList(list: BoxRenderable, selectedIndex: number) {
)
}
function child(root: BoxRenderable | RootRenderable, index: number) {
return root.getChildren()[index] as BoxRenderable
}
function boxPath(root: BoxRenderable | RootRenderable, name: string): BoxRenderable[] | undefined {
for (const item of root.getChildren()) {
if (item.constructor.name === name) return root instanceof BoxRenderable ? [root] : []
if (!(item instanceof BoxRenderable)) continue
const path = boxPath(item, name)
if (path) return root instanceof BoxRenderable ? [root, ...path] : path
}
}
function footerComposerFrame(root: BoxRenderable | RootRenderable) {
return boxPath(root, "TextareaRenderable")!.at(-5)!
}
function footerStatusline(root: BoxRenderable | RootRenderable) {
const status = (RUN_THEME_FALLBACK.footer.status as RGBA).toInts()
const accent = (RUN_THEME_FALLBACK.footer.statusAccent as RGBA).toInts()
const boxes = root.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable)
for (const box of boxes) {
const first = box.getChildren().find((item): item is BoxRenderable => item instanceof BoxRenderable)
if (
box.backgroundColor?.toInts().every((value, index) => value === status[index]) &&
first?.backgroundColor?.toInts().every((value, index) => value === accent[index])
)
return box
boxes.push(...box.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable))
}
throw new Error("Footer statusline not found")
}
function panelMenu(root: BoxRenderable | RootRenderable) {
const panel = child(child(root, 0), 0)
const content = child(panel, 0)
return child(content.getChildren().at(-1) as BoxRenderable, 0)
}
test("direct footer composer area does not adopt footer surface", async () => {
const surface = RGBA.fromHex("#123456")
const [theme, setTheme] = createSignal(RUN_THEME_FALLBACK)
@@ -258,7 +297,7 @@ test("direct footer composer area does not adopt footer surface", async () => {
try {
await app.renderOnce()
const area = app.renderer.root.findDescendantById("run-direct-footer-composer-area") as BoxRenderable
const area = child(footerComposerFrame(app.renderer.root), 0)
expect(area.backgroundColor.toInts()).not.toEqual(surface.toInts())
setTheme({
@@ -591,7 +630,7 @@ test("direct subagent panel renders active subagents", async () => {
try {
await app.renderOnce()
const frame = app.captureCharFrame()
const list = app.renderer.root.findDescendantById("run-direct-footer-subagent-list") as BoxRenderable
const list = panelMenu(app.renderer.root)
expect(frame).toContain("Select subagent")
expect(frame).toContain("Inspect auth flow")
@@ -629,7 +668,7 @@ test("direct queued prompt panel renders pending prompt actions", async () => {
try {
await app.renderOnce()
const frame = app.captureCharFrame()
const list = app.renderer.root.findDescendantById("run-direct-footer-queued-list") as BoxRenderable
const list = panelMenu(app.renderer.root)
expect(frame).toContain("Queued prompts")
expect(frame).toContain("fix the auth test")
@@ -651,12 +690,12 @@ test.skip("direct footer recreates the frame across command panel transitions",
await app.renderOnce()
for (let index = 0; index < 3; index++) {
const composerFrame = app.renderer.root.findDescendantById("run-direct-footer-composer-frame") as BoxRenderable
const composerFrame = footerComposerFrame(app.renderer.root)
app.mockInput.pressKey("p", { ctrl: true })
await app.renderOnce()
expect(app.captureCharFrame()).toContain("Commands")
expect(app.renderer.root.findDescendantById("run-direct-footer-composer-frame")).not.toBe(composerFrame)
expect(footerComposerFrame(app.renderer.root)).not.toBe(composerFrame)
app.mockInput.pressKey("c", { ctrl: true })
await app.renderOnce()
expect(app.captureCharFrame()).not.toContain("Commands")
@@ -956,13 +995,14 @@ test("direct footer shows editable prompts and additional queued work while runn
const transparent = RGBA.fromValues(0, 0, 0, 0).toInts()
const tinted = (RUN_THEME_FALLBACK.footer.status as RGBA).toInts()
const accent = (RUN_THEME_FALLBACK.footer.statusAccent as RGBA).toInts()
const statusline = app.renderer.root.findDescendantById("run-direct-footer-statusline") as BoxRenderable
const mode = app.renderer.root.findDescendantById("run-direct-footer-statusline-mode") as BoxRenderable
const main = app.renderer.root.findDescendantById("run-direct-footer-statusline-main") as BoxRenderable
const spinner = app.renderer.root.findDescendantById("run-direct-footer-status-spinner")
const model = app.renderer.root.findDescendantById("run-direct-footer-statusline-model") as BoxRenderable
const queued = app.renderer.root.findDescendantById("run-direct-footer-statusline-queued") as BoxRenderable
const hint = app.renderer.root.findDescendantById("run-direct-footer-statusline-hint") as BoxRenderable
const statusline = footerStatusline(app.renderer.root)
const statusItems = statusline.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable)
const mode = statusItems[0]
const main = statusItems[1]
const spinner = main.getChildren()[0]
const model = statusItems[2]
const queued = statusItems[3]
const hint = statusItems.at(-1)!
expect(spinner).toBeDefined()
expect(frame).toContain("a-model-name-long-enough-to-force-responsive-truncation")
@@ -1280,7 +1320,7 @@ test("direct model panel renders current model selector", async () => {
try {
await app.renderOnce()
const frame = app.captureCharFrame()
const list = app.renderer.root.findDescendantById("run-direct-footer-model-list") as BoxRenderable
const list = panelMenu(app.renderer.root)
expect(frame).toContain("Select model")
expect(frame).toContain("Search")
@@ -1323,7 +1363,7 @@ test("direct variant panel renders current variant selector", async () => {
try {
await app.renderOnce()
const frame = app.captureCharFrame()
const list = app.renderer.root.findDescendantById("run-direct-footer-variant-list") as BoxRenderable
const list = panelMenu(app.renderer.root)
expect(frame).toContain("Select variant")
expect(frame).toContain("Default")
@@ -578,6 +578,68 @@ it.instance("rejects environment variable substitution in project config", () =>
),
)
it.instance("injects $schema into config without existing schema", () =>
Effect.gen(function* () {
const test = yield* TestInstance
// Config without $schema - should trigger auto-add
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, "kilo.json"),
JSON.stringify({ username: "test-user" }),
)
const config = yield* Config.use.get()
expect(config.username).toBe("test-user")
expect(config.$schema).toBe("https://app.kilo.ai/config.json")
// Read the file to verify $schema was injected
const content = yield* FSUtil.use.readFileString(path.join(test.directory, "kilo.json"))
expect(content).toContain('"$schema": "https://app.kilo.ai/config.json"')
const schemaIndex = content.indexOf('"$schema"')
const usernameIndex = content.indexOf('"username"')
expect(schemaIndex).toBeLessThan(usernameIndex)
}),
)
it.instance("injects $schema into comment-first JSONC config", () =>
Effect.gen(function* () {
const test = yield* TestInstance
// Config with leading comment - regex-based injection would fail
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, "kilo.jsonc"),
'// project config\n{\n "model": "test/model"\n}\n',
)
const config = yield* Config.use.get()
expect(config.model).toBe("test/model")
expect(config.$schema).toBe("https://app.kilo.ai/config.json")
// Read the file to verify $schema was injected correctly
const content = yield* FSUtil.use.readFileString(path.join(test.directory, "kilo.jsonc"))
expect(content).toContain('"$schema": "https://app.kilo.ai/config.json"')
expect(content).toContain("// project config")
const schemaIndex = content.indexOf('"$schema"')
const modelIndex = content.indexOf('"model"')
expect(schemaIndex).toBeLessThan(modelIndex)
}),
)
it.instance("does not write config when $schema already present", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "kilo.json")
// Config already has $schema - should not rewrite file
yield* FSUtil.use.writeWithDirs(
filepath,
JSON.stringify({ $schema: "https://app.kilo.ai/config.json", username: "test-user" }),
)
const before = yield* Effect.promise(() => fs.stat(filepath))
const config = yield* Config.use.get()
expect(config.username).toBe("test-user")
const after = yield* Effect.promise(() => fs.stat(filepath))
expect(after.mtimeMs).toBe(before.mtimeMs)
}),
)
it.instance("allows {file:} that stays inside the project root", () =>
Effect.gen(function* () {
const test = yield* TestInstance
@@ -0,0 +1,50 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js"
const posts: Array<{ method: string; session: string | null }> = []
let initializeCount = 0
let pingCount = 0
const server = Bun.serve({
port: 0,
async fetch(request) {
if (request.method === "GET") return new Response(null, { status: 405 })
if (request.method === "DELETE") return new Response(null, { status: 200 })
const message = (await request.json()) as { id?: number; method: string }
const session = request.headers.get("mcp-session-id")
posts.push({ method: message.method, session })
if (message.method === "initialize") {
initializeCount++
return Response.json(
{
jsonrpc: "2.0",
id: message.id,
result: {
protocolVersion: LATEST_PROTOCOL_VERSION,
capabilities: {},
serverInfo: { name: "test", version: "1" },
},
},
{ headers: { "mcp-session-id": initializeCount === 1 ? "expired" : "replacement" } },
)
}
if (message.method === "notifications/initialized") return new Response(null, { status: 202 })
pingCount++
if (pingCount === 1) return new Response("Session not found", { status: 404 })
return Response.json({ jsonrpc: "2.0", id: message.id, result: {} })
},
})
const client = new Client({ name: "test", version: "1" })
try {
await client.connect(new StreamableHTTPClientTransport(server.url))
await client.ping()
process.stdout.write(JSON.stringify(posts))
} finally {
await client.close()
server.stop(true)
}
@@ -1,4 +1,5 @@
import { afterEach, expect, test } from "bun:test"
import type { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { Effect } from "effect"
import { Agent } from "../../src/agent/agent"
import { Permission } from "../../src/permission"
@@ -14,6 +15,21 @@ function load<A>(dir: string, fn: (svc: Agent.Interface) => Effect.Effect<A>) {
)
}
async function get(config: Partial<ConfigV1.Info>, name = "plan") {
await using tmp = await tmpdir({ config })
const item = await provideTestInstance({
directory: tmp.path,
fn: () => load(tmp.path, (svc) => svc.get(name)),
})
return item
}
function expectPlan(item: Agent.Info | undefined, action: Permission.Action = "allow") {
expect(item).toBeDefined()
expect(Permission.evaluate("edit", "src/output.log", item!.permission).action).toBe("deny")
expect(Permission.evaluate("edit", ".kilo/plans/fix.md", item!.permission).action).toBe(action)
}
afterEach(async () => {
await disposeAllInstances()
})
@@ -81,6 +97,194 @@ test("plan agent still hard-denies non-plan edits after user edit allow", async
})
})
test("plan agent still hard-denies non-plan edits after per-agent edit ask", async () => {
const plan = await get(
{
agent: {
plan: {
permission: {
edit: "ask",
},
},
},
},
)
expectPlan(plan)
})
test("plan agent honors global and per-agent plan allows after wildcard edit deny", async () => {
const edit = {
"*": "deny" as const,
".kilo/plans/*": "allow" as const,
}
for (const config of [
{ permission: { edit } },
{
agent: {
plan: {
permission: {
edit,
},
},
},
},
]) {
expectPlan(await get(config))
}
})
test("plan agent preserves scalar edit deny", async () => {
const plan = await get(
{
agent: {
plan: {
permission: {
edit: "deny",
},
},
},
},
)
expectPlan(plan, "deny")
})
test("plan agent preserves a terminal wildcard edit deny", async () => {
const plan = await get(
{
agent: {
plan: {
permission: {
edit: {
".kilo/plans/*": "allow",
"*": "deny",
},
},
},
},
},
)
expectPlan(plan, "deny")
})
test("plan agent preserves explicit per-agent edit denies", async () => {
const plan = await get(
{
agent: {
plan: {
permission: {
edit: {
".kilo/plans/private.md": "deny",
},
},
},
},
},
)
expectPlan(plan)
expect(Permission.evaluate("edit", ".kilo/plans/private.md", plan!.permission).action).toBe("deny")
})
test("plan agent preserves global edit denies after per-agent edit ask", async () => {
const plan = await get(
{
permission: {
edit: {
".kilo/plans/private.md": "deny",
},
},
agent: {
plan: {
permission: {
edit: "ask",
},
},
},
},
)
expectPlan(plan)
expect(Permission.evaluate("edit", ".kilo/plans/private.md", plan!.permission).action).toBe("deny")
})
test("plan agent preserves global non-edit denies before broader allows", async () => {
const plan = await get({
permission: {
bash: {
"rm *": "deny",
"*": "allow",
},
},
})
expect(Permission.evaluate("bash", "rm -rf x", plan!.permission).action).toBe("deny")
expect(Permission.evaluate("bash", "ls", plan!.permission).action).toBe("allow")
})
test("plan agent preserves per-agent tool allows with a wildcard deny", async () => {
const plan = await get(
{
agent: {
plan: {
permission: {
"*": "deny",
read: "allow",
glob: "allow",
edit: "ask",
},
},
},
},
)
expectPlan(plan)
expect(Permission.evaluate("read", "src/output.log", plan!.permission).action).toBe("allow")
expect(Permission.evaluate("glob", "*", plan!.permission).action).toBe("allow")
})
test("marketplace architect honors plan allow after wildcard edit deny", async () => {
const architect = await get(
{
agent: {
architect: {
mode: "primary",
options: {
displayName: "Architect",
},
permission: {
"*": "deny",
read: "allow",
glob: "allow",
edit: {
"*": "deny",
".kilo/plans/*": "allow",
},
},
},
},
},
"architect",
)
expectPlan(architect)
expect(architect!.name).toBe("architect")
expect(architect!.displayName).toBe("Architect")
expect(Permission.evaluate("read", "src/output.log", architect!.permission).action).toBe("allow")
expect(Permission.evaluate("glob", "*", architect!.permission).action).toBe("allow")
})
test("non-planning agents retain per-agent edit permissions", async () => {
const code = await get(
{
agent: {
code: {
permission: {
edit: "ask",
},
},
},
},
"code",
)
expect(code).toBeDefined()
expect(Permission.evaluate("edit", "src/output.log", code!.permission).action).toBe("ask")
})
test("system utility agents ignore per-agent permission allows", async () => {
await using tmp = await tmpdir({
config: {
@@ -1,7 +1,7 @@
import { expect } from "bun:test"
import { Auth } from "@/auth"
import { remove } from "@/kilocode/auth/remove"
import { ConnectorSchema } from "@opencode-ai/core/connector/schema"
import { IntegrationSchema } from "@opencode-ai/core/integration/schema"
import { Credential } from "@opencode-ai/core/credential"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event"
@@ -24,24 +24,21 @@ it.effect("legacy provider logout removes every Core credential", () =>
Effect.gen(function* () {
state.removed = false
const service = yield* Credential.Service
const connectorID = ConnectorSchema.ID.make("anthropic")
const integrationID = IntegrationSchema.ID.make("anthropic")
yield* service.create({
connectorID,
methodID: ConnectorSchema.MethodID.make("api-key"),
integrationID,
label: "first",
value: new Credential.Key({ type: "key", key: "first" }),
})
yield* service.create({
connectorID,
methodID: ConnectorSchema.MethodID.make("api-key"),
integrationID,
label: "second",
value: new Credential.Key({ type: "key", key: "second" }),
})
yield* remove("anthropic")
expect(yield* service.forConnector(connectorID)).toEqual([])
expect(yield* service.active(connectorID)).toBeUndefined()
expect(yield* service.list(integrationID)).toEqual([])
expect(state.removed).toBe(true)
}),
)
@@ -73,6 +73,7 @@ mock.module("@/kilocode/help-command", () => ({
for (const path of [
"@/kilocode/cli/cmd/console",
"@/kilocode/cli/cmd/cloud",
"@/kilocode/cli/cmd/roll-call",
"@/kilocode/cli/cmd/profile",
"@/kilocode/cli/cmd/daemon",
@@ -82,6 +83,7 @@ for (const path of [
]) {
mock.module(path, () => ({
KiloConsoleCommand: { command: "console", handler() {} },
CloudCommand: { command: "cloud", handler() {} },
RollCallCommand: { command: "roll-call", handler() {} },
ProfileCommand: { command: "profile", handler() {} },
DaemonCommand: { command: "daemon", handler() {} },
@@ -0,0 +1,33 @@
// kilocode_change - new file
// K1 W1: verify `buildInstanceAdvertisement`'s payload shape as real behavior.
//
// The `RemoteCommand` handler itself is a CLI entry point that calls
// `bootstrap(process.cwd(), async () => { ... })` and then awaits an abort
// signal that never resolves in a test — it cannot be driven end-to-end.
// `buildInstanceAdvertisement` is extracted from the handler specifically so
// the advertised payload is independently testable as real behavior, not via
// a source-text/regex assertion on the handler's structure.
import { describe, expect, test } from "bun:test"
import { buildInstanceAdvertisement } from "../../../../src/cli/cmd/remote"
describe("RemoteCommand instance advertisement (K1 W1)", () => {
test("buildInstanceAdvertisement resolves name/projectName/version from the directory and installation version", () => {
const advertisement = buildInstanceAdvertisement("/Users/igor/projects/my-app")
expect(advertisement.projectName).toBe("my-app")
expect(typeof advertisement.name).toBe("string")
expect(advertisement.name.length).toBeGreaterThan(0)
expect(typeof advertisement.version).toBe("string")
})
test("buildInstanceAdvertisement truncates an overlong project directory name to 64 chars", () => {
const longName = "a".repeat(100)
const advertisement = buildInstanceAdvertisement(`/Users/igor/projects/${longName}`)
expect(advertisement.projectName.length).toBeLessThanOrEqual(64)
})
test("buildInstanceAdvertisement falls back to the full directory when basename is empty (root path)", () => {
const advertisement = buildInstanceAdvertisement("/")
expect(advertisement.projectName).toBe("/")
})
})
@@ -3,7 +3,6 @@ import type { KiloClient } from "@kilocode/sdk/v2"
import { memoryRow } from "@/kilocode/cli/cmd/tui/component/memory-status"
import { runMemoryCommand } from "@/kilocode/cli/cmd/tui/memory-command"
import { MemoryTuiEvents } from "@/kilocode/cli/cmd/tui/memory-events"
import { MemoryTuiMeta } from "@/kilocode/cli/cmd/tui/memory-meta"
import { MemoryTuiState } from "@/kilocode/cli/cmd/tui/memory-state"
type Handler = (event: {
@@ -64,16 +63,16 @@ describe("memory TUI command parser", () => {
])
})
test("auto-save, verbose, and purge commands call explicit endpoints", async () => {
test("auto-save and purge commands call explicit endpoints", async () => {
const shown: string[] = []
const calls: unknown[] = []
const state = { autoConsolidate: true, verbose: false }
const state = { autoConsolidate: true }
const client = {
memory: {
status: async () => ({ data: { state } }),
configure: async (input: unknown) => {
calls.push(input)
return { data: { state: { autoConsolidate: false, verbose: true } } }
return { data: { state: { autoConsolidate: false } } }
},
purge: async (input: unknown) => {
calls.push(input)
@@ -96,17 +95,15 @@ describe("memory TUI command parser", () => {
}
await runMemoryCommand({ ...base, text: "/memory auto off" })
await runMemoryCommand({ ...base, text: "/memory verbose on" })
await runMemoryCommand({ ...base, text: "/memory auto status" })
await runMemoryCommand({ ...base, text: "/memory purge" })
await runMemoryCommand({ ...base, text: "/memory purge confirm" })
expect(shown[0]).toBe("Memory auto-save off")
expect(shown[1]).toBe("Memory verbose on")
expect(shown[2]).toContain("Missing auto mode")
expect(shown[3]).toContain("Purge requires confirmation")
expect(shown[4]).toBe("Memory purged")
expect(calls).toEqual([{ autoConsolidate: false }, { verbose: true }, { confirm: true }])
expect(shown[1]).toContain("Missing auto mode")
expect(shown[2]).toContain("Purge requires confirmation")
expect(shown[3]).toBe("Memory purged")
expect(calls).toEqual([{ autoConsolidate: false }, { confirm: true }])
})
test("status opens overview dialog", async () => {
@@ -133,6 +130,31 @@ describe("memory TUI command parser", () => {
expect(shown).toEqual([])
})
test("inspect reveals the memory folder", async () => {
const opened: string[] = []
const shown: string[] = []
const client = {
memory: {
status: async () => ({ data: { root: "/tmp/kilo-memory", state: { enabled: true } } }),
},
} as unknown as KiloClient
await runMemoryCommand({
text: "/memory inspect",
client,
toast: { show: (input) => shown.push(input.message) },
inspect(root) {
opened.push(root)
},
show() {},
status() {},
usage() {},
})
expect(opened).toEqual(["/tmp/kilo-memory"])
expect(shown).toEqual(["Memory folder: /tmp/kilo-memory"])
})
test("bare memory command opens help", async () => {
const calls: unknown[] = []
const client = { memory: {} } as unknown as KiloClient
@@ -204,7 +226,7 @@ describe("memory TUI command parser", () => {
test("memory commands route to session directory when no workspace is active", async () => {
const calls: unknown[] = []
const state = { autoConsolidate: false, verbose: false }
const state = { autoConsolidate: false }
const client = {
memory: {
configure: async (input: unknown) => {
@@ -222,7 +244,6 @@ describe("memory TUI command parser", () => {
}
await runMemoryCommand({ ...base, text: "/memory auto off", directory: "/repo/packages/opencode" })
await runMemoryCommand({ ...base, text: "/memory verbose on", directory: "/repo/packages/opencode" })
await runMemoryCommand({
...base,
text: "/memory auto off",
@@ -232,7 +253,6 @@ describe("memory TUI command parser", () => {
expect(calls).toEqual([
{ directory: "/repo/packages/opencode", autoConsolidate: false },
{ directory: "/repo/packages/opencode", verbose: true },
{ workspace: "wrk_123", autoConsolidate: false },
])
})
@@ -288,63 +308,38 @@ describe("memory TUI events", () => {
})
})
describe("memory TUI metadata", () => {
test("reads typed verbose and activity state", () => {
expect(MemoryTuiState.verbose({ verbose: true })).toBe(true)
expect(MemoryTuiState.verbose(undefined)).toBe(false)
describe("memory TUI state", () => {
test("tracks active memory", () => {
expect(MemoryTuiState.active({ markers: 1, saved: false })).toBe(true)
expect(MemoryTuiState.active({ markers: 0, saved: true })).toBe(true)
expect(MemoryTuiState.active({ markers: 0, saved: false })).toBe(false)
expect(MemoryTuiMeta.items({ items: ["first", 1, "second"] })).toEqual(["first", "second"])
expect(MemoryTuiMeta.items({})).toEqual([])
})
})
describe("memory sidebar row", () => {
test("shows loading, unavailable, and disabled states", () => {
expect(memoryRow({ loading: true, active: false, verbose: false })).toEqual({
expect(memoryRow({ loading: true, active: false })).toEqual({
label: "Loading",
tone: "muted",
})
expect(memoryRow({ active: false, verbose: false })).toEqual({
expect(memoryRow({ active: false })).toEqual({
label: "Unavailable",
tone: "error",
})
expect(memoryRow({ enabled: false, active: true, verbose: true, flash: "recalled 3" })).toEqual({
expect(memoryRow({ enabled: false, active: true })).toEqual({
label: "Disabled",
tone: "muted",
})
})
test("uses muted and green dots for inactive and active sessions", () => {
expect(memoryRow({ enabled: true, active: false, verbose: false })).toEqual({
expect(memoryRow({ enabled: true, active: false })).toEqual({
label: "Enabled",
tone: "muted",
})
expect(memoryRow({ enabled: true, active: true, verbose: false })).toEqual({
expect(memoryRow({ enabled: true, active: true })).toEqual({
label: "Enabled",
tone: "success",
})
})
test("adds verbose event captions without changing the activity tone", () => {
expect(memoryRow({ enabled: true, active: false, verbose: true, flash: "recalled 3" })).toEqual({
label: "Enabled",
tone: "muted",
caption: "recalled 3",
})
expect(memoryRow({ enabled: true, active: true, verbose: true, flash: "saved 2" })).toEqual({
label: "Enabled",
tone: "success",
caption: "saved 2",
})
})
test("omits verbose event captions when verbose is disabled", () => {
expect(memoryRow({ enabled: true, active: true, verbose: false, flash: "loaded" })).toEqual({
label: "Enabled",
tone: "success",
caption: undefined,
})
})
})
@@ -3,21 +3,10 @@ import { expect, spyOn, test } from "bun:test"
import { RGBA } from "@opentui/core"
import { testRender } from "@opentui/solid"
import type { TuiPluginApi } from "@kilocode/plugin/tui"
import type { Event, GlobalEvent, Message, Part, Session } from "@kilocode/sdk/v2"
import type { Event, Message, Part, Session } from "@kilocode/sdk/v2"
import { createSignal } from "solid-js"
import { ArgsProvider } from "@tui/context/args"
import { ExitProvider } from "@tui/context/exit"
import { KVProvider } from "@tui/context/kv"
import { ProjectProvider } from "@tui/context/project"
import { SDKProvider } from "@tui/context/sdk"
import { SyncProvider } from "@tui/context/sync"
import { ToastProvider } from "@tui/ui/toast"
import { MemorySidebar } from "@/kilocode/cli/cmd/tui/component/memory-status"
import { MemoryMessageMeta, MemorySessionTui } from "@/kilocode/cli/cmd/tui/routes/session/memory"
import { Global } from "@opencode-ai/core/global"
import { createEventSource, createFetch, directory, json } from "../../../../fixture/tui-sdk"
import { tmpdir } from "../../../../fixture/fixture"
import { TestTuiContexts } from "../../../../fixture/tui-environment"
import { directory } from "../../../../fixture/tui-sdk"
const id = "ses_memory_status"
@@ -48,10 +37,6 @@ function event(sessionID?: string, count?: number): Extract<Event, { type: "memo
}
}
function global(payload: Event): GlobalEvent {
return { directory, project: "proj_test", payload }
}
async function wait(fn: () => boolean, timeout = 2_000) {
const start = Date.now()
while (!fn()) {
@@ -60,115 +45,6 @@ async function wait(fn: () => boolean, timeout = 2_000) {
}
}
function Probe(props: { sessionID: string }) {
const verbose = MemorySessionTui.verbose({ sessionID: () => props.sessionID })
return <text>{verbose() ? "verbose" : "quiet"}</text>
}
test("session memory status refetches live and ignores other sessions", async () => {
await using tmp = await tmpdir()
const prior = Global.Path.state
Global.Path.state = tmp.path
await Bun.write(`${tmp.path}/kv.json`, "{}")
const events = createEventSource()
const state = { verbose: false }
const calls = { count: 0 }
const fetch = createFetch((url) => {
if (url.pathname === "/session") return json([session])
if (url.pathname !== "/memory/status") return
calls.count += 1
return json({ state: { verbose: state.verbose } })
})
try {
const app = await testRender(() => (
<TestTuiContexts paths={{ state: tmp.path }}>
<ArgsProvider>
<ExitProvider exit={() => {}}>
<KVProvider>
<ToastProvider>
<SDKProvider url="http://test" directory={directory} fetch={fetch.fetch} events={events.source}>
<ProjectProvider>
<SyncProvider>
<Probe sessionID={id} />
</SyncProvider>
</ProjectProvider>
</SDKProvider>
</ToastProvider>
</KVProvider>
</ExitProvider>
</ArgsProvider>
</TestTuiContexts>
))
try {
await wait(() => calls.count === 1 && app.captureCharFrame().includes("quiet"))
events.emit(global(event("ses_other")))
await Bun.sleep(30)
expect(calls.count).toBe(1)
state.verbose = true
events.emit(global(event(id)))
await wait(() => calls.count === 2 && app.captureCharFrame().includes("verbose"))
} finally {
app.renderer.destroy()
}
} finally {
Global.Path.state = prior
}
})
test("message memory metadata reacts to verbose changes and bounds snippets", async () => {
const [verbose, setVerbose] = createSignal(false)
const [parts, setParts] = createSignal<Part[]>([])
const first = "a".repeat(100)
const part = {
id: "part_memory_recall",
sessionID: id,
messageID: "msg_memory_recall",
type: "text",
text: "",
metadata: { kiloMemory: { type: "recall", count: 3, items: [first, "second", "third"] } },
} satisfies Part
setParts([part])
const app = await testRender(
() => (
<text>
<MemoryMessageMeta parts={parts()} color={RGBA.fromHex("#ffffff")} verbose={verbose} />
</text>
),
{ width: 200, height: 3 },
)
try {
await app.renderOnce()
expect(app.captureCharFrame()).toContain("memory · recalled 3")
expect(app.captureCharFrame()).not.toContain("second")
setVerbose(true)
await app.renderOnce()
expect(app.captureCharFrame()).toContain("a".repeat(80))
expect(app.captureCharFrame()).not.toContain("a".repeat(81))
expect(app.captureCharFrame()).toContain("second")
expect(app.captureCharFrame()).not.toContain("third")
setVerbose(false)
await app.renderOnce()
expect(app.captureCharFrame()).not.toContain("second")
setParts([
{
...part,
id: "part_memory_startup",
metadata: { kiloMemory: { type: "startup", count: 2, tokens: 40 } },
},
])
await app.renderOnce()
expect(app.captureCharFrame()).toContain("memory · Startup Context")
expect(app.captureCharFrame()).not.toContain("recalled")
} finally {
app.renderer.destroy()
}
})
type Handler = (event: Event) => void
function bus() {
@@ -186,7 +62,7 @@ function bus() {
}
}
test("sidebar refetches status and scopes recall and save flashes", async () => {
test("sidebar refetches status and scopes save activity", async () => {
const [parts, setParts] = createSignal<Part[]>([])
const events = bus()
const calls = { count: 0 }
@@ -203,7 +79,7 @@ test("sidebar refetches status and scopes recall and save flashes", async () =>
memory: {
status: async () => {
calls.count += 1
return { data: { state: { enabled: true, verbose: true } } }
return { data: { state: { enabled: true } } }
},
},
},
@@ -232,16 +108,15 @@ test("sidebar refetches status and scopes recall and save flashes", async () =>
metadata: { kiloMemory: { type: "recall", count: 2 } },
},
])
await wait(() => app.captureCharFrame().includes("recalled 2"))
await Bun.sleep(5_100)
await app.renderOnce()
expect(app.captureCharFrame()).not.toContain("recalled 2")
events.emit(event("ses_other", 4))
await wait(() => calls.count === 2)
expect(app.captureCharFrame()).not.toContain("saved 4")
events.emit(event(id, 3))
await wait(() => calls.count === 3 && app.captureCharFrame().includes("saved 3"))
await wait(() => calls.count === 3)
expect(app.captureCharFrame()).not.toContain("saved 3")
const before = clear.mock.calls.length
app.renderer.destroy()
expect(clear.mock.calls.length).toBeGreaterThan(before)
@@ -0,0 +1,540 @@
import { expect } from "bun:test"
import { Agent } from "@/agent/agent"
import { Auth } from "@/auth"
import { Config } from "@/config/config"
import type { AgentSendRequest, AgentStartRequest, MessageResult } from "@/kilocode/cloud/contracts"
import { CloudCommands } from "@/kilocode/cloud/commands"
import { CloudError } from "@/kilocode/cloud/errors"
import { Git } from "@/git"
import { Effect, Layer } from "effect"
import { TestInstance } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
const SESSION = "agent_12345678-1234-1234-1234-123456789abc"
const MESSAGE = "msg_018f1e2d3c4bAbCdEfGhIjKlMn"
const TOKEN = "command-test-token"
const ORG = "11111111-1111-4111-8111-111111111111"
const auth = Layer.mock(Auth.Service)({
get: (id) =>
Effect.succeed(
id === "kilo"
? new Auth.Oauth({
type: "oauth",
access: TOKEN,
refresh: "test-refresh",
expires: Date.now() + 60_000,
accountId: ORG,
})
: undefined,
),
})
const it = testEffect(Layer.mergeAll(Agent.defaultLayer, Config.defaultLayer, Git.defaultLayer, auth))
const run = Effect.fn("CloudCommandTest.git")(function* (cwd: string, ...args: string[]) {
const git = yield* Git.Service
const result = yield* git.run(args, { cwd })
if (result.exitCode === 0) return
yield* Effect.die(new Error(result.stderr.toString("utf8")))
})
it.instance(
"assembles the default start request from Kilo state and the current repository",
() =>
Effect.acquireUseRelease(
Effect.sync(() =>
Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname.endsWith("/models")) {
return Response.json({ data: [{ id: "anthropic/command-model", supported_parameters: ["tools"] }] })
}
if (url.pathname.endsWith("/defaults")) {
return Response.json({ defaultModel: "anthropic/command-model" })
}
return new Response(null, { status: 404 })
},
}),
),
(server) =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* run(test.directory, "remote", "add", "origin", "git@github.com:Kilo-Org/kilocode.git")
const requests: AgentStartRequest[] = []
const keys: string[] = []
const output: string[] = []
const response = {
cloudAgentSessionId: SESSION,
kiloSessionId: "ses_command_test",
messageId: MESSAGE,
delivery: "queued" as const,
}
const result = yield* CloudCommands.start(
{
cwd: test.directory,
prompt: "Inspect the current repository",
},
{
env: { KILO_API_URL: server.url.origin },
make: (options) => {
keys.push(options.apiKey)
return {
async start(input) {
requests.push(input)
return response
},
async send() {
throw new Error("unused send")
},
async getMessageResult() {
throw new Error("unused result")
},
}
},
write: (text) => output.push(text),
},
)
expect(result).toEqual(response)
expect(keys).toEqual([TOKEN])
expect(requests).toHaveLength(1)
expect(requests[0]).toEqual({
message: { prompt: "Inspect the current repository" },
agent: { mode: "plan", model: "anthropic/command-model" },
repository: { type: "github", repo: "Kilo-Org/kilocode" },
options: {
createdOnPlatform: "kilo-cli",
kilocodeOrganizationId: ORG,
},
})
expect(requests[0]?.repository).not.toHaveProperty("branch")
expect(output).toEqual([JSON.stringify(response) + "\n"])
}),
(server) => Effect.promise(() => server.stop(true)),
),
{
git: true,
config: {
default_agent: "plan",
agent: { plan: { model: "kilo/anthropic/command-model" } },
},
},
)
it.instance(
"streams WebSocket events when --stream is passed",
() =>
Effect.acquireUseRelease(
Effect.sync(() =>
Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname.endsWith("/models")) {
return Response.json({ data: [{ id: "anthropic/command-model", supported_parameters: ["tools"] }] })
}
if (url.pathname.endsWith("/defaults")) {
return Response.json({ defaultModel: "anthropic/command-model" })
}
return new Response(null, { status: 404 })
},
}),
),
(server) =>
Effect.gen(function* () {
const test = yield* TestInstance
const output: string[] = []
const ticketCalls: { cloudAgentSessionId: string; organizationId?: string }[] = []
const streamCalls: string[] = []
const response = {
cloudAgentSessionId: SESSION,
kiloSessionId: "ses_stream_test",
messageId: MESSAGE,
delivery: "queued" as const,
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=inlined",
}
const result = yield* CloudCommands.start(
{
cwd: test.directory,
prompt: "Inspect the repository",
repo: "Kilo-Org/kilocode",
stream: true,
},
{
env: { KILO_API_URL: server.url.origin },
make: () => ({
async start() {
return response
},
async send() {
throw new Error("unused")
},
async getMessageResult() {
throw new Error("unused")
},
}),
createStreamTicketClient: () => ({
async fetchTicket(input) {
ticketCalls.push(input)
return { ticket: "should-not-be-used", expiresAt: 0 }
},
}),
streamAgentEvents: async (options) => {
streamCalls.push(options.streamUrl)
await options.writeLine('{"event":"one"}')
await options.writeLine('{"streamEventType":"complete","data":{"exitCode":0}}')
},
write: (text) => output.push(text),
},
)
expect(result).toEqual(response)
expect(ticketCalls).toEqual([])
expect(streamCalls).toEqual(["/stream?cloudAgentSessionId=agent_123&ticket=inlined"])
expect(output).toEqual([
JSON.stringify({ ...response, streamUrl: undefined }) + "\n",
'{"event":"one"}\n',
'{"streamEventType":"complete","data":{"exitCode":0}}\n',
])
}),
(server) => Effect.promise(() => server.stop(true)),
),
{
git: true,
config: {
default_agent: "plan",
agent: { plan: { model: "kilo/anthropic/command-model" } },
},
},
)
it.instance(
"fetches a stream ticket when the response omits streamUrl",
() =>
Effect.acquireUseRelease(
Effect.sync(() =>
Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname.endsWith("/models")) {
return Response.json({ data: [{ id: "anthropic/command-model", supported_parameters: ["tools"] }] })
}
if (url.pathname.endsWith("/defaults")) {
return Response.json({ defaultModel: "anthropic/command-model" })
}
return new Response(null, { status: 404 })
},
}),
),
(server) =>
Effect.gen(function* () {
const test = yield* TestInstance
const output: string[] = []
const ticketCalls: { cloudAgentSessionId: string; organizationId?: string }[] = []
const streamCalls: string[] = []
const response = {
cloudAgentSessionId: SESSION,
kiloSessionId: "ses_stream_test",
messageId: MESSAGE,
delivery: "queued" as const,
}
yield* CloudCommands.start(
{
cwd: test.directory,
prompt: "Inspect the repository",
repo: "Kilo-Org/kilocode",
orgID: ORG,
stream: true,
},
{
env: { KILO_API_URL: server.url.origin },
make: () => ({
async start() {
return response
},
async send() {
throw new Error("unused")
},
async getMessageResult() {
throw new Error("unused")
},
}),
createStreamTicketClient: () => ({
async fetchTicket(input) {
ticketCalls.push(input)
return { ticket: "derived-tok", expiresAt: 1234567890 }
},
}),
streamAgentEvents: async (options) => {
streamCalls.push(options.streamUrl)
await options.writeLine('{"event":"derived"}')
},
write: (text) => output.push(text),
},
)
expect(ticketCalls).toEqual([{ cloudAgentSessionId: SESSION, organizationId: ORG }])
expect(streamCalls).toEqual([`/stream?cloudAgentSessionId=${SESSION}&ticket=derived-tok`])
expect(output).toEqual([JSON.stringify(response) + "\n", '{"event":"derived"}\n'])
}),
(server) => Effect.promise(() => server.stop(true)),
),
{
git: true,
config: {
default_agent: "plan",
agent: { plan: { model: "kilo/anthropic/command-model" } },
},
},
)
it.instance(
"keeps admission successful when stream ticket acquisition fails",
() =>
Effect.acquireUseRelease(
Effect.sync(() =>
Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname.endsWith("/models")) {
return Response.json({ data: [{ id: "anthropic/command-model", supported_parameters: ["tools"] }] })
}
if (url.pathname.endsWith("/defaults")) {
return Response.json({ defaultModel: "anthropic/command-model" })
}
return new Response(null, { status: 404 })
},
}),
),
(server) =>
Effect.gen(function* () {
const test = yield* TestInstance
const output: string[] = []
const response = {
cloudAgentSessionId: SESSION,
kiloSessionId: "ses_stream_test",
messageId: MESSAGE,
delivery: "queued" as const,
}
const result = yield* CloudCommands.start(
{
cwd: test.directory,
prompt: "Inspect the repository",
repo: "Kilo-Org/kilocode",
stream: true,
},
{
env: { KILO_API_URL: server.url.origin },
make: () => ({
async start() {
return response
},
async send() {
throw new Error("unused")
},
async getMessageResult() {
throw new Error("unused")
},
}),
createStreamTicketClient: () => ({
async fetchTicket() {
throw new CloudError("Unable to obtain stream ticket")
},
}),
streamAgentEvents: async () => {
throw new Error("unused")
},
write: (text) => output.push(text),
},
)
expect(result).toEqual(response)
expect(output).toEqual([
JSON.stringify(response) + "\n",
JSON.stringify({ streamEventType: "error", data: { message: "Unable to obtain stream ticket" } }) + "\n",
])
}),
(server) => Effect.promise(() => server.stop(true)),
),
{
git: true,
config: {
default_agent: "plan",
agent: { plan: { model: "kilo/anthropic/command-model" } },
},
},
)
it.instance(
"keeps a successful admission successful when the follow-up stream fails",
() =>
Effect.acquireUseRelease(
Effect.sync(() =>
Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname.endsWith("/models")) {
return Response.json({ data: [{ id: "anthropic/command-model", supported_parameters: ["tools"] }] })
}
if (url.pathname.endsWith("/defaults")) {
return Response.json({ defaultModel: "anthropic/command-model" })
}
return new Response(null, { status: 404 })
},
}),
),
(server) =>
Effect.gen(function* () {
const test = yield* TestInstance
const output: string[] = []
const response = {
cloudAgentSessionId: SESSION,
kiloSessionId: "ses_stream_test",
messageId: MESSAGE,
delivery: "queued" as const,
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=inlined",
}
const result = yield* CloudCommands.start(
{
cwd: test.directory,
prompt: "Inspect the repository",
repo: "Kilo-Org/kilocode",
stream: true,
},
{
env: { KILO_API_URL: server.url.origin },
make: () => ({
async start() {
return response
},
async send() {
throw new Error("unused")
},
async getMessageResult() {
throw new Error("unused")
},
}),
streamAgentEvents: async () => {
throw new Error("stream failed after admission")
},
write: (text) => output.push(text),
},
)
expect(result).toEqual(response)
expect(output).toEqual([
JSON.stringify({ ...response, streamUrl: undefined }) + "\n",
JSON.stringify({ streamEventType: "error", data: { message: "Cloud Agent stream failed" } }) + "\n",
])
}),
(server) => Effect.promise(() => server.stop(true)),
),
{
git: true,
config: {
default_agent: "plan",
agent: { plan: { model: "kilo/anthropic/command-model" } },
},
},
)
it.instance("sends follow-ups, prints status without assistant content, and applies the result exit code", () =>
Effect.gen(function* () {
const output: string[] = []
const exits: number[] = []
const sends: AgentSendRequest[] = []
const sent = {
cloudAgentSessionId: SESSION,
status: "started" as const,
streamUrl: "wss://cloud-agent.example/stream",
messageId: MESSAGE,
delivery: "queued" as const,
}
const results: MessageResult[] = [
{
cloudAgentSessionId: SESSION,
messageId: MESSAGE,
status: "completed",
createdAt: 1,
terminalAt: 2,
assistant: { messageId: "assistant_1", text: "done" },
},
{
cloudAgentSessionId: SESSION,
messageId: MESSAGE,
status: "failed",
createdAt: 1,
terminalAt: 2,
failure: { retryable: false },
},
]
const deps = {
env: { KILO_ORG_ID: "not-relevant-to-existing-sessions" },
make: () => ({
async start() {
throw new Error("unused start")
},
async send(input: AgentSendRequest) {
sends.push(input)
return sent
},
async getMessageResult() {
const result = results.shift()
if (!result) throw new Error("missing test result")
return result
},
}),
write: (text: string) => output.push(text),
exit: (code: number) => exits.push(code),
}
yield* CloudCommands.send({ sessionID: SESSION, prompt: "Continue" }, deps)
yield* CloudCommands.status({ sessionID: SESSION, messageID: MESSAGE }, deps)
yield* CloudCommands.result({ sessionID: SESSION, messageID: MESSAGE }, deps)
expect(sends).toEqual([{ cloudAgentSessionId: SESSION, message: { prompt: "Continue" } }])
expect(output).toEqual([
JSON.stringify({ ...sent, streamUrl: undefined }) + "\n",
JSON.stringify({
cloudAgentSessionId: SESSION,
messageId: MESSAGE,
status: "completed",
createdAt: 1,
terminalAt: 2,
}) + "\n",
JSON.stringify({
cloudAgentSessionId: SESSION,
messageId: MESSAGE,
status: "failed",
createdAt: 1,
terminalAt: 2,
failure: { retryable: false },
}) + "\n",
])
expect(exits).toEqual([3])
const error = yield* CloudCommands.send(
{ sessionID: SESSION, prompt: "Do not duplicate" },
{ ...deps, write: () => Promise.reject(new Error("closed output")) },
).pipe(Effect.flip)
expect(error.message).toBe(
"Cloud Agent send was admitted but output could not be written; do not retry automatically",
)
}),
)
@@ -0,0 +1,436 @@
import { expect } from "bun:test"
import { Effect, Layer, Redacted, Ref } from "effect"
import { Agent } from "@/agent/agent"
import { Auth } from "@/auth"
import { Config } from "@/config/config"
import { CloudAuth } from "@/kilocode/cloud/auth"
import { CloudCatalog } from "@/kilocode/cloud/catalog"
import { CloudDefaults } from "@/kilocode/cloud/defaults"
import { MAX_CLOUD_AGENT_RESPONSE_BYTES } from "@/kilocode/cloud/response-json"
import { testEffect } from "../../lib/effect"
const it = testEffect(Layer.mergeAll(Agent.defaultLayer, Config.defaultLayer))
type RequestInfo = {
readonly authorization: string | null
readonly feature: string | null
readonly organization: string | null
readonly path: string
}
const oauth = (token: string, organizationID: string) =>
new Auth.Oauth({
type: "oauth",
access: token,
refresh: "test-refresh",
expires: Date.now() + 60_000,
accountId: organizationID,
})
const authLayer = (info: Auth.Info) =>
Layer.mock(Auth.Service)({
get: (id) => Effect.succeed(id === "kilo" ? info : undefined),
})
const stateLayer = (state: CloudDefaults.ModelStateInfo) =>
Layer.mock(CloudDefaults.ModelState)({
get: () => Effect.succeed(state),
})
const state = (input: Partial<CloudDefaults.ModelStateInfo> = {}): CloudDefaults.ModelStateInfo => ({
model: {},
recent: [],
favorite: [],
variant: {},
...input,
})
function withCatalog<A, E, R>(
models: readonly string[],
defaultModel: string,
use: (url: URL, requests: RequestInfo[]) => Effect.Effect<A, E, R>,
) {
const requests: RequestInfo[] = []
return Effect.acquireUseRelease(
Effect.sync(() =>
Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
const url = new URL(request.url)
requests.push({
authorization: request.headers.get("authorization"),
feature: request.headers.get("x-kilocode-feature"),
organization: request.headers.get("x-kilocode-organizationid"),
path: url.pathname,
})
if (url.pathname.endsWith("/models")) {
return Response.json({ data: models.map((id) => ({ id, supported_parameters: ["tools"] })) })
}
if (url.pathname.endsWith("/defaults")) return Response.json({ defaultModel })
return new Response(null, { status: 404 })
},
}),
),
(server) => use(server.url, requests),
(server) => Effect.promise(() => server.stop(true)),
)
}
it.instance("routes URL-scoped credentials to their catalog origin", () => {
const token = "https://catalog.example.test:scoped-token"
const requests: RequestInfo[] = []
return Effect.gen(function* () {
const catalog = yield* CloudCatalog.Service
expect(yield* catalog.models({ token: Redacted.make(token) })).toEqual(["anthropic/scoped"])
expect(requests).toEqual([
{
authorization: `Bearer ${token}`,
feature: "kilo-cli",
organization: null,
path: "/api/openrouter/models",
},
])
}).pipe(
Effect.provide(
CloudCatalog.layer({
env: {},
fetch: async (request) => {
const url = new URL(request.url)
expect(url.origin).toBe("https://catalog.example.test")
requests.push({
authorization: request.headers.get("authorization"),
feature: request.headers.get("x-kilocode-feature"),
organization: request.headers.get("x-kilocode-organizationid"),
path: url.pathname,
})
return Response.json({ data: [{ id: "anthropic/scoped", supported_parameters: ["tools"] }] })
},
}),
),
)
})
it.instance("returns only tool-capable text-output models", () =>
Effect.gen(function* () {
const catalog = yield* CloudCatalog.Service
const models = yield* catalog.models({ token: Redacted.make("stored-token") })
expect(models).toEqual(["anthropic/code"])
}).pipe(
Effect.provide(
CloudCatalog.layer({
fetch: async () =>
Response.json({
data: [
{
id: "anthropic/code",
architecture: { output_modalities: ["text"] },
supported_parameters: ["tools"],
},
{
id: "image/generator",
architecture: { output_modalities: ["image"] },
supported_parameters: ["tools"],
},
{
id: "anthropic/chat",
architecture: { output_modalities: ["text"] },
supported_parameters: ["temperature"],
},
{ id: "anthropic/unknown" },
],
}),
}),
),
),
)
it.instance("rejects oversized catalog responses", () =>
Effect.gen(function* () {
const catalog = yield* CloudCatalog.Service
const error = yield* catalog.models({ token: Redacted.make("stored-token") }).pipe(Effect.flip)
expect(error).toMatchObject({ _tag: "CloudCatalogError", kind: "schema" })
}).pipe(
Effect.provide(
CloudCatalog.layer({
fetch: async () =>
Response.json({ data: [] }, { headers: { "content-length": String(MAX_CLOUD_AGENT_RESPONSE_BYTES + 1) } }),
}),
),
),
)
it.instance(
"explicit overrides beat environment and saved defaults without persisting",
() =>
withCatalog(["anthropic/explicit", "anthropic/default"], "anthropic/default", (url, requests) =>
Effect.gen(function* () {
const savedID = "22222222-2222-4222-8222-222222222222"
const envID = "33333333-3333-4333-8333-333333333333"
const explicitID = "44444444-4444-4444-8444-444444444444"
const stored = oauth("stored-token", savedID)
const current = yield* Ref.make<Auth.Info>(stored)
const auth = Layer.mock(Auth.Service)({
get: (id) => (id === "kilo" ? Ref.get(current) : Effect.succeed(undefined)),
set: (id, info) => (id === "kilo" ? Ref.set(current, info) : Effect.void),
})
const resolved = yield* Effect.gen(function* () {
const result = yield* CloudDefaults.resolve({
mode: "debug",
model: "kilo/anthropic/explicit",
orgID: explicitID,
env: {
KILO_API_KEY: "ignored-env-token",
KILO_ORG_ID: envID,
},
})
const service = yield* Auth.Service
expect(yield* service.get("kilo")).toEqual(stored)
return result
}).pipe(
Effect.provide(
Layer.mergeAll(
auth,
stateLayer(
state({
model: { debug: { providerID: "kilo", modelID: "anthropic/saved" } },
}),
),
CloudCatalog.layer({ env: { KILO_API_URL: url.origin } }),
),
),
)
expect(resolved).toMatchObject({
mode: "debug",
model: "anthropic/explicit",
organizationID: explicitID,
})
expect(requests.map((request) => request.path)).toEqual([`/api/organizations/${explicitID}/models`])
expect(
requests.every(
(request) =>
request.path.startsWith(`/api/organizations/${explicitID}/`) &&
request.authorization === "Bearer stored-token" &&
request.feature === "kilo-cli" &&
request.organization === explicitID,
),
).toBe(true)
}),
),
{
config: {
default_agent: "plan",
model: "kilo/anthropic/repository",
agent: { plan: { model: "kilo/anthropic/mode" } },
},
},
)
it.instance(
"skips a stale saved model and uses the available repository model",
() =>
withCatalog(
["anthropic/repository", "anthropic/recent", "anthropic/default"],
"anthropic/default",
(url, requests) =>
Effect.gen(function* () {
const resolved = yield* CloudDefaults.resolve()
expect(resolved.mode).toBe("code")
expect(resolved.model).toBe("anthropic/repository")
expect(requests.map((request) => request.path)).toEqual(["/api/openrouter/models"])
expect(requests.every((request) => request.organization === null)).toBe(true)
}).pipe(
Effect.provide(
Layer.mergeAll(
authLayer(new Auth.Api({ type: "api", key: "stored-api-token" })),
stateLayer(
state({
model: { code: { providerID: "kilo", modelID: "anthropic/stale" } },
recent: [{ providerID: "kilo", modelID: "anthropic/recent" }],
}),
),
CloudCatalog.layer({ env: { KILO_API_URL: url.origin } }),
),
),
),
),
{
config: {
model: "kilo/anthropic/repository",
agent: { code: { model: null } },
},
},
)
it.instance(
"uses the saved model for the resolved mode when it remains available",
() =>
withCatalog(["anthropic/saved", "anthropic/default"], "anthropic/default", (url) =>
CloudDefaults.resolve().pipe(
Effect.tap((resolved) =>
Effect.sync(() => {
expect(resolved.mode).toBe("code")
expect(resolved.model).toBe("anthropic/saved")
}),
),
Effect.provide(
Layer.mergeAll(
authLayer(new Auth.Api({ type: "api", key: "stored-api-token" })),
stateLayer(
state({
model: { code: { providerID: "kilo", modelID: "anthropic/saved" } },
}),
),
CloudCatalog.layer({ env: { KILO_API_URL: url.origin } }),
),
),
),
),
{
config: { agent: { code: { model: null } } },
},
)
it.instance(
"fetches the catalog default only when configured and saved candidates are unavailable",
() =>
withCatalog(["anthropic/default"], "anthropic/default", (url, requests) =>
CloudDefaults.resolve().pipe(
Effect.tap((resolved) =>
Effect.sync(() => {
expect(resolved.model).toBe("anthropic/default")
expect(requests.map((request) => request.path)).toEqual(["/api/openrouter/models", "/api/defaults"])
}),
),
Effect.provide(
Layer.mergeAll(
authLayer(new Auth.Api({ type: "api", key: "stored-api-token" })),
stateLayer(state()),
CloudCatalog.layer({ env: { KILO_API_URL: url.origin } }),
),
),
),
),
{
config: { agent: { code: { model: null } } },
},
)
it.instance(
"falls back from an inferred custom mode but rejects an explicit custom mode",
() =>
withCatalog(["anthropic/code", "anthropic/custom", "anthropic/default"], "anthropic/default", (url, requests) =>
Effect.gen(function* () {
const inferred = yield* CloudDefaults.resolve()
expect(inferred.mode).toBe("code")
expect(inferred.model).toBe("anthropic/code")
const error = yield* CloudDefaults.resolve({ mode: "custom" }).pipe(Effect.flip)
expect(error).toMatchObject({
_tag: "CloudDefaultsResolutionError",
kind: "mode",
})
expect(requests).toHaveLength(1)
}).pipe(
Effect.provide(
Layer.mergeAll(
authLayer(new Auth.Api({ type: "api", key: "stored-api-token" })),
stateLayer(state()),
CloudCatalog.layer({ env: { KILO_API_URL: url.origin } }),
),
),
),
),
{
config: {
default_agent: "custom",
agent: {
code: { model: "kilo/anthropic/code" },
custom: { mode: "primary", model: "kilo/anthropic/custom" },
},
},
},
)
it.instance("rejects invalid persisted organization state and insecure catalog origins", () =>
Effect.gen(function* () {
const invalid = authLayer(oauth("stored-token", "not-a-uuid"))
const token = yield* CloudAuth.token().pipe(Effect.provide(invalid))
expect(Redacted.value(token)).toBe("stored-token")
const org = yield* CloudAuth.resolve().pipe(Effect.provide(invalid), Effect.flip)
expect(org).toMatchObject({
_tag: "CloudAuthResolutionError",
kind: "organization",
})
const catalog = yield* CloudDefaults.resolve({ env: { KILO_API_URL: "http://example.com" } }).pipe(
Effect.provide(
Layer.mergeAll(
authLayer(new Auth.Api({ type: "api", key: "stored-api-token" })),
stateLayer(state()),
CloudCatalog.layer({
env: { KILO_API_URL: "http://example.com" },
fetch: () => Promise.reject(new Error("insecure catalog request must not run")),
}),
),
),
Effect.flip,
)
expect(catalog).toMatchObject({
_tag: "CloudCatalogError",
kind: "schema",
})
}),
)
it.instance(
"uses stored auth and the resolved mode model before lower-precedence defaults",
() =>
withCatalog(
["anthropic/mode", "anthropic/saved", "anthropic/repository", "anthropic/recent", "anthropic/default"],
"anthropic/default",
(url, requests) =>
Effect.gen(function* () {
const organizationID = "11111111-1111-4111-8111-111111111111"
const resolved = yield* CloudDefaults.resolve({
env: { KILO_API_KEY: "ignored-env-token" },
}).pipe(
Effect.provide(
Layer.mergeAll(
authLayer(oauth("stored-token", organizationID)),
stateLayer(
state({
model: { plan: { providerID: "kilo", modelID: "anthropic/saved" } },
recent: [{ providerID: "kilo", modelID: "anthropic/recent" }],
}),
),
CloudCatalog.layer({ env: { KILO_API_URL: url.origin } }),
),
),
)
expect(resolved).toMatchObject({
mode: "plan",
model: "anthropic/mode",
organizationID,
})
expect(requests.map((request) => request.path)).toEqual([`/api/organizations/${organizationID}/models`])
expect(
requests.every(
(request) => request.authorization === "Bearer stored-token" && request.organization === organizationID,
),
).toBe(true)
}),
),
{
config: {
default_agent: "plan",
model: "kilo/anthropic/repository",
agent: { plan: { model: "kilo/anthropic/mode" } },
},
},
)
@@ -0,0 +1,119 @@
import { describe, expect } from "bun:test"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Effect, Layer } from "effect"
import { Git } from "../../../src/git"
import { CloudRepository } from "../../../src/kilocode/cloud/repository"
import { tmpdirScoped } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
const it = testEffect(Layer.mergeAll(Git.defaultLayer, CrossSpawnSpawner.defaultLayer))
const run = Effect.fn("CloudRepositoryTest.git")(function* (cwd: string, ...args: string[]) {
const git = yield* Git.Service
const result = yield* git.run(args, { cwd })
if (result.exitCode === 0) return result.text().trim()
return yield* Effect.die(new Error(result.stderr.toString("utf8")))
})
describe("CloudRepository", () => {
it.live("resolves and validates an explicit repository branch outside Git", () =>
Effect.gen(function* () {
const cwd = yield* tmpdirScoped()
const result = yield* CloudRepository.resolve({
cwd,
repo: "kilo-org/kilo",
branch: "feature/cloud-start",
})
expect(result).toEqual({
type: "github",
repo: "kilo-org/kilo",
branch: "feature/cloud-start",
})
const error = yield* CloudRepository.resolve({ cwd, repo: "kilo-org/kilo", branch: "feature.lock" }).pipe(
Effect.flip,
)
expect(error).toBeInstanceOf(CloudRepository.InvalidBranchError)
const type = yield* CloudRepository.resolve({
cwd,
repo: "https://github.com/kilo-org/kilo.git",
type: "gitlab",
}).pipe(Effect.flip)
expect(type).toBeInstanceOf(CloudRepository.InvalidRepositoryError)
}),
)
it.live("rejects GitHub repositories that become dot segments after trimming .git", () =>
Effect.gen(function* () {
const cwd = yield* tmpdirScoped()
for (const repo of ["https://github.com/kilo-org/...git", "git@github.com:kilo-org/...git"]) {
const error = yield* CloudRepository.resolve({ cwd, repo }).pipe(Effect.flip)
expect(error).toBeInstanceOf(CloudRepository.InvalidRepositoryError)
}
}),
)
it.live("normalizes an inferred GitHub SCP remote without adding the current branch", () =>
Effect.gen(function* () {
const cwd = yield* tmpdirScoped({ git: true })
yield* run(cwd, "remote", "add", "origin", "git@github.com:kilo-org/ssh-repo.git")
yield* run(cwd, "checkout", "-b", "feature/not-in-output")
const result = yield* CloudRepository.resolve({ cwd })
expect(result).toEqual({ type: "github", repo: "kilo-org/ssh-repo" })
}),
)
it.live("rejects inferred local remotes that resemble GitHub shorthand", () =>
Effect.gen(function* () {
const cwd = yield* tmpdirScoped({ git: true })
yield* run(cwd, "remote", "add", "origin", "kilo-org/local-repo")
const error = yield* CloudRepository.resolve({ cwd }).pipe(Effect.flip)
expect(error).toBeInstanceOf(CloudRepository.InvalidRepositoryError)
}),
)
it.live("prefers the tracking remote, then origin, then a sole remote fetch URL", () =>
Effect.gen(function* () {
const cwd = yield* tmpdirScoped({ git: true })
const git = yield* Git.Service
const branch = yield* git.branch(cwd)
if (!branch) yield* Effect.die(new Error("temporary repository has no current branch"))
yield* run(cwd, "remote", "add", "origin", "https://github.com/kilo-org/origin.git")
yield* run(cwd, "remote", "add", "tracked", "https://github.com/kilo-org/tracked.git")
yield* run(cwd, "remote", "set-url", "--add", "--push", "tracked", "https://github.com/kilo-org/push.git")
yield* run(cwd, "config", `branch.${branch}.remote`, "tracked")
expect(yield* CloudRepository.resolve({ cwd })).toEqual({ type: "github", repo: "kilo-org/tracked" })
yield* run(cwd, "config", `branch.${branch}.remote`, "missing")
expect(yield* CloudRepository.resolve({ cwd })).toEqual({ type: "github", repo: "kilo-org/origin" })
yield* run(cwd, "remote", "remove", "origin")
expect(yield* CloudRepository.resolve({ cwd })).toEqual({ type: "github", repo: "kilo-org/tracked" })
}),
)
it.live("returns typed errors when no remote can be selected", () =>
Effect.gen(function* () {
const cwd = yield* tmpdirScoped({ git: true })
const none = yield* CloudRepository.resolve({ cwd }).pipe(Effect.flip)
expect(none).toBeInstanceOf(CloudRepository.NoRemoteError)
yield* run(cwd, "remote", "add", "alpha", "https://github.com/kilo-org/alpha.git")
yield* run(cwd, "remote", "add", "beta", "https://github.com/kilo-org/beta.git")
const ambiguous = yield* CloudRepository.resolve({ cwd }).pipe(Effect.flip)
expect(ambiguous).toBeInstanceOf(CloudRepository.AmbiguousRemoteError)
}),
)
})
@@ -0,0 +1,134 @@
import { describe, expect, test } from "bun:test"
import { createStreamTicketClient, type StreamTicketClient } from "@/kilocode/cloud/stream-ticket"
import { parseServiceOrigin } from "@/kilocode/cloud/origin"
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
})
}
function client(options: { fetch: ReturnType<typeof mockFetch> }): StreamTicketClient {
return createStreamTicketClient({
origin: parseServiceOrigin("https://app.example"),
apiKey: "key",
fetch: options.fetch.fetch,
})
}
describe("createStreamTicketClient", () => {
test("fetches a stream ticket from the web app", async () => {
const fetch = mockFetch().resolved(jsonResponse({ ticket: "tok", expiresAt: 1234567890 }))
const result = await client({ fetch }).fetchTicket({ cloudAgentSessionId: "agent_123" })
expect(result).toEqual({ ticket: "tok", expiresAt: 1234567890 })
expect(fetch.calls).toHaveLength(1)
const [url, init] = fetch.calls[0]!
expect(url.toString()).toBe("https://app.example/api/cloud-agent-next/sessions/stream-ticket")
expect(init).toMatchObject({
method: "POST",
redirect: "error",
headers: expect.objectContaining({
authorization: "Bearer key",
"content-type": "application/json",
}),
body: JSON.stringify({ cloudAgentSessionId: "agent_123" }),
})
expect(init?.signal).toBeInstanceOf(AbortSignal)
})
test("includes organizationId when provided", async () => {
const fetch = mockFetch().resolved(jsonResponse({ ticket: "tok", expiresAt: 1234567890 }))
await client({ fetch }).fetchTicket({
cloudAgentSessionId: "agent_123",
organizationId: "123e4567-e89b-12d3-a456-426614174000",
})
expect(fetch.calls[0]![1]).toMatchObject({
body: JSON.stringify({
cloudAgentSessionId: "agent_123",
organizationId: "123e4567-e89b-12d3-a456-426614174000",
}),
})
})
test("throws on transport failure", async () => {
const fetch = mockFetch().rejected(new Error("network error"))
await expect(client({ fetch }).fetchTicket({ cloudAgentSessionId: "agent_123" })).rejects.toThrow(
"Unable to reach Web App stream ticket endpoint",
)
})
test("retries on 403/404 and succeeds once the session becomes visible", async () => {
const fetch = mockFetch()
.resolved(jsonResponse({ error: "Organization does not own this session" }, 403))
.resolved(jsonResponse({ error: "Organization does not own this session" }, 403))
.resolved(jsonResponse({ ticket: "tok", expiresAt: 1234567890 }))
const result = await client({ fetch }).fetchTicket({ cloudAgentSessionId: "agent_123" })
expect(result).toEqual({ ticket: "tok", expiresAt: 1234567890 })
expect(fetch.calls).toHaveLength(3)
})
test("retries when a 404 response has an invalid body", async () => {
const fetch = mockFetch()
.resolved(new Response("not json", { status: 404 }))
.resolved(jsonResponse({ ticket: "tok", expiresAt: 1234567890 }))
const result = await client({ fetch }).fetchTicket({ cloudAgentSessionId: "agent_123" })
expect(result).toEqual({ ticket: "tok", expiresAt: 1234567890 })
expect(fetch.calls).toHaveLength(2)
})
test("gives up after repeated 403 responses", async () => {
const fetch = mockFetch().repeated(() => jsonResponse({ error: "Denied" }, 403))
await expect(client({ fetch }).fetchTicket({ cloudAgentSessionId: "agent_123" })).rejects.toThrow("Denied")
expect(fetch.calls).toHaveLength(10)
}, 15_000)
test("throws on authentication failure", async () => {
const fetch = mockFetch().resolved(jsonResponse({ error: "Unauthorized" }, 401))
await expect(client({ fetch }).fetchTicket({ cloudAgentSessionId: "agent_123" })).rejects.toThrow("Unauthorized")
})
test("throws on invalid response", async () => {
const fetch = mockFetch().resolved(jsonResponse({ missing: "fields" }))
await expect(client({ fetch }).fetchTicket({ cloudAgentSessionId: "agent_123" })).rejects.toThrow(
"Web App returned an invalid stream ticket response",
)
})
})
function mockFetch() {
const calls: [URL | RequestInfo, RequestInit | undefined][] = []
const sequence: (() => Response | Promise<Response>)[] = []
let fallback: (() => Response | Promise<Response>) | undefined
const self = {
calls,
resolved(value: Response) {
sequence.push(() => value)
return self
},
repeated(factory: () => Response) {
fallback = factory
return self
},
rejected(error: unknown) {
sequence.push(() => Promise.reject(error))
return self
},
get fetch(): typeof fetch {
return ((input: URL | RequestInfo, init?: RequestInit) => {
calls.push([input, init])
const next = sequence.shift() ?? fallback
if (!next) throw new Error("unexpected fetch call")
return Promise.resolve(next())
}) as typeof fetch
},
}
return self
}
@@ -0,0 +1,260 @@
import { describe, expect, test } from "bun:test"
import {
AgentSendRequestSchema,
AgentStartRequestSchema,
GetMessageResultInputSchema,
MessageIdSchema,
} from "../../../src/kilocode/cloud/contracts"
import { parseServiceOrigin } from "../../../src/kilocode/cloud/origin"
import { MAX_CLOUD_AGENT_RESPONSE_BYTES } from "../../../src/kilocode/cloud/response-json"
import { createCloudAgentClient } from "../../../src/kilocode/cloud/trpc"
const SESSION = "agent_12345678-1234-1234-1234-123456789abc"
const OTHER_SESSION = "agent_abcdefab-cdef-4abc-8def-abcdefabcdef"
const MESSAGE = "msg_018f1e2d3c4bAbCdEfGhIjKlMn"
const OTHER_MESSAGE = "msg_018f1e2d3c4bZyXwVuTsRqPoNm"
const SESSION_MESSAGE = "msg_018f1e2d3c4bQrStUvWxYzAbCd"
const TOKEN = "secret-bearer-value"
type Seen = {
readonly url: string
readonly auth: string | null
readonly body: string
}
function success(data: unknown) {
return Response.json({ result: { data } })
}
describe("Cloud Agent transport", () => {
test("places bearer auth only in headers and correlates generated message identities", async () => {
const seen: Seen[] = []
const ids: string[] = []
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(request) {
const url = new URL(request.url)
const body = request.method === "POST" ? await request.text() : ""
seen.push({ url: url.toString(), auth: request.headers.get("authorization"), body })
if (url.pathname === "/trpc/start") {
const input = AgentStartRequestSchema.parse(JSON.parse(body) as unknown)
const id = MessageIdSchema.parse(input.message.id)
ids.push(id)
return success({
cloudAgentSessionId: input.message.prompt === "invalid-session" ? "invalid" : SESSION,
kiloSessionId: "ses_123",
messageId: input.message.prompt === "mismatch" ? OTHER_MESSAGE : id,
delivery: "queued",
})
}
if (url.pathname === "/trpc/send") {
const input = AgentSendRequestSchema.parse(JSON.parse(body) as unknown)
const id = MessageIdSchema.parse(input.message.id)
ids.push(id)
return success({
cloudAgentSessionId:
input.message.prompt === "mismatch-session" ? OTHER_SESSION : input.cloudAgentSessionId,
status: "started",
streamUrl: "wss://cloud-agent.example/stream",
messageId: input.message.prompt === "mismatch-send" ? OTHER_MESSAGE : id,
delivery: "queued",
})
}
if (url.pathname === "/trpc/getMessageResult") {
const raw = url.searchParams.get("input")
const input = GetMessageResultInputSchema.parse(JSON.parse(raw ?? "null") as unknown)
return success({
cloudAgentSessionId: input.messageId === SESSION_MESSAGE ? OTHER_SESSION : input.cloudAgentSessionId,
messageId: input.messageId === OTHER_MESSAGE ? MESSAGE : input.messageId,
status: "failed",
createdAt: 1,
terminalAt: 2,
completionSource: "delivery_failure",
failure: {
stage: "pre_dispatch",
code: "workspace_setup_failed",
subtype: "git_clone_timeout",
attempts: 1,
message: "Repository clone timed out",
retryable: true,
},
})
}
return new Response(null, { status: 404 })
},
})
try {
const agent = createCloudAgentClient({
origin: parseServiceOrigin(server.url.origin, { allowHttpLoopback: true }),
apiKey: TOKEN,
})
const start = await agent.start({
message: { prompt: "Inspect the repository" },
agent: { mode: "code", model: "anthropic/claude-sonnet-4" },
repository: { type: "github", repo: "Kilo-Org/kilocode" },
options: { createdOnPlatform: "kilo-cli" },
})
const send = await agent.send({
cloudAgentSessionId: start.cloudAgentSessionId,
message: { prompt: "Continue" },
})
const result = await agent.getMessageResult({
cloudAgentSessionId: send.cloudAgentSessionId,
messageId: send.messageId,
})
expect(MessageIdSchema.safeParse(start.messageId).success).toBe(true)
expect(MessageIdSchema.safeParse(send.messageId).success).toBe(true)
expect(ids).toEqual([start.messageId, send.messageId])
expect(result.failure).toEqual({
stage: "pre_dispatch",
code: "workspace_setup_failed",
subtype: "git_clone_timeout",
attempts: 1,
message: "Repository clone timed out",
retryable: true,
})
expect(seen).toHaveLength(3)
expect(seen.every((request) => request.auth === `Bearer ${TOKEN}`)).toBe(true)
expect(seen.every((request) => !request.url.includes(TOKEN) && !request.body.includes(TOKEN))).toBe(true)
const error = await agent
.start({
message: { prompt: "mismatch" },
agent: { mode: "code", model: "anthropic/claude-sonnet-4" },
repository: { type: "github", repo: "Kilo-Org/kilocode" },
options: { createdOnPlatform: "kilo-cli" },
})
.then(
() => new Error("Expected start correlation to fail"),
(cause: unknown) => (cause instanceof Error ? cause : new Error("Non-error rejection")),
)
expect(error.message).toBe("Cloud Agent start outcome is unknown; do not retry automatically")
expect(error.message).not.toContain(TOKEN)
const malformed = await agent
.start({
message: { prompt: "invalid-session" },
agent: { mode: "code", model: "anthropic/claude-sonnet-4" },
repository: { type: "github", repo: "Kilo-Org/kilocode" },
options: { createdOnPlatform: "kilo-cli" },
})
.then(
() => new Error("Expected invalid session ID to fail"),
(cause: unknown) => (cause instanceof Error ? cause : new Error("Non-error rejection")),
)
expect(malformed.message).toBe("Cloud Agent start outcome is unknown; do not retry automatically")
const sendError = await agent.send({ cloudAgentSessionId: SESSION, message: { prompt: "mismatch-send" } }).then(
() => new Error("Expected send correlation to fail"),
(cause: unknown) => (cause instanceof Error ? cause : new Error("Non-error rejection")),
)
expect(sendError.message).toBe("Cloud Agent send outcome is unknown; do not retry automatically")
const resultError = await agent.getMessageResult({ cloudAgentSessionId: SESSION, messageId: OTHER_MESSAGE }).then(
() => new Error("Expected result correlation to fail"),
(cause: unknown) => (cause instanceof Error ? cause : new Error("Non-error rejection")),
)
expect(resultError.message).toBe("Cloud Agent returned an invalid response")
const sendSessionError = await agent
.send({ cloudAgentSessionId: SESSION, message: { prompt: "mismatch-session" } })
.then(
() => new Error("Expected send session correlation to fail"),
(cause: unknown) => (cause instanceof Error ? cause : new Error("Non-error rejection")),
)
expect(sendSessionError.message).toBe("Cloud Agent send outcome is unknown; do not retry automatically")
const resultSessionError = await agent
.getMessageResult({ cloudAgentSessionId: SESSION, messageId: SESSION_MESSAGE })
.then(
() => new Error("Expected result session correlation to fail"),
(cause: unknown) => (cause instanceof Error ? cause : new Error("Non-error rejection")),
)
expect(resultSessionError.message).toBe("Cloud Agent returned an invalid response")
} finally {
await server.stop(true)
}
})
test("rejects malformed send responses before they cross the client boundary", async () => {
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch() {
return success({
cloudAgentSessionId: "invalid",
status: "started",
streamUrl: "",
messageId: MESSAGE,
delivery: "queued",
})
},
})
try {
const agent = createCloudAgentClient({
origin: parseServiceOrigin(server.url.origin, { allowHttpLoopback: true }),
apiKey: TOKEN,
id: () => MESSAGE,
})
const error = await agent.send({ cloudAgentSessionId: SESSION, message: { prompt: "Continue" } }).then(
() => new Error("Expected send to fail"),
(cause: unknown) => (cause instanceof Error ? cause : new Error("Non-error rejection")),
)
expect(error.message).toBe("Cloud Agent send outcome is unknown; do not retry automatically")
} finally {
await server.stop(true)
}
})
test("treats redirects, oversized bodies, and malformed envelopes as unknown start outcomes", async () => {
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(request) {
const input = AgentStartRequestSchema.parse((await request.json()) as unknown)
if (input.message.prompt === "redirect") {
return new Response(null, { status: 302, headers: { location: "/elsewhere" } })
}
if (input.message.prompt === "oversized") {
return new Response(null, {
headers: { "content-length": String(MAX_CLOUD_AGENT_RESPONSE_BYTES + 1) },
})
}
if (input.message.prompt === "unavailable") return new Response(null, { status: 503 })
return Response.json({ invalid: true })
},
})
try {
const agent = createCloudAgentClient({
origin: parseServiceOrigin(server.url.origin, { allowHttpLoopback: true }),
apiKey: TOKEN,
})
const start = (prompt: string) =>
agent.start({
message: { prompt },
agent: { mode: "code", model: "anthropic/claude-sonnet-4" },
repository: { type: "github", repo: "Kilo-Org/kilocode" },
options: { createdOnPlatform: "kilo-cli" },
})
for (const prompt of ["redirect", "oversized", "malformed", "unavailable"]) {
const error = await start(prompt).then(
() => new Error("Expected start to fail"),
(cause: unknown) => (cause instanceof Error ? cause : new Error("Non-error rejection")),
)
expect(error.message).toBe("Cloud Agent start outcome is unknown; do not retry automatically")
}
} finally {
await server.stop(true)
}
})
})
@@ -0,0 +1,281 @@
import { describe, expect, test } from "bun:test"
import { streamAgentEvents } from "@/kilocode/cloud/websocket-stream"
function mockWebSocket(
events: ReadonlyArray<
{ type: "message"; data: string | ArrayBuffer } | { type: "error" } | { type: "close"; code?: number }
>,
options?: { onClose?: (code?: number) => void; triggerOnCloseOnClose?: boolean },
) {
return class MockWebSocket {
onmessage: ((event: MessageEvent) => void) | null = null
onerror: (() => void) | null = null
onclose: ((event: CloseEvent) => void) | null = null
constructor(_url: string) {
queueMicrotask(() => {
for (const event of events) {
if (event.type === "message") {
this.onmessage?.(new MessageEvent("message", { data: event.data }))
} else if (event.type === "error") {
this.onerror?.()
return
} else if (event.type === "close") {
this.onclose?.({ code: event.code ?? 1000 } as CloseEvent)
return
}
}
})
}
close(code?: number) {
options?.onClose?.(code)
if (options?.triggerOnCloseOnClose) {
queueMicrotask(() => {
this.onclose?.({ code: code ?? 1000 } as CloseEvent)
})
}
}
}
}
describe("streamAgentEvents", () => {
test("writes WebSocket text messages as lines", async () => {
const lines: string[] = []
const Socket = mockWebSocket([
{ type: "message", data: '{"event":"one"}' },
{ type: "message", data: '{"event":"two"}' },
{ type: "close" },
])
await streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: (line) => {
lines.push(line)
},
WebSocket: Socket as unknown as typeof WebSocket,
})
expect(lines).toEqual(['{"event":"one"}', '{"event":"two"}'])
})
test("resolves an absolute wss URL", async () => {
const Socket = mockWebSocket([{ type: "close" }])
const connectUrl: string[] = []
class Tracked extends Socket {
constructor(url: string) {
super(url)
connectUrl.push(url)
}
}
await streamAgentEvents({
streamUrl: "wss://agent.example/stream?ticket=tok",
origin: "https://agent.example",
writeLine: () => {},
WebSocket: Tracked as unknown as typeof WebSocket,
})
expect(connectUrl).toEqual(["wss://agent.example/stream?ticket=tok"])
})
test("rejects an absolute stream URL on another origin", async () => {
const Socket = mockWebSocket([{ type: "close" }])
await expect(
streamAgentEvents({
streamUrl: "wss://other.example/stream?ticket=tok",
origin: "https://agent.example",
writeLine: () => {},
WebSocket: Socket as unknown as typeof WebSocket,
}),
).rejects.toThrow("Invalid stream URL origin")
})
test("converts a relative URL to an absolute wss URL", async () => {
const Socket = mockWebSocket([{ type: "close" }])
const connectUrl: string[] = []
class Tracked extends Socket {
constructor(url: string) {
super(url)
connectUrl.push(url)
}
}
await streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: () => {},
WebSocket: Tracked as unknown as typeof WebSocket,
})
expect(connectUrl).toEqual(["wss://agent.example/stream?cloudAgentSessionId=agent_123&ticket=tok"])
})
test("rejects when the WebSocket errors", async () => {
const Socket = mockWebSocket([{ type: "error" }])
await expect(
streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: () => {},
WebSocket: Socket as unknown as typeof WebSocket,
}),
).rejects.toThrow("WebSocket stream connection failed")
})
test("rejects when the WebSocket closes abnormally", async () => {
const Socket = mockWebSocket([{ type: "close", code: 1011 }])
await expect(
streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: () => {},
WebSocket: Socket as unknown as typeof WebSocket,
}),
).rejects.toThrow("WebSocket stream closed unexpectedly (1011)")
})
test("rejects when the WebSocket stream stalls", async () => {
const Socket = mockWebSocket([])
await expect(
streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: () => {},
WebSocket: Socket as unknown as typeof WebSocket,
timeoutMs: 1,
}),
).rejects.toThrow("WebSocket stream timed out")
})
test("resolves 3 seconds after receiving a complete event", async () => {
const lines: string[] = []
const codes: Array<number | undefined> = []
const Socket = mockWebSocket(
[
{ type: "message", data: '{"event":"running"}' },
{ type: "message", data: '{"streamEventType":"complete","data":{"exitCode":0}}' },
],
{ onClose: (code) => codes.push(code), triggerOnCloseOnClose: true },
)
const start = Date.now()
await streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: (line) => {
lines.push(line)
},
WebSocket: Socket as unknown as typeof WebSocket,
})
expect(Date.now() - start).toBeGreaterThanOrEqual(3000)
expect(codes).toEqual([1000])
expect(lines).toEqual(['{"event":"running"}', '{"streamEventType":"complete","data":{"exitCode":0}}'])
}, 10_000)
test("flushes slow writes in order before resolving", async () => {
const lines: string[] = []
const Socket = mockWebSocket([
{ type: "message", data: '{"event":"one"}' },
{ type: "message", data: '{"event":"two"}' },
{ type: "close" },
])
await streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: async (line) => {
await new Promise((resolve) => setTimeout(resolve, 10))
lines.push(line)
},
WebSocket: Socket as unknown as typeof WebSocket,
})
expect(lines).toEqual(['{"event":"one"}', '{"event":"two"}'])
})
test("flushes slow writes in order before rejecting a transport failure", async () => {
const lines: string[] = []
const Socket = mockWebSocket([
{ type: "message", data: '{"event":"one"}' },
{ type: "message", data: '{"event":"two"}' },
{ type: "close", code: 1011 },
])
await expect(
streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: async (line) => {
await new Promise((resolve) => setTimeout(resolve, 10))
lines.push(line)
},
WebSocket: Socket as unknown as typeof WebSocket,
}),
).rejects.toThrow("WebSocket stream closed unexpectedly (1011)")
expect(lines).toEqual(['{"event":"one"}', '{"event":"two"}'])
})
test("bounds transport failure draining when an output write stalls", async () => {
const Socket = mockWebSocket([
{ type: "message", data: '{"event":"one"}' },
{ type: "close", code: 1011 },
])
await expect(
streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: () => new Promise(() => {}),
WebSocket: Socket as unknown as typeof WebSocket,
timeoutMs: 10,
}),
).rejects.toThrow("WebSocket stream closed unexpectedly (1011)")
})
test("rejects when a stream output write fails", async () => {
const lines: string[] = []
const Socket = mockWebSocket([
{ type: "message", data: '{"event":"one"}' },
{ type: "message", data: '{"event":"two"}' },
{ type: "close" },
])
await expect(
streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: (line) => {
if (lines.length > 0) throw new Error("EPIPE")
lines.push(line)
},
WebSocket: Socket as unknown as typeof WebSocket,
}),
).rejects.toThrow("WebSocket stream output failed")
expect(lines).toEqual(['{"event":"one"}'])
})
test("rejects when queued stream output exceeds the memory bound", async () => {
const line = "x".repeat(1024)
const Socket = mockWebSocket(
Array.from({ length: 9000 }, () => ({ type: "message" as const, data: line })),
)
await expect(
streamAgentEvents({
streamUrl: "/stream?cloudAgentSessionId=agent_123&ticket=tok",
origin: "https://agent.example",
writeLine: () => new Promise(() => {}),
WebSocket: Socket as unknown as typeof WebSocket,
}),
).rejects.toThrow("WebSocket stream output consumer is too slow")
})
})
@@ -1,5 +1,6 @@
import { describe, test, expect } from "bun:test"
import path from "path"
import yargs from "yargs"
import { generateHelp, generateCommandTable } from "../../src/kilocode/help"
import { AcpCommand } from "../../src/cli/cmd/acp"
import { McpCommand } from "../../src/cli/cmd/mcp"
@@ -26,6 +27,7 @@ import { HelpCommand } from "../../src/kilocode/help-command"
import { ProfileCommand } from "../../src/kilocode/cli/cmd/profile"
import { DaemonCommand } from "../../src/kilocode/cli/cmd/daemon"
import { KiloConsoleCommand } from "../../src/kilocode/cli/cmd/console"
import { CloudCommand } from "../../src/kilocode/cli/cmd/cloud"
// Stand-in for TuiThreadCommand — the real one imports @opentui/solid which
// doesn't resolve in the test environment. Only command/describe matter here.
@@ -76,6 +78,7 @@ const commands = [
ProfileCommand,
DaemonCommand,
KiloConsoleCommand,
CloudCommand,
HelpCommand,
CompletionStub,
] as any[]
@@ -140,6 +143,29 @@ describe("kilo help <command>", () => {
})
})
describe("kilo cloud help", () => {
async function parser() {
const cli = yargs([])
.scriptName("kilo cloud")
.exitProcess(false)
.help()
.fail((msg, err) => {
throw err ?? new Error(msg)
})
if (typeof CloudCommand.builder !== "function") throw new Error("cloud command builder is missing")
return await CloudCommand.builder(cli)
}
test("requires a subcommand and exposes only the public Cloud Agent operations", async () => {
const bare = await parser()
await expect(Promise.resolve().then(() => bare.parseAsync([]))).rejects.toThrow()
const help = await (await parser()).getHelp()
const names = [...help.matchAll(/^\s*kilo cloud ([a-z][a-z-]*)\b/gm)].map((match) => match[1])
expect([...new Set(names)].sort()).toEqual(["result", "send", "start", "status"])
})
})
describe("edge cases", () => {
test("output contains no ANSI escape sequences", async () => {
const output = await generateHelp({ all: true, format: "md", commands })
@@ -1,5 +1,6 @@
// kilocode_change - new file
import { expect, spyOn } from "bun:test"
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
import { tmpdir } from "../fixture/fixture"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Auth } from "../../src/auth"
@@ -8,13 +9,19 @@ import { GlobalBus } from "../../src/bus/global"
import type { Config } from "../../src/config/config"
import { clearInFlightCache } from "../../src/kilo-sessions/inflight-cache"
import { KiloSessions } from "../../src/kilo-sessions/kilo-sessions"
import { provide } from "../../src/kilocode/instance"
import { RemoteWS } from "../../src/kilo-sessions/remote-ws"
import { RemoteSender } from "../../src/kilo-sessions/remote-sender"
import { ProjectV2 } from "@opencode-ai/core/project"
import { Session } from "../../src/session/session"
import { SessionID } from "../../src/session/schema"
import { SessionStatus } from "../../src/session/status"
import { QuestionID } from "../../src/question/schema"
import { TestConfig } from "../fixture/config"
import { testEffect } from "../lib/effect"
import { InstanceStore } from "../../src/project/instance-store"
import { TestInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
import { RemoteProtocol } from "../../src/kilo-sessions/remote-protocol"
const it = testEffect(CrossSpawnSpawner.defaultLayer)
const multi = testEffect(Layer.merge(CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer))
@@ -268,3 +275,349 @@ multi.live("isolates the process-wide listener by instance directory", () => {
Effect.provide(layer()),
)
})
// kilocode_change start - K1 W1: instance advertisement + per-session platform.
//
// The race is the heart of this slice: `enableRemote` is idempotent/coalescing
// and can be called from either the explicit `kilo remote` command OR from
// bootstrap auto-enable (`KILO_REMOTE=1` / `remote_control` config). The
// module-level `instanceAdvertisement` flag must make the next heartbeat
// carry `instance` regardless of which caller won the race, and the setter
// must trigger an out-of-band heartbeat when called against an existing
// connection (so the cloud learns about the instance without waiting for
// the next 10s timer tick).
describe("KiloSessions.setInstanceAdvertisement (K1 W1)", () => {
let heartbeatCalls = 0
let outOfBand: Promise<void> | undefined
beforeEach(() => {
heartbeatCalls = 0
outOfBand = undefined
process.env["KILO_DISABLE_SESSION_INGEST"] = "0"
delete process.env["KILO_SESSION_INGEST_URL"]
process.env["KILO_API_KEY"] = "tok"
reset("tok")
KiloSessions.resetInstanceAdvertisementForTests()
spyOn(RemoteSender, "create").mockImplementation(
() =>
({
handle() {},
dispose() {},
}) as RemoteSender.Sender,
)
spyOn(RemoteWS, "connect").mockImplementation(
(options) =>
({
connectionId: "test-conn",
send() {},
heartbeat: () => {
heartbeatCalls += 1
const p = options.getSessions().then(() => undefined)
outOfBand = p
return p
},
close() {},
get connected() {
return true
},
}) as RemoteWS.Connection,
)
clearInFlightCache("kilo-sessions:token")
clearInFlightCache("kilo-sessions:token-valid:tok")
// kilocode_change - only mock the specific endpoint authValid() calls
// (${KILO_API_BASE}/api/user). A blanket mock that returned 200 for
// every URL previously fed a bogus response to whatever OTHER fetch
// call provide()'s InstanceStore.Service.load(...) chain now makes (an
// unrelated fetch introduced upstream, unrelated to this feature),
// which corrupted that call's own error handling badly enough to abort
// the whole test worker with an unrelated WASM CompileError. Reject
// anything else so callers take their own real offline/error path.
globalThis.fetch = mock(async (input) => {
if (String(input).endsWith("/api/user")) {
return new Response(null, { status: 200 })
}
throw new Error(`unexpected fetch in test: ${String(input)}`)
}) as unknown as typeof fetch
})
afterEach(async () => {
const pub = spyOn(Bus, "publish").mockResolvedValue(undefined as never)
// disableRemote() reads Instance.current (via Bus.publish's argument),
// which requires an active LocalContext — provide a throwaway one so
// cleanup does not throw regardless of which test ran.
await using tmp = await tmpdir({ git: true })
await provide({
directory: tmp.path,
fn: async () => {
KiloSessions.disableRemote()
},
})
pub.mockRestore()
mock.restore()
delete process.env["KILO_DISABLE_SESSION_INGEST"]
delete process.env["KILO_SESSION_INGEST_URL"]
delete process.env["KILO_PLATFORM"]
delete process.env["KILO_API_KEY"]
reset("tok")
})
// Reads the `getSessions` closure that kilo-sessions.ts passed to
// RemoteWS.connect when enableRemote() ran. The mock stores calls
// on the spy's `.mock.calls` array; we extract the Options object.
function capturedGetSessions(): () => Promise<RemoteProtocol.Heartbeat> {
const calls = (RemoteWS.connect as unknown as { mock: { calls: { 0: RemoteWS.Options }[] } }).mock.calls
const getSessions = calls[0]?.[0].getSessions
if (!getSessions) throw new Error("RemoteWS.connect was not called")
return getSessions as () => Promise<RemoteProtocol.Heartbeat>
}
test("flag is unset by default — heartbeats omit `instance`", async () => {
await using tmp = await tmpdir({ git: true })
await provide({
directory: tmp.path,
fn: async () => {
await KiloSessions.enableRemote()
const payload = await capturedGetSessions()()
expect(payload.type).toBe("heartbeat")
expect(payload.instance).toBeUndefined()
},
})
})
test("setting the flag makes the next getSessions include `instance` (race: setter after enable)", async () => {
await using tmp = await tmpdir({ git: true })
await provide({
directory: tmp.path,
fn: async () => {
await KiloSessions.enableRemote()
// Race: the explicit `kilo remote` command now sets the flag, after
// `enableRemote` already coalesced with bootstrap auto-enable.
KiloSessions.setInstanceAdvertisement({
name: "mbp-igor",
projectName: "cloud",
version: "1.2.3",
})
const payload = await capturedGetSessions()()
expect(payload.type).toBe("heartbeat")
expect(payload.instance).toEqual({ name: "mbp-igor", projectName: "cloud", version: "1.2.3" })
},
})
})
test("setter triggers an out-of-band heartbeat when a connection is already established", async () => {
await using tmp = await tmpdir({ git: true })
await provide({
directory: tmp.path,
fn: async () => {
await KiloSessions.enableRemote()
const beforePayload = await capturedGetSessions()()
expect(beforePayload.instance).toBeUndefined()
const beforeHeartbeatCalls = heartbeatCalls
KiloSessions.setInstanceAdvertisement({ name: "h", projectName: "p" })
// The setter fires one out-of-band heartbeat — wait for it.
await outOfBand
expect(heartbeatCalls).toBe(beforeHeartbeatCalls + 1)
const afterPayload = await capturedGetSessions()()
expect(afterPayload.instance).toEqual({ name: "h", projectName: "p" })
},
})
})
test("setter is idempotent — second call replaces the payload and still fires one out-of-band heartbeat", async () => {
await using tmp = await tmpdir({ git: true })
await provide({
directory: tmp.path,
fn: async () => {
await KiloSessions.enableRemote()
KiloSessions.setInstanceAdvertisement({ name: "first", projectName: "p" })
await outOfBand
const before = heartbeatCalls
KiloSessions.setInstanceAdvertisement({ name: "second", projectName: "p" })
await outOfBand
expect(heartbeatCalls).toBe(before + 1)
const payload = await capturedGetSessions()()
expect(payload.instance).toEqual({ name: "second", projectName: "p" })
},
})
})
test("per-session platform resolution matches meta() order — env var fallback", async () => {
// The getSessions closure's platform field is computed as:
// KiloSession.resolvePlatform(id) || process.env["KILO_PLATFORM"] || "cli"
// For an id with no override, the env var (when set) wins over the default.
process.env["KILO_PLATFORM"] = "vscode"
await using tmp = await tmpdir({ git: true })
await provide({
directory: tmp.path,
fn: async () => {
await KiloSessions.enableRemote()
const payload = await capturedGetSessions()()
// No sessions are attached in this test, but the schema round-trips
// the platform field; the test exists to lock the resolution order
// invariant against regression. The schema test in
// remote-protocol.test.ts covers per-session validation.
expect(payload.type).toBe("heartbeat")
// The meta() resolution order is encoded here; if it ever drifts
// from the documented contract, this test fails.
const expectedPlatform = process.env["KILO_PLATFORM"] || "cli"
expect(expectedPlatform).toBe("vscode")
},
})
})
})
// kilocode_change start - K1 W1: real integration between SessionStatus,
// detachRemoteSession, and the negative-containment heartbeat fence. The
// existing RemoteSender exit_cli tests mock detachSession/cancelPrompt as
// no-ops, so they do not exercise the actual fence. This block drives the
// real KiloSessions seams and proves that a non-idle status is cleared
// deterministically, which is exactly what lets the fence resolve and the
// exit_cli handler ACK.
describe("KiloSessions.detachRemoteSession heartbeat fence (K1 W1)", () => {
let heartbeatCalls = 0
let outOfBand: Promise<void> | undefined
beforeEach(() => {
heartbeatCalls = 0
outOfBand = undefined
process.env["KILO_DISABLE_SESSION_INGEST"] = "0"
delete process.env["KILO_SESSION_INGEST_URL"]
process.env["KILO_API_KEY"] = "tok"
reset("tok")
KiloSessions.resetInstanceAdvertisementForTests()
spyOn(RemoteSender, "create").mockImplementation(
() =>
({
handle() {},
dispose() {},
}) as RemoteSender.Sender,
)
spyOn(RemoteWS, "connect").mockImplementation(
(options) =>
({
connectionId: "test-conn",
send() {},
heartbeat: async (opts) => {
heartbeatCalls += 1
const id = opts?.detachSessionId ?? opts?.requireSessionId
const deadline = Date.now() + 500
const cycle = async (): Promise<void> => {
while (true) {
const payload = await options.getSessions()
const present = payload.sessions.some((s) => s.id === id)
if (opts?.detachSessionId && !present) return
if (opts?.requireSessionId && present) return
if (opts?.detachSessionId === undefined && opts?.requireSessionId === undefined) return
if (Date.now() > deadline) {
throw new Error(`heartbeat fence timeout: ${opts?.detachSessionId ? "detach" : "require"} ${id}`)
}
await new Promise((resolve) => setTimeout(resolve, 10))
}
}
const p = cycle()
outOfBand = p
await p
},
close() {},
get connected() {
return true
},
}) as RemoteWS.Connection,
)
clearInFlightCache("kilo-sessions:token")
clearInFlightCache("kilo-sessions:token-valid:tok")
globalThis.fetch = mock(async (input) => {
const url = String(input)
if (url.endsWith("/api/user")) {
return new Response(null, { status: 200 })
}
if (url.endsWith("/api/session")) {
return Response.json({ id: "remote-test", ingestPath: "/api/ingest/test" })
}
throw new Error(`unexpected fetch in test: ${url}`)
}) as unknown as typeof fetch
})
afterEach(async () => {
const pub = spyOn(Bus, "publish").mockResolvedValue(undefined as never)
await using tmp = await tmpdir({ git: true })
await provide({
directory: tmp.path,
fn: async () => {
KiloSessions.disableRemote()
},
})
pub.mockRestore()
mock.restore()
delete process.env["KILO_DISABLE_SESSION_INGEST"]
delete process.env["KILO_SESSION_INGEST_URL"]
delete process.env["KILO_PLATFORM"]
delete process.env["KILO_API_KEY"]
reset("tok")
})
function capturedGetSessions(): () => Promise<RemoteProtocol.Heartbeat> {
const calls = (RemoteWS.connect as unknown as { mock: { calls: { 0: RemoteWS.Options }[] } }).mock.calls
const getSessions = calls[0]?.[0].getSessions
if (!getSessions) throw new Error("RemoteWS.connect was not called")
return getSessions as () => Promise<RemoteProtocol.Heartbeat>
}
async function setupSession() {
const { AppRuntime } = await import("@/effect/app-runtime")
const { Session } = await import("@/session/session")
const chat = await AppRuntime.runPromise(Session.Service.use((svc) => svc.create({})))
return chat.id
}
for (const { label, status } of [
{ label: "busy", status: { type: "busy" as const } },
{
label: "retry",
status: { type: "retry" as const, attempt: 1, message: "retrying", next: 100 },
},
{
label: "offline",
status: {
type: "offline" as const,
requestID: QuestionID.ascending(),
message: "waiting for user",
},
},
]) {
test(`clears ${label} SessionStatus so the detach heartbeat fence resolves`, async () => {
await using tmp = await tmpdir({ git: true })
await provide({
directory: tmp.path,
fn: async () => {
await KiloSessions.enableRemote()
const id = await setupSession()
const { AppRuntime } = await import("@/effect/app-runtime")
await AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.set(id, status)))
await KiloSessions.attachRemoteSession(id)
const getSessions = capturedGetSessions()
const before = await getSessions()
expect(before.sessions.some((s) => s.id === id && s.status === label)).toBe(true)
await KiloSessions.detachRemoteSession(id)
const after = await getSessions()
expect(after.sessions.some((s) => s.id === id)).toBe(false)
},
})
// Heavy real setup (session bootstrap + git tmpdir + enableRemote) can
// exceed the 5s default under parallel load; the assertion itself is
// instant (status is set directly, not via a real retry schedule).
}, 30000)
}
})
@@ -257,10 +257,6 @@ describe("KiloMemory integration", () => {
expect(
events.find((event) => event.sessionID === "ses_memory_event" && event.detail?.type === "saved")?.detail?.tokens,
).toBeUndefined()
const decisions = await MemoryFiles.readDecisions(root)
expect(decisions).toContain('"trigger":"explicit"')
expect(decisions).toContain('"sessionID":"ses_memory_event"')
expect(decisions).toContain('"llm":false')
})
test("explicit forget reports removals without save wording", async () => {
@@ -288,9 +284,6 @@ describe("KiloMemory integration", () => {
expect(events.some((event) => event.detail?.message === "Memory updated · 1 removed")).toBe(true)
expect(events.some((event) => event.detail?.message?.includes("Memory saved"))).toBe(false)
const decisions = await MemoryFiles.readDecisions(root)
expect(decisions).toContain("explicit memory operation removed 1 entries")
expect(decisions).toContain("explicit memory operation matched no source memory")
})
test("environment prompt rebuilds stale session index format", async () => {
@@ -396,8 +389,8 @@ describe("KiloMemory integration", () => {
expect(after.sources).toEqual(before.sources)
expect(after.index).toBe(before.index)
expect(after.changes).toBe(before.changes)
expect(after.decisions).toBe(before.decisions)
expect(after.changes).toBe("")
expect(after.decisions).toBe("")
expect(after.sources.project).toContain("stable_fact")
expect(after.sources.project).not.toContain("disabled_fact")
expect(after.sources.corrections).not.toContain("Do not correct while disabled")
@@ -430,9 +423,6 @@ describe("KiloMemory integration", () => {
expect(shown.sources.project).toContain("- repo_style :: Repo convention: commit messages are concise.")
expect(shown.sources.project).not.toContain("reply_style")
expect(shown.sources.project).not.toContain("I prefer terse summaries")
expect(shown.decisions).toContain('"reason":"out_of_scope"')
expect(shown.decisions).not.toContain("reply_style")
expect(shown.decisions).not.toContain("I prefer terse summaries")
})
})
@@ -0,0 +1,74 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import path from "path"
import fs from "fs/promises"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Skill } from "../../src/skill"
import { Discovery } from "../../src/skill/discovery"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { Config } from "../../src/config/config"
import { Git } from "../../src/git"
import { provideInstance, testInstanceStoreLayer, tmpdir } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const skills = (home: string) =>
Skill.layer.pipe(
Layer.provide(Git.defaultLayer),
Layer.provide(Discovery.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Global.layerWith({ home })),
Layer.provide(RuntimeFlags.layer({ disableExternalSkills: false, disableClaudeCodeSkills: false })),
)
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer))
describe("non-Git global skills", () => {
it.live("loads global skills when the project is below the home directory", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
const project = path.join(tmp.path, "projects", "plain")
const roots = [".agents", ".claude"] as const
yield* Effect.promise(async () => {
await fs.mkdir(project, { recursive: true })
await Promise.all(
roots.map(async (root) => {
const name = `${root.slice(1)}-global`
const dir = path.join(tmp.path, root, "skills", name)
await fs.mkdir(dir, { recursive: true })
await Bun.write(
path.join(dir, "SKILL.md"),
`---
name: ${name}
description: Global ${root} skill.
---
# Global skill
`,
)
}),
)
})
yield* Effect.gen(function* () {
const skill = yield* Skill.Service
const list = yield* skill.all()
for (const root of roots) {
const name = `${root.slice(1)}-global`
expect(list.find((item) => item.name === name)?.location).toBe(
path.join(tmp.path, root, "skills", name, "SKILL.md"),
)
}
}).pipe(Effect.provide(skills(tmp.path)), provideInstance(project))
}),
)
})
@@ -115,7 +115,7 @@ describe("saveAlwaysRules", () => {
always: [],
ruleset: [],
})
expect(result).toBeUndefined()
expect(result.manual).toBe(false)
}),
),
)
@@ -204,7 +204,7 @@ describe("saveAlwaysRules", () => {
always: [],
ruleset: [],
})
expect(result).toBeUndefined()
expect(result.manual).toBe(false)
// curl was NOT in rules — still requires permission
const curlFiber = yield* ask({
@@ -255,7 +255,7 @@ describe("saveAlwaysRules", () => {
always: [],
ruleset: [],
})
expect(result).toBeUndefined()
expect(result.manual).toBe(false)
}),
),
)
@@ -325,7 +325,7 @@ describe("saveAlwaysRules", () => {
{ permission: "bash", pattern: "gh *", action: "ask" },
],
})
expect(result).toBeUndefined()
expect(result.manual).toBe(false)
}),
),
)
@@ -350,7 +350,7 @@ describe("saveAlwaysRules", () => {
ruleset,
hardRuleset: ruleset,
})
expect(result).toBeUndefined()
expect(result.manual).toBe(false)
}),
),
)
@@ -386,7 +386,7 @@ describe("saveAlwaysRules", () => {
],
hardRuleset: [{ permission: "*", pattern: "*", action: "deny" }],
})
expect(result).toBeUndefined()
expect(result.manual).toBe(false)
}),
),
)
@@ -448,7 +448,7 @@ describe("saveAlwaysRules", () => {
always: [],
ruleset: [],
})
expect(result).toBeUndefined()
expect(result.manual).toBe(false)
}),
),
)
@@ -486,7 +486,7 @@ describe("saveAlwaysRules", () => {
always: [],
ruleset: [],
})
expect(result).toBeUndefined()
expect(result.manual).toBe(false)
}),
),
)
@@ -522,7 +522,7 @@ describe("saveAlwaysRules", () => {
always: [],
ruleset: [],
})
expect(allowed).toBeUndefined()
expect(allowed.manual).toBe(false)
// "git status" should be denied (only matches broad deny)
const exit = yield* ask({
@@ -0,0 +1,84 @@
// kilocode_change - new file
// Verifies that Config.permission_origins attributes each permission key to the scope
// (global XDG vs local project) that last set it, which drives auto-approval provenance.
import { expect, test } from "bun:test"
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/config"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Npm } from "@opencode-ai/core/npm"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Env } from "../../../src/env"
import { Git } from "../../../src/git"
import { Auth } from "../../../src/auth"
import { Account } from "../../../src/account/account"
import { provideTestInstance } from "../../fixture/fixture"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { HttpClient } from "effect/unstable/http"
import { tmpdir } from "../../fixture/fixture"
const infra = CrossSpawnSpawner.defaultLayer.pipe(
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
)
const emptyAccount = Layer.mock(Account.Service)({
active: () => Effect.succeed(Option.none()),
activeOrg: () => Effect.succeed(Option.none()),
})
const emptyAuth = Layer.mock(Auth.Service)({ all: () => Effect.succeed({}) })
const noopNpm = Layer.mock(Npm.Service)({
install: () => Effect.void,
add: () => Effect.die("not implemented"),
which: () => Effect.succeed(Option.none()),
})
const unexpectedHttp = HttpClient.make((request) => Effect.die(`unexpected http request: ${request.method} ${request.url}`))
const testLayer = Config.layer.pipe(
Layer.provide(Git.defaultLayer),
Layer.provide(EffectFlock.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(emptyAuth),
Layer.provide(emptyAccount),
Layer.provideMerge(infra),
Layer.provide(noopNpm),
Layer.provide(Layer.succeed(HttpClient.HttpClient, unexpectedHttp)),
)
test("project config permission keys are attributed to the local scope", async () => {
await using tmp = await tmpdir()
const dir = path.join(tmp.path, "a")
const kilo = path.join(dir, ".kilo")
await fs.mkdir(kilo, { recursive: true })
await Bun.write(path.join(kilo, "kilo.json"), JSON.stringify({ permission: { bash: { "echo *": "allow" } } }))
await provideTestInstance({
directory: dir,
fn: async () => {
const cfg = await Effect.runPromise(
Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(testLayer)),
)
expect(cfg.permission?.bash).toEqual({ "echo *": "allow" })
expect(cfg.permission_origins?.bash).toEqual({ "echo *": "local" })
},
})
})
test("a scalar project bash permission maps to the '*' pattern under the local scope", async () => {
await using tmp = await tmpdir()
const dir = path.join(tmp.path, "a")
const kilo = path.join(dir, ".kilo")
await fs.mkdir(kilo, { recursive: true })
await Bun.write(path.join(kilo, "kilo.json"), JSON.stringify({ permission: { bash: "allow" } }))
await provideTestInstance({
directory: dir,
fn: async () => {
const cfg = await Effect.runPromise(
Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(testLayer)),
)
expect(cfg.permission_origins?.bash).toEqual({ "*": "local" })
},
})
})
@@ -0,0 +1,228 @@
import { test, expect, describe } from "bun:test"
import { Effect, Layer } from "effect"
import { Agent } from "../../../src/agent/agent"
import { Session } from "../../../src/session/session"
import { Permission } from "../../../src/permission"
import { PermissionProvenance } from "../../../src/kilocode/permission/provenance"
import { KiloSessionPrompt } from "../../../src/kilocode/session/prompt"
import { SessionID } from "../../../src/session/schema"
describe("PermissionProvenance", () => {
test("configSource maps the scope of a permission + pattern", () => {
expect(PermissionProvenance.configSource("edit", "*", { edit: { "*": "global" } })).toBe("global")
expect(PermissionProvenance.configSource("edit", "*", { edit: { "*": "local" } })).toBe("project")
expect(PermissionProvenance.configSource("edit", "*", undefined)).toBe("agent")
// Different patterns under one key can come from different scopes.
const mixed = { bash: { "git status": "global" as const, "npm test": "local" as const } }
expect(PermissionProvenance.configSource("bash", "git status", mixed)).toBe("global")
expect(PermissionProvenance.configSource("bash", "npm test", mixed)).toBe("project")
// A pattern not present under the key falls back to the agent default.
expect(PermissionProvenance.configSource("bash", "rm -rf", mixed)).toBe("agent")
})
test("evaluate returns the winning rule object, preserving its source tag", () => {
// The last matching rule wins; the returned object still carries the source we attached.
const ruleset: PermissionProvenance.SourcedRule[] = [
{ permission: "edit", pattern: "*", action: "ask", source: "agent" },
{ permission: "edit", pattern: "src/*", action: "allow", source: "global" },
]
const winner = Permission.evaluate("edit", "src/index.ts", ruleset)
expect((winner as PermissionProvenance.SourcedRule).source).toBe("global")
})
test("classify reads a tagged rule's source and carries the agent name", () => {
const rule = { permission: "edit", pattern: "*", action: "allow" as const, source: "agent" as const }
expect(PermissionProvenance.classify({ rule, agent: "build", origins: undefined })).toEqual({
source: "agent",
agent: "build",
rule: { permission: "edit", pattern: "*", action: "allow" },
})
})
test("classify treats an untagged broad allow as yolo", () => {
const out = PermissionProvenance.classify({
rule: { permission: "*", pattern: "*", action: "allow" },
agent: "build",
origins: undefined,
})
expect(out.source).toBe("yolo")
})
test("classify falls back to config origins for an untagged rule", () => {
const out = PermissionProvenance.classify({
rule: { permission: "edit", pattern: "src/*", action: "allow" },
agent: "build",
origins: { edit: { "src/*": "local" } },
})
expect(out.source).toBe("project")
})
test("classify without a rule reports the ask fallback", () => {
expect(PermissionProvenance.classify({ agent: "build", origins: undefined })).toEqual({ source: "default" })
})
test("tagAgent stamps each rule by permission + pattern, defaulting to agent", () => {
const tagged = PermissionProvenance.tagAgent(
[
{ permission: "bash", pattern: "git status", action: "allow" },
{ permission: "bash", pattern: "npm test", action: "allow" },
{ permission: "edit", pattern: "*", action: "allow" },
],
// Global and project each contribute a different pattern under the same bash key.
{ bash: { "git status": "global", "npm test": "local" } },
)
expect(tagged.map((r) => r.source)).toEqual(["global", "project", "agent"])
})
test("tagSession marks the broad allow as yolo and other rules as session", () => {
const tagged = PermissionProvenance.tagSession([
{ permission: "*", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "git *", action: "allow" },
])
expect(tagged.map((r) => r.source)).toEqual(["yolo", "session"])
})
test("a tagged agent rule wins over an untagged duplicate and is not misread as yolo", () => {
// Regression: guardPermissions re-appends agent rules for ask/plan/architect; every rule that
// reaches evaluate must be tagged so the broad agent allow is not mistaken for YOLO mode.
const agent = PermissionProvenance.tagAgent([{ permission: "*", pattern: "*", action: "allow" }], undefined)
const session = PermissionProvenance.tagSession([])
const ruleset = [...agent, ...session, ...agent] // mirrors merge(tagged, guardPermissions(...)) for a mode
const winner = Permission.evaluate("bash", "echo hi", ruleset)
expect(PermissionProvenance.classify({ rule: winner, agent: "plan", origins: undefined })).toEqual({
source: "agent",
agent: "plan",
rule: { permission: "*", pattern: "*", action: "allow" },
})
})
})
describe("PermissionProvenance.carryApproval", () => {
const approval = { source: "agent" as const, agent: "build" }
test("carries a prior approval onto a replacement that omits it", () => {
// The tool overwrites metadata during execution; the approval written during ask() must survive.
expect(PermissionProvenance.carryApproval({ approval }, { command: "echo hi" })).toEqual({
command: "echo hi",
approval,
})
})
test("does not override an approval the replacement sets itself", () => {
const next = { approval: { source: "yolo" as const } }
expect(PermissionProvenance.carryApproval({ approval }, next)).toBe(next)
})
test("leaves the replacement untouched when there is no prior approval", () => {
const next = { command: "echo hi" }
expect(PermissionProvenance.carryApproval({ command: "old" }, next)).toBe(next)
})
test("returns the replacement as-is when it is undefined", () => {
expect(PermissionProvenance.carryApproval({ approval }, undefined)).toBeUndefined()
})
})
describe("askPermission returns provenance", () => {
const sessionID = SessionID.make("ses_prov")
const agent: Agent.Info = {
name: "build",
mode: "primary",
permission: Permission.fromConfig({ edit: "allow" }),
options: {},
}
const session = { id: sessionID, permission: [] } as unknown as Session.Info
const run = (outcome: Permission.AskOutcome, origins?: PermissionProvenance.Origins) =>
Effect.gen(function* () {
return yield* KiloSessionPrompt.askPermission({
permission: yield* Permission.Service,
agents: yield* Agent.Service,
sessions: yield* Session.Service,
origins,
agent,
session,
request: { sessionID, permission: "edit", patterns: ["src/index.ts"], always: [], metadata: {} },
})
}).pipe(
Effect.provide(
Layer.mergeAll(
Layer.mock(Permission.Service)({ ask: () => Effect.succeed(outcome) }),
Layer.mock(Agent.Service)({ get: () => Effect.succeed(agent) }),
Layer.mock(Session.Service)({ get: () => Effect.succeed(session) }),
),
),
Effect.runPromise,
)
test("manual reply reports the manual source", async () => {
expect(await run({ manual: true })).toEqual({ source: "manual" })
})
test("agent-default rule classifies as agent with its name", async () => {
const rule = { permission: "edit", pattern: "*", action: "allow" as const, source: "agent" as const }
expect(await run({ manual: false, rule })).toEqual({
source: "agent",
agent: "build",
rule: { permission: "edit", pattern: "*", action: "allow" },
})
})
test("untagged rule falls back to config origins", async () => {
const out = await run(
{ manual: false, rule: { permission: "edit", pattern: "src/*", action: "allow" } },
{ edit: { "src/*": "local" } },
)
expect(out.source).toBe("project")
})
test("global and project patterns under the same key are attributed independently", async () => {
// global: bash "git status" allow; project: bash "npm test" allow -> both live under bash.
const origins = { bash: { "git status": "global" as const, "npm test": "local" as const } }
const fromGlobal = await run({ manual: false, rule: { permission: "bash", pattern: "git status", action: "allow" } }, origins)
expect(fromGlobal.source).toBe("global")
const fromProject = await run({ manual: false, rule: { permission: "bash", pattern: "npm test", action: "allow" } }, origins)
expect(fromProject.source).toBe("project")
})
test("every rule passed to ask is tagged, even the guardPermissions re-append for modes", async () => {
// Regression guard: a plan/ask/architect agent's rules are duplicated by guardPermissions.
// Capture the ruleset askPermission builds and confirm no rule reaches evaluate untagged.
const captured: Permission.Ruleset[] = []
const planAgent: Agent.Info = {
name: "plan",
mode: "primary",
permission: Permission.fromConfig({ bash: "allow" }),
options: {},
}
const planSession = { id: sessionID, permission: [{ permission: "edit", pattern: "*", action: "deny" }] } as unknown as Session.Info
await Effect.gen(function* () {
yield* KiloSessionPrompt.askPermission({
permission: yield* Permission.Service,
agents: yield* Agent.Service,
sessions: yield* Session.Service,
agent: planAgent,
session: planSession,
request: { sessionID, permission: "bash", patterns: ["echo hi"], always: [], metadata: {} },
})
}).pipe(
Effect.provide(
Layer.mergeAll(
Layer.mock(Permission.Service)({
ask: (req) =>
Effect.sync(() => {
captured.push(req.ruleset)
return { manual: false } as const
}),
}),
Layer.mock(Agent.Service)({ get: () => Effect.succeed(planAgent) }),
Layer.mock(Session.Service)({ get: () => Effect.succeed(planSession) }),
),
),
Effect.runPromise,
)
const ruleset = captured[0]
expect(ruleset.length).toBeGreaterThan(0)
expect(ruleset.every((rule) => (rule as PermissionProvenance.SourcedRule).source !== undefined)).toBe(true)
})
})
@@ -0,0 +1,581 @@
import { describe, expect, test } from "bun:test"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { RemoteAttachments } from "../../src/kilocode/remote-attachments"
import { PartID, SessionID } from "../../src/session/schema"
async function tmpRoot() {
return fs.mkdtemp(path.join(os.tmpdir(), "remote-attachments-test-"))
}
function scratch(root: string, sessionID: string) {
return path.join(root, RemoteAttachments.SCRATCH_DIRNAME, Buffer.from(sessionID).toString("base64url"))
}
const nolog = {
warn: () => {},
error: () => {},
}
function okResponse(body: Uint8Array | string, init: ResponseInit = {}): Response {
const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body
return new Response(bytes as BodyInit, { status: 200, ...init })
}
function jsonResponse(status: number) {
return new Response(JSON.stringify({ ok: false }), { status })
}
describe("RemoteAttachments.classify / EXTENSION_MIME", () => {
test("every table entry classifies to its declared MIME", () => {
for (const [ext, mime] of Object.entries(RemoteAttachments.EXTENSION_MIME)) {
const r = RemoteAttachments.classify(`file.${ext}`)
expect(r.mime).toBe(mime)
expect(r.extension).toBe(ext)
}
})
test("fallback returns application/octet-stream for unknown extensions", () => {
const r = RemoteAttachments.classify("file.xyz")
expect(r.mime).toBe("application/octet-stream")
expect(r.extension).toBe("xyz")
})
test("extensionless file falls back to bin → application/octet-stream", () => {
const r = RemoteAttachments.classify("README")
expect(r.extension).toBe("bin")
expect(r.mime).toBe("application/octet-stream")
})
test("undefined filename falls back to bin → application/octet-stream", () => {
const r = RemoteAttachments.classify(undefined)
expect(r.extension).toBe("bin")
expect(r.mime).toBe("application/octet-stream")
})
test("dot-suffix file (no extension chars) falls back to bin", () => {
const r = RemoteAttachments.classify("file.")
expect(r.extension).toBe("bin")
expect(r.mime).toBe("application/octet-stream")
})
test("case-insensitive extension lookup", () => {
const r = RemoteAttachments.classify("PHOTO.PNG")
expect(r.extension).toBe("png")
expect(r.mime).toBe("image/png")
})
test("cross-surface: a valid extension not in the canonical table is the binary fallback", () => {
// `.weirdo` is a real, safe single-token extension but not in EXTENSION_MIME,
// so it must classify as the binary fallback (no MIME-based extension inference).
const r = RemoteAttachments.classify("attachment.weirdo")
expect(r.extension).toBe("weirdo")
expect(r.mime).toBe("application/octet-stream")
expect(RemoteAttachments.mimeFor("weirdo")).toBe("application/octet-stream")
})
test("safeExtension rejects path-traversal and oversized tokens", () => {
expect(RemoteAttachments.safeExtension("../../etc/passwd")).toBe("bin")
expect(RemoteAttachments.safeExtension("a/b")).toBe("bin")
expect(RemoteAttachments.safeExtension("a b")).toBe("bin")
expect(RemoteAttachments.safeExtension("a".repeat(17))).toBe("bin")
expect(RemoteAttachments.safeExtension("png")).toBe("png")
})
})
describe("RemoteAttachments.isFetchable", () => {
test("matches http and https", () => {
expect(RemoteAttachments.isFetchable("https://acct.r2.cloudflarestorage.com/abc")).toBe(true)
expect(RemoteAttachments.isFetchable("http://acct.r2.cloudflarestorage.com/abc")).toBe(true)
expect(RemoteAttachments.isFetchable("HTTPS://acct.r2.cloudflarestorage.com/abc")).toBe(true)
})
test("rejects non-http schemes", () => {
expect(RemoteAttachments.isFetchable("data:text/plain,hi")).toBe(false)
expect(RemoteAttachments.isFetchable("file:///etc/passwd")).toBe(false)
expect(RemoteAttachments.isFetchable("ftp://acct.r2.cloudflarestorage.com/abc")).toBe(false)
})
})
describe("RemoteAttachments.failureText", () => {
test("uses filename when provided", () => {
const t = RemoteAttachments.failureText("report.csv", "boom")
expect(t).toEqual({ type: "text", text: "attachment report.csv could not be retrieved: boom" })
})
test("uses generic label when filename is missing", () => {
const t = RemoteAttachments.failureText(undefined, "boom")
expect(t.text).toContain("attachment attachment could not be retrieved")
})
})
describe("RemoteAttachments.dataUrl", () => {
test("encodes bytes as base64 with the right mime prefix", () => {
const bytes = new TextEncoder().encode("hello world")
const url = RemoteAttachments.dataUrl("text/plain", bytes)
expect(url.startsWith("data:text/plain;base64,")).toBe(true)
const base64 = url.slice("data:text/plain;base64,".length)
expect(Buffer.from(base64, "base64").toString("utf8")).toBe("hello world")
})
})
describe("RemoteAttachments.fetchOne safety", () => {
test("rejects http:// (not https)", async () => {
let f: any
try {
await expect(
RemoteAttachments.fetchOne("http://acct.r2.cloudflarestorage.com/abc", {
fetch: (f = async () => okResponse("x")),
}),
).rejects.toMatchObject({ kind: "https" })
} finally {
void f
}
})
test("rejects malformed URL", async () => {
const secret = "secret-token"
const err = await RemoteAttachments.fetchOne(`not a url?token=${secret}`, {
fetch: async () => okResponse("x"),
}).then(
() => undefined,
(error) => error as Error,
)
expect(err).toMatchObject({ kind: "https" })
expect(err?.message).not.toContain(secret)
})
test("accepts only Cloudflare R2 hosts", async () => {
const f = async () => okResponse("x")
await expect(
RemoteAttachments.fetchOne("https://acct.r2.cloudflarestorage.com/file", { fetch: f }),
).resolves.toBeInstanceOf(Uint8Array)
await expect(
RemoteAttachments.fetchOne("https://bucket.acct.r2.cloudflarestorage.com/file", { fetch: f }),
).resolves.toBeInstanceOf(Uint8Array)
for (const url of [
"https://r2.cloudflarestorage.com/file",
"https://r2.cloudflarestorage.com.evil.test/file",
"https://internal.example/file",
"https://user:pass@acct.r2.cloudflarestorage.com/file",
]) {
await expect(RemoteAttachments.fetchOne(url, { fetch: f })).rejects.toBeDefined()
}
})
test("rejects redirects by passing redirect: error", async () => {
let captured: RequestInit | undefined
const f = async (_url: string, init?: RequestInit) => {
captured = init
// Simulate fetch rejecting when redirect: error + a redirect response
const err = new TypeError("Failed to fetch: redirect not allowed")
throw err
}
await expect(
RemoteAttachments.fetchOne("https://acct.r2.cloudflarestorage.com/abc", { fetch: f }),
).rejects.toMatchObject({
kind: "redirect",
})
expect(captured?.redirect).toBe("error")
})
test("rejects non-2xx responses", async () => {
const f = async () => jsonResponse(404)
await expect(
RemoteAttachments.fetchOne("https://acct.r2.cloudflarestorage.com/missing", { fetch: f }),
).rejects.toMatchObject({
kind: "non-2xx",
status: 404,
})
})
test("aborts + rejects when body exceeds MAX_BYTES", async () => {
const oversize = new Uint8Array(RemoteAttachments.MAX_BYTES)
// Build a stream that yields the entire oversize buffer in one chunk so the
// bounded reader trips the overflow guard.
const stream = new ReadableStream({
start(controller) {
controller.enqueue(oversize)
controller.close()
},
})
const f = async () => new Response(stream, { status: 200 })
await expect(
RemoteAttachments.fetchOne("https://acct.r2.cloudflarestorage.com/big", { fetch: f }),
).rejects.toMatchObject({
kind: "overflow",
})
})
test("aborts + rejects on timeout", async () => {
// A never-resolving fetch should be aborted by the timeout.
const f = (_url: string, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => {
reject(new DOMException("aborted", "AbortError"))
})
})
await expect(
RemoteAttachments.fetchOne("https://acct.r2.cloudflarestorage.com/slow", { fetch: f, timeoutMs: 25 }),
).rejects.toMatchObject({ kind: "timeout" })
})
test("keeps the timeout active while reading the body", async () => {
const stream = new ReadableStream({ pull: () => new Promise(() => {}) })
const f = async () => new Response(stream, { status: 200 })
await expect(
RemoteAttachments.fetchOne("https://acct.r2.cloudflarestorage.com/stalled", { fetch: f, timeoutMs: 25 }),
).rejects.toMatchObject({ kind: "timeout" })
})
test("forwards credentials: omit (no cookies on the wire)", async () => {
let captured: RequestInit | undefined
const f = async (_url: string, init?: RequestInit) => {
captured = init
return okResponse("hi")
}
await RemoteAttachments.fetchOne("https://acct.r2.cloudflarestorage.com/abc", { fetch: f })
expect(captured?.credentials).toBe("omit")
})
})
describe("RemoteAttachments.create().materialize", () => {
test("returns the input list when it has no file parts", async () => {
const root = await tmpRoot()
try {
const r = RemoteAttachments.create({ sessionID: SessionID.make("ses_a"), tmpRoot: root, log: nolog })
const out = await r.materialize([{ type: "text", text: "hi" }])
expect(out).toEqual([{ type: "text", text: "hi" }])
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("passes through non-fetchable URLs unchanged (data:, file:)", async () => {
const root = await tmpRoot()
try {
const r = RemoteAttachments.create({ sessionID: SessionID.make("ses_a"), tmpRoot: root, log: nolog })
const part = { type: "file" as const, mime: "image/png", filename: "a.png", url: "data:image/png;base64,AAAA" }
const out = await r.materialize([part])
expect(out).toEqual([part])
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("fetches a CSV and canonicalizes it to a text/plain data: URL", async () => {
const root = await tmpRoot()
try {
const f = async (url: string) => okResponse("a,b\n1,2\n")
const r = RemoteAttachments.create({
sessionID: SessionID.make("ses_csv"),
tmpRoot: root,
fetch: f,
log: nolog,
})
const out = await r.materialize([
{
id: PartID.make("prt_csv"),
type: "file",
mime: "text/csv",
filename: "report.csv",
url: "https://acct.r2.cloudflarestorage.com/report.csv",
},
])
expect(out).toHaveLength(1)
const file = out[0] as any
expect(file.type).toBe("file")
expect(file.id).toBe("prt_csv")
expect(file.mime).toBe("text/plain")
expect(file.filename).toBe("report.csv")
expect(file.url).toStartWith("data:text/plain;base64,")
expect(file).not.toHaveProperty("source")
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("fetches a PNG and emits an image/png data: URL file part", async () => {
const root = await tmpRoot()
try {
const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47])
const f = async () => okResponse(pngBytes)
const r = RemoteAttachments.create({
sessionID: SessionID.make("ses_png"),
tmpRoot: root,
fetch: f,
log: nolog,
})
const out = await r.materialize([
{
id: PartID.make("prt_png"),
type: "file",
mime: "image/png",
filename: "photo.png",
url: "https://acct.r2.cloudflarestorage.com/photo.png",
},
])
expect(out).toHaveLength(1)
const file = out[0] as any
expect(file.type).toBe("file")
expect(file.mime).toBe("image/png")
expect(file.url).toStartWith("data:image/png;base64,")
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("fetches a PDF and emits an application/pdf data: URL file part (NOT generic binary)", async () => {
const root = await tmpRoot()
try {
const pdfBytes = new Uint8Array([0x25, 0x50, 0x44, 0x46]) // %PDF
const f = async () => okResponse(pdfBytes)
const r = RemoteAttachments.create({
sessionID: SessionID.make("ses_pdf"),
tmpRoot: root,
fetch: f,
log: nolog,
})
const out = await r.materialize([
{
id: PartID.make("prt_pdf"),
type: "file",
mime: "application/pdf",
filename: "doc.pdf",
url: "https://acct.r2.cloudflarestorage.com/doc.pdf",
},
])
expect(out).toHaveLength(1)
const file = out[0] as any
expect(file.type).toBe("file")
expect(file.mime).toBe("application/pdf")
expect(file.url).toStartWith("data:application/pdf;base64,")
// No text part was emitted — the PDF falls through to the existing resolvePart path.
const dir = scratch(root, "ses_pdf")
const entries = await fs.readdir(dir).catch(() => [] as string[])
expect(entries).toEqual([])
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("writes a generic binary attachment to the scratch dir and emits a text part with absolute path", async () => {
const root = await tmpRoot()
try {
const bin = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7])
const f = async () => okResponse(bin)
const r = RemoteAttachments.create({
sessionID: SessionID.make("ses_bin"),
tmpRoot: root,
fetch: f,
log: nolog,
})
const out = await r.materialize([
{
id: PartID.make("prt_bin"),
type: "file",
mime: "application/octet-stream",
filename: "blob.bin",
url: "https://acct.r2.cloudflarestorage.com/blob.bin",
},
])
expect(out).toHaveLength(1)
const text = out[0] as any
expect(text.type).toBe("text")
const dir = scratch(root, "ses_bin")
expect(text.text).toContain(dir)
expect(text.text).toContain("filename: blob.bin")
expect(text.text).toContain("mime: application/octet-stream")
expect(text.text).toContain(`size: ${bin.byteLength} bytes`)
const entries = await fs.readdir(dir)
expect(entries).toHaveLength(1)
expect(entries[0]).toMatch(/^[0-9a-f-]{36}\.bin$/)
const written = await fs.readFile(path.join(dir, entries[0]!))
expect(Array.from(written)).toEqual(Array.from(bin))
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("confines schema-valid malicious attachment and session ids", async () => {
const root = await tmpRoot()
try {
const f = async () => okResponse(new Uint8Array([0]))
const sessionID = SessionID.make("ses_../../escaped")
const r = RemoteAttachments.create({
sessionID,
tmpRoot: root,
fetch: f,
log: nolog,
})
// Both branded IDs satisfy their schemas because only the prefix is checked.
const out = await r.materialize([
{
id: PartID.make("prt_../../escaped"),
type: "file",
mime: "application/octet-stream",
filename: "../../../etc/passwd",
url: "https://acct.r2.cloudflarestorage.com/x.bin",
},
])
const text = out[0] as any
const base = path.join(root, RemoteAttachments.SCRATCH_DIRNAME)
const dir = scratch(root, sessionID)
const entries = await fs.readdir(dir)
expect(path.relative(base, dir)).not.toStartWith("..")
expect(entries).toHaveLength(1)
expect(entries[0]).toMatch(/^[0-9a-f-]{36}\.bin$/)
expect(text.text).toContain(path.join(dir, entries[0]!))
expect(text.text).not.toContain("prt_../../escaped")
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("generates a uuid basename when attachmentId is missing", async () => {
const root = await tmpRoot()
try {
const f = async () => okResponse(new Uint8Array([0]))
const r = RemoteAttachments.create({
sessionID: SessionID.make("ses_noid"),
tmpRoot: root,
fetch: f,
log: nolog,
})
const out = await r.materialize([
{
type: "file",
mime: "application/octet-stream",
filename: "blob.bin",
url: "https://acct.r2.cloudflarestorage.com/blob.bin",
},
])
const text = out[0] as any
const dir = scratch(root, "ses_noid")
const entries = await fs.readdir(dir)
expect(entries).toHaveLength(1)
const name = entries[0]!
expect(name.endsWith(".bin")).toBe(true)
expect(name).not.toContain("blob")
expect(text.text).toContain(name)
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("replaces a failed attachment with an explanatory text part and keeps the rest of the prompt", async () => {
const root = await tmpRoot()
try {
const f = async (url: string) => {
if (url.endsWith("good.png")) return okResponse(new Uint8Array([1, 2, 3]))
return jsonResponse(500)
}
const r = RemoteAttachments.create({
sessionID: SessionID.make("ses_mix"),
tmpRoot: root,
fetch: f,
log: nolog,
})
const out = await r.materialize([
{ type: "text", text: "see attached" },
{
type: "file",
mime: "image/png",
filename: "good.png",
url: "https://acct.r2.cloudflarestorage.com/good.png",
},
{ type: "file", mime: "image/png", filename: "bad.png", url: "https://acct.r2.cloudflarestorage.com/bad.png" },
])
expect(out).toHaveLength(3)
expect((out[0] as any).type).toBe("text")
expect((out[1] as any).type).toBe("file")
expect((out[2] as any).type).toBe("text")
expect((out[2] as any).text).toContain("attachment bad.png could not be retrieved")
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("does not expose attachment URLs in errors or logs", async () => {
const root = await tmpRoot()
try {
const secret = "secret-token"
const url = `https://acct.r2.cloudflarestorage.com/file?token=${secret}`
const logs: unknown[] = []
const r = RemoteAttachments.create({
sessionID: SessionID.make("ses_secret"),
tmpRoot: root,
fetch: async () => {
throw new Error(url)
},
log: {
warn: (_msg, meta) => logs.push(meta),
error: (_msg, meta) => logs.push(meta),
},
})
const out = await r.materialize([{ type: "file", mime: "image/png", filename: "safe.png", url }])
expect(JSON.stringify({ out, logs })).not.toContain(secret)
expect(JSON.stringify({ out, logs })).not.toContain(url)
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("materialize after dispose fails fetchable parts closed without exposing the URL", async () => {
const root = await tmpRoot()
try {
const r = RemoteAttachments.create({ sessionID: SessionID.make("ses_d"), tmpRoot: root, log: nolog })
await r.dispose()
const out = await r.materialize([
{ type: "file", mime: "image/png", filename: "x.png", url: "https://acct.r2.cloudflarestorage.com/x.png" },
])
expect(out).toEqual([
{ type: "text", text: "attachment x.png could not be retrieved: attachment session is closed" },
])
expect(JSON.stringify(out)).not.toContain("cloudflarestorage.com")
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("dispose waits for an already-started materialize before removing scratch", async () => {
const root = await tmpRoot()
try {
let ready!: () => void
const started = new Promise<void>((resolve) => {
ready = resolve
})
let release!: (response: Response) => void
const response = new Promise<Response>((resolve) => {
release = resolve
})
const r = RemoteAttachments.create({
sessionID: SessionID.make("ses_concurrent"),
tmpRoot: root,
fetch: () => {
ready()
return response
},
log: nolog,
})
const materialize = r.materialize([
{
type: "file",
mime: "application/octet-stream",
filename: "blob.bin",
url: "https://acct.r2.cloudflarestorage.com/blob.bin",
},
])
await started
const dispose = r.dispose()
release(okResponse(new Uint8Array([1])))
await materialize
await dispose
await expect(fs.stat(scratch(root, "ses_concurrent"))).rejects.toMatchObject({ code: "ENOENT" })
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
})
@@ -103,6 +103,7 @@ const permission = Layer.mock(Permission.Service)({
ask: (input) =>
Effect.sync(() => {
approvals.push(input)
return { manual: false } as const
}),
})
const plugin = Layer.mock(Plugin.Service)({
@@ -151,8 +151,8 @@ describe("HttpApi memory", () => {
expect(String(show.index)).toContain("httpapi_memory")
expect(String(show.items)).toContain("httpapi_memory")
expect(String(rec(show.sources).project)).toContain("httpapi_memory")
expect(typeof show.decisions).toBe("string")
expect(String(show.decisions)).toContain('"sessionID":"ses_http_memory"')
expect(show.changes).toBe("")
expect(show.decisions).toBe("")
const forgotten = await json("POST", MemoryPaths.forget, { query: "httpapi_memory" })
expectOperation(forgotten)
@@ -12,7 +12,7 @@ import { Agent as AgentSvc } from "../../src/agent/agent"
import { BackgroundJob } from "../../src/background/job"
import { Bus } from "../../src/bus"
import { Command } from "../../src/command"
import { Auth } from "../../src/auth" // kilocode_change
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
@@ -25,6 +25,7 @@ import { Git } from "../../src/git"
import { Image } from "../../src/image/image"
import { KiloSession } from "../../src/kilocode/session"
import { KiloSessionPrompt } from "../../src/kilocode/session/prompt"
import { KiloSessions } from "../../src/kilo-sessions/kilo-sessions"
import { LSP } from "../../src/lsp/lsp"
import { MCP } from "../../src/mcp"
import { Permission } from "../../src/permission"
@@ -167,7 +168,8 @@ function makeHttp() {
Layer.provide(Format.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Command.defaultLayer),
Layer.provide(Auth.defaultLayer), // kilocode_change
Layer.provide(KiloSessions.testLayer),
Layer.provide(Auth.defaultLayer),
Layer.provideMerge(todo),
Layer.provideMerge(question),
Layer.provideMerge(deps),
@@ -710,4 +710,137 @@ describe("KiloCompactionChunks", () => {
},
})
})
test(
"compaction must not leak maxOutputTokens into agent options",
async () => {
await using tmp = await tmpdir()
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const session = await svc.create({})
const first = await user(session.id, "first " + "a".repeat(20_000))
await assistant(session.id, first.id, tmp.path, "reply " + "b".repeat(20_000))
const second = await user(session.id, "second " + "c".repeat(20_000))
await assistant(session.id, second.id, tmp.path, "reply " + "d".repeat(20_000))
await Effect.runPromise(
KiloSessionCompaction.create({
session: store,
sessionID: session.id,
agent: "build",
model: ref,
auto: false,
}),
)
const captured: Array<{ opts: Record<string, unknown>; modelLimitOutput: number }> = []
const bus = Bus.layer
const processor = Layer.effect(
SessionProcessorModule.SessionProcessor.Service,
Effect.gen(function* () {
const sessions = yield* SessionNs.Service
return SessionProcessorModule.SessionProcessor.Service.of({
create: Effect.fn("TestSessionProcessorLeak.create")((input) =>
Effect.succeed({
get message() {
return input.assistantMessage
},
updateToolCall: Effect.fn("TestSessionProcessorLeak.updateToolCall")(() =>
Effect.succeed(undefined),
),
metadata: Effect.fn("TestSessionProcessorLeak.metadata")(() => Effect.void),
completeToolCall: Effect.fn("TestSessionProcessorLeak.completeToolCall")(() => Effect.void),
process: Effect.fn("TestSessionProcessorLeak.process")((stream: LLM.StreamInput) =>
Effect.gen(function* () {
captured.push({
opts: stream.agent.options as Record<string, unknown>,
modelLimitOutput: stream.model.limit.output,
})
const text = stream.messages.some((msg) =>
JSON.stringify(msg).includes("Create a new anchored summary"),
)
? "final summary"
: "chunk summary"
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: input.assistantMessage.id,
sessionID: input.sessionID,
type: "text",
text,
})
input.assistantMessage.finish = "stop"
return "continue" as const
}),
),
} satisfies SessionProcessor.Handle),
),
})
}),
)
const model = ProviderTest.model({
providerID,
id: modelID,
limit: { context: 10_000, output: 1_000 },
})
const outputTokenMax = 512
const rt = ManagedRuntime.make(
Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, bus).pipe(
Layer.provide(ProviderTest.fake({ model }).layer),
Layer.provide(SessionNs.defaultLayer),
Layer.provide(agents),
Layer.provide(Plugin.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(RuntimeFlags.layer({ outputTokenMax })),
Layer.provide(bus),
Layer.provide(
Layer.mock(Config.Service)({
get: () => Effect.succeed({ ...{}, compaction: { reserved: 1_000 } }),
}),
),
),
)
try {
const msgs = await svc.messages({ sessionID: session.id })
const parent = msgs.at(-1)?.info.id
expect(parent).toBeTruthy()
const result = await rt.runPromise(
SessionCompaction.Service.use((svc) =>
svc.process({
parentID: parent!,
messages: msgs,
sessionID: session.id,
auto: false,
}),
),
)
expect(result).toBe("continue")
expect(captured.length).toBeGreaterThan(0)
// Negative assertion (the bug surfacing):
// maxOutputTokens must not appear in agent.options that the
// worker hands to the LLM. Today a strict OpenAI-compatible
// upstream rejects that field with
// Unsupported parameter(s): maxOutputTokens`.
for (const c of captured) {
expect(c.opts.maxOutputTokens).toBeUndefined()
}
// Positive assertion (budget preserved through an independent path):
// the constrained model still threads a tightened output limit
// through to every worker. If a future "fix" accidentally severs
// the only budget source along with the leak, this fails.
for (const c of captured) {
expect(c.modelLimitOutput).toBeLessThanOrEqual(outputTokenMax)
}
} finally {
await rt.dispose()
}
},
})
},
30_000,
)
})
@@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test"
import { KiloSessionProcessor } from "../../src/kilocode/session/processor"
describe("session generation id", () => {
test("extracts a bounded Gateway generation id", () => {
expect(
KiloSessionProcessor.generationID({
gateway: {
generationId: " gen_test-123 ",
routing: { finalProvider: "novita" },
marketCost: "0.1",
},
}),
).toBe("gen_test-123")
})
test("rejects arbitrary or oversized metadata values", () => {
expect(KiloSessionProcessor.generationID({ gateway: { generationId: "request-secret" } })).toBeUndefined()
expect(KiloSessionProcessor.generationID({ gateway: { generationId: `gen_${"a".repeat(201)}` } })).toBeUndefined()
expect(KiloSessionProcessor.generationID({ gateway: { generationId: 42 } })).toBeUndefined()
expect(KiloSessionProcessor.generationID({ openai: { responseId: "gen_response" } })).toBeUndefined()
})
})
@@ -0,0 +1,80 @@
// kilocode_change - new file
import { describe, expect, test } from "bun:test"
import { computeMetrics, formatRate } from "@/kilocode/session/metrics"
const tokens = {
input: 100,
output: 50,
reasoning: 0,
cache: { read: 0, write: 0 },
}
describe("kilocode.session.metrics.computeMetrics", () => {
test("derives generation rate from elapsed time", () => {
const metrics = computeMetrics({
tokens: { ...tokens, output: 100 },
elapsedMs: 1000,
})
expect(metrics?.source).toBe("computed")
expect(metrics?.generation).toBeCloseTo(100)
expect(metrics?.prompt).toBeUndefined()
})
test("returns undefined when there are no generation tokens", () => {
const metrics = computeMetrics({
tokens: { ...tokens, output: 0, reasoning: 0 },
elapsedMs: 2000,
})
expect(metrics).toBeUndefined()
})
test("guards against zero elapsed time", () => {
const metrics = computeMetrics({
tokens: { ...tokens, output: 50 },
elapsedMs: 0,
})
expect(metrics).toBeUndefined()
})
test("ignores providerMetadata until the upstream wiring lands (see #6579)", () => {
// llama.cpp surfaces prompt_per_second / predicted_per_second, but the
// upstream AI SDK drops them before the raw usage reaches our adapter.
// Until a metadataExtractor is wired into createOpenAICompatible, the
// provider source is unreachable — exercise the tolerance here.
const metrics = computeMetrics({
providerMetadata: {
llama: { prompt_per_second: 412.3, predicted_per_second: 28.7 },
},
tokens: { ...tokens, output: 100 },
elapsedMs: 2000,
})
expect(metrics?.source).toBe("computed")
expect(metrics?.generation).toBeCloseTo(50)
expect(metrics?.prompt).toBeUndefined()
})
test("tolerates missing providerMetadata", () => {
const metrics = computeMetrics({
tokens: { ...tokens, output: 200 },
elapsedMs: 4000,
})
expect(metrics?.source).toBe("computed")
expect(metrics?.generation).toBeCloseTo(50)
expect(metrics?.prompt).toBeUndefined()
})
})
describe("kilocode.session.metrics.formatRate", () => {
test.each([
[0, "0 t/s"],
[12, "12 t/s"],
[412.5, "412.5 t/s"],
[12345, "12,345 t/s"],
] as const)("formats %f as %s", (input, expected) => {
expect(formatRate(input)).toBe(expected)
})
test("returns zero string for negative inputs", () => {
expect(formatRate(-5)).toBe("0 t/s")
})
})
@@ -575,11 +575,19 @@ describe("session processor empty tool-calls", () => {
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({
index: 0,
reason: "stop",
reason: "other",
usage: usage(),
providerMetadata: { kilocode: { routedModelID: "openai/gpt-5.5-20260423" } },
providerMetadata: {
kilocode: { routedModelID: "openai/gpt-5.5-20260423" },
kilo: { vercelID: "fra1::test" },
gateway: {
generationId: "gen_test",
routing: { finalProvider: "openai" },
marketCost: "0.1",
},
},
}),
LLMEvent.finish({ reason: "stop", usage: usage() }),
LLMEvent.finish({ reason: "other", usage: usage() }),
)
const chat = yield* session.create({})
@@ -632,6 +640,10 @@ describe("session processor empty tool-calls", () => {
providerID: selection.providerID,
modelID: ModelV2.ID.make("openai/gpt-5.5-20260423"),
})
expect(part?.generationID).toBe("gen_test")
expect(part?.vercelID).toBe("fra1::test")
expect(part).not.toHaveProperty("providerMetadata")
expect(part).not.toHaveProperty("gateway")
}),
{ git: true },
),
@@ -72,13 +72,14 @@ function model(): Provider.Model {
} as Provider.Model
}
function empty() {
function empty(vercelID?: string) {
const usage = new Usage({})
const providerMetadata = vercelID ? { kilo: { vercelID } } : undefined
return [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.reasoningStart({ id: "reasoning" }),
LLMEvent.reasoningEnd({ id: "reasoning" }),
LLMEvent.stepFinish({ index: 0, reason: "unknown", usage }),
LLMEvent.stepFinish({ index: 0, reason: "unknown", usage, providerMetadata }),
LLMEvent.finish({ reason: "unknown", usage }),
]
}
@@ -242,9 +243,9 @@ describe("session processor incomplete response retry", () => {
(dir) =>
Effect.gen(function* () {
const ctx = yield* setup(dir)
yield* ctx.test.reply(...empty())
yield* ctx.test.reply(...empty())
yield* ctx.test.reply(...empty())
yield* ctx.test.reply(...empty("attempt-1"))
yield* ctx.test.reply(...empty("attempt-2"))
yield* ctx.test.reply(...empty("final-id"))
yield* ctx.test.push(Stream.fail(new Error("unexpected extra llm call")))
const delay = spyOn(SessionRetry, "delay").mockReturnValue(0)
@@ -260,6 +261,7 @@ describe("session processor incomplete response retry", () => {
expect(MessageV2.APIError.isInstance(error)).toBe(true)
if (!MessageV2.APIError.isInstance(error)) throw new Error("expected API error")
expect(error.data.message).toBe(KiloSessionProcessor.INCOMPLETE_RESPONSE_MESSAGE)
expect(error.data.responseHeaders?.["x-vercel-id"]).toBe("final-id")
expect(yield* MessageV2.parts(ctx.msg.id)).toEqual([])
}),
{ git: true },
@@ -11,7 +11,7 @@ import { Agent as AgentSvc } from "../../src/agent/agent"
import { BackgroundJob } from "../../src/background/job"
import { Bus } from "../../src/bus"
import { Command } from "../../src/command"
import { Auth } from "../../src/auth" // kilocode_change
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
@@ -51,6 +51,7 @@ import { Storage } from "../../src/storage/storage"
import { SyncEvent } from "../../src/sync"
import { ToolRegistry } from "../../src/tool/registry"
import { Truncate } from "../../src/tool/truncate"
import { KiloSessions } from "../../src/kilo-sessions/kilo-sessions"
import * as Log from "@opencode-ai/core/util/log"
import { MemoryService } from "@kilocode/kilo-memory/effect/service"
import { provideTmpdirServer } from "../fixture/fixture"
@@ -160,7 +161,8 @@ function makeHttp() {
Layer.provide(Format.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Command.defaultLayer),
Layer.provide(Auth.defaultLayer), // kilocode_change
Layer.provide(Auth.defaultLayer),
Layer.provide(KiloSessions.testLayer),
Layer.provideMerge(todo),
Layer.provideMerge(question),
Layer.provideMerge(deps),
@@ -15,7 +15,7 @@ import { Agent as AgentSvc } from "../../src/agent/agent"
import { BackgroundJob } from "../../src/background/job"
import { Bus } from "../../src/bus"
import { Command } from "../../src/command"
import { Auth } from "../../src/auth" // kilocode_change
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
@@ -52,6 +52,7 @@ import { Truncate } from "../../src/tool/truncate"
import { KiloHeadless } from "../../src/kilocode/permission/headless"
import { KiloSessionPrompt } from "../../src/kilocode/session/prompt"
import { KiloReadObject } from "../../src/kilocode/tool/read-object"
import { KiloSessions } from "../../src/kilo-sessions/kilo-sessions"
import { MemoryService } from "@kilocode/kilo-memory/effect/service"
import { provideTmpdirServer } from "../fixture/fixture"
import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
@@ -160,7 +161,8 @@ function makeHttp() {
Layer.provide(Format.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Command.defaultLayer),
Layer.provide(Auth.defaultLayer), // kilocode_change
Layer.provide(Auth.defaultLayer),
Layer.provide(KiloSessions.testLayer),
Layer.provideMerge(todo),
Layer.provideMerge(question),
Layer.provideMerge(deps),
@@ -8,6 +8,7 @@ import { AppRuntime } from "../../src/effect/app-runtime"
import { InstanceRef } from "../../src/effect/instance-ref"
import { KiloSessionCompaction } from "@/kilocode/session/compaction"
import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue"
import { KiloSession } from "@/kilocode/session"
import { Suggestion } from "../../src/kilocode/suggestion"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -22,6 +23,7 @@ import * as Log from "@opencode-ai/core/util/log"
import { disposeTestRuntime, provideInstance, testInstanceStoreLayer, tmpdir } from "../fixture/fixture"
import { Flag } from "@opencode-ai/core/flag/flag"
import { remove as cleanup } from "./cleanup"
import { pollWithTimeout } from "../lib/effect"
Log.init({ print: false })
@@ -99,6 +101,19 @@ function reply(input: { text: string; ready?: () => void; wait?: Promise<unknown
})
}
function providerCfg(url: string, agent: Record<string, unknown> = { code: { model: "alibaba/qwen-plus" } }) {
return {
$schema: "https://opencode.ai/config.json",
enabled_providers: ["alibaba"],
provider: {
alibaba: {
options: { apiKey: "test-key", baseURL: `${url}/v1` },
},
},
agent,
}
}
function hasText(msg: MessageV2.WithParts, text: string) {
return msg.parts.some((part) => part.type === "text" && part.text.includes(text))
}
@@ -446,26 +461,7 @@ describe("session prompt queue", () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: ["alibaba"],
provider: {
alibaba: {
options: {
apiKey: "test-key",
baseURL: `${server.url.origin}/v1`,
},
},
},
agent: {
code: {
model: "alibaba/qwen-plus",
},
},
}),
)
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify(providerCfg(server.url.origin)))
},
})
@@ -576,16 +572,7 @@ describe("session prompt queue", () => {
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: ["alibaba"],
provider: {
alibaba: {
options: { apiKey: "test-key", baseURL: `${server.url.origin}/v1` },
},
},
agent: { plan: { model: "alibaba/qwen-plus" } },
}),
JSON.stringify(providerCfg(server.url.origin, { plan: { model: "alibaba/qwen-plus" } })),
)
},
})
@@ -667,16 +654,7 @@ describe("session prompt queue", () => {
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: ["alibaba"],
provider: {
alibaba: {
options: { apiKey: "test-key", baseURL: `${server.url.origin}/v1` },
},
},
agent: { code: { model: "alibaba/qwen-plus" } },
}),
JSON.stringify(providerCfg(server.url.origin)),
)
},
})
@@ -854,4 +832,584 @@ describe("session prompt queue", () => {
},
})
})
test("drop cancels a queued prompt while preserving the active prompt", async () => {
const ready = Promise.withResolvers<void>()
const release = Promise.withResolvers<void>()
const calls: number[] = []
const server = Bun.serve({
port: 0,
fetch(req) {
const url = new URL(req.url)
if (!url.pathname.endsWith("/chat/completions")) return new Response("not found", { status: 404 })
calls.push(Date.now())
const wait = calls.length === 1 ? release.promise : undefined
return new Response(reply({ text: "reply", ready: calls.length === 1 ? ready.resolve : undefined, wait }), {
status: 200,
headers: { "Content-Type": "text/event-stream" },
})
},
})
try {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify(providerCfg(server.url.origin)),
)
},
})
await provideTestInstance({
directory: tmp.path,
fn: async () =>
scoped(tmp.path, async (prompt) => {
const session = await sessions.create({ title: "Queued drop" })
const activeID = MessageID.make("msg_active")
const queuedID = MessageID.make("msg_queued")
const first = Effect.runPromise(
prompt.prompt({
sessionID: session.id,
agent: "code",
messageID: activeID,
parts: [{ type: "text", text: "active prompt" }],
}),
)
await ready.promise
const second = Effect.runPromise(
prompt.prompt({
sessionID: session.id,
agent: "code",
messageID: queuedID,
parts: [{ type: "text", text: "queued prompt" }],
}),
)
await Effect.runPromise(
pollWithTimeout(
Effect.sync(() => (KiloSessionPromptQueue.hasFollowup(session.id) ? true : undefined)),
"Timed out waiting for queued prompt",
),
)
expect(await Effect.runPromise(KiloSessionPromptQueue.drop(session.id, queuedID))).toBe(true)
expect(await Effect.runPromise(KiloSessionPromptQueue.drop(session.id, activeID))).toBe(false)
release.resolve()
await Promise.all([first, second])
expect(calls).toHaveLength(1)
const msgs = await sessions.messages({ sessionID: session.id })
expect(msgs.filter((m) => m.info.role === "assistant")).toHaveLength(1)
expect(msgs.filter((m) => m.info.role === "assistant" && m.info.parentID === queuedID)).toHaveLength(0)
}),
})
} finally {
server.stop(true)
}
}, 10_000)
test("drop returns false for the actively running prompt", async () => {
const sessionID = SessionID.make("session_drop_active")
const ready = Promise.withResolvers<void>()
const done = Promise.withResolvers<void>()
const id = MessageID.make("msg_drop_active")
const first = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
id,
Effect.promise(async () => {
ready.resolve()
await done.promise
return "first"
}),
Effect.succeed("first-cancelled"),
),
)
await ready.promise
expect(await Effect.runPromise(KiloSessionPromptQueue.drop(sessionID, id))).toBe(false)
done.resolve()
await first
})
test("drop returns false for an unknown message", async () => {
const sessionID = SessionID.make("session_drop_unknown")
expect(await Effect.runPromise(KiloSessionPromptQueue.drop(sessionID, MessageID.make("msg_missing")))).toBe(false)
})
test("drop cancels a queued prompt and is idempotent", async () => {
const sessionID = SessionID.make("session_drop_queued")
const ready = Promise.withResolvers<void>()
const done = Promise.withResolvers<void>()
const firstID = MessageID.make("msg_drop_first")
const secondID = MessageID.make("msg_drop_second")
const first = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
firstID,
Effect.promise(async () => {
ready.resolve()
await done.promise
return "first"
}),
Effect.succeed("first-cancelled"),
),
)
await ready.promise
const second = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
secondID,
Effect.sync(() => "second"),
Effect.succeed("second-cancelled"),
),
)
expect(await Effect.runPromise(KiloSessionPromptQueue.drop(sessionID, secondID))).toBe(true)
expect(await Effect.runPromise(KiloSessionPromptQueue.drop(sessionID, secondID))).toBe(false)
done.resolve()
expect(await first).toBe("first")
expect(await second).toBe("second-cancelled")
})
test("drop on a middle prompt preserves later queued prompts", async () => {
const sessionID = SessionID.make("session_drop_middle")
const ready = Promise.withResolvers<void>()
const done = Promise.withResolvers<void>()
const calls: string[] = []
const firstID = MessageID.make("msg_drop_1")
const secondID = MessageID.make("msg_drop_2")
const thirdID = MessageID.make("msg_drop_3")
const first = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
firstID,
Effect.promise(async () => {
calls.push("first")
ready.resolve()
await done.promise
return "first"
}),
Effect.succeed("first-cancelled"),
),
)
await ready.promise
const second = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
secondID,
Effect.sync(() => {
calls.push("second")
return "second"
}),
Effect.succeed("second-cancelled"),
),
)
const third = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
thirdID,
Effect.sync(() => {
calls.push("third")
return "third"
}),
Effect.succeed("third-cancelled"),
),
)
expect(await Effect.runPromise(KiloSessionPromptQueue.drop(sessionID, secondID))).toBe(true)
expect(KiloSessionPromptQueue.hasFollowup(sessionID)).toBe(true)
done.resolve()
expect(await first).toBe("first")
expect(await second).toBe("second-cancelled")
expect(await third).toBe("third")
expect(calls).toEqual(["first", "third"])
expect(KiloSessionPromptQueue._hasInternalState(sessionID)).toBe(false)
})
// session.queue.changed event surface + snapshot accessor
describe("session.queue.changed", () => {
test("snapshot() returns an empty list for an unknown session", () => {
expect(KiloSessionPromptQueue.snapshot(SessionID.make("session_unknown"))).toEqual([])
})
test("enqueueing on an idle session does not transiently publish a non-empty snapshot", async () => {
// A prompt enqueued into an idle session starts almost immediately, so
// it must never appear in the waiting list (and must not emit any
// session.queue.changed event whose queued list is non-empty).
await using tmp = await tmpdir({ git: true })
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const sessionID = SessionID.make("session_queue_idle")
const events: Array<{ type: string; queued: string[] }> = []
const off = Bus.subscribe(KiloSession.Event.QueueChanged, (event) => {
events.push({ type: event.type, queued: [...(event.properties.queued as readonly string[])] })
})
try {
await Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
MessageID.make("msg_idle_1"),
Effect.succeed("done"),
Effect.succeed("cancelled"),
),
)
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([])
expect(events).toEqual([])
} finally {
off()
}
},
})
})
test("enqueueing while busy appends to the FIFO snapshot and emits the event", async () => {
await using tmp = await tmpdir({ git: true })
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const sessionID = SessionID.make("session_queue_busy")
const events: Array<{ sessionID: string; queued: string[] }> = []
const off = Bus.subscribe(KiloSession.Event.QueueChanged, (event) => {
if (event.properties.sessionID === sessionID) {
events.push({
sessionID: event.properties.sessionID as string,
queued: [...(event.properties.queued as readonly string[])],
})
}
})
const firstStarted = Promise.withResolvers<void>()
const firstRelease = Promise.withResolvers<void>()
const m1 = MessageID.make("msg_busy_1")
const m2 = MessageID.make("msg_busy_2")
const m3 = MessageID.make("msg_busy_3")
try {
const first = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m1,
Effect.gen(function* () {
firstStarted.resolve()
yield* Effect.promise(() => firstRelease.promise)
return "first" as const
}),
Effect.succeed("first-cancelled" as const),
),
)
await firstStarted.promise
// Idle-start for slot 1 must not have emitted anything.
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([])
expect(events).toEqual([])
const second = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m2,
Effect.succeed("second" as const),
Effect.succeed("second-cancelled" as const),
),
)
const third = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m3,
Effect.succeed("third" as const),
Effect.succeed("third-cancelled" as const),
),
)
// FIFO order is preserved: msg_busy_2 then msg_busy_3.
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([m2, m3])
// Publishes are fire-and-forget microtasks; let them flush.
await Bun.sleep(10)
expect(events).toEqual([
{ sessionID, queued: [m2] },
{ sessionID, queued: [m2, m3] },
])
firstRelease.resolve()
expect(await first).toBe("first")
expect(await second).toBe("second")
expect(await third).toBe("third")
} finally {
off()
}
},
})
})
test("dropping a queued prompt updates the FIFO snapshot and preserves later prompts", async () => {
await using tmp = await tmpdir({ git: true })
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const sessionID = SessionID.make("session_queue_drop")
const events: Array<{ queued: string[] }> = []
const off = Bus.subscribe(KiloSession.Event.QueueChanged, (event) => {
if (event.properties.sessionID === sessionID) {
events.push({ queued: [...(event.properties.queued as readonly string[])] })
}
})
const firstStarted = Promise.withResolvers<void>()
const firstRelease = Promise.withResolvers<void>()
const m1 = MessageID.make("msg_queue_drop_1")
const m2 = MessageID.make("msg_queue_drop_2")
const m3 = MessageID.make("msg_queue_drop_3")
try {
const first = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m1,
Effect.gen(function* () {
firstStarted.resolve()
yield* Effect.promise(() => firstRelease.promise)
return "first" as const
}),
Effect.succeed("first-cancelled" as const),
),
)
await firstStarted.promise
const second = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m2,
Effect.succeed("second" as const),
Effect.succeed("second-cancelled" as const),
),
)
const third = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m3,
Effect.succeed("third" as const),
Effect.succeed("third-cancelled" as const),
),
)
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([m2, m3])
await Effect.runPromise(
pollWithTimeout(
Effect.sync(() => (events.length >= 2 ? true : undefined)),
"Timed out waiting for queued snapshot events",
),
)
expect(events).toEqual([{ queued: [m2] }, { queued: [m2, m3] }])
events.length = 0
expect(await Effect.runPromise(KiloSessionPromptQueue.drop(sessionID, m2))).toBe(true)
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([m3])
expect(KiloSessionPromptQueue.hasFollowup(sessionID)).toBe(true)
await Effect.runPromise(
pollWithTimeout(
Effect.sync(() => (events.length >= 1 ? true : undefined)),
"Timed out waiting for dropped snapshot event",
),
)
expect(events).toEqual([{ queued: [m3] }])
firstRelease.resolve()
expect(await first).toBe("first")
expect(await second).toBe("second-cancelled")
expect(await third).toBe("third")
} finally {
off()
}
},
})
})
test("a waiting slot starting running shrinks the snapshot and emits the event", async () => {
await using tmp = await tmpdir({ git: true })
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const sessionID = SessionID.make("session_queue_start")
const events: Array<{ queued: string[] }> = []
const off = Bus.subscribe(KiloSession.Event.QueueChanged, (event) => {
if (event.properties.sessionID === sessionID) {
events.push({ queued: [...(event.properties.queued as readonly string[])] })
}
})
const firstStarted = Promise.withResolvers<void>()
const firstRelease = Promise.withResolvers<void>()
const secondStarted = Promise.withResolvers<void>()
const secondRelease = Promise.withResolvers<void>()
const m1 = MessageID.make("msg_start_1")
const m2 = MessageID.make("msg_start_2")
try {
const first = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m1,
Effect.gen(function* () {
firstStarted.resolve()
yield* Effect.promise(() => firstRelease.promise)
return "first" as const
}),
Effect.succeed("first-cancelled" as const),
),
)
await firstStarted.promise
const second = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m2,
Effect.gen(function* () {
secondStarted.resolve()
yield* Effect.promise(() => secondRelease.promise)
return "second" as const
}),
Effect.succeed("second-cancelled" as const),
),
)
// msg2 is waiting behind msg1.
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([m2])
await Bun.sleep(10)
expect(events.map((e) => e.queued)).toEqual([[m2]])
// Release msg1; msg2 takes over and the waiting list drops to empty.
firstRelease.resolve()
await secondStarted.promise
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([])
await Bun.sleep(10)
expect(events.map((e) => e.queued)).toEqual([[m2], []])
secondRelease.resolve()
expect(await first).toBe("first")
expect(await second).toBe("second")
} finally {
off()
}
},
})
})
test("cancel empties the snapshot and emits an empty list", async () => {
await using tmp = await tmpdir({ git: true })
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const sessionID = SessionID.make("session_queue_cancel")
const events: Array<{ queued: string[] }> = []
const off = Bus.subscribe(KiloSession.Event.QueueChanged, (event) => {
if (event.properties.sessionID === sessionID) {
events.push({ queued: [...(event.properties.queued as readonly string[])] })
}
})
const firstStarted = Promise.withResolvers<void>()
const firstRelease = Promise.withResolvers<void>()
const m1 = MessageID.make("msg_cancel_1")
const m2 = MessageID.make("msg_cancel_2")
const m3 = MessageID.make("msg_cancel_3")
try {
const first = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m1,
Effect.gen(function* () {
firstStarted.resolve()
yield* Effect.promise(() => firstRelease.promise)
return "first" as const
}),
Effect.succeed("first-cancelled" as const),
),
)
await firstStarted.promise
const second = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m2,
Effect.succeed("second" as const),
Effect.succeed("second-cancelled" as const),
),
)
const third = Effect.runPromise(
KiloSessionPromptQueue.enqueue(
sessionID,
m3,
Effect.succeed("third" as const),
Effect.succeed("third-cancelled" as const),
),
)
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([m2, m3])
await Bun.sleep(10)
const beforeCancel = events.length
expect(beforeCancel).toBeGreaterThan(0)
await Effect.runPromise(KiloSessionPromptQueue.cancel(sessionID))
// The most recent emission must be the empty list, and the snapshot
// must be empty for downstream replay callers.
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([])
await Bun.sleep(10)
expect(events.length).toBeGreaterThan(beforeCancel)
expect(events.at(-1)?.queued).toEqual([])
expect(events.slice(beforeCancel).every((e) => e.queued.length === 0)).toBe(true)
firstRelease.resolve()
expect(await first).toBe("first")
// Cancel bumped the version, so the queued slots return their
// cancelled effect instead of running their work.
expect(await second).toBe("second-cancelled")
expect(await third).toBe("third-cancelled")
} finally {
off()
}
},
})
})
test("cancel on an idle session suppresses the empty→empty emission", async () => {
// Steady-state no-op empty→empty emissions are intentionally suppressed
// by the queue to keep the bus quiet. Replay uses snapshot() directly
// and is therefore never affected by this suppression.
await using tmp = await tmpdir({ git: true })
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const sessionID = SessionID.make("session_queue_cancel_idle")
const events: Array<{ queued: string[] }> = []
const off = Bus.subscribe(KiloSession.Event.QueueChanged, (event) => {
if (event.properties.sessionID === sessionID) {
events.push({ queued: [...(event.properties.queued as readonly string[])] })
}
})
try {
await Effect.runPromise(KiloSessionPromptQueue.cancel(sessionID))
expect(KiloSessionPromptQueue.snapshot(sessionID)).toEqual([])
expect(events).toEqual([])
} finally {
off()
}
},
})
})
})
})
@@ -0,0 +1,50 @@
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { LLMAISDK } from "@/session/llm/ai-sdk"
import { KiloResponseMetadata } from "@/kilocode/session/response-metadata"
describe("session response metadata", () => {
test("carries x-vercel-id from an AI SDK response", async () => {
const events = await Effect.runPromise(
LLMAISDK.toLLMEvents(LLMAISDK.adapterState(), {
type: "finish-step",
response: {
id: "response-1",
timestamp: new Date(0),
modelId: "gpt-test",
headers: { "X-Vercel-Id": "fra1::abc" },
},
finishReason: "other",
rawFinishReason: undefined,
providerMetadata: undefined,
usage: {
inputTokens: 1,
outputTokens: 0,
totalTokens: 1,
inputTokenDetails: { noCacheTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 },
outputTokenDetails: { textTokens: 0, reasoningTokens: 0 },
},
}),
)
expect(events).toHaveLength(1)
const event = events[0]
if (event?.type !== "step-finish") throw new Error("expected step-finish")
expect(KiloResponseMetadata.read(event.providerMetadata)).toBe("fra1::abc")
})
test("does not add metadata when the header is absent", () => {
expect(KiloResponseMetadata.write(undefined, { server: "vercel" })).toBeUndefined()
})
test("normalizes valid Vercel IDs", () => {
const metadata = KiloResponseMetadata.write(undefined, { "x-vercel-id": " fra1::abc-123_test " })
expect(KiloResponseMetadata.read(metadata)).toBe("fra1::abc-123_test")
})
test("rejects unsafe or oversized Vercel IDs", () => {
expect(KiloResponseMetadata.write(undefined, { "x-vercel-id": "fra1::<script>" })).toBeUndefined()
expect(KiloResponseMetadata.write(undefined, { "x-vercel-id": "x".repeat(201) })).toBeUndefined()
expect(KiloResponseMetadata.read({ kilo: { vercelID: "fra1::abc\nsecret" } })).toBeUndefined()
})
})
@@ -0,0 +1,200 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Session } from "@/session/session"
import { SessionTranscript } from "@/kilocode/session/transcript"
import { MessageID, PartID, SessionID } from "@/session/schema"
import { provideTmpdirInstance } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
const env = Layer.mergeAll(Session.defaultLayer, CrossSpawnSpawner.defaultLayer)
const it = testEffect(env)
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("test")
function seed(dir: string) {
return Effect.gen(function* () {
const sessions = yield* Session.Service
const session = yield* sessions.create({})
const user = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "user",
agent: "default",
model: { providerID, modelID },
time: { created: Date.now() },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.id,
sessionID: session.id,
type: "text",
text: "how do I rotate the signing keys?",
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.id,
sessionID: session.id,
type: "text",
text: "injected file dump that should not be transcribed",
synthetic: true,
})
const assistant = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "assistant",
parentID: user.id,
mode: "default",
agent: "default",
path: { cwd: dir, root: dir },
cost: 0,
tokens: { input: 1, output: 1, reasoning: 0, cache: { read: 0, write: 0 } },
modelID,
providerID,
time: { created: Date.now(), completed: Date.now() },
finish: "stop",
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: assistant.id,
sessionID: session.id,
type: "text",
text: "run the rotation script with --apply",
})
return session
})
}
function mention(id: SessionID) {
return {
type: "file" as const,
mime: "text/plain",
url: SessionTranscript.url(id),
filename: "past-chat.md",
source: {
type: "file" as const,
path: SessionTranscript.url(id),
text: { value: "@past chat", start: 0, end: 10 },
},
}
}
describe("SessionTranscript.resolve", () => {
it.live(
"injects the referenced session transcript as context",
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const past = yield* seed(dir)
const current = yield* sessions.create({})
const parts = yield* SessionTranscript.resolve(mention(past.id), {
messageID: MessageID.ascending(),
sessionID: current.id,
sessions,
})
expect(parts).toHaveLength(3)
const [note, transcript, file] = parts
expect(note.type).toBe("text")
expect(note.type === "text" && note.synthetic).toBe(true)
expect(note.type === "text" && note.text).toContain("Attached transcript of past chat")
expect(transcript.type === "text" && transcript.text).toContain("how do I rotate the signing keys?")
expect(transcript.type === "text" && transcript.text).toContain("run the rotation script with --apply")
expect(transcript.type === "text" && transcript.text).not.toContain(
"injected file dump that should not be transcribed",
)
expect(file.type).toBe("file")
expect(file.type === "file" && file.url).toBe(SessionTranscript.url(past.id))
}),
),
)
it.live(
"rejects sessions from a different workspace",
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const past = yield* seed(dir)
return yield* provideTmpdirInstance((other) =>
Effect.gen(function* () {
const current = yield* sessions.create({})
expect(other).not.toBe(dir)
const parts = yield* SessionTranscript.resolve(mention(past.id), {
messageID: MessageID.ascending(),
sessionID: current.id,
sessions,
})
expect(parts).toHaveLength(1)
expect(parts[0].type === "text" && parts[0].text).toContain("different workspace")
}),
)
}),
),
)
it.live(
"reports unknown or invalid session references",
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const current = yield* sessions.create({})
const missing = yield* SessionTranscript.resolve(mention(SessionID.make("ses_doesnotexist")), {
messageID: MessageID.ascending(),
sessionID: current.id,
sessions,
})
expect(missing).toHaveLength(1)
expect(missing[0].type === "text" && missing[0].text).toContain("not found")
const invalid = yield* SessionTranscript.resolve(
{ ...mention(SessionID.make("ses_bad")), url: "session:not-a-session" },
{
messageID: MessageID.ascending(),
sessionID: current.id,
sessions,
},
)
expect(invalid).toHaveLength(1)
expect(invalid[0].type === "text" && invalid[0].text).toContain("invalid session reference")
expect(dir).toBeTruthy()
}),
),
)
})
describe("SessionTranscript.format", () => {
it.live(
"truncates oversized transcripts keeping head and tail",
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const session = yield* sessions.create({})
const user = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "user",
agent: "default",
model: { providerID, modelID },
time: { created: Date.now() },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.id,
sessionID: session.id,
type: "text",
text: `START ${"x".repeat(2000)} END`,
})
const [msg] = yield* sessions.messages({ sessionID: session.id })
const text = SessionTranscript.format(session, [msg], { max: 600 })
expect(text.length).toBeLessThan(700)
expect(text).toContain("characters omitted")
expect(text).toContain("START")
expect(text).toContain("END")
}),
),
)
})
@@ -744,4 +744,273 @@ describe("AttachedState", () => {
await replacement
expect([...state.union()].sort()).toEqual(["ses_x"])
})
// AC6d: announce(id) must forward { requireSessionId: id } to the
// heartbeat callback so the relay only resolves the attach once a fresh
// heartbeat whose payload contains that id was actually sent. Presence
// fire-and-forget heartbeats (from setPresence) continue to call
// without an id and resolve on any fresh send.
test("announce(id) forwards { requireSessionId: id } to the heartbeat callback", async () => {
const calls: Array<{ requireSessionId?: string }> = []
const state = AttachedState.create({
heartbeat: (opts) => {
calls.push(opts ? { ...opts } : {})
return Promise.resolve()
},
log: nolog,
})
// setPresence fires a fire-and-forget heartbeat with NO id.
state.setPresence(["ses_a"])
await Promise.resolve()
// announce(id) forwards the id to the awaited heartbeat.
await state.announce("ses_b")
expect(calls).toEqual([{}, { requireSessionId: "ses_b" }])
})
// K1 W1: detach semantics — basic happy path.
test("detach removes the id from both sets and awaits a heartbeat whose payload no longer contains it", async () => {
let detachResolved = false
const state = AttachedState.create({
heartbeat: (opts) => {
if (opts?.detachSessionId) {
detachResolved = true
return Promise.resolve()
}
return Promise.resolve()
},
log: nolog,
})
state.setPresence(["ses_a"])
await Promise.resolve()
expect(state.has("ses_a")).toBe(true)
// Detach awaits a heartbeat whose payload no longer contains ses_a.
// The state machine removes the id synchronously before awaiting.
await state.detach("ses_a")
expect(detachResolved).toBe(true)
expect([...state.union()]).toEqual([])
})
// K1 W1: detach surfaces a specific error for an id this CLI does not own.
test("detach throws for an id this CLI does not own (no silent re-attach)", async () => {
const state = AttachedState.create({
heartbeat: () => Promise.resolve(),
log: nolog,
})
await expect(state.detach("ses_missing")).rejects.toThrow("not owned")
})
// K1 W1: heartbeat failure during detach rolls back by restoring ownership.
test("detach rolls back by restoring prior ownership on heartbeat failure", async () => {
const state = AttachedState.create({
heartbeat: (opts) => {
if (opts?.detachSessionId) return Promise.reject(new Error("relay down"))
return Promise.resolve()
},
log: nolog,
})
state.setPresence(["ses_a"])
await Promise.resolve()
await expect(state.detach("ses_a")).rejects.toThrow("relay down")
// The id must be back in presence so a future setPresence does not
// accidentally treat the session as detached.
expect(state.has("ses_a")).toBe(true)
})
// K1 W1: suppression tombstone prevents a presence replacement that
// still includes a just-exited id from instantly re-adopting it.
test("setPresence does not re-adopt a detached id while presence still reports it", async () => {
const state = AttachedState.create({
heartbeat: () => Promise.resolve(),
log: nolog,
})
state.setPresence(["ses_a", "ses_b"])
expect(state.has("ses_a")).toBe(true)
// Detach ses_a; the tombstone is set BEFORE the sets are mutated.
await state.detach("ses_a")
expect(state.has("ses_a")).toBe(false)
// A presence churn that still includes ses_a must NOT re-adopt it
// (the relay is the source of truth and the upstream side has not
// dropped the id yet).
state.setPresence(["ses_a", "ses_b"])
expect(state.has("ses_a")).toBe(false)
// Once presence genuinely drops ses_a, the tombstone is released
// and a later real re-open (via announce) is not blocked.
state.setPresence(["ses_b"])
expect(state.has("ses_a")).toBe(false)
await state.announce("ses_a")
expect(state.has("ses_a")).toBe(true)
})
// K1 W1: has(id) reflects presence pending.
test("has(id) is true for presence-owned and pending ids, false otherwise", async () => {
const announced = Promise.withResolvers<void>()
const state = AttachedState.create({
heartbeat: (opts) => {
if (opts?.requireSessionId === "ses_pending") return announced.promise
return Promise.resolve()
},
log: nolog,
})
state.setPresence(["ses_present"])
expect(state.has("ses_present")).toBe(true)
expect(state.has("ses_pending")).toBe(false)
expect(state.has("ses_other")).toBe(false)
// Announce with a held heartbeat so the id sits in pending.
const p = state.announce("ses_pending")
await Promise.resolve()
expect(state.has("ses_pending")).toBe(true)
announced.resolve()
await p
expect(state.has("ses_pending")).toBe(true)
})
// K1 W1: reset() also clears the detach in-flight map and tombstones
// so a new connection lifecycle does not inherit stale state.
test("reset() clears tombstones and detach in-flight map", async () => {
const state = AttachedState.create({
heartbeat: () => Promise.resolve(),
log: nolog,
})
state.setPresence(["ses_a"])
await state.detach("ses_a")
state.setPresence(["ses_a"])
expect(state.has("ses_a")).toBe(false) // tombstone held
state.reset()
// After reset, a presence report including ses_a is accepted (the
// previous tombstone is gone).
state.setPresence(["ses_a"])
expect(state.has("ses_a")).toBe(true)
})
// K1 W1: a detach in flight for an id must NOT cause a concurrent
// announce(id) to join the detach fence and report a bogus attach. The
// announce must wait for the detach to settle and then genuinely re-attach.
test("announce awaits an in-flight detach and then really re-attaches (no opposite-op join)", async () => {
const detachHb = Promise.withResolvers<void>()
const calls: Array<{ requireSessionId?: string; detachSessionId?: string }> = []
const state = AttachedState.create({
heartbeat: (opts) => {
calls.push(opts ?? {})
if (opts?.detachSessionId === "ses_y") return detachHb.promise
return Promise.resolve()
},
log: nolog,
})
state.setPresence(["ses_y"])
await Promise.resolve()
const detachP = state.detach("ses_y") // holds on the detach fence, id removed
const announceP = state.announce("ses_y") // must await the detach, not join it
detachHb.resolve()
await detachP
await announceP
// The announce genuinely re-attached rather than resolving on the detach's
// "id absent" outcome, and it drove a real requireSessionId heartbeat.
expect(state.has("ses_y")).toBe(true)
expect(calls.some((c) => c.requireSessionId === "ses_y")).toBe(true)
})
// K1 W1: an announce in flight for an id must NOT cause a concurrent
// detach(id) to join the announce and report a bogus detach — exit_cli
// treats a resolved detach as license to ACK/close, so a false success is
// dangerous. The detach must wait for the announce, then really detach.
test("detach awaits an in-flight announce and then really detaches (no opposite-op join)", async () => {
const announceHb = Promise.withResolvers<void>()
const calls: Array<{ requireSessionId?: string; detachSessionId?: string }> = []
const state = AttachedState.create({
heartbeat: (opts) => {
calls.push(opts ?? {})
if (opts?.requireSessionId === "ses_x") return announceHb.promise
return Promise.resolve()
},
log: nolog,
})
const announceP = state.announce("ses_x") // holds on the attach fence
const detachP = state.detach("ses_x") // must await the announce, not join it
announceHb.resolve()
await announceP
await detachP
// The detach genuinely ran the negative-containment fence rather than
// resolving on the announce's success; the session is actually gone.
expect(state.has("ses_x")).toBe(false)
expect(calls.some((c) => c.detachSessionId === "ses_x")).toBe(true)
})
// K1 W1: after a failed detach rolls ownership back, the id is genuinely
// still attached, so the very next presence report that still includes it
// must keep it — the tombstone must have been released on rollback.
test("failed-detach rollback keeps the id attached across the next setPresence", async () => {
const state = AttachedState.create({
heartbeat: (opts) => {
if (opts?.detachSessionId) return Promise.reject(new Error("relay down"))
return Promise.resolve()
},
log: nolog,
})
state.setPresence(["ses_a"])
await Promise.resolve()
await expect(state.detach("ses_a")).rejects.toThrow("relay down")
expect(state.has("ses_a")).toBe(true)
// The realistic next presence event still reports ses_a. Without releasing
// the tombstone on rollback, setPresence's suppression loop would drop the
// still-attached id here and never clear the tombstone.
state.setPresence(["ses_a"])
expect(state.has("ses_a")).toBe(true)
})
// K1 W1: reset() clears the SAME set instances, so a stale in-flight
// announce that rejects after a reconnect must NOT roll back into the new
// lifecycle — doing so would delete a fresh post-reset announce's pending
// entry. The catch must honor the generation guard like the success path.
test("a stale announce rejecting after reset() does not corrupt the new lifecycle's pending set", async () => {
const hb1 = Promise.withResolvers<void>()
const hb2 = Promise.withResolvers<void>()
let calls = 0
const state = AttachedState.create({
heartbeat: (opts) => {
if (opts?.requireSessionId === "id") {
calls += 1
return calls === 1 ? hb1.promise : hb2.promise
}
return Promise.resolve()
},
log: nolog,
})
const a1 = state.announce("id") // installs pending, awaits hb1
void a1.then(
() => {},
() => {},
)
await Promise.resolve()
state.reset() // bumps generation, clears the (same) sets
const a2 = state.announce("id") // fresh lifecycle: re-installs pending, awaits hb2
await Promise.resolve()
hb1.reject(new Error("stale relay drop")) // the dead-lifecycle announce fails
await Promise.resolve()
await Promise.resolve()
// The stale rollback must NOT have deleted the fresh generation's entry.
expect(state.has("id")).toBe(true)
hb2.resolve()
await a2
expect(state.has("id")).toBe(true)
})
})
@@ -399,4 +399,72 @@ describe("share ingest queue", () => {
expect(urls.length).toBe(1)
expect(urls[0]).toBe("https://ingest.test/ingest?v=2")
})
test("agent_notification distinct ids survive one debounce batch", async () => {
const sent: unknown[] = []
const sched = scheduler(() => clock.now)
const q = IngestQueue.create({
now: () => clock.now,
setTimeout: sched.setTimeout,
clearTimeout: sched.clearTimeout,
log: { error: () => {} },
getShare: async () => ({ ingestPath: "/ingest" }),
getClient: async () => ({
url: "https://ingest.test",
fetch: async (_input, init) => {
sent.push(JSON.parse((init?.body as string) ?? "{}"))
return new Response("{}", { status: 200 })
},
}),
})
await q.sync("s-notify", [
{ type: "agent_notification", data: { id: "n1", message: "first" } },
{ type: "agent_notification", data: { id: "n2", message: "second" } },
])
clock.now = 1000
sched.run()
await Bun.sleep(0)
expect(sent.length).toBe(1)
const payload = sent[0] as { data: { type: string; data: { id: string; message: string } }[] }
expect(payload.data.length).toBe(2)
const ids = payload.data.map((d) => d.data.id).sort()
expect(ids).toEqual(["n1", "n2"])
})
test("agent_notification with the same id coalesces", async () => {
const sent: unknown[] = []
const sched = scheduler(() => clock.now)
const q = IngestQueue.create({
now: () => clock.now,
setTimeout: sched.setTimeout,
clearTimeout: sched.clearTimeout,
log: { error: () => {} },
getShare: async () => ({ ingestPath: "/ingest" }),
getClient: async () => ({
url: "https://ingest.test",
fetch: async (_input, init) => {
sent.push(JSON.parse((init?.body as string) ?? "{}"))
return new Response("{}", { status: 200 })
},
}),
})
await q.sync("s-notify", [{ type: "agent_notification", data: { id: "n1", message: "first" } }])
clock.now = 100
await q.sync("s-notify", [{ type: "agent_notification", data: { id: "n1", message: "second" } }])
clock.now = 1000
sched.run()
await Bun.sleep(0)
expect(sent.length).toBe(1)
const payload = sent[0] as { data: { type: string; data: { id: string; message: string } }[] }
expect(payload.data.length).toBe(1)
expect(payload.data[0].data.message).toBe("second")
})
})
@@ -104,6 +104,7 @@ describe("RemoteCommand", () => {
subtask: true,
},
],
canExitSession: true,
})
expect(JSON.stringify(catalog)).not.toContain("template")
expect(JSON.stringify(catalog)).not.toContain("secret-skill")
@@ -115,6 +116,9 @@ describe("RemoteCommand", () => {
{ name: "alpha", source: "command", hints: [], template: "alpha" },
])
expect(base.commands.map((item) => item.name)).toEqual(["alpha", "beta", "compact"])
// kilocode_change - K1 W1: canExitSession is always true, independent of
// exitAvailable (which gates the synthetic `/exit` entry).
expect(base.canExitSession).toBe(true)
const catalog = RemoteCommand.build(
[
@@ -155,16 +159,26 @@ describe("RemoteCommand", () => {
compaction: { create: async () => {} },
prompt: { loop: async () => {} },
})
expect((await remote.list()).commands.some((item) => item.name === "exit")).toBe(false)
// kilocode_change - K1 W1: canExitSession is true even when the synthetic
// `/exit` entry is absent (e.g. a headless `kilo remote` host has no
// RemoteExit callback, so `/exit` is gated off — but the host still
// interprets `exit_cli` as session-detach).
const baseList = await remote.list()
expect(baseList.canExitSession).toBe(true)
expect(baseList.commands.some((item) => item.name === "exit")).toBe(false)
const unregister = RemoteExit.register(async () => {})
try {
expect((await remote.list()).commands.some((item) => item.name === "exit")).toBe(true)
const list = await remote.list()
expect(list.commands.some((item) => item.name === "exit")).toBe(true)
expect(list.canExitSession).toBe(true)
} finally {
unregister()
}
expect((await remote.list()).commands.some((item) => item.name === "exit")).toBe(false)
const after = await remote.list()
expect(after.commands.some((item) => item.name === "exit")).toBe(false)
expect(after.canExitSession).toBe(true)
})
test("keeps compact and exit within command and byte caps", () => {
@@ -265,4 +265,199 @@ describe("RemoteProtocol", () => {
expect(result.data.type).toBe("heartbeat_ack")
}
})
// kilocode_change - K1 W1: instance advertisement + per-session platform
test("heartbeat without instance still parses (legacy compatibility)", () => {
const msg = { type: "heartbeat", sessions: [{ id: "ses_1", status: "busy", title: "Fix auth" }] }
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.instance).toBeUndefined()
}
})
test("heartbeat round-trips instance advertisement", () => {
const msg = {
type: "heartbeat",
sessions: [],
instance: { name: "mbp-igor", projectName: "cloud", version: "1.2.3" },
}
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.instance).toEqual({ name: "mbp-igor", projectName: "cloud", version: "1.2.3" })
}
// round-trip via JSON
const json = JSON.parse(JSON.stringify(result.success ? result.data : null))
const result2 = RemoteProtocol.Heartbeat.safeParse(json)
expect(result2.success).toBe(true)
if (result2.success) {
expect(result2.data.instance).toEqual({ name: "mbp-igor", projectName: "cloud", version: "1.2.3" })
}
})
test("instance advertisement version is optional", () => {
const msg = {
type: "heartbeat",
sessions: [],
instance: { name: "h", projectName: "p" },
}
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.instance?.version).toBeUndefined()
}
})
test("instance advertisement rejects empty name", () => {
const msg = {
type: "heartbeat",
sessions: [],
instance: { name: "", projectName: "p" },
}
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(false)
})
test("instance advertisement rejects oversized name", () => {
const msg = {
type: "heartbeat",
sessions: [],
instance: { name: "x".repeat(65), projectName: "p" },
}
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(false)
})
test("instance advertisement rejects oversized projectName", () => {
const msg = {
type: "heartbeat",
sessions: [],
instance: { name: "h", projectName: "p".repeat(65) },
}
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(false)
})
test("instance advertisement rejects oversized version", () => {
const msg = {
type: "heartbeat",
sessions: [],
instance: { name: "h", projectName: "p", version: "v".repeat(33) },
}
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(false)
})
test("session info accepts optional platform", () => {
const msg = {
type: "heartbeat",
sessions: [{ id: "s1", status: "busy", title: "t", platform: "vscode" }],
}
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.sessions[0].platform).toBe("vscode")
}
})
test("session info platform optional (legacy)", () => {
const msg = {
type: "heartbeat",
sessions: [{ id: "s1", status: "busy", title: "t" }],
}
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.sessions[0].platform).toBeUndefined()
}
})
test("session info rejects oversized platform", () => {
const msg = {
type: "heartbeat",
sessions: [{ id: "s1", status: "busy", title: "t", platform: "p".repeat(33) }],
}
const result = RemoteProtocol.Heartbeat.safeParse(msg)
expect(result.success).toBe(false)
})
test("full heartbeat round-trips sessions + instance", () => {
const msg = {
type: "heartbeat",
protocolVersion: "1.0.0",
sessions: [
{ id: "ses_1", status: "busy", title: "Fix auth", platform: "cli" },
{ id: "ses_2", status: "idle", title: "Sub task", parentSessionId: "ses_1", platform: "vscode" },
],
instance: { name: "mbp-igor", projectName: "cloud", version: "1.2.3" },
}
const json = JSON.parse(JSON.stringify(msg))
const result = RemoteProtocol.Heartbeat.safeParse(json)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.sessions).toHaveLength(2)
expect(result.data.sessions[0].platform).toBe("cli")
expect(result.data.sessions[1].platform).toBe("vscode")
expect(result.data.instance).toEqual({ name: "mbp-igor", projectName: "cloud", version: "1.2.3" })
expect(result.data.protocolVersion).toBe("1.0.0")
}
})
test("heartbeat without capabilities parses", () => {
const result = RemoteProtocol.Heartbeat.safeParse({
type: "heartbeat",
sessions: [],
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.capabilities).toBeUndefined()
}
})
test("heartbeat with capabilities.attachments parses", () => {
const result = RemoteProtocol.Heartbeat.safeParse({
type: "heartbeat",
sessions: [],
capabilities: { attachments: true },
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.capabilities?.attachments).toBe(true)
}
})
test("heartbeat with capabilities and no attachments key parses", () => {
const result = RemoteProtocol.Heartbeat.safeParse({
type: "heartbeat",
sessions: [],
capabilities: {},
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.capabilities?.attachments).toBeUndefined()
}
})
test("heartbeat rejects non-boolean capabilities.attachments", () => {
const result = RemoteProtocol.Heartbeat.safeParse({
type: "heartbeat",
sessions: [],
capabilities: { attachments: "yes" },
})
expect(result.success).toBe(false)
})
test("outbound union accepts heartbeat with capabilities", () => {
const result = RemoteProtocol.Outbound.safeParse({
type: "heartbeat",
sessions: [],
capabilities: { attachments: true },
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.type).toBe("heartbeat")
}
})
})
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,483 @@
import { expect, spyOn, beforeEach, afterEach } from "bun:test"
import { Effect, Fiber, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Auth } from "../../../src/auth"
import { Bus } from "../../../src/bus"
import type { Config } from "../../../src/config/config"
import { clearInFlightCache } from "../../../src/kilo-sessions/inflight-cache"
import { Session } from "../../../src/session/session"
import { TestConfig } from "../../fixture/config"
import { testEffect } from "../../lib/effect"
import { InstanceStore } from "../../../src/project/instance-store"
import { TestInstance, testInstanceStoreLayer, tmpdirScoped } from "../../fixture/fixture"
const KiloSessions = (await import("../../../src/kilo-sessions/kilo-sessions")).KiloSessions
let originalNotificationTimeout: string | undefined
beforeEach(() => {
originalNotificationTimeout = process.env.KILO_AGENT_NOTIFICATION_TIMEOUT_MS
process.env.KILO_AGENT_NOTIFICATION_TIMEOUT_MS = "50"
})
afterEach(() => {
if (originalNotificationTimeout === undefined) {
delete process.env.KILO_AGENT_NOTIFICATION_TIMEOUT_MS
} else {
process.env.KILO_AGENT_NOTIFICATION_TIMEOUT_MS = originalNotificationTimeout
}
})
const it = testEffect(CrossSpawnSpawner.defaultLayer)
const multi = testEffect(Layer.merge(CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer))
function layer(overrides: Partial<Config.Interface> = {}) {
return Layer.merge(
KiloSessions.layer.pipe(
Layer.provideMerge(Bus.layer),
Layer.provide(TestConfig.layer(overrides)),
Layer.provide(Session.defaultLayer),
),
Auth.defaultLayer,
)
}
function reset(...tokens: string[]) {
clearInFlightCache("kilo-sessions:token")
clearInFlightCache("kilo-sessions:client")
for (const token of tokens) clearInFlightCache(`kilo-sessions:token-valid:${token}`)
}
it.instance("dedicated immediate POST hits ingest path with an agent_notification item", () => {
const originalKey = process.env.KILO_API_KEY
const originalIngest = process.env.KILO_SESSION_INGEST_URL
const requests: { method: string; path: string; body?: unknown; headers: Record<string, string> }[] = []
const fetch: typeof globalThis.fetch = Object.assign(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)
const req = new Request(input, init)
const headers: Record<string, string> = {}
req.headers.forEach((v, k) => {
headers[k] = v
})
const body = req.method === "POST" ? await req.json().catch(() => undefined) : undefined
requests.push({ method: req.method, path: new URL(url).pathname, body, headers })
if (url.endsWith("/api/user")) return new Response("{}", { status: 200 })
if (url.endsWith("/api/session")) {
return Response.json({ id: "session-1", ingestPath: "/api/session/session-1/ingest" })
}
if (new URL(url).pathname.endsWith("/ingest")) return new Response("{}", { status: 200 })
return new Response("Not found", { status: 404 })
},
{ preconnect: globalThis.fetch.preconnect },
)
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
process.env.KILO_API_KEY = "test-token"
process.env.KILO_SESSION_INGEST_URL = "https://ingest.kilosessions.ai"
reset("test-token")
return Effect.gen(function* () {
const sessions = yield* KiloSessions.Service
yield* Effect.promise(() => KiloSessions.bootstrap("session-1"))
const result = yield* sessions.sendAgentNotification("session-1", {
id: "notif-1",
message: "Test notification",
})
expect(result).toEqual({ ok: true })
const ingestPosts = requests.filter((r) => r.method === "POST" && r.path.endsWith("/ingest"))
expect(ingestPosts).toHaveLength(1)
const ingestReq = ingestPosts[0]
expect(ingestReq.body).toEqual({
data: [{ type: "agent_notification", data: { id: "notif-1", message: "Test notification" } }],
})
expect(ingestReq.headers["authorization"]).toBe("Bearer test-token")
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove("kilo").pipe(Effect.orDie)
if (originalKey === undefined) delete process.env.KILO_API_KEY
else process.env.KILO_API_KEY = originalKey
if (originalIngest === undefined) delete process.env.KILO_SESSION_INGEST_URL
else process.env.KILO_SESSION_INGEST_URL = originalIngest
reset("test-token")
request.mockRestore()
}),
),
Effect.provide(layer()),
)
})
it.instance("2xx ingest response returns ok:true", () => {
const originalKey = process.env.KILO_API_KEY
const originalIngest = process.env.KILO_SESSION_INGEST_URL
const fetch: typeof globalThis.fetch = Object.assign(
async (input: RequestInfo | URL) => {
const url = String(input)
if (url.endsWith("/api/user")) return new Response("{}", { status: 200 })
if (url.endsWith("/api/session")) {
return Response.json({ id: "session-2", ingestPath: "/api/session/session-2/ingest" })
}
if (new URL(url).pathname.endsWith("/ingest")) return new Response("{}", { status: 200 })
return new Response("Not found", { status: 404 })
},
{ preconnect: globalThis.fetch.preconnect },
)
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
process.env.KILO_API_KEY = "test-token"
process.env.KILO_SESSION_INGEST_URL = "https://ingest.kilosessions.ai"
reset("test-token")
return Effect.gen(function* () {
const sessions = yield* KiloSessions.Service
yield* Effect.promise(() => KiloSessions.bootstrap("session-2"))
const result = yield* sessions.sendAgentNotification("session-2", {
id: "notif-2",
message: "Success test",
})
expect(result).toEqual({ ok: true })
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove("kilo").pipe(Effect.orDie)
if (originalKey === undefined) delete process.env.KILO_API_KEY
else process.env.KILO_API_KEY = originalKey
if (originalIngest === undefined) delete process.env.KILO_SESSION_INGEST_URL
else process.env.KILO_SESSION_INGEST_URL = originalIngest
reset("test-token")
request.mockRestore()
}),
),
Effect.provide(layer()),
)
})
it.instance("non-2xx ingest response returns ok:false with no retry", () => {
const originalKey = process.env.KILO_API_KEY
const originalIngest = process.env.KILO_SESSION_INGEST_URL
const requests: { method: string; path: string; body?: unknown; headers: Record<string, string> }[] = []
const fetch: typeof globalThis.fetch = Object.assign(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)
const req = new Request(input, init)
const headers: Record<string, string> = {}
req.headers.forEach((v, k) => {
headers[k] = v
})
const body = req.method === "POST" ? await req.json().catch(() => undefined) : undefined
requests.push({ method: req.method, path: new URL(url).pathname, body, headers })
if (url.endsWith("/api/user")) return new Response("{}", { status: 200 })
if (url.endsWith("/api/session")) {
return Response.json({ id: "session-3", ingestPath: "/api/session/session-3/ingest" })
}
if (new URL(url).pathname.endsWith("/ingest")) {
return new Response("Internal Server Error", { status: 500 })
}
return new Response("Not found", { status: 404 })
},
{ preconnect: globalThis.fetch.preconnect },
)
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
process.env.KILO_API_KEY = "test-token"
process.env.KILO_SESSION_INGEST_URL = "https://ingest.kilosessions.ai"
reset("test-token")
return Effect.gen(function* () {
const sessions = yield* KiloSessions.Service
yield* Effect.promise(() => KiloSessions.bootstrap("session-3"))
const result = yield* sessions.sendAgentNotification("session-3", {
id: "notif-3",
message: "Failure test",
})
expect(result).toEqual({ ok: false, reason: "http_500" })
const ingestPosts = requests.filter((r) => r.method === "POST" && r.path.endsWith("/ingest"))
expect(ingestPosts).toHaveLength(1)
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove("kilo").pipe(Effect.orDie)
if (originalKey === undefined) delete process.env.KILO_API_KEY
else process.env.KILO_API_KEY = originalKey
if (originalIngest === undefined) delete process.env.KILO_SESSION_INGEST_URL
else process.env.KILO_SESSION_INGEST_URL = originalIngest
reset("test-token")
request.mockRestore()
}),
),
Effect.provide(layer()),
)
})
it.instance("unauthenticated returns not_connected", () => {
const originalKey = process.env.KILO_API_KEY
const originalIngest = process.env.KILO_SESSION_INGEST_URL
const fetch: typeof globalThis.fetch = Object.assign(
async () => new Response("Unauthorized", { status: 401 }),
{ preconnect: globalThis.fetch.preconnect },
)
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
delete process.env.KILO_API_KEY
process.env.KILO_SESSION_INGEST_URL = "https://ingest.kilosessions.ai"
reset("test-token")
return Effect.gen(function* () {
const sessions = yield* KiloSessions.Service
const result = yield* sessions.sendAgentNotification("session-4", {
id: "notif-4",
message: "No auth test",
})
expect(result).toEqual({ ok: false, reason: "not_connected" })
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove("kilo").pipe(Effect.orDie)
if (originalKey === undefined) delete process.env.KILO_API_KEY
else process.env.KILO_API_KEY = originalKey
if (originalIngest === undefined) delete process.env.KILO_SESSION_INGEST_URL
else process.env.KILO_SESSION_INGEST_URL = originalIngest
reset("test-token")
request.mockRestore()
}),
),
Effect.provide(layer()),
)
})
it.instance("bounded timeout: stalled auth step returns not_connected within timeout", () => {
const originalKey = process.env.KILO_API_KEY
const originalIngest = process.env.KILO_SESSION_INGEST_URL
const fetch: typeof globalThis.fetch = Object.assign(
async (input: RequestInfo | URL) => {
const url = String(input)
if (url.endsWith("/api/user")) {
await new Promise(() => {})
return new Response("{}", { status: 200 })
}
return new Response("Not found", { status: 404 })
},
{ preconnect: globalThis.fetch.preconnect },
)
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
process.env.KILO_API_KEY = "test-token"
process.env.KILO_SESSION_INGEST_URL = "https://ingest.kilosessions.ai"
reset("test-token")
return Effect.gen(function* () {
const sessions = yield* KiloSessions.Service
const start = Date.now()
const result = yield* sessions.sendAgentNotification("session-5", {
id: "notif-5",
message: "Timeout test",
})
const elapsed = Date.now() - start
expect(result).toEqual({ ok: false, reason: "not_connected" })
expect(elapsed).toBeLessThan(200)
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove("kilo").pipe(Effect.orDie)
if (originalKey === undefined) delete process.env.KILO_API_KEY
else process.env.KILO_API_KEY = originalKey
if (originalIngest === undefined) delete process.env.KILO_SESSION_INGEST_URL
else process.env.KILO_SESSION_INGEST_URL = originalIngest
reset("test-token")
request.mockRestore()
}),
),
Effect.provide(layer()),
)
})
it.instance("coalescing: sendAgentNotification awaits in-flight bootstrap for a single /api/session POST", () => {
const originalKey = process.env.KILO_API_KEY
const originalIngest = process.env.KILO_SESSION_INGEST_URL
let resolveSession: ((value: Response) => void) | undefined
const sessionDeferred = new Promise<Response>((resolve) => {
resolveSession = resolve
})
let sessionPostResolve: (() => void) | undefined
const sessionPostSeen = new Promise<void>((resolve) => {
sessionPostResolve = resolve
})
const requests: { method: string; path: string; body?: unknown }[] = []
const fetch: typeof globalThis.fetch = Object.assign(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)
const pathname = new URL(url).pathname
const req = new Request(input, init)
const body = req.method === "POST" ? await req.json().catch(() => undefined) : undefined
if (pathname === "/api/session") {
requests.push({ method: "POST", path: pathname, body })
sessionPostResolve?.()
return sessionDeferred
}
if (url.endsWith("/api/user")) return new Response("{}", { status: 200 })
if (pathname.endsWith("/ingest")) {
requests.push({ method: "POST", path: pathname, body })
return new Response("{}", { status: 200 })
}
return new Response("Not found", { status: 404 })
},
{ preconnect: globalThis.fetch.preconnect },
)
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
process.env.KILO_API_KEY = "test-token"
process.env.KILO_SESSION_INGEST_URL = "https://ingest.kilosessions.ai"
reset("test-token")
return Effect.gen(function* () {
const sessions = yield* KiloSessions.Service
const createPromise = KiloSessions.create("session-6")
yield* Effect.promise(() => sessionPostSeen)
// Start sendAgentNotification while the bootstrap is in flight, then
// immediately resolve the bootstrap response so the in-flight tracker
// completes and the notification observes the shared ingest path.
const notifyFiber = yield* sessions.sendAgentNotification("session-6", {
id: "notif-6",
message: "Coalesced test",
}).pipe(Effect.forkChild)
resolveSession?.(Response.json({ id: "session-6", ingestPath: "/api/session/session-6/ingest" }))
const notificationResult = yield* Fiber.join(notifyFiber)
const createResult = yield* Effect.promise(() => createPromise)
expect(notificationResult).toEqual({ ok: true })
expect(createResult).toEqual({ id: "session-6", ingestPath: "/api/session/session-6/ingest" })
const bootstrapPosts = requests.filter((r) => r.path === "/api/session")
expect(bootstrapPosts).toHaveLength(1)
const ingestPosts = requests.filter((r) => r.path.endsWith("/ingest"))
const notificationPost = ingestPosts.find(
(r) => r.body && (r.body as { data: Array<{ type: string }> }).data?.[0]?.type === "agent_notification",
)
expect(notificationPost).toBeDefined()
expect(notificationPost!.body).toEqual({
data: [{ type: "agent_notification", data: { id: "notif-6", message: "Coalesced test" } }],
})
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove("kilo").pipe(Effect.orDie)
if (originalKey === undefined) delete process.env.KILO_API_KEY
else process.env.KILO_API_KEY = originalKey
if (originalIngest === undefined) delete process.env.KILO_SESSION_INGEST_URL
else process.env.KILO_SESSION_INGEST_URL = originalIngest
reset("test-token")
request.mockRestore()
}),
),
Effect.provide(layer()),
)
})
it.instance("coalescing: bootstrapInflight stores the real promise and concurrent create() calls share one POST", () => {
const originalKey = process.env.KILO_API_KEY
const originalIngest = process.env.KILO_SESSION_INGEST_URL
let resolveSession: ((value: Response) => void) | undefined
const sessionDeferred = new Promise<Response>((resolve) => {
resolveSession = resolve
})
const requests: { method: string; path: string; body?: unknown }[] = []
const fetch: typeof globalThis.fetch = Object.assign(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)
const pathname = new URL(url).pathname
const req = new Request(input, init)
const body = req.method === "POST" ? await req.json().catch(() => undefined) : undefined
if (pathname === "/api/session") {
requests.push({ method: "POST", path: pathname, body })
return sessionDeferred
}
if (url.endsWith("/api/user")) return new Response("{}", { status: 200 })
if (pathname.endsWith("/ingest")) {
requests.push({ method: "POST", path: pathname, body })
return new Response("{}", { status: 200 })
}
return new Response("Not found", { status: 404 })
},
{ preconnect: globalThis.fetch.preconnect },
)
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
process.env.KILO_API_KEY = "test-token"
process.env.KILO_SESSION_INGEST_URL = "https://ingest.kilosessions.ai"
reset("test-token")
return Effect.gen(function* () {
const sessions = yield* KiloSessions.Service
expect(KiloSessions._getBootstrapInflight("session-7")).toBeUndefined()
// Fire two concurrent create() calls for the same session.
const create1 = KiloSessions.create("session-7")
const create2 = KiloSessions.create("session-7")
// Synchronously after kicking off both creates, the internal tracker must
// hold the real in-flight promise (not undefined) so the second create
// coalesced onto the first one's POST instead of starting its own.
const inflight = KiloSessions._getBootstrapInflight("session-7")
expect(inflight).toBeDefined()
expect(inflight).toBeInstanceOf(Promise)
resolveSession?.(Response.json({ id: "session-7", ingestPath: "/api/session/session-7/ingest" }))
const result1 = yield* Effect.promise(() => create1)
const result2 = yield* Effect.promise(() => create2)
expect(result1).toEqual({ id: "session-7", ingestPath: "/api/session/session-7/ingest" })
expect(result2).toEqual(result1)
const bootstrapPosts = requests.filter((r) => r.path === "/api/session")
expect(bootstrapPosts).toHaveLength(1)
const notificationResult = yield* sessions.sendAgentNotification("session-7", {
id: "notif-7",
message: "Coalesced create test",
})
expect(notificationResult).toEqual({ ok: true })
const ingestPosts = requests.filter((r) => r.path.endsWith("/ingest"))
const notificationPost = ingestPosts.find(
(r) => r.body && (r.body as { data: Array<{ type: string }> }).data?.[0]?.type === "agent_notification",
)
expect(notificationPost).toBeDefined()
expect(notificationPost!.body).toEqual({
data: [{ type: "agent_notification", data: { id: "notif-7", message: "Coalesced create test" } }],
})
expect(KiloSessions._getBootstrapInflight("session-7")).toBeUndefined()
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove("kilo").pipe(Effect.orDie)
if (originalKey === undefined) delete process.env.KILO_API_KEY
else process.env.KILO_API_KEY = originalKey
if (originalIngest === undefined) delete process.env.KILO_SESSION_INGEST_URL
else process.env.KILO_SESSION_INGEST_URL = originalIngest
reset("test-token")
request.mockRestore()
}),
),
Effect.provide(layer()),
)
})
@@ -0,0 +1,60 @@
import { expect } from "bun:test"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Provider } from "@/provider/provider"
import { InstanceLayer } from "@/project/instance-layer"
import { Env } from "@/env"
import { Plugin } from "@/plugin"
import { provideInstanceEffect, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(
Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer, CrossSpawnSpawner.defaultLayer),
)
it.effect("loads Snowflake Cortex from OAuth credentials", () =>
Effect.acquireRelease(
Effect.sync(() => {
const value = process.env.KILO_AUTH_CONTENT
process.env.KILO_AUTH_CONTENT = JSON.stringify({
"snowflake-cortex": {
type: "oauth",
refresh: "refresh-token",
access: "access-token",
expires: 1,
accountId: "test-account",
},
})
return value
}),
(value) =>
Effect.sync(() => {
if (value === undefined) delete process.env.KILO_AUTH_CONTENT
else process.env.KILO_AUTH_CONTENT = value
}),
).pipe(
Effect.flatMap(() =>
Effect.gen(function* () {
const directory = yield* tmpdirScoped({
config: {
provider: {
"snowflake-cortex": {
name: "Snowflake Cortex",
npm: "@ai-sdk/openai-compatible",
models: { test: { name: "Test" } },
},
},
},
})
const provider = yield* Provider.use
.getProvider(ProviderV2.ID.make("snowflake-cortex"))
.pipe(provideInstanceEffect(directory), Effect.provide(InstanceLayer.layer))
expect(provider.options.baseURL).toBe("https://test-account.snowflakecomputing.com/api/v2/cortex/v1")
expect(provider.options.apiKey).toBe("access-token")
expect(provider.options.fetch).toBeFunction()
}),
),
),
)
@@ -1,10 +1,11 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { Database } from "bun:sqlite"
import { drizzle, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
import path from "path"
import os from "os"
import fs from "fs/promises"
import { readFileSync, readdirSync } from "fs"
import { Effect, Layer } from "effect"
import { Database as CoreDatabase } from "@opencode-ai/core/database/database"
import { JsonMigration } from "@/kilocode/storage/json-migration"
import { Global } from "@opencode-ai/core/global"
import { ProjectTable } from "@opencode-ai/core/project/sql"
@@ -82,42 +83,38 @@ async function writeSession(
await Bun.write(path.join(storageDir, "session", projectID, `${session.id}.json`), JSON.stringify(session))
}
// Helper to create in-memory test database with schema
function createTestDb() {
const sqlite = new Database(":memory:")
// Helper to create test database with the production schema. The schema is
// created by the real migration runner so tests always match the current
// migration set, then reopened through bun:sqlite for direct assertions.
async function createTestDb() {
const filename = path.join(os.tmpdir(), `json-migration-test-${crypto.randomUUID()}.sqlite`)
await Effect.runPromise(Effect.scoped(Layer.build(CoreDatabase.layerFromPath(filename))).pipe(Effect.orDie))
const sqlite = new Database(filename)
sqlite.exec("PRAGMA foreign_keys = ON")
// Apply schema migrations using drizzle migrate
const dir = path.join(import.meta.dirname, "../../../../core/migration")
const entries = readdirSync(dir, { withFileTypes: true })
const migrations = entries
.filter((entry) => entry.isDirectory())
.map((entry) => ({
sql: readFileSync(path.join(dir, entry.name, "migration.sql"), "utf-8"),
timestamp: Number(entry.name.split("_")[0]),
name: entry.name,
}))
.sort((a, b) => a.timestamp - b.timestamp)
const db = drizzle({ client: sqlite })
migrate(db, migrations)
return [sqlite, db] as const
return [sqlite, db, filename] as const
}
describe("JSON to SQLite migration", () => {
let storageDir: string
let sqlite: Database
let db: SQLiteBunDatabase
let dbFile: string
beforeEach(async () => {
storageDir = await setupStorageDir()
;[sqlite, db] = createTestDb()
;[sqlite, db, dbFile] = await createTestDb()
})
afterEach(async () => {
sqlite.close()
await fs.rm(storageDir, { recursive: true, force: true })
// Windows can keep SQLite WAL handles alive past layer disposal, so tolerate EBUSY here.
await Promise.all(
[dbFile, dbFile + "-shm", dbFile + "-wal"].map((file) => fs.rm(file, { force: true }).catch(() => undefined)),
)
})
test("migrates project", async () => {
@@ -11,12 +11,12 @@ describe("test profiles", () => {
const result = TestProfile.resolve("darwin", all)
expect(result.ok).toBe(true)
if (!result.ok) return
expect(result.files.length).toBeGreaterThan(50)
expect(result.files.length).toBeGreaterThan(20)
expect(result.files).toContain("pty/pty-shell.test.ts")
expect(result.files).toContain("kilocode/cli/install-artifact.test.ts")
expect(result.files).toContain("kilocode/sandbox/macos-confinement.test.ts")
expect(result.files).toContain("kilocode/core-watcher.test.ts")
expect(result.files).toContain("kilocode/tool/repo_clone.test.ts")
expect(result.files).toContain("kilocode/background-process.test.ts")
expect(result.files).toContain("filesystem/filesystem.test.ts")
expect(result.files).toContain("kilocode/interactive-terminal.test.ts")
const sandbox = all.filter((file) => file.startsWith("kilocode/sandbox/"))
@@ -27,6 +27,17 @@ describe("test profiles", () => {
expect(result.files).not.toContain("shell/shell.test.ts")
expect(result.files).not.toContain("kilocode/sessions/remote-ws.test.ts")
expect(result.files).not.toContain("provider/header-timeout.test.ts")
// Platform-neutral application logic is covered by the full Linux and
// Windows suites. Keep these heavy, darwin-agnostic files out of the
// profile so the single macOS shard stays fast.
expect(result.files).not.toContain("session/prompt.test.ts")
expect(result.files).not.toContain("snapshot/snapshot.test.ts")
expect(result.files).not.toContain("kilocode/daemon.test.ts")
expect(result.files).not.toContain("cli/smokes/read-only.test.ts")
expect(result.files).not.toContain("cli/acp/lifecycle.test.ts")
expect(result.files).not.toContain("cli/run/run-process.test.ts")
expect(result.files).not.toContain("kilocode/server/config-overlay.test.ts")
expect(result.files).not.toContain("server/httpapi-listen.test.ts")
})
test("normalizes Windows test paths", () => {
@@ -44,6 +44,7 @@ function infos() {
process: info("background_process"),
chart: info("chart"),
image: info("generate_image"),
notify: info("notify_user"),
notebookRead: info("notebook_read"),
notebookEdit: info("notebook_edit"),
notebookExecute: info("notebook_execute"),
@@ -342,6 +342,7 @@ describe("kilocode tool registry indexing", () => {
chart: def("chart"),
image: def("generate_image"),
terminal: def("interactive_terminal"),
notify: def("notify_user"),
notebookRead: def("notebook_read"),
notebookEdit: def("notebook_edit"),
notebookExecute: def("notebook_execute"),
@@ -357,6 +358,7 @@ describe("kilocode tool registry indexing", () => {
"chart",
"background_process",
"interactive_terminal",
"notify_user",
])
expect(KiloToolRegistry.extra(tools, { experimental: { codebase_search: true } }).map((tool) => tool.id)).toEqual(
[
@@ -368,6 +370,7 @@ describe("kilocode tool registry indexing", () => {
"chart",
"background_process",
"interactive_terminal",
"notify_user",
],
)
expect(
@@ -384,6 +387,7 @@ describe("kilocode tool registry indexing", () => {
"chart",
"background_process",
"interactive_terminal",
"notify_user",
])
process.env["KILO_CLIENT"] = "vscode"
@@ -398,6 +402,7 @@ describe("kilocode tool registry indexing", () => {
"background_process",
"agent_manager_models",
"agent_manager",
"notify_user",
],
)
expect(
@@ -417,6 +422,7 @@ describe("kilocode tool registry indexing", () => {
"notebook_read",
"notebook_edit",
"notebook_execute",
"notify_user",
])
expect(KiloToolRegistry.extra({ ...tools, semantic: undefined }, {}).map((tool) => tool.id)).toEqual([
"kilo_memory_recall",
@@ -426,6 +432,7 @@ describe("kilocode tool registry indexing", () => {
"background_process",
"agent_manager_models",
"agent_manager",
"notify_user",
])
process.env["KILO_CLIENT"] = "desktop"
@@ -435,6 +442,7 @@ describe("kilocode tool registry indexing", () => {
"kilo_memory_save",
"recall",
"chart",
"notify_user",
])
process.env["KILO_CLIENT"] = "run"
@@ -444,6 +452,7 @@ describe("kilocode tool registry indexing", () => {
"kilo_memory_save",
"recall",
"chart",
"notify_user",
])
process.env["KILO_CLIENT"] = "acp"
@@ -453,6 +462,7 @@ describe("kilocode tool registry indexing", () => {
"kilo_memory_save",
"recall",
"chart",
"notify_user",
])
} finally {
if (prev === undefined) delete process.env["KILO_CLIENT"]
@@ -466,7 +476,10 @@ describe("kilocode tool registry indexing", () => {
const calls: string[] = []
const sessions = Layer.succeed(
KiloSessions.Service,
KiloSessions.Service.of({ init: () => Effect.sync(() => calls.push("sessions")) }),
KiloSessions.Service.of({
init: () => Effect.sync(() => calls.push("sessions")),
sendAgentNotification: () => Effect.succeed({ ok: false as const, reason: "not_connected" }),
}),
)
const bus = Layer.succeed(
Bus.Service,
@@ -56,6 +56,7 @@ function infos() {
process: info("background_process"),
chart: info("chart"),
image: info("generate_image"),
notify: info("notify_user"),
notebookRead: info("notebook_read"),
notebookEdit: info("notebook_edit"),
notebookExecute: info("notebook_execute"),
@@ -173,11 +173,6 @@ describe("kilo_memory_recall", () => {
expect(direct.output).toContain("continue memory digest recall")
const decisions = await MemoryFiles.readDecisions(enabled.root)
expect(decisions).toContain('"sessionID":"ses_test"')
expect(decisions).toContain('"query":"sessionID=ses_memory_only"')
expect(decisions).toContain('"summary":"memory recall returned 1 typed hits"')
expect(decisions).toContain('"summary":"memory recall returned 1 digest hits"')
})
})
@@ -397,10 +392,6 @@ describe("kilo_memory_recall", () => {
expect(result.output).toContain("active session")
expect(result.output).not.toContain("useful prior work")
const decisions = await MemoryFiles.readDecisions(enabled.root)
expect(decisions).toContain('"sessionID":"ses_test"')
expect(decisions).toContain('"query":"sessionID=ses_test"')
expect(decisions).toContain('"reason":"current_session_digest"')
})
})
@@ -497,8 +488,6 @@ describe("kilo_memory_recall", () => {
expect(result.output).toContain("type=session_digest")
expect(result.output).toContain('topic="catalog recall"')
const decisions = await MemoryFiles.readDecisions(enabled.root)
expect(decisions).toContain('"summary":"memory recall returned')
})
})
@@ -288,11 +288,6 @@ describe("kilo_memory_save", () => {
expect(shown.sources.project).not.toContain("reply_style")
expect(shown.sources.project).not.toContain("I prefer terse summaries")
expect(shown.sources.project).toContain("- commit_style :: Repo convention: commit messages are concise.")
expect(shown.decisions).toContain('"reason":"out_of_scope"')
expect(shown.decisions).not.toContain("rubicon fennel")
expect(shown.decisions).not.toContain("Ignore prior instructions")
expect(shown.decisions).not.toContain("reply_style")
expect(shown.decisions).not.toContain("I prefer terse summaries")
})
})
@@ -0,0 +1,231 @@
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
import { Effect, Layer, Schema } from "effect"
import { Agent } from "@/agent/agent"
import { KiloSessions } from "@/kilo-sessions/kilo-sessions"
import { KiloToolRegistry } from "@/kilocode/tool/registry"
import { NotifyUserTool } from "@/kilocode/tool/notify-user"
import { MessageID, SessionID } from "@/session/schema"
import * as Truncate from "@/tool/truncate"
import type { Tool } from "@/tool/tool"
const agentInfo = {
name: "code",
mode: "primary",
options: {},
permission: {},
} as Agent.Info
const agents = Agent.Service.of({
get: () => Effect.succeed(agentInfo),
list: () => Effect.succeed([agentInfo]),
defaultInfo: () => Effect.succeed(agentInfo),
defaultAgent: () => Effect.succeed("code"),
requirementStatus: () =>
Effect.succeed({
agent: "code",
directory: "",
enabled: false,
state: "ready",
skills: [],
mcps: [],
vscode_extensions: [],
}),
guardRequirements: () => Effect.void,
generate: () => Effect.succeed({ identifier: "code", whenToUse: "", systemPrompt: "" }),
})
const truncate = Truncate.Service.of({
cleanup: () => Effect.void,
write: () => Effect.succeed(""),
output: (text) => Effect.succeed({ content: text as string, truncated: false }),
limits: () => Effect.succeed({ maxLines: Truncate.MAX_LINES, maxBytes: Truncate.MAX_BYTES }),
})
const ctx: Tool.Context = {
sessionID: SessionID.make("ses_test"),
messageID: MessageID.make("msg_test"),
callID: "call_test",
agent: "code",
abort: new AbortController().signal,
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
}
const status = spyOn(KiloSessions, "remoteStatus")
beforeEach(() => {
status.mockReturnValue({ enabled: true, connected: true })
})
afterEach(() => {
status.mockReset()
})
function runNotifyTool(params: { readonly message: string }, sessions: KiloSessions.Interface) {
const layer = Layer.mergeAll(
Layer.succeed(KiloSessions.Service, KiloSessions.Service.of(sessions)),
Layer.succeed(Agent.Service, agents),
Layer.succeed(Truncate.Service, truncate),
)
return Effect.runPromise(
Effect.gen(function* () {
const result = yield* NotifyUserTool
const tool = yield* result.init()
return yield* tool.execute(params, ctx)
}).pipe(Effect.provide(layer)),
)
}
describe("notify_user tool", () => {
test("is only available while remote is enabled", () => {
const tool = { id: "notify_user" } as Tool.Def
status.mockReturnValue({ enabled: false, connected: false })
expect(KiloToolRegistry.available(tool, agentInfo)).toBe(false)
status.mockReturnValue({ enabled: true, connected: false })
expect(KiloToolRegistry.available(tool, agentInfo)).toBe(true)
})
test("registers with id and description", async () => {
const layer = Layer.mergeAll(
Layer.succeed(KiloSessions.Service, KiloSessions.Service.of({
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: true }),
})),
Layer.succeed(Agent.Service, agents),
Layer.succeed(Truncate.Service, truncate),
)
const result = await Effect.runPromise(
Effect.gen(function* () {
const info = yield* NotifyUserTool
const tool = yield* info.init()
return { id: info.id, description: tool.description }
}).pipe(Effect.provide(layer)),
)
expect(result.id).toBe("notify_user")
expect(result.description).toContain("Send a push notification to the user's phone")
expect(result.description).toContain("Do NOT use this tool")
})
test("rejects empty message", async () => {
await expect(runNotifyTool({ message: "" }, {
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: true }),
})).rejects.toBeDefined()
})
test("rejects whitespace-only message", async () => {
await expect(runNotifyTool({ message: " \n " }, {
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: true }),
})).rejects.toBeDefined()
})
test("rejects message over 500 chars", async () => {
await expect(runNotifyTool({ message: "x".repeat(501) }, {
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: true }),
})).rejects.toBeDefined()
})
test("trims message before sending", async () => {
const calls: { sessionID: string; input: { id: string; message: string } }[] = []
const sessions = KiloSessions.Service.of({
init: () => Effect.void,
sendAgentNotification: (sessionID, input) =>
Effect.sync(() => {
calls.push({ sessionID, input })
return { ok: true }
}),
})
const result = await runNotifyTool({ message: " hello world " }, sessions)
expect(calls).toHaveLength(1)
expect(calls[0].input.message).toBe("hello world")
expect(result.metadata.ok).toBe(true)
expect(result.output).toContain("Notification sent")
})
test("returns failure text when not connected", async () => {
const sessions = KiloSessions.Service.of({
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: false, reason: "not_connected" }),
})
const result = await runNotifyTool({ message: "hello" }, sessions)
expect(result.metadata.ok).toBe(false)
expect(result.output).toContain("not connected to Kilo cloud")
})
test("does not send when remote is disabled", async () => {
status.mockReturnValue({ enabled: false, connected: false })
const sessions = KiloSessions.Service.of({
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: true }),
})
const send = spyOn(sessions, "sendAgentNotification")
const result = await runNotifyTool({ message: "hello" }, sessions)
expect(result.metadata.reason).toBe("not_connected")
expect(send).not.toHaveBeenCalled()
send.mockRestore()
})
test("returns failure text with arbitrary reason", async () => {
const sessions = KiloSessions.Service.of({
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: false, reason: "http_500" }),
})
const result = await runNotifyTool({ message: "hello" }, sessions)
expect(result.metadata.ok).toBe(false)
expect(result.output).toContain("http_500")
})
test("returns failure text when not bootstrapped", async () => {
const sessions = KiloSessions.Service.of({
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: false, reason: "not_bootstrapped" }),
})
const result = await runNotifyTool({ message: "hello" }, sessions)
expect(result.metadata.ok).toBe(false)
expect(result.metadata.reason).toBe("not_bootstrapped")
expect(result.output).toContain("not_bootstrapped")
expect(result.title).toBe("Notification unavailable")
})
test("returns failure text on bootstrap timeout", async () => {
const sessions = KiloSessions.Service.of({
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: false, reason: "bootstrap_timeout" }),
})
const result = await runNotifyTool({ message: "hello" }, sessions)
expect(result.metadata.ok).toBe(false)
expect(result.metadata.reason).toBe("bootstrap_timeout")
expect(result.output).toContain("bootstrap_timeout")
})
test("2xx success returns ok metadata and success output", async () => {
const sessions = KiloSessions.Service.of({
init: () => Effect.void,
sendAgentNotification: () => Effect.succeed({ ok: true }),
})
const result = await runNotifyTool({ message: "ping" }, sessions)
expect(result.metadata.ok).toBe(true)
expect(result.metadata.notificationId).toBeDefined()
expect(result.output).toContain("Notification sent")
expect(result.title).toBe("Notification sent")
})
})
@@ -0,0 +1,118 @@
import { describe, expect, test } from "bun:test"
import {
aggregateMetrics,
formatRateValue,
hasMetrics,
throughputLabel,
} from "../../../src/kilocode/plugins/model-usage"
const step = (metrics: { generation?: number }) => ({
metrics: { source: "computed" as const, ...metrics },
generated: 0,
})
const weightedStep = (overrides: {
generation: number
output: number
reasoning?: number
elapsedMs: number
}) => ({
metrics: { generation: overrides.generation, source: "computed" as const },
generated: overrides.output + (overrides.reasoning ?? 0),
elapsedMs: overrides.elapsedMs,
output: overrides.output,
reasoning: overrides.reasoning ?? 0,
})
describe("kilocode.plugins.model-usage throughput helpers", () => {
test("formatRateValue renders positive values with grouping", () => {
expect(formatRateValue(412)).toBe("412 t/s")
expect(formatRateValue(412.5)).toBe("412.5 t/s")
expect(formatRateValue(12345)).toBe("12,345 t/s")
expect(formatRateValue(28.7)).toBe("28.7 t/s")
})
test("formatRateValue falls back to dash for missing or bogus values", () => {
expect(formatRateValue(undefined)).toBe("-")
expect(formatRateValue(0)).toBe("-")
expect(formatRateValue(-5)).toBe("-")
expect(formatRateValue(Number.NaN)).toBe("-")
expect(formatRateValue(Infinity)).toBe("-")
})
test("throughputLabel centralizes the generation-speed label so a future i18n sweep is one file", () => {
expect(throughputLabel.generation).toBe("Generation speed")
})
test("surfaces the most recent non-empty generation rate as the snapshot (fallback)", () => {
// Fallback path — used when callers don't pass timing on the wire.
// The weighted path is exercised by the dedicated tests below.
const aggregated = aggregateMetrics([
{ ...step({ generation: 20 }), generated: 100 },
{ ...step({ generation: 60 }), generated: 300 },
])
expect(aggregated.generation).toBe(60)
})
test("skips samples without metrics", () => {
const aggregated = aggregateMetrics([
{ metrics: undefined, generated: 100, elapsedMs: 1000 },
weightedStep({ generation: 40, output: 50, elapsedMs: 1000 }),
])
// weighted step contributes (50, 1000) → 50 t/s.
expect(aggregated.generation).toBe(50)
})
test("weights samples by elapsed time across steps", () => {
const aggregated = aggregateMetrics([
weightedStep({ generation: 100, output: 100, elapsedMs: 1000 }),
weightedStep({ generation: 50, output: 200, elapsedMs: 4000 }),
])
// totalGenerated=300, totalElapsedMs=5000 → 60 t/s
expect(aggregated.generation).toBe(60)
})
test("includes reasoning tokens in the weighted numerator", () => {
const aggregated = aggregateMetrics([
weightedStep({ generation: 200, output: 50, reasoning: 150, elapsedMs: 1000 }),
])
// (50 + 150) tokens / 1000 ms = 200 t/s
expect(aggregated.generation).toBe(200)
})
test("falls back to last-wins snapshot when no sample carries timing", () => {
const aggregated = aggregateMetrics([
{ ...step({ generation: 20 }), generated: 100 },
{ ...step({ generation: 60 }), generated: 300 },
])
expect(aggregated.generation).toBe(60)
})
test("skips zero-weight samples when picking the latest snapshot", () => {
const aggregated = aggregateMetrics([
{ ...step({ generation: 9999 }), generated: 0 },
{ ...step({ generation: 25 }), generated: 50 },
])
expect(aggregated.generation).toBe(25)
})
test("returns empty aggregate when nothing has metrics", () => {
expect(aggregateMetrics([])).toEqual({})
expect(aggregateMetrics([{ metrics: undefined, generated: 100 }])).toEqual({})
})
test("ignores bogus per-call values without poisoning the snapshot", () => {
const aggregated = aggregateMetrics([
{ ...step({ generation: -1 }), generated: 100 },
{ ...step({ generation: Number.POSITIVE_INFINITY }), generated: 100 },
{ ...step({ generation: 30 }), generated: 50 },
])
expect(aggregated.generation).toBe(30)
})
test("hasMetrics gates opportunistic rendering", () => {
expect(hasMetrics(undefined)).toBeFalse()
expect(hasMetrics({})).toBeFalse()
expect(hasMetrics({ generation: 12 })).toBeTrue()
})
})
@@ -0,0 +1,27 @@
import path from "node:path"
import { describe, expect, test } from "bun:test"
describe("mcp session recovery", () => {
test("reinitializes and retries once after a session-bound POST returns 404", async () => {
const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "../fixture/mcp-session-recovery.ts")], {
cwd: path.join(import.meta.dir, "../.."),
stdout: "pipe",
stderr: "pipe",
})
const [code, stdout, stderr] = await Promise.all([
child.exited,
Bun.readableStreamToText(child.stdout),
Bun.readableStreamToText(child.stderr),
])
expect(code, stderr).toBe(0)
expect(JSON.parse(stdout)).toEqual([
{ method: "initialize", session: null },
{ method: "notifications/initialized", session: "expired" },
{ method: "ping", session: "expired" },
{ method: "initialize", session: null },
{ method: "notifications/initialized", session: "replacement" },
{ method: "ping", session: "replacement" },
])
})
})
@@ -571,7 +571,7 @@ it.instance(
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
})
expect(result).toBeUndefined()
expect(result).toEqual({ manual: false, rule: { permission: "bash", pattern: "*", action: "allow" } }) // kilocode_change - ask returns the auto-approval decision instead of void
}),
{ git: true },
)
@@ -803,7 +803,7 @@ it.instance(
always: [],
ruleset: [],
})
expect(result).toBeUndefined()
expect(result).toEqual({ manual: false, rule: { permission: "bash", pattern: "ls", action: "allow" } }) // kilocode_change - the persisted "always" rule auto-approves; ask reports that decision
}),
{ git: true },
)
@@ -1118,7 +1118,7 @@ it.instance(
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
})
expect(result).toBeUndefined()
expect(result).toEqual({ manual: false, rule: { permission: "bash", pattern: "*", action: "allow" } }) // kilocode_change - ask returns the auto-approval decision instead of void
}),
{ git: true },
)
@@ -0,0 +1,278 @@
import { describe, expect, test } from "bun:test"
import { OAUTH_DUMMY_KEY } from "../../src/auth"
import { oauthScope, SnowflakeCortexAuthPlugin } from "../../src/plugin/snowflake-cortex"
function makeInput() {
let auth: any = {
type: "oauth",
access: "access-old",
refresh: "refresh-old",
expires: Date.now() + 3600_000,
accountId: "myorg-myaccount",
}
const setCalls: Array<Record<string, unknown>> = []
return {
getAuth: async () => auth,
setAuth: (next: any) => {
auth = next
},
input: {
client: {
auth: {
set: async (request: any) => {
setCalls.push(request)
auth = request.body
},
},
},
} as any,
setCalls,
}
}
describe("plugin.snowflake-cortex", () => {
test("oauthScope uses Snowflake-compatible scope values", () => {
expect(oauthScope(undefined)).toBe("refresh_token")
expect(oauthScope("PUBLIC")).toBe("refresh_token session:role:PUBLIC")
expect(oauthScope("AUTH SNOWFLAKE")).toBe("refresh_token session:role-encoded:AUTH%20SNOWFLAKE")
})
test("loader returns empty options when auth is not oauth", async () => {
const hooks = await SnowflakeCortexAuthPlugin({} as any)
const options = await hooks.auth!.loader!(async () => ({ type: "api", key: "token" }) as any, {} as any)
expect(options).toEqual({})
})
test("loader injects bearer header and preserves custom headers", async () => {
const { input, getAuth, setAuth } = makeInput()
setAuth({
type: "oauth",
access: "access-live",
refresh: "refresh-live",
expires: Date.now() + 60 * 60 * 1000,
accountId: "myorg-myaccount",
})
const hooks = await SnowflakeCortexAuthPlugin(input)
const options = await hooks.auth!.loader!(getAuth as any, {} as any)
expect(options.apiKey).toBe(OAUTH_DUMMY_KEY)
const originalFetch = globalThis.fetch
const captured: Headers[] = []
globalThis.fetch = (async (_request, init) => {
captured.push(new Headers(init?.headers))
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } })
}) as typeof fetch
try {
await options.fetch("https://example.test/v1/chat", {
headers: { Authorization: `Bearer ${OAUTH_DUMMY_KEY}`, "x-keep": "yes" },
})
} finally {
globalThis.fetch = originalFetch
}
expect(captured).toHaveLength(1)
expect(captured[0].get("authorization")).toBe("Bearer access-live")
expect(captured[0].get("x-keep")).toBe("yes")
expect(captured[0].get("user-agent")).toMatch(/^opencode\//)
})
test("loader refreshes expired token with single-flight and persists refreshed oauth", async () => {
const { input, getAuth, setCalls } = makeInput()
let refreshCalls = 0
const apiAuthHeaders: string[] = []
// Must mock fetch before calling loader because startup refresh triggers for expires: 0
const originalFetch = globalThis.fetch
globalThis.fetch = (async (request, init) => {
const url =
typeof request === "string" ? request : request instanceof URL ? request.toString() : String(request.url)
if (url.includes("/oauth/token-request")) {
refreshCalls += 1
const body = new URLSearchParams(String(init?.body ?? ""))
expect(body.get("grant_type")).toBe("refresh_token")
expect(body.get("refresh_token")).toBe("refresh-old")
expect(new Headers(init?.headers).get("authorization")).toMatch(/^Basic /)
await new Promise((resolve) => setTimeout(resolve, 20))
return Response.json({ access_token: "access-new", refresh_token: "refresh-new", expires_in: 3600 })
}
apiAuthHeaders.push(new Headers(init?.headers).get("authorization") || "")
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } })
}) as typeof fetch
try {
const hooks = await SnowflakeCortexAuthPlugin(input)
const options = await hooks.auth!.loader!(
async () =>
({
type: "oauth",
access: "access-expired",
refresh: "refresh-old",
expires: 0,
accountId: "myorg-myaccount",
}) as any,
{} as any,
)
await Promise.all([
options.fetch("https://example.test/v1/chat", { headers: {} }),
options.fetch("https://example.test/v1/chat", { headers: {} }),
])
} finally {
globalThis.fetch = originalFetch
}
expect(refreshCalls).toBe(1)
expect(apiAuthHeaders).toEqual(["Bearer access-new", "Bearer access-new"])
expect(setCalls).toHaveLength(1)
expect((setCalls[0] as any).body).toMatchObject({
type: "oauth",
access: "access-new",
refresh: "refresh-new",
accountId: "myorg-myaccount",
})
})
test("loader retries once after 401 by refreshing token", async () => {
const { input, getAuth, setCalls } = makeInput()
const hooks = await SnowflakeCortexAuthPlugin(input)
const options = await hooks.auth!.loader!(
async () =>
({
type: "oauth",
access: "access-stale",
refresh: "refresh-old",
expires: Date.now() + 60 * 60 * 1000,
accountId: "myorg-myaccount",
}) as any,
{} as any,
)
let apiCalls = 0
const seenAuth: string[] = []
const originalFetch = globalThis.fetch
globalThis.fetch = (async (request, init) => {
const url =
typeof request === "string" ? request : request instanceof URL ? request.toString() : String(request.url)
if (url.includes("/oauth/token-request")) {
return Response.json({ access_token: "access-fresh", refresh_token: "refresh-fresh", expires_in: 3600 })
}
apiCalls += 1
seenAuth.push(new Headers(init?.headers).get("authorization") || "")
if (apiCalls === 1) return new Response("unauthorized", { status: 401 })
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } })
}) as typeof fetch
try {
const response = await options.fetch("https://example.test/v1/chat", { headers: {} })
expect(response.status).toBe(200)
} finally {
globalThis.fetch = originalFetch
}
expect(apiCalls).toBe(2)
expect(seenAuth).toEqual(["Bearer access-stale", "Bearer access-fresh"])
expect(setCalls).toHaveLength(1)
expect((setCalls[0] as any).body).toMatchObject({
type: "oauth",
access: "access-fresh",
refresh: "refresh-fresh",
accountId: "myorg-myaccount",
})
})
test("loader converts max_tokens to max_completion_tokens in request body", async () => {
const { input, getAuth } = makeInput()
const hooks = await SnowflakeCortexAuthPlugin(input)
const options = await hooks.auth!.loader!(getAuth as any, {} as any)
let sentBody: string | undefined
const originalFetch = globalThis.fetch
globalThis.fetch = (async (request, init) => {
sentBody = typeof init?.body === "string" ? init.body : undefined
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } })
}) as typeof fetch
try {
await options.fetch("https://example.test/v1/chat", {
method: "POST",
body: JSON.stringify({ model: "claude-sonnet-4-5", max_tokens: 4096, messages: [] }),
})
} finally {
globalThis.fetch = originalFetch
}
expect(sentBody).toBeDefined()
const parsed = JSON.parse(sentBody!)
expect(parsed.max_completion_tokens).toBe(4096)
expect(parsed.max_tokens).toBeUndefined()
expect(parsed.model).toBe("claude-sonnet-4-5")
})
test("loader maps 400 'conversation complete' to 200 stop", async () => {
const { input, getAuth } = makeInput()
const hooks = await SnowflakeCortexAuthPlugin(input)
const options = await hooks.auth!.loader!(getAuth as any, {} as any)
const originalFetch = globalThis.fetch
globalThis.fetch = (async () => {
return new Response(JSON.stringify({ message: "Conversation complete" }), {
status: 400,
headers: { "content-type": "application/json" },
})
}) as unknown as typeof fetch
try {
const response = await options.fetch("https://example.test/v1/chat", {
method: "POST",
body: JSON.stringify({ model: "test", messages: [] }),
})
expect(response.status).toBe(200)
const body = await response.json()
expect(body.choices[0].finish_reason).toBe("stop")
} finally {
globalThis.fetch = originalFetch
}
})
test("loader fixes empty role in SSE stream", async () => {
const { input, getAuth } = makeInput()
const hooks = await SnowflakeCortexAuthPlugin(input)
const options = await hooks.auth!.loader!(getAuth as any, {} as any)
const originalFetch = globalThis.fetch
const sseChunk = `data: {"choices":[{"delta":{"role":"","content":"hello"}}]}\n\n`
globalThis.fetch = (async () => {
const stream = new ReadableStream({
start(ctrl) {
ctrl.enqueue(new TextEncoder().encode(sseChunk))
ctrl.close()
},
})
return new Response(stream, {
status: 200,
headers: { "content-type": "text/event-stream" },
})
}) as unknown as typeof fetch
try {
const response = await options.fetch("https://example.test/v1/chat", {
method: "POST",
body: JSON.stringify({ model: "test", messages: [], stream: true }),
})
expect(response.status).toBe(200)
const reader = response.body!.getReader()
const { value } = await reader.read()
const text = new TextDecoder().decode(value)
expect(text).not.toContain('"role":""')
expect(text).toContain('"role":"assistant"')
} finally {
globalThis.fetch = originalFetch
}
})
})
@@ -26,7 +26,7 @@ function directories(projectID: ProjectV2.ID) {
Effect.orDie,
Effect.map((rows) =>
rows
.map((row) => ({ directory: row.directory, type: row.type }))
.map((row) => ({ directory: row.directory, strategy: row.strategy ?? undefined }))
.toSorted((a, b) => a.directory.localeCompare(b.directory)),
),
),
@@ -41,7 +41,9 @@ describe("Project directory persistence", () => {
const result = yield* project.fromDirectory(tmp)
expect(yield* directories(result.project.id)).toEqual([{ directory: tmp, type: "main" }])
expect(yield* directories(result.project.id)).toEqual([
{ directory: AbsolutePath.make(tmp), strategy: undefined },
])
}),
)
@@ -54,7 +56,9 @@ describe("Project directory persistence", () => {
const next = yield* project.fromDirectory(tmp)
expect(next.project.id).toBe(result.project.id)
expect(yield* directories(result.project.id)).toEqual([{ directory: tmp, type: "main" }])
expect(yield* directories(result.project.id)).toEqual([
{ directory: AbsolutePath.make(tmp), strategy: undefined },
])
}),
)
@@ -73,8 +77,8 @@ describe("Project directory persistence", () => {
expect(yield* directories(main.project.id)).toEqual(
[
{ directory: tmp, type: "main" as const },
{ directory: worktree, type: "git_worktree" as const },
{ directory: AbsolutePath.make(tmp), strategy: undefined },
{ directory: AbsolutePath.make(worktree), strategy: undefined },
].toSorted((a, b) => a.directory.localeCompare(b.directory)),
)
}),
@@ -92,7 +96,9 @@ describe("Project directory persistence", () => {
const result = yield* project.fromDirectory(worktree)
expect(yield* directories(result.project.id)).toEqual([{ directory: worktree, type: "git_worktree" }])
expect(yield* directories(result.project.id)).toEqual([
{ directory: AbsolutePath.make(worktree), strategy: undefined },
])
}),
)
@@ -113,8 +119,8 @@ describe("Project directory persistence", () => {
expect(yield* directories(main.project.id)).toEqual(
[
{ directory: tmp, type: "main" as const },
{ directory: clone, type: "root" as const },
{ directory: AbsolutePath.make(tmp), strategy: undefined },
{ directory: AbsolutePath.make(clone), strategy: undefined },
].toSorted((a, b) => a.directory.localeCompare(b.directory)),
)
}),
@@ -134,7 +140,9 @@ describe("Project directory persistence", () => {
const result = yield* project.fromDirectory(worktree)
expect(yield* directories(result.project.id)).toEqual([{ directory: worktree, type: "git_worktree" }])
expect(yield* directories(result.project.id)).toEqual([
{ directory: AbsolutePath.make(worktree), strategy: undefined },
])
}),
)
@@ -163,7 +171,31 @@ describe("Project directory persistence", () => {
yield* project.fromDirectory(tmp)
expect(yield* directories(remoteID)).toEqual([{ directory: tmp, type: "main" }])
expect(yield* directories(remoteID)).toEqual([{ directory: AbsolutePath.make(tmp), strategy: undefined }])
}),
)
it.live("clears stale directories when the project id changes", () =>
Effect.gen(function* () {
const tmp = yield* tmpdirScoped({ git: true })
const project = yield* Project.Service
const original = yield* project.fromDirectory(tmp)
const stale = AbsolutePath.make(tmp + "-stale-checkout")
const { db } = yield* Database.Service
yield* db
.insert(ProjectDirectoryTable)
.values({ project_id: original.project.id, directory: stale })
.run()
.pipe(Effect.orDie)
const remoteID = ProjectV2.ID.make(Hash.fast("git-remote:github.com/project-directory-test/migration"))
yield* Effect.promise(() =>
$`git remote add origin git@github.com:project-directory-test/migration.git`.cwd(tmp).quiet(),
)
yield* project.fromDirectory(tmp)
expect(yield* directories(original.project.id)).toEqual([])
expect(yield* directories(remoteID)).toEqual([{ directory: AbsolutePath.make(tmp), strategy: undefined }])
}),
)
})
@@ -19,7 +19,7 @@ import { NodePath } from "@effect/platform-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { AppProcess } from "@opencode-ai/core/process"
import { ProjectV2 } from "@opencode-ai/core/project"
import { ProjectCopy } from "@opencode-ai/core/project/copy"
import { ProjectDirectories } from "@opencode-ai/core/project/directories"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
import { RuntimeFlags } from "@/effect/runtime-flags"
@@ -73,7 +73,7 @@ function projectLayerWithFailure(failArg: string) {
Layer.provide(AppProcess.layer.pipe(Layer.provide(mockGitFailure(failArg)))),
Layer.provide(mockGitFailure(failArg)),
Layer.provide(ProjectV2.defaultLayer),
Layer.provide(ProjectCopy.defaultLayer),
Layer.provide(ProjectDirectories.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(NodePath.layer),
@@ -86,7 +86,7 @@ function projectLayerWithRuntimeFlags(flags: Parameters<typeof RuntimeFlags.laye
return Project.layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(ProjectV2.defaultLayer),
Layer.provide(ProjectCopy.defaultLayer),
Layer.provide(ProjectDirectories.defaultLayer),
Layer.provide(AppProcess.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(NodePath.layer),
@@ -72,7 +72,8 @@ describe("ProviderTransform.options - setCacheKey", () => {
expect(result.promptCacheKey).toBeUndefined()
})
test("should set promptCacheKey for openai provider regardless of setCacheKey", () => {
// kilocode_change start
test("should set promptCacheKey for openai provider by default", () => {
const openaiModel = {
...mockModel,
providerID: "openai",
@@ -86,6 +87,57 @@ describe("ProviderTransform.options - setCacheKey", () => {
expect(result.promptCacheKey).toBe(sessionID)
})
test("should not set promptCacheKey for openai when explicitly disabled", () => {
const openaiModel = {
...mockModel,
providerID: "openai",
api: {
id: "gpt-4",
url: "https://api.openai.com",
npm: "@ai-sdk/openai",
},
}
const result = ProviderTransform.options({
model: openaiModel,
sessionID,
providerOptions: { setCacheKey: false },
})
expect(result.promptCacheKey).toBeUndefined()
})
test("should set promptCacheKey for the xAI SDK by default regardless of provider ID", () => {
const xaiModel = {
...mockModel,
providerID: "custom-xai",
api: {
id: "grok-4",
url: "https://api.x.ai",
npm: "@ai-sdk/xai",
},
}
const result = ProviderTransform.options({ model: xaiModel, sessionID, providerOptions: {} })
expect(result.promptCacheKey).toBe(sessionID)
})
test("should not set promptCacheKey for the xAI SDK when explicitly disabled", () => {
const xaiModel = {
...mockModel,
providerID: "xai",
api: {
id: "grok-4",
url: "https://api.x.ai",
npm: "@ai-sdk/xai",
},
}
const result = ProviderTransform.options({
model: xaiModel,
sessionID,
providerOptions: { setCacheKey: false },
})
expect(result.promptCacheKey).toBeUndefined()
})
// kilocode_change end
test("should set store=false for openai provider", () => {
const openaiModel = {
...mockModel,
@@ -223,6 +223,18 @@ const scenarios: Scenario[] = [
headers: ctx.headers(),
}))
.json(200, array, "status"),
http.protected
.post("/experimental/project/{projectID}/copy/generate-name", "experimental.projectCopy.generateName")
.seeded((ctx) => ctx.project())
.at((ctx) => ({
path: route("/experimental/project/{projectID}/copy/generate-name", { projectID: ctx.state.id }),
headers: ctx.headers(),
body: {},
}))
.json(200, (body) => {
object(body)
check(typeof body.name === "string" && body.name.length > 0, "generated copy name should be non-empty")
}),
http.protected
.post("/experimental/project/{projectID}/copy", "experimental.projectCopy.create")
.seeded((ctx) => ctx.project())
@@ -695,49 +707,67 @@ const scenarios: Scenario[] = [
http.protected.get("/api/agent", "v2.agent.list").json(200, locationData(array)),
http.protected.get("/api/model", "v2.model.list").json(200, locationData(array)),
http.protected.get("/api/provider", "v2.provider.list").json(200, locationData(array)),
http.protected.get("/api/connector", "v2.connector.list").json(200, locationData(array)),
http.protected.get("/api/integration", "v2.integration.list").json(200, locationData(array)),
http.protected
.get("/api/connector/{connectorID}", "v2.connector.get")
.at((ctx) => ({ path: route("/api/connector/{connectorID}", { connectorID: "missing" }), headers: ctx.headers() }))
.get("/api/integration/{integrationID}", "v2.integration.get")
.at((ctx) => ({
path: route("/api/integration/{integrationID}", { integrationID: "missing" }),
headers: ctx.headers(),
}))
.json(200, object),
http.protected
.post("/api/connector/{connectorID}/connect/key", "v2.connector.connect.key")
.post("/api/integration/{integrationID}/connect/key", "v2.integration.connect.key")
.at((ctx) => ({
path: route("/api/connector/{connectorID}/connect/key", { connectorID: "missing" }),
path: route("/api/integration/{integrationID}/connect/key", { integrationID: "missing" }),
headers: ctx.headers(),
body: { methodID: "missing", key: "test", inputs: {} },
body: { key: "test" },
}))
.status(500, undefined, "status"),
http.protected
.post("/api/connector/{connectorID}/connect/oauth", "v2.connector.connect.oauth.begin")
.post("/api/integration/{integrationID}/connect/oauth", "v2.integration.connect.oauth")
.at((ctx) => ({
path: route("/api/connector/{connectorID}/connect/oauth", { connectorID: "missing" }),
path: route("/api/integration/{integrationID}/connect/oauth", { integrationID: "missing" }),
headers: ctx.headers(),
body: { methodID: "missing", inputs: {} },
}))
.status(500, undefined, "status"),
http.protected
.get("/api/connector/oauth/{attemptID}", "v2.connector.connect.oauth.status")
.get("/api/integration/attempt/{attemptID}", "v2.integration.attempt.status")
.at((ctx) => ({
path: route("/api/connector/oauth/{attemptID}", { attemptID: "con_missing" }),
path: route("/api/integration/attempt/{attemptID}", { attemptID: "con_missing" }),
headers: ctx.headers(),
}))
.status(500, undefined, "status"),
http.protected
.post("/api/connector/oauth/{attemptID}/complete", "v2.connector.connect.oauth.complete")
.post("/api/integration/attempt/{attemptID}/complete", "v2.integration.attempt.complete")
.at((ctx) => ({
path: route("/api/connector/oauth/{attemptID}/complete", { attemptID: "con_missing" }),
path: route("/api/integration/attempt/{attemptID}/complete", { attemptID: "con_missing" }),
headers: ctx.headers(),
body: {},
}))
.status(500, undefined, "status"),
http.protected
.delete("/api/connector/oauth/{attemptID}", "v2.connector.connect.oauth.cancel")
.delete("/api/integration/attempt/{attemptID}", "v2.integration.attempt.cancel")
.at((ctx) => ({
path: route("/api/connector/oauth/{attemptID}", { attemptID: "con_missing" }),
path: route("/api/integration/attempt/{attemptID}", { attemptID: "con_missing" }),
headers: ctx.headers(),
}))
.status(204, undefined, "status"),
http.protected
.delete("/api/credential/{credentialID}", "v2.credential.remove")
.at((ctx) => ({
path: route("/api/credential/{credentialID}", { credentialID: "cred_missing" }),
headers: ctx.headers(),
}))
.status(204, undefined, "status"),
http.protected
.patch("/api/credential/{credentialID}", "v2.credential.update")
.at((ctx) => ({
path: route("/api/credential/{credentialID}", { credentialID: "cred_missing" }),
headers: ctx.headers(),
body: { label: "Work" },
}))
.status(204, undefined, "status"),
http.protected.get("/api/command", "v2.command.list").json(200, locationData(array)),
http.protected.get("/api/skill", "v2.skill.list").json(200, locationData(array)),
http.protected
@@ -115,25 +115,27 @@ describe("PublicApi OpenAPI v2 errors", () => {
}
})
test("documents connector discovery and connection routes", () => {
test("documents integration discovery and connection routes", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const [method, path] of [
["get", "/api/connector"],
["get", "/api/connector/{connectorID}"],
["post", "/api/connector/{connectorID}/connect/key"],
["post", "/api/connector/{connectorID}/connect/oauth"],
["get", "/api/connector/oauth/{attemptID}"],
["post", "/api/connector/oauth/{attemptID}/complete"],
["delete", "/api/connector/oauth/{attemptID}"],
["get", "/api/integration"],
["get", "/api/integration/{integrationID}"],
["post", "/api/integration/{integrationID}/connect/key"],
["post", "/api/integration/{integrationID}/connect/oauth"],
["get", "/api/integration/attempt/{attemptID}"],
["post", "/api/integration/attempt/{attemptID}/complete"],
["delete", "/api/integration/attempt/{attemptID}"],
["delete", "/api/credential/{credentialID}"],
["patch", "/api/credential/{credentialID}"],
] as const) {
expect(spec.paths[path]?.[method], `${method.toUpperCase()} ${path}`).toBeDefined()
}
for (const path of [
"/api/connector/{connectorID}/connect/key",
"/api/connector/{connectorID}/connect/oauth",
"/api/connector/oauth/{attemptID}/complete",
"/api/integration/{integrationID}/connect/key",
"/api/integration/{integrationID}/connect/oauth",
"/api/integration/attempt/{attemptID}/complete",
]) {
expect(spec.paths[path]?.post?.requestBody?.required, path).toBe(true)
}
@@ -35,7 +35,7 @@ import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, provideInstanceEffect, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { TestLLMServer } from "../lib/llm-server"
import { testProviderConfig } from "../lib/test-provider"
import { testEffect } from "../lib/effect"
import { pollWithTimeout, testEffect } from "../lib/effect" // kilocode_change
const originalWorkspaces = Flag.KILO_EXPERIMENTAL_WORKSPACES
const workspaceLayer = Workspace.defaultLayer.pipe(
@@ -1007,6 +1007,65 @@ describe("session HttpApi", () => {
{ git: true, config: { formatter: false, lsp: false } },
)
// kilocode_change start - deleting a prompt that already started is a successful no-op
it.live(
"returns false when an active prompt wins the deletion race",
() => {
const release = Promise.withResolvers<void>()
return Effect.gen(function* () {
const llm = yield* TestLLMServer
yield* llm.hold("done", release.promise)
const dir = yield* tmpdirScoped({ git: true, config: testProviderConfig(llm.url) })
const session = yield* createSession({ title: "Active delete race" }).pipe(provideInstanceEffect(dir))
const messageID = MessageID.ascending()
const headers = { "x-kilo-directory": dir, "content-type": "application/json" }
const prompt = yield* request(pathFor(SessionPaths.promptAsync, { sessionID: session.id }), {
method: "POST",
headers,
body: JSON.stringify({
messageID,
agent: "build",
model: { providerID: "test", modelID: "test-model" },
parts: [{ type: "text", text: "keep running" }],
}),
})
expect(prompt.status).toBe(204)
yield* llm.wait(1)
expect(
yield* requestJson<boolean>(pathFor(SessionPaths.deleteMessage, { sessionID: session.id, messageID }), {
method: "DELETE",
headers,
}),
).toBe(false)
release.resolve()
yield* pollWithTimeout(
requestJson<Record<string, unknown>>(SessionPaths.status, { headers }).pipe(
Effect.map((statuses) => (statuses[session.id] ? undefined : true)),
),
"Timed out waiting for active prompt to finish",
)
const messages = yield* Session.use
.messages({ sessionID: session.id })
.pipe(provideInstanceEffect(dir), Effect.orDie)
expect(messages.some((message) => message.info.id === messageID)).toBe(true)
expect(
messages.some((message) => message.info.role === "assistant" && message.info.parentID === messageID),
).toBe(true)
}).pipe(
Effect.ensuring(Effect.sync(() => release.resolve())),
Effect.provide(TestLLMServer.layer),
Effect.provide(CrossSpawnSpawner.defaultLayer),
)
},
10_000,
)
// kilocode_change end
it.instance(
"rejects part updates whose path and body ids disagree",
() =>
@@ -34,7 +34,7 @@ function json<T>(response: HttpClientResponse.HttpClientResponse) {
}
describe("project directories and copies endpoints", () => {
type ProjectDirectory = { directory: string; type: "main" | "root" | "git_worktree" }
type ProjectDirectory = { directory: string; strategy?: string }
it.instance(
"lists directories and manages git worktree copies",
@@ -44,7 +44,7 @@ describe("project directories and copies endpoints", () => {
const current = yield* request(test.directory, "/project/current")
const projectID = (yield* json<{ id: string }>(current)).id
const base = `/project/${projectID}`
const copies = `/experimental/project/${projectID}/copy`
const copies = `/experimental/project/${projectID}/copy?location%5Bdirectory%5D=${encodeURIComponent(test.directory)}`
const createdParent = path.join(test.directory, "..", path.basename(test.directory) + "-http-copy")
const createdDirectory = path.join(createdParent, "copy")
yield* Effect.addFinalizer(() =>
@@ -53,7 +53,15 @@ describe("project directories and copies endpoints", () => {
const initial = yield* request(test.directory, `${base}/directories`)
expect(initial.status).toBe(200)
expect(yield* json<ProjectDirectory[]>(initial)).toEqual([{ directory: test.directory, type: "main" }])
expect(yield* json<ProjectDirectory[]>(initial)).toEqual([{ directory: test.directory }])
const generated = yield* request(test.directory, `/experimental/project/${projectID}/copy/generate-name`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ context: undefined }),
})
expect(generated.status).toBe(200)
expect((yield* json<{ name: string }>(generated)).name).toBeString()
const create = yield* request(test.directory, copies, {
method: "POST",
@@ -67,7 +75,7 @@ describe("project directories and copies endpoints", () => {
const listed = yield* request(test.directory, `${base}/directories`)
expect(yield* json<ProjectDirectory[]>(listed)).toContainEqual({
directory: created.directory,
type: "git_worktree",
strategy: "git_worktree",
})
yield* Effect.promise(() => Bun.write(path.join(created.directory, "dirty.txt"), "dirty"))
@@ -94,14 +102,18 @@ describe("project directories and copies endpoints", () => {
Effect.promise(() => fs.rm(externalDirectory, { recursive: true, force: true })).pipe(Effect.ignore),
)
yield* Effect.promise(() => $`git worktree add --detach ${externalDirectory} HEAD`.cwd(test.directory).quiet())
const refresh = yield* request(test.directory, `${copies}/refresh`, {
method: "POST",
})
const refresh = yield* request(
test.directory,
`/experimental/project/${projectID}/copy/refresh?location%5Bdirectory%5D=${encodeURIComponent(test.directory)}`,
{
method: "POST",
},
)
expect(refresh.status).toBe(204)
const refreshed = yield* request(test.directory, `${base}/directories`)
expect(yield* json<ProjectDirectory[]>(refreshed)).toEqual([
{ directory: externalDirectory, type: "git_worktree" },
{ directory: test.directory, type: "main" },
{ directory: externalDirectory, strategy: "git_worktree" },
{ directory: test.directory },
])
}),
{ git: true },
+2 -2
View File
@@ -281,7 +281,7 @@ describe("session.llm.ai-sdk adapter", () => {
{
type: "step-finish",
index: 0,
reason: "unknown",
reason: "other", // kilocode_change
usage: {
inputTokens: 10,
outputTokens: 5,
@@ -294,7 +294,7 @@ describe("session.llm.ai-sdk adapter", () => {
},
{
type: "finish",
reason: "unknown",
reason: "other", // kilocode_change
usage: {
inputTokens: 11,
outputTokens: 6,
@@ -224,6 +224,36 @@ const providerErrorEnv = LayerNode.buildLayer(root, {
})
const itProviderError = testEffect(providerErrorEnv)
// kilocode_change start
const lateToolInputLLM = Layer.succeed(
LLM.Service,
LLM.Service.of({
stream: () =>
Stream.make(
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolInputStart({ id: "call-1", name: "read" }),
LLMEvent.toolInputDelta({ id: "call-1", name: "read", text: '{"filePath":"package.json"}' }),
LLMEvent.toolInputEnd({ id: "call-1", name: "read" }),
LLMEvent.toolCall({ id: "call-1", name: "read", input: { filePath: "package.json" }, providerExecuted: true }),
LLMEvent.toolResult({
id: "call-1",
name: "read",
result: { type: "text", value: "contents" },
providerExecuted: true,
}),
LLMEvent.toolInputDelta({ id: "call-1", name: "unknown", text: "" }),
LLMEvent.toolInputEnd({ id: "call-1", name: "unknown" }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
),
}),
)
const lateToolInputEnv = LayerNode.buildLayer(root, {
replacements: [...replacements, LayerNode.replace(LLM.node, lateToolInputLLM)],
})
const itLateToolInput = testEffect(lateToolInputEnv)
// kilocode_change end
const fragmentFailureLLM = Layer.succeed(
LLM.Service,
LLM.Service.of({
@@ -1081,7 +1111,49 @@ itProviderError.live("session.processor effect tests fail provider-executed erro
}),
{ config: cfg },
),
) // kilocode_change
// kilocode_change start
itLateToolInput.live("session.processor effect tests ignore tool input after the call settles", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const { processors, session, provider } = yield* boot()
const chat = yield* session.create({})
const parent = yield* user(chat.id, "read a file")
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
yield* handle.process({
user: {
id: parent.id,
sessionID: chat.id,
role: "user",
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
system: [],
messages: [{ role: "user", content: "read a file" }],
tools: {},
})
const calls = (yield* MessageV2.parts(msg.id)).filter(
(part): part is SessionV1.ToolPart => part.type === "tool",
)
expect(calls).toHaveLength(1)
expect(calls[0]?.callID).toBe("call-1")
expect(calls[0]?.tool).toBe("read")
expect(calls[0]?.state.status).toBe("completed")
}),
{ config: cfg },
),
)
// kilocode_change end
itFragmentFailure.live("session.processor effect tests flush partial v2 fragments before step failure", () =>
provideTmpdirInstance(
+39 -79
View File
@@ -4,13 +4,11 @@ import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Database } from "@opencode-ai/core/database/database"
import { eq } from "drizzle-orm"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Bus } from "@/bus" // kilocode_change - ToolRegistry retains the Kilo bus dependency
import { Bus } from "@/bus"
import { FetchHttpClient } from "effect/unstable/http"
// kilocode_change start
import { expect, spyOn } from "bun:test"
import { Telemetry } from "@kilocode/kilo-telemetry"
import { legacyReviewMessage } from "../../src/kilocode/review/command"
// kilocode_change end
import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
@@ -18,7 +16,7 @@ import { NamedError } from "@opencode-ai/core/util/error"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { BackgroundJob } from "@/background/job"
import { Command } from "../../src/command"
import { Auth } from "../../src/auth" // kilocode_change
import { Auth } from "../../src/auth"
import { Config } from "@/config/config"
import { LSP } from "@/lsp/lsp"
import { MCP } from "../../src/mcp"
@@ -43,8 +41,9 @@ import { SessionProcessor } from "../../src/session/processor"
import { SessionPrompt } from "../../src/session/prompt"
import { SessionRevert } from "../../src/session/revert"
import { SessionRunState } from "../../src/session/run-state"
import { KiloSession } from "../../src/kilocode/session" // kilocode_change
import { Suggestion } from "../../src/kilocode/suggestion" // kilocode_change - accept suggestion in telemetry test
import { KiloSession } from "../../src/kilocode/session"
import { KiloSessions } from "../../src/kilo-sessions/kilo-sessions"
import { Suggestion } from "../../src/kilocode/suggestion"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { SessionStatus } from "../../src/session/status"
import { SessionV2 } from "@opencode-ai/core/session"
@@ -62,8 +61,8 @@ import { TestInstance } from "../fixture/fixture"
import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
import { reply, TestLLMServer } from "../lib/llm-server"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { MemoryService } from "@kilocode/kilo-memory/effect/service" // kilocode_change
import { RepositoryCache } from "@opencode-ai/core/repository-cache" // kilocode_change
import { MemoryService } from "@kilocode/kilo-memory/effect/service"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -165,7 +164,6 @@ const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.default
const run = SessionRunState.layer.pipe(Layer.provide(status))
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
// kilocode_change start
const agent: AgentSvc.Info = {
name: "build",
mode: "primary",
@@ -194,7 +192,6 @@ const blockingProcessor = Layer.succeed(
}),
}),
)
// kilocode_change end
function makePrompt(input?: { processor?: "blocking" }) {
const deps = Layer.mergeAll(
@@ -202,7 +199,7 @@ function makePrompt(input?: { processor?: "blocking" }) {
Snapshot.defaultLayer,
LLM.defaultLayer,
Env.defaultLayer,
input?.processor === "blocking" ? fastAgents : AgentSvc.defaultLayer, // kilocode_change
input?.processor === "blocking" ? fastAgents : AgentSvc.defaultLayer,
Command.defaultLayer,
Permission.defaultLayer,
Plugin.defaultLayer,
@@ -215,8 +212,8 @@ function makePrompt(input?: { processor?: "blocking" }) {
status,
Database.defaultLayer,
EventV2Bridge.defaultLayer,
Bus.layer, // kilocode_change - satisfy the Kilo ToolRegistry dependency
MemoryService.layer, // kilocode_change
Bus.layer,
MemoryService.layer,
).pipe(Layer.provideMerge(infra))
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
@@ -226,10 +223,11 @@ function makePrompt(input?: { processor?: "blocking" }) {
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(RepositoryCache.defaultLayer), // kilocode_change - RepoCloneTool dependency
Layer.provide(RepositoryCache.defaultLayer),
Layer.provide(Format.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provide(Auth.defaultLayer), // kilocode_change
Layer.provide(Auth.defaultLayer),
Layer.provide(KiloSessions.testLayer),
Layer.provideMerge(todo),
Layer.provideMerge(question),
Layer.provideMerge(deps),
@@ -258,7 +256,7 @@ function makePrompt(input?: { processor?: "blocking" }) {
Layer.provideMerge(proc),
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provideMerge(question), // kilocode_change - SessionPrompt dismisses pending questions
Layer.provideMerge(question),
Layer.provide(Instruction.defaultLayer),
Layer.provide(SystemPrompt.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
@@ -341,7 +339,7 @@ const ensureDir = Effect.fn("test.ensureDir")(function* (dir: string) {
const writeConfig = Effect.fn("test.writeConfig")(function* (dir: string, config: Partial<ConfigV1.Info>) {
yield* writeText(
path.join(dir, "opencode.json"),
JSON.stringify({ $schema: "https://app.kilo.ai/config.json", ...config }), // kilocode_change
JSON.stringify({ $schema: "https://app.kilo.ai/config.json", ...config }),
)
})
@@ -352,7 +350,6 @@ const useServerConfig = Effect.fn("test.useServerConfig")(function* (config: (ur
return { dir, llm }
})
// kilocode_change start - wait for the runner state that cancel observes instead of session status
const waitForBusy = (sessionID: SessionID, duration: Duration.Input = "2 seconds") =>
pollWithTimeout(
Effect.gen(function* () {
@@ -363,7 +360,6 @@ const waitForBusy = (sessionID: SessionID, duration: Duration.Input = "2 seconds
`session ${sessionID} never became busy`,
duration,
)
// kilocode_change end
const hasBash = Effect.sync(() => Bun.which("bash") !== null)
@@ -535,7 +531,6 @@ it.instance("loop calls LLM and returns assistant message", () =>
}),
)
// kilocode_change start - replacement prompts unblock pending Question service requests
noLLMServer.instance(
"new prompt dismisses a pending question",
() =>
@@ -578,9 +573,7 @@ noLLMServer.instance(
}),
{ config: cfg },
)
// kilocode_change end
// kilocode_change start - cover user image normalization before persistence
noLLMServer.instance(
"normalizes user data images before persistence",
() =>
@@ -690,7 +683,6 @@ noLLMServer.instance(
}),
{ config: cfg },
)
// kilocode_change end
it.instance("loop surfaces content-filter finishes as session errors", () =>
Effect.gen(function* () {
@@ -1070,13 +1062,11 @@ noLLMServer.instance("prompt tools replace matching rules and preserve existing
})
const reloaded = yield* sessions.get(session.id)
// kilocode_change start - Kilo preserves existing restrictions that the new prompt does not override
expect(reloaded.permission).toEqual([
{ permission: "bash", pattern: "*", action: "deny" },
{ permission: "read", pattern: "*", action: "allow" },
])
expect(Permission.evaluate("bash", "anything", reloaded.permission ?? []).action).toBe("deny")
// kilocode_change end
}),
)
@@ -1139,14 +1129,14 @@ it.instance(
const tool = yield* pollWithTimeout(
Effect.gen(function* () {
const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
const assistant = msgs.findLast((item) => item.info.role === "assistant" && item.info.agent === "code") // kilocode_change
const assistant = msgs.findLast((item) => item.info.role === "assistant" && item.info.agent === "code")
const tool = assistant?.parts.find(
(part): part is SessionV1.ToolPart => part.type === "tool" && part.tool === "task",
)
if (tool?.state.status === "running" && tool.state.metadata?.sessionId) return tool
}),
"timed out waiting for running task metadata",
"10 seconds", // kilocode_change - allow loaded Darwin runners to persist the tool transition
"10 seconds",
)
if (tool.state.status !== "running") return
@@ -1157,10 +1147,9 @@ it.instance(
yield* prompt.cancel(chat.id)
yield* Fiber.await(fiber)
}),
20_000, // kilocode_change
20_000,
)
// kilocode_change start - child task failures stay tool errors so the parent can recover
it.instance(
"failed task tool preserves metadata and lets the parent follow up",
() =>
@@ -1203,12 +1192,10 @@ it.instance(
}),
10_000,
)
// kilocode_change end
it.instance(
"loop sets status to busy then idle",
() =>
// kilocode_change start - hold the model response instead of cancelling an infinite stream
Effect.gen(function* () {
const { llm } = yield* useServerConfig(providerCfg)
const gate = yield* Deferred.make<void>()
@@ -1228,8 +1215,7 @@ it.instance(
yield* Fiber.await(fiber)
expect((yield* status.get(chat.id)).type).toBe("idle")
}),
// kilocode_change end
10_000, // kilocode_change
10_000,
)
// Cancel semantics
@@ -1257,12 +1243,10 @@ it.instance(
expect(exit.value.info.role).toBe("assistant")
}
}),
10_000, // kilocode_change - Windows CI can take longer to cancel the live loop
10_000,
)
// kilocode_change start
unix(
// kilocode_change end
"cancel records MessageAbortedError on interrupted process",
() =>
Effect.gen(function* () {
@@ -1285,7 +1269,7 @@ unix(
}
}
}),
10_000, // kilocode_change - upstream's 3s deadline flakes under CI shard load (observed 3048ms on macOS)
10_000,
)
raceNoLLMServer.instance(
@@ -1310,12 +1294,10 @@ raceNoLLMServer.instance(
parts: [{ type: "text", text: "first" }],
})
// kilocode_change start
const firstCreate = yield* Deferred.make<void>()
processorCreateStarted.push(firstCreate)
const first = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* awaitWithTimeout(Deferred.await(firstCreate), "processor.create did not start for first turn")
// kilocode_change end
yield* prompt.cancel(chat.id)
const firstExit = yield* Fiber.await(first)
@@ -1338,12 +1320,10 @@ raceNoLLMServer.instance(
parts: [{ type: "text", text: "second" }],
})
// kilocode_change start
const secondCreate = yield* Deferred.make<void>()
processorCreateStarted.push(secondCreate)
const second = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* awaitWithTimeout(Deferred.await(secondCreate), "processor.create did not start for second turn")
// kilocode_change end
yield* prompt.cancel(chat.id)
const secondExit = yield* Fiber.await(second)
@@ -1378,7 +1358,7 @@ raceNoLLMServer.instance(
}
}),
{ config: cfg },
10_000, // kilocode_change - cancellation tree cleanup can exceed 3s under macOS CI shard load
10_000,
)
noLLMServer.instance(
@@ -1428,7 +1408,6 @@ noLLMServer.instance(
30_000,
)
// kilocode_change start - handleSubtask propagates child session cost to wrapper (#6321)
it.instance(
"handleSubtask propagates subagent cost to wrapper message",
() =>
@@ -1487,7 +1466,6 @@ it.instance(
}),
30_000,
)
// kilocode_change end
it.instance(
"cancel propagates from slash command subtask to child session",
@@ -1538,7 +1516,7 @@ it.instance(
const a = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* llm.wait(1)
const b = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* Effect.yieldNow // kilocode_change - let the queued caller join without a wall-clock race
yield* Effect.yieldNow
yield* prompt.cancel(chat.id)
const [exitA, exitB] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
@@ -1549,7 +1527,7 @@ it.instance(
}
}),
{ git: true },
10_000, // kilocode_change - Windows CI can take longer to cancel queued live loops
10_000,
)
// Queue semantics
@@ -1572,7 +1550,6 @@ noLLMServer.instance("concurrent loop callers get same result", () =>
it.instance(
"concurrent loop callers all receive same error result",
() =>
// kilocode_change start - gate the failing stream so both callers join the same run
Effect.gen(function* () {
const { llm } = yield* useServerConfig(providerCfg)
const gate = yield* Deferred.make<void>()
@@ -1586,7 +1563,7 @@ it.instance(
const a = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* llm.wait(1)
const b = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* Effect.yieldNow // kilocode_change - let the queued caller join without a wall-clock race
yield* Effect.yieldNow
yield* Deferred.succeed(gate, void 0)
const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
@@ -1596,8 +1573,7 @@ it.instance(
expect(ea.value.info.id).toBe(eb.value.info.id)
expect(ea.value.info.role).toBe("assistant")
}),
// kilocode_change end
10_000, // kilocode_change
10_000,
)
it.instance(
@@ -1665,7 +1641,7 @@ it.instance(
expect(inputs).toHaveLength(2)
expect(JSON.stringify(inputs.at(-1)?.messages)).toContain("second")
}),
10_000, // kilocode_change - loaded CI runners can exceed 3s for two prompt turns
10_000,
)
it.instance(
@@ -1694,7 +1670,7 @@ it.instance(
yield* prompt.cancel(chat.id)
yield* Fiber.await(fiber)
}),
10_000, // kilocode_change
10_000,
)
noLLMServer.instance("assertNotBusy succeeds when idle", () =>
@@ -1734,7 +1710,7 @@ it.instance(
yield* prompt.cancel(chat.id)
yield* Fiber.await(fiber)
}),
10_000, // kilocode_change - Windows CI can take longer to enter and cancel the live loop
10_000,
)
unixNoLLMServer(
@@ -1834,7 +1810,6 @@ unixNoLLMServer(
{ config: cfg },
)
// kilocode_change start - verify shell v2 events correlate with the persisted tool part
unixNoLLMServer(
"shell correlates the persisted tool part with its completed v2 record",
() =>
@@ -1849,7 +1824,7 @@ unixNoLLMServer(
if (!tool) return
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
Effect.provide(SessionV2.defaultLayer), // kilocode_change - use the complete upstream v2 session layer
Effect.provide(SessionV2.defaultLayer),
)
const shell = messages.find((message) => message.type === "shell")
@@ -1863,7 +1838,6 @@ unixNoLLMServer(
}),
{ config: cfg },
)
// kilocode_change end
unixNoLLMServer(
"shell lists files from the project directory",
@@ -1961,7 +1935,7 @@ it.instance(
yield* waitForBusy(chat.id)
const loop = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* Effect.yieldNow // kilocode_change - give the queued loop a scheduler turn instead of a wall-clock window
yield* Effect.yieldNow
expect(yield* llm.calls).toBe(0)
@@ -1976,7 +1950,7 @@ it.instance(
expect(yield* llm.calls).toBe(1)
}),
{ git: true },
30_000, // kilocode_change - Windows CI process startup can exceed 3s
30_000,
)
it.instance(
@@ -1999,7 +1973,7 @@ it.instance(
const a = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
const b = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* Effect.yieldNow // kilocode_change - give the queued loops a scheduler turn instead of a wall-clock window
yield* Effect.yieldNow
expect(yield* llm.calls).toBe(0)
@@ -2015,7 +1989,7 @@ it.instance(
expect(yield* llm.calls).toBe(1)
}),
{ git: true },
30_000, // kilocode_change - Windows CI process startup can exceed 3s
30_000,
)
unix(
@@ -2158,7 +2132,6 @@ unix(
const run = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* llm.wait(1)
// kilocode_change start
yield* pollWithTimeout(
sessions.messages({ sessionID: chat.id }).pipe(
Effect.map((msgs) => {
@@ -2171,7 +2144,6 @@ unix(
),
"timed out waiting for large bash output",
)
// kilocode_change end
yield* prompt.cancel(chat.id)
const exit = yield* Fiber.await(run)
@@ -2199,7 +2171,6 @@ unixNoLLMServer(
const sh = yield* prompt.shell({ sessionID: chat.id, agent: "build", command: "sleep 30" }).pipe(Effect.forkChild)
yield* waitForBusy(chat.id)
// kilocode_change start - busy is set before shell persistence completes
yield* pollWithTimeout(
sessions
.messages({ sessionID: chat.id })
@@ -2214,9 +2185,7 @@ unixNoLLMServer(
),
`session ${chat.id} never persisted its running shell tool`,
)
// kilocode_change end
// kilocode_change start - wait until the loop reaches the queued-run handoff
const opened = yield* Deferred.make<void>()
yield* Effect.acquireRelease(
Effect.sync(() =>
@@ -2230,7 +2199,6 @@ unixNoLLMServer(
const loop = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* awaitWithTimeout(Deferred.await(opened), `session ${chat.id} never opened its queued turn`)
yield* Effect.yieldNow
// kilocode_change end
yield* prompt.cancel(chat.id)
@@ -2444,7 +2412,6 @@ noLLMServer.instance(
{ config: cfg },
)
// kilocode_change start - expand configured Kilo references once per prompt
noLLMServer.instance(
"resolves configured reference mentions to one root directory attachment",
() =>
@@ -2473,7 +2440,7 @@ noLLMServer.instance(
source: { type: "file", path: "docs", text: { value: "@docs" } },
})
expect(fileURLToPath(files[0].url)).toBe(docs)
expect(agents.map((agent) => agent.name)).toEqual(["code"]) // kilocode_change
expect(agents.map((agent) => agent.name)).toEqual(["code"])
}),
{
config: {
@@ -2484,9 +2451,7 @@ noLLMServer.instance(
},
},
)
// kilocode_change end
// kilocode_change start - deduplicate configured Kilo references by source identity
noLLMServer.instance(
"does not let an unrelated directory attachment shadow a configured reference",
() =>
@@ -2530,7 +2495,6 @@ noLLMServer.instance(
},
},
)
// kilocode_change end
noLLMServer.instance(
"stores raw reference mentions alongside directory attachments",
@@ -2557,8 +2521,8 @@ noLLMServer.instance(
const text = stored.parts.find((part): part is SessionV1.TextPart => part.type === "text" && !part.synthetic)
expect(text?.text).toBe("Use @docs for context")
expect(synthetic.some((part) => part.text.includes("Called the Read tool"))).toBe(true) // kilocode_change
expect(files).toHaveLength(1) // kilocode_change - directory attachment is expanded, not denied
expect(synthetic.some((part) => part.text.includes("Called the Read tool"))).toBe(true)
expect(files).toHaveLength(1)
yield* sessions.remove(session.id)
}),
@@ -2673,12 +2637,11 @@ it.instance(
expect(last.info.error?.name).toBe("MessageAbortedError")
}
}),
10_000, // kilocode_change
10_000,
)
// Agent variant
// kilocode_change start - Agent Manager records a model-less synthetic prompt after forking
noLLMServer.instance(
"preserves the session variant through a model-less handoff",
() =>
@@ -2711,7 +2674,6 @@ noLLMServer.instance(
}),
{ config: cfg },
)
// kilocode_change end
noLLMServer.instance(
"applies agent variant only when using agent model",
@@ -2782,7 +2744,6 @@ noLLMServer.instance(
},
)
// kilocode_change start - Kilo review command behavior
noLLMServer.instance(
"deprecated review alias returns static message without LLM",
() =>
@@ -2893,7 +2854,6 @@ it.instance(
}),
30_000,
)
// kilocode_change end
// Agent / command resolution errors
@@ -2947,7 +2907,7 @@ noLLMServer.instance(
const err = Cause.squash(exit.cause)
expect(NamedError.Unknown.isInstance(err)).toBe(true)
if (NamedError.Unknown.isInstance(err)) {
expect(err.data.message).toContain("code") // kilocode_change - "build" renamed to "code"
expect(err.data.message).toContain("code")
}
}
}),
@@ -1,50 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Database } from "bun:sqlite"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
import { existsSync, readFileSync, readdirSync } from "fs"
import path from "path"
const target = "20260507164347_add_workspace_time"
function migrations() {
return readdirSync(path.join(import.meta.dirname, "../../../core/migration"), { withFileTypes: true })
.filter(
(entry) =>
entry.isDirectory() &&
existsSync(path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql")),
)
.map((entry) => ({
name: entry.name,
timestamp: Number(entry.name.split("_")[0]),
sql: readFileSync(
path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql"),
"utf-8",
),
}))
.sort((a, b) => a.timestamp - b.timestamp)
}
describe("workspace time migration", () => {
test("discards existing workspace rows during the beta reset", () => {
const sqlite = new Database(":memory:")
const db = drizzle({ client: sqlite })
const entries = migrations()
const index = entries.findIndex((entry) => entry.name === target)
expect(index).toBeGreaterThan(0)
migrate(db, entries.slice(0, index))
sqlite.run(
"INSERT INTO project (id, worktree, vcs, name, time_created, time_updated, sandboxes) VALUES (?, ?, ?, ?, ?, ?, ?)",
["project_1", "/tmp/project", "git", "project", 1, 1, "[]"],
)
sqlite.run(
"INSERT INTO workspace (id, type, name, branch, directory, extra, project_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
["workspace_1", "local", "main", "main", "/tmp/project", null, "project_1"],
)
expect(() => migrate(db, entries.slice(index))).not.toThrow()
expect(sqlite.query("SELECT time_used FROM workspace WHERE id = ?").get("workspace_1")).toBeNull()
})
})