Files
opencode/packages/core/test/config/compaction.test.ts
T
2026-08-19 21:40:33 -05:00

168 lines
6.2 KiB
TypeScript

import { describe, expect } from "bun:test"
import { LanguageModel, LLMClient, LLMEvent } from "@opencode-ai/ai"
import { OpenAIChat } from "@opencode-ai/ai/protocols"
import { Bus } from "@opencode-ai/core/bus"
import { Config } from "@opencode-ai/core/config"
import { ConfigCompactionPlugin } from "@opencode-ai/core/config/plugin/compaction"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { llmClient } from "@opencode-ai/core/effect/app-node-platform"
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { Session } from "@opencode-ai/core/session"
import { Agent } from "@opencode-ai/core/agent"
import { Location } from "@opencode-ai/core/location"
import { Project } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { ConfigCompaction } from "@opencode-ai/schema/config/compaction"
import { Document, Event, Info } from "@opencode-ai/schema/config"
import { Money } from "@opencode-ai/schema/money"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { DateTime, Effect, Fiber, Layer, Option, Schema, Stream } from "effect"
import { testEffect } from "../lib/effect"
import { host } from "../plugin/host"
const model = LanguageModel.make({
id: "test-model",
provider: "test-provider",
route: OpenAIChat.route,
})
const limit = { context: 100_000, output: 1_000 }
const resolved = SessionRunnerModel.resolved(model, {
capabilities: { tools: true, input: ["text"], output: ["text"] },
cost: [],
limit,
})
const config = Config.testLayer()
const it = testEffect(
Layer.merge(
config,
AppNodeBuilder.build(LayerNode.group([SessionCompaction.node, Config.node, Bus.node]), [
[
llmClient,
Layer.mock(LLMClient.Service)({
stream: () => Stream.make(LLMEvent.textDelta({ id: "summary", text: "summary" })),
}),
],
[
SessionRunnerModel.node,
Layer.mock(SessionRunnerModel.Service)({
resolve: () => Effect.succeed(resolved),
}),
],
[Config.node, config],
]),
),
)
describe("ConfigCompactionPlugin.Plugin", () => {
it.live("merges settings and reloads changed config", () =>
Effect.gen(function* () {
const compaction = yield* SessionCompaction.Service
const config = yield* Config.Test
const bus = yield* Bus.Service
yield* config.setEntries([
new Document({
type: "document",
info: new Info({ compaction: new ConfigCompaction.Info({ auto: false, buffer: 20_000 }) }),
}),
new Document({
type: "document",
info: new Info({
compaction: new ConfigCompaction.Info({
buffer: 10_000,
keep: new ConfigCompaction.Keep({ tokens: 0 }),
}),
}),
}),
])
yield* ConfigCompactionPlugin.Plugin.effect(host({ event: { subscribe: () => bus.subscribe(Event.Updated) } }))
expect(compaction.required(nearInput)).toBe(false)
const started = yield* bus
.subscribe(SessionEvent.Compaction.Started)
.pipe(Stream.runHead, Effect.forkScoped({ startImmediately: true }))
expect(
yield* compaction.compactManual({
session,
messages: [
{
id: SessionMessage.ID.create(),
type: "user",
text: "Older context",
time: { created: DateTime.makeUnsafe(0) },
},
{
id: SessionMessage.ID.create(),
type: "user",
text: "Recent context",
time: { created: DateTime.makeUnsafe(1) },
},
],
inputID: SessionMessage.ID.make("msg_compaction_manual"),
}),
).toEqual({ status: "completed" })
expect(Option.getOrThrow(yield* Fiber.join(started)).data.recent).toContain("Recent context")
yield* config.setEntries([
new Document({
type: "document",
info: new Info({ compaction: new ConfigCompaction.Info({ auto: true, buffer: 20_000 }) }),
}),
new Document({
type: "document",
info: new Info({ compaction: new ConfigCompaction.Info({ buffer: 10_000 }) }),
}),
])
yield* bus.publish(Event.Updated, {})
yield* Effect.gen(function* () {
for (let attempt = 0; attempt < 200; attempt++) {
if (compaction.required(nearInput)) return
yield* Effect.sleep("10 millis")
}
yield* Effect.die(new Error("Timed out waiting for compaction config reload"))
})
expect(compaction.required(bufferedInput)).toBe(false)
yield* config.setEntries([
new Document({
type: "document",
info: new Info({ compaction: new ConfigCompaction.Info({ auto: true, buffer: 20_000 }) }),
}),
])
yield* bus.publish(Event.Updated, {})
for (let attempt = 0; attempt < 200; attempt++) {
if (compaction.required(bufferedInput)) return
yield* Effect.sleep("10 millis")
}
yield* Effect.die(new Error("Timed out waiting for compaction config reload"))
}),
)
})
const session = Session.Info.make({
id: Session.ID.make("ses_compaction_config"),
projectID: Project.ID.global,
cost: Money.USD.zero,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: Location.Ref.make({ directory: AbsolutePath.make("/tmp") }),
})
const input = (tokens: number) => ({
session,
resolved,
messages: [
Schema.decodeUnknownSync(SessionMessage.Assistant)({
id: SessionMessage.ID.make("msg_compaction_config"),
type: "assistant",
agent: Agent.defaultID,
model: { id: "test-model", providerID: "test-provider" },
content: [],
tokens: { input: tokens, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, completed: 0 },
}),
],
})
const bufferedInput = input(85_000)
const nearInput = input(95_000)