opencode/packages/core/test/session-model-request.test.ts
2026-07-31 13:57:04 -05:00

112 lines
3.8 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { Message, ToolResultPart } from "@opencode-ai/ai"
import { boundImages, unsupportedParts } from "@opencode-ai/core/session/model-request"
const capabilities = (input: string[]) => ({ tools: true, input, output: ["text"] })
describe("SessionModelRequest.unsupportedParts", () => {
test("replaces unsupported user media with a visible error", () => {
const messages = unsupportedParts(
[
Message.user([
Message.text("Describe this image"),
{ type: "media", mediaType: "image/png", data: "aGVsbG8=", filename: "logo.png" },
]),
],
capabilities(["text"]),
)
expect(messages[0]?.content).toEqual([
Message.text("Describe this image"),
Message.text('ERROR: Cannot read "logo.png" (this model does not support image input). Inform the user.'),
])
})
test("replaces unsupported media nested in tool results", () => {
const messages = unsupportedParts(
[
Message.tool(
ToolResultPart.make({
id: "call_1",
name: "read",
result: {
type: "content",
value: [
{ type: "text", text: "Image read successfully" },
{ type: "file", uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "logo.png" },
],
},
}),
),
],
capabilities(["text"]),
)
expect(messages[0]?.content[0]).toMatchObject({
type: "tool-result",
result: {
type: "content",
value: [
{ type: "text", text: "Image read successfully" },
{
type: "text",
text: 'ERROR: Cannot read "logo.png" (this model does not support image input). Inform the user.',
},
],
},
})
})
test("preserves supported media", () => {
const message = Message.user({ type: "media", mediaType: "image/png", data: "aGVsbG8=" })
expect(unsupportedParts([message], capabilities(["text", "image"]))[0]?.content).toEqual(message.content)
})
})
describe("SessionModelRequest.boundImages", () => {
test("preserves images below the trigger", () => {
const messages = [Message.user({ type: "media", mediaType: "image/png", data: "aGVsbG8=" })]
expect(boundImages(messages)).toBe(messages)
})
test("replaces oldest images until the retained payload reaches the target", () => {
const image = "a".repeat(9 * 1024 * 1024)
const messages = [
Message.user({ type: "media", mediaType: "image/png", data: image, filename: "first.png" }),
Message.user({ type: "media", mediaType: "image/png", data: image, filename: "second.png" }),
Message.user({ type: "media", mediaType: "image/png", data: image, filename: "third.png" }),
]
const result = boundImages(messages)
expect(result[0]?.content[0]).toMatchObject({ type: "text" })
expect(result[1]?.content[0]).toMatchObject({ type: "text" })
expect(result[2]?.content[0]).toMatchObject({ type: "media", filename: "third.png" })
})
test("replaces images nested in tool results", () => {
const image = "a".repeat(13 * 1024 * 1024)
const result = boundImages([
Message.tool(
ToolResultPart.make({
id: "call_1",
name: "read",
result: {
type: "content",
value: [
{ type: "file", uri: `data:image/png;base64,${image}`, mime: "image/png", name: "first.png" },
{ type: "file", uri: `data:image/png;base64,${image}`, mime: "image/png", name: "second.png" },
],
},
}),
),
])
expect(result[0]?.content[0]).toMatchObject({
type: "tool-result",
result: {
type: "content",
value: [{ type: "text" }, { type: "file", name: "second.png" }],
},
})
})
})