mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Effect Migration for Kilo callsites (follow-up 2) (#10616)
* refactor(opencode): remove raw LLM interface in favor of stream-based text helper Introduce KiloLLM.text() to consume Effect streams and extract text while propagating error/abort events. Replace all usages of the removed LLM.raw() method and the legacy LLM.stream() async wrapper with dedicated runtime helpers (CommitMessageRuntime.generate, PlanFollowupRuntime.handover) that leverage the new stream-based approach. - Add KiloLLM.text() stream combinator in kilocode/session/llm.ts - Remove Interface.raw and the exported LLM.stream() async function - Refactor commit-message generation to use CommitMessageRuntime - Refactor plan-followup handover to use PlanFollowupRuntime.handover - Update all test LLM mocks to remove raw() stubs - Add unit tests for KiloLLM.text() covering text joining, error propagation, and abort handling * feat(skill): migrate Skill module to Effect service pattern and remove legacy promise wrappers Remove the makeRuntime-based promise helpers (Skill.all, Skill.get, Skill.dirs) from the skill module and convert the builtin-skills test suite to use Effect generators with the testEffect harness. - Delete legacy runPromise wrappers from packages/opencode/src/skill/index.ts - Rewrite builtin-skills.test.ts to use testEffect and Effect.gen - Replace WithInstance.provide/tmpdir with TestInstance yield pattern - Use Skill.Service directly within Effect generators for all assertions * refactor(session-status): drop makeRuntime promise helpers and wire Service through Effect context Replace the standalone `SessionStatus.list`, `.get`, `.set` promise wrappers with direct `SessionStatus.Service` usage via Effect generators and `AppRuntime.runPromise` at Kilo callsites that remain imperative. - Remove makeRuntime-based exports from session/status.ts - Update kilo-sessions.ts and plan-followup.ts to use AppRuntime.runPromise with SessionStatus.Service - Thread SessionStatus.Service as a dependency through SuggestTool and ToolRegistry layers - Replace spy-based mocks in suggestion tool tests with an in-memory service stub for deterministic assertions
This commit is contained in:
@@ -182,7 +182,8 @@ export namespace KiloSessions {
|
||||
const questions = (await Question.list()).filter((q) => q.sessionID === sessionID)
|
||||
if (questions.length > 0) return "question"
|
||||
|
||||
const status = await SessionStatus.get(SessionID.make(sessionID))
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
const status = await AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.get(SessionID.make(sessionID))))
|
||||
if (status.type === "offline") return "retry"
|
||||
return status.type
|
||||
}
|
||||
@@ -344,7 +345,8 @@ export namespace KiloSessions {
|
||||
getGitUrl().catch(() => undefined),
|
||||
Vcs.branch().catch(() => undefined),
|
||||
])
|
||||
const statusMap = await SessionStatus.list()
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
const statusMap = await AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.list()))
|
||||
const statuses: Record<string, SessionStatus.Info> = Object.fromEntries(statusMap)
|
||||
const ids = new Set(Object.keys(statuses))
|
||||
for (const id of focused) ids.add(id)
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { LLM } from "@/session/llm"
|
||||
import { KiloLLM } from "@/kilocode/session/llm"
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import type { CommitMessageRequest, CommitMessageResponse, GitContext } from "./types"
|
||||
import { getGitContext } from "./git-context"
|
||||
|
||||
const log = Log.create({ service: "commit-message" })
|
||||
|
||||
export const CommitMessageRuntime = {
|
||||
generate(input: LLM.StreamInput, signal: AbortSignal) {
|
||||
// runPromise is needed until generateCommitMessage() uses Effect
|
||||
return AppRuntime.runPromise(
|
||||
LLM.Service.use((svc) => KiloLLM.text(svc.stream(input)).pipe(Effect.orDie)),
|
||||
{
|
||||
signal,
|
||||
},
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT = `You are an expert Git commit message generator that creates conventional commit messages based on staged changes. Analyze the provided git diff output and generate an appropriate conventional commit message following the specification.
|
||||
|
||||
## Conventional Commits Format
|
||||
@@ -150,44 +165,37 @@ export async function generateCommitMessage(request: CommitMessageRequest): Prom
|
||||
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
const stream = await LLM.stream({
|
||||
agent,
|
||||
user: {
|
||||
id: "commit-message",
|
||||
const result = await CommitMessageRuntime.generate(
|
||||
{
|
||||
agent,
|
||||
user: {
|
||||
id: "commit-message",
|
||||
sessionID: "commit-message",
|
||||
role: "user",
|
||||
model: {
|
||||
providerID: model.providerID,
|
||||
modelID: model.id,
|
||||
},
|
||||
time: {
|
||||
created: Date.now(),
|
||||
completed: Date.now(),
|
||||
},
|
||||
} as any,
|
||||
tools: {},
|
||||
model,
|
||||
small: true,
|
||||
messages: [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: userMessage,
|
||||
},
|
||||
],
|
||||
sessionID: "commit-message",
|
||||
role: "user",
|
||||
model: {
|
||||
providerID: model.providerID,
|
||||
modelID: model.id,
|
||||
},
|
||||
time: {
|
||||
created: Date.now(),
|
||||
completed: Date.now(),
|
||||
},
|
||||
} as any,
|
||||
tools: {},
|
||||
model,
|
||||
small: true,
|
||||
messages: [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: userMessage,
|
||||
},
|
||||
],
|
||||
abort: controller.signal,
|
||||
sessionID: "commit-message",
|
||||
system: [],
|
||||
retries: 3,
|
||||
})
|
||||
|
||||
// Consume the stream explicitly so that stream-level errors surface
|
||||
// immediately instead of leaving the .text promise hanging (issue #7345).
|
||||
// With some providers/versions of the Vercel AI SDK, `await stream.text`
|
||||
// never resolves when the underlying stream errors out early.
|
||||
let result = ""
|
||||
for await (const chunk of stream.textStream) {
|
||||
result += chunk
|
||||
}
|
||||
system: [],
|
||||
retries: 3,
|
||||
},
|
||||
controller.signal,
|
||||
)
|
||||
|
||||
log.info("generated", { message: result })
|
||||
return { message: clean(result) }
|
||||
|
||||
@@ -12,10 +12,12 @@ import { Question } from "@/question"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionID, MessageID, PartID } from "@/session/schema"
|
||||
import { LLM } from "@/session/llm"
|
||||
import { KiloLLM } from "@/kilocode/session/llm"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Effect } from "effect"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -26,6 +28,7 @@ const agents = lazy(() => makeRuntime(Agent.Service, Agent.defaultLayer))
|
||||
const providers = lazy(() => makeRuntime(Provider.Service, Provider.defaultLayer))
|
||||
const questions = lazy(() => makeRuntime(Question.Service, Question.defaultLayer))
|
||||
const todo = lazy(() => makeRuntime(Todo.Service, Todo.defaultLayer))
|
||||
const llm = lazy(() => makeRuntime(LLM.Service, LLM.defaultLayer))
|
||||
const pending = new Map<SessionID, AbortController>()
|
||||
|
||||
export const PlanFollowupRuntime = {
|
||||
@@ -54,6 +57,9 @@ export const PlanFollowupRuntime = {
|
||||
return todo().runPromise((svc) => svc.update(input))
|
||||
},
|
||||
},
|
||||
handover(input: LLM.StreamInput, signal: AbortSignal) {
|
||||
return llm().runPromise((svc) => KiloLLM.text(svc.stream(input)).pipe(Effect.orDie), { signal })
|
||||
},
|
||||
async loop(sessionID: SessionID) {
|
||||
const item = await import("@/session/prompt")
|
||||
const prompt = makeRuntime(item.SessionPrompt.Service, item.SessionPrompt.defaultLayer)
|
||||
@@ -123,31 +129,31 @@ export async function generateHandover(input: {
|
||||
model: input.model,
|
||||
}
|
||||
|
||||
const stream = await LLM.stream({
|
||||
agent: entry ?? {
|
||||
name: "compaction",
|
||||
mode: "subagent",
|
||||
permission: [],
|
||||
options: {},
|
||||
},
|
||||
user: userMsg,
|
||||
tools: {},
|
||||
model,
|
||||
small: true,
|
||||
messages: [
|
||||
...(await MessageV2.toModelMessages(input.messages, model)),
|
||||
{
|
||||
role: "user" as const,
|
||||
content: HANDOVER_PROMPT,
|
||||
const result = await PlanFollowupRuntime.handover(
|
||||
{
|
||||
agent: entry ?? {
|
||||
name: "compaction",
|
||||
mode: "subagent",
|
||||
permission: [],
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
abort: input.abort ? AbortSignal.any([input.abort, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000),
|
||||
sessionID,
|
||||
system: [],
|
||||
retries: 1,
|
||||
})
|
||||
|
||||
const result = await stream.text
|
||||
user: userMsg,
|
||||
tools: {},
|
||||
model,
|
||||
small: true,
|
||||
messages: [
|
||||
...(await MessageV2.toModelMessages(input.messages, model)),
|
||||
{
|
||||
role: "user" as const,
|
||||
content: HANDOVER_PROMPT,
|
||||
},
|
||||
],
|
||||
sessionID,
|
||||
system: [],
|
||||
retries: 1,
|
||||
},
|
||||
input.abort ? AbortSignal.any([input.abort, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000),
|
||||
)
|
||||
return result.trim()
|
||||
} catch (error) {
|
||||
if (input.abort?.aborted) return ""
|
||||
@@ -345,11 +351,12 @@ export namespace PlanFollowup {
|
||||
const next = await Session.create({})
|
||||
const ctl = new AbortController()
|
||||
pending.set(next.id, ctl)
|
||||
await SessionStatus.set(next.id, { type: "busy" })
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
await AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.set(next.id, { type: "busy" })))
|
||||
await Bus.publish(TuiEvent.SessionSelect, { sessionID: next.id })
|
||||
|
||||
const idle = () =>
|
||||
SessionStatus.set(next.id, { type: "idle" }).catch((err) => {
|
||||
AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.set(next.id, { type: "idle" }))).catch((err) => {
|
||||
log.warn("failed to clear follow-up busy status", { sessionID: next.id, err })
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { ModelMessage } from "ai"
|
||||
import { Effect } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import type { Event } from "@/session/llm"
|
||||
import { Token } from "@/util/token"
|
||||
|
||||
// Token.estimate consistently under-counts by ~15-30% vs. actual provider tokenizers.
|
||||
@@ -9,6 +12,19 @@ const SAFETY = 2048
|
||||
const MIN_OUTPUT = 1024
|
||||
|
||||
export namespace KiloLLM {
|
||||
// Preserve error and abort events while collecting text so Kilo callers can detect failed generations.
|
||||
export function text(stream: Stream.Stream<Event, unknown>) {
|
||||
return stream.pipe(
|
||||
Stream.mapEffect((event) => {
|
||||
if (event.type === "error") return Effect.fail(event.error)
|
||||
if (event.type === "abort") return Effect.fail(new DOMException("Aborted", "AbortError"))
|
||||
if (event.type !== "text-delta") return Effect.succeed("")
|
||||
return Effect.succeed(event.text)
|
||||
}),
|
||||
Stream.mkString,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Caps `maxOutputTokens` to fit within the model's context window after
|
||||
* accounting for the actual estimated input tokens (messages + tool schemas).
|
||||
|
||||
@@ -61,57 +61,59 @@ export function resolvePrompt(prompt: string, commands: Command.Interface) {
|
||||
})
|
||||
}
|
||||
|
||||
export const SuggestTool = Tool.define<typeof Params, Meta, Command.Service, "suggest">(
|
||||
export const SuggestTool = Tool.define<typeof Params, Meta, Command.Service | SessionStatus.Service, "suggest">(
|
||||
"suggest",
|
||||
Effect.gen(function* () {
|
||||
const commands = yield* Command.Service
|
||||
const status = yield* SessionStatus.Service
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: Params,
|
||||
execute: (params, ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const action = yield* Effect.promise(async () => {
|
||||
const promise = Suggestion.show({
|
||||
sessionID: ctx.sessionID,
|
||||
text: params.suggest,
|
||||
actions: params.actions.map((a) => ({ ...a })),
|
||||
blocking: false, // render above an active input; VS Code does the same
|
||||
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
|
||||
const promise = Suggestion.show({
|
||||
sessionID: ctx.sessionID,
|
||||
text: params.suggest,
|
||||
actions: params.actions.map((a) => ({ ...a })),
|
||||
blocking: false, // render above an active input; VS Code does the same
|
||||
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
|
||||
})
|
||||
|
||||
const listener = () =>
|
||||
Suggestion.list().then((items: Suggestion.Request[]) => {
|
||||
const match = items.find((item: Suggestion.Request) => item.tool?.callID === ctx.callID)
|
||||
if (match) return Suggestion.dismiss(match.id)
|
||||
})
|
||||
ctx.abort.addEventListener("abort", listener, { once: true })
|
||||
|
||||
const listener = () =>
|
||||
Suggestion.list().then((items: Suggestion.Request[]) => {
|
||||
const match = items.find((item: Suggestion.Request) => item.tool?.callID === ctx.callID)
|
||||
if (match) return Suggestion.dismiss(match.id)
|
||||
})
|
||||
ctx.abort.addEventListener("abort", listener, { once: true })
|
||||
// Mark the session as idle while waiting for user interaction so the
|
||||
// session doesn't appear stuck/busy. The loop will set it back to busy
|
||||
// when the suggestion resolves and processing continues.
|
||||
yield* status
|
||||
.set(SessionID.make(ctx.sessionID), { type: "idle" })
|
||||
.pipe(Effect.catchCause((cause) => Effect.sync(() => log.warn("failed to set idle status", { cause }))))
|
||||
|
||||
// Mark the session as idle while waiting for user interaction so the
|
||||
// session doesn't appear stuck/busy. The loop will set it back to busy
|
||||
// when the suggestion resolves and processing continues.
|
||||
await SessionStatus.set(SessionID.make(ctx.sessionID), { type: "idle" }).catch((err) => {
|
||||
log.warn("failed to set idle status", { err })
|
||||
})
|
||||
|
||||
const action = await promise
|
||||
const action = yield* Effect.promise(() =>
|
||||
promise
|
||||
.catch((error) => {
|
||||
if (error instanceof Suggestion.DismissedError) return undefined
|
||||
throw error
|
||||
})
|
||||
.finally(() => {
|
||||
ctx.abort.removeEventListener("abort", listener)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// Restore busy immediately on accept so the session doesn't flash idle
|
||||
// while the follow-up response is being generated. The next runLoop
|
||||
// iteration sets busy too, but not until after the stream finalizes.
|
||||
if (action) {
|
||||
await SessionStatus.set(SessionID.make(ctx.sessionID), { type: "busy" }).catch((err) => {
|
||||
log.warn("failed to restore busy status", { err })
|
||||
})
|
||||
}
|
||||
return action
|
||||
})
|
||||
// Restore busy immediately on accept so the session doesn't flash idle
|
||||
// while the follow-up response is being generated. The next runLoop
|
||||
// iteration sets busy too, but not until after the stream finalizes.
|
||||
if (action) {
|
||||
yield* status
|
||||
.set(SessionID.make(ctx.sessionID), { type: "busy" })
|
||||
.pipe(
|
||||
Effect.catchCause((cause) => Effect.sync(() => log.warn("failed to restore busy status", { cause }))),
|
||||
)
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
const metadata: Meta = {
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
HEADER_TASKID,
|
||||
} from "@kilocode/kilo-gateway"
|
||||
import { Identity } from "@kilocode/kilo-telemetry"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { KiloSession } from "@/kilocode/session"
|
||||
import { KiloLLM } from "@/kilocode/session/llm"
|
||||
// kilocode_change end
|
||||
@@ -71,7 +70,6 @@ export type Event = Result["fullStream"] extends AsyncIterable<infer T> ? T : ne
|
||||
|
||||
export interface Interface {
|
||||
readonly stream: (input: StreamInput) => Stream.Stream<Event, unknown>
|
||||
readonly raw: (input: StreamRequest) => Effect.Effect<Result> // kilocode_change - raw streamText result for Kilo helpers
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LLM") {}
|
||||
@@ -481,8 +479,7 @@ const live: Layer.Layer<
|
||||
),
|
||||
)
|
||||
|
||||
// kilocode_change - expose raw streamText result for Kilo helpers; Effect.orDie collapses AuthError into a defect
|
||||
return Service.of({ stream, raw: (input) => run(input).pipe(Effect.orDie) })
|
||||
return Service.of({ stream })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -497,13 +494,6 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
),
|
||||
)
|
||||
|
||||
// kilocode_change start - keep raw async stream wrapper for Kilo callsites during Effect migration
|
||||
const runtime = makeRuntime(Service, defaultLayer)
|
||||
export async function stream(input: StreamRequest) {
|
||||
return runtime.runPromise((svc) => svc.raw(input), { signal: input.abort })
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
|
||||
const disabled = Permission.disabled(
|
||||
Object.keys(input.tools),
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Bus } from "@/bus"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { SessionID } from "./schema"
|
||||
import { QuestionID } from "@/question/schema" // kilocode_change
|
||||
import { makeRuntime } from "@/effect/run-service" // kilocode_change
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
@@ -94,12 +93,4 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
|
||||
|
||||
// kilocode_change start - legacy promise helpers for Kilo callsites
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export const list = () => runPromise((svc) => svc.list())
|
||||
export const get = (sessionID: SessionID) => runPromise((svc) => svc.get(sessionID))
|
||||
export const set = (sessionID: SessionID, status: Info) => runPromise((svc) => svc.set(sessionID, status))
|
||||
// kilocode_change end
|
||||
|
||||
export * as SessionStatus from "./status"
|
||||
|
||||
@@ -7,7 +7,6 @@ import { withStatics } from "@/util/schema"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import type { Agent } from "@/agent/agent"
|
||||
import { Bus } from "@/bus"
|
||||
import { makeRuntime } from "@/effect/run-service" // kilocode_change
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
@@ -285,13 +284,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Global.layer),
|
||||
)
|
||||
|
||||
// kilocode_change start - legacy promise helpers for Kilo callsites
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
export const all = () => runPromise((svc) => svc.all())
|
||||
export const get = (name: string) => runPromise((svc) => svc.get(name))
|
||||
export const dirs = () => runPromise((svc) => svc.dirs())
|
||||
// kilocode_change end
|
||||
|
||||
export function fmt(list: Info[], opts: { verbose: boolean }) {
|
||||
if (list.length === 0) return "No skills are currently available."
|
||||
if (opts.verbose) {
|
||||
|
||||
@@ -53,6 +53,7 @@ import { Agent } from "../agent/agent"
|
||||
import { Git } from "../git" // kilocode_change
|
||||
import { Skill } from "../skill"
|
||||
import { Permission } from "@/permission"
|
||||
import { SessionStatus } from "@/session/status" // kilocode_change
|
||||
|
||||
const log = Log.create({ service: "tool.registry" })
|
||||
|
||||
@@ -97,6 +98,7 @@ export const layer: Layer.Layer<
|
||||
| Truncate.Service
|
||||
| Command.Service // kilocode_change
|
||||
| Git.Service // kilocode_change
|
||||
| SessionStatus.Service // kilocode_change
|
||||
> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
@@ -378,6 +380,7 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
Layer.provide(Truncate.defaultLayer),
|
||||
Layer.provide(Command.defaultLayer), // kilocode_change
|
||||
Layer.provide(Git.defaultLayer), // kilocode_change
|
||||
Layer.provide(SessionStatus.defaultLayer), // kilocode_change
|
||||
),
|
||||
)
|
||||
// kilocode_change start
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { afterEach, test, expect } from "bun:test"
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import path from "path"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { BUILTIN_SKILLS } from "../../src/kilocode/skills/builtin"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
})
|
||||
const it = testEffect(Layer.mergeAll(Skill.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
test("built-in skills are present in empty project", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
it.instance(
|
||||
"built-in skills are present in empty project",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const skill = yield* Skill.Service
|
||||
const skills = yield* skill.all()
|
||||
for (const builtin of BUILTIN_SKILLS) {
|
||||
const found = skills.find((s) => s.name === builtin.name)
|
||||
expect(found).toBeDefined()
|
||||
@@ -23,33 +22,34 @@ test("built-in skills are present in empty project", async () => {
|
||||
expect(found!.description).toBe(builtin.description)
|
||||
expect(found!.content.length).toBeGreaterThan(0)
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
test("built-in skill has correct metadata", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
it.instance(
|
||||
"built-in skill has correct metadata",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const skill = yield* Skill.Service
|
||||
const item = yield* skill.get("kilo-config")
|
||||
expect(item).toBeDefined()
|
||||
expect(item!.name).toBe("kilo-config")
|
||||
expect(item!.location).toBe(Skill.BUILTIN_LOCATION)
|
||||
expect(item!.content).toContain("kilo")
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skill = await Skill.get("kilo-config")
|
||||
expect(skill).toBeDefined()
|
||||
expect(skill!.name).toBe("kilo-config")
|
||||
expect(skill!.location).toBe(Skill.BUILTIN_LOCATION)
|
||||
expect(skill!.content).toContain("kilo")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("user skill overrides built-in with same name", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".kilo", "skill", "kilo-config")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
it.instance(
|
||||
"user skill overrides built-in with same name",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const instance = yield* TestInstance
|
||||
const dir = path.join(instance.directory, ".kilo", "skill", "kilo-config")
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(
|
||||
path.join(dir, "SKILL.md"),
|
||||
`---
|
||||
name: kilo-config
|
||||
description: User override of kilo-config.
|
||||
---
|
||||
@@ -58,18 +58,15 @@ description: User override of kilo-config.
|
||||
|
||||
User-provided content.
|
||||
`,
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skill = await Skill.get("kilo-config")
|
||||
expect(skill).toBeDefined()
|
||||
expect(skill!.description).toBe("User override of kilo-config.")
|
||||
expect(skill!.location).not.toBe(Skill.BUILTIN_LOCATION)
|
||||
expect(skill!.location).toContain(path.join("skill", "kilo-config", "SKILL.md"))
|
||||
},
|
||||
})
|
||||
})
|
||||
const skill = yield* Skill.Service
|
||||
const item = yield* skill.get("kilo-config")
|
||||
expect(item).toBeDefined()
|
||||
expect(item!.description).toBe("User override of kilo-config.")
|
||||
expect(item!.location).not.toBe(Skill.BUILTIN_LOCATION)
|
||||
expect(item!.location).toContain(path.join("skill", "kilo-config", "SKILL.md"))
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, test, mock, beforeEach } from "bun:test"
|
||||
import { describe, expect, test, mock, beforeEach, spyOn } from "bun:test"
|
||||
import type { GitContext } from "@/kilocode/commit-message/types"
|
||||
|
||||
// Mock dependencies before importing the module under test.
|
||||
@@ -8,7 +8,6 @@ import type { GitContext } from "@/kilocode/commit-message/types"
|
||||
|
||||
const realLog = await import("@opencode-ai/core/util/log")
|
||||
const realProvider = await import("@/provider/provider")
|
||||
const realLLM = await import("@/session/llm")
|
||||
const realAgent = await import("@/agent/agent")
|
||||
const realGitContext = await import("@/kilocode/commit-message/git-context")
|
||||
|
||||
@@ -50,19 +49,6 @@ mock.module("@/provider/provider", () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module("@/session/llm", () => ({
|
||||
...realLLM,
|
||||
LLM: {
|
||||
...realLLM.LLM,
|
||||
stream: async () => ({
|
||||
textStream: (async function* () {
|
||||
yield mockStreamText
|
||||
})(),
|
||||
text: Promise.resolve(mockStreamText),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module("@/agent/agent", () => ({
|
||||
...realAgent,
|
||||
Agent: {},
|
||||
@@ -78,10 +64,13 @@ mock.module("@opencode-ai/core/util/log", () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
import { generateCommitMessage } from "../../../src/kilocode/commit-message/generate"
|
||||
import { CommitMessageRuntime, generateCommitMessage } from "../../../src/kilocode/commit-message/generate"
|
||||
|
||||
const stream = spyOn(CommitMessageRuntime, "generate").mockImplementation(async () => mockStreamText)
|
||||
|
||||
describe("commit-message.generate", () => {
|
||||
beforeEach(() => {
|
||||
stream.mockImplementation(async () => mockStreamText)
|
||||
mockStreamText = "feat(src): add hello world logging"
|
||||
mockGitContext = { ...defaultGitContext }
|
||||
captured = { path: "" }
|
||||
|
||||
@@ -125,7 +125,6 @@ function llm() {
|
||||
const stream = typeof item === "function" ? item(input) : item
|
||||
return stream.pipe(Stream.mapEffect((event) => Effect.succeed(event)))
|
||||
},
|
||||
raw: () => Effect.die("raw not implemented in test LLM"),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { Question } from "../../src/question"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
@@ -235,17 +234,15 @@ function mockHandoverDeps(text: string, opts?: { agent?: Agent.Info | null }) {
|
||||
(opts?.agent === null ? undefined : (opts?.agent ?? fakeAgent)) as any,
|
||||
)
|
||||
const modelSpy = spyOn(PlanFollowupRuntime, "model").mockResolvedValue(fakeModel)
|
||||
const llmSpy = spyOn(LLM, "stream").mockResolvedValue({
|
||||
text: Promise.resolve(text),
|
||||
} as any)
|
||||
const handoverSpy = spyOn(PlanFollowupRuntime, "handover").mockResolvedValue(text)
|
||||
return {
|
||||
agentSpy,
|
||||
modelSpy,
|
||||
llmSpy,
|
||||
handoverSpy,
|
||||
[Symbol.dispose]() {
|
||||
agentSpy.mockRestore()
|
||||
modelSpy.mockRestore()
|
||||
llmSpy.mockRestore()
|
||||
handoverSpy.mockRestore()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -568,16 +565,14 @@ describe("plan follow-up", () => {
|
||||
return fakeModel
|
||||
},
|
||||
)
|
||||
const llmSpy = spyOn(LLM, "stream").mockResolvedValue({
|
||||
text: Promise.resolve(
|
||||
"## Discoveries\n\nFound REST endpoints in src/api.ts\n\n## Relevant Files\n\n- src/api.ts: REST endpoints\n- src/db.ts: Database layer",
|
||||
),
|
||||
} as any)
|
||||
const handoverSpy = spyOn(PlanFollowupRuntime, "handover").mockResolvedValue(
|
||||
"## Discoveries\n\nFound REST endpoints in src/api.ts\n\n## Relevant Files\n\n- src/api.ts: REST endpoints\n- src/db.ts: Database layer",
|
||||
)
|
||||
using _mocks = {
|
||||
llmSpy,
|
||||
handoverSpy,
|
||||
[Symbol.dispose]() {
|
||||
modelSpy.mockRestore()
|
||||
llmSpy.mockRestore()
|
||||
handoverSpy.mockRestore()
|
||||
},
|
||||
}
|
||||
using _loop = {
|
||||
@@ -626,7 +621,7 @@ describe("plan follow-up", () => {
|
||||
expect(added).toHaveLength(1)
|
||||
expect(created).toHaveLength(1)
|
||||
expect(loop).toHaveBeenCalledTimes(1)
|
||||
expect(_mocks.llmSpy).toHaveBeenCalledTimes(1)
|
||||
expect(_mocks.handoverSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
const newSessionID = created[0]
|
||||
const next = added[0]
|
||||
@@ -665,9 +660,7 @@ describe("plan follow-up", () => {
|
||||
await using other = await tmpdir({ git: true })
|
||||
const get = spyOn(PlanFollowupRuntime, "agent").mockImplementation(async () => undefined as any)
|
||||
const modelSpy = spyOn(PlanFollowupRuntime, "model").mockResolvedValue(fakeModel)
|
||||
const llmSpy = spyOn(LLM, "stream").mockResolvedValue({
|
||||
text: Promise.resolve(""),
|
||||
} as any)
|
||||
const handoverSpy = spyOn(PlanFollowupRuntime, "handover").mockResolvedValue("")
|
||||
const loop = spyOn(PlanFollowupRuntime, "loop").mockResolvedValue({
|
||||
info: {
|
||||
id: MessageID.make("msg_test"),
|
||||
@@ -695,7 +688,7 @@ describe("plan follow-up", () => {
|
||||
[Symbol.dispose]() {
|
||||
get.mockRestore()
|
||||
modelSpy.mockRestore()
|
||||
llmSpy.mockRestore()
|
||||
handoverSpy.mockRestore()
|
||||
loop.mockRestore()
|
||||
},
|
||||
}
|
||||
@@ -984,12 +977,12 @@ describe("plan follow-up", () => {
|
||||
const deferred = Promise.withResolvers<string>()
|
||||
const agentSpy = spyOn(PlanFollowupRuntime, "agent").mockResolvedValue(fakeAgent as any)
|
||||
const modelSpy = spyOn(PlanFollowupRuntime, "model").mockResolvedValue(fakeModel)
|
||||
const llmSpy = spyOn(LLM, "stream").mockResolvedValue({
|
||||
text: deferred.promise.then((t) => {
|
||||
const handoverSpy = spyOn(PlanFollowupRuntime, "handover").mockImplementation(() =>
|
||||
deferred.promise.then((text) => {
|
||||
handoverResolvedAt = performance.now()
|
||||
return t
|
||||
return text
|
||||
}),
|
||||
} as any)
|
||||
)
|
||||
const loop = spyOn(PlanFollowupRuntime, "loop").mockResolvedValue({
|
||||
info: {
|
||||
id: MessageID.make("msg_test"),
|
||||
@@ -1017,7 +1010,7 @@ describe("plan follow-up", () => {
|
||||
[Symbol.dispose]() {
|
||||
agentSpy.mockRestore()
|
||||
modelSpy.mockRestore()
|
||||
llmSpy.mockRestore()
|
||||
handoverSpy.mockRestore()
|
||||
loop.mockRestore()
|
||||
unsub()
|
||||
},
|
||||
@@ -1071,9 +1064,7 @@ describe("plan follow-up", () => {
|
||||
const deferred = Promise.withResolvers<string>()
|
||||
const agentSpy = spyOn(PlanFollowupRuntime, "agent").mockResolvedValue(fakeAgent as any)
|
||||
const modelSpy = spyOn(PlanFollowupRuntime, "model").mockResolvedValue(fakeModel)
|
||||
const llmSpy = spyOn(LLM, "stream").mockResolvedValue({
|
||||
text: deferred.promise,
|
||||
} as any)
|
||||
const handoverSpy = spyOn(PlanFollowupRuntime, "handover").mockImplementation(() => deferred.promise)
|
||||
const loop = spyOn(PlanFollowupRuntime, "loop").mockResolvedValue({
|
||||
info: {
|
||||
id: MessageID.make("msg_test"),
|
||||
@@ -1095,7 +1086,7 @@ describe("plan follow-up", () => {
|
||||
[Symbol.dispose]() {
|
||||
agentSpy.mockRestore()
|
||||
modelSpy.mockRestore()
|
||||
llmSpy.mockRestore()
|
||||
handoverSpy.mockRestore()
|
||||
loop.mockRestore()
|
||||
unsub()
|
||||
},
|
||||
@@ -1170,9 +1161,7 @@ describe("plan follow-up", () => {
|
||||
const deferred = Promise.withResolvers<string>()
|
||||
const agentSpy = spyOn(PlanFollowupRuntime, "agent").mockResolvedValue(fakeAgent as any)
|
||||
const modelSpy = spyOn(PlanFollowupRuntime, "model").mockResolvedValue(fakeModel)
|
||||
const llmSpy = spyOn(LLM, "stream").mockResolvedValue({
|
||||
text: deferred.promise,
|
||||
} as any)
|
||||
const handoverSpy = spyOn(PlanFollowupRuntime, "handover").mockImplementation(() => deferred.promise)
|
||||
const loop = spyOn(PlanFollowupRuntime, "loop").mockResolvedValue({
|
||||
info: {
|
||||
id: MessageID.make("msg_test"),
|
||||
@@ -1194,7 +1183,7 @@ describe("plan follow-up", () => {
|
||||
[Symbol.dispose]() {
|
||||
agentSpy.mockRestore()
|
||||
modelSpy.mockRestore()
|
||||
llmSpy.mockRestore()
|
||||
handoverSpy.mockRestore()
|
||||
loop.mockRestore()
|
||||
created()
|
||||
status()
|
||||
@@ -1316,16 +1305,16 @@ describe("plan follow-up", () => {
|
||||
expect(result).toBe("- [x] Set up project\n- [~] Write code\n- [ ] Add tests\n- [-] Dropped task")
|
||||
})
|
||||
|
||||
test("generateHandover - returns empty string on LLM.stream failure", () =>
|
||||
test("generateHandover - returns empty string on LLM stream failure", () =>
|
||||
withInstance(async () => {
|
||||
const agentSpy = spyOn(PlanFollowupRuntime, "agent").mockResolvedValue(fakeAgent)
|
||||
const modelSpy = spyOn(PlanFollowupRuntime, "model").mockResolvedValue(fakeModel)
|
||||
const llmSpy = spyOn(LLM, "stream").mockRejectedValue(new Error("provider unavailable"))
|
||||
const handoverSpy = spyOn(PlanFollowupRuntime, "handover").mockRejectedValue(new Error("provider unavailable"))
|
||||
using _ = {
|
||||
[Symbol.dispose]() {
|
||||
agentSpy.mockRestore()
|
||||
modelSpy.mockRestore()
|
||||
llmSpy.mockRestore()
|
||||
handoverSpy.mockRestore()
|
||||
},
|
||||
}
|
||||
const seeded = await seed({ text: "1. Build\n2. Test" })
|
||||
@@ -1333,22 +1322,16 @@ describe("plan follow-up", () => {
|
||||
expect(result).toBe("")
|
||||
}))
|
||||
|
||||
test("generateHandover - returns empty string on stream.text rejection", () =>
|
||||
test("generateHandover - returns empty string on text stream rejection", () =>
|
||||
withInstance(async () => {
|
||||
const agentSpy = spyOn(PlanFollowupRuntime, "agent").mockResolvedValue(fakeAgent)
|
||||
const modelSpy = spyOn(PlanFollowupRuntime, "model").mockResolvedValue(fakeModel)
|
||||
const textPromise = new Promise<string>((_, reject) => {
|
||||
setTimeout(() => reject(new Error("stream aborted")), 0)
|
||||
})
|
||||
textPromise.catch(() => {})
|
||||
const llmSpy = spyOn(LLM, "stream").mockResolvedValue({
|
||||
text: textPromise,
|
||||
} as any)
|
||||
const handoverSpy = spyOn(PlanFollowupRuntime, "handover").mockRejectedValue(new Error("stream aborted"))
|
||||
using _ = {
|
||||
[Symbol.dispose]() {
|
||||
agentSpy.mockRestore()
|
||||
modelSpy.mockRestore()
|
||||
llmSpy.mockRestore()
|
||||
handoverSpy.mockRestore()
|
||||
},
|
||||
}
|
||||
const seeded = await seed({ text: "1. Build\n2. Test" })
|
||||
@@ -1363,7 +1346,7 @@ describe("plan follow-up", () => {
|
||||
const result = await generateHandover({ messages: seeded.messages, model })
|
||||
expect(result).toBe("## Discoveries\n\nFallback works")
|
||||
expect(mocks.agentSpy).toHaveBeenCalledWith("compaction")
|
||||
expect(mocks.llmSpy).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.handoverSpy).toHaveBeenCalledTimes(1)
|
||||
}))
|
||||
|
||||
test("generateHandover - returns LLM output on success", () =>
|
||||
@@ -1372,6 +1355,6 @@ describe("plan follow-up", () => {
|
||||
const seeded = await seed({ text: "1. Build\n2. Test" })
|
||||
const result = await generateHandover({ messages: seeded.messages, model })
|
||||
expect(result).toBe("## Discoveries\n\nKey finding here")
|
||||
expect(mocks.llmSpy).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.handoverSpy).toHaveBeenCalledTimes(1)
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -117,7 +117,6 @@ function llm() {
|
||||
const stream = typeof item === "function" ? item(input) : item
|
||||
return stream.pipe(Stream.mapEffect((event) => Effect.succeed(event)))
|
||||
},
|
||||
raw: () => Effect.die("raw not implemented in test LLM"),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ const llm = Layer.unwrap(
|
||||
const item = queue.shift() ?? Stream.empty
|
||||
return item
|
||||
},
|
||||
raw: () => Effect.die("raw not implemented in TestLLM"),
|
||||
}),
|
||||
),
|
||||
Layer.succeed(TestLLM, TestLLM.of({ reply })),
|
||||
|
||||
@@ -83,7 +83,6 @@ const llm = Layer.unwrap(
|
||||
const item = queue.shift() ?? Stream.empty
|
||||
return item
|
||||
},
|
||||
raw: () => Effect.die("raw not implemented in TestLLM"),
|
||||
}),
|
||||
),
|
||||
Layer.succeed(TestLLM, TestLLM.of({ push })),
|
||||
|
||||
@@ -96,7 +96,6 @@ const llm = Layer.unwrap(
|
||||
const item = queue.shift() ?? Stream.fail(new Error("unexpected extra llm call"))
|
||||
return item
|
||||
},
|
||||
raw: () => Effect.die("raw not implemented in TestLLM"),
|
||||
}),
|
||||
),
|
||||
Layer.succeed(TestLLM, TestLLM.of({ push, calls: Effect.sync(() => calls) })),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { KiloLLM } from "@/kilocode/session/llm"
|
||||
import type { LLM } from "@/session/llm"
|
||||
|
||||
describe("kilocode.session.llm.text", () => {
|
||||
test("joins text delta events", async () => {
|
||||
const out = await Effect.runPromise(
|
||||
KiloLLM.text(
|
||||
Stream.make(
|
||||
{ type: "text-delta", id: "text", text: "hello ", delta: "hello " } as LLM.Event,
|
||||
{ type: "text-delta", id: "text", text: "world", delta: "world" } as LLM.Event,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(out).toBe("hello world")
|
||||
})
|
||||
|
||||
test("fails on error events after partial text", async () => {
|
||||
const err = new Error("provider unavailable")
|
||||
const text = KiloLLM.text(
|
||||
Stream.make(
|
||||
{ type: "text-delta", id: "text", text: "partial", delta: "partial" } as LLM.Event,
|
||||
{ type: "error", error: err } as LLM.Event,
|
||||
),
|
||||
)
|
||||
|
||||
await expect(Effect.runPromise(text)).rejects.toThrow("provider unavailable")
|
||||
})
|
||||
|
||||
test("fails on abort events", async () => {
|
||||
const text = KiloLLM.text(Stream.make({ type: "abort" } as LLM.Event))
|
||||
|
||||
await expect(Effect.runPromise(text)).rejects.toMatchObject({ name: "AbortError" })
|
||||
})
|
||||
})
|
||||
@@ -22,7 +22,16 @@ const command = Layer.succeed(
|
||||
list: () => Effect.succeed(Object.values(cmds)),
|
||||
}),
|
||||
)
|
||||
const it = testEffect(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer, command))
|
||||
const statuses: Array<[string, SessionStatus.Info]> = []
|
||||
const status = Layer.succeed(
|
||||
SessionStatus.Service,
|
||||
SessionStatus.Service.of({
|
||||
get: () => Effect.succeed({ type: "idle" }),
|
||||
list: () => Effect.succeed(new Map()),
|
||||
set: (sessionID, value) => Effect.sync(() => statuses.push([sessionID, value])),
|
||||
}),
|
||||
)
|
||||
const it = testEffect(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer, command, status))
|
||||
|
||||
const init = Effect.fn("SuggestToolTest.init")(function* () {
|
||||
const info = yield* SuggestTool
|
||||
@@ -54,18 +63,16 @@ const ctx = {
|
||||
|
||||
describe("tool.suggest", () => {
|
||||
let show: ReturnType<typeof spyOn>
|
||||
let statusSet: ReturnType<typeof spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
show = spyOn(Suggestion, "show")
|
||||
statusSet = spyOn(SessionStatus, "set").mockResolvedValue(undefined as any)
|
||||
names.length = 0
|
||||
statuses.length = 0
|
||||
for (const name of Object.keys(cmds)) delete cmds[name]
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
show.mockRestore()
|
||||
statusSet.mockRestore()
|
||||
})
|
||||
|
||||
it.live("returns dismissal result when suggestion is dismissed", () =>
|
||||
@@ -250,8 +257,8 @@ describe("tool.suggest", () => {
|
||||
// idle status call has been issued.
|
||||
yield* Effect.sleep("10 millis")
|
||||
|
||||
expect(statusSet).toHaveBeenCalledWith(ctx.sessionID, { type: "idle" })
|
||||
expect(statusSet).not.toHaveBeenCalledWith(ctx.sessionID, { type: "busy" })
|
||||
expect(statuses).toContainEqual([ctx.sessionID, { type: "idle" }])
|
||||
expect(statuses).not.toContainEqual([ctx.sessionID, { type: "busy" }])
|
||||
|
||||
resolveShow({ label: "Start", prompt: "do it" })
|
||||
yield* Fiber.join(pending)
|
||||
@@ -275,10 +282,7 @@ describe("tool.suggest", () => {
|
||||
ctx as any,
|
||||
)
|
||||
|
||||
const statuses = statusSet.mock.calls
|
||||
.filter((call: unknown[]) => call[0] === ctx.sessionID)
|
||||
.map((call: unknown[]) => (call[1] as { type: string }).type)
|
||||
expect(statuses).toEqual(["idle", "busy"])
|
||||
expect(statuses.map(([, value]) => value.type)).toEqual(["idle", "busy"])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -299,10 +303,7 @@ describe("tool.suggest", () => {
|
||||
ctx as any,
|
||||
)
|
||||
|
||||
const statuses = statusSet.mock.calls
|
||||
.filter((call: unknown[]) => call[0] === ctx.sessionID)
|
||||
.map((call: unknown[]) => (call[1] as { type: string }).type)
|
||||
expect(statuses).toEqual(["idle"])
|
||||
expect(statuses.map(([, value]) => value.type)).toEqual(["idle"])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -270,7 +270,6 @@ function llm() {
|
||||
const stream = typeof item === "function" ? item(input) : item
|
||||
return stream.pipe(Stream.mapEffect((event) => Effect.succeed(event)))
|
||||
},
|
||||
raw: () => Effect.die("raw not implemented in test LLM"),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Ripgrep } from "@/file/ripgrep"
|
||||
import * as Truncate from "@/tool/truncate"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { WithInstance } from "@/project/with-instance"
|
||||
import { SessionStatus } from "@/session/status" // kilocode_change
|
||||
|
||||
const node = CrossSpawnSpawner.defaultLayer
|
||||
const configLayer = TestConfig.layer({
|
||||
@@ -52,6 +53,7 @@ const registryLayer = ToolRegistry.layer.pipe(
|
||||
Layer.provide(Truncate.defaultLayer),
|
||||
Layer.provide(Command.defaultLayer), // kilocode_change
|
||||
Layer.provide(Git.defaultLayer), // kilocode_change
|
||||
Layer.provide(SessionStatus.defaultLayer), // kilocode_change
|
||||
)
|
||||
|
||||
const it = testEffect(Layer.mergeAll(registryLayer, node))
|
||||
|
||||
Reference in New Issue
Block a user