branch:
message-builder.test.ts
31501 bytesRaw
import { describe, it, expect } from "vitest";
import {
applyChunkToParts,
type MessageParts,
type StreamChunkData
} from "../message-builder";
function makeParts(): MessageParts {
return [];
}
describe("applyChunkToParts", () => {
describe("text chunks", () => {
it("text-start creates a streaming text part", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "text-start",
id: "t1"
});
expect(handled).toBe(true);
expect(parts.length).toBe(1);
expect(parts[0]).toEqual({
type: "text",
text: "",
state: "streaming"
});
});
it("text-delta appends to existing text part", () => {
const parts = makeParts();
applyChunkToParts(parts, { type: "text-start", id: "t1" });
applyChunkToParts(parts, {
type: "text-delta",
id: "t1",
delta: "Hello "
});
applyChunkToParts(parts, {
type: "text-delta",
id: "t1",
delta: "world!"
});
expect(parts.length).toBe(1);
expect((parts[0] as { text: string }).text).toBe("Hello world!");
});
it("text-delta creates new text part with streaming state if no text-start received", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "text-delta",
id: "t1",
delta: "fallback"
});
expect(parts.length).toBe(1);
expect((parts[0] as { text: string }).text).toBe("fallback");
expect((parts[0] as { state: string }).state).toBe("streaming");
});
it("text-end marks fallback text part as done", () => {
const parts = makeParts();
// No text-start — simulates stream resumption
applyChunkToParts(parts, {
type: "text-delta",
id: "t1",
delta: "resumed"
});
applyChunkToParts(parts, { type: "text-end", id: "t1" });
expect((parts[0] as { state: string }).state).toBe("done");
expect((parts[0] as { text: string }).text).toBe("resumed");
});
it("text-end marks text part as done", () => {
const parts = makeParts();
applyChunkToParts(parts, { type: "text-start", id: "t1" });
applyChunkToParts(parts, {
type: "text-delta",
id: "t1",
delta: "content"
});
applyChunkToParts(parts, { type: "text-end", id: "t1" });
expect((parts[0] as { state: string }).state).toBe("done");
expect((parts[0] as { text: string }).text).toBe("content");
});
it("handles a full text lifecycle", () => {
const parts = makeParts();
applyChunkToParts(parts, { type: "text-start", id: "t1" });
applyChunkToParts(parts, {
type: "text-delta",
id: "t1",
delta: "The "
});
applyChunkToParts(parts, {
type: "text-delta",
id: "t1",
delta: "answer is 42."
});
applyChunkToParts(parts, { type: "text-end", id: "t1" });
expect(parts.length).toBe(1);
expect(parts[0]).toEqual({
type: "text",
text: "The answer is 42.",
state: "done"
});
});
});
describe("reasoning chunks", () => {
it("reasoning-start creates a streaming reasoning part", () => {
const parts = makeParts();
applyChunkToParts(parts, { type: "reasoning-start", id: "r1" });
expect(parts.length).toBe(1);
expect(parts[0]).toEqual({
type: "reasoning",
text: "",
state: "streaming"
});
});
it("reasoning-delta appends to reasoning part", () => {
const parts = makeParts();
applyChunkToParts(parts, { type: "reasoning-start", id: "r1" });
applyChunkToParts(parts, {
type: "reasoning-delta",
id: "r1",
delta: "thinking..."
});
expect((parts[0] as { text: string }).text).toBe("thinking...");
});
it("reasoning-end marks reasoning as done", () => {
const parts = makeParts();
applyChunkToParts(parts, { type: "reasoning-start", id: "r1" });
applyChunkToParts(parts, {
type: "reasoning-delta",
id: "r1",
delta: "done thinking"
});
applyChunkToParts(parts, { type: "reasoning-end", id: "r1" });
expect((parts[0] as { state: string }).state).toBe("done");
});
it("reasoning-delta creates fallback part when no reasoning-start received", () => {
const parts = makeParts();
// No reasoning-start — simulates stream resumption where start was missed
applyChunkToParts(parts, {
type: "reasoning-delta",
id: "r1",
delta: "resumed thinking"
});
expect(parts.length).toBe(1);
expect(parts[0]).toEqual({
type: "reasoning",
text: "resumed thinking",
state: "streaming"
});
// Subsequent deltas append normally
applyChunkToParts(parts, {
type: "reasoning-delta",
id: "r1",
delta: " more"
});
expect((parts[0] as { text: string }).text).toBe("resumed thinking more");
});
});
describe("file chunks", () => {
it("creates a file part", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "file",
mediaType: "image/png",
url: "https://example.com/image.png"
});
expect(parts.length).toBe(1);
expect(parts[0]).toEqual({
type: "file",
mediaType: "image/png",
url: "https://example.com/image.png"
});
});
});
describe("source chunks", () => {
it("creates a source-url part", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "source-url",
sourceId: "s1",
url: "https://example.com",
title: "Example"
});
expect(parts[0]).toEqual({
type: "source-url",
sourceId: "s1",
url: "https://example.com",
title: "Example",
providerMetadata: undefined
});
});
it("creates a source-document part", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "source-document",
sourceId: "d1",
mediaType: "application/pdf",
title: "Doc",
filename: "doc.pdf"
});
expect(parts[0]).toEqual({
type: "source-document",
sourceId: "d1",
mediaType: "application/pdf",
title: "Doc",
filename: "doc.pdf",
providerMetadata: undefined
});
});
});
describe("tool chunks", () => {
it("tool-input-available creates a tool part", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" }
});
expect(parts.length).toBe(1);
const part = parts[0] as Record<string, unknown>;
expect(part.type).toBe("tool-getWeather");
expect(part.toolCallId).toBe("call_1");
expect(part.state).toBe("input-available");
expect(part.input).toEqual({ city: "London" });
});
it("tool-output-available updates existing tool part", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" }
});
applyChunkToParts(parts, {
type: "tool-output-available",
toolCallId: "call_1",
output: "Sunny, 22°C"
});
expect(parts.length).toBe(1);
const part = parts[0] as Record<string, unknown>;
expect(part.state).toBe("output-available");
expect(part.output).toBe("Sunny, 22°C");
});
it("tool-output-available is a no-op if tool part not found", () => {
const parts = makeParts();
// No tool-input-available first
applyChunkToParts(parts, {
type: "tool-output-available",
toolCallId: "nonexistent",
output: "result"
});
expect(parts.length).toBe(0); // Nothing added
});
});
describe("step chunks", () => {
it("start-step adds a step-start part", () => {
const parts = makeParts();
applyChunkToParts(parts, { type: "start-step" });
expect(parts.length).toBe(1);
expect(parts[0].type).toBe("step-start");
});
it("step-start (client alias) also adds a step-start part", () => {
const parts = makeParts();
applyChunkToParts(parts, { type: "step-start" });
expect(parts.length).toBe(1);
expect(parts[0].type).toBe("step-start");
});
});
describe("data-* chunks", () => {
it("appends a new data part", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "data-sources",
data: { query: "hello", results: [] }
});
expect(handled).toBe(true);
expect(parts.length).toBe(1);
expect(parts[0]).toEqual({
type: "data-sources",
data: { query: "hello", results: [] }
});
});
it("preserves the id field when present", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "data-sources",
id: "src-1",
data: { query: "hello", status: "searching" }
});
expect(parts[0]).toEqual({
type: "data-sources",
id: "src-1",
data: { query: "hello", status: "searching" }
});
});
it("reconciles by type+id (updates data in-place)", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "data-sources",
id: "src-1",
data: { status: "searching", results: [] }
});
applyChunkToParts(parts, {
type: "data-sources",
id: "src-1",
data: { status: "found", results: ["doc1", "doc2"] }
});
// Should update in-place, not append
expect(parts.length).toBe(1);
expect((parts[0] as Record<string, unknown>).data).toEqual({
status: "found",
results: ["doc1", "doc2"]
});
});
it("does not reconcile when ids differ", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "data-sources",
id: "src-1",
data: { query: "first" }
});
applyChunkToParts(parts, {
type: "data-sources",
id: "src-2",
data: { query: "second" }
});
expect(parts.length).toBe(2);
});
it("does not reconcile when types differ", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "data-sources",
id: "x",
data: { from: "sources" }
});
applyChunkToParts(parts, {
type: "data-usage",
id: "x",
data: { from: "usage" }
});
expect(parts.length).toBe(2);
});
it("does not reconcile parts without ids (appends each time)", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "data-status",
data: { step: 1 }
});
applyChunkToParts(parts, {
type: "data-status",
data: { step: 2 }
});
expect(parts.length).toBe(2);
});
it("transient parts return true but are not added to parts", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "data-thinking",
transient: true,
data: { model: "gpt-4o" }
});
expect(handled).toBe(true);
expect(parts.length).toBe(0);
});
it("transient parts with id are still skipped (not reconciled)", () => {
const parts = makeParts();
// First: non-transient part with id
applyChunkToParts(parts, {
type: "data-progress",
id: "p-1",
data: { step: 1 }
});
expect(parts.length).toBe(1);
// Second: transient part with SAME type+id — should not reconcile
const handled = applyChunkToParts(parts, {
type: "data-progress",
id: "p-1",
transient: true,
data: { step: 2 }
});
expect(handled).toBe(true);
// Original part unchanged
expect(parts.length).toBe(1);
expect((parts[0] as Record<string, unknown>).data).toEqual({ step: 1 });
});
it("handles undefined data (part persisted without data field)", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "data-empty"
} as StreamChunkData);
expect(handled).toBe(true);
expect(parts.length).toBe(1);
expect(parts[0].type).toBe("data-empty");
// data is undefined — JSON.stringify would drop it
expect((parts[0] as Record<string, unknown>).data).toBeUndefined();
});
it("non-data-* prefixed types still return false", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "unknown-type"
} as StreamChunkData);
expect(handled).toBe(false);
expect(parts.length).toBe(0);
});
it("coexists with other part types", () => {
const parts = makeParts();
// Text
applyChunkToParts(parts, { type: "text-start", id: "t1" });
applyChunkToParts(parts, {
type: "text-delta",
id: "t1",
delta: "Hello"
});
applyChunkToParts(parts, { type: "text-end", id: "t1" });
// Data part
applyChunkToParts(parts, {
type: "data-usage",
data: { tokens: 42 }
});
expect(parts.length).toBe(2);
expect(parts[0].type).toBe("text");
expect(parts[1].type).toBe("data-usage");
});
});
describe("unrecognized chunks", () => {
it("returns false for unknown chunk types", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "unknown-type"
} as StreamChunkData);
expect(handled).toBe(false);
expect(parts.length).toBe(0);
});
});
describe("mixed content", () => {
it("builds a complex message with text + reasoning + tool + file", () => {
const parts = makeParts();
// Reasoning first
applyChunkToParts(parts, { type: "reasoning-start", id: "r1" });
applyChunkToParts(parts, {
type: "reasoning-delta",
id: "r1",
delta: "Let me think..."
});
applyChunkToParts(parts, { type: "reasoning-end", id: "r1" });
// Step boundary
applyChunkToParts(parts, { type: "start-step" });
// Text
applyChunkToParts(parts, { type: "text-start", id: "t1" });
applyChunkToParts(parts, {
type: "text-delta",
id: "t1",
delta: "Here's the weather"
});
applyChunkToParts(parts, { type: "text-end", id: "t1" });
// Tool call
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" }
});
applyChunkToParts(parts, {
type: "tool-output-available",
toolCallId: "call_1",
output: { temp: 22, condition: "Sunny" }
});
// File
applyChunkToParts(parts, {
type: "file",
mediaType: "image/png",
url: "https://example.com/chart.png"
});
// reasoning + step-start + text + tool (updated in place) + file = 5
expect(parts.length).toBe(5);
expect(parts[0].type).toBe("reasoning");
expect(parts[1].type).toBe("step-start");
expect(parts[2].type).toBe("text");
expect(parts[3].type).toBe("tool-getWeather");
expect((parts[3] as Record<string, unknown>).state).toBe(
"output-available"
);
expect(parts[4].type).toBe("file");
});
});
describe("tool streaming lifecycle", () => {
it("tool-input-start creates a tool part in input-streaming state", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "getWeather"
});
expect(handled).toBe(true);
expect(parts.length).toBe(1);
const part = parts[0] as Record<string, unknown>;
expect(part.type).toBe("tool-getWeather");
expect(part.toolCallId).toBe("call_1");
expect(part.state).toBe("input-streaming");
expect(part.input).toBeUndefined();
});
it("tool-input-delta updates the tool part with partial input", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "getWeather"
});
applyChunkToParts(parts, {
type: "tool-input-delta",
toolCallId: "call_1",
input: { city: "Lon" }
});
expect(parts.length).toBe(1);
expect((parts[0] as Record<string, unknown>).input).toEqual({
city: "Lon"
});
expect((parts[0] as Record<string, unknown>).state).toBe(
"input-streaming"
);
});
it("tool-input-available finalizes an existing streaming tool part", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "getWeather"
});
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" }
});
expect(parts.length).toBe(1);
const part = parts[0] as Record<string, unknown>;
expect(part.state).toBe("input-available");
expect(part.input).toEqual({ city: "London" });
});
it("tool-input-available creates a new part if no tool-input-start", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" }
});
expect(parts.length).toBe(1);
expect((parts[0] as Record<string, unknown>).state).toBe(
"input-available"
);
});
it("full tool streaming lifecycle: start -> delta -> available -> output", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "getWeather"
});
expect((parts[0] as Record<string, unknown>).state).toBe(
"input-streaming"
);
applyChunkToParts(parts, {
type: "tool-input-delta",
toolCallId: "call_1",
input: { city: "Lon" }
});
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" }
});
expect((parts[0] as Record<string, unknown>).state).toBe(
"input-available"
);
applyChunkToParts(parts, {
type: "tool-output-available",
toolCallId: "call_1",
output: "Sunny, 22C"
});
expect((parts[0] as Record<string, unknown>).state).toBe(
"output-available"
);
expect((parts[0] as Record<string, unknown>).output).toBe("Sunny, 22C");
expect(parts.length).toBe(1);
});
});
describe("tool error handling", () => {
it("tool-input-error marks tool part as output-error", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "getWeather"
});
applyChunkToParts(parts, {
type: "tool-input-error",
toolCallId: "call_1",
toolName: "getWeather",
input: '{"city": "Lond',
errorText: "Unexpected end of JSON input"
});
const part = parts[0] as Record<string, unknown>;
expect(part.state).toBe("output-error");
expect(part.errorText).toBe("Unexpected end of JSON input");
});
it("tool-input-error creates a new part if no tool-input-start", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-error",
toolCallId: "call_1",
toolName: "getWeather",
errorText: "Schema validation failed"
});
expect(parts.length).toBe(1);
expect((parts[0] as Record<string, unknown>).state).toBe("output-error");
});
it("tool-output-error marks tool part as output-error", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" }
});
applyChunkToParts(parts, {
type: "tool-output-error",
toolCallId: "call_1",
errorText: "API rate limit exceeded"
});
const part = parts[0] as Record<string, unknown>;
expect(part.state).toBe("output-error");
expect(part.errorText).toBe("API rate limit exceeded");
});
it("tool-output-error is a no-op if tool part not found", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-output-error",
toolCallId: "nonexistent",
errorText: "error"
});
expect(parts.length).toBe(0);
});
});
describe("preliminary tool output", () => {
it("tool-output-available with preliminary=true marks output as preliminary", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "streamingTool",
input: {}
});
applyChunkToParts(parts, {
type: "tool-output-available",
toolCallId: "call_1",
output: { partial: true, data: "chunk1" },
preliminary: true
});
const part = parts[0] as Record<string, unknown>;
expect(part.state).toBe("output-available");
expect(part.preliminary).toBe(true);
expect(part.output).toEqual({ partial: true, data: "chunk1" });
});
it("subsequent tool-output-available with preliminary=false finalizes", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "streamingTool",
input: {}
});
applyChunkToParts(parts, {
type: "tool-output-available",
toolCallId: "call_1",
output: "partial",
preliminary: true
});
expect((parts[0] as Record<string, unknown>).preliminary).toBe(true);
applyChunkToParts(parts, {
type: "tool-output-available",
toolCallId: "call_1",
output: "complete result",
preliminary: false
});
const part = parts[0] as Record<string, unknown>;
expect(part.preliminary).toBe(false);
expect(part.output).toBe("complete result");
});
it("tool-output-available without preliminary does not set the flag", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" }
});
applyChunkToParts(parts, {
type: "tool-output-available",
toolCallId: "call_1",
output: "Sunny"
});
const part = parts[0] as Record<string, unknown>;
expect(part.preliminary).toBeUndefined();
});
});
describe("tool provider metadata (callProviderMetadata, providerExecuted, title)", () => {
it("tool-input-available preserves callProviderMetadata from providerMetadata", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "askQuestion",
input: { question: "What is your name?" },
providerMetadata: {
google: { thoughtSignature: "sig_abc123" }
}
});
const part = parts[0] as Record<string, unknown>;
expect(part.callProviderMetadata).toEqual({
google: { thoughtSignature: "sig_abc123" }
});
});
it("tool-input-available update path preserves callProviderMetadata", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "askQuestion"
});
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "askQuestion",
input: { question: "Name?" },
providerMetadata: {
google: { thoughtSignature: "sig_xyz" }
}
});
const part = parts[0] as Record<string, unknown>;
expect(part.callProviderMetadata).toEqual({
google: { thoughtSignature: "sig_xyz" }
});
});
it("tool-input-start preserves callProviderMetadata", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "askQuestion",
providerMetadata: {
google: { thoughtSignature: "sig_start" }
}
});
const part = parts[0] as Record<string, unknown>;
expect(part.callProviderMetadata).toEqual({
google: { thoughtSignature: "sig_start" }
});
});
it("tool-input-error preserves callProviderMetadata (create path)", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-error",
toolCallId: "call_1",
toolName: "askQuestion",
errorText: "Parse error",
providerMetadata: {
google: { thoughtSignature: "sig_err" }
}
});
const part = parts[0] as Record<string, unknown>;
expect(part.callProviderMetadata).toEqual({
google: { thoughtSignature: "sig_err" }
});
});
it("tool-input-error preserves callProviderMetadata (update path)", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "askQuestion"
});
applyChunkToParts(parts, {
type: "tool-input-error",
toolCallId: "call_1",
toolName: "askQuestion",
errorText: "Parse error",
providerMetadata: {
google: { thoughtSignature: "sig_err2" }
}
});
const part = parts[0] as Record<string, unknown>;
expect(part.callProviderMetadata).toEqual({
google: { thoughtSignature: "sig_err2" }
});
});
it("does not set callProviderMetadata when providerMetadata is absent", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" }
});
const part = parts[0] as Record<string, unknown>;
expect(part.callProviderMetadata).toBeUndefined();
});
it("tool-input-available preserves providerExecuted", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "codeExec",
input: { code: "1+1" },
providerExecuted: true
} as StreamChunkData);
const part = parts[0] as Record<string, unknown>;
expect(part.providerExecuted).toBe(true);
});
it("tool-input-available update path preserves providerExecuted", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "codeExec"
});
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "codeExec",
input: { code: "1+1" },
providerExecuted: true
} as StreamChunkData);
const part = parts[0] as Record<string, unknown>;
expect(part.providerExecuted).toBe(true);
});
it("tool-input-available preserves title", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" },
title: "Get Weather"
});
const part = parts[0] as Record<string, unknown>;
expect(part.title).toBe("Get Weather");
});
it("tool-input-available update path preserves title", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "getWeather"
});
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "getWeather",
input: { city: "London" },
title: "Get Weather"
});
const part = parts[0] as Record<string, unknown>;
expect(part.title).toBe("Get Weather");
});
it("tool-input-start preserves providerExecuted and title", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-start",
toolCallId: "call_1",
toolName: "codeExec",
providerExecuted: true,
title: "Code Execution"
} as StreamChunkData);
const part = parts[0] as Record<string, unknown>;
expect(part.providerExecuted).toBe(true);
expect(part.title).toBe("Code Execution");
});
it("preserves all three fields together on tool-input-available", () => {
const parts = makeParts();
applyChunkToParts(parts, {
type: "tool-input-available",
toolCallId: "call_1",
toolName: "askQuestion",
input: { question: "Name?" },
providerMetadata: {
google: { thoughtSignature: "sig_full" }
},
providerExecuted: false,
title: "Ask Question"
} as StreamChunkData);
const part = parts[0] as Record<string, unknown>;
expect(part.callProviderMetadata).toEqual({
google: { thoughtSignature: "sig_full" }
});
expect(part.providerExecuted).toBe(false);
expect(part.title).toBe("Ask Question");
});
});
describe("metadata and message-level chunks", () => {
it("returns false for 'start' chunk (caller handles metadata)", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "start",
messageId: "msg-1",
messageMetadata: { model: "gpt-4o" }
} as StreamChunkData);
expect(handled).toBe(false);
// Should not add any parts
expect(parts.length).toBe(0);
});
it("returns false for 'finish' chunk (caller handles metadata)", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "finish",
messageMetadata: { totalTokens: 100 }
} as StreamChunkData);
expect(handled).toBe(false);
expect(parts.length).toBe(0);
});
it("returns false for 'message-metadata' chunk (caller handles metadata)", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "message-metadata",
messageMetadata: { createdAt: 1234567890 }
} as StreamChunkData);
expect(handled).toBe(false);
expect(parts.length).toBe(0);
});
it("returns false for 'finish-step' chunk", () => {
const parts = makeParts();
const handled = applyChunkToParts(parts, {
type: "finish-step"
} as StreamChunkData);
expect(handled).toBe(false);
expect(parts.length).toBe(0);
});
});
});