mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
chore: update kilo-vscode visual regression baselines
This commit is contained in:
committed by
Johnny Amancio
parent
76d06fd8ba
commit
5ca97417a0
@@ -8,6 +8,7 @@ import { Flag } from "../flag/flag"
|
||||
import { isAbsolute, join } from "path"
|
||||
import { existsSync } from "fs" // kilocode_change
|
||||
import { DbPreflight } from "../kilocode/db-preflight" // kilocode_change
|
||||
import { ensure as compat } from "../kilocode/database-compat" // kilocode_change
|
||||
import { DatabaseMigration } from "./migration"
|
||||
import { InstallationChannel } from "../installation/version"
|
||||
import { makeGlobalNode } from "../effect/app-node"
|
||||
@@ -33,6 +34,7 @@ const layer = Layer.effect(
|
||||
yield* db.run("PRAGMA foreign_keys = ON")
|
||||
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
yield* DatabaseMigration.apply(db)
|
||||
yield* compat(db) // kilocode_change - keep the shared database usable by released CLIs
|
||||
|
||||
return { db }
|
||||
}).pipe(Effect.orDie),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Effect } from "effect"
|
||||
import type { Database } from "../database/database"
|
||||
|
||||
type Db = Database.Interface["db"]
|
||||
|
||||
export function ensure(db: Db) {
|
||||
return db.transaction(
|
||||
(tx) =>
|
||||
Effect.gen(function* () {
|
||||
const rows = yield* tx.all<{ name: string }>("PRAGMA table_info('session_context_epoch')")
|
||||
const names = new Set(rows.map((row) => row.name))
|
||||
|
||||
if (!names.has("agent"))
|
||||
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `agent` text DEFAULT 'build' NOT NULL")
|
||||
if (!names.has("replacement_seq"))
|
||||
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `replacement_seq` integer")
|
||||
if (!names.has("revision"))
|
||||
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `revision` integer DEFAULT 0 NOT NULL")
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { FSUtil } from "../fs-util"
|
||||
import { LocationMutation } from "../location-mutation"
|
||||
import { Patch } from "../patch"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { ToolOutputStore } from "../tool-output-store" // kilocode_change
|
||||
import { ToolRegistry } from "./registry"
|
||||
import { Tool } from "./tool"
|
||||
import { Tools } from "./tools"
|
||||
@@ -34,6 +35,21 @@ export const Output = Schema.Struct({
|
||||
})
|
||||
export type Output = typeof Output.Type
|
||||
|
||||
// kilocode_change start - keep durable/SSE tool records bounded without hiding normal-sized diff previews
|
||||
const compact = (output: Output): Output => {
|
||||
if (Buffer.byteLength(JSON.stringify(output), "utf-8") <= ToolOutputStore.MAX_BYTES) return output
|
||||
return {
|
||||
...output,
|
||||
files: output.files.map((file) => ({
|
||||
additions: file.additions,
|
||||
deletions: file.deletions,
|
||||
...(file.file === undefined ? {} : { file: file.file }),
|
||||
...(file.status === undefined ? {} : { status: file.status }),
|
||||
})),
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
export const toModelOutput = (output: Output) =>
|
||||
[
|
||||
"Applied patch sequentially:",
|
||||
@@ -72,6 +88,8 @@ const layer = Layer.effectDiscard(
|
||||
"Apply one patch containing add, update, and delete file operations. All targets are resolved and approved before target contents are read. Operations apply sequentially; if a later operation fails, earlier operations remain applied and the failure reports them explicitly. Moves and atomic rollback are not supported yet.",
|
||||
input: Input,
|
||||
output: Output,
|
||||
structured: Output, // kilocode_change
|
||||
toStructuredOutput: ({ output }) => compact(output), // kilocode_change
|
||||
toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }],
|
||||
execute: (input, context) => {
|
||||
const applied: Array<typeof Applied.Type> = []
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FileMutation } from "../file-mutation"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { LocationMutation } from "../location-mutation"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { ToolOutputStore } from "../tool-output-store" // kilocode_change
|
||||
import { ToolRegistry } from "./registry"
|
||||
import { Tool } from "./tool"
|
||||
import { Tools } from "./tools"
|
||||
@@ -41,6 +42,21 @@ export const Output = Schema.Struct({
|
||||
})
|
||||
export type Output = typeof Output.Type
|
||||
|
||||
// kilocode_change start - keep durable/SSE tool records bounded without hiding normal-sized diff previews
|
||||
const compact = (output: Output): Output => {
|
||||
if (Buffer.byteLength(JSON.stringify(output), "utf-8") <= ToolOutputStore.MAX_BYTES) return output
|
||||
return {
|
||||
...output,
|
||||
files: output.files.map((file) => ({
|
||||
additions: file.additions,
|
||||
deletions: file.deletions,
|
||||
...(file.file === undefined ? {} : { file: file.file }),
|
||||
...(file.status === undefined ? {} : { status: file.status }),
|
||||
})),
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
const normalizeLineEndings = (text: string) => text.replaceAll("\r\n", "\n")
|
||||
const detectLineEnding = (text: string): "\n" | "\r\n" => (text.includes("\r\n") ? "\r\n" : "\n")
|
||||
const convertToLineEnding = (text: string, ending: "\n" | "\r\n") =>
|
||||
@@ -105,6 +121,8 @@ const layer = Layer.effectDiscard(
|
||||
"Replace exact text in one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.",
|
||||
input: Input,
|
||||
output: Output,
|
||||
structured: Output, // kilocode_change
|
||||
toStructuredOutput: ({ output }) => compact(output), // kilocode_change
|
||||
toModelOutput: ({ input, output }) => [
|
||||
{ type: "text", text: toModelOutput(output, input.oldString, input.newString) },
|
||||
],
|
||||
|
||||
@@ -11,6 +11,8 @@ import path from "path"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
import { SessionHistory } from "@opencode-ai/core/session/history"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { ensure } from "@opencode-ai/core/kilocode/database-compat"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
|
||||
const make = EffectDrizzleSqlite.makeWithDefaults()
|
||||
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
||||
@@ -43,18 +45,30 @@ describe("database migration compatibility", () => {
|
||||
)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, migrations.slice(split))
|
||||
yield* ensure(db)
|
||||
|
||||
// This is the projection shape written by the CLI bundled with VS Code v7.4.7.
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('message', 'ses_session', 'user', 1, 1, '{}')`,
|
||||
)
|
||||
yield* db.run(sql`UPDATE session_message SET data = '{"text":"updated"}' WHERE id = 'message'`)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_context_epoch (session_id, baseline, agent, snapshot, baseline_seq, revision) VALUES ('ses_session', 'baseline', 'build', '{}', 0, 0)`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`UPDATE session_context_epoch SET replacement_seq = 4, revision = revision + 1 WHERE session_id = 'ses_session'`,
|
||||
)
|
||||
|
||||
expect(yield* db.get(sql`SELECT id, seq, data FROM session_message WHERE id = 'message'`)).toEqual({
|
||||
id: "message",
|
||||
seq: null,
|
||||
data: '{"text":"updated"}',
|
||||
})
|
||||
expect(
|
||||
yield* db.get(
|
||||
sql`SELECT agent, replacement_seq AS replacementSeq, revision FROM session_context_epoch WHERE session_id = 'ses_session'`,
|
||||
),
|
||||
).toEqual({ agent: "build", replacementSeq: 4, revision: 1 })
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('msg_sequenced', 'ses_session', 'user', 1, 2, 2, '{"text":"current","files":[],"agents":[],"time":{"created":2}}')`,
|
||||
)
|
||||
@@ -119,6 +133,37 @@ describe("database migration compatibility", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("keeps released context epoch writes compatible on fresh current databases", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filename = path.join(tmp.path, "kilo.db")
|
||||
await Effect.runPromise(
|
||||
Database.Service.use((service) =>
|
||||
Effect.gen(function* () {
|
||||
const db = service.db
|
||||
yield* ensure(db)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('project', '/repo', 1, 1, '[]')`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'project', 'session', '/repo', 'Session', '7.4.7', 1, 1)`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_context_epoch (session_id, baseline, agent, snapshot, baseline_seq, revision) VALUES ('session', 'baseline', 'build', '{}', 0, 0)`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`UPDATE session_context_epoch SET replacement_seq = 4, revision = revision + 1 WHERE session_id = 'session'`,
|
||||
)
|
||||
|
||||
expect(
|
||||
yield* db.get(
|
||||
sql`SELECT agent, replacement_seq AS replacementSeq, revision FROM session_context_epoch WHERE session_id = 'session'`,
|
||||
),
|
||||
).toEqual({ agent: "build", replacementSeq: 4, revision: 1 })
|
||||
}),
|
||||
).pipe(Effect.provide(Database.layerFromPath(filename)), Effect.scoped),
|
||||
)
|
||||
})
|
||||
|
||||
test("preserves sequenced projections when repairing an already-migrated database", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -202,6 +202,33 @@ describe("ApplyPatchTool", () => {
|
||||
),
|
||||
)
|
||||
|
||||
// kilocode_change start
|
||||
it.live("omits oversized patches from durable structured output", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
const content = "x".repeat(ToolOutputStore.MAX_BYTES)
|
||||
const patch = `*** Begin Patch\n*** Add File: large.txt\n+${content}\n*** End Patch`
|
||||
return withTool(tmp.path, (registry) => settleTool(registry, call(patch))).pipe(
|
||||
Effect.tap((settled) =>
|
||||
Effect.sync(() => {
|
||||
expect(settled.output?.structured).toEqual({
|
||||
applied: [{ type: "add", resource: "large.txt", target: path.join(tmp.path, "large.txt") }],
|
||||
files: [{ file: "large.txt", status: "added", additions: 1, deletions: 0 }],
|
||||
})
|
||||
expect(Buffer.byteLength(JSON.stringify(settled.output?.structured), "utf-8")).toBeLessThanOrEqual(
|
||||
ToolOutputStore.MAX_BYTES,
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
it.live("rejects moves before applying any hunk", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
||||
@@ -159,6 +159,39 @@ describe("EditTool", () => {
|
||||
),
|
||||
)
|
||||
|
||||
// kilocode_change start
|
||||
it.live("omits an oversized patch from durable structured output", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
const target = path.join(tmp.path, "large.txt")
|
||||
const before = "a".repeat(ToolOutputStore.MAX_BYTES)
|
||||
const after = "b".repeat(ToolOutputStore.MAX_BYTES)
|
||||
return Effect.promise(() => fs.writeFile(target, before)).pipe(
|
||||
Effect.andThen(
|
||||
withTool(tmp.path, (registry) =>
|
||||
settleTool(registry, call({ path: "large.txt", oldString: before, newString: after })),
|
||||
),
|
||||
),
|
||||
Effect.tap((settled) =>
|
||||
Effect.sync(() => {
|
||||
expect(settled.output?.structured).toEqual({
|
||||
replacements: 1,
|
||||
files: [{ file: "large.txt", status: "modified", additions: 1, deletions: 1 }],
|
||||
})
|
||||
expect(Buffer.byteLength(JSON.stringify(settled.output?.structured), "utf-8")).toBeLessThanOrEqual(
|
||||
ToolOutputStore.MAX_BYTES,
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
it.live("accepts an absolute file path inside the active Location", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:58fef726cab9c37deeb8f3e4fac8f3d6c943acf11c46c0d8d53af93df028a4c2
|
||||
size 46120
|
||||
oid sha256:cef414cf59da1f632a7d5d9a2e44f792e7e2ea6b4041bcc214cb32c052f851d8
|
||||
size 46930
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b21ced6db14cbc68efe2f996542b7810e000a9c8a5e714c0aa434928865046ab
|
||||
size 32798
|
||||
oid sha256:bd700edca458baefb4ce6cea68ed78f8c002bfb6edd842dbea318c2fa443b814
|
||||
size 32789
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57e417f0d51ea0d9b15ab1dcee4d5b49fe064bfd6d9dad6c8d5cc38e59eea049
|
||||
size 27189
|
||||
oid sha256:d7fc23fcb7adf483c0b771ef601b23cb365dc7f40033b00fce703b35910aa4fc
|
||||
size 27159
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8ae5166647579b4ebe139206f34ce2227d7fa8c5fde537b7808355fd8a86c70a
|
||||
size 27309
|
||||
oid sha256:d11f4004ed3170647d385c14df077d235f5bb9bd6e5dec07557ee2014d553233
|
||||
size 27302
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0fc65d066f7ec387cb4f1a8980178281ad961b9b57df2bf1261d6650637414b8
|
||||
size 29764
|
||||
oid sha256:f8bd56ba87d0c2bbef8a2325ab2e9f0e956c89e2602ff8ca7415d5fb7c9fa1f3
|
||||
size 29709
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60de70c3e280fde6486647a2d037bbdb83070b9d1a34e96d4deac8835bb11bcf
|
||||
size 4238
|
||||
oid sha256:222790e6c32346cf6b6cc52a0859ee01d364b52a1ef8b9f385a6e9798adfccfb
|
||||
size 4256
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d86411a4a93770b39e06881761864f24bf7890b8da5faf21d065125cb4662b52
|
||||
size 6899
|
||||
oid sha256:e854dd761ae03a44210467d2550849b1e49dd9a3ee4efef50d5116962a9a4059
|
||||
size 6907
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7020df6386028c96fac00bedf8df1170146bb8ea851d2511944d6cee37365597
|
||||
size 34013
|
||||
oid sha256:c3ebab71919a7bafe609933ba8909f22e68171d3546473cca7ad5e3d859787ef
|
||||
size 30655
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3e104fbc1ff457317f7715937fb939142374ea20e20cda6aeb1d412867cf66c4
|
||||
size 7481
|
||||
oid sha256:76b359a64ad4dede420eb4669f973f80f87be63b5d0a1dfe36829f0c19698381
|
||||
size 7486
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5594e89d1abea2e4a0b534fe99d8802fa14edb60479b418bc4b9bee1fa85f7b6
|
||||
size 5172
|
||||
oid sha256:eecccfc0bbd53bd52261ff8cfc02c444c6d97f6438e6693d8cd998c36909708a
|
||||
size 5178
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:393b2bf1cfeb3ab01e864084f520bdc8ef1cbbeba87e47bb5037e90ca71e109e
|
||||
size 6757
|
||||
oid sha256:43ea789b53b0bd402231f30970e9f8f1648ac1d0bcd971102ed4769bd002d9e6
|
||||
size 6764
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49ecad51e50b6fcdb3c9f5eac8ef516032844de2beb77fee68d206216c1be8fe
|
||||
size 669184
|
||||
oid sha256:b7482c1c495c5f96fe1cda00a956e2e0480f261af043e15441f047e687c4b063
|
||||
size 660234
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1c09f8d7adef66fd01ec50a13dd8fdad1fe92f789216fd5bd8f83b92dc2e4ac1
|
||||
size 18496
|
||||
oid sha256:77964c111d0454d7985e3368b821e161e4d6d9fffee5438ee094ab34ff464d52
|
||||
size 18623
|
||||
|
||||
@@ -64,6 +64,9 @@ import { Truncate } from "@/tool/truncate"
|
||||
import { Image } from "@/image/image"
|
||||
import { decodeDataUrl } from "@/util/data-url"
|
||||
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect"
|
||||
import * as DateTime from "effect/DateTime" // kilocode_change
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event" // kilocode_change
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message" // kilocode_change
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { Instance } from "@/kilocode/instance"
|
||||
@@ -628,6 +631,17 @@ export const layer = Layer.effect(
|
||||
},
|
||||
}
|
||||
yield* sessions.updatePart(part)
|
||||
// kilocode_change start - dual-write the v2 shell record so the durable timeline correlates with the tool part
|
||||
if (flags.experimentalEventSystem) {
|
||||
yield* events.publish(SessionEvent.Shell.Started, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: SessionMessage.ID.create(),
|
||||
timestamp: DateTime.makeUnsafe(started),
|
||||
callID: part.callID,
|
||||
command: input.command,
|
||||
})
|
||||
}
|
||||
// kilocode_change end
|
||||
return { msg, part, cwd: ctx.directory }
|
||||
}).pipe(Effect.ensuring(markReady))
|
||||
|
||||
@@ -645,6 +659,16 @@ export const layer = Layer.effect(
|
||||
}
|
||||
if (timeout) output += "\n\n" + ["<metadata>", timeout, "</metadata>"].join("\n") // kilocode_change
|
||||
const completed = Date.now()
|
||||
// kilocode_change start
|
||||
if (flags.experimentalEventSystem) {
|
||||
yield* events.publish(SessionEvent.Shell.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(completed),
|
||||
callID: part.callID,
|
||||
output,
|
||||
})
|
||||
}
|
||||
// kilocode_change end
|
||||
if (!msg.time.completed) {
|
||||
msg.time.completed = completed
|
||||
yield* sessions.updateMessage(msg)
|
||||
|
||||
@@ -157,13 +157,13 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
yield* llm.tool("bash", { command: "sed -n 1,5p README.md" })
|
||||
const result = yield* opencode.run("run sed on readme", {
|
||||
format: "json",
|
||||
timeoutMs: 45_000,
|
||||
timeoutMs: 75_000,
|
||||
})
|
||||
opencode.expectExit(result, 1)
|
||||
expect(result.stderr).toContain("run ended with an auto-rejected permission; pass --auto for autonomous use")
|
||||
expect(opencode.parseJsonEvents(result.stdout).some((event) => event.type === "error")).toBe(true)
|
||||
}),
|
||||
60_000,
|
||||
90_000,
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
@@ -302,13 +302,17 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(events.map((event) => event.type)).toEqual([
|
||||
"step_start",
|
||||
"text",
|
||||
"tool_use",
|
||||
"text", // kilocode_change - a pre-denied tool settles before the SDK closes its preceding text part
|
||||
"step_finish",
|
||||
"step_start",
|
||||
"step_finish",
|
||||
])
|
||||
expect(events[1]?.part).toEqual(expect.objectContaining({ type: "text", text: "partial json" }))
|
||||
// kilocode_change start - the pre-denied tool completes before text-end
|
||||
expect(events.find((event) => event.type === "text")?.part).toEqual(
|
||||
expect.objectContaining({ type: "text", text: "partial json" }),
|
||||
)
|
||||
// kilocode_change end
|
||||
// kilocode_change - upstream asserts reason "unknown" here. Reaching that requires the bash call
|
||||
// to proceed without permission friction, which a Kilo headless run never does: left alone the ask
|
||||
// is auto-rejected (exit 1, no second step), and settling it up front changes the request sequence
|
||||
@@ -399,12 +403,13 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
yield* llm.hang
|
||||
const run = yield* opencode.startRun("wait forever")
|
||||
yield* llm.wait(1)
|
||||
const interrupted = Date.now() // kilocode_change - assert signal handling, independent of contended CLI startup
|
||||
run.interrupt()
|
||||
const result = yield* run.result
|
||||
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.durationMs).toBeLessThan(30_000)
|
||||
expect(Date.now() - interrupted).toBeLessThan(10_000) // kilocode_change
|
||||
}),
|
||||
30_000,
|
||||
60_000, // kilocode_change
|
||||
)
|
||||
})
|
||||
|
||||
@@ -37,11 +37,11 @@ describe("tui thread", () => {
|
||||
await check(".")
|
||||
})
|
||||
|
||||
test("resolves a relative mini project from PWD when cwd differs", async () => {
|
||||
test("ignores stale PWD when resolving a relative mini project", async () => { // kilocode_change
|
||||
await using pwd = await tmpdir({ git: true })
|
||||
await using cwd = await tmpdir({ git: true })
|
||||
|
||||
expect(resolveThreadDirectory(".", pwd.path, cwd.path)).toBe(pwd.path)
|
||||
expect(resolveThreadDirectory(".", pwd.path, cwd.path)).toBe(cwd.path) // kilocode_change
|
||||
expect(resolveThreadDirectory(undefined, pwd.path, cwd.path)).toBe(cwd.path)
|
||||
})
|
||||
|
||||
|
||||
@@ -9,12 +9,13 @@ describe("public event manifest", () => {
|
||||
expect(EventManifest.Definitions).toBe(SchemaEventManifest.Definitions)
|
||||
expect(EventManifest.Latest).toBe(SchemaEventManifest.Latest)
|
||||
expect(EventManifest.Durable).toBe(SchemaEventManifest.Durable)
|
||||
expect(EventManifest.Latest.size).toBe(88)
|
||||
expect(EventManifest.Latest.size).toBe(89) // kilocode_change - include global.config.updated
|
||||
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
|
||||
expect(EventManifest.Latest.get("todo.updated")).toBe(Todo.Event.Updated)
|
||||
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
|
||||
expect(EventManifest.Latest.has("server.connected")).toBe(true)
|
||||
expect(EventManifest.Latest.has("global.disposed")).toBe(true)
|
||||
expect(EventManifest.Latest.has("global.config.updated")).toBe(true) // kilocode_change
|
||||
})
|
||||
|
||||
test("contains only the current step settlement versions", () => {
|
||||
|
||||
Vendored
+2
-2
@@ -22,7 +22,7 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"instructions\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
|
||||
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"instructions\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
@@ -38,7 +38,7 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_uoEsDnHNhxMLpCUy6hqEyHme\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_uoEsDnHNhxMLpCUy6hqEyHme\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"instructions\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
|
||||
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_uoEsDnHNhxMLpCUy6hqEyHme\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_uoEsDnHNhxMLpCUy6hqEyHme\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"instructions\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
|
||||
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
@@ -40,7 +40,7 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
|
||||
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
import { APICallError } from "ai"
|
||||
import { Effect, Layer, ManagedRuntime, Scope } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { LLMEvent, type LLMEvent as Event } from "@opencode-ai/llm"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "../../src/event-v2-bridge"
|
||||
import { Image } from "../../src/image/image"
|
||||
import { KiloCompactionPayloadRecovery } from "../../src/kilocode/session/compaction-payload-recovery"
|
||||
import { KiloSessionCompaction } from "../../src/kilocode/session/compaction"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { Plugin } from "../../src/plugin"
|
||||
import { provideTestInstance } from "../fixture/fixture"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import * as SessionProcessorModule from "../../src/session/processor"
|
||||
import { Session as SessionNs } from "../../src/session/session"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { SessionCompaction } from "../../src/session/compaction"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
import { SessionSummary } from "../../src/session/summary"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
import { ProviderTest } from "../fake/provider"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const sessionID = SessionID.make("ses_payload_recovery")
|
||||
@@ -68,6 +62,7 @@ const summary = Layer.succeed(
|
||||
computeDiff: () => Effect.succeed([]),
|
||||
}),
|
||||
)
|
||||
const summaryNode = LayerNode.make({ service: SessionSummary.Service, layer: summary, deps: [] })
|
||||
|
||||
function base(id: MessageID) {
|
||||
return {
|
||||
@@ -156,29 +151,26 @@ function reply(
|
||||
const scope = Layer.effect(Scope.Scope, Scope.make())
|
||||
|
||||
function runtime(layer: Layer.Layer<LLM.Service>, config = AppNodeBuilder.build(Config.node)) {
|
||||
const bus = Bus.layer
|
||||
const status = AppNodeBuilder.build(SessionStatus.node)
|
||||
const processor = AppNodeBuilder.build(SessionProcessorModule.SessionProcessor.node, [
|
||||
[SessionSummary.node, summary],
|
||||
])
|
||||
const model = ProviderTest.model({ providerID, id: modelID, limit: { context: 100_000, output: 32_000 } })
|
||||
const llm = LayerNode.make({ service: LLM.Service, layer, deps: [] })
|
||||
const cfg = LayerNode.make({ service: Config.Service, layer: config, deps: [] })
|
||||
const provider = LayerNode.make({
|
||||
service: Provider.Service,
|
||||
layer: ProviderTest.fake({ model }).layer,
|
||||
deps: [],
|
||||
})
|
||||
const flags = LayerNode.make({ service: RuntimeFlags.Service, layer: RuntimeFlags.layer(), deps: [] })
|
||||
return ManagedRuntime.make(
|
||||
Layer.mergeAll(AppNodeBuilder.build(SessionCompaction.node, [[SessionProcessorModule.SessionProcessor.node, processor], [SessionSummary.node, summary]]), processor, bus, status).pipe(
|
||||
Layer.provide(ProviderTest.fake({ model }).layer),
|
||||
Layer.provideMerge(AppNodeBuilder.build(SessionNs.node)),
|
||||
Layer.provide(AppNodeBuilder.build(Snapshot.node)),
|
||||
Layer.provide(layer),
|
||||
Layer.provide(AppNodeBuilder.build(Permission.node)),
|
||||
Layer.provide(AppNodeBuilder.build(Agent.node)),
|
||||
Layer.provide(AppNodeBuilder.build(Plugin.node)),
|
||||
Layer.provide(status),
|
||||
Layer.provide(bus),
|
||||
Layer.provide(config),
|
||||
Layer.provide(RuntimeFlags.layer()),
|
||||
LayerNode.compile(LayerNode.group([SessionCompaction.node, SessionNs.node, SessionProjector.node]), [
|
||||
[LLM.node, llm],
|
||||
[Config.node, cfg],
|
||||
[Provider.node, provider],
|
||||
[RuntimeFlags.node, flags],
|
||||
[SessionSummary.node, summaryNode],
|
||||
]).pipe(
|
||||
Layer.provide(Bus.layer),
|
||||
Layer.provide(scope),
|
||||
Layer.provide(SyncEvent.defaultLayer),
|
||||
Layer.provide(AppNodeBuilder.build(EventV2Bridge.node)),
|
||||
Layer.provide(AppNodeBuilder.build(Database.node)),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -268,132 +260,136 @@ describe("KiloCompactionPayloadRecovery", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("retries compaction without media and tool outputs after payload-size failure", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const stub = llm()
|
||||
const captures: string[] = []
|
||||
stub.push((input) => {
|
||||
captures.push(JSON.stringify(input.messages))
|
||||
return Stream.fail(
|
||||
new APICallError({
|
||||
message: "Request Entity Too Large",
|
||||
url: "https://api.kilo.ai/api/openrouter/responses",
|
||||
requestBodyValues: {},
|
||||
statusCode: 413,
|
||||
responseHeaders: { "content-type": "text/plain" },
|
||||
responseBody: "Request Entity Too Large\n\nFUNCTION_PAYLOAD_TOO_LARGE",
|
||||
isRetryable: false,
|
||||
}),
|
||||
)
|
||||
})
|
||||
stub.push(
|
||||
reply("summary", (input) => {
|
||||
test(
|
||||
"retries compaction without media and tool outputs after payload-size failure",
|
||||
async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const stub = llm()
|
||||
const captures: string[] = []
|
||||
stub.push((input) => {
|
||||
captures.push(JSON.stringify(input.messages))
|
||||
}),
|
||||
)
|
||||
|
||||
await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const rt = runtime(stub.layer, AppNodeBuilder.build(Config.node))
|
||||
const svc = service(rt)
|
||||
const session = await svc.create({})
|
||||
const old = await user(svc, session.id, "old image turn")
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: old.id,
|
||||
sessionID: session.id,
|
||||
type: "file",
|
||||
mime: "image/png",
|
||||
filename: "old.png",
|
||||
url: `data:image/png;base64,${"a".repeat(8_000)}`,
|
||||
})
|
||||
const oldReply = await assistant(svc, session.id, old.id, tmp.path)
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: oldReply.id,
|
||||
sessionID: session.id,
|
||||
type: "tool",
|
||||
callID: crypto.randomUUID(),
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: {},
|
||||
output: "old output".repeat(10_000),
|
||||
title: "old",
|
||||
metadata: {},
|
||||
time: { start: Date.now(), end: Date.now() },
|
||||
},
|
||||
})
|
||||
await user(svc, session.id, "latest turn")
|
||||
const keep = await user(svc, session.id, "preserved tail turn")
|
||||
const keepReply = await assistant(svc, session.id, keep.id, tmp.path)
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: keepReply.id,
|
||||
sessionID: session.id,
|
||||
type: "tool",
|
||||
callID: crypto.randomUUID(),
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: {},
|
||||
output: "keep output",
|
||||
title: "keep",
|
||||
metadata: {},
|
||||
time: { start: Date.now(), end: Date.now() },
|
||||
},
|
||||
})
|
||||
await Effect.runPromise(
|
||||
KiloSessionCompaction.create({
|
||||
session: {
|
||||
updateMessage: (msg) => Effect.promise(() => svc.updateMessage(msg)),
|
||||
updatePart: (part) => Effect.promise(() => svc.updatePart(part)),
|
||||
},
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
model: ref,
|
||||
auto: false,
|
||||
return Stream.fail(
|
||||
new APICallError({
|
||||
message: "Request Entity Too Large",
|
||||
url: "https://api.kilo.ai/api/openrouter/responses",
|
||||
requestBodyValues: {},
|
||||
statusCode: 413,
|
||||
responseHeaders: { "content-type": "text/plain" },
|
||||
responseBody: "Request Entity Too Large\n\nFUNCTION_PAYLOAD_TOO_LARGE",
|
||||
isRetryable: false,
|
||||
}),
|
||||
)
|
||||
})
|
||||
stub.push(
|
||||
reply("summary", (input) => {
|
||||
captures.push(JSON.stringify(input.messages))
|
||||
}),
|
||||
)
|
||||
|
||||
try {
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const parent = msgs.at(-1)?.info.id
|
||||
expect(parent).toBeTruthy()
|
||||
const result = await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
parentID: parent!,
|
||||
messages: msgs,
|
||||
sessionID: session.id,
|
||||
auto: false,
|
||||
}),
|
||||
),
|
||||
await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const rt = runtime(stub.layer, AppNodeBuilder.build(Config.node))
|
||||
const svc = service(rt)
|
||||
const session = await svc.create({})
|
||||
const old = await user(svc, session.id, "old image turn")
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: old.id,
|
||||
sessionID: session.id,
|
||||
type: "file",
|
||||
mime: "image/png",
|
||||
filename: "old.png",
|
||||
url: `data:image/png;base64,${"a".repeat(8_000)}`,
|
||||
})
|
||||
const oldReply = await assistant(svc, session.id, old.id, tmp.path)
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: oldReply.id,
|
||||
sessionID: session.id,
|
||||
type: "tool",
|
||||
callID: crypto.randomUUID(),
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: {},
|
||||
output: "old output".repeat(10_000),
|
||||
title: "old",
|
||||
metadata: {},
|
||||
time: { start: Date.now(), end: Date.now() },
|
||||
},
|
||||
})
|
||||
await user(svc, session.id, "latest turn")
|
||||
const keep = await user(svc, session.id, "preserved tail turn")
|
||||
const keepReply = await assistant(svc, session.id, keep.id, tmp.path)
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: keepReply.id,
|
||||
sessionID: session.id,
|
||||
type: "tool",
|
||||
callID: crypto.randomUUID(),
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: {},
|
||||
output: "keep output",
|
||||
title: "keep",
|
||||
metadata: {},
|
||||
time: { start: Date.now(), end: Date.now() },
|
||||
},
|
||||
})
|
||||
await Effect.runPromise(
|
||||
KiloSessionCompaction.create({
|
||||
session: {
|
||||
updateMessage: (msg) => Effect.promise(() => svc.updateMessage(msg)),
|
||||
updatePart: (part) => Effect.promise(() => svc.updatePart(part)),
|
||||
},
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
model: ref,
|
||||
auto: false,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result).toBe("continue")
|
||||
expect(captures).toHaveLength(2)
|
||||
expect(captures[0]).toContain("Attached image/png: old.png")
|
||||
expect(captures[0]).toContain("old output")
|
||||
expect(captures[0]).not.toContain("keep output")
|
||||
expect(captures[1]).not.toContain("data:image/png;base64")
|
||||
expect(captures[1]).not.toContain("old output")
|
||||
expect(captures[1]).not.toContain("keep output")
|
||||
expect(captures[1]).toContain("Attached image/png: old.png")
|
||||
expect(captures[1]).toContain("Old tool result content cleared")
|
||||
const tools = (await svc.messages({ sessionID: session.id }))
|
||||
.flatMap((msg) => msg.parts)
|
||||
.filter((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||
expect(tools).toHaveLength(2)
|
||||
expect(tools[0]?.type).toBe("tool")
|
||||
if (tools[0]?.state.status === "completed") expect(tools[0].state.time.compacted).toBeNumber()
|
||||
expect(tools[1]?.type).toBe("tool")
|
||||
if (tools[1]?.state.status === "completed") expect(tools[1].state.time.compacted).toBeUndefined()
|
||||
} finally {
|
||||
await rt.dispose()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
try {
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const parent = msgs.at(-1)?.info.id
|
||||
expect(parent).toBeTruthy()
|
||||
const result = await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
parentID: parent!,
|
||||
messages: msgs,
|
||||
sessionID: session.id,
|
||||
auto: false,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toBe("continue")
|
||||
expect(captures).toHaveLength(2)
|
||||
expect(captures[0]).toContain("Attached image/png: old.png")
|
||||
expect(captures[0]).toContain("old output")
|
||||
expect(captures[0]).not.toContain("keep output")
|
||||
expect(captures[1]).not.toContain("data:image/png;base64")
|
||||
expect(captures[1]).not.toContain("old output")
|
||||
expect(captures[1]).not.toContain("keep output")
|
||||
expect(captures[1]).toContain("Attached image/png: old.png")
|
||||
expect(captures[1]).toContain("Old tool result content cleared")
|
||||
const tools = (await svc.messages({ sessionID: session.id }))
|
||||
.flatMap((msg) => msg.parts)
|
||||
.filter((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||
expect(tools).toHaveLength(2)
|
||||
expect(tools[0]?.type).toBe("tool")
|
||||
if (tools[0]?.state.status === "completed") expect(tools[0].state.time.compacted).toBeNumber()
|
||||
expect(tools[1]?.type).toBe("tool")
|
||||
if (tools[1]?.state.status === "completed") expect(tools[1].state.time.compacted).toBeUndefined()
|
||||
} finally {
|
||||
await rt.dispose()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
30_000,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
// the same parent assistant message. Without the internal lock, parallel
|
||||
// subagent completions race on read-modify-write and lose deltas (#6321).
|
||||
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { KiloCostPropagation } from "../../src/kilocode/session/cost-propagation"
|
||||
import { Instance } from "../../src/kilocode/instance"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID } from "../../src/session/schema"
|
||||
@@ -31,7 +31,16 @@ const ref = {
|
||||
}
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(AppNodeBuilder.build(Session.node), Bus.layer, AppNodeBuilder.build(Database.node), AppNodeBuilder.build(CrossSpawnSpawner.node)),
|
||||
LayerNode.compile(
|
||||
LayerNode.group([
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
MessageV2.node,
|
||||
Bus.node,
|
||||
Database.node,
|
||||
CrossSpawnSpawner.node,
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
const seed = Effect.fn("CostPropagationTest.seed")(function* () {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// Progress is mirrored to a JSON file so tests can assert what the provider saw
|
||||
// without sharing module state with the plugin that loads this file.
|
||||
|
||||
import { rename } from "node:fs/promises"
|
||||
|
||||
export type StallState = { calls: number; stalls: number; recovered: number }
|
||||
|
||||
const HEAD = { id: "chatcmpl-stall", object: "chat.completion.chunk", created: 0, model: "mock-model" }
|
||||
@@ -84,7 +86,16 @@ function stalling() {
|
||||
|
||||
export function createStallTransport(input: { state: string; answer?: string; command?: string }) {
|
||||
const state: StallState = { calls: 0, stalls: 0, recovered: 0 }
|
||||
const save = () => Bun.write(input.state, JSON.stringify(state))
|
||||
let pending = Promise.resolve()
|
||||
const save = () => {
|
||||
const json = JSON.stringify(state)
|
||||
const tmp = `${input.state}.${crypto.randomUUID()}.tmp`
|
||||
pending = pending.then(async () => {
|
||||
await Bun.write(tmp, json)
|
||||
await rename(tmp, input.state)
|
||||
})
|
||||
return pending
|
||||
}
|
||||
|
||||
return async (_input: unknown, init?: { body?: unknown }) => {
|
||||
const body = typeof init?.body === "string" ? init.body : ""
|
||||
|
||||
@@ -4,12 +4,17 @@ 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 { InstanceBootstrap } from "../../src/project/bootstrap"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { KilocodeWatcher } from "../../src/kilocode/watcher"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { awaitWithTimeout, testEffect } from "../lib/effect"
|
||||
|
||||
const layer = Layer.mergeAll(AppNodeBuilder.build(InstanceStore.node), AppNodeBuilder.build(Git.node), AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
const layer = Layer.mergeAll(
|
||||
AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
|
||||
AppNodeBuilder.build(Git.node),
|
||||
AppNodeBuilder.build(CrossSpawnSpawner.node),
|
||||
)
|
||||
const it = testEffect(layer)
|
||||
|
||||
// The suite disables the file watcher (see test/preload.ts); this file tests it, so opt back in.
|
||||
|
||||
@@ -77,6 +77,7 @@ const realToast = await import("@tui/ui/toast")
|
||||
const realEvent = await import("@tui/context/event")
|
||||
const realRoute = await import("@tui/context/route")
|
||||
const realRuntime = await import("@tui/context/runtime")
|
||||
const realPermission = await import("@tui/context/permission")
|
||||
|
||||
let capturedInit: (() => any) | undefined
|
||||
|
||||
@@ -169,6 +170,11 @@ mock.module("@tui/context/route", () => ({
|
||||
useRoute: () => ({ data: { type: "home" }, navigate: () => {} }),
|
||||
}))
|
||||
|
||||
mock.module("@tui/context/permission", () => ({
|
||||
...realPermission,
|
||||
usePermission: () => ({ mode: "normal", set: () => {}, toggle: () => {} }),
|
||||
}))
|
||||
|
||||
// Import the real Global to get the state path (set by test preload via XDG_STATE_HOME)
|
||||
const { Global } = await import("@opencode-ai/core/global")
|
||||
const modelJsonPath = path.join(Global.Path.state, "model.json")
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Identifier } from "../../src/id/id"
|
||||
import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { Instance } from "../../src/kilocode/instance"
|
||||
import { provideTestInstance } from "../fixture/fixture"
|
||||
import { PlanFollowup } from "../../src/kilocode/plan-followup"
|
||||
@@ -21,7 +23,10 @@ import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const session = makeRuntime(Session.Service, Session.defaultLayer)
|
||||
const session = makeRuntime(
|
||||
Session.Service,
|
||||
LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node])),
|
||||
)
|
||||
const sessions = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
session.runPromise((svc) => svc.create(input)),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||
import { Effect, ManagedRuntime } from "effect"
|
||||
import path from "path"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { PlanFile } from "../../src/kilocode/plan-file"
|
||||
@@ -10,12 +10,15 @@ import { Session } from "../../src/session/session"
|
||||
import { MessageID, PartID } from "../../src/session/schema"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { PlanExitTool } from "../../src/tool/plan"
|
||||
import { Tool } from "../../src/tool/tool"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const rt = ManagedRuntime.make(Layer.mergeAll(AppNodeBuilder.build(Agent.node), AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Truncate.node)))
|
||||
const rt = ManagedRuntime.make(
|
||||
LayerNode.compile(LayerNode.group([Agent.node, Session.node, SessionProjector.node, Truncate.node])),
|
||||
)
|
||||
|
||||
async function init() {
|
||||
return rt.runPromise(
|
||||
|
||||
@@ -11,6 +11,8 @@ import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { formatTodos, generateHandover, PlanFollowup, PlanFollowupRuntime } from "../../src/kilocode/plan-followup"
|
||||
import { Instance } from "../../src/kilocode/instance"
|
||||
import * as KiloInstance from "../../src/kilocode/instance"
|
||||
@@ -67,7 +69,10 @@ const todo = {
|
||||
},
|
||||
}
|
||||
|
||||
const session = makeRuntime(Session.Service, Session.defaultLayer)
|
||||
const session = makeRuntime(
|
||||
Session.Service,
|
||||
LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node])),
|
||||
)
|
||||
const store = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) => session.runPromise((svc) => svc.create(input)),
|
||||
get: (id: SessionID) => session.runPromise((svc) => svc.get(id)),
|
||||
|
||||
@@ -11,7 +11,10 @@ import { awaitWithTimeout, testEffect } from "../../lib/effect"
|
||||
|
||||
const bootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(AppNodeBuilder.build(InstanceStore.node), AppNodeBuilder.build(CrossSpawnSpawner.node)).pipe(Layer.provide(bootstrap)),
|
||||
Layer.mergeAll(
|
||||
AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, bootstrap]]),
|
||||
AppNodeBuilder.build(CrossSpawnSpawner.node),
|
||||
),
|
||||
)
|
||||
|
||||
const register = (disposer: (directory: string) => Promise<void>) =>
|
||||
|
||||
@@ -25,7 +25,7 @@ import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { Option } from "../../src/question"
|
||||
|
||||
const SOURCE = path.resolve(import.meta.dir, "../../src/question/index.ts")
|
||||
const SOURCE = path.resolve(import.meta.dir, "../../../schema/src/v1/question.ts")
|
||||
|
||||
describe("QuestionOption schema — Kilo-specific field contract", () => {
|
||||
test("Option class accepts and round-trips the mode field", () => {
|
||||
@@ -56,14 +56,14 @@ describe("QuestionOption schema — Kilo-specific field contract", () => {
|
||||
// resolution that drops the fields is caught immediately.
|
||||
test("source declares mode as an optional field inside a kilocode_change block", () => {
|
||||
const src = fs.readFileSync(SOURCE, "utf-8")
|
||||
expect(src).toMatch(/kilocode_change start[^\n]*hint to UI clients/)
|
||||
expect(src).toMatch(/kilocode_change start[^\n]*localization and mode selection hints/)
|
||||
expect(src).toMatch(/mode:\s*Schema\.optional\(Schema\.String\)/)
|
||||
expect(src).toMatch(/kilocode_change end/)
|
||||
})
|
||||
|
||||
test("source declares labelKey and descriptionKey inside a kilocode_change block", () => {
|
||||
const src = fs.readFileSync(SOURCE, "utf-8")
|
||||
expect(src).toMatch(/kilocode_change start[^\n]*i18n keys/)
|
||||
expect(src).toMatch(/kilocode_change start[^\n]*localization and mode selection hints/)
|
||||
expect(src).toMatch(/labelKey:\s*Schema\.optional\(Schema\.String\)/)
|
||||
expect(src).toMatch(/descriptionKey:\s*Schema\.optional\(Schema\.String\)/)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { RecallSearch } from "../../src/kilocode/session/recall-search"
|
||||
import { Instance } from "../../src/kilocode/instance"
|
||||
import { Session } from "../../src/session/session"
|
||||
@@ -16,7 +17,7 @@ import { testEffect } from "../lib/effect"
|
||||
|
||||
type Stored<T> = T extends unknown ? Omit<T, "id" | "sessionID" | "messageID"> : never
|
||||
|
||||
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Database.node)))
|
||||
const it = testEffect(LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, Database.node])))
|
||||
|
||||
const add = Effect.fn("RecallSearchTest.add")(function* (
|
||||
sessionID: SessionID,
|
||||
|
||||
@@ -106,9 +106,19 @@ function data(diff = false) {
|
||||
|
||||
async function request(directory: string, body: unknown) {
|
||||
const auth = process.env.KILO_AUTH_CONTENT
|
||||
const fetcher = globalThis.fetch
|
||||
const token = `test-${crypto.randomUUID()}`
|
||||
bodies.set(token, body)
|
||||
process.env.KILO_AUTH_CONTENT = JSON.stringify({ kilo: { type: "api", key: token } })
|
||||
globalThis.fetch = Object.assign(
|
||||
(input: URL | RequestInfo, init?: BunFetchRequestInit | RequestInit) => {
|
||||
const req = new Request(input, init)
|
||||
const url = new URL(req.url)
|
||||
if (url.pathname !== "/api/session/ses_cloud/export") return fetcher(req)
|
||||
return fetcher(new Request(`${ingest.url.origin}${url.pathname}${url.search}`, req))
|
||||
},
|
||||
{ preconnect: fetcher.preconnect },
|
||||
)
|
||||
try {
|
||||
return await HttpApiApp.webHandler().handler(
|
||||
new Request(`http://localhost${KiloGatewayPaths.cloudSessionImport}`, {
|
||||
@@ -120,6 +130,7 @@ async function request(directory: string, body: unknown) {
|
||||
)
|
||||
} finally {
|
||||
bodies.delete(token)
|
||||
globalThis.fetch = fetcher
|
||||
if (auth === undefined) delete process.env.KILO_AUTH_CONTENT
|
||||
else process.env.KILO_AUTH_CONTENT = auth
|
||||
}
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { Cause, Effect, Exit, Fiber } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Bus } from "../../../src/bus"
|
||||
import * as Config from "../../../src/config/config"
|
||||
import { AllowEverythingPermission } from "../../../src/kilocode/permission/allow-everything"
|
||||
import { Permission } from "../../../src/permission"
|
||||
import { EventV2Bridge } from "../../../src/event-v2-bridge"
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { provideTestInstance } from "../../fixture/fixture"
|
||||
import { Server } from "../../../src/server/server"
|
||||
import { Session } from "../../../src/session/session"
|
||||
import { provideTmpdirInstance, tmpdir } from "../../fixture/fixture"
|
||||
import { testEffect } from "../../lib/effect"
|
||||
|
||||
const bus = Bus.layer
|
||||
const env = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
AppNodeBuilder.build(Config.node),
|
||||
AppNodeBuilder.build(Session.node),
|
||||
bus,
|
||||
AppNodeBuilder.build(CrossSpawnSpawner.node),
|
||||
const env = LayerNode.compile(
|
||||
LayerNode.group([
|
||||
Permission.node,
|
||||
Config.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
Bus.node,
|
||||
CrossSpawnSpawner.node,
|
||||
]),
|
||||
)
|
||||
const it = testEffect(env)
|
||||
const original = {
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
// overflows the model context, and that the exhausted turn surfaces as an
|
||||
// error (rather than silently completing).
|
||||
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Deferred, Effect, Layer } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
||||
import { BackgroundJob } from "../../src/background/job"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Command } from "../../src/command"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "../../src/event-v2-bridge"
|
||||
@@ -35,7 +33,6 @@ import { Provider as ProviderSvc } from "../../src/provider/provider"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Question } from "../../src/question"
|
||||
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { SessionCompaction } from "../../src/session/compaction"
|
||||
import { Instruction } from "../../src/session/instruction"
|
||||
@@ -52,8 +49,6 @@ import { SessionSummary } from "../../src/session/summary"
|
||||
import { Todo } from "../../src/session/todo"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import { Storage } from "../../src/storage/storage"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
import { ToolRegistry } from "../../src/tool/registry"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
@@ -133,72 +128,59 @@ const lsp = Layer.succeed(
|
||||
}),
|
||||
)
|
||||
|
||||
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
|
||||
const runState = SessionRunState.layer.pipe(Layer.provide(status))
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
const memoryNode = LayerNode.make({ service: MemoryService.Service, layer: MemoryService.layer, deps: [] })
|
||||
const serverNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
|
||||
const root = LayerNode.group([
|
||||
SessionPrompt.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
MessageV2.node,
|
||||
Snapshot.node,
|
||||
LLM.node,
|
||||
Env.node,
|
||||
AgentSvc.node,
|
||||
Command.node,
|
||||
Permission.node,
|
||||
Plugin.node,
|
||||
Config.node,
|
||||
ProviderSvc.node,
|
||||
LSP.node,
|
||||
MCP.node,
|
||||
FSUtil.node,
|
||||
BackgroundJob.node,
|
||||
SessionStatus.node,
|
||||
SessionRunState.node,
|
||||
Database.node,
|
||||
EventV2Bridge.node,
|
||||
Question.node,
|
||||
Todo.node,
|
||||
ToolRegistry.node,
|
||||
Skill.node,
|
||||
Git.node,
|
||||
Ripgrep.node,
|
||||
Format.node,
|
||||
Truncate.node,
|
||||
SessionProcessor.node,
|
||||
Image.node,
|
||||
SessionCompaction.node,
|
||||
SessionRevert.node,
|
||||
Instruction.node,
|
||||
SystemPrompt.node,
|
||||
CrossSpawnSpawner.node,
|
||||
RuntimeFlags.node,
|
||||
memoryNode,
|
||||
serverNode,
|
||||
])
|
||||
|
||||
function makeHttp() {
|
||||
const deps = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(BackgroundJob.node),
|
||||
AppNodeBuilder.build(Snapshot.node),
|
||||
AppNodeBuilder.build(LLM.node),
|
||||
AppNodeBuilder.build(Env.node),
|
||||
AppNodeBuilder.build(AgentSvc.node),
|
||||
AppNodeBuilder.build(Command.node),
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
plugin,
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(ProviderSvc.node),
|
||||
lsp,
|
||||
mcp,
|
||||
AppNodeBuilder.build(FSUtil.node),
|
||||
SyncEvent.defaultLayer,
|
||||
AppNodeBuilder.build(EventV2Bridge.node),
|
||||
AppNodeBuilder.build(Database.node),
|
||||
status,
|
||||
MemoryService.layer,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const question = Question.layer.pipe(Layer.provideMerge(deps))
|
||||
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
|
||||
const registry = AppNodeBuilder.build(ToolRegistry.node, [[KiloSessions.node, KiloSessions.testLayer]])
|
||||
const trunc = AppNodeBuilder.build(Truncate.node)
|
||||
const proc = AppNodeBuilder.build(SessionProcessor.node, [[SessionSummary.node, summary]])
|
||||
const compact = AppNodeBuilder.build(SessionCompaction.node, [
|
||||
[SessionProcessor.node, proc],
|
||||
return LayerNode.compile(root, [
|
||||
[SessionSummary.node, summary],
|
||||
[Plugin.node, plugin],
|
||||
[LSP.node, lsp],
|
||||
[MCP.node, mcp],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer()],
|
||||
[KiloSessions.node, KiloSessions.testLayer],
|
||||
])
|
||||
return Layer.mergeAll(
|
||||
TestLLMServer.layer,
|
||||
SessionPrompt.layer.pipe(
|
||||
Layer.provide(AppNodeBuilder.build(SessionRevert.node)),
|
||||
Layer.provide(AppNodeBuilder.build(Image.node)),
|
||||
Layer.provide(summary),
|
||||
Layer.provideMerge(runState),
|
||||
Layer.provideMerge(compact),
|
||||
Layer.provideMerge(proc),
|
||||
Layer.provideMerge(registry),
|
||||
Layer.provideMerge(trunc),
|
||||
Layer.provideMerge(question),
|
||||
Layer.provide(AppNodeBuilder.build(Instruction.node)),
|
||||
Layer.provide(AppNodeBuilder.build(SystemPrompt.node)),
|
||||
Layer.provideMerge(deps),
|
||||
),
|
||||
).pipe(
|
||||
Layer.provide(
|
||||
Layer.mergeAll(
|
||||
summary,
|
||||
deps,
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(BackgroundJob.node),
|
||||
Bus.layer,
|
||||
infra,
|
||||
AppNodeBuilder.build(Storage.node),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const it = testEffect(makeHttp())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, mock, test } from "bun:test"
|
||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||
import fs from "fs/promises"
|
||||
@@ -35,6 +35,7 @@ import { ProviderTest } from "../fake/provider"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { remove as cleanup } from "./cleanup"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
@@ -47,6 +48,8 @@ const agents = Layer.mock(Agent.Service)({
|
||||
})
|
||||
const previous = Flag.KILO_DB
|
||||
const dbfile = path.join(os.tmpdir(), `kilo-compaction-chunks-${process.pid}-${crypto.randomUUID()}.db`)
|
||||
const layer = LayerNode.compile(LayerNode.group([SessionNs.node, SessionProjector.node]))
|
||||
const runtime = makeRuntime(SessionNs.Service, layer)
|
||||
|
||||
beforeAll(async () => {
|
||||
await fs.rm(dbfile, { force: true })
|
||||
@@ -54,16 +57,13 @@ beforeAll(async () => {
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await runtime.dispose()
|
||||
await AppRuntime.dispose()
|
||||
await disposeTestRuntime()
|
||||
Flag.KILO_DB = previous
|
||||
await Promise.all([dbfile, `${dbfile}-wal`, `${dbfile}-shm`].map(cleanup))
|
||||
})
|
||||
|
||||
function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
|
||||
return Effect.runPromise(fx.pipe(Effect.provide(AppNodeBuilder.build(SessionNs.node))))
|
||||
}
|
||||
|
||||
const store = {
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) => Effect.promise(() => svc.updateMessage(msg)),
|
||||
updatePart: <T extends MessageV2.Part>(part: T) => Effect.promise(() => svc.updatePart(part)),
|
||||
@@ -71,16 +71,16 @@ const store = {
|
||||
|
||||
const svc = {
|
||||
create(input?: SessionNs.CreateInput) {
|
||||
return run(SessionNs.Service.use((svc) => svc.create(input)))
|
||||
return runtime.runPromise((svc) => svc.create(input))
|
||||
},
|
||||
messages(input: Parameters<SessionNs.Interface["messages"]>[0]) {
|
||||
return run(SessionNs.Service.use((svc) => svc.messages(input)))
|
||||
return runtime.runPromise((svc) => svc.messages(input))
|
||||
},
|
||||
updateMessage<T extends MessageV2.Info>(msg: T) {
|
||||
return run(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
|
||||
return runtime.runPromise((svc) => svc.updateMessage(msg))
|
||||
},
|
||||
updatePart<T extends MessageV2.Part>(part: T) {
|
||||
return run(SessionNs.Service.use((svc) => svc.updatePart(part)))
|
||||
return runtime.runPromise((svc) => svc.updatePart(part))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -224,13 +224,18 @@ function fakeRuntime(outputTokenMax?: number, error?: MessageV2.Assistant["error
|
||||
})
|
||||
}),
|
||||
)
|
||||
const processorNode = LayerNode.make({
|
||||
service: SessionProcessorModule.SessionProcessor.Service,
|
||||
layer: processor,
|
||||
deps: [SessionNs.node],
|
||||
})
|
||||
const model = ProviderTest.model({ providerID, id: modelID, limit: { context: 10_000, output: 1_000 } })
|
||||
return {
|
||||
calls,
|
||||
outputs,
|
||||
rt: ManagedRuntime.make(
|
||||
AppNodeBuilder.build(LayerNode.group([SessionCompaction.node, Bus.node]), [
|
||||
[SessionProcessorModule.SessionProcessor.node, processor],
|
||||
LayerNode.compile(LayerNode.group([SessionCompaction.node, SessionNs.node, SessionProjector.node, Bus.node]), [
|
||||
[SessionProcessorModule.SessionProcessor.node, processorNode],
|
||||
[Provider.node, ProviderTest.fake({ model }).layer],
|
||||
[Agent.node, agents],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer({ outputTokenMax })],
|
||||
@@ -238,6 +243,7 @@ function fakeRuntime(outputTokenMax?: number, error?: MessageV2.Assistant["error
|
||||
Config.node,
|
||||
Layer.mock(Config.Service)({
|
||||
get: () => Effect.succeed({ ...{}, compaction: { reserved: 1_000 } }),
|
||||
directories: () => Effect.succeed([]),
|
||||
}),
|
||||
],
|
||||
]),
|
||||
@@ -290,8 +296,15 @@ async function failure(error?: MessageV2.Assistant["error"], empty = false) {
|
||||
function liveRuntime(layer: Layer.Layer<LLM.Service>, context = 10_000) {
|
||||
const model = ProviderTest.model({ providerID, id: modelID, limit: { context, output: 1_000 } })
|
||||
return ManagedRuntime.make(
|
||||
AppNodeBuilder.build(
|
||||
LayerNode.group([SessionCompaction.node, SessionProcessorModule.SessionProcessor.node, Bus.node, SessionStatus.node]),
|
||||
LayerNode.compile(
|
||||
LayerNode.group([
|
||||
SessionCompaction.node,
|
||||
SessionProcessorModule.SessionProcessor.node,
|
||||
SessionNs.node,
|
||||
SessionProjector.node,
|
||||
Bus.node,
|
||||
SessionStatus.node,
|
||||
]),
|
||||
[
|
||||
[SessionSummary.node, summary],
|
||||
[Provider.node, ProviderTest.fake({ model }).layer],
|
||||
@@ -301,6 +314,7 @@ function liveRuntime(layer: Layer.Layer<LLM.Service>, context = 10_000) {
|
||||
Config.node,
|
||||
Layer.mock(Config.Service)({
|
||||
get: () => Effect.succeed({ ...{}, compaction: { reserved: 1_000 } }),
|
||||
directories: () => Effect.succeed([]),
|
||||
}),
|
||||
],
|
||||
],
|
||||
@@ -698,82 +712,85 @@ describe("KiloCompactionChunks", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test(
|
||||
"compaction must not leak maxOutputTokens into agent options",
|
||||
async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
const first = await user(session.id, "first " + "a".repeat(20_000))
|
||||
await assistant(session.id, first.id, tmp.path, "reply " + "b".repeat(20_000))
|
||||
const second = await user(session.id, "second " + "c".repeat(20_000))
|
||||
await assistant(session.id, second.id, tmp.path, "reply " + "d".repeat(20_000))
|
||||
await Effect.runPromise(
|
||||
KiloSessionCompaction.create({
|
||||
session: store,
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
model: ref,
|
||||
auto: false,
|
||||
}),
|
||||
)
|
||||
test("compaction must not leak maxOutputTokens into agent options", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
const first = await user(session.id, "first " + "a".repeat(20_000))
|
||||
await assistant(session.id, first.id, tmp.path, "reply " + "b".repeat(20_000))
|
||||
const second = await user(session.id, "second " + "c".repeat(20_000))
|
||||
await assistant(session.id, second.id, tmp.path, "reply " + "d".repeat(20_000))
|
||||
await Effect.runPromise(
|
||||
KiloSessionCompaction.create({
|
||||
session: store,
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
model: ref,
|
||||
auto: false,
|
||||
}),
|
||||
)
|
||||
|
||||
const captured: Array<{ opts: Record<string, unknown>; modelLimitOutput: number }> = []
|
||||
const bus = Bus.layer
|
||||
const processor = Layer.effect(
|
||||
SessionProcessorModule.SessionProcessor.Service,
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* SessionNs.Service
|
||||
return SessionProcessorModule.SessionProcessor.Service.of({
|
||||
create: Effect.fn("TestSessionProcessorLeak.create")((input) =>
|
||||
Effect.succeed({
|
||||
get message() {
|
||||
return input.assistantMessage
|
||||
},
|
||||
updateToolCall: Effect.fn("TestSessionProcessorLeak.updateToolCall")(() =>
|
||||
Effect.succeed(undefined),
|
||||
),
|
||||
metadata: Effect.fn("TestSessionProcessorLeak.metadata")(() => Effect.void),
|
||||
completeToolCall: Effect.fn("TestSessionProcessorLeak.completeToolCall")(() => Effect.void),
|
||||
process: Effect.fn("TestSessionProcessorLeak.process")((stream: LLM.StreamInput) =>
|
||||
Effect.gen(function* () {
|
||||
captured.push({
|
||||
opts: stream.agent.options as Record<string, unknown>,
|
||||
modelLimitOutput: stream.model.limit.output,
|
||||
})
|
||||
const text = stream.messages.some((msg) =>
|
||||
JSON.stringify(msg).includes("Create a new anchored summary"),
|
||||
)
|
||||
? "final summary"
|
||||
: "chunk summary"
|
||||
yield* sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: input.assistantMessage.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
text,
|
||||
})
|
||||
input.assistantMessage.finish = "stop"
|
||||
return "continue" as const
|
||||
}),
|
||||
),
|
||||
} satisfies SessionProcessor.Handle),
|
||||
),
|
||||
})
|
||||
}),
|
||||
)
|
||||
const captured: Array<{ opts: Record<string, unknown>; modelLimitOutput: number }> = []
|
||||
const bus = Bus.layer
|
||||
const processor = Layer.effect(
|
||||
SessionProcessorModule.SessionProcessor.Service,
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* SessionNs.Service
|
||||
return SessionProcessorModule.SessionProcessor.Service.of({
|
||||
create: Effect.fn("TestSessionProcessorLeak.create")((input) =>
|
||||
Effect.succeed({
|
||||
get message() {
|
||||
return input.assistantMessage
|
||||
},
|
||||
updateToolCall: Effect.fn("TestSessionProcessorLeak.updateToolCall")(() => Effect.succeed(undefined)),
|
||||
metadata: Effect.fn("TestSessionProcessorLeak.metadata")(() => Effect.void),
|
||||
completeToolCall: Effect.fn("TestSessionProcessorLeak.completeToolCall")(() => Effect.void),
|
||||
process: Effect.fn("TestSessionProcessorLeak.process")((stream: LLM.StreamInput) =>
|
||||
Effect.gen(function* () {
|
||||
captured.push({
|
||||
opts: stream.agent.options as Record<string, unknown>,
|
||||
modelLimitOutput: stream.model.limit.output,
|
||||
})
|
||||
const text = stream.messages.some((msg) =>
|
||||
JSON.stringify(msg).includes("Create a new anchored summary"),
|
||||
)
|
||||
? "final summary"
|
||||
: "chunk summary"
|
||||
yield* sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: input.assistantMessage.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
text,
|
||||
})
|
||||
input.assistantMessage.finish = "stop"
|
||||
return "continue" as const
|
||||
}),
|
||||
),
|
||||
} satisfies SessionProcessor.Handle),
|
||||
),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
const model = ProviderTest.model({
|
||||
providerID,
|
||||
id: modelID,
|
||||
limit: { context: 10_000, output: 1_000 },
|
||||
})
|
||||
const outputTokenMax = 512
|
||||
const rt = ManagedRuntime.make(
|
||||
AppNodeBuilder.build(LayerNode.group([SessionCompaction.node, Bus.node]), [
|
||||
[SessionProcessorModule.SessionProcessor.node, processor],
|
||||
const model = ProviderTest.model({
|
||||
providerID,
|
||||
id: modelID,
|
||||
limit: { context: 10_000, output: 1_000 },
|
||||
})
|
||||
const processorNode = LayerNode.make({
|
||||
service: SessionProcessorModule.SessionProcessor.Service,
|
||||
layer: processor,
|
||||
deps: [SessionNs.node],
|
||||
})
|
||||
const outputTokenMax = 512
|
||||
const rt = ManagedRuntime.make(
|
||||
LayerNode.compile(
|
||||
LayerNode.group([SessionCompaction.node, SessionNs.node, SessionProjector.node, Bus.node]),
|
||||
[
|
||||
[SessionProcessorModule.SessionProcessor.node, processorNode],
|
||||
[Provider.node, ProviderTest.fake({ model }).layer],
|
||||
[Agent.node, agents],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer({ outputTokenMax })],
|
||||
@@ -781,49 +798,49 @@ describe("KiloCompactionChunks", () => {
|
||||
Config.node,
|
||||
Layer.mock(Config.Service)({
|
||||
get: () => Effect.succeed({ ...{}, compaction: { reserved: 1_000 } }),
|
||||
directories: () => Effect.succeed([]),
|
||||
}),
|
||||
],
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
try {
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const parent = msgs.at(-1)?.info.id
|
||||
expect(parent).toBeTruthy()
|
||||
const result = await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
parentID: parent!,
|
||||
messages: msgs,
|
||||
sessionID: session.id,
|
||||
auto: false,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
try {
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const parent = msgs.at(-1)?.info.id
|
||||
expect(parent).toBeTruthy()
|
||||
const result = await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
parentID: parent!,
|
||||
messages: msgs,
|
||||
sessionID: session.id,
|
||||
auto: false,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toBe("continue")
|
||||
expect(captured.length).toBeGreaterThan(0)
|
||||
// Negative assertion (the bug surfacing):
|
||||
// maxOutputTokens must not appear in agent.options that the
|
||||
// worker hands to the LLM. Today a strict OpenAI-compatible
|
||||
// upstream rejects that field with
|
||||
// Unsupported parameter(s): maxOutputTokens`.
|
||||
for (const c of captured) {
|
||||
expect(c.opts.maxOutputTokens).toBeUndefined()
|
||||
}
|
||||
// Positive assertion (budget preserved through an independent path):
|
||||
// the constrained model still threads a tightened output limit
|
||||
// through to every worker. If a future "fix" accidentally severs
|
||||
// the only budget source along with the leak, this fails.
|
||||
for (const c of captured) {
|
||||
expect(c.modelLimitOutput).toBeLessThanOrEqual(outputTokenMax)
|
||||
}
|
||||
} finally {
|
||||
await rt.dispose()
|
||||
expect(result).toBe("continue")
|
||||
expect(captured.length).toBeGreaterThan(0)
|
||||
// Negative assertion (the bug surfacing):
|
||||
// maxOutputTokens must not appear in agent.options that the
|
||||
// worker hands to the LLM. Today a strict OpenAI-compatible
|
||||
// upstream rejects that field with
|
||||
// Unsupported parameter(s): maxOutputTokens`.
|
||||
for (const c of captured) {
|
||||
expect(c.opts.maxOutputTokens).toBeUndefined()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
30_000,
|
||||
)
|
||||
// Positive assertion (budget preserved through an independent path):
|
||||
// the constrained model still threads a tightened output limit
|
||||
// through to every worker. If a future "fix" accidentally severs
|
||||
// the only budget source along with the leak, this fails.
|
||||
for (const c of captured) {
|
||||
expect(c.modelLimitOutput).toBeLessThanOrEqual(outputTokenMax)
|
||||
}
|
||||
} finally {
|
||||
await rt.dispose()
|
||||
}
|
||||
},
|
||||
})
|
||||
}, 30_000)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpRouter } from "effect/unstable/http"
|
||||
@@ -23,12 +25,15 @@ import path from "path"
|
||||
import os from "os"
|
||||
import fs from "fs/promises"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { remove as cleanup } from "./cleanup"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const previous = Flag.KILO_DB
|
||||
const dbfile = path.join(os.tmpdir(), `kilo-fork-${process.pid}-${crypto.randomUUID()}.db`)
|
||||
const layer = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node]))
|
||||
const runtime = makeRuntime(Session.Service, layer)
|
||||
|
||||
beforeAll(async () => {
|
||||
await fs.rm(dbfile, { force: true })
|
||||
@@ -36,6 +41,7 @@ beforeAll(async () => {
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await runtime.dispose()
|
||||
await AppRuntime.dispose()
|
||||
await disposeTestRuntime()
|
||||
Flag.KILO_DB = previous
|
||||
@@ -44,14 +50,14 @@ afterAll(async () => {
|
||||
|
||||
const sessions = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
|
||||
list: () => Effect.runPromise(Session.Service.use((svc) => svc.list()).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
|
||||
runtime.runPromise((svc) => svc.create(input)),
|
||||
list: () => runtime.runPromise((svc) => svc.list()),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
|
||||
runtime.runPromise((svc) => svc.messages(input)),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
|
||||
runtime.runPromise((svc) => svc.updateMessage(msg)),
|
||||
updatePart: <T extends MessageV2.Part>(part: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
|
||||
runtime.runPromise((svc) => svc.updatePart(part)),
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import path from "path"
|
||||
import { seedProject } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -15,7 +16,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
Log.init({ print: false })
|
||||
const layer = Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Database.node))
|
||||
const layer = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, Database.node]))
|
||||
const it = testEffect(layer)
|
||||
|
||||
describe("Kilo Session.list", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { ModelUsage } from "@/kilocode/session/model-usage"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
@@ -16,7 +17,7 @@ import { eq } from "drizzle-orm"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Database.node)))
|
||||
const it = testEffect(LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, Database.node])))
|
||||
|
||||
const ref = (providerID: string, modelID: string) => ({
|
||||
providerID: ProviderV2.ID.make(providerID),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
@@ -49,6 +50,8 @@ class TestLLM extends Context.Service<
|
||||
}
|
||||
>()("@test/EmptyToolCallsLLM") {}
|
||||
|
||||
class State extends Context.Service<State, { readonly queue: Script[] }>()("@test/EmptyToolCallsState") {}
|
||||
|
||||
function model(selection = ref): Provider.Model {
|
||||
return {
|
||||
id: selection.modelID,
|
||||
@@ -77,48 +80,58 @@ function usage() {
|
||||
}
|
||||
}
|
||||
|
||||
const llm = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const queue: Script[] = []
|
||||
const push = (item: Script) => {
|
||||
queue.push(item)
|
||||
return Effect.void
|
||||
}
|
||||
const reply = (...items: Event[]) => push(Stream.make(...items))
|
||||
return Layer.mergeAll(
|
||||
Layer.succeed(
|
||||
LLM.Service,
|
||||
LLM.Service.of({
|
||||
stream: () => {
|
||||
const item = queue.shift() ?? Stream.empty
|
||||
return item
|
||||
},
|
||||
}),
|
||||
),
|
||||
Layer.succeed(TestLLM, TestLLM.of({ reply, script: push })),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
const deps = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(Snapshot.node),
|
||||
AppNodeBuilder.build(AgentSvc.node),
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
AppNodeBuilder.build(Plugin.node),
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(SessionSummary.node),
|
||||
AppNodeBuilder.build(Image.node),
|
||||
SyncEvent.defaultLayer,
|
||||
AppNodeBuilder.build(EventV2Bridge.node),
|
||||
AppNodeBuilder.build(Database.node),
|
||||
status,
|
||||
llm,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const env = AppNodeBuilder.build(SessionProcessor.node).pipe(Layer.provideMerge(deps))
|
||||
const stateNode = LayerNode.make({
|
||||
service: State,
|
||||
layer: Layer.sync(State, () => State.of({ queue: [] })),
|
||||
deps: [],
|
||||
})
|
||||
const llmNode = LayerNode.make({
|
||||
service: LLM.Service,
|
||||
layer: Layer.effect(
|
||||
LLM.Service,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* State
|
||||
return LLM.Service.of({ stream: () => state.queue.shift() ?? Stream.empty })
|
||||
}),
|
||||
),
|
||||
deps: [stateNode],
|
||||
})
|
||||
const testNode = LayerNode.make({
|
||||
service: TestLLM,
|
||||
layer: Layer.effect(
|
||||
TestLLM,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* State
|
||||
const push = (item: Script) => Effect.sync(() => state.queue.push(item)).pipe(Effect.asVoid)
|
||||
return TestLLM.of({ reply: (...items) => push(Stream.make(...items)), script: push })
|
||||
}),
|
||||
),
|
||||
deps: [stateNode],
|
||||
})
|
||||
const root = LayerNode.group([
|
||||
SessionProcessor.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
MessageV2.node,
|
||||
Snapshot.node,
|
||||
AgentSvc.node,
|
||||
Permission.node,
|
||||
Plugin.node,
|
||||
Config.node,
|
||||
SessionSummary.node,
|
||||
Image.node,
|
||||
SessionStatus.node,
|
||||
EventV2Bridge.node,
|
||||
Database.node,
|
||||
CrossSpawnSpawner.node,
|
||||
RuntimeFlags.node,
|
||||
LLM.node,
|
||||
testNode,
|
||||
])
|
||||
const env = LayerNode.compile(root, [
|
||||
[LLM.node, llmNode],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer()],
|
||||
]).pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, Bus.layer, SyncEvent.defaultLayer)))
|
||||
|
||||
const it = testEffect(env)
|
||||
|
||||
|
||||
+79
-58
@@ -1,4 +1,5 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { describe, expect, spyOn } from "bun:test"
|
||||
import { APICallError } from "ai"
|
||||
@@ -53,6 +54,10 @@ class TestLLM extends Context.Service<
|
||||
}
|
||||
>()("@test/IncompleteResponseRetryLLM") {}
|
||||
|
||||
class State extends Context.Service<State, { readonly queue: Script[]; calls: number }>()(
|
||||
"@test/IncompleteResponseRetryState",
|
||||
) {}
|
||||
|
||||
function model(): Provider.Model {
|
||||
return {
|
||||
id: ref.modelID,
|
||||
@@ -108,63 +113,72 @@ function retryable429() {
|
||||
})
|
||||
}
|
||||
|
||||
const llm = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const queue: Script[] = []
|
||||
let calls = 0
|
||||
const push = (stream: Script) => {
|
||||
queue.push(stream)
|
||||
return Effect.void
|
||||
}
|
||||
return Layer.mergeAll(
|
||||
Layer.succeed(
|
||||
LLM.Service,
|
||||
LLM.Service.of({
|
||||
stream: () => {
|
||||
calls += 1
|
||||
return queue.shift() ?? Stream.fail(new Error("unexpected extra llm call"))
|
||||
},
|
||||
}),
|
||||
),
|
||||
Layer.succeed(
|
||||
TestLLM,
|
||||
TestLLM.of({
|
||||
push,
|
||||
reply: (...events) => push(Stream.make(...events)),
|
||||
calls: Effect.sync(() => calls),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const reference = Layer.mock(Reference.Service, {
|
||||
list: () => Effect.succeed([]),
|
||||
})
|
||||
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
const stateNode = LayerNode.make({
|
||||
service: State,
|
||||
layer: Layer.sync(State, () => State.of({ queue: [], calls: 0 })),
|
||||
deps: [],
|
||||
})
|
||||
const llmNode = LayerNode.make({
|
||||
service: LLM.Service,
|
||||
layer: Layer.effect(
|
||||
LLM.Service,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* State
|
||||
return LLM.Service.of({
|
||||
stream: () => {
|
||||
state.calls += 1
|
||||
return state.queue.shift() ?? Stream.fail(new Error("unexpected extra llm call"))
|
||||
},
|
||||
})
|
||||
}),
|
||||
),
|
||||
deps: [stateNode],
|
||||
})
|
||||
const testNode = LayerNode.make({
|
||||
service: TestLLM,
|
||||
layer: Layer.effect(
|
||||
TestLLM,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* State
|
||||
const push = (stream: Script) => Effect.sync(() => state.queue.push(stream)).pipe(Effect.asVoid)
|
||||
return TestLLM.of({
|
||||
push,
|
||||
reply: (...events) => push(Stream.make(...events)),
|
||||
calls: Effect.sync(() => state.calls),
|
||||
})
|
||||
}),
|
||||
),
|
||||
deps: [stateNode],
|
||||
})
|
||||
const root = LayerNode.group([
|
||||
SessionProcessor.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
MessageV2.node,
|
||||
Snapshot.node,
|
||||
AgentSvc.node,
|
||||
Permission.node,
|
||||
Plugin.node,
|
||||
Config.node,
|
||||
SessionSummary.node,
|
||||
Image.node,
|
||||
SessionStatus.node,
|
||||
EventV2Bridge.node,
|
||||
Database.node,
|
||||
CrossSpawnSpawner.node,
|
||||
RuntimeFlags.node,
|
||||
LLM.node,
|
||||
testNode,
|
||||
])
|
||||
const env = (event = false) =>
|
||||
AppNodeBuilder.build(SessionProcessor.node).pipe(
|
||||
Layer.provideMerge(
|
||||
Layer.mergeAll(
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(Snapshot.node),
|
||||
AppNodeBuilder.build(AgentSvc.node),
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
AppNodeBuilder.build(Plugin.node),
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer({ experimentalEventSystem: event }),
|
||||
reference,
|
||||
AppNodeBuilder.build(SessionSummary.node),
|
||||
AppNodeBuilder.build(Image.node),
|
||||
SyncEvent.defaultLayer,
|
||||
AppNodeBuilder.build(EventV2Bridge.node),
|
||||
AppNodeBuilder.build(Database.node),
|
||||
status,
|
||||
llm,
|
||||
).pipe(Layer.provideMerge(infra)),
|
||||
),
|
||||
Layer.provide(reference),
|
||||
LayerNode.compile(root, [
|
||||
[LLM.node, llmNode],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: event })],
|
||||
]).pipe(
|
||||
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, Bus.layer, SyncEvent.defaultLayer, reference)),
|
||||
)
|
||||
|
||||
const it = testEffect(env())
|
||||
@@ -560,16 +574,23 @@ describe("session processor incomplete response retry", () => {
|
||||
),
|
||||
)
|
||||
|
||||
eventIt.effect("does not retry when Event V2 mirroring is enabled", () =>
|
||||
eventIt.effect("retries an empty response the same way when the event system flag is on", () =>
|
||||
provideTmpdirProject(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
const ctx = yield* setup(dir)
|
||||
yield* ctx.test.reply(...empty())
|
||||
yield* ctx.test.reply(...success())
|
||||
const delay = spyOn(SessionRetry, "delay").mockReturnValue(0)
|
||||
|
||||
expect(yield* ctx.handle.process(ctx.input)).toBe("continue")
|
||||
expect(yield* ctx.test.calls).toBe(1)
|
||||
expect(ctx.handle.message.finish).toBe("unknown")
|
||||
try {
|
||||
expect(yield* ctx.handle.process(ctx.input)).toBe("continue")
|
||||
} finally {
|
||||
delay.mockRestore()
|
||||
}
|
||||
|
||||
expect(yield* ctx.test.calls).toBe(2)
|
||||
expect(ctx.handle.message.finish).toBe("stop")
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { describe, expect, spyOn } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
@@ -48,6 +49,8 @@ class TestLLM extends Context.Service<
|
||||
}
|
||||
>()("@test/OfflineLLM") {}
|
||||
|
||||
class State extends Context.Service<State, { readonly queue: Script[] }>()("@test/OfflineLLMState") {}
|
||||
|
||||
function model(): Provider.Model {
|
||||
return {
|
||||
id: "test-model",
|
||||
@@ -76,47 +79,59 @@ function usage() {
|
||||
}
|
||||
}
|
||||
|
||||
const llm = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const queue: Script[] = []
|
||||
const push = (item: Script) => {
|
||||
queue.push(item)
|
||||
return Effect.void
|
||||
}
|
||||
return Layer.mergeAll(
|
||||
Layer.succeed(
|
||||
LLM.Service,
|
||||
LLM.Service.of({
|
||||
stream: () => {
|
||||
const item = queue.shift() ?? Stream.empty
|
||||
return item
|
||||
},
|
||||
}),
|
||||
),
|
||||
Layer.succeed(TestLLM, TestLLM.of({ push })),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
const deps = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(Snapshot.node),
|
||||
AppNodeBuilder.build(AgentSvc.node),
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
AppNodeBuilder.build(Plugin.node),
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(SessionSummary.node),
|
||||
AppNodeBuilder.build(Image.node),
|
||||
SyncEvent.defaultLayer,
|
||||
AppNodeBuilder.build(EventV2Bridge.node),
|
||||
AppNodeBuilder.build(Database.node),
|
||||
status,
|
||||
llm,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const env = AppNodeBuilder.build(SessionProcessor.node).pipe(Layer.provideMerge(deps))
|
||||
const stateNode = LayerNode.make({
|
||||
service: State,
|
||||
layer: Layer.sync(State, () => State.of({ queue: [] })),
|
||||
deps: [],
|
||||
})
|
||||
const llmNode = LayerNode.make({
|
||||
service: LLM.Service,
|
||||
layer: Layer.effect(
|
||||
LLM.Service,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* State
|
||||
return LLM.Service.of({ stream: () => state.queue.shift() ?? Stream.empty })
|
||||
}),
|
||||
),
|
||||
deps: [stateNode],
|
||||
})
|
||||
const testNode = LayerNode.make({
|
||||
service: TestLLM,
|
||||
layer: Layer.effect(
|
||||
TestLLM,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* State
|
||||
return TestLLM.of({
|
||||
push: (item) => Effect.sync(() => state.queue.push(item)).pipe(Effect.asVoid),
|
||||
})
|
||||
}),
|
||||
),
|
||||
deps: [stateNode],
|
||||
})
|
||||
const root = LayerNode.group([
|
||||
SessionProcessor.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
MessageV2.node,
|
||||
Snapshot.node,
|
||||
AgentSvc.node,
|
||||
Permission.node,
|
||||
Plugin.node,
|
||||
Config.node,
|
||||
SessionSummary.node,
|
||||
Image.node,
|
||||
SessionStatus.node,
|
||||
EventV2Bridge.node,
|
||||
Database.node,
|
||||
CrossSpawnSpawner.node,
|
||||
RuntimeFlags.node,
|
||||
LLM.node,
|
||||
testNode,
|
||||
])
|
||||
const env = LayerNode.compile(root, [
|
||||
[LLM.node, llmNode],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer()],
|
||||
]).pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, Bus.layer, SyncEvent.defaultLayer)))
|
||||
|
||||
const it = testEffect(env)
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// transitively load flag.ts to ensure the env is captured at load time.
|
||||
process.env.KILO_SESSION_RETRY_LIMIT = "2"
|
||||
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { afterEach, describe, expect, spyOn } from "bun:test"
|
||||
import { APICallError } from "ai"
|
||||
@@ -56,6 +57,8 @@ class TestLLM extends Context.Service<
|
||||
}
|
||||
>()("@test/RetryLimitLLM") {}
|
||||
|
||||
class State extends Context.Service<State, { readonly queue: Script[]; calls: number }>()("@test/RetryLimitLLMState") {}
|
||||
|
||||
function model(): Provider.Model {
|
||||
return {
|
||||
id: "test-model",
|
||||
@@ -87,49 +90,65 @@ function retryable429() {
|
||||
})
|
||||
}
|
||||
|
||||
const llm = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const queue: Script[] = []
|
||||
let calls = 0
|
||||
const push = (item: Script) => {
|
||||
queue.push(item)
|
||||
return Effect.void
|
||||
}
|
||||
return Layer.mergeAll(
|
||||
Layer.succeed(
|
||||
LLM.Service,
|
||||
LLM.Service.of({
|
||||
stream: () => {
|
||||
calls += 1
|
||||
const item = queue.shift() ?? Stream.fail(new Error("unexpected extra llm call"))
|
||||
return item
|
||||
},
|
||||
}),
|
||||
),
|
||||
Layer.succeed(TestLLM, TestLLM.of({ push, calls: Effect.sync(() => calls) })),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
const deps = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(Snapshot.node),
|
||||
AppNodeBuilder.build(AgentSvc.node),
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
AppNodeBuilder.build(Plugin.node),
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(SessionSummary.node),
|
||||
AppNodeBuilder.build(Image.node),
|
||||
SyncEvent.defaultLayer,
|
||||
AppNodeBuilder.build(EventV2Bridge.node),
|
||||
AppNodeBuilder.build(Database.node),
|
||||
status,
|
||||
llm,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const env = AppNodeBuilder.build(SessionProcessor.node).pipe(Layer.provideMerge(deps))
|
||||
const stateNode = LayerNode.make({
|
||||
service: State,
|
||||
layer: Layer.sync(State, () => State.of({ queue: [], calls: 0 })),
|
||||
deps: [],
|
||||
})
|
||||
const llmNode = LayerNode.make({
|
||||
service: LLM.Service,
|
||||
layer: Layer.effect(
|
||||
LLM.Service,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* State
|
||||
return LLM.Service.of({
|
||||
stream: () => {
|
||||
state.calls += 1
|
||||
return state.queue.shift() ?? Stream.fail(new Error("unexpected extra llm call"))
|
||||
},
|
||||
})
|
||||
}),
|
||||
),
|
||||
deps: [stateNode],
|
||||
})
|
||||
const testNode = LayerNode.make({
|
||||
service: TestLLM,
|
||||
layer: Layer.effect(
|
||||
TestLLM,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* State
|
||||
return TestLLM.of({
|
||||
push: (item) => Effect.sync(() => state.queue.push(item)).pipe(Effect.asVoid),
|
||||
calls: Effect.sync(() => state.calls),
|
||||
})
|
||||
}),
|
||||
),
|
||||
deps: [stateNode],
|
||||
})
|
||||
const root = LayerNode.group([
|
||||
SessionProcessor.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
MessageV2.node,
|
||||
Snapshot.node,
|
||||
AgentSvc.node,
|
||||
Permission.node,
|
||||
Plugin.node,
|
||||
Config.node,
|
||||
SessionSummary.node,
|
||||
Image.node,
|
||||
SessionStatus.node,
|
||||
EventV2Bridge.node,
|
||||
Database.node,
|
||||
CrossSpawnSpawner.node,
|
||||
RuntimeFlags.node,
|
||||
LLM.node,
|
||||
testNode,
|
||||
])
|
||||
const env = LayerNode.compile(root, [
|
||||
[LLM.node, llmNode],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer()],
|
||||
]).pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, Bus.layer, SyncEvent.defaultLayer)))
|
||||
|
||||
const it = testEffect(env)
|
||||
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
// Ensures Kilo's post-filterCompacted trim and post-summary media strip are
|
||||
// applied before messages are serialized for the provider request.
|
||||
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
||||
import { BackgroundJob } from "../../src/background/job"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Command } from "../../src/command"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "../../src/event-v2-bridge"
|
||||
@@ -31,7 +29,6 @@ import { Provider as ProviderSvc } from "../../src/provider/provider"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Question } from "../../src/question"
|
||||
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { SessionCompaction } from "../../src/session/compaction"
|
||||
import { Instruction } from "../../src/session/instruction"
|
||||
@@ -48,8 +45,6 @@ import { SessionSummary } from "../../src/session/summary"
|
||||
import { Todo } from "../../src/session/todo"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import { Storage } from "../../src/storage/storage"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
import { ToolRegistry } from "../../src/tool/registry"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import { KiloSessions } from "../../src/kilo-sessions/kilo-sessions"
|
||||
@@ -126,72 +121,59 @@ const lsp = Layer.succeed(
|
||||
}),
|
||||
)
|
||||
|
||||
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
|
||||
const run = SessionRunState.layer.pipe(Layer.provide(status))
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
const memoryNode = LayerNode.make({ service: MemoryService.Service, layer: MemoryService.layer, deps: [] })
|
||||
const serverNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
|
||||
const root = LayerNode.group([
|
||||
SessionPrompt.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
MessageV2.node,
|
||||
Snapshot.node,
|
||||
LLM.node,
|
||||
Env.node,
|
||||
AgentSvc.node,
|
||||
Command.node,
|
||||
Permission.node,
|
||||
Plugin.node,
|
||||
Config.node,
|
||||
ProviderSvc.node,
|
||||
LSP.node,
|
||||
MCP.node,
|
||||
FSUtil.node,
|
||||
BackgroundJob.node,
|
||||
SessionStatus.node,
|
||||
SessionRunState.node,
|
||||
Database.node,
|
||||
EventV2Bridge.node,
|
||||
Question.node,
|
||||
Todo.node,
|
||||
ToolRegistry.node,
|
||||
Skill.node,
|
||||
Git.node,
|
||||
Ripgrep.node,
|
||||
Format.node,
|
||||
Truncate.node,
|
||||
SessionProcessor.node,
|
||||
Image.node,
|
||||
SessionCompaction.node,
|
||||
SessionRevert.node,
|
||||
Instruction.node,
|
||||
SystemPrompt.node,
|
||||
CrossSpawnSpawner.node,
|
||||
RuntimeFlags.node,
|
||||
memoryNode,
|
||||
serverNode,
|
||||
])
|
||||
|
||||
function makeHttp() {
|
||||
const deps = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(BackgroundJob.node),
|
||||
AppNodeBuilder.build(Snapshot.node),
|
||||
AppNodeBuilder.build(LLM.node),
|
||||
AppNodeBuilder.build(Env.node),
|
||||
AppNodeBuilder.build(AgentSvc.node),
|
||||
AppNodeBuilder.build(Command.node),
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
plugin,
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(ProviderSvc.node),
|
||||
lsp,
|
||||
mcp,
|
||||
AppNodeBuilder.build(FSUtil.node),
|
||||
SyncEvent.defaultLayer,
|
||||
AppNodeBuilder.build(EventV2Bridge.node),
|
||||
AppNodeBuilder.build(Database.node),
|
||||
status,
|
||||
MemoryService.layer,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const question = Question.layer.pipe(Layer.provideMerge(deps))
|
||||
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
|
||||
const registry = AppNodeBuilder.build(ToolRegistry.node, [[KiloSessions.node, KiloSessions.testLayer]])
|
||||
const trunc = AppNodeBuilder.build(Truncate.node)
|
||||
const proc = AppNodeBuilder.build(SessionProcessor.node, [[SessionSummary.node, summary]])
|
||||
const compact = AppNodeBuilder.build(SessionCompaction.node, [
|
||||
[SessionProcessor.node, proc],
|
||||
return LayerNode.compile(root, [
|
||||
[SessionSummary.node, summary],
|
||||
[Plugin.node, plugin],
|
||||
[LSP.node, lsp],
|
||||
[MCP.node, mcp],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer()],
|
||||
[KiloSessions.node, KiloSessions.testLayer],
|
||||
])
|
||||
return Layer.mergeAll(
|
||||
TestLLMServer.layer,
|
||||
SessionPrompt.layer.pipe(
|
||||
Layer.provide(AppNodeBuilder.build(SessionRevert.node)),
|
||||
Layer.provide(AppNodeBuilder.build(Image.node)),
|
||||
Layer.provide(summary),
|
||||
Layer.provideMerge(run),
|
||||
Layer.provideMerge(compact),
|
||||
Layer.provideMerge(proc),
|
||||
Layer.provideMerge(registry),
|
||||
Layer.provideMerge(trunc),
|
||||
Layer.provideMerge(question),
|
||||
Layer.provide(AppNodeBuilder.build(Instruction.node)),
|
||||
Layer.provide(AppNodeBuilder.build(SystemPrompt.node)),
|
||||
Layer.provideMerge(deps),
|
||||
),
|
||||
).pipe(
|
||||
Layer.provide(
|
||||
Layer.mergeAll(
|
||||
summary,
|
||||
deps,
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(BackgroundJob.node),
|
||||
Bus.layer,
|
||||
infra,
|
||||
AppNodeBuilder.build(Storage.node),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const it = testEffect(makeHttp())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
@@ -34,6 +35,7 @@ import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
||||
import { SessionCompaction } from "../../src/session/compaction"
|
||||
import { Instruction } from "../../src/session/instruction"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { SessionProcessor } from "../../src/session/processor"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
import { SessionRevert } from "../../src/session/revert"
|
||||
@@ -45,8 +47,6 @@ import { SessionSummary } from "../../src/session/summary"
|
||||
import { Todo } from "../../src/session/todo"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import { Storage } from "../../src/storage/storage"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { ToolRegistry } from "../../src/tool/registry"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
@@ -126,74 +126,62 @@ const lsp = Layer.succeed(
|
||||
}),
|
||||
)
|
||||
|
||||
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
|
||||
const run = SessionRunState.layer.pipe(Layer.provide(status))
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
// One compiled graph, mirroring test/session/prompt.test.ts. Effect v4 does
|
||||
// not memoize nested layers, so per-service AppNodeBuilder.build calls each stood up their own
|
||||
// Database and sessions written by the test were invisible to the prompt loop.
|
||||
const memoryNode = LayerNode.make({ service: MemoryService.Service, layer: MemoryService.layer, deps: [] })
|
||||
const testLLMServerNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
|
||||
|
||||
const promptRoot = LayerNode.group([
|
||||
SessionPrompt.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
MessageV2.node,
|
||||
Snapshot.node,
|
||||
LLM.node,
|
||||
Env.node,
|
||||
AgentSvc.node,
|
||||
Command.node,
|
||||
Permission.node,
|
||||
Plugin.node,
|
||||
Config.node,
|
||||
ProviderSvc.node,
|
||||
LSP.node,
|
||||
MCP.node,
|
||||
FSUtil.node,
|
||||
BackgroundJob.node,
|
||||
SessionStatus.node,
|
||||
SessionRunState.node,
|
||||
Database.node,
|
||||
EventV2Bridge.node,
|
||||
Question.node,
|
||||
Todo.node,
|
||||
ToolRegistry.node,
|
||||
Skill.node,
|
||||
Git.node,
|
||||
Ripgrep.node,
|
||||
Format.node,
|
||||
Truncate.node,
|
||||
SessionProcessor.node,
|
||||
Image.node,
|
||||
SessionCompaction.node,
|
||||
SessionRevert.node,
|
||||
Instruction.node,
|
||||
SystemPrompt.node,
|
||||
CrossSpawnSpawner.node,
|
||||
RuntimeFlags.node,
|
||||
memoryNode,
|
||||
testLLMServerNode,
|
||||
])
|
||||
|
||||
function makeHttp() {
|
||||
const deps = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(BackgroundJob.node),
|
||||
AppNodeBuilder.build(Snapshot.node),
|
||||
AppNodeBuilder.build(LLM.node),
|
||||
AppNodeBuilder.build(Env.node),
|
||||
AppNodeBuilder.build(AgentSvc.node),
|
||||
AppNodeBuilder.build(Command.node),
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
AppNodeBuilder.build(Plugin.node),
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(ProviderSvc.node),
|
||||
lsp,
|
||||
mcp,
|
||||
AppNodeBuilder.build(FSUtil.node),
|
||||
SyncEvent.defaultLayer,
|
||||
AppNodeBuilder.build(EventV2Bridge.node),
|
||||
AppNodeBuilder.build(Database.node),
|
||||
status,
|
||||
MemoryService.layer,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const question = Question.layer.pipe(Layer.provideMerge(deps))
|
||||
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
|
||||
const registry = AppNodeBuilder.build(ToolRegistry.node, [[KiloSessions.node, KiloSessions.testLayer]])
|
||||
const trunc = AppNodeBuilder.build(Truncate.node)
|
||||
const proc = AppNodeBuilder.build(SessionProcessor.node, [[SessionSummary.node, summary]])
|
||||
const compact = AppNodeBuilder.build(SessionCompaction.node, [
|
||||
[SessionProcessor.node, proc],
|
||||
return LayerNode.compile(promptRoot, [
|
||||
[SessionSummary.node, summary],
|
||||
[LSP.node, lsp],
|
||||
[MCP.node, mcp],
|
||||
[KiloSessions.node, KiloSessions.testLayer],
|
||||
])
|
||||
return Layer.mergeAll(
|
||||
TestLLMServer.layer,
|
||||
SessionPrompt.layer.pipe(
|
||||
Layer.provide(AppNodeBuilder.build(SessionRevert.node)),
|
||||
Layer.provide(AppNodeBuilder.build(Image.node)),
|
||||
Layer.provide(summary),
|
||||
Layer.provideMerge(run),
|
||||
Layer.provideMerge(compact),
|
||||
Layer.provideMerge(proc),
|
||||
Layer.provideMerge(registry),
|
||||
Layer.provideMerge(trunc),
|
||||
Layer.provideMerge(question),
|
||||
Layer.provide(AppNodeBuilder.build(Instruction.node)),
|
||||
Layer.provide(AppNodeBuilder.build(SystemPrompt.node)),
|
||||
Layer.provideMerge(deps),
|
||||
),
|
||||
).pipe(
|
||||
Layer.provide(
|
||||
Layer.mergeAll(
|
||||
summary,
|
||||
deps,
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(BackgroundJob.node),
|
||||
Bus.layer,
|
||||
infra,
|
||||
AppNodeBuilder.build(Storage.node),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const it = testEffect(makeHttp())
|
||||
const symlinkIt = process.platform === "win32" ? it.live.skip : it.live
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from "path"
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
||||
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { KiloSessionCompaction } from "@/kilocode/session/compaction"
|
||||
import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue"
|
||||
@@ -12,6 +13,8 @@ import { KiloSession } from "@/kilocode/session"
|
||||
import { Suggestion } from "../../src/kilocode/suggestion"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { provideTestInstance } from "../fixture/fixture"
|
||||
import { Session } from "../../src/session/session"
|
||||
@@ -26,9 +29,13 @@ import { remove as cleanup } from "./cleanup"
|
||||
import { pollWithTimeout } from "../lib/effect"
|
||||
|
||||
Log.init({ print: false })
|
||||
setDefaultTimeout(15_000)
|
||||
|
||||
const previous = Flag.KILO_DB
|
||||
const dbfile = path.join(os.tmpdir(), `kilo-prompt-queue-${process.pid}-${crypto.randomUUID()}.db`)
|
||||
const layer = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node]))
|
||||
const prompt = LayerNode.compile(LayerNode.group([SessionPrompt.node, SessionProjector.node]))
|
||||
const runtime = makeRuntime(Session.Service, layer)
|
||||
|
||||
beforeAll(async () => {
|
||||
await fs.rm(dbfile, { force: true })
|
||||
@@ -36,6 +43,7 @@ beforeAll(async () => {
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await runtime.dispose()
|
||||
await AppRuntime.dispose()
|
||||
await disposeTestRuntime()
|
||||
Flag.KILO_DB = previous
|
||||
@@ -49,13 +57,13 @@ const store = {
|
||||
|
||||
const sessions = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
runtime.runPromise((svc) => svc.create(input)),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
runtime.runPromise((svc) => svc.messages(input)),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
runtime.runPromise((svc) => svc.updateMessage(msg)),
|
||||
updatePart: <T extends MessageV2.Part>(part: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
runtime.runPromise((svc) => svc.updatePart(part)),
|
||||
}
|
||||
|
||||
function line(input: unknown) {
|
||||
@@ -121,7 +129,7 @@ function hasText(msg: MessageV2.WithParts, text: string) {
|
||||
function scoped<T>(dir: string, fn: (prompt: SessionPrompt.Interface) => Promise<T>) {
|
||||
return Effect.runPromise(
|
||||
SessionPrompt.Service.use((prompt) => Effect.promise(() => fn(prompt))).pipe(
|
||||
Effect.provide(SessionPrompt.defaultLayer),
|
||||
Effect.provide(prompt),
|
||||
provideInstance(dir),
|
||||
Effect.provide(testInstanceStoreLayer),
|
||||
Effect.scoped,
|
||||
|
||||
@@ -4,7 +4,10 @@ import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
@@ -20,6 +23,9 @@ import { remove as cleanup } from "./cleanup"
|
||||
|
||||
const previous = Flag.KILO_DB
|
||||
const dbfile = path.join(os.tmpdir(), `kilo-prompt-steering-${process.pid}-${crypto.randomUUID()}.db`)
|
||||
const layer = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node]))
|
||||
const prompt = LayerNode.compile(LayerNode.group([SessionPrompt.node, SessionProjector.node]))
|
||||
const runtime = makeRuntime(Session.Service, layer)
|
||||
|
||||
beforeAll(async () => {
|
||||
await fs.rm(dbfile, { force: true })
|
||||
@@ -27,6 +33,7 @@ beforeAll(async () => {
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await runtime.dispose()
|
||||
await AppRuntime.dispose()
|
||||
await disposeTestRuntime()
|
||||
Flag.KILO_DB = previous
|
||||
@@ -105,11 +112,9 @@ function question() {
|
||||
|
||||
const sessions = {
|
||||
create: (input: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
runtime.runPromise((svc) => svc.create(input)),
|
||||
messages: (sessionID: SessionID) =>
|
||||
Effect.runPromise(
|
||||
Session.Service.use((svc) => svc.messages({ sessionID })).pipe(Effect.provide(Session.defaultLayer)),
|
||||
),
|
||||
runtime.runPromise((svc) => svc.messages({ sessionID })),
|
||||
}
|
||||
|
||||
async function wait(sessionID: SessionID) {
|
||||
@@ -130,7 +135,7 @@ async function wait(sessionID: SessionID) {
|
||||
function scoped<T>(dir: string, fn: (prompt: SessionPrompt.Interface) => Promise<T>) {
|
||||
return Effect.runPromise(
|
||||
SessionPrompt.Service.use((prompt) => Effect.promise(() => fn(prompt))).pipe(
|
||||
Effect.provide(SessionPrompt.defaultLayer),
|
||||
Effect.provide(prompt),
|
||||
provideInstance(dir),
|
||||
Effect.provide(testInstanceStoreLayer),
|
||||
Effect.scoped,
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { expect, spyOn } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { SessionShare } from "../../src/share/session"
|
||||
import { Storage } from "../../src/storage/storage"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(AppNodeBuilder.build(Auth.node), AppNodeBuilder.build(Storage.node), AppNodeBuilder.build(CrossSpawnSpawner.node), RuntimeFlags.layer()),
|
||||
)
|
||||
|
||||
const layer = AppNodeBuilder.build(SessionShare.node).pipe(
|
||||
Layer.provideMerge(AppNodeBuilder.build(Session.node)),
|
||||
LayerNode.compile(
|
||||
LayerNode.group([
|
||||
SessionShare.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
Auth.node,
|
||||
Storage.node,
|
||||
CrossSpawnSpawner.node,
|
||||
RuntimeFlags.node,
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
it.instance("shares and unshares sessions through Kilo public URLs", () => {
|
||||
@@ -60,6 +65,5 @@ it.instance("shares and unshares sessions through Kilo public URLs", () => {
|
||||
request.mockRestore()
|
||||
}),
|
||||
),
|
||||
Effect.provide(layer),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Exit, Layer } from "effect"
|
||||
import { Effect, Exit } from "effect"
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
@@ -15,11 +16,14 @@ import { Snapshot } from "@/snapshot"
|
||||
import { provideInstance, provideTmpdirInstance } from "../../fixture/fixture"
|
||||
import { testEffect } from "../../lib/effect"
|
||||
|
||||
const env = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(SessionRevert.node),
|
||||
AppNodeBuilder.build(Snapshot.node),
|
||||
AppNodeBuilder.build(CrossSpawnSpawner.node),
|
||||
const env = LayerNode.compile(
|
||||
LayerNode.group([
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
SessionRevert.node,
|
||||
Snapshot.node,
|
||||
CrossSpawnSpawner.node,
|
||||
]),
|
||||
)
|
||||
const it = testEffect(env)
|
||||
const guarded = process.platform === "win32" ? it.live.skip : it.live
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
@@ -10,7 +11,7 @@ import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||
import { provideTmpdirInstance } from "../../fixture/fixture"
|
||||
import { testEffect } from "../../lib/effect"
|
||||
|
||||
const env = Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
const env = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, CrossSpawnSpawner.node]))
|
||||
const it = testEffect(env)
|
||||
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
|
||||
@@ -6,9 +6,8 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Env } from "@/env"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { provideInstanceEffect, tmpdirScoped } from "../fixture/fixture"
|
||||
import { provideInstanceEffect, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(AppNodeBuilder.build(Provider.node), AppNodeBuilder.build(Env.node), AppNodeBuilder.build(Plugin.node), AppNodeBuilder.build(CrossSpawnSpawner.node)),
|
||||
@@ -50,7 +49,7 @@ it.effect("loads Snowflake Cortex from OAuth credentials", () =>
|
||||
})
|
||||
const provider = yield* Provider.use
|
||||
.getProvider(ProviderV2.ID.make("snowflake-cortex"))
|
||||
.pipe(provideInstanceEffect(directory), Effect.provide(AppNodeBuilder.build(InstanceStore.node)))
|
||||
.pipe(provideInstanceEffect(directory), Effect.provide(testInstanceStoreLayer))
|
||||
|
||||
expect(provider.options.baseURL).toBe("https://test-account.snowflakecomputing.com/api/v2/cortex/v1")
|
||||
expect(provider.options.apiKey).toBe("access-token")
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
// tool propagates each child session's total cost up to the parent's
|
||||
// tool-wrapper assistant message (#6321).
|
||||
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { aggregateSessionStats } from "../../src/cli/cmd/stats"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
@@ -18,7 +19,7 @@ import { testEffect } from "../lib/effect"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Database.node)))
|
||||
const it = testEffect(LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, Database.node])))
|
||||
|
||||
const ref = {
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect, Exit, Layer } from "effect"
|
||||
import { Effect, Exit } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { BackgroundJob } from "../../src/background/job"
|
||||
@@ -35,20 +36,23 @@ const ref = {
|
||||
}
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
AppNodeBuilder.build(Agent.node),
|
||||
AppNodeBuilder.build(BackgroundJob.node),
|
||||
AppNodeBuilder.build(Bus.node),
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(SessionRunState.node),
|
||||
AppNodeBuilder.build(SessionStatus.node),
|
||||
AppNodeBuilder.build(CrossSpawnSpawner.node),
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(Truncate.node),
|
||||
AppNodeBuilder.build(Provider.node),
|
||||
AppNodeBuilder.build(ToolRegistry.node),
|
||||
AppNodeBuilder.build(Database.node),
|
||||
LayerNode.compile(
|
||||
LayerNode.group([
|
||||
Agent.node,
|
||||
BackgroundJob.node,
|
||||
Bus.node,
|
||||
Config.node,
|
||||
RuntimeFlags.node,
|
||||
SessionRunState.node,
|
||||
SessionStatus.node,
|
||||
CrossSpawnSpawner.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
Truncate.node,
|
||||
Provider.node,
|
||||
ToolRegistry.node,
|
||||
Database.node,
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { afterEach, beforeAll, describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
@@ -100,20 +101,23 @@ const catalog = {
|
||||
}
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
AppNodeBuilder.build(Agent.node),
|
||||
AppNodeBuilder.build(BackgroundJob.node),
|
||||
AppNodeBuilder.build(Bus.node),
|
||||
AppNodeBuilder.build(Config.node),
|
||||
RuntimeFlags.layer(),
|
||||
AppNodeBuilder.build(SessionRunState.node),
|
||||
AppNodeBuilder.build(SessionStatus.node),
|
||||
AppNodeBuilder.build(CrossSpawnSpawner.node),
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(Truncate.node),
|
||||
AppNodeBuilder.build(Provider.node),
|
||||
AppNodeBuilder.build(ToolRegistry.node),
|
||||
AppNodeBuilder.build(Database.node),
|
||||
LayerNode.compile(
|
||||
LayerNode.group([
|
||||
Agent.node,
|
||||
BackgroundJob.node,
|
||||
Bus.node,
|
||||
Config.node,
|
||||
RuntimeFlags.node,
|
||||
SessionRunState.node,
|
||||
SessionStatus.node,
|
||||
CrossSpawnSpawner.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
Truncate.node,
|
||||
Provider.node,
|
||||
ToolRegistry.node,
|
||||
Database.node,
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -5,11 +5,19 @@ import * as fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Worktree } from "../../src/worktree"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(Worktree.node), AppNodeBuilder.build(CrossSpawnSpawner.node)))
|
||||
const bootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
AppNodeBuilder.build(Worktree.node, [[InstanceStore.bootstrapNode, bootstrap]]),
|
||||
AppNodeBuilder.build(CrossSpawnSpawner.node),
|
||||
),
|
||||
)
|
||||
|
||||
describe("Worktree.remove lock retries", () => {
|
||||
it.live("retries transient git remove lock failures", () =>
|
||||
|
||||
@@ -212,7 +212,7 @@ export function withCliFixture<A, E>(
|
||||
|
||||
const spawn = Effect.fn("opencode.spawn")(function* (args: string[], opts?: SpawnOpts) {
|
||||
const start = Date.now()
|
||||
const timeoutMs = opts?.timeoutMs ?? 30_000
|
||||
const timeoutMs = opts?.timeoutMs ?? 45_000 // kilocode_change - current full CLI startup leaves less than 30s for multi-step runs
|
||||
// stdin: "ignore" so the child doesn't see a piped stdin and block
|
||||
// on `Bun.stdin.text()` (see src/cli/cmd/run.ts — non-TTY stdin is
|
||||
// consumed as the prompt). The old Process.run wrapper defaulted to
|
||||
@@ -537,8 +537,8 @@ export const cliIt = {
|
||||
body: (input: CliFixture) => Effect.Effect<A, E, Scope.Scope | HttpClient.HttpClient>,
|
||||
opts?: number | TestOptions,
|
||||
) =>
|
||||
// kilocode_change start - Windows CI cannot reliably start nested CLI trees concurrently
|
||||
(process.platform === "win32" ? test : test.concurrent)(
|
||||
// kilocode_change start - full CLI processes contend heavily during startup after the Effect graph migration
|
||||
test.serial(
|
||||
name,
|
||||
() => Effect.runPromise(Effect.scoped(withCliFixture(body))),
|
||||
opts,
|
||||
|
||||
@@ -21,7 +21,7 @@ const env = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
events,
|
||||
AppNodeBuilder.build(CrossSpawnSpawner.node),
|
||||
AppNodeBuilder.build(InstanceStore.node).pipe(Layer.provide(noopBootstrap)),
|
||||
AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]]), // kilocode_change
|
||||
).pipe(Layer.provide(RuntimeFlags.layer()), Layer.provide(AppNodeBuilder.build(Config.node)))
|
||||
const it = testEffect(Layer.mergeAll(env, RuntimeFlags.layer()))
|
||||
|
||||
|
||||
@@ -1,49 +1,41 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
|
||||
import { Plugin } from "@/plugin"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { Auth } from "@/auth"
|
||||
import { ModelCache } from "@/provider/model-cache" // kilocode_change
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { TestConfig } from "../fixture/config"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Config } from "@/config/config"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(CrossSpawnSpawner.node), AppNodeBuilder.build(FSUtil.node)))
|
||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([CrossSpawnSpawner.node, FSUtil.node])))
|
||||
|
||||
function layer(directory: string, plugins: string[]) {
|
||||
return AppNodeBuilder.build(ProviderAuth.node).pipe(
|
||||
Layer.provide(AppNodeBuilder.build(Auth.node)),
|
||||
Layer.provide(AppNodeBuilder.build(ModelCache.node)), // kilocode_change
|
||||
Layer.provide(
|
||||
AppNodeBuilder.build(Plugin.node).pipe(
|
||||
Layer.provide(AppNodeBuilder.build(EventV2Bridge.node)),
|
||||
Layer.provide(RuntimeFlags.layer()),
|
||||
Layer.provide(
|
||||
TestConfig.layer({
|
||||
get: () =>
|
||||
Effect.succeed({
|
||||
plugin: plugins,
|
||||
plugin_origins: plugins.map((plugin) => ({
|
||||
spec: plugin,
|
||||
source: path.join(directory, "opencode.json"),
|
||||
scope: "local" as const,
|
||||
})),
|
||||
}),
|
||||
directories: () => Effect.succeed([directory]),
|
||||
function providerAuthLayer(directory: string, plugins: string[]) {
|
||||
return AppNodeBuilder.build(ProviderAuth.node, [
|
||||
[
|
||||
Config.node,
|
||||
TestConfig.layer({
|
||||
get: () =>
|
||||
Effect.succeed({
|
||||
plugin: plugins,
|
||||
plugin_origins: plugins.map((plugin) => ({
|
||||
spec: plugin,
|
||||
source: path.join(directory, "opencode.json"),
|
||||
scope: "local" as const,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
directories: () => Effect.succeed([directory]),
|
||||
}),
|
||||
],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer()],
|
||||
])
|
||||
}
|
||||
|
||||
describe("plugin.auth-override", () => {
|
||||
@@ -76,10 +68,12 @@ describe("plugin.auth-override", () => {
|
||||
|
||||
const plain = yield* tmpdirScoped({ git: true })
|
||||
const plugin = pathToFileURL(path.join(pluginDir, "custom-copilot-auth.ts")).href
|
||||
const methods = yield* ProviderAuth.use.methods().pipe(Effect.provide(layer(tmp.directory, [plugin])))
|
||||
const methods = yield* ProviderAuth.use
|
||||
.methods()
|
||||
.pipe(Effect.provide(providerAuthLayer(tmp.directory, [plugin])))
|
||||
const plainMethods = yield* ProviderAuth.use
|
||||
.methods()
|
||||
.pipe(Effect.provide(layer(plain, [])), provideInstance(plain))
|
||||
.pipe(Effect.provide(providerAuthLayer(plain, [])), provideInstance(plain))
|
||||
|
||||
const copilot = methods[ProviderV2.ID.make("github-copilot")]
|
||||
expect(copilot).toBeDefined()
|
||||
|
||||
@@ -727,11 +727,13 @@ it.instance(
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"getSmallModel ignores model IDs without family metadata",
|
||||
// kilocode_change start - Kilo always has an auto-routed small-model fallback
|
||||
"getSmallModel falls back to Kilo auto when model IDs lack family metadata",
|
||||
Effect.gen(function* () {
|
||||
const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("test-provider"))
|
||||
expect(model).toBeUndefined()
|
||||
expect(model).toMatchObject({ providerID: "kilo", id: "kilo-auto/small" })
|
||||
}),
|
||||
// kilocode_change end
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
|
||||
@@ -40,6 +40,7 @@ import { SessionCompaction } from "../../src/session/compaction"
|
||||
import { SessionSummary } from "../../src/session/summary"
|
||||
import { Instruction } from "../../src/session/instruction"
|
||||
import { SessionProcessor } from "../../src/session/processor"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector" // kilocode_change
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
import { SessionRevert } from "../../src/session/revert"
|
||||
import { SessionRunState } from "../../src/session/run-state"
|
||||
@@ -167,10 +168,10 @@ const lsp = Layer.succeed(
|
||||
}),
|
||||
)
|
||||
|
||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(AppNodeBuilder.build(EventV2Bridge.node)))
|
||||
const run = SessionRunState.layer.pipe(Layer.provide(status))
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
|
||||
|
||||
// kilocode_change start - one compiled graph per env. Effect v4 does not memoize nested layers, so
|
||||
// LayerNode.compile's cache is the only dedupe; building services with separate AppNodeBuilder.build
|
||||
// calls gave this file three Database instances and every prompt died with "Session not found".
|
||||
// Mirrors upstream's harness, with Kilo's KiloSessions/MemoryService/fastAgents deltas.
|
||||
const agent: AgentSvc.Info = {
|
||||
name: "build",
|
||||
mode: "primary",
|
||||
@@ -200,72 +201,93 @@ const blockingProcessor = Layer.succeed(
|
||||
}),
|
||||
)
|
||||
|
||||
const runtimeFlags = RuntimeFlags.layer({ experimentalEventSystem: true })
|
||||
|
||||
const memoryNode = LayerNode.make({ service: MemoryService.Service, layer: MemoryService.layer, deps: [] })
|
||||
const testLLMServerNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
|
||||
|
||||
const promptRoot = LayerNode.group([
|
||||
SessionPrompt.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
MessageV2.node,
|
||||
Snapshot.node,
|
||||
LLM.node,
|
||||
Env.node,
|
||||
AgentSvc.node,
|
||||
Command.node,
|
||||
Permission.node,
|
||||
Plugin.node,
|
||||
Config.node,
|
||||
ProviderSvc.node,
|
||||
LSP.node,
|
||||
MCP.node,
|
||||
FSUtil.node,
|
||||
BackgroundJob.node,
|
||||
SessionStatus.node,
|
||||
SessionRunState.node,
|
||||
Database.node,
|
||||
EventV2Bridge.node,
|
||||
Question.node,
|
||||
Todo.node,
|
||||
ToolRegistry.node,
|
||||
Skill.node,
|
||||
Git.node,
|
||||
Ripgrep.node,
|
||||
Format.node,
|
||||
Truncate.node,
|
||||
SessionProcessor.node,
|
||||
Image.node,
|
||||
SessionCompaction.node,
|
||||
SessionRevert.node,
|
||||
Instruction.node,
|
||||
SystemPrompt.node,
|
||||
CrossSpawnSpawner.node,
|
||||
RuntimeFlags.node,
|
||||
memoryNode,
|
||||
])
|
||||
|
||||
function makePrompt(input?: { processor?: "blocking" }) {
|
||||
const deps = Layer.mergeAll(
|
||||
AppNodeBuilder.build(Session.node),
|
||||
AppNodeBuilder.build(Snapshot.node),
|
||||
AppNodeBuilder.build(LLM.node),
|
||||
AppNodeBuilder.build(Env.node),
|
||||
input?.processor === "blocking" ? fastAgents : AppNodeBuilder.build(AgentSvc.node),
|
||||
AppNodeBuilder.build(Command.node),
|
||||
AppNodeBuilder.build(Permission.node),
|
||||
AppNodeBuilder.build(Plugin.node),
|
||||
AppNodeBuilder.build(Config.node),
|
||||
AppNodeBuilder.build(ProviderSvc.node),
|
||||
lsp,
|
||||
makeMcp(),
|
||||
AppNodeBuilder.build(FSUtil.node),
|
||||
AppNodeBuilder.build(BackgroundJob.node),
|
||||
status,
|
||||
AppNodeBuilder.build(Database.node),
|
||||
AppNodeBuilder.build(EventV2Bridge.node),
|
||||
Bus.layer,
|
||||
MemoryService.layer,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const question = Question.layer.pipe(Layer.provideMerge(deps))
|
||||
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
|
||||
const registry = AppNodeBuilder.build(ToolRegistry.node, [
|
||||
[KiloSessions.node, KiloSessions.testLayer],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
|
||||
])
|
||||
const trunc = AppNodeBuilder.build(Truncate.node)
|
||||
const proc =
|
||||
input?.processor === "blocking"
|
||||
? blockingProcessor
|
||||
: AppNodeBuilder.build(SessionProcessor.node, [
|
||||
[SessionSummary.node, summary],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
|
||||
])
|
||||
const compact = AppNodeBuilder.build(SessionCompaction.node, [
|
||||
[SessionProcessor.node, proc],
|
||||
const replacements = [
|
||||
[SessionSummary.node, summary],
|
||||
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
|
||||
])
|
||||
return SessionPrompt.layer.pipe(
|
||||
Layer.provide(AppNodeBuilder.build(SessionRevert.node)),
|
||||
Layer.provide(AppNodeBuilder.build(Image.node)),
|
||||
Layer.provide(summary),
|
||||
Layer.provideMerge(run),
|
||||
Layer.provideMerge(compact),
|
||||
Layer.provideMerge(proc),
|
||||
Layer.provideMerge(registry),
|
||||
Layer.provideMerge(trunc),
|
||||
Layer.provideMerge(question),
|
||||
Layer.provide(AppNodeBuilder.build(Instruction.node)),
|
||||
Layer.provide(AppNodeBuilder.build(SystemPrompt.node)),
|
||||
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
|
||||
Layer.provideMerge(deps),
|
||||
Layer.provide(summary),
|
||||
)
|
||||
[LSP.node, lsp],
|
||||
[MCP.node, makeMcp()],
|
||||
[RuntimeFlags.node, runtimeFlags],
|
||||
[KiloSessions.node, KiloSessions.testLayer],
|
||||
] as const
|
||||
if (input?.processor === "blocking") {
|
||||
return LayerNode.compile(promptRoot, [
|
||||
...replacements,
|
||||
[SessionProcessor.node, blockingProcessor],
|
||||
[AgentSvc.node, fastAgents],
|
||||
])
|
||||
}
|
||||
return LayerNode.compile(promptRoot, replacements)
|
||||
}
|
||||
|
||||
function makeHttp(input?: { processor?: "blocking" }) {
|
||||
return Layer.mergeAll(TestLLMServer.layer, makePrompt(input))
|
||||
const root = LayerNode.group([promptRoot, testLLMServerNode])
|
||||
const replacements = [
|
||||
[SessionSummary.node, summary],
|
||||
[LSP.node, lsp],
|
||||
[MCP.node, makeMcp()],
|
||||
[RuntimeFlags.node, runtimeFlags],
|
||||
[KiloSessions.node, KiloSessions.testLayer],
|
||||
] as const
|
||||
if (input?.processor === "blocking") {
|
||||
return LayerNode.compile(root, [
|
||||
...replacements,
|
||||
[SessionProcessor.node, blockingProcessor],
|
||||
[AgentSvc.node, fastAgents],
|
||||
])
|
||||
}
|
||||
return LayerNode.compile(root, replacements)
|
||||
}
|
||||
|
||||
function makeHttpNoLLMServer(input?: { processor?: "blocking" }) {
|
||||
return makePrompt(input)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
const it = testEffect(makeHttp())
|
||||
const noLLMServer = testEffect(makeHttpNoLLMServer())
|
||||
@@ -777,6 +799,7 @@ noLLMServer.instance.skip(
|
||||
],
|
||||
})
|
||||
|
||||
// kilocode_change start - compile the v2 reader against this test's database graph
|
||||
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
||||
Effect.provide(
|
||||
LayerNode.compile(SessionV2.node, [
|
||||
@@ -785,6 +808,7 @@ noLLMServer.instance.skip(
|
||||
]),
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
const { db } = yield* Database.Service
|
||||
const row = yield* db
|
||||
.select()
|
||||
@@ -1829,9 +1853,16 @@ unixNoLLMServer(
|
||||
const tool = completedTool(result.parts)
|
||||
if (!tool) return
|
||||
|
||||
// kilocode_change start - bind v2 execution and location services in the consolidated test graph
|
||||
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
||||
Effect.provide(AppNodeBuilder.build(SessionV2.node)),
|
||||
Effect.provide(
|
||||
LayerNode.compile(SessionV2.node, [
|
||||
[SessionExecution.node, SessionExecution.noopLayer],
|
||||
[LocationServiceMap.node, locationServiceMapLayer],
|
||||
]),
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
const shell = messages.find((message) => message.type === "shell")
|
||||
|
||||
expect(shell).toMatchObject({
|
||||
|
||||
Reference in New Issue
Block a user