opencode/packages/cli/test/acp/compaction.test.ts
2026-10-20 00:42:13 -05:00

94 lines
4.1 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import type { CompactionUpdate, SessionNotification } from "@agentclientprotocol/sdk"
import type { OpenCodeEventEncoded } from "@opencode/protocol/groups/event"
import { Schema } from "effect"
import { durableEvent, enqueued, ephemeralEvent, startSession, turn, type InitializeOptions } from "./wire-fixture"
const summary = "Summary of the earlier conversation"
const decodeCompact = Schema.decodeUnknownSync(Schema.Struct({ id: Schema.String }))
describe("acp standard compaction updates over the wire", () => {
test("streams a /compact turn's summary and completes it with the full summary", async () => {
const compacted = await compactTurn(
(sessionID, id) => [
durableEvent("session.compaction.started", { sessionID, reason: "manual", inputID: id }),
ephemeralEvent("session.compaction.delta", { sessionID, text: "Summary of " }),
ephemeralEvent("session.compaction.delta", { sessionID, text: "the earlier conversation" }),
durableEvent("session.compaction.ended", { sessionID, reason: "manual", text: summary }),
],
{ compaction: {} },
)
await using acp = compacted.acp
expect(turnUpdates(acp.updates)).toEqual([
compaction(acp.sessionId, { compactionId: compacted.id, status: "in_progress" }),
summaryChunk(acp.sessionId, compacted.id, "Summary of "),
summaryChunk(acp.sessionId, compacted.id, "the earlier conversation"),
compaction(acp.sessionId, {
compactionId: compacted.id,
status: "completed",
summary: [{ type: "text", text: summary }],
}),
])
expect(compacted.response.stopReason).toBe("end_turn")
})
test("treats a null compaction capability as unsupported", async () => {
const compacted = await compactTurn(
(sessionID, id) => [
durableEvent("session.compaction.started", { sessionID, reason: "manual", inputID: id }),
ephemeralEvent("session.compaction.delta", { sessionID, text: summary }),
durableEvent("session.compaction.ended", { sessionID, reason: "manual", text: summary }),
],
{ compaction: null },
)
await using acp = compacted.acp
expect(turnUpdates(acp.updates)).toEqual([
marker(acp.sessionId, { status: "started", messageId: compacted.id, reason: "manual" }),
marker(acp.sessionId, { status: "completed", messageId: compacted.id, reason: "manual" }),
])
})
})
// Holds the compact response so the test can publish the turn's events while the request is in flight.
async function compactTurn(
events: (sessionID: string, id: string) => OpenCodeEventEncoded[],
capabilities: InitializeOptions,
) {
const held = Promise.withResolvers<Response>()
const acp = await startSession({
capabilities,
fetch: (request) => (request.path.endsWith("/compact") ? held.promise : undefined),
})
const response = acp.prompt(acp.sessionId, "/compact")
const request = await acp.until(
() => acp.server.requests.find((item) => item.path.endsWith("/compact")),
"compact request",
)
const id = decodeCompact(request.body).id
acp.server.send(...turn(acp.sessionId, id, ...events(acp.sessionId, id)))
held.resolve(enqueued(acp.sessionId, id, "compaction", {}))
return { acp, id, response: await response }
}
function marker(sessionId: string, value: Record<string, unknown>): SessionNotification {
return { sessionId, update: { sessionUpdate: "session_info_update", _meta: { "opencode/compaction": value } } }
}
function compaction(sessionId: string, update: CompactionUpdate): SessionNotification {
return { sessionId, update: { sessionUpdate: "compaction_update", ...update } }
}
function summaryChunk(sessionId: string, compactionId: string, text: string): SessionNotification {
return {
sessionId,
update: { sessionUpdate: "compaction_summary_chunk", compactionId, content: { type: "text", text } },
}
}
function turnUpdates(updates: readonly SessionNotification[]) {
return updates.filter(
(item) => item.update.sessionUpdate !== "available_commands_update" && item.update.sessionUpdate !== "usage_update",
)
}