Merge remote-tracking branch 'origin/main' into marius-kilocode/kilo-opencode-v1.17.5

This commit is contained in:
marius-kilocode
2026-07-21 12:35:20 +02:00
259 changed files with 6952 additions and 2489 deletions
+11
View File
@@ -68,6 +68,15 @@ export async function disposeAllInstances() {
await Promise.all([InstanceRuntime.disposeAllInstances(), runTestInstanceStore((store) => store.disposeAll())])
}
// kilocode_change start - dispose a directory's instance (and its watchers) before the directory is deleted
async function disposeInstancesFor(directory: string) {
await Promise.allSettled([
InstanceRuntime.disposeDirectory(directory),
runTestInstanceStore((store) => store.disposeDirectory(directory)),
])
}
// kilocode_change end
// Strip null bytes from paths (defensive fix for CI environment issues)
function sanitizePath(p: string): string {
return p.replace(/\0/g, "")
@@ -122,6 +131,7 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
try {
await options?.dispose?.(realpath)
} finally {
await disposeInstancesFor(realpath) // kilocode_change - see disposeInstancesFor
if (options?.git) await stop(realpath).catch(() => undefined)
await clean(realpath).catch(() => undefined)
}
@@ -146,6 +156,7 @@ export function tmpdirScoped<E = never, R = never>(options?: {
yield* Effect.addFinalizer(() =>
Effect.promise(async () => {
await disposeInstancesFor(dir) // kilocode_change - see disposeInstancesFor
if (options?.git) await stop(dir).catch(() => undefined)
await clean(dir).catch(() => undefined)
}),
@@ -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)
@@ -7,6 +7,7 @@ import {
resolveThreadDirectory,
runEmbeddedRemoteExitBridge,
} from "../../../../src/cli/cmd/tui"
import { preload } from "../../../../src/kilocode/cli/cmd/tui"
import { KiloTuiThreadDaemon } from "../../../../src/kilocode/cli/cmd/tui/thread"
import { DaemonClient } from "../../../../src/kilocode/daemon/client"
@@ -15,6 +16,31 @@ afterEach(() => {
})
describe("kilo tui thread", () => {
test("skips preload resolver invocation in compiled mode", () => {
let calls = 0
expect(
preload(true, () => {
calls++
return "/resolved/preload"
}),
).toEqual([])
expect(calls).toBe(0)
})
test("resolves the preload once in source mode", () => {
let calls = 0
const path = "/resolved/preload"
expect(
preload(false, () => {
calls++
return path
}),
).toEqual([path])
expect(calls).toBe(1)
})
test("ignores stale PWD after cwd is changed by a process wrapper", async () => {
await using root = await tmpdir()
const pkg = path.join(root.path, "packages", "opencode")
@@ -0,0 +1,69 @@
import { afterAll, beforeAll, expect } from "bun:test"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Deferred, Effect, Fiber, Layer } from "effect"
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
import { Git } from "../../src/git"
import { InstanceLayer } from "../../src/project/instance-layer"
import { InstanceStore } from "../../src/project/instance-store"
import { tmpdirScoped } from "../fixture/fixture"
import { awaitWithTimeout, testEffect } from "../lib/effect"
const layer = Layer.mergeAll(InstanceLayer.layer, Git.defaultLayer, CrossSpawnSpawner.defaultLayer)
const it = testEffect(layer)
// The suite disables the file watcher (see test/preload.ts); this file tests it, so opt back in.
const disableFilewatcher = process.env.KILO_EXPERIMENTAL_DISABLE_FILEWATCHER
beforeAll(() => {
delete process.env.KILO_EXPERIMENTAL_DISABLE_FILEWATCHER
})
afterAll(() => {
if (disableFilewatcher !== undefined) process.env.KILO_EXPERIMENTAL_DISABLE_FILEWATCHER = disableFilewatcher
})
// The watcher is unreliable on Windows CI, so this test only runs on unix.
const live = process.platform === "win32" ? it.live.skip : it.live
live("instances publish branch updates after git switch", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const git = yield* Git.Service
const store = yield* InstanceStore.Service
const current = yield* git.branch(dir)
if (!current) return yield* Effect.die("missing initial branch")
const branch = `watch-${Math.random().toString(36).slice(2)}`
const created = yield* git.run(["branch", branch], { cwd: dir })
expect(created.exitCode).toBe(0)
yield* store.load({ directory: dir })
const pending = yield* Deferred.make<string | undefined>()
const handler = (event: GlobalEvent) => {
if (event.directory !== dir || event.payload.type !== "vcs.branch.updated") return
if (event.payload.properties.branch !== branch) return
Deferred.doneUnsafe(pending, Effect.succeed(event.payload.properties.branch))
}
GlobalBus.on("event", handler)
yield* Effect.addFinalizer(() => Effect.sync(() => GlobalBus.off("event", handler)))
// The watcher exposes no readiness signal (its .git subscription is forked
// during instance warm-up), so keep generating HEAD churn in the background
// and synchronize on the event itself with the full test budget.
const churn = yield* Effect.gen(function* () {
while (true) {
yield* git.run(["switch", current], { cwd: dir })
yield* Effect.sleep("50 millis")
yield* git.run(["switch", branch], { cwd: dir })
yield* Effect.sleep("100 millis")
}
}).pipe(Effect.forkScoped)
const updated = yield* awaitWithTimeout(
Deferred.await(pending),
"timed out waiting for vcs.branch.updated",
"15 seconds",
)
yield* Fiber.interrupt(churn)
expect(updated).toBe(branch)
}),
20_000,
)
@@ -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")
})
})
@@ -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)
@@ -1,5 +1,7 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import fs from "node:fs/promises"
import path from "node:path"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -84,3 +86,160 @@ describe("partial assistant revert", () => {
),
)
})
describe("workspace revert status", () => {
it.live(
"reports disabled snapshots when conversation-only revert leaves files unchanged",
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const revert = yield* SessionRevert.Service
const session = yield* sessions.create({})
const file = path.join(dir, "created.txt")
const providerID = ProviderV2.ID.make("test")
yield* Effect.promise(() => fs.writeFile(file, "created"))
const user = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "user",
agent: "default",
model: { providerID, modelID: ModelV2.ID.make("test") },
time: { created: Date.now() },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.id,
sessionID: session.id,
type: "text",
text: "create a file",
})
const result = yield* revert.revert({ sessionID: session.id, messageID: user.id })
expect(result.revert?.workspace).toBe("snapshots-disabled")
expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe("created")
}),
{ git: true, config: { snapshot: false } },
),
)
it.live(
"reports unavailable when historical turns have no file checkpoint",
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const revert = yield* SessionRevert.Service
const session = yield* sessions.create({})
const file = path.join(dir, "created.txt")
const providerID = ProviderV2.ID.make("test")
yield* Effect.promise(() => fs.writeFile(file, "created"))
const user = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "user",
agent: "default",
model: { providerID, modelID: ModelV2.ID.make("test") },
time: { created: Date.now() },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.id,
sessionID: session.id,
type: "text",
text: "create a file",
})
const result = yield* revert.revert({ sessionID: session.id, messageID: user.id })
expect(result.revert?.workspace).toBe("unavailable")
expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe("created")
}),
{ git: true },
),
)
it.live(
"reports restored when historical patches restore a file",
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const revert = yield* SessionRevert.Service
const snapshot = yield* Snapshot.Service
const session = yield* sessions.create({})
const file = path.join(dir, "tracked.txt")
const providerID = ProviderV2.ID.make("test")
yield* Effect.promise(() => fs.writeFile(file, "before"))
const user = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "user",
agent: "default",
model: { providerID, modelID: ModelV2.ID.make("test") },
time: { created: Date.now() },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.id,
sessionID: session.id,
type: "text",
text: "change a file",
})
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: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
modelID: ModelV2.ID.make("test"),
providerID,
time: { created: Date.now() },
finish: "end_turn",
})
const before = yield* snapshot.track()
if (!before) throw new Error("expected snapshot")
yield* Effect.promise(() => fs.writeFile(file, "after"))
const after = yield* snapshot.track()
if (!after) throw new Error("expected snapshot")
const patch = yield* snapshot.patch(before)
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: assistant.id,
sessionID: session.id,
type: "step-start",
snapshot: before,
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: assistant.id,
sessionID: session.id,
type: "step-finish",
reason: "stop",
snapshot: after,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: assistant.id,
sessionID: session.id,
type: "patch",
hash: patch.hash,
files: patch.files,
})
const result = yield* revert.revert({ sessionID: session.id, messageID: user.id })
expect(result.revert?.workspace).toBe("restored")
expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe("before")
}),
{ git: true },
),
)
})
@@ -5,6 +5,7 @@ import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { KiloIndexing } from "../../src/kilocode/indexing"
import { KilocodeBootstrap } from "../../src/kilocode/bootstrap"
import { KilocodeWatcher } from "../../src/kilocode/watcher"
import { KiloSessions } from "../../src/kilo-sessions/kilo-sessions"
import { KiloMemory } from "@kilocode/kilo-memory/effect"
import { MemoryService } from "@kilocode/kilo-memory/effect/service"
@@ -471,6 +472,7 @@ describe("kilocode tool registry indexing", () => {
const session = Layer.succeed(Session.Service, {} as Session.Interface)
const summary = Layer.succeed(SessionSummary.Service, {} as SessionSummary.Interface)
const provider = Layer.succeed(Provider.Service, {} as Provider.Interface)
const watcher = Layer.succeed(KilocodeWatcher.Service, KilocodeWatcher.Service.of({ init: () => Effect.void }))
const indexing = spyOn(KiloIndexing, "init").mockRejectedValue(err)
const warn = spyOn(logger, "warn").mockImplementation(() => {})
@@ -478,7 +480,7 @@ describe("kilocode tool registry indexing", () => {
await Effect.runPromise(
KilocodeBootstrap.Service.use((svc) => svc.init()).pipe(
Effect.provide(
KilocodeBootstrap.layer.pipe(Layer.provide([sessions, bus, memory, session, summary, provider])),
KilocodeBootstrap.layer.pipe(Layer.provide([sessions, bus, memory, session, summary, provider, watcher])),
),
Effect.scoped,
),
@@ -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")
})
})
+1
View File
@@ -40,6 +40,7 @@ process.env["XDG_STATE_HOME"] = path.join(dir, "state")
process.env["KILO_MODELS_PATH"] = path.join(import.meta.dir, "tool", "fixtures", "models-api.json")
process.env["KILO_EXPERIMENTAL_EVENT_SYSTEM"] = "true"
process.env["KILO_EXPERIMENTAL_WORKSPACES"] = "true"
process.env["KILO_EXPERIMENTAL_DISABLE_FILEWATCHER"] ??= "true" // kilocode_change - see test.yml: per-instance watchers are too heavy/racy for unit tests; watcher tests opt back in
// Set test home directory to isolate tests from user's actual home directory
// This prevents tests from picking up real user configs/skills from ~/.claude/skills