tf_code/packages/opencode/test/session/message-v2.test.ts

841 lines
20 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { MessageV2 } from "../../src/session/message-v2"
import type { Provider } from "../../src/provider/provider"
const sessionID = "session"
const model: Provider.Model = {
id: "test-model",
providerID: "test",
api: {
id: "test-model",
url: "https://example.com",
npm: "@ai-sdk/openai",
},
name: "Test Model",
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: {
text: true,
audio: false,
image: false,
video: false,
pdf: false,
},
output: {
text: true,
audio: false,
image: false,
video: false,
pdf: false,
},
interleaved: false,
},
cost: {
input: 0,
output: 0,
cache: {
read: 0,
write: 0,
},
},
limit: {
context: 0,
input: 0,
output: 0,
},
status: "active",
options: {},
headers: {},
release_date: "2026-01-01",
}
function userInfo(id: string): MessageV2.User {
return {
id,
sessionID,
role: "user",
time: { created: 0 },
agent: "user",
model: { providerID: "test", modelID: "test" },
tools: {},
mode: "",
} as unknown as MessageV2.User
}
function assistantInfo(
id: string,
parentID: string,
error?: MessageV2.Assistant["error"],
meta?: { providerID: string; modelID: string },
): MessageV2.Assistant {
const infoModel = meta ?? { providerID: model.providerID, modelID: model.api.id }
return {
id,
sessionID,
role: "assistant",
time: { created: 0 },
error,
parentID,
modelID: infoModel.modelID,
providerID: infoModel.providerID,
mode: "",
agent: "agent",
path: { cwd: "/", root: "/" },
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
} as unknown as MessageV2.Assistant
}
function basePart(messageID: string, id: string) {
return {
id,
sessionID,
messageID,
}
}
describe("session.message-v2.toModelMessage", () => {
test("filters out messages with no parts", () => {
const input: MessageV2.WithParts[] = [
{
info: userInfo("m-empty"),
parts: [],
},
{
info: userInfo("m-user"),
parts: [
{
...basePart("m-user", "p1"),
type: "text",
text: "hello",
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "user",
content: [{ type: "text", text: "hello" }],
},
])
})
test("filters out messages with only ignored parts", () => {
const messageID = "m-user"
const input: MessageV2.WithParts[] = [
{
info: userInfo(messageID),
parts: [
{
...basePart(messageID, "p1"),
type: "text",
text: "ignored",
ignored: true,
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
})
test("includes synthetic text parts", () => {
const messageID = "m-user"
const input: MessageV2.WithParts[] = [
{
info: userInfo(messageID),
parts: [
{
...basePart(messageID, "p1"),
type: "text",
text: "hello",
synthetic: true,
},
] as MessageV2.Part[],
},
{
info: assistantInfo("m-assistant", messageID),
parts: [
{
...basePart("m-assistant", "a1"),
type: "text",
text: "assistant",
synthetic: true,
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "user",
content: [{ type: "text", text: "hello" }],
},
{
role: "assistant",
content: [{ type: "text", text: "assistant" }],
},
])
})
test("converts user text/file parts and injects compaction/subtask prompts", () => {
const messageID = "m-user"
const input: MessageV2.WithParts[] = [
{
info: userInfo(messageID),
parts: [
{
...basePart(messageID, "p1"),
type: "text",
text: "hello",
},
{
...basePart(messageID, "p2"),
type: "text",
text: "ignored",
ignored: true,
},
{
...basePart(messageID, "p3"),
type: "file",
mime: "image/png",
filename: "img.png",
url: "https://example.com/img.png",
},
{
...basePart(messageID, "p4"),
type: "file",
mime: "text/plain",
filename: "note.txt",
url: "https://example.com/note.txt",
},
{
...basePart(messageID, "p5"),
type: "file",
mime: "application/x-directory",
filename: "dir",
url: "https://example.com/dir",
},
{
...basePart(messageID, "p6"),
type: "compaction",
auto: true,
},
{
...basePart(messageID, "p7"),
type: "subtask",
prompt: "prompt",
description: "desc",
agent: "agent",
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "user",
content: [
{ type: "text", text: "hello" },
{
type: "file",
mediaType: "image/png",
filename: "img.png",
data: "https://example.com/img.png",
},
{ type: "text", text: "What did we do so far?" },
{ type: "text", text: "The following tool was executed by the user" },
],
},
])
})
test("converts assistant tool completion into tool-call + tool-result messages with attachments", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: MessageV2.WithParts[] = [
{
info: userInfo(userID),
parts: [
{
...basePart(userID, "u1"),
type: "text",
text: "run tool",
},
] as MessageV2.Part[],
},
{
info: assistantInfo(assistantID, userID),
parts: [
{
...basePart(assistantID, "a1"),
type: "text",
text: "done",
metadata: { openai: { assistant: "meta" } },
},
{
...basePart(assistantID, "a2"),
type: "tool",
callID: "call-1",
tool: "bash",
state: {
status: "completed",
input: { cmd: "ls" },
output: "ok",
title: "Bash",
metadata: {},
time: { start: 0, end: 1 },
attachments: [
{
...basePart(assistantID, "file-1"),
type: "file",
mime: "image/png",
filename: "attachment.png",
url: "data:image/png;base64,Zm9v",
},
],
},
metadata: { openai: { tool: "meta" } },
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "user",
content: [{ type: "text", text: "run tool" }],
},
{
role: "assistant",
content: [
{ type: "text", text: "done", providerOptions: { openai: { assistant: "meta" } } },
{
type: "tool-call",
toolCallId: "call-1",
toolName: "bash",
input: { cmd: "ls" },
providerExecuted: undefined,
providerOptions: { openai: { tool: "meta" } },
},
],
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-1",
toolName: "bash",
output: {
type: "content",
value: [
{ type: "text", text: "ok" },
{ type: "media", mediaType: "image/png", data: "Zm9v" },
],
},
providerOptions: { openai: { tool: "meta" } },
},
],
},
])
})
test("omits provider metadata when assistant model differs", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: MessageV2.WithParts[] = [
{
info: userInfo(userID),
parts: [
{
...basePart(userID, "u1"),
type: "text",
text: "run tool",
},
] as MessageV2.Part[],
},
{
info: assistantInfo(assistantID, userID, undefined, { providerID: "other", modelID: "other" }),
parts: [
{
...basePart(assistantID, "a1"),
type: "text",
text: "done",
metadata: { openai: { assistant: "meta" } },
},
{
...basePart(assistantID, "a2"),
type: "tool",
callID: "call-1",
tool: "bash",
state: {
status: "completed",
input: { cmd: "ls" },
output: "ok",
title: "Bash",
metadata: {},
time: { start: 0, end: 1 },
},
metadata: { openai: { tool: "meta" } },
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "user",
content: [{ type: "text", text: "run tool" }],
},
{
role: "assistant",
content: [
{ type: "text", text: "done" },
{
type: "tool-call",
toolCallId: "call-1",
toolName: "bash",
input: { cmd: "ls" },
providerExecuted: undefined,
},
],
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-1",
toolName: "bash",
output: { type: "text", value: "ok" },
},
],
},
])
})
test("replaces compacted tool output with placeholder", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: MessageV2.WithParts[] = [
{
info: userInfo(userID),
parts: [
{
...basePart(userID, "u1"),
type: "text",
text: "run tool",
},
] as MessageV2.Part[],
},
{
info: assistantInfo(assistantID, userID),
parts: [
{
...basePart(assistantID, "a1"),
type: "tool",
callID: "call-1",
tool: "bash",
state: {
status: "completed",
input: { cmd: "ls" },
output: "this should be cleared",
title: "Bash",
metadata: {},
time: { start: 0, end: 1, compacted: 1 },
},
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "user",
content: [{ type: "text", text: "run tool" }],
},
{
role: "assistant",
content: [
{
type: "tool-call",
toolCallId: "call-1",
toolName: "bash",
input: { cmd: "ls" },
providerExecuted: undefined,
},
],
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-1",
toolName: "bash",
output: { type: "text", value: "[Old tool result content cleared]" },
},
],
},
])
})
test("converts assistant tool error into error-text tool result", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: MessageV2.WithParts[] = [
{
info: userInfo(userID),
parts: [
{
...basePart(userID, "u1"),
type: "text",
text: "run tool",
},
] as MessageV2.Part[],
},
{
info: assistantInfo(assistantID, userID),
parts: [
{
...basePart(assistantID, "a1"),
type: "tool",
callID: "call-1",
tool: "bash",
state: {
status: "error",
input: { cmd: "ls" },
error: "nope",
time: { start: 0, end: 1 },
metadata: {},
},
metadata: { openai: { tool: "meta" } },
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "user",
content: [{ type: "text", text: "run tool" }],
},
{
role: "assistant",
content: [
{
type: "tool-call",
toolCallId: "call-1",
toolName: "bash",
input: { cmd: "ls" },
providerExecuted: undefined,
providerOptions: { openai: { tool: "meta" } },
},
],
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-1",
toolName: "bash",
output: { type: "error-text", value: "nope" },
providerOptions: { openai: { tool: "meta" } },
},
],
},
])
})
test("filters assistant messages with non-abort errors", () => {
const assistantID = "m-assistant"
const input: MessageV2.WithParts[] = [
{
info: assistantInfo(
assistantID,
"m-parent",
new MessageV2.APIError({ message: "boom", isRetryable: true }).toObject() as MessageV2.APIError,
),
parts: [
{
...basePart(assistantID, "a1"),
type: "text",
text: "should not render",
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
})
test("includes aborted assistant messages only when they have non-step-start/reasoning content", () => {
const assistantID1 = "m-assistant-1"
const assistantID2 = "m-assistant-2"
const aborted = new MessageV2.AbortedError({ message: "aborted" }).toObject() as MessageV2.Assistant["error"]
const input: MessageV2.WithParts[] = [
{
info: assistantInfo(assistantID1, "m-parent", aborted),
parts: [
{
...basePart(assistantID1, "a1"),
type: "reasoning",
text: "thinking",
time: { start: 0 },
},
{
...basePart(assistantID1, "a2"),
type: "text",
text: "partial answer",
},
] as MessageV2.Part[],
},
{
info: assistantInfo(assistantID2, "m-parent", aborted),
parts: [
{
...basePart(assistantID2, "b1"),
type: "step-start",
},
{
...basePart(assistantID2, "b2"),
type: "reasoning",
text: "thinking",
time: { start: 0 },
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "assistant",
content: [
{ type: "reasoning", text: "thinking", providerOptions: undefined },
{ type: "text", text: "partial answer" },
],
},
])
})
test("splits assistant messages on step-start boundaries", () => {
const assistantID = "m-assistant"
const input: MessageV2.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
{
...basePart(assistantID, "p1"),
type: "text",
text: "first",
},
{
...basePart(assistantID, "p2"),
type: "step-start",
},
{
...basePart(assistantID, "p3"),
type: "text",
text: "second",
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "assistant",
content: [{ type: "text", text: "first" }],
},
{
role: "assistant",
content: [{ type: "text", text: "second" }],
},
])
})
test("drops messages that only contain step-start parts", () => {
const assistantID = "m-assistant"
const input: MessageV2.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
{
...basePart(assistantID, "p1"),
type: "step-start",
},
] as MessageV2.Part[],
},
]
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
})
test("converts pending/running tool calls to error results to prevent dangling tool_use", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: MessageV2.WithParts[] = [
{
info: userInfo(userID),
parts: [
{
...basePart(userID, "u1"),
type: "text",
text: "run tool",
},
] as MessageV2.Part[],
},
{
info: assistantInfo(assistantID, userID),
parts: [
{
...basePart(assistantID, "a1"),
type: "tool",
callID: "call-pending",
tool: "bash",
state: {
status: "pending",
input: { cmd: "ls" },
raw: "",
},
},
{
...basePart(assistantID, "a2"),
type: "tool",
callID: "call-running",
tool: "read",
state: {
status: "running",
input: { path: "/tmp" },
time: { start: 0 },
},
},
] as MessageV2.Part[],
},
]
const result = MessageV2.toModelMessages(input, model)
expect(result).toStrictEqual([
{
role: "user",
content: [{ type: "text", text: "run tool" }],
},
{
role: "assistant",
content: [
{
type: "tool-call",
toolCallId: "call-pending",
toolName: "bash",
input: { cmd: "ls" },
providerExecuted: undefined,
},
{
type: "tool-call",
toolCallId: "call-running",
toolName: "read",
input: { path: "/tmp" },
providerExecuted: undefined,
},
],
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-pending",
toolName: "bash",
output: { type: "error-text", value: "[Tool execution was interrupted]" },
},
{
type: "tool-result",
toolCallId: "call-running",
toolName: "read",
output: { type: "error-text", value: "[Tool execution was interrupted]" },
},
],
},
])
})
})
describe("session.message-v2.fromError", () => {
test("serializes response error codes", () => {
const cases = [
{
code: "context_length_exceeded",
message: "Input exceeds context window of this model",
},
{
code: "insufficient_quota",
message: "Quota exceeded. Check your plan and billing details.",
},
{
code: "usage_not_included",
message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
},
{
code: "invalid_prompt",
message: "Invalid prompt from test",
},
]
cases.forEach((item) => {
const input = {
type: "error",
error: {
code: item.code,
message: item.code === "invalid_prompt" ? item.message : undefined,
},
}
const result = MessageV2.fromError(input, { providerID: "test" })
expect(result).toStrictEqual({
name: "APIError",
data: {
message: item.message,
isRetryable: false,
responseBody: JSON.stringify(input),
},
})
})
})
test("serializes unknown inputs", () => {
const result = MessageV2.fromError(123, { providerID: "test" })
expect(result).toStrictEqual({
name: "UnknownError",
data: {
message: "123",
},
})
})
})