| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751 |
- import { describe, expect, it } from "bun:test"
- import type { AgentSideConnection } from "@agentclientprotocol/sdk"
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import type { Event, Message, OpencodeClient, Part, SessionMessageResponse, ToolPart } from "@kirincode-ai/sdk/v2"
- import { Effect, ManagedRuntime } from "effect"
- import { ACPEvent } from "@/acp/event"
- import * as ACPService from "@/acp/service"
- import { Directory } from "@/acp/directory"
- import { ACPSession } from "@/acp/session"
- type SessionUpdateParams = Parameters<AgentSideConnection["sessionUpdate"]>[0]
- type ToolSessionUpdateParams = SessionUpdateParams & {
- update: Extract<SessionUpdateParams["update"], { sessionUpdate: "tool_call" | "tool_call_update" }>
- }
- type GlobalEventEnvelope = {
- payload?: Event
- }
- type DeltaPartType = Extract<Part, { type: "text" | "reasoning" }>["type"]
- const pollUntil = async (
- check: () => boolean | Promise<boolean>,
- message: string,
- opts?: { timeoutMs?: number; intervalMs?: number },
- ) => {
- const started = Date.now()
- while (true) {
- if (await check()) return
- if (Date.now() - started > (opts?.timeoutMs ?? 2000)) throw new Error(message)
- await new Promise((resolve) => setTimeout(resolve, opts?.intervalMs ?? 5))
- }
- }
- function makeSessionService() {
- return ManagedRuntime.make(LayerNode.compile(ACPSession.node)).runSync(
- ACPSession.Service.use((service) => Effect.succeed(service)),
- )
- }
- function createEventStream() {
- const queue: GlobalEventEnvelope[] = []
- const waiters: Array<(value: GlobalEventEnvelope | undefined) => void> = []
- const state = { closed: false }
- const push = (event: GlobalEventEnvelope) => {
- const waiter = waiters.shift()
- if (waiter) {
- waiter(event)
- return
- }
- queue.push(event)
- }
- const close = () => {
- state.closed = true
- for (const waiter of waiters.splice(0)) {
- waiter(undefined)
- }
- }
- const stream = async function* (signal?: AbortSignal) {
- while (true) {
- if (signal?.aborted) return
- const next = queue.shift()
- if (next) {
- yield next
- continue
- }
- if (state.closed) return
- const value = await new Promise<GlobalEventEnvelope | undefined>((resolve) => {
- waiters.push(resolve)
- signal?.addEventListener("abort", () => resolve(undefined), { once: true })
- })
- if (!value) return
- yield value
- }
- }
- return { push, close, stream }
- }
- function createHarness(messages: Record<string, SessionMessageResponse> = {}) {
- const updates: SessionUpdateParams[] = []
- const calls = {
- eventSubscribe: 0,
- message: 0,
- }
- const events = createEventStream()
- const sdk = {
- global: {
- event: (options?: { signal?: AbortSignal }) => {
- calls.eventSubscribe++
- return Promise.resolve({ stream: events.stream(options?.signal) })
- },
- },
- session: {
- message: (input: { messageID: string }) => {
- calls.message++
- return Promise.resolve({ data: messages[input.messageID] })
- },
- get: () => Promise.resolve({ data: { id: "ses_loaded" } }),
- messages: () => Promise.resolve({ data: [] }),
- },
- } as unknown as OpencodeClient
- const connection = {
- sessionUpdate: (params: SessionUpdateParams) => {
- updates.push(params)
- return Promise.resolve()
- },
- } satisfies Pick<AgentSideConnection, "sessionUpdate">
- const session = makeSessionService()
- const subscription = new ACPEvent.Subscription({ sdk, connection, session })
- return { calls, connection, events, sdk, session, subscription, updates }
- }
- function textDelta(sessionID: string, messageID: string, partID: string, delta: string): Event {
- return {
- id: `evt_${sessionID}_${messageID}_${partID}_${delta}`,
- type: "message.part.delta",
- properties: {
- sessionID,
- messageID,
- partID,
- field: "text",
- delta,
- },
- }
- }
- function partUpdated(sessionID: string, messageID: string, partID: string, type: DeltaPartType): Event {
- return {
- id: `evt_${sessionID}_${messageID}_${partID}`,
- type: "message.part.updated",
- properties: {
- sessionID,
- time: Date.now(),
- part:
- type === "text"
- ? {
- id: partID,
- sessionID,
- messageID,
- type: "text",
- text: "",
- }
- : {
- id: partID,
- sessionID,
- messageID,
- type: "reasoning",
- text: "",
- time: { start: Date.now() },
- },
- },
- }
- }
- function toolUpdated(part: ToolPart): Event {
- return {
- id: `evt_${part.sessionID}_${part.messageID}_${part.id}_${part.state.status}`,
- type: "message.part.updated",
- properties: {
- sessionID: part.sessionID,
- time: Date.now(),
- part,
- },
- }
- }
- function assistantMessage(sessionID: string, messageID: string, partID: string, type: DeltaPartType) {
- return {
- info: {
- id: messageID,
- sessionID,
- role: "assistant",
- time: { created: Date.now() },
- parentID: "msg_parent",
- modelID: "model",
- providerID: "provider",
- mode: "build",
- agent: "build",
- path: { cwd: "/workspace", root: "/workspace" },
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- },
- parts: [
- type === "text"
- ? {
- id: partID,
- sessionID,
- messageID,
- type: "text",
- text: "",
- }
- : {
- id: partID,
- sessionID,
- messageID,
- type: "reasoning",
- text: "",
- time: { start: Date.now() },
- },
- ],
- } satisfies SessionMessageResponse
- }
- function assistantToolMessage(part: ToolPart) {
- return {
- info: {
- id: part.messageID,
- sessionID: part.sessionID,
- role: "assistant",
- time: { created: Date.now() },
- parentID: "msg_parent",
- modelID: "model",
- providerID: "provider",
- mode: "build",
- agent: "build",
- path: { cwd: "/workspace", root: "/workspace" },
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- },
- parts: [part],
- } satisfies SessionMessageResponse
- }
- function runningTool(
- sessionID: string,
- callID: string,
- output?: string,
- input: Record<string, unknown> = { cmd: "printf hello" },
- ) {
- return {
- id: `part_${callID}`,
- sessionID,
- messageID: `msg_${callID}`,
- type: "tool",
- callID,
- tool: "bash",
- state: {
- status: "running",
- input,
- title: "bash",
- ...(output !== undefined ? { metadata: { output } } : {}),
- time: { start: Date.now() },
- },
- } satisfies ToolPart
- }
- function completedTool(
- sessionID: string,
- callID: string,
- output = "done",
- attachments: Extract<ToolPart["state"], { status: "completed" }>["attachments"] = [],
- options: {
- readonly tool?: string
- readonly input?: Record<string, unknown>
- readonly metadata?: Record<string, unknown>
- } = {},
- ) {
- return {
- id: `part_${callID}`,
- sessionID,
- messageID: `msg_${callID}`,
- type: "tool",
- callID,
- tool: options.tool ?? "bash",
- state: {
- status: "completed",
- input: options.input ?? { cmd: "printf done" },
- output,
- title: "bash",
- metadata: options.metadata ?? { exit: 0 },
- time: { start: Date.now() - 1, end: Date.now() },
- ...(attachments.length ? { attachments } : {}),
- },
- } satisfies ToolPart
- }
- function errorTool(sessionID: string, callID: string) {
- return {
- id: `part_${callID}`,
- sessionID,
- messageID: `msg_${callID}`,
- type: "tool",
- callID,
- tool: "bash",
- state: {
- status: "error",
- input: { cmd: "exit 1" },
- error: "failed hard",
- metadata: { exit: 1 },
- time: { start: Date.now() - 1, end: Date.now() },
- },
- } satisfies ToolPart
- }
- function toolUpdates(updates: SessionUpdateParams[]) {
- return updates.filter((item): item is ToolSessionUpdateParams => {
- return item.update.sessionUpdate === "tool_call" || item.update.sessionUpdate === "tool_call_update"
- })
- }
- async function createKnownSession(
- session: ACPSession.Interface,
- sessionId: string,
- part: { messageId: string; partId: string; partType: Part["type"]; role?: Message["role"] },
- ) {
- await Effect.runPromise(session.create({ id: sessionId, cwd: "/workspace" }))
- await Effect.runPromise(
- session.recordPartMetadata({
- sessionId,
- messageId: part.messageId,
- partId: part.partId,
- partType: part.partType,
- role: part.role ?? "assistant",
- }),
- )
- }
- describe("acp event routing", () => {
- it("routes message.part.delta by sessionID without cross-session pollution", async () => {
- const harness = createHarness()
- await createKnownSession(harness.session, "ses_a", { messageId: "msg_a", partId: "part_a", partType: "text" })
- await createKnownSession(harness.session, "ses_b", { messageId: "msg_b", partId: "part_b", partType: "text" })
- await harness.subscription.handle(textDelta("ses_b", "msg_b", "part_b", "hello"))
- expect(harness.updates.map((update) => update.sessionId)).toEqual(["ses_b"])
- expect(harness.updates[0]?.update.sessionUpdate).toBe("agent_message_chunk")
- })
- it("keeps interleaved sessions isolated for text and reasoning deltas", async () => {
- const harness = createHarness()
- await createKnownSession(harness.session, "ses_a", { messageId: "msg_a", partId: "part_a", partType: "text" })
- await createKnownSession(harness.session, "ses_b", {
- messageId: "msg_b",
- partId: "part_b",
- partType: "reasoning",
- })
- await harness.subscription.handle(textDelta("ses_a", "msg_a", "part_a", "A1"))
- await harness.subscription.handle(textDelta("ses_b", "msg_b", "part_b", "B1"))
- await harness.subscription.handle(textDelta("ses_a", "msg_a", "part_a", "A2"))
- await harness.subscription.handle(textDelta("ses_b", "msg_b", "part_b", "B2"))
- expect(
- harness.updates.filter((update) => update.sessionId === "ses_a").map((update) => update.update.sessionUpdate),
- ).toEqual(["agent_message_chunk", "agent_message_chunk"])
- expect(
- harness.updates.filter((update) => update.sessionId === "ses_b").map((update) => update.update.sessionUpdate),
- ).toEqual(["agent_thought_chunk", "agent_thought_chunk"])
- })
- it("does not create extra subscriptions on repeated loadSession", async () => {
- const harness = createHarness()
- let subscription: ACPEvent.Subscription | undefined
- const service = ACPService.make({
- sdk: harness.sdk,
- connection: harness.connection,
- directory: {
- get: () =>
- Effect.succeed(
- Directory.build({
- directory: "/workspace",
- providers: {},
- modes: [],
- defaultModeID: "build",
- commands: [],
- }),
- ),
- refresh: () =>
- Effect.succeed(
- Directory.build({
- directory: "/workspace",
- providers: {},
- modes: [],
- defaultModeID: "build",
- commands: [],
- }),
- ),
- variants: Directory.variants,
- },
- session: harness.session,
- eventSubscription: (started) => {
- subscription = started
- },
- })
- await pollUntil(() => harness.calls.eventSubscribe === 1, "event subscription did not start")
- await Effect.runPromise(service.loadSession({ cwd: "/workspace", sessionId: "ses_loaded", mcpServers: [] }))
- await Effect.runPromise(service.loadSession({ cwd: "/workspace", sessionId: "ses_loaded", mcpServers: [] }))
- await Effect.runPromise(service.loadSession({ cwd: "/workspace", sessionId: "ses_loaded", mcpServers: [] }))
- expect(harness.calls.eventSubscribe).toBe(1)
- subscription?.stop()
- harness.events.close()
- })
- it("does not call sdk.session.message repeatedly when metadata is known", async () => {
- const harness = createHarness()
- await createKnownSession(harness.session, "ses_a", { messageId: "msg_a", partId: "part_a", partType: "text" })
- for (const delta of ["a", "b", "c", "d", "e"]) {
- await harness.subscription.handle(textDelta("ses_a", "msg_a", "part_a", delta))
- }
- expect(harness.calls.message).toBe(0)
- expect(harness.updates).toHaveLength(5)
- })
- it("fetches unknown part metadata once and reuses it for later deltas", async () => {
- const harness = createHarness({
- msg_a: assistantMessage("ses_a", "msg_a", "part_a", "text"),
- })
- await Effect.runPromise(harness.session.create({ id: "ses_a", cwd: "/workspace" }))
- await harness.subscription.handle(partUpdated("ses_a", "msg_a", "part_a", "text"))
- await harness.subscription.handle(textDelta("ses_a", "msg_a", "part_a", "a"))
- await harness.subscription.handle(textDelta("ses_a", "msg_a", "part_a", "b"))
- expect(harness.calls.message).toBe(1)
- expect(harness.updates).toHaveLength(2)
- })
- it("replays loaded session messages sequentially and continues after update failures", async () => {
- const events = createEventStream()
- const updates: SessionUpdateParams[] = []
- const connection = {
- sessionUpdate: (params: SessionUpdateParams) => {
- if (params.update.sessionUpdate === "tool_call" && params.update.toolCallId === "call_slow") {
- return new Promise<void>((resolve) => {
- setTimeout(() => {
- updates.push(params)
- resolve()
- }, 20)
- })
- }
- if (params.update.sessionUpdate === "tool_call_update" && params.update.toolCallId === "call_slow") {
- return Promise.reject(new Error("replay send failed"))
- }
- updates.push(params)
- return Promise.resolve()
- },
- } satisfies Pick<AgentSideConnection, "sessionUpdate">
- let subscription: ACPEvent.Subscription | undefined
- const service = ACPService.make({
- sdk: {
- global: {
- event: (options?: { signal?: AbortSignal }) => Promise.resolve({ stream: events.stream(options?.signal) }),
- },
- session: {
- get: () => Promise.resolve({ data: { id: "ses_loaded" } }),
- messages: () =>
- Promise.resolve({
- data: [
- assistantToolMessage(completedTool("ses_loaded", "call_slow", "slow")),
- assistantToolMessage(completedTool("ses_loaded", "call_after", "after")),
- ],
- }),
- },
- } as unknown as OpencodeClient,
- connection,
- directory: {
- get: () =>
- Effect.succeed(
- Directory.build({
- directory: "/workspace",
- providers: {},
- modes: [],
- defaultModeID: "build",
- commands: [],
- }),
- ),
- refresh: () =>
- Effect.succeed(
- Directory.build({
- directory: "/workspace",
- providers: {},
- modes: [],
- defaultModeID: "build",
- commands: [],
- }),
- ),
- variants: Directory.variants,
- },
- eventSubscription: (started) => {
- subscription = started
- },
- })
- await Effect.runPromise(service.loadSession({ cwd: "/workspace", sessionId: "ses_loaded", mcpServers: [] }))
- expect(toolUpdates(updates).map((item) => item.update.toolCallId)).toEqual([
- "call_slow",
- "call_after",
- "call_after",
- ])
- subscription?.stop()
- events.close()
- })
- it("ignores unknown sessions and live user parts without user_message_chunk duplication", async () => {
- const harness = createHarness()
- await createKnownSession(harness.session, "ses_user", {
- messageId: "msg_user",
- partId: "part_user",
- partType: "text",
- role: "user",
- })
- await harness.subscription.handle(textDelta("ses_missing", "msg_missing", "part_missing", "ignored"))
- await harness.subscription.handle(partUpdated("ses_user", "msg_user", "part_live", "text"))
- await harness.subscription.handle(textDelta("ses_user", "msg_user", "part_user", "hello"))
- expect(harness.updates).toHaveLength(0)
- })
- it("exposes the shell command on the synthetic pending tool call", async () => {
- const harness = createHarness()
- await Effect.runPromise(harness.session.create({ id: "ses_tool", cwd: "/workspace" }))
- await harness.subscription.handle(toolUpdated(runningTool("ses_tool", "call_1", "hello")))
- expect(toolUpdates(harness.updates).map((item) => item.update.sessionUpdate)).toEqual([
- "tool_call",
- "tool_call_update",
- ])
- expect(harness.updates[0]?.update).toMatchObject({
- status: "pending",
- toolCallId: "call_1",
- title: "printf hello",
- kind: "execute",
- locations: [{ path: "/workspace" }],
- rawInput: { cmd: "printf hello", cwd: "/workspace" },
- })
- expect(harness.updates[1]?.update).toMatchObject({ status: "in_progress", toolCallId: "call_1" })
- })
- it("includes available input in the synthetic pending tool call", async () => {
- const harness = createHarness()
- await Effect.runPromise(harness.session.create({ id: "ses_pending_input", cwd: "/workspace" }))
- await harness.subscription.handle(
- toolUpdated({
- id: "part_call_read",
- sessionID: "ses_pending_input",
- messageID: "msg_call_read",
- type: "tool",
- callID: "call_read",
- tool: "read",
- state: {
- status: "running",
- input: { filePath: "/workspace/file.ts" },
- title: "Read file.ts",
- time: { start: Date.now() },
- },
- } satisfies ToolPart),
- )
- expect(harness.updates[0]?.update).toMatchObject({
- sessionUpdate: "tool_call",
- toolCallId: "call_read",
- status: "pending",
- title: "Read file.ts",
- kind: "read",
- rawInput: { filePath: "/workspace/file.ts" },
- locations: [{ path: "/workspace/file.ts" }],
- })
- })
- it("does not emit duplicate synthetic pending after a replayed running tool", async () => {
- const harness = createHarness()
- await Effect.runPromise(harness.session.create({ id: "ses_replay", cwd: "/workspace" }))
- await harness.subscription.replayMessage(assistantToolMessage(runningTool("ses_replay", "call_replay", "first")))
- await harness.subscription.handle(toolUpdated(runningTool("ses_replay", "call_replay", "second")))
- expect(toolUpdates(harness.updates).filter((item) => item.update.sessionUpdate === "tool_call")).toHaveLength(1)
- expect(toolUpdates(harness.updates).map((item) => item.update.sessionUpdate)).toEqual([
- "tool_call",
- "tool_call_update",
- "tool_call_update",
- ])
- })
- it("dedupes shell output snapshots while still sending status-only running updates", async () => {
- const harness = createHarness()
- await Effect.runPromise(harness.session.create({ id: "ses_shell", cwd: "/workspace" }))
- await harness.subscription.handle(toolUpdated(runningTool("ses_shell", "call_shell", "same")))
- await harness.subscription.handle(toolUpdated(runningTool("ses_shell", "call_shell", "same")))
- const updates = toolUpdates(harness.updates)
- expect(updates).toHaveLength(3)
- expect(updates[1]?.update).toMatchObject({
- sessionUpdate: "tool_call_update",
- content: [{ type: "content", content: { type: "text", text: "same" } }],
- })
- expect(updates[2]?.update).toMatchObject({ sessionUpdate: "tool_call_update", status: "in_progress" })
- expect("content" in updates[2]!.update).toBe(false)
- })
- it("clears shell snapshot marker when a tool returns to pending", async () => {
- const harness = createHarness()
- await Effect.runPromise(harness.session.create({ id: "ses_pending", cwd: "/workspace" }))
- await harness.subscription.handle(toolUpdated(runningTool("ses_pending", "call_pending", "repeat")))
- await harness.subscription.handle(
- toolUpdated({
- id: "part_call_pending",
- sessionID: "ses_pending",
- messageID: "msg_call_pending",
- type: "tool",
- callID: "call_pending",
- tool: "bash",
- state: {
- status: "pending",
- input: { cmd: "printf repeat" },
- raw: '{"cmd":"printf repeat"}',
- },
- }),
- )
- await harness.subscription.handle(toolUpdated(runningTool("ses_pending", "call_pending", "repeat")))
- expect(
- toolUpdates(harness.updates)
- .filter((item) => item.update.sessionUpdate === "tool_call_update")
- .map((item) => ("content" in item.update ? item.update.content : undefined)),
- ).toEqual([
- [{ type: "content", content: { type: "text", text: "repeat" } }],
- [{ type: "content", content: { type: "text", text: "repeat" } }],
- ])
- })
- it("emits completed tool output and rawOutput", async () => {
- const harness = createHarness()
- await Effect.runPromise(harness.session.create({ id: "ses_done", cwd: "/workspace" }))
- await harness.subscription.handle(toolUpdated(completedTool("ses_done", "call_done", "finished")))
- expect(harness.updates.at(-1)?.update).toMatchObject({
- sessionUpdate: "tool_call_update",
- toolCallId: "call_done",
- status: "completed",
- content: [{ type: "content", content: { type: "text", text: "finished" } }],
- rawOutput: { output: "finished", metadata: { exit: 0 } },
- })
- })
- it("emits clean read display content and preserves rawOutput", async () => {
- const harness = createHarness()
- await Effect.runPromise(harness.session.create({ id: "ses_read", cwd: "/workspace" }))
- const output = [
- "<path>/workspace/file.ts</path>",
- "<type>file</type>",
- "<content>",
- "1: import { value } from './value'",
- "2: export { value }",
- "",
- "(End of file - total 2 lines)",
- "</content>",
- ].join("\n")
- const metadata = {
- display: {
- type: "file",
- path: "/workspace/file.ts",
- text: "import { value } from './value'\nexport { value }",
- lineStart: 1,
- lineEnd: 2,
- totalLines: 2,
- truncated: false,
- },
- }
- await harness.subscription.handle(
- toolUpdated(
- completedTool("ses_read", "call_read", output, [], {
- tool: "read",
- input: { filePath: "/workspace/file.ts" },
- metadata,
- }),
- ),
- )
- expect(harness.updates.at(-1)?.update).toMatchObject({
- sessionUpdate: "tool_call_update",
- toolCallId: "call_read",
- status: "completed",
- content: [
- {
- type: "content",
- content: { type: "text", text: "import { value } from './value'\nexport { value }" },
- },
- ],
- rawOutput: { output, metadata },
- })
- })
- it("emits error tool output", async () => {
- const harness = createHarness()
- await Effect.runPromise(harness.session.create({ id: "ses_error", cwd: "/workspace" }))
- await harness.subscription.handle(toolUpdated(errorTool("ses_error", "call_error")))
- expect(harness.updates.at(-1)?.update).toMatchObject({
- sessionUpdate: "tool_call_update",
- toolCallId: "call_error",
- status: "failed",
- content: [{ type: "content", content: { type: "text", text: "failed hard" } }],
- rawOutput: { error: "failed hard", metadata: { exit: 1 } },
- })
- })
- it("emits image attachments as ACP image content for live and replayed completed tool updates", async () => {
- const harness = createHarness()
- const image = Buffer.from("image-data").toString("base64")
- const attachment = {
- id: "file_image",
- sessionID: "ses_image",
- messageID: "msg_image",
- type: "file",
- mime: "image/png",
- filename: "image.png",
- url: `data:image/png;base64,${image}`,
- } as const
- await Effect.runPromise(harness.session.create({ id: "ses_image", cwd: "/workspace" }))
- await harness.subscription.handle(toolUpdated(completedTool("ses_image", "call_live", "live", [attachment])))
- await harness.subscription.replayMessage(
- assistantToolMessage(completedTool("ses_image", "call_replayed", "replayed", [attachment])),
- )
- expect(
- toolUpdates(harness.updates)
- .filter((item) => item.update.sessionUpdate === "tool_call_update" && item.update.status === "completed")
- .map((item) => ("content" in item.update ? item.update.content : [])),
- ).toEqual([
- [
- { type: "content", content: { type: "text", text: "live" } },
- { type: "content", content: { type: "image", mimeType: "image/png", data: image } },
- ],
- [
- { type: "content", content: { type: "text", text: "replayed" } },
- { type: "content", content: { type: "image", mimeType: "image/png", data: image } },
- ],
- ])
- })
- })
|