mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
Merge remote-tracking branch 'origin/main' into marius-kilocode/kilo-opencode-v1.17.5
This commit is contained in:
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user