mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
fix(cli): bound XLSX read input size
This commit is contained in:
@@ -16,7 +16,11 @@ const it = testEffect(CrossSpawnSpawner.defaultLayer)
|
||||
|
||||
function layer(overrides: Partial<Config.Interface> = {}) {
|
||||
return Layer.merge(
|
||||
KiloSessions.layer.pipe(Layer.provideMerge(Bus.layer), Layer.provide(TestConfig.layer(overrides))),
|
||||
KiloSessions.layer.pipe(
|
||||
Layer.provideMerge(Bus.layer),
|
||||
Layer.provide(TestConfig.layer(overrides)),
|
||||
Layer.provide(Session.defaultLayer),
|
||||
),
|
||||
Auth.defaultLayer,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
@@ -16,6 +17,19 @@ import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const sessions = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
get: (id: SessionID) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.get(id)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updatePart: <T extends MessageV2.Part>(part: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
}
|
||||
|
||||
const model = {
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
@@ -32,8 +46,8 @@ async function seed(input: {
|
||||
tools?: Array<{ tool: string; input: Record<string, unknown>; output: string }>
|
||||
finish?: string
|
||||
}) {
|
||||
const session = await Session.create({})
|
||||
const user = await Session.updateMessage({
|
||||
const session = await sessions.create({})
|
||||
const user = await sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -41,7 +55,7 @@ async function seed(input: {
|
||||
agent: input.agent ?? "plan",
|
||||
model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -73,9 +87,9 @@ async function seed(input: {
|
||||
},
|
||||
finish: (input.finish as MessageV2.Assistant["finish"]) ?? "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant)
|
||||
await sessions.updateMessage(assistant)
|
||||
if (input.text !== undefined) {
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -85,7 +99,7 @@ async function seed(input: {
|
||||
}
|
||||
|
||||
for (const t of input.tools ?? []) {
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -103,7 +117,7 @@ async function seed(input: {
|
||||
} satisfies MessageV2.ToolPart)
|
||||
}
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
return { sessionID: session.id, messages }
|
||||
}
|
||||
|
||||
@@ -217,7 +231,7 @@ describe("plan_exit detection", () => {
|
||||
|
||||
await expect(pending).resolves.toBe("continue")
|
||||
|
||||
const messages = await Session.messages({ sessionID: seeded.sessionID })
|
||||
const messages = await sessions.messages({ sessionID: seeded.sessionID })
|
||||
const user = messages
|
||||
.slice()
|
||||
.reverse()
|
||||
@@ -239,8 +253,8 @@ describe("plan_exit detection", () => {
|
||||
|
||||
test("plan_exit with non-completed status does NOT trigger", () =>
|
||||
withInstance(async () => {
|
||||
const session = await Session.create({})
|
||||
const user = await Session.updateMessage({
|
||||
const session = await sessions.create({})
|
||||
const user = await sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -248,7 +262,7 @@ describe("plan_exit detection", () => {
|
||||
agent: "plan",
|
||||
model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -277,15 +291,15 @@ describe("plan_exit detection", () => {
|
||||
},
|
||||
finish: "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant)
|
||||
await Session.updatePart({
|
||||
await sessions.updateMessage(assistant)
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
type: "text",
|
||||
text: "Here is the plan",
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -301,7 +315,7 @@ describe("plan_exit detection", () => {
|
||||
},
|
||||
} satisfies MessageV2.ToolPart)
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
|
||||
// Verify the tool part IS present but errored (not completed)
|
||||
const toolPart = messages.flatMap((msg) => msg.parts).find((p) => p.type === "tool" && p.tool === "plan_exit")
|
||||
@@ -318,10 +332,10 @@ describe("plan_exit detection", () => {
|
||||
|
||||
test("plan_exit on earlier assistant message triggers when later message has text only", () =>
|
||||
withInstance(async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await sessions.create({})
|
||||
// Use explicit timestamps to ensure deterministic message ordering
|
||||
const now = Date.now()
|
||||
const user = await Session.updateMessage({
|
||||
const user = await sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -329,7 +343,7 @@ describe("plan_exit detection", () => {
|
||||
agent: "plan",
|
||||
model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -353,8 +367,8 @@ describe("plan_exit detection", () => {
|
||||
tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
finish: "tool-calls",
|
||||
}
|
||||
await Session.updateMessage(assistant1)
|
||||
await Session.updatePart({
|
||||
await sessions.updateMessage(assistant1)
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant1.id,
|
||||
sessionID: session.id,
|
||||
@@ -387,8 +401,8 @@ describe("plan_exit detection", () => {
|
||||
tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
finish: "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant2)
|
||||
await Session.updatePart({
|
||||
await sessions.updateMessage(assistant2)
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant2.id,
|
||||
sessionID: session.id,
|
||||
@@ -396,7 +410,7 @@ describe("plan_exit detection", () => {
|
||||
text: "The plan is complete. I've called plan_exit.",
|
||||
})
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
expect(SessionPrompt.shouldAskPlanFollowup({ messages, abort: AbortSignal.any([]) })).toBe(true)
|
||||
}))
|
||||
|
||||
@@ -412,7 +426,7 @@ describe("plan_exit detection", () => {
|
||||
],
|
||||
})
|
||||
|
||||
const session = await Session.get(seeded.sessionID)
|
||||
const session = await sessions.get(seeded.sessionID)
|
||||
const plan = Session.plan(session, Instance.current)
|
||||
await fs.mkdir(path.dirname(plan), { recursive: true })
|
||||
await Bun.write(plan, "Do implementation step 1")
|
||||
@@ -435,10 +449,10 @@ describe("plan_exit detection", () => {
|
||||
|
||||
test("PlanFollowup.ask shows prompt when plan text is on earlier assistant and last assistant is empty", () =>
|
||||
withInstance(async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await sessions.create({})
|
||||
// Use explicit timestamps to ensure deterministic message ordering
|
||||
const now = Date.now()
|
||||
const user = await Session.updateMessage({
|
||||
const user = await sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -446,7 +460,7 @@ describe("plan_exit detection", () => {
|
||||
agent: "plan",
|
||||
model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -470,15 +484,15 @@ describe("plan_exit detection", () => {
|
||||
tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
finish: "tool-calls",
|
||||
}
|
||||
await Session.updateMessage(assistant1)
|
||||
await Session.updatePart({
|
||||
await sessions.updateMessage(assistant1)
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant1.id,
|
||||
sessionID: session.id,
|
||||
type: "text",
|
||||
text: "Here is the detailed plan:\n\n## Step 1\nDo something\n\n## Step 2\nDo something else",
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant1.id,
|
||||
sessionID: session.id,
|
||||
@@ -511,9 +525,9 @@ describe("plan_exit detection", () => {
|
||||
tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
finish: "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant2)
|
||||
await sessions.updateMessage(assistant2)
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
|
||||
// shouldAskPlanFollowup should detect plan_exit on the earlier message
|
||||
expect(SessionPrompt.shouldAskPlanFollowup({ messages, abort: AbortSignal.any([]) })).toBe(true)
|
||||
|
||||
@@ -46,6 +46,19 @@ const todo = {
|
||||
},
|
||||
}
|
||||
|
||||
const store = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
get: (id: SessionID) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.get(id)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updatePart: <T extends MessageV2.Part>(part: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
}
|
||||
|
||||
const model = {
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
@@ -90,8 +103,8 @@ async function seed(input: {
|
||||
variant?: string
|
||||
tools?: Array<{ tool: string; input: Record<string, unknown>; output: string }>
|
||||
}) {
|
||||
const session = await Session.create({})
|
||||
const user = await Session.updateMessage({
|
||||
const session = await store.create({})
|
||||
const user = await store.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -101,7 +114,7 @@ async function seed(input: {
|
||||
agent: "plan",
|
||||
model: input.variant ? { ...model, variant: input.variant } : model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await store.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -138,8 +151,8 @@ async function seed(input: {
|
||||
},
|
||||
finish: "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant)
|
||||
await Session.updatePart({
|
||||
await store.updateMessage(assistant)
|
||||
await store.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -148,7 +161,7 @@ async function seed(input: {
|
||||
})
|
||||
|
||||
for (const t of input.tools ?? []) {
|
||||
await Session.updatePart({
|
||||
await store.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -166,7 +179,7 @@ async function seed(input: {
|
||||
} satisfies MessageV2.ToolPart)
|
||||
}
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await store.messages({ sessionID: session.id })
|
||||
return {
|
||||
sessionID: session.id,
|
||||
messages,
|
||||
@@ -174,7 +187,7 @@ async function seed(input: {
|
||||
}
|
||||
|
||||
async function latestUser(sessionID: SessionID) {
|
||||
const messages = await Session.messages({ sessionID })
|
||||
const messages = await store.messages({ sessionID })
|
||||
return messages
|
||||
.slice()
|
||||
.reverse()
|
||||
@@ -500,7 +513,7 @@ describe("plan follow-up", () => {
|
||||
await expect(pending).resolves.toBe("continue")
|
||||
|
||||
// The injected user message must be visible when scoped
|
||||
const all = await Session.messages({ sessionID: seeded.sessionID })
|
||||
const all = await store.messages({ sessionID: seeded.sessionID })
|
||||
const scoped = KiloSessionPromptQueue.scope(seeded.sessionID, all)
|
||||
const injected = scoped.findLast((m) => m.info.role === "user")
|
||||
expect(injected).toBeDefined()
|
||||
@@ -629,8 +642,8 @@ describe("plan follow-up", () => {
|
||||
if (!newSessionID || !next) throw new Error("expected follow-up session")
|
||||
expect(next.id).toBe(newSessionID)
|
||||
expect(next.parentID).toBeUndefined()
|
||||
const planPath = Session.plan(await Session.get(seeded.sessionID), Instance.current)
|
||||
const messages = await Session.messages({ sessionID: newSessionID })
|
||||
const planPath = Session.plan(await store.get(seeded.sessionID), Instance.current)
|
||||
const messages = await store.messages({ sessionID: newSessionID })
|
||||
const user = messages.find((item) => item.info.role === "user")
|
||||
expect(user?.info.role).toBe("user")
|
||||
if (!user || user.info.role !== "user") throw new Error("expected seeded user message")
|
||||
@@ -737,9 +750,9 @@ describe("plan follow-up", () => {
|
||||
if (next) {
|
||||
const planPath = await WithInstance.provide({
|
||||
directory: dir,
|
||||
fn: async () => Session.plan(await Session.get(seeded.sessionID), Instance.current),
|
||||
fn: async () => Session.plan(await store.get(seeded.sessionID), Instance.current),
|
||||
})
|
||||
const messages = await Session.messages({ sessionID: next.id })
|
||||
const messages = await store.messages({ sessionID: next.id })
|
||||
const user = messages.find((item) => item.info.role === "user")
|
||||
if (!user || user.info.role !== "user") throw new Error("expected user message")
|
||||
const part = user.parts.find((item) => item.type === "text")
|
||||
@@ -947,7 +960,7 @@ describe("plan follow-up", () => {
|
||||
|
||||
const newSessionID = created[0]
|
||||
if (!newSessionID) throw new Error("expected follow-up session")
|
||||
const messages = await Session.messages({ sessionID: newSessionID })
|
||||
const messages = await store.messages({ sessionID: newSessionID })
|
||||
const user = messages.find((item) => item.info.role === "user")
|
||||
if (!user || user.info.role !== "user") throw new Error("expected user message")
|
||||
const part = user.parts.find((item) => item.type === "text")
|
||||
@@ -1111,7 +1124,7 @@ describe("plan follow-up", () => {
|
||||
// deferred has not resolved yet.
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (followup) {
|
||||
const msgs = await Session.messages({ sessionID: followup })
|
||||
const msgs = await store.messages({ sessionID: followup })
|
||||
const user = msgs.find((m) => m.info.role === "user")
|
||||
const part = user?.parts.find((p) => p.type === "text")
|
||||
if (part?.type === "text" && part.text.includes("Implement the following plan:")) break
|
||||
@@ -1121,7 +1134,7 @@ describe("plan follow-up", () => {
|
||||
|
||||
expect(followup).toBeDefined()
|
||||
if (!followup) return
|
||||
const initial = await Session.messages({ sessionID: followup })
|
||||
const initial = await store.messages({ sessionID: followup })
|
||||
const initialUser = initial.find((m) => m.info.role === "user")
|
||||
const initialPart = initialUser?.parts.find((p) => p.type === "text")
|
||||
expect(initialPart?.type).toBe("text")
|
||||
@@ -1135,7 +1148,7 @@ describe("plan follow-up", () => {
|
||||
await expect(pending).resolves.toBe("break")
|
||||
|
||||
// Same part ID updated in-place — handover section now present.
|
||||
const final = await Session.messages({ sessionID: followup })
|
||||
const final = await store.messages({ sessionID: followup })
|
||||
const finalUser = final.find((m) => m.info.role === "user")
|
||||
const finalPart = finalUser?.parts.find((p) => p.type === "text")
|
||||
if (finalPart?.type !== "text") return
|
||||
@@ -1294,7 +1307,7 @@ describe("plan follow-up", () => {
|
||||
})
|
||||
|
||||
await expect(pending).resolves.toBe("break")
|
||||
expect((await Session.messages({ sessionID: seeded.sessionID })).length).toBe(2)
|
||||
expect((await store.messages({ sessionID: seeded.sessionID })).length).toBe(2)
|
||||
}))
|
||||
|
||||
test("formatTodos - returns empty string for no todos", () => {
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import path from "path"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { LSP } from "../../src/lsp/lsp"
|
||||
import { Instruction } from "../../src/session/instruction"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { ReadTool } from "../../src/tool/read"
|
||||
import * as Tool from "../../src/tool/tool"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const ctx = {
|
||||
sessionID: SessionID.make("ses_test-notebook"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "code",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
Agent.defaultLayer,
|
||||
AppFileSystem.defaultLayer,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
Instruction.defaultLayer,
|
||||
LSP.defaultLayer,
|
||||
Truncate.defaultLayer,
|
||||
),
|
||||
)
|
||||
|
||||
const run = Effect.fn("NotebookReadTest.run")(function* (dir: string, args: Tool.InferParameters<typeof ReadTool>) {
|
||||
return yield* provideInstance(dir)(
|
||||
Effect.gen(function* () {
|
||||
const info = yield* ReadTool
|
||||
const tool = yield* Tool.init(info)
|
||||
return yield* tool.execute(args, ctx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const put = Effect.fn("NotebookReadTest.put")(function* (filepath: string, content: string | Uint8Array) {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
yield* fs.writeWithDirs(filepath, content)
|
||||
})
|
||||
|
||||
const notebook = JSON.stringify({
|
||||
metadata: { secret: "ignore-notebook-metadata" },
|
||||
cells: [
|
||||
{
|
||||
cell_type: "markdown",
|
||||
metadata: { private: "ignore-cell-metadata" },
|
||||
source: ["# Analysis\n", "Useful introduction"],
|
||||
},
|
||||
{
|
||||
cell_type: "raw",
|
||||
source: ["ignore raw cell"],
|
||||
},
|
||||
{
|
||||
cell_type: "code",
|
||||
execution_count: 7,
|
||||
metadata: {},
|
||||
source: ["value = 42\n", "print(value)"],
|
||||
outputs: [{ output_type: "stream", text: ["ignore-output-payload"] }],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
describe("kilocode notebook reads", () => {
|
||||
it.live("extracts markdown and code cells without notebook payloads", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "analysis.ipynb")
|
||||
yield* put(filepath, notebook)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("<markdown_cell>")
|
||||
expect(result.output).toContain("# Analysis")
|
||||
expect(result.output).toContain("<code_cell>")
|
||||
expect(result.output).toContain("value = 42")
|
||||
expect(result.output.indexOf("# Analysis")).toBeLessThan(result.output.indexOf("value = 42"))
|
||||
expect(result.output).not.toContain("ignore-output-payload")
|
||||
expect(result.output).not.toContain("ignore-notebook-metadata")
|
||||
expect(result.output).not.toContain("ignore-cell-metadata")
|
||||
expect(result.output).not.toContain("ignore raw cell")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("skips invalid cells without exposing raw notebook payloads", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "partial.ipynb")
|
||||
const content = JSON.stringify({
|
||||
cells: [
|
||||
null,
|
||||
{ cell_type: "code", source: null, outputs: ["INVALID_OUTPUT_SHOULD_NOT_APPEAR"] },
|
||||
{ cell_type: "markdown", source: ["Readable cell"], metadata: { marker: "CELL_METADATA_SHOULD_NOT_APPEAR" } },
|
||||
],
|
||||
})
|
||||
yield* put(filepath, content)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("Readable cell")
|
||||
expect(result.output).not.toContain("INVALID_OUTPUT_SHOULD_NOT_APPEAR")
|
||||
expect(result.output).not.toContain("CELL_METADATA_SHOULD_NOT_APPEAR")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("reports valid notebooks with no readable cells without exposing payloads", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "empty-content.ipynb")
|
||||
const content = JSON.stringify({
|
||||
metadata: { marker: "NOTEBOOK_METADATA_SHOULD_NOT_APPEAR" },
|
||||
cells: [
|
||||
null,
|
||||
{ cell_type: "raw", source: ["RAW_CONTENT_SHOULD_NOT_APPEAR"] },
|
||||
{ cell_type: "code", source: null, outputs: ["INVALID_OUTPUT_SHOULD_NOT_APPEAR"] },
|
||||
],
|
||||
})
|
||||
yield* put(filepath, content)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("Notebook contains no markdown or code cell content")
|
||||
expect(result.output).not.toContain("NOTEBOOK_METADATA_SHOULD_NOT_APPEAR")
|
||||
expect(result.output).not.toContain("RAW_CONTENT_SHOULD_NOT_APPEAR")
|
||||
expect(result.output).not.toContain("INVALID_OUTPUT_SHOULD_NOT_APPEAR")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("applies read pagination to extracted cell text", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "paged.ipynb")
|
||||
yield* put(filepath, notebook)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath, offset: 2, limit: 2 })
|
||||
|
||||
expect(result.output).toContain("2: # Analysis")
|
||||
expect(result.output).toContain("3: Useful introduction")
|
||||
expect(result.output).not.toContain("value = 42")
|
||||
expect(result.metadata.preview).toBe("# Analysis\nUseful introduction")
|
||||
expect(result.metadata.truncated).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("falls back to raw text for malformed notebooks", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "broken.ipynb")
|
||||
const content = '{"cells":[{"cell_type":"markdown","source":["unfinished"]}'
|
||||
yield* put(filepath, content)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain(content)
|
||||
expect(result.output).not.toContain("<markdown_cell>")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("keeps ordinary text reads unchanged", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "notes.txt")
|
||||
yield* put(filepath, "plain text")
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("1: plain text")
|
||||
expect(result.output).not.toContain("<markdown_cell>")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("keeps PDF files as native attachments", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "document.pdf")
|
||||
yield* put(filepath, "%PDF-1.4\nminimal content")
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toBe("PDF read successfully")
|
||||
expect(result.attachments?.[0].mime).toBe("application/pdf")
|
||||
expect(result.metadata.truncated).toBe(false)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { truncate } from "fs/promises"
|
||||
import path from "path"
|
||||
import { write, utils, type WorkBook, type WorkSheet } from "xlsx"
|
||||
import { TextReader, TextWriter, Uint8ArrayReader, Uint8ArrayWriter, ZipReader, ZipWriter } from "@zip.js/zip.js"
|
||||
@@ -198,6 +199,20 @@ describe("kilocode XLSX reads", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("rejects spreadsheets larger than the parser input limit", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const file = path.join(dir, "large-input.xlsx")
|
||||
yield* put(file, new Uint8Array([0x50, 0x4b]))
|
||||
yield* Effect.promise(() => truncate(file, 50 * 1024 * 1024 + 1))
|
||||
|
||||
const err = yield* fail(dir, file)
|
||||
|
||||
expect(err.message).toContain("Cannot read spreadsheet file")
|
||||
expect(err.message).toContain("exceeds the 50 MB size limit")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("continues rejecting unsupported workbook formats as binary", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
|
||||
@@ -17,6 +17,7 @@ const bus = Bus.layer
|
||||
const env = Layer.mergeAll(
|
||||
Permission.layer.pipe(Layer.provide(bus), Layer.provide(Config.defaultLayer)),
|
||||
Config.defaultLayer,
|
||||
Session.defaultLayer,
|
||||
bus,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
)
|
||||
@@ -72,10 +73,11 @@ describe("AllowEverythingPermission", () => {
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
expect(yield* AllowEverythingPermission.effect({ enable: true })).toBe(true)
|
||||
expect(yield* AllowEverythingPermission.effect({ enable: false })).toBe(true)
|
||||
|
||||
const session = yield* Effect.promise(() => Session.create({}))
|
||||
const session = yield* sessions.create({})
|
||||
const pending = yield* ask({
|
||||
id: PermissionID.make("permission_global_disable"),
|
||||
sessionID: session.id,
|
||||
@@ -106,16 +108,15 @@ describe("AllowEverythingPermission", () => {
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Effect.promise(() =>
|
||||
Session.create({
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
}),
|
||||
)
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
})
|
||||
|
||||
expect(yield* AllowEverythingPermission.effect({ enable: true, sessionID: session.id })).toBe(true)
|
||||
expect(yield* AllowEverythingPermission.effect({ enable: false, sessionID: session.id })).toBe(true)
|
||||
|
||||
const next = yield* Effect.promise(() => Session.get(session.id))
|
||||
const next = yield* sessions.get(session.id)
|
||||
expect(next.permission ?? []).toEqual([])
|
||||
|
||||
const pending = yield* ask({
|
||||
@@ -140,7 +141,7 @@ describe("AllowEverythingPermission", () => {
|
||||
expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
|
||||
}
|
||||
|
||||
const other = yield* Effect.promise(() => Session.create({}))
|
||||
const other = yield* sessions.create({})
|
||||
const blocked = yield* ask({
|
||||
id: PermissionID.make("permission_other_session"),
|
||||
sessionID: other.id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
@@ -8,6 +9,19 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const sessions = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
get: (id: SessionID) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.get(id)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updatePart: <T extends MessageV2.Part>(part: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
})
|
||||
@@ -36,7 +50,7 @@ function taskPart(input: { messageID: string; sessionID: string; childSessionID:
|
||||
|
||||
async function userMsg(sid: string) {
|
||||
const id = MessageID.ascending()
|
||||
await Session.updateMessage({
|
||||
await sessions.updateMessage({
|
||||
id,
|
||||
sessionID: SessionID.make(sid),
|
||||
role: "user",
|
||||
@@ -50,7 +64,7 @@ async function userMsg(sid: string) {
|
||||
|
||||
async function asstMsg(sid: string, parent: string) {
|
||||
const id = MessageID.ascending()
|
||||
await Session.updateMessage({
|
||||
await sessions.updateMessage({
|
||||
id,
|
||||
sessionID: SessionID.make(sid),
|
||||
role: "assistant",
|
||||
@@ -75,12 +89,12 @@ describe("Session.fork child session remapping", () => {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const parent = await Session.create({ title: "parent" })
|
||||
const child = await Session.create({ parentID: parent.id, title: "child subagent" })
|
||||
const parent = await sessions.create({ title: "parent" })
|
||||
const child = await sessions.create({ parentID: parent.id, title: "child subagent" })
|
||||
|
||||
// Add a user message to the child so it has content
|
||||
const childMsgId = await userMsg(child.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: childMsgId,
|
||||
sessionID: child.id,
|
||||
@@ -90,7 +104,7 @@ describe("Session.fork child session remapping", () => {
|
||||
|
||||
// Add a user message then an assistant message with a task tool part referencing the child
|
||||
const parentUserMsg = await userMsg(parent.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: parentUserMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -99,7 +113,7 @@ describe("Session.fork child session remapping", () => {
|
||||
} as MessageV2.TextPart)
|
||||
|
||||
const parentAsstMsg = await asstMsg(parent.id, parentUserMsg)
|
||||
await Session.updatePart(
|
||||
await sessions.updatePart(
|
||||
taskPart({
|
||||
messageID: parentAsstMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -112,7 +126,7 @@ describe("Session.fork child session remapping", () => {
|
||||
expect(forked.id).not.toBe(parent.id)
|
||||
|
||||
// Check that the forked session's task part references a DIFFERENT child session
|
||||
const forkedMsgs = await Session.messages({ sessionID: forked.id })
|
||||
const forkedMsgs = await sessions.messages({ sessionID: forked.id })
|
||||
const parts = forkedMsgs.flatMap((m) => m.parts)
|
||||
const tools = parts.filter((p) => p.type === "tool" && p.tool === "task") as MessageV2.ToolPart[]
|
||||
|
||||
@@ -121,11 +135,11 @@ describe("Session.fork child session remapping", () => {
|
||||
expect(meta.sessionId).not.toBe(child.id)
|
||||
|
||||
// Verify the forked child session actually exists and has content
|
||||
const forkedChild = await Session.get(SessionID.make(meta.sessionId))
|
||||
const forkedChild = await sessions.get(SessionID.make(meta.sessionId))
|
||||
expect(forkedChild).toBeDefined()
|
||||
expect(forkedChild.id).not.toBe(child.id)
|
||||
|
||||
const forkedChildMsgs = await Session.messages({ sessionID: forkedChild.id })
|
||||
const forkedChildMsgs = await sessions.messages({ sessionID: forkedChild.id })
|
||||
expect(forkedChildMsgs).toHaveLength(1)
|
||||
expect(forkedChildMsgs[0].parts[0].type).toBe("text")
|
||||
},
|
||||
@@ -142,13 +156,13 @@ describe("Session.fork child session remapping", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// grandchild -> child -> parent
|
||||
const parent = await Session.create({ title: "parent" })
|
||||
const child = await Session.create({ parentID: parent.id, title: "child" })
|
||||
const grandchild = await Session.create({ parentID: child.id, title: "grandchild" })
|
||||
const parent = await sessions.create({ title: "parent" })
|
||||
const child = await sessions.create({ parentID: parent.id, title: "child" })
|
||||
const grandchild = await sessions.create({ parentID: child.id, title: "grandchild" })
|
||||
|
||||
// grandchild has a text message
|
||||
const gcMsgId = await userMsg(grandchild.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: gcMsgId,
|
||||
sessionID: grandchild.id,
|
||||
@@ -158,7 +172,7 @@ describe("Session.fork child session remapping", () => {
|
||||
|
||||
// child references grandchild via task part
|
||||
const childUserMsg = await userMsg(child.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: childUserMsg,
|
||||
sessionID: child.id,
|
||||
@@ -166,7 +180,7 @@ describe("Session.fork child session remapping", () => {
|
||||
text: "question",
|
||||
} as MessageV2.TextPart)
|
||||
const childAsstMsg = await asstMsg(child.id, childUserMsg)
|
||||
await Session.updatePart(
|
||||
await sessions.updatePart(
|
||||
taskPart({
|
||||
messageID: childAsstMsg,
|
||||
sessionID: child.id,
|
||||
@@ -176,7 +190,7 @@ describe("Session.fork child session remapping", () => {
|
||||
|
||||
// parent references child via task part
|
||||
const parentUserMsg = await userMsg(parent.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: parentUserMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -184,7 +198,7 @@ describe("Session.fork child session remapping", () => {
|
||||
text: "request",
|
||||
} as MessageV2.TextPart)
|
||||
const parentAsstMsg = await asstMsg(parent.id, parentUserMsg)
|
||||
await Session.updatePart(
|
||||
await sessions.updatePart(
|
||||
taskPart({
|
||||
messageID: parentAsstMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -195,7 +209,7 @@ describe("Session.fork child session remapping", () => {
|
||||
const forked = await Session.fork({ sessionID: parent.id })
|
||||
|
||||
// Verify parent-level remap
|
||||
const forkedMsgs = await Session.messages({ sessionID: forked.id })
|
||||
const forkedMsgs = await sessions.messages({ sessionID: forked.id })
|
||||
const tools = forkedMsgs
|
||||
.flatMap((m) => m.parts)
|
||||
.filter((p) => p.type === "tool" && p.tool === "task") as MessageV2.ToolPart[]
|
||||
@@ -203,7 +217,7 @@ describe("Session.fork child session remapping", () => {
|
||||
expect(forkedChildID).not.toBe(child.id)
|
||||
|
||||
// Verify child-level remap (grandchild)
|
||||
const forkedChildMsgs = await Session.messages({ sessionID: SessionID.make(forkedChildID) })
|
||||
const forkedChildMsgs = await sessions.messages({ sessionID: SessionID.make(forkedChildID) })
|
||||
const childTools = forkedChildMsgs
|
||||
.flatMap((m) => m.parts)
|
||||
.filter((p) => p.type === "tool" && p.tool === "task") as MessageV2.ToolPart[]
|
||||
@@ -213,7 +227,7 @@ describe("Session.fork child session remapping", () => {
|
||||
expect(forkedGrandchildID).not.toBe(grandchild.id)
|
||||
|
||||
// Verify grandchild content was copied
|
||||
const gcMsgs = await Session.messages({ sessionID: SessionID.make(forkedGrandchildID) })
|
||||
const gcMsgs = await sessions.messages({ sessionID: SessionID.make(forkedGrandchildID) })
|
||||
expect(gcMsgs).toHaveLength(1)
|
||||
},
|
||||
})
|
||||
@@ -228,9 +242,9 @@ describe("Session.fork child session remapping", () => {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const parent = await Session.create({ title: "parent" })
|
||||
const parent = await sessions.create({ title: "parent" })
|
||||
const parentUserMsg = await userMsg(parent.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: parentUserMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -239,7 +253,7 @@ describe("Session.fork child session remapping", () => {
|
||||
} as MessageV2.TextPart)
|
||||
|
||||
const forked = await Session.fork({ sessionID: parent.id })
|
||||
const forkedMsgs = await Session.messages({ sessionID: forked.id })
|
||||
const forkedMsgs = await sessions.messages({ sessionID: forked.id })
|
||||
expect(forkedMsgs).toHaveLength(1)
|
||||
expect(forkedMsgs[0].parts[0].type).toBe("text")
|
||||
expect((forkedMsgs[0].parts[0] as MessageV2.TextPart).text).toBe("hello")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { ProjectTable } from "../../src/project/project.sql"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { SessionTable } from "../../src/session/session.sql"
|
||||
import { Database, eq } from "../../src/storage/db"
|
||||
@@ -21,7 +21,9 @@ describe("Kilo Session.list", () => {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({ title: "legacy-session" })
|
||||
const session = await Effect.runPromise(
|
||||
Session.Service.use((svc) => svc.create({ title: "legacy-session" })).pipe(Effect.provide(Session.defaultLayer)),
|
||||
)
|
||||
const project = ProjectID.make("legacy-project")
|
||||
Database.use((db) => {
|
||||
db.insert(ProjectTable)
|
||||
@@ -37,7 +39,9 @@ describe("Kilo Session.list", () => {
|
||||
db.update(SessionTable).set({ project_id: project }).where(eq(SessionTable.id, session.id)).run()
|
||||
})
|
||||
|
||||
const sessions = await AppRuntime.runPromise(Session.Service.use((svc) => svc.list({ directory: tmp.path })))
|
||||
const sessions = await Effect.runPromise(
|
||||
Session.Service.use((svc) => svc.list({ directory: tmp.path })).pipe(Effect.provide(Session.defaultLayer)),
|
||||
)
|
||||
const ids = sessions.map((item) => item.id)
|
||||
|
||||
expect(ids).toContain(session.id)
|
||||
|
||||
@@ -17,6 +17,15 @@ import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const sessions = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
}
|
||||
|
||||
function line(input: unknown) {
|
||||
return `data: ${JSON.stringify(input)}\n\n`
|
||||
}
|
||||
@@ -259,14 +268,14 @@ describe("session prompt queue", () => {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({ title: "Queued compaction regression" })
|
||||
const session = await sessions.create({ title: "Queued compaction regression" })
|
||||
const first = MessageID.ascending()
|
||||
const ans = MessageID.ascending()
|
||||
const queued = MessageID.ascending()
|
||||
|
||||
await Session.updateMessage(user(session.id, first).info)
|
||||
await Session.updateMessage(assistant(session.id, ans, first).info)
|
||||
await Session.updateMessage(user(session.id, queued).info)
|
||||
await sessions.updateMessage(user(session.id, first).info)
|
||||
await sessions.updateMessage(assistant(session.id, ans, first).info)
|
||||
await sessions.updateMessage(user(session.id, queued).info)
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
KiloSessionPromptQueue.enqueue(
|
||||
@@ -280,7 +289,7 @@ describe("session prompt queue", () => {
|
||||
auto: true,
|
||||
overflow: true,
|
||||
})
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
const compact = messages.find((msg) => msg.parts.some((part) => part.type === "compaction"))?.info.id
|
||||
return { compact, ids: KiloSessionPromptQueue.scope(session.id, messages).map((item) => item.info.id) }
|
||||
}),
|
||||
@@ -426,7 +435,7 @@ describe("session prompt queue", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
scoped(tmp.path, async (prompt) => {
|
||||
const session = await Session.create({ title: "Queued prompt regression" })
|
||||
const session = await sessions.create({ title: "Queued prompt regression" })
|
||||
const first = Effect.runPromise(
|
||||
prompt.prompt({
|
||||
sessionID: session.id,
|
||||
@@ -457,7 +466,7 @@ describe("session prompt queue", () => {
|
||||
expect(hasText(one, "first reply")).toBe(true)
|
||||
expect(hasText(two, "second reply")).toBe(true)
|
||||
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await sessions.messages({ sessionID: session.id })
|
||||
const users = msgs.filter((msg) => msg.info.role === "user")
|
||||
const assistants = msgs.filter((msg) => msg.info.role === "assistant")
|
||||
const prompts = users.flatMap((msg) =>
|
||||
@@ -548,7 +557,7 @@ describe("session prompt queue", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
scoped(tmp.path, async (prompt) => {
|
||||
const session = await Session.create({ title: "Queued cancel regression" })
|
||||
const session = await sessions.create({ title: "Queued cancel regression" })
|
||||
const first = Effect.runPromise(
|
||||
prompt.prompt({
|
||||
sessionID: session.id,
|
||||
@@ -582,7 +591,7 @@ describe("session prompt queue", () => {
|
||||
|
||||
// The queued prompts must never reach the LLM once cancel flushes the queue.
|
||||
expect(calls).toHaveLength(1)
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await sessions.messages({ sessionID: session.id })
|
||||
const assistants = msgs.filter((msg) => msg.info.role === "assistant")
|
||||
expect(assistants).toHaveLength(1)
|
||||
expect(msgs.filter((msg) => msg.info.role === "user")).toHaveLength(3)
|
||||
@@ -614,7 +623,7 @@ describe("session prompt queue", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
scoped(tmp.path, async (prompt) => {
|
||||
const session = await Session.create({ title: "Suggestion unblock regression" })
|
||||
const session = await sessions.create({ title: "Suggestion unblock regression" })
|
||||
const offShown = Bus.subscribe(Suggestion.Event.Shown, (event) => {
|
||||
if (event.properties.sessionID === session.id) shown.resolve()
|
||||
})
|
||||
@@ -662,7 +671,7 @@ describe("session prompt queue", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
scoped(tmp.path, async (prompt) => {
|
||||
const session = await Session.create({ title: "Question unblock regression" })
|
||||
const session = await sessions.create({ title: "Question unblock regression" })
|
||||
const offAsked = Bus.subscribe(Question.Event.Asked, (event) => {
|
||||
if (event.properties.sessionID === session.id) asked.resolve()
|
||||
})
|
||||
|
||||
@@ -25,8 +25,10 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
function run<A>(body: (snapshot: Snapshot.Interface) => Effect.Effect<A>) {
|
||||
return Effect.runPromise(Snapshot.Service.use(body).pipe(Effect.provide(Snapshot.defaultLayer)))
|
||||
function run<A>(body: (snapshot: Snapshot.Interface) => Effect.Effect<A, never, Session.Service>) {
|
||||
return Effect.runPromise(
|
||||
Snapshot.Service.use(body).pipe(Effect.provide(Snapshot.defaultLayer), Effect.provide(Session.defaultLayer)),
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -55,7 +57,8 @@ test("pathological diffFull workload finishes quickly and does not block abort",
|
||||
fn: () =>
|
||||
run((snapshot) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Effect.promise(() => Session.create({}))
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({})
|
||||
|
||||
const before = yield* snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -7,6 +7,8 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { RemoteSender } from "../../src/kilo-sessions/remote-sender"
|
||||
import { Effect } from "effect"
|
||||
import { Session } from "../../src/session/session"
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(RemoteSender, "create").mockReturnValue({ handle() {}, dispose() {} })
|
||||
@@ -19,6 +21,9 @@ afterEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
const create = (title: string) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create({ title })).pipe(Effect.provide(Session.defaultLayer)))
|
||||
|
||||
describe("experimental.session.list", () => {
|
||||
test("filters sessions by repo worktree family even when project IDs drift", async () => {
|
||||
await using first = await tmpdir({ git: true })
|
||||
@@ -30,12 +35,11 @@ describe("experimental.session.list", () => {
|
||||
|
||||
try {
|
||||
const { Server } = await import("../../src/server/server")
|
||||
const { Session } = await import("../../src/session/session")
|
||||
|
||||
// Create worktree session first so it computes its own project ID via rev-list
|
||||
const branch = await WithInstance.provide({
|
||||
directory: worktree,
|
||||
fn: async () => Session.create({ title: "worktree-session" }),
|
||||
fn: () => create("worktree-session"),
|
||||
})
|
||||
|
||||
// Now write a stale project ID to .git/kilo — this overrides the root's cached ID
|
||||
@@ -48,14 +52,14 @@ describe("experimental.session.list", () => {
|
||||
project: await Server.Default().app.request("/project/current", {
|
||||
headers: { "x-kilo-directory": first.path },
|
||||
}),
|
||||
session: await Session.create({ title: "root-session" }),
|
||||
session: await create("root-session"),
|
||||
}),
|
||||
})
|
||||
await Bun.file(path.join(first.path, ".git", "kilo")).delete()
|
||||
|
||||
await WithInstance.provide({
|
||||
directory: second.path,
|
||||
fn: async () => Session.create({ title: "other-project-session" }),
|
||||
fn: () => create("other-project-session"),
|
||||
})
|
||||
|
||||
const app = root.app
|
||||
@@ -96,11 +100,10 @@ describe("experimental.session.list", () => {
|
||||
|
||||
try {
|
||||
const { Server } = await import("../../src/server/server")
|
||||
const { Session } = await import("../../src/session/session")
|
||||
|
||||
const branch = await WithInstance.provide({
|
||||
directory: worktree,
|
||||
fn: async () => Session.create({ title: "worktree-session" }),
|
||||
fn: () => create("worktree-session"),
|
||||
})
|
||||
|
||||
const root = await WithInstance.provide({
|
||||
@@ -110,13 +113,13 @@ describe("experimental.session.list", () => {
|
||||
project: await Server.Default().app.request("/project/current", {
|
||||
headers: { "x-kilo-directory": first.path },
|
||||
}),
|
||||
session: await Session.create({ title: "root-session" }),
|
||||
session: await create("root-session"),
|
||||
}),
|
||||
})
|
||||
|
||||
await WithInstance.provide({
|
||||
directory: second.path,
|
||||
fn: async () => Session.create({ title: "other-project-session" }),
|
||||
fn: () => create("other-project-session"),
|
||||
})
|
||||
|
||||
const app = root.app
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import { Effect } from "effect"
|
||||
import { Session } from "../../src/session/session"
|
||||
import path from "path"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { RecallTool } from "../../src/tool/recall"
|
||||
@@ -32,6 +33,9 @@ afterEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
const create = (title: string) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create({ title })).pipe(Effect.provide(Session.defaultLayer)))
|
||||
|
||||
describe("tool.recall", () => {
|
||||
test("search is limited to the current project worktrees", async () => {
|
||||
await using first = await tmpdir({ git: true })
|
||||
@@ -43,18 +47,17 @@ describe("tool.recall", () => {
|
||||
await Bun.write(path.join(first.path, ".git", "opencode"), "stale-project-id")
|
||||
|
||||
try {
|
||||
const { Session } = await import("../../src/session/session")
|
||||
await WithInstance.provide({
|
||||
directory: first.path,
|
||||
fn: async () => Session.create({ title: "search-target root" }),
|
||||
fn: () => create("search-target root"),
|
||||
})
|
||||
await WithInstance.provide({
|
||||
directory: worktree,
|
||||
fn: async () => Session.create({ title: "search-target worktree" }),
|
||||
fn: () => create("search-target worktree"),
|
||||
})
|
||||
await WithInstance.provide({
|
||||
directory: second.path,
|
||||
fn: async () => Session.create({ title: "search-target other" }),
|
||||
fn: () => create("search-target other"),
|
||||
})
|
||||
|
||||
const result = await WithInstance.provide({
|
||||
@@ -82,10 +85,9 @@ describe("tool.recall", () => {
|
||||
await using second = await tmpdir({ git: true })
|
||||
|
||||
try {
|
||||
const { Session } = await import("../../src/session/session")
|
||||
const session = await WithInstance.provide({
|
||||
directory: second.path,
|
||||
fn: async () => Session.create({ title: "other-project-session" }),
|
||||
fn: () => create("other-project-session"),
|
||||
})
|
||||
|
||||
const err = await WithInstance.provide({
|
||||
@@ -115,10 +117,9 @@ describe("tool.recall", () => {
|
||||
await Bun.write(path.join(first.path, ".git", "opencode"), "stale-project-id")
|
||||
|
||||
try {
|
||||
const { Session } = await import("../../src/session/session")
|
||||
const session = await WithInstance.provide({
|
||||
directory: worktree,
|
||||
fn: async () => Session.create({ title: "worktree readable" }),
|
||||
fn: () => create("worktree readable"),
|
||||
})
|
||||
|
||||
const result = await WithInstance.provide({
|
||||
|
||||
Reference in New Issue
Block a user