mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 07:02:44 +00:00
test(agents): centralize AgentMessage fixtures and remove unsafe casts
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
sanitizeGoogleTurnOrdering,
|
sanitizeGoogleTurnOrdering,
|
||||||
sanitizeSessionMessagesImages,
|
sanitizeSessionMessagesImages,
|
||||||
} from "./pi-embedded-helpers.js";
|
} from "./pi-embedded-helpers.js";
|
||||||
|
import { castAgentMessages } from "./test-helpers/agent-message-fixtures.js";
|
||||||
|
|
||||||
let testTimestamp = 1;
|
let testTimestamp = 1;
|
||||||
const nextTimestamp = () => testTimestamp++;
|
const nextTimestamp = () => testTimestamp++;
|
||||||
@@ -93,7 +94,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not synthesize tool call input when missing", async () => {
|
it("does not synthesize tool call input when missing", async () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_1", name: "read" }],
|
content: [{ type: "toolCall", id: "call_1", name: "read" }],
|
||||||
@@ -111,7 +112,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
stopReason: "toolUse",
|
stopReason: "toolUse",
|
||||||
timestamp: nextTimestamp(),
|
timestamp: nextTimestamp(),
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = await sanitizeSessionMessagesImages(input, "test");
|
const out = await sanitizeSessionMessagesImages(input, "test");
|
||||||
const assistant = out[0] as { content?: Array<Record<string, unknown>> };
|
const assistant = out[0] as { content?: Array<Record<string, unknown>> };
|
||||||
@@ -122,7 +123,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("removes empty assistant text blocks but preserves tool calls", async () => {
|
it("removes empty assistant text blocks but preserves tool calls", async () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -143,7 +144,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
stopReason: "toolUse",
|
stopReason: "toolUse",
|
||||||
timestamp: nextTimestamp(),
|
timestamp: nextTimestamp(),
|
||||||
},
|
},
|
||||||
] as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = await sanitizeSessionMessagesImages(input, "test");
|
const out = await sanitizeSessionMessagesImages(input, "test");
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sanitizes tool ids in strict mode (alphanumeric only)", async () => {
|
it("sanitizes tool ids in strict mode (alphanumeric only)", async () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -171,7 +172,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
toolUseId: "call_abc|item:123",
|
toolUseId: "call_abc|item:123",
|
||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = await sanitizeSessionMessagesImages(input, "test", {
|
const out = await sanitizeSessionMessagesImages(input, "test", {
|
||||||
sanitizeToolCallIds: true,
|
sanitizeToolCallIds: true,
|
||||||
@@ -188,7 +189,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sanitizes tool IDs in images-only mode when explicitly enabled", async () => {
|
it("sanitizes tool IDs in images-only mode when explicitly enabled", async () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_123|fc_456", name: "read", arguments: {} }],
|
content: [{ type: "toolCall", id: "call_123|fc_456", name: "read", arguments: {} }],
|
||||||
@@ -214,7 +215,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
isError: false,
|
isError: false,
|
||||||
timestamp: nextTimestamp(),
|
timestamp: nextTimestamp(),
|
||||||
},
|
},
|
||||||
] as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = await sanitizeSessionMessagesImages(input, "test", {
|
const out = await sanitizeSessionMessagesImages(input, "test", {
|
||||||
sanitizeMode: "images-only",
|
sanitizeMode: "images-only",
|
||||||
@@ -236,7 +237,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it("filters whitespace-only assistant text blocks", async () => {
|
it("filters whitespace-only assistant text blocks", async () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -257,7 +258,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
stopReason: "stop",
|
stopReason: "stop",
|
||||||
timestamp: nextTimestamp(),
|
timestamp: nextTimestamp(),
|
||||||
},
|
},
|
||||||
] as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = await sanitizeSessionMessagesImages(input, "test");
|
const out = await sanitizeSessionMessagesImages(input, "test");
|
||||||
|
|
||||||
@@ -266,7 +267,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("drops assistant messages that only contain empty text", async () => {
|
it("drops assistant messages that only contain empty text", async () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{ role: "user", content: "hello", timestamp: nextTimestamp() } satisfies UserMessage,
|
{ role: "user", content: "hello", timestamp: nextTimestamp() } satisfies UserMessage,
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
@@ -285,7 +286,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
stopReason: "stop",
|
stopReason: "stop",
|
||||||
timestamp: nextTimestamp(),
|
timestamp: nextTimestamp(),
|
||||||
} satisfies AssistantMessage,
|
} satisfies AssistantMessage,
|
||||||
];
|
]);
|
||||||
|
|
||||||
const out = await sanitizeSessionMessagesImages(input, "test");
|
const out = await sanitizeSessionMessagesImages(input, "test");
|
||||||
|
|
||||||
@@ -293,7 +294,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
expect(out[0]?.role).toBe("user");
|
expect(out[0]?.role).toBe("user");
|
||||||
});
|
});
|
||||||
it("keeps empty assistant error messages", async () => {
|
it("keeps empty assistant error messages", async () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{ role: "user", content: "hello", timestamp: nextTimestamp() } satisfies UserMessage,
|
{ role: "user", content: "hello", timestamp: nextTimestamp() } satisfies UserMessage,
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
@@ -329,7 +330,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
},
|
},
|
||||||
timestamp: nextTimestamp(),
|
timestamp: nextTimestamp(),
|
||||||
} satisfies AssistantMessage,
|
} satisfies AssistantMessage,
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = await sanitizeSessionMessagesImages(input, "test");
|
const out = await sanitizeSessionMessagesImages(input, "test");
|
||||||
|
|
||||||
@@ -360,7 +361,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
|
|
||||||
describe("thought_signature stripping", () => {
|
describe("thought_signature stripping", () => {
|
||||||
it("strips msg_-prefixed thought_signature from assistant message content blocks", async () => {
|
it("strips msg_-prefixed thought_signature from assistant message content blocks", async () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -372,7 +373,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = await sanitizeSessionMessagesImages(input, "test");
|
const out = await sanitizeSessionMessagesImages(input, "test");
|
||||||
|
|
||||||
@@ -387,19 +388,19 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
|
|
||||||
describe("sanitizeGoogleTurnOrdering", () => {
|
describe("sanitizeGoogleTurnOrdering", () => {
|
||||||
it("prepends a synthetic user turn when history starts with assistant", () => {
|
it("prepends a synthetic user turn when history starts with assistant", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
|
content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeGoogleTurnOrdering(input);
|
const out = sanitizeGoogleTurnOrdering(input);
|
||||||
expect(out[0]?.role).toBe("user");
|
expect(out[0]?.role).toBe("user");
|
||||||
expect(out[1]?.role).toBe("assistant");
|
expect(out[1]?.role).toBe("assistant");
|
||||||
});
|
});
|
||||||
it("is a no-op when history starts with user", () => {
|
it("is a no-op when history starts with user", () => {
|
||||||
const input = [{ role: "user", content: "hi" }] as unknown as AgentMessage[];
|
const input = castAgentMessages([{ role: "user", content: "hi" }]);
|
||||||
const out = sanitizeGoogleTurnOrdering(input);
|
const out = sanitizeGoogleTurnOrdering(input);
|
||||||
expect(out).toBe(input);
|
expect(out).toBe(input);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|||||||
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { applyGoogleTurnOrderingFix } from "./pi-embedded-runner.js";
|
import { applyGoogleTurnOrderingFix } from "./pi-embedded-runner.js";
|
||||||
|
import { castAgentMessage } from "./test-helpers/agent-message-fixtures.js";
|
||||||
|
|
||||||
describe("applyGoogleTurnOrderingFix", () => {
|
describe("applyGoogleTurnOrderingFix", () => {
|
||||||
const makeAssistantFirst = (): AgentMessage[] => [
|
const makeAssistantFirst = (): AgentMessage[] => [
|
||||||
{
|
castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
|
content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
it("prepends a bootstrap once and records a marker for Google models", () => {
|
it("prepends a bootstrap once and records a marker for Google models", () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
makeModelSnapshotEntry,
|
makeModelSnapshotEntry,
|
||||||
} from "./pi-embedded-runner.sanitize-session-history.test-harness.js";
|
} from "./pi-embedded-runner.sanitize-session-history.test-harness.js";
|
||||||
import { sanitizeSessionHistory } from "./pi-embedded-runner/google.js";
|
import { sanitizeSessionHistory } from "./pi-embedded-runner/google.js";
|
||||||
|
import { castAgentMessage } from "./test-helpers/agent-message-fixtures.js";
|
||||||
|
|
||||||
describe("sanitizeSessionHistory openai tool id preservation", () => {
|
describe("sanitizeSessionHistory openai tool id preservation", () => {
|
||||||
const makeSessionManager = () =>
|
const makeSessionManager = () =>
|
||||||
@@ -17,7 +18,7 @@ describe("sanitizeSessionHistory openai tool id preservation", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const makeMessages = (withReasoning: boolean): AgentMessage[] => [
|
const makeMessages = (withReasoning: boolean): AgentMessage[] => [
|
||||||
{
|
castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
...(withReasoning
|
...(withReasoning
|
||||||
@@ -31,14 +32,14 @@ describe("sanitizeSessionHistory openai tool id preservation", () => {
|
|||||||
: []),
|
: []),
|
||||||
{ type: "toolCall", id: "call_123|fc_123", name: "noop", arguments: {} },
|
{ type: "toolCall", id: "call_123|fc_123", name: "noop", arguments: {} },
|
||||||
],
|
],
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
{
|
castAgentMessage({
|
||||||
role: "toolResult",
|
role: "toolResult",
|
||||||
toolCallId: "call_123|fc_123",
|
toolCallId: "call_123|fc_123",
|
||||||
toolName: "noop",
|
toolName: "noop",
|
||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
isError: false,
|
isError: false,
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
sanitizeWithOpenAIResponses,
|
sanitizeWithOpenAIResponses,
|
||||||
TEST_SESSION_ID,
|
TEST_SESSION_ID,
|
||||||
} from "./pi-embedded-runner.sanitize-session-history.test-harness.js";
|
} from "./pi-embedded-runner.sanitize-session-history.test-harness.js";
|
||||||
|
import { castAgentMessage, castAgentMessages } from "./test-helpers/agent-message-fixtures.js";
|
||||||
import { makeZeroUsageSnapshot } from "./usage.js";
|
import { makeZeroUsageSnapshot } from "./usage.js";
|
||||||
|
|
||||||
vi.mock("./pi-embedded-helpers.js", async () => ({
|
vi.mock("./pi-embedded-helpers.js", async () => ({
|
||||||
@@ -136,12 +137,12 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const makeCompactionSummaryMessage = (tokensBefore: number, timestamp: string) =>
|
const makeCompactionSummaryMessage = (tokensBefore: number, timestamp: string) =>
|
||||||
({
|
castAgentMessage({
|
||||||
role: "compactionSummary",
|
role: "compactionSummary",
|
||||||
summary: "compressed",
|
summary: "compressed",
|
||||||
tokensBefore,
|
tokensBefore,
|
||||||
timestamp,
|
timestamp,
|
||||||
}) as unknown as AgentMessage;
|
});
|
||||||
|
|
||||||
const sanitizeOpenAIHistory = async (
|
const sanitizeOpenAIHistory = async (
|
||||||
messages: AgentMessage[],
|
messages: AgentMessage[],
|
||||||
@@ -258,7 +259,7 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
setNonGoogleModelApi();
|
setNonGoogleModelApi();
|
||||||
|
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
{
|
castAgentMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: "forwarded instruction",
|
content: "forwarded instruction",
|
||||||
provenance: {
|
provenance: {
|
||||||
@@ -266,7 +267,7 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
sourceSessionKey: "agent:main:req",
|
sourceSessionKey: "agent:main:req",
|
||||||
sourceTool: "sessions_send",
|
sourceTool: "sessions_send",
|
||||||
},
|
},
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = await sanitizeSessionHistory({
|
const result = await sanitizeSessionHistory({
|
||||||
@@ -287,14 +288,14 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
it("drops stale assistant usage snapshots kept before latest compaction summary", async () => {
|
it("drops stale assistant usage snapshots kept before latest compaction summary", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const messages = [
|
const messages = castAgentMessages([
|
||||||
{ role: "user", content: "old context" },
|
{ role: "user", content: "old context" },
|
||||||
makeAssistantUsageMessage({
|
makeAssistantUsageMessage({
|
||||||
text: "old answer",
|
text: "old answer",
|
||||||
usage: makeUsage(191_919, 2_000, 193_919),
|
usage: makeUsage(191_919, 2_000, 193_919),
|
||||||
}),
|
}),
|
||||||
makeCompactionSummaryMessage(191_919, new Date().toISOString()),
|
makeCompactionSummaryMessage(191_919, new Date().toISOString()),
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const result = await sanitizeOpenAIHistory(messages);
|
const result = await sanitizeOpenAIHistory(messages);
|
||||||
|
|
||||||
@@ -308,7 +309,7 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
it("preserves fresh assistant usage snapshots created after latest compaction summary", async () => {
|
it("preserves fresh assistant usage snapshots created after latest compaction summary", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const messages = [
|
const messages = castAgentMessages([
|
||||||
makeAssistantUsageMessage({
|
makeAssistantUsageMessage({
|
||||||
text: "pre-compaction answer",
|
text: "pre-compaction answer",
|
||||||
usage: makeUsage(120_000, 3_000, 123_000),
|
usage: makeUsage(120_000, 3_000, 123_000),
|
||||||
@@ -319,7 +320,7 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
text: "fresh answer",
|
text: "fresh answer",
|
||||||
usage: makeUsage(1_000, 250, 1_250),
|
usage: makeUsage(1_000, 250, 1_250),
|
||||||
}),
|
}),
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const result = await sanitizeOpenAIHistory(messages);
|
const result = await sanitizeOpenAIHistory(messages);
|
||||||
|
|
||||||
@@ -333,14 +334,14 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const compactionTs = Date.parse("2026-02-26T12:00:00.000Z");
|
const compactionTs = Date.parse("2026-02-26T12:00:00.000Z");
|
||||||
const messages = [
|
const messages = castAgentMessages([
|
||||||
makeCompactionSummaryMessage(191_919, new Date(compactionTs).toISOString()),
|
makeCompactionSummaryMessage(191_919, new Date(compactionTs).toISOString()),
|
||||||
makeAssistantUsageMessage({
|
makeAssistantUsageMessage({
|
||||||
text: "kept pre-compaction answer",
|
text: "kept pre-compaction answer",
|
||||||
timestamp: compactionTs - 1_000,
|
timestamp: compactionTs - 1_000,
|
||||||
usage: makeUsage(191_919, 2_000, 193_919),
|
usage: makeUsage(191_919, 2_000, 193_919),
|
||||||
}),
|
}),
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const result = await sanitizeOpenAIHistory(messages);
|
const result = await sanitizeOpenAIHistory(messages);
|
||||||
|
|
||||||
@@ -354,7 +355,7 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
const compactionTs = Date.parse("2026-02-26T12:00:00.000Z");
|
const compactionTs = Date.parse("2026-02-26T12:00:00.000Z");
|
||||||
const messages = [
|
const messages = castAgentMessages([
|
||||||
makeCompactionSummaryMessage(123_000, new Date(compactionTs).toISOString()),
|
makeCompactionSummaryMessage(123_000, new Date(compactionTs).toISOString()),
|
||||||
makeAssistantUsageMessage({
|
makeAssistantUsageMessage({
|
||||||
text: "kept pre-compaction answer",
|
text: "kept pre-compaction answer",
|
||||||
@@ -367,7 +368,7 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
timestamp: compactionTs + 2_000,
|
timestamp: compactionTs + 2_000,
|
||||||
usage: makeUsage(1_000, 250, 1_250),
|
usage: makeUsage(1_000, 250, 1_250),
|
||||||
}),
|
}),
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const result = await sanitizeOpenAIHistory(messages);
|
const result = await sanitizeOpenAIHistory(messages);
|
||||||
|
|
||||||
@@ -431,13 +432,13 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
{
|
{
|
||||||
name: "missing input or arguments",
|
name: "missing input or arguments",
|
||||||
makeMessages: () =>
|
makeMessages: () =>
|
||||||
[
|
castAgentMessages([
|
||||||
{
|
castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_1", name: "read" }],
|
content: [{ type: "toolCall", id: "call_1", name: "read" }],
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
makeUserMessage("hello"),
|
makeUserMessage("hello"),
|
||||||
] as AgentMessage[],
|
]),
|
||||||
overrides: { sessionId: "test-session" } as Partial<
|
overrides: { sessionId: "test-session" } as Partial<
|
||||||
Parameters<typeof sanitizeOpenAIHistory>[1]
|
Parameters<typeof sanitizeOpenAIHistory>[1]
|
||||||
>,
|
>,
|
||||||
@@ -445,7 +446,7 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
{
|
{
|
||||||
name: "invalid or overlong names",
|
name: "invalid or overlong names",
|
||||||
makeMessages: () =>
|
makeMessages: () =>
|
||||||
[
|
castAgentMessages([
|
||||||
makeAssistantMessage(
|
makeAssistantMessage(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -464,7 +465,7 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
{ stopReason: "toolUse" },
|
{ stopReason: "toolUse" },
|
||||||
),
|
),
|
||||||
makeUserMessage("hello"),
|
makeUserMessage("hello"),
|
||||||
] as AgentMessage[],
|
]),
|
||||||
overrides: {} as Partial<Parameters<typeof sanitizeOpenAIHistory>[1]>,
|
overrides: {} as Partial<Parameters<typeof sanitizeOpenAIHistory>[1]>,
|
||||||
},
|
},
|
||||||
])("drops malformed tool calls: $name", async ({ makeMessages, overrides }) => {
|
])("drops malformed tool calls: $name", async ({ makeMessages, overrides }) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { castAgentMessage } from "../../test-helpers/agent-message-fixtures.js";
|
||||||
import {
|
import {
|
||||||
selectCompactionTimeoutSnapshot,
|
selectCompactionTimeoutSnapshot,
|
||||||
shouldFlagCompactionTimeout,
|
shouldFlagCompactionTimeout,
|
||||||
@@ -32,8 +32,8 @@ describe("compaction-timeout helpers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses pre-compaction snapshot when compaction timeout occurs", () => {
|
it("uses pre-compaction snapshot when compaction timeout occurs", () => {
|
||||||
const pre = [{ role: "assistant", content: "pre" } as unknown as AgentMessage] as const;
|
const pre = [castAgentMessage({ role: "assistant", content: "pre" })] as const;
|
||||||
const current = [{ role: "assistant", content: "current" } as unknown as AgentMessage] as const;
|
const current = [castAgentMessage({ role: "assistant", content: "current" })] as const;
|
||||||
const selected = selectCompactionTimeoutSnapshot({
|
const selected = selectCompactionTimeoutSnapshot({
|
||||||
timedOutDuringCompaction: true,
|
timedOutDuringCompaction: true,
|
||||||
preCompactionSnapshot: [...pre],
|
preCompactionSnapshot: [...pre],
|
||||||
@@ -47,7 +47,7 @@ describe("compaction-timeout helpers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to current snapshot when pre-compaction snapshot is unavailable", () => {
|
it("falls back to current snapshot when pre-compaction snapshot is unavailable", () => {
|
||||||
const current = [{ role: "assistant", content: "current" } as unknown as AgentMessage] as const;
|
const current = [castAgentMessage({ role: "assistant", content: "current" })] as const;
|
||||||
const selected = selectCompactionTimeoutSnapshot({
|
const selected = selectCompactionTimeoutSnapshot({
|
||||||
timedOutDuringCompaction: true,
|
timedOutDuringCompaction: true,
|
||||||
preCompactionSnapshot: null,
|
preCompactionSnapshot: null,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { ImageContent } from "@mariozechner/pi-ai";
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { castAgentMessage } from "../../test-helpers/agent-message-fixtures.js";
|
||||||
import { PRUNED_HISTORY_IMAGE_MARKER, pruneProcessedHistoryImages } from "./history-image-prune.js";
|
import { PRUNED_HISTORY_IMAGE_MARKER, pruneProcessedHistoryImages } from "./history-image-prune.js";
|
||||||
|
|
||||||
describe("pruneProcessedHistoryImages", () => {
|
describe("pruneProcessedHistoryImages", () => {
|
||||||
@@ -8,14 +9,14 @@ describe("pruneProcessedHistoryImages", () => {
|
|||||||
|
|
||||||
it("prunes image blocks from user messages that already have assistant replies", () => {
|
it("prunes image blocks from user messages that already have assistant replies", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
{
|
castAgentMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: "See /tmp/photo.png" }, { ...image }],
|
content: [{ type: "text", text: "See /tmp/photo.png" }, { ...image }],
|
||||||
} as AgentMessage,
|
}),
|
||||||
{
|
castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "got it",
|
content: "got it",
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const didMutate = pruneProcessedHistoryImages(messages);
|
const didMutate = pruneProcessedHistoryImages(messages);
|
||||||
@@ -31,10 +32,10 @@ describe("pruneProcessedHistoryImages", () => {
|
|||||||
|
|
||||||
it("does not prune latest user message when no assistant response exists yet", () => {
|
it("does not prune latest user message when no assistant response exists yet", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
{
|
castAgentMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: "See /tmp/photo.png" }, { ...image }],
|
content: [{ type: "text", text: "See /tmp/photo.png" }, { ...image }],
|
||||||
} as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const didMutate = pruneProcessedHistoryImages(messages);
|
const didMutate = pruneProcessedHistoryImages(messages);
|
||||||
@@ -50,10 +51,10 @@ describe("pruneProcessedHistoryImages", () => {
|
|||||||
|
|
||||||
it("does not change messages when no assistant turn exists", () => {
|
it("does not change messages when no assistant turn exists", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
{
|
castAgentMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: "noop",
|
content: "noop",
|
||||||
} as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const didMutate = pruneProcessedHistoryImages(messages);
|
const didMutate = pruneProcessedHistoryImages(messages);
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { castAgentMessage } from "../test-helpers/agent-message-fixtures.js";
|
||||||
import { dropThinkingBlocks, isAssistantMessageWithContent } from "./thinking.js";
|
import { dropThinkingBlocks, isAssistantMessageWithContent } from "./thinking.js";
|
||||||
|
|
||||||
describe("isAssistantMessageWithContent", () => {
|
describe("isAssistantMessageWithContent", () => {
|
||||||
it("accepts assistant messages with array content and rejects others", () => {
|
it("accepts assistant messages with array content and rejects others", () => {
|
||||||
const assistant = {
|
const assistant = castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
} as AgentMessage;
|
});
|
||||||
const user = { role: "user", content: "hi" } as AgentMessage;
|
const user = castAgentMessage({ role: "user", content: "hi" });
|
||||||
const malformed = { role: "assistant", content: "not-array" } as unknown as AgentMessage;
|
const malformed = castAgentMessage({ role: "assistant", content: "not-array" });
|
||||||
|
|
||||||
expect(isAssistantMessageWithContent(assistant)).toBe(true);
|
expect(isAssistantMessageWithContent(assistant)).toBe(true);
|
||||||
expect(isAssistantMessageWithContent(user)).toBe(false);
|
expect(isAssistantMessageWithContent(user)).toBe(false);
|
||||||
@@ -20,8 +21,8 @@ describe("isAssistantMessageWithContent", () => {
|
|||||||
describe("dropThinkingBlocks", () => {
|
describe("dropThinkingBlocks", () => {
|
||||||
it("returns the original reference when no thinking blocks are present", () => {
|
it("returns the original reference when no thinking blocks are present", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
{ role: "user", content: "hello" } as AgentMessage,
|
castAgentMessage({ role: "user", content: "hello" }),
|
||||||
{ role: "assistant", content: [{ type: "text", text: "world" }] } as AgentMessage,
|
castAgentMessage({ role: "assistant", content: [{ type: "text", text: "world" }] }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = dropThinkingBlocks(messages);
|
const result = dropThinkingBlocks(messages);
|
||||||
@@ -30,13 +31,13 @@ describe("dropThinkingBlocks", () => {
|
|||||||
|
|
||||||
it("drops thinking blocks while preserving non-thinking assistant content", () => {
|
it("drops thinking blocks while preserving non-thinking assistant content", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
{
|
castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
{ type: "thinking", thinking: "internal" },
|
{ type: "thinking", thinking: "internal" },
|
||||||
{ type: "text", text: "final" },
|
{ type: "text", text: "final" },
|
||||||
],
|
],
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = dropThinkingBlocks(messages);
|
const result = dropThinkingBlocks(messages);
|
||||||
@@ -47,10 +48,10 @@ describe("dropThinkingBlocks", () => {
|
|||||||
|
|
||||||
it("keeps assistant turn structure when all content blocks were thinking", () => {
|
it("keeps assistant turn structure when all content blocks were thinking", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
{
|
castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "thinking", thinking: "internal-only" }],
|
content: [{ type: "thinking", thinking: "internal-only" }],
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = dropThinkingBlocks(messages);
|
const result = dropThinkingBlocks(messages);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { castAgentMessage } from "../test-helpers/agent-message-fixtures.js";
|
||||||
import {
|
import {
|
||||||
CONTEXT_LIMIT_TRUNCATION_NOTICE,
|
CONTEXT_LIMIT_TRUNCATION_NOTICE,
|
||||||
PREEMPTIVE_TOOL_RESULT_COMPACTION_PLACEHOLDER,
|
PREEMPTIVE_TOOL_RESULT_COMPACTION_PLACEHOLDER,
|
||||||
@@ -7,35 +8,35 @@ import {
|
|||||||
} from "./tool-result-context-guard.js";
|
} from "./tool-result-context-guard.js";
|
||||||
|
|
||||||
function makeUser(text: string): AgentMessage {
|
function makeUser(text: string): AgentMessage {
|
||||||
return {
|
return castAgentMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: text,
|
content: text,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
} as unknown as AgentMessage;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeToolResult(id: string, text: string): AgentMessage {
|
function makeToolResult(id: string, text: string): AgentMessage {
|
||||||
return {
|
return castAgentMessage({
|
||||||
role: "toolResult",
|
role: "toolResult",
|
||||||
toolCallId: id,
|
toolCallId: id,
|
||||||
toolName: "read",
|
toolName: "read",
|
||||||
content: [{ type: "text", text }],
|
content: [{ type: "text", text }],
|
||||||
isError: false,
|
isError: false,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
} as unknown as AgentMessage;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeLegacyToolResult(id: string, text: string): AgentMessage {
|
function makeLegacyToolResult(id: string, text: string): AgentMessage {
|
||||||
return {
|
return castAgentMessage({
|
||||||
role: "tool",
|
role: "tool",
|
||||||
tool_call_id: id,
|
tool_call_id: id,
|
||||||
tool_name: "read",
|
tool_name: "read",
|
||||||
content: text,
|
content: text,
|
||||||
} as unknown as AgentMessage;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeToolResultWithDetails(id: string, text: string, detailText: string): AgentMessage {
|
function makeToolResultWithDetails(id: string, text: string, detailText: string): AgentMessage {
|
||||||
return {
|
return castAgentMessage({
|
||||||
role: "toolResult",
|
role: "toolResult",
|
||||||
toolCallId: id,
|
toolCallId: id,
|
||||||
toolName: "read",
|
toolName: "read",
|
||||||
@@ -49,7 +50,7 @@ function makeToolResultWithDetails(id: string, text: string, detailText: string)
|
|||||||
},
|
},
|
||||||
isError: false,
|
isError: false,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
} as unknown as AgentMessage;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToolResultText(msg: AgentMessage): string {
|
function getToolResultText(msg: AgentMessage): string {
|
||||||
@@ -199,11 +200,10 @@ describe("installToolResultContextGuard", () => {
|
|||||||
|
|
||||||
it("wraps an existing transformContext and guards the transformed output", async () => {
|
it("wraps an existing transformContext and guards the transformed output", async () => {
|
||||||
const agent = makeGuardableAgent((messages) => {
|
const agent = makeGuardableAgent((messages) => {
|
||||||
return messages.map(
|
return messages.map((msg) =>
|
||||||
(msg) =>
|
castAgentMessage({
|
||||||
({
|
...(msg as unknown as Record<string, unknown>),
|
||||||
...(msg as unknown as Record<string, unknown>),
|
}),
|
||||||
}) as unknown as AgentMessage,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const contextForNextCall = makeTwoToolResultOverflowContext();
|
const contextForNextCall = makeTwoToolResultOverflowContext();
|
||||||
@@ -254,10 +254,10 @@ describe("installToolResultContextGuard", () => {
|
|||||||
|
|
||||||
await agent.transformContext?.(contextForNextCall, new AbortController().signal);
|
await agent.transformContext?.(contextForNextCall, new AbortController().signal);
|
||||||
|
|
||||||
const oldResult = contextForNextCall[1] as unknown as {
|
const oldResult = contextForNextCall[1] as {
|
||||||
details?: unknown;
|
details?: unknown;
|
||||||
};
|
};
|
||||||
const newResult = contextForNextCall[2] as unknown as {
|
const newResult = contextForNextCall[2] as {
|
||||||
details?: unknown;
|
details?: unknown;
|
||||||
};
|
};
|
||||||
const oldResultText = getToolResultText(contextForNextCall[1]);
|
const oldResultText = getToolResultText(contextForNextCall[1]);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|||||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||||
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { castAgentMessage } from "../test-helpers/agent-message-fixtures.js";
|
||||||
import {
|
import {
|
||||||
getCompactionSafeguardRuntime,
|
getCompactionSafeguardRuntime,
|
||||||
setCompactionSafeguardRuntime,
|
setCompactionSafeguardRuntime,
|
||||||
@@ -218,11 +219,11 @@ describe("computeAdaptiveChunkRatio", () => {
|
|||||||
// Small messages: 1000 tokens each, well under 10% of context
|
// Small messages: 1000 tokens each, well under 10% of context
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
{ role: "user", content: "x".repeat(1000), timestamp: Date.now() },
|
{ role: "user", content: "x".repeat(1000), timestamp: Date.now() },
|
||||||
{
|
castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "text", text: "y".repeat(1000) }],
|
content: [{ type: "text", text: "y".repeat(1000) }],
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const ratio = computeAdaptiveChunkRatio(messages, CONTEXT_WINDOW);
|
const ratio = computeAdaptiveChunkRatio(messages, CONTEXT_WINDOW);
|
||||||
@@ -233,11 +234,11 @@ describe("computeAdaptiveChunkRatio", () => {
|
|||||||
// Large messages: ~50K tokens each (25% of context)
|
// Large messages: ~50K tokens each (25% of context)
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
{ role: "user", content: "x".repeat(50_000 * 4), timestamp: Date.now() },
|
{ role: "user", content: "x".repeat(50_000 * 4), timestamp: Date.now() },
|
||||||
{
|
castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "text", text: "y".repeat(50_000 * 4) }],
|
content: [{ type: "text", text: "y".repeat(50_000 * 4) }],
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const ratio = computeAdaptiveChunkRatio(messages, CONTEXT_WINDOW);
|
const ratio = computeAdaptiveChunkRatio(messages, CONTEXT_WINDOW);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|||||||
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { installSessionToolResultGuard } from "./session-tool-result-guard.js";
|
import { installSessionToolResultGuard } from "./session-tool-result-guard.js";
|
||||||
|
import { castAgentMessage } from "./test-helpers/agent-message-fixtures.js";
|
||||||
|
|
||||||
type AppendMessage = Parameters<SessionManager["appendMessage"]>[0];
|
type AppendMessage = Parameters<SessionManager["appendMessage"]>[0];
|
||||||
|
|
||||||
@@ -388,10 +389,10 @@ describe("installSessionToolResultGuard", () => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
message: {
|
message: castAgentMessage({
|
||||||
...(message as unknown as Record<string, unknown>),
|
...(message as unknown as Record<string, unknown>),
|
||||||
content: [{ type: "text", text: "rewritten by hook" }],
|
content: [{ type: "text", text: "rewritten by hook" }],
|
||||||
} as unknown as AgentMessage,
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -425,10 +426,10 @@ describe("installSessionToolResultGuard", () => {
|
|||||||
installSessionToolResultGuard(sm, {
|
installSessionToolResultGuard(sm, {
|
||||||
transformMessageForPersistence: (message) =>
|
transformMessageForPersistence: (message) =>
|
||||||
(message as { role?: string }).role === "user"
|
(message as { role?: string }).role === "user"
|
||||||
? ({
|
? castAgentMessage({
|
||||||
...(message as unknown as Record<string, unknown>),
|
...(message as unknown as Record<string, unknown>),
|
||||||
provenance: { kind: "inter_session", sourceTool: "sessions_send" },
|
provenance: { kind: "inter_session", sourceTool: "sessions_send" },
|
||||||
} as unknown as AgentMessage)
|
})
|
||||||
: message,
|
: message,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { sanitizeToolCallInputs } from "./session-transcript-repair.js";
|
import { sanitizeToolCallInputs } from "./session-transcript-repair.js";
|
||||||
|
import { castAgentMessage, castAgentMessages } from "./test-helpers/agent-message-fixtures.js";
|
||||||
|
|
||||||
function mkSessionsSpawnToolCall(content: string): AgentMessage {
|
function mkSessionsSpawnToolCall(content: string): AgentMessage {
|
||||||
return {
|
return castAgentMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -23,7 +24,7 @@ function mkSessionsSpawnToolCall(content: string): AgentMessage {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
} as unknown as AgentMessage;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("sanitizeToolCallInputs redacts sessions_spawn attachments", () => {
|
describe("sanitizeToolCallInputs redacts sessions_spawn attachments", () => {
|
||||||
@@ -44,7 +45,7 @@ describe("sanitizeToolCallInputs redacts sessions_spawn attachments", () => {
|
|||||||
|
|
||||||
it("redacts attachments content from tool input payloads too", () => {
|
it("redacts attachments content from tool input payloads too", () => {
|
||||||
const secret = "INPUT_SECRET_SHOULD_NOT_PERSIST";
|
const secret = "INPUT_SECRET_SHOULD_NOT_PERSIST";
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -59,7 +60,7 @@ describe("sanitizeToolCallInputs redacts sessions_spawn attachments", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallInputs(input);
|
const out = sanitizeToolCallInputs(input);
|
||||||
const msg = out[0] as { content?: unknown[] };
|
const msg = out[0] as { content?: unknown[] };
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
repairToolUseResultPairing,
|
repairToolUseResultPairing,
|
||||||
stripToolResultDetails,
|
stripToolResultDetails,
|
||||||
} from "./session-transcript-repair.js";
|
} 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"]);
|
const TOOL_CALL_BLOCK_TYPES = new Set(["toolCall", "toolUse", "functionCall"]);
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
middleMessage?: unknown;
|
middleMessage?: unknown;
|
||||||
secondText?: string;
|
secondText?: string;
|
||||||
}): AgentMessage[] =>
|
}): AgentMessage[] =>
|
||||||
[
|
castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
|
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
|
||||||
@@ -37,7 +38,7 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
content: [{ type: "text", text: "first" }],
|
content: [{ type: "text", text: "first" }],
|
||||||
isError: false,
|
isError: false,
|
||||||
},
|
},
|
||||||
...(opts?.middleMessage ? [opts.middleMessage as AgentMessage] : []),
|
...(opts?.middleMessage ? [castAgentMessage(opts.middleMessage)] : []),
|
||||||
{
|
{
|
||||||
role: "toolResult",
|
role: "toolResult",
|
||||||
toolCallId: "call_1",
|
toolCallId: "call_1",
|
||||||
@@ -45,10 +46,10 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
content: [{ type: "text", text: opts?.secondText ?? "second" }],
|
content: [{ type: "text", text: opts?.secondText ?? "second" }],
|
||||||
isError: false,
|
isError: false,
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
it("moves tool results directly after tool calls and inserts missing results", () => {
|
it("moves tool results directly after tool calls and inserts missing results", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -64,7 +65,7 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
isError: false,
|
isError: false,
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolUseResultPairing(input);
|
const out = sanitizeToolUseResultPairing(input);
|
||||||
expect(out[0]?.role).toBe("assistant");
|
expect(out[0]?.role).toBe("assistant");
|
||||||
@@ -76,7 +77,7 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("repairs blank tool result names from matching tool calls", () => {
|
it("repairs blank tool result names from matching tool calls", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
|
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
|
||||||
@@ -88,7 +89,7 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
isError: false,
|
isError: false,
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolUseResultPairing(input);
|
const out = sanitizeToolUseResultPairing(input);
|
||||||
const toolResult = out.find((message) => message.role === "toolResult") as {
|
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", () => {
|
it("drops duplicate tool results for the same id within a span", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
...buildDuplicateToolResultInput(),
|
...buildDuplicateToolResultInput(),
|
||||||
{ role: "user", content: "ok" },
|
{ role: "user", content: "ok" },
|
||||||
] as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolUseResultPairing(input);
|
const out = sanitizeToolUseResultPairing(input);
|
||||||
expect(out.filter((m) => m.role === "toolResult")).toHaveLength(1);
|
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", () => {
|
it("drops orphan tool results that do not match any tool call", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{ role: "user", content: "hello" },
|
{ role: "user", content: "hello" },
|
||||||
{
|
{
|
||||||
role: "toolResult",
|
role: "toolResult",
|
||||||
@@ -136,7 +137,7 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolUseResultPairing(input);
|
const out = sanitizeToolUseResultPairing(input);
|
||||||
expect(out.some((m) => m.role === "toolResult")).toBe(false);
|
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
|
// When an assistant message has stopReason: "error", its tool_use blocks may be
|
||||||
// incomplete/malformed. We should NOT create synthetic tool_results for them,
|
// 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"
|
// as this causes API 400 errors: "unexpected tool_use_id found in tool_result blocks"
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_error", name: "exec", arguments: {} }],
|
content: [{ type: "toolCall", id: "call_error", name: "exec", arguments: {} }],
|
||||||
stopReason: "error",
|
stopReason: "error",
|
||||||
},
|
},
|
||||||
{ role: "user", content: "something went wrong" },
|
{ role: "user", content: "something went wrong" },
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const result = repairToolUseResultPairing(input);
|
const result = repairToolUseResultPairing(input);
|
||||||
|
|
||||||
@@ -169,14 +170,14 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
it("skips tool call extraction for assistant messages with stopReason 'aborted'", () => {
|
it("skips tool call extraction for assistant messages with stopReason 'aborted'", () => {
|
||||||
// When a request is aborted mid-stream, the assistant message may have incomplete
|
// 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.
|
// tool_use blocks (with partialJson). We should NOT create synthetic tool_results.
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_aborted", name: "Bash", arguments: {} }],
|
content: [{ type: "toolCall", id: "call_aborted", name: "Bash", arguments: {} }],
|
||||||
stopReason: "aborted",
|
stopReason: "aborted",
|
||||||
},
|
},
|
||||||
{ role: "user", content: "retrying after abort" },
|
{ role: "user", content: "retrying after abort" },
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const result = repairToolUseResultPairing(input);
|
const result = repairToolUseResultPairing(input);
|
||||||
|
|
||||||
@@ -190,14 +191,14 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
|
|
||||||
it("still repairs tool results for normal assistant messages with stopReason 'toolUse'", () => {
|
it("still repairs tool results for normal assistant messages with stopReason 'toolUse'", () => {
|
||||||
// Normal tool calls (stopReason: "toolUse" or "stop") should still be repaired
|
// Normal tool calls (stopReason: "toolUse" or "stop") should still be repaired
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_normal", name: "read", arguments: {} }],
|
content: [{ type: "toolCall", id: "call_normal", name: "read", arguments: {} }],
|
||||||
stopReason: "toolUse",
|
stopReason: "toolUse",
|
||||||
},
|
},
|
||||||
{ role: "user", content: "user message" },
|
{ role: "user", content: "user message" },
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const result = repairToolUseResultPairing(input);
|
const result = repairToolUseResultPairing(input);
|
||||||
|
|
||||||
@@ -210,7 +211,7 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
// When an assistant message is aborted, any tool results that follow should be
|
// 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).
|
// 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.
|
// This addresses the edge case where a partial tool result was persisted before abort.
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_aborted", name: "exec", arguments: {} }],
|
content: [{ type: "toolCall", id: "call_aborted", name: "exec", arguments: {} }],
|
||||||
@@ -224,7 +225,7 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
isError: false,
|
isError: false,
|
||||||
},
|
},
|
||||||
{ role: "user", content: "retrying" },
|
{ role: "user", content: "retrying" },
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const result = repairToolUseResultPairing(input);
|
const result = repairToolUseResultPairing(input);
|
||||||
|
|
||||||
@@ -244,12 +245,12 @@ describe("sanitizeToolCallInputs", () => {
|
|||||||
options?: Parameters<typeof sanitizeToolCallInputs>[1],
|
options?: Parameters<typeof sanitizeToolCallInputs>[1],
|
||||||
) {
|
) {
|
||||||
return sanitizeToolCallInputs(
|
return sanitizeToolCallInputs(
|
||||||
[
|
castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[],
|
]),
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -262,13 +263,13 @@ describe("sanitizeToolCallInputs", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it("drops tool calls missing input or arguments", () => {
|
it("drops tool calls missing input or arguments", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call_1", name: "read" }],
|
content: [{ type: "toolCall", id: "call_1", name: "read" }],
|
||||||
},
|
},
|
||||||
{ role: "user", content: "hello" },
|
{ role: "user", content: "hello" },
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallInputs(input);
|
const out = sanitizeToolCallInputs(input);
|
||||||
expect(out.map((m) => m.role)).toEqual(["user"]);
|
expect(out.map((m) => m.role)).toEqual(["user"]);
|
||||||
@@ -325,7 +326,7 @@ describe("sanitizeToolCallInputs", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps valid tool calls and preserves text blocks", () => {
|
it("keeps valid tool calls and preserves text blocks", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -334,7 +335,7 @@ describe("sanitizeToolCallInputs", () => {
|
|||||||
{ type: "toolCall", id: "call_drop", name: "read" },
|
{ type: "toolCall", id: "call_drop", name: "read" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallInputs(input);
|
const out = sanitizeToolCallInputs(input);
|
||||||
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
|
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", () => {
|
it("preserves toolUse input shape for sessions_spawn when no attachments are present", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -396,7 +397,7 @@ describe("sanitizeToolCallInputs", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallInputs(input);
|
const out = sanitizeToolCallInputs(input);
|
||||||
const toolCalls = getAssistantToolCallBlocks(out) as Array<Record<string, unknown>>;
|
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", () => {
|
it("redacts sessions_spawn attachments for mixed-case and padded tool names", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -423,7 +424,7 @@ describe("sanitizeToolCallInputs", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallInputs(input);
|
const out = sanitizeToolCallInputs(input);
|
||||||
const toolCalls = getAssistantToolCallBlocks(out) as Array<Record<string, unknown>>;
|
const toolCalls = getAssistantToolCallBlocks(out) as Array<Record<string, unknown>>;
|
||||||
@@ -448,7 +449,7 @@ describe("sanitizeToolCallInputs", () => {
|
|||||||
|
|
||||||
describe("stripToolResultDetails", () => {
|
describe("stripToolResultDetails", () => {
|
||||||
it("removes details only from toolResult messages", () => {
|
it("removes details only from toolResult messages", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "toolResult",
|
role: "toolResult",
|
||||||
toolCallId: "call_1",
|
toolCallId: "call_1",
|
||||||
@@ -458,7 +459,7 @@ describe("stripToolResultDetails", () => {
|
|||||||
},
|
},
|
||||||
{ role: "assistant", content: [{ type: "text", text: "keep me" }], details: { no: "touch" } },
|
{ role: "assistant", content: [{ type: "text", text: "keep me" }], details: { no: "touch" } },
|
||||||
{ role: "user", content: "hello" },
|
{ role: "user", content: "hello" },
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = stripToolResultDetails(input) as unknown as Array<Record<string, unknown>>;
|
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", () => {
|
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: "assistant", content: [{ type: "text", text: "a" }] },
|
||||||
{
|
{
|
||||||
role: "toolResult",
|
role: "toolResult",
|
||||||
@@ -481,7 +482,7 @@ describe("stripToolResultDetails", () => {
|
|||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
},
|
},
|
||||||
{ role: "user", content: "b" },
|
{ role: "user", content: "b" },
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = stripToolResultDetails(input);
|
const out = stripToolResultDetails(input);
|
||||||
expect(out).toBe(input);
|
expect(out).toBe(input);
|
||||||
|
|||||||
66
src/agents/test-helpers/agent-message-fixtures.ts
Normal file
66
src/agents/test-helpers/agent-message-fixtures.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
|
import type { AssistantMessage, ToolResultMessage, Usage, UserMessage } from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
|
const ZERO_USAGE: Usage = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function castAgentMessage(message: unknown): AgentMessage {
|
||||||
|
return message as AgentMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function castAgentMessages(messages: unknown[]): AgentMessage[] {
|
||||||
|
return messages as AgentMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeAgentUserMessage(
|
||||||
|
overrides: Partial<UserMessage> & Pick<UserMessage, "content">,
|
||||||
|
): UserMessage {
|
||||||
|
return {
|
||||||
|
role: "user",
|
||||||
|
timestamp: 0,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeAgentAssistantMessage(
|
||||||
|
overrides: Partial<AssistantMessage> & Pick<AssistantMessage, "content">,
|
||||||
|
): AssistantMessage {
|
||||||
|
return {
|
||||||
|
role: "assistant",
|
||||||
|
api: "openai-responses",
|
||||||
|
provider: "openai",
|
||||||
|
model: "test-model",
|
||||||
|
usage: ZERO_USAGE,
|
||||||
|
stopReason: "stop",
|
||||||
|
timestamp: 0,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeAgentToolResultMessage(
|
||||||
|
overrides: Partial<ToolResultMessage> &
|
||||||
|
Pick<ToolResultMessage, "toolCallId" | "toolName" | "content">,
|
||||||
|
): ToolResultMessage {
|
||||||
|
const { toolCallId, toolName, content, ...rest } = overrides;
|
||||||
|
return {
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId,
|
||||||
|
toolName,
|
||||||
|
content,
|
||||||
|
isError: false,
|
||||||
|
timestamp: 0,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { castAgentMessages } from "./test-helpers/agent-message-fixtures.js";
|
||||||
import {
|
import {
|
||||||
isValidCloudCodeAssistToolId,
|
isValidCloudCodeAssistToolId,
|
||||||
sanitizeToolCallIdsForCloudCodeAssist,
|
sanitizeToolCallIdsForCloudCodeAssist,
|
||||||
} from "./tool-call-id.js";
|
} from "./tool-call-id.js";
|
||||||
|
|
||||||
const buildDuplicateIdCollisionInput = () =>
|
const buildDuplicateIdCollisionInput = () =>
|
||||||
[
|
castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -26,7 +27,7 @@ const buildDuplicateIdCollisionInput = () =>
|
|||||||
toolName: "read",
|
toolName: "read",
|
||||||
content: [{ type: "text", text: "two" }],
|
content: [{ type: "text", text: "two" }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
function expectCollisionIdsRemainDistinct(
|
function expectCollisionIdsRemainDistinct(
|
||||||
out: AgentMessage[],
|
out: AgentMessage[],
|
||||||
@@ -65,7 +66,7 @@ function expectSingleToolCallRewrite(
|
|||||||
describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
||||||
describe("strict mode (default)", () => {
|
describe("strict mode (default)", () => {
|
||||||
it("is a no-op for already-valid non-colliding IDs", () => {
|
it("is a no-op for already-valid non-colliding IDs", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call1", name: "read", arguments: {} }],
|
content: [{ type: "toolCall", id: "call1", name: "read", arguments: {} }],
|
||||||
@@ -76,14 +77,14 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
toolName: "read",
|
toolName: "read",
|
||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
|
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
|
||||||
expect(out).toBe(input);
|
expect(out).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips non-alphanumeric characters from tool call IDs", () => {
|
it("strips non-alphanumeric characters from tool call IDs", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "call|item:123", name: "read", arguments: {} }],
|
content: [{ type: "toolCall", id: "call|item:123", name: "read", arguments: {} }],
|
||||||
@@ -94,7 +95,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
toolName: "read",
|
toolName: "read",
|
||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
|
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
|
||||||
expect(out).not.toBe(input);
|
expect(out).not.toBe(input);
|
||||||
@@ -113,7 +114,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
it("caps tool call IDs at 40 chars while preserving uniqueness", () => {
|
it("caps tool call IDs at 40 chars while preserving uniqueness", () => {
|
||||||
const longA = `call_${"a".repeat(60)}`;
|
const longA = `call_${"a".repeat(60)}`;
|
||||||
const longB = `call_${"a".repeat(59)}b`;
|
const longB = `call_${"a".repeat(59)}b`;
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -133,7 +134,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
toolName: "read",
|
toolName: "read",
|
||||||
content: [{ type: "text", text: "two" }],
|
content: [{ type: "text", text: "two" }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
|
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
|
||||||
const { aId, bId } = expectCollisionIdsRemainDistinct(out, "strict");
|
const { aId, bId } = expectCollisionIdsRemainDistinct(out, "strict");
|
||||||
@@ -144,7 +145,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
|
|
||||||
describe("strict mode (alphanumeric only)", () => {
|
describe("strict mode (alphanumeric only)", () => {
|
||||||
it("strips underscores and hyphens from tool call IDs", () => {
|
it("strips underscores and hyphens from tool call IDs", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -162,7 +163,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
toolName: "login",
|
toolName: "login",
|
||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict");
|
const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict");
|
||||||
expect(out).not.toBe(input);
|
expect(out).not.toBe(input);
|
||||||
@@ -184,7 +185,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
|
|
||||||
describe("strict9 mode (Mistral tool call IDs)", () => {
|
describe("strict9 mode (Mistral tool call IDs)", () => {
|
||||||
it("is a no-op for already-valid 9-char alphanumeric IDs", () => {
|
it("is a no-op for already-valid 9-char alphanumeric IDs", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "toolCall", id: "abc123XYZ", name: "read", arguments: {} }],
|
content: [{ type: "toolCall", id: "abc123XYZ", name: "read", arguments: {} }],
|
||||||
@@ -195,14 +196,14 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
toolName: "read",
|
toolName: "read",
|
||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict9");
|
const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict9");
|
||||||
expect(out).toBe(input);
|
expect(out).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("enforces alphanumeric IDs with length 9", () => {
|
it("enforces alphanumeric IDs with length 9", () => {
|
||||||
const input = [
|
const input = castAgentMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -222,7 +223,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
|||||||
toolName: "read",
|
toolName: "read",
|
||||||
content: [{ type: "text", text: "two" }],
|
content: [{ type: "text", text: "two" }],
|
||||||
},
|
},
|
||||||
] as unknown as AgentMessage[];
|
]);
|
||||||
|
|
||||||
const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict9");
|
const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict9");
|
||||||
expect(out).not.toBe(input);
|
expect(out).not.toBe(input);
|
||||||
|
|||||||
Reference in New Issue
Block a user