test(agents): centralize AgentMessage fixtures and remove unsafe casts

This commit is contained in:
Peter Steinberger
2026-03-03 02:13:43 +00:00
parent 15a0455d04
commit 70db52de71
14 changed files with 216 additions and 139 deletions

View File

@@ -6,6 +6,7 @@ import {
repairToolUseResultPairing,
stripToolResultDetails,
} from "./session-transcript-repair.js";
import { castAgentMessage, castAgentMessages } from "./test-helpers/agent-message-fixtures.js";
const TOOL_CALL_BLOCK_TYPES = new Set(["toolCall", "toolUse", "functionCall"]);
@@ -25,7 +26,7 @@ describe("sanitizeToolUseResultPairing", () => {
middleMessage?: unknown;
secondText?: string;
}): AgentMessage[] =>
[
castAgentMessages([
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
@@ -37,7 +38,7 @@ describe("sanitizeToolUseResultPairing", () => {
content: [{ type: "text", text: "first" }],
isError: false,
},
...(opts?.middleMessage ? [opts.middleMessage as AgentMessage] : []),
...(opts?.middleMessage ? [castAgentMessage(opts.middleMessage)] : []),
{
role: "toolResult",
toolCallId: "call_1",
@@ -45,10 +46,10 @@ describe("sanitizeToolUseResultPairing", () => {
content: [{ type: "text", text: opts?.secondText ?? "second" }],
isError: false,
},
] as unknown as AgentMessage[];
]);
it("moves tool results directly after tool calls and inserts missing results", () => {
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [
@@ -64,7 +65,7 @@ describe("sanitizeToolUseResultPairing", () => {
content: [{ type: "text", text: "ok" }],
isError: false,
},
] as unknown as AgentMessage[];
]);
const out = sanitizeToolUseResultPairing(input);
expect(out[0]?.role).toBe("assistant");
@@ -76,7 +77,7 @@ describe("sanitizeToolUseResultPairing", () => {
});
it("repairs blank tool result names from matching tool calls", () => {
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
@@ -88,7 +89,7 @@ describe("sanitizeToolUseResultPairing", () => {
content: [{ type: "text", text: "ok" }],
isError: false,
},
] as unknown as AgentMessage[];
]);
const out = sanitizeToolUseResultPairing(input);
const toolResult = out.find((message) => message.role === "toolResult") as {
@@ -99,10 +100,10 @@ describe("sanitizeToolUseResultPairing", () => {
});
it("drops duplicate tool results for the same id within a span", () => {
const input = [
const input = castAgentMessages([
...buildDuplicateToolResultInput(),
{ role: "user", content: "ok" },
] as AgentMessage[];
]);
const out = sanitizeToolUseResultPairing(input);
expect(out.filter((m) => m.role === "toolResult")).toHaveLength(1);
@@ -123,7 +124,7 @@ describe("sanitizeToolUseResultPairing", () => {
});
it("drops orphan tool results that do not match any tool call", () => {
const input = [
const input = castAgentMessages([
{ role: "user", content: "hello" },
{
role: "toolResult",
@@ -136,7 +137,7 @@ describe("sanitizeToolUseResultPairing", () => {
role: "assistant",
content: [{ type: "text", text: "ok" }],
},
] as unknown as AgentMessage[];
]);
const out = sanitizeToolUseResultPairing(input);
expect(out.some((m) => m.role === "toolResult")).toBe(false);
@@ -147,14 +148,14 @@ describe("sanitizeToolUseResultPairing", () => {
// When an assistant message has stopReason: "error", its tool_use blocks may be
// incomplete/malformed. We should NOT create synthetic tool_results for them,
// as this causes API 400 errors: "unexpected tool_use_id found in tool_result blocks"
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [{ type: "toolCall", id: "call_error", name: "exec", arguments: {} }],
stopReason: "error",
},
{ role: "user", content: "something went wrong" },
] as unknown as AgentMessage[];
]);
const result = repairToolUseResultPairing(input);
@@ -169,14 +170,14 @@ describe("sanitizeToolUseResultPairing", () => {
it("skips tool call extraction for assistant messages with stopReason 'aborted'", () => {
// When a request is aborted mid-stream, the assistant message may have incomplete
// tool_use blocks (with partialJson). We should NOT create synthetic tool_results.
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [{ type: "toolCall", id: "call_aborted", name: "Bash", arguments: {} }],
stopReason: "aborted",
},
{ role: "user", content: "retrying after abort" },
] as unknown as AgentMessage[];
]);
const result = repairToolUseResultPairing(input);
@@ -190,14 +191,14 @@ describe("sanitizeToolUseResultPairing", () => {
it("still repairs tool results for normal assistant messages with stopReason 'toolUse'", () => {
// Normal tool calls (stopReason: "toolUse" or "stop") should still be repaired
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [{ type: "toolCall", id: "call_normal", name: "read", arguments: {} }],
stopReason: "toolUse",
},
{ role: "user", content: "user message" },
] as unknown as AgentMessage[];
]);
const result = repairToolUseResultPairing(input);
@@ -210,7 +211,7 @@ describe("sanitizeToolUseResultPairing", () => {
// When an assistant message is aborted, any tool results that follow should be
// dropped as orphans (since we skip extracting tool calls from aborted messages).
// This addresses the edge case where a partial tool result was persisted before abort.
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [{ type: "toolCall", id: "call_aborted", name: "exec", arguments: {} }],
@@ -224,7 +225,7 @@ describe("sanitizeToolUseResultPairing", () => {
isError: false,
},
{ role: "user", content: "retrying" },
] as unknown as AgentMessage[];
]);
const result = repairToolUseResultPairing(input);
@@ -244,12 +245,12 @@ describe("sanitizeToolCallInputs", () => {
options?: Parameters<typeof sanitizeToolCallInputs>[1],
) {
return sanitizeToolCallInputs(
[
castAgentMessages([
{
role: "assistant",
content,
},
] as unknown as AgentMessage[],
]),
options,
);
}
@@ -262,13 +263,13 @@ describe("sanitizeToolCallInputs", () => {
}
it("drops tool calls missing input or arguments", () => {
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read" }],
},
{ role: "user", content: "hello" },
] as unknown as AgentMessage[];
]);
const out = sanitizeToolCallInputs(input);
expect(out.map((m) => m.role)).toEqual(["user"]);
@@ -325,7 +326,7 @@ describe("sanitizeToolCallInputs", () => {
});
it("keeps valid tool calls and preserves text blocks", () => {
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [
@@ -334,7 +335,7 @@ describe("sanitizeToolCallInputs", () => {
{ type: "toolCall", id: "call_drop", name: "read" },
],
},
] as unknown as AgentMessage[];
]);
const out = sanitizeToolCallInputs(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
@@ -384,7 +385,7 @@ describe("sanitizeToolCallInputs", () => {
});
it("preserves toolUse input shape for sessions_spawn when no attachments are present", () => {
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [
@@ -396,7 +397,7 @@ describe("sanitizeToolCallInputs", () => {
},
],
},
] as unknown as AgentMessage[];
]);
const out = sanitizeToolCallInputs(input);
const toolCalls = getAssistantToolCallBlocks(out) as Array<Record<string, unknown>>;
@@ -408,7 +409,7 @@ describe("sanitizeToolCallInputs", () => {
});
it("redacts sessions_spawn attachments for mixed-case and padded tool names", () => {
const input = [
const input = castAgentMessages([
{
role: "assistant",
content: [
@@ -423,7 +424,7 @@ describe("sanitizeToolCallInputs", () => {
},
],
},
] as unknown as AgentMessage[];
]);
const out = sanitizeToolCallInputs(input);
const toolCalls = getAssistantToolCallBlocks(out) as Array<Record<string, unknown>>;
@@ -448,7 +449,7 @@ describe("sanitizeToolCallInputs", () => {
describe("stripToolResultDetails", () => {
it("removes details only from toolResult messages", () => {
const input = [
const input = castAgentMessages([
{
role: "toolResult",
toolCallId: "call_1",
@@ -458,7 +459,7 @@ describe("stripToolResultDetails", () => {
},
{ role: "assistant", content: [{ type: "text", text: "keep me" }], details: { no: "touch" } },
{ role: "user", content: "hello" },
] as unknown as AgentMessage[];
]);
const out = stripToolResultDetails(input) as unknown as Array<Record<string, unknown>>;
@@ -472,7 +473,7 @@ describe("stripToolResultDetails", () => {
});
it("returns the same array reference when there are no toolResult details", () => {
const input = [
const input = castAgentMessages([
{ role: "assistant", content: [{ type: "text", text: "a" }] },
{
role: "toolResult",
@@ -481,7 +482,7 @@ describe("stripToolResultDetails", () => {
content: [{ type: "text", text: "ok" }],
},
{ role: "user", content: "b" },
] as unknown as AgentMessage[];
]);
const out = stripToolResultDetails(input);
expect(out).toBe(input);