openclaw/src/agents/tool-call-id.test.ts
zerone0x d0f9e22a4b fix(agents): use alphanumeric-only tool call IDs for OpenRouter compatibility
Some providers like Mistral via OpenRouter require strictly alphanumeric
tool call IDs. The error message indicates: "Tool call id was
whatsapp_login_1768799841527_1 but must be a-z, A-Z, 0-9, with a length
of 9."

Changes:
- Update sanitizeToolCallId to strip all non-alphanumeric characters
  (previously allowed underscores and hyphens)
- Update makeUniqueToolId to use alphanumeric suffixes (x2, x3, etc.)
  instead of underscores
- Update isValidCloudCodeAssistToolId to validate alphanumeric-only IDs
- Update tests to reflect stricter sanitization

Fixes #1359

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 00:41:22 +00:00

142 lines
4.7 KiB
TypeScript

import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { describe, expect, it } from "vitest";
import {
isValidCloudCodeAssistToolId,
sanitizeToolCallIdsForCloudCodeAssist,
} from "./tool-call-id.js";
describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
it("is a no-op for already-valid non-colliding alphanumeric IDs", () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call1", name: "read", arguments: {} }],
},
{
role: "toolResult",
toolCallId: "call1",
toolName: "read",
content: [{ type: "text", text: "ok" }],
},
] satisfies AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
expect(out).toBe(input);
});
it("strips underscores from tool call IDs (Mistral/OpenRouter compatibility)", () => {
const input = [
{
role: "assistant",
content: [
{ type: "toolCall", id: "whatsapp_login_1768799841527_1", name: "login", arguments: {} },
],
},
{
role: "toolResult",
toolCallId: "whatsapp_login_1768799841527_1",
toolName: "login",
content: [{ type: "text", text: "ok" }],
},
] satisfies AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
expect(out).not.toBe(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
const toolCall = assistant.content?.[0] as { id?: string };
// ID should be alphanumeric only, no underscores
expect(toolCall.id).toBe("whatsapplogin17687998415271");
expect(isValidCloudCodeAssistToolId(toolCall.id as string)).toBe(true);
const result = out[1] as Extract<AgentMessage, { role: "toolResult" }>;
expect(result.toolCallId).toBe(toolCall.id);
});
it("avoids collisions when sanitization would produce duplicate IDs", () => {
const input = [
{
role: "assistant",
content: [
{ type: "toolCall", id: "call_a|b", name: "read", arguments: {} },
{ type: "toolCall", id: "call_a:b", name: "read", arguments: {} },
],
},
{
role: "toolResult",
toolCallId: "call_a|b",
toolName: "read",
content: [{ type: "text", text: "one" }],
},
{
role: "toolResult",
toolCallId: "call_a:b",
toolName: "read",
content: [{ type: "text", text: "two" }],
},
] satisfies AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
expect(out).not.toBe(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
const a = assistant.content?.[0] as { id?: string };
const b = assistant.content?.[1] as { id?: string };
expect(typeof a.id).toBe("string");
expect(typeof b.id).toBe("string");
expect(a.id).not.toBe(b.id);
expect(isValidCloudCodeAssistToolId(a.id as string)).toBe(true);
expect(isValidCloudCodeAssistToolId(b.id as string)).toBe(true);
const r1 = out[1] as Extract<AgentMessage, { role: "toolResult" }>;
const r2 = out[2] as Extract<AgentMessage, { role: "toolResult" }>;
expect(r1.toolCallId).toBe(a.id);
expect(r2.toolCallId).toBe(b.id);
});
it("caps tool call IDs at 40 chars while preserving uniqueness", () => {
const longA = `call_${"a".repeat(60)}`;
const longB = `call_${"a".repeat(59)}b`;
const input = [
{
role: "assistant",
content: [
{ type: "toolCall", id: longA, name: "read", arguments: {} },
{ type: "toolCall", id: longB, name: "read", arguments: {} },
],
},
{
role: "toolResult",
toolCallId: longA,
toolName: "read",
content: [{ type: "text", text: "one" }],
},
{
role: "toolResult",
toolCallId: longB,
toolName: "read",
content: [{ type: "text", text: "two" }],
},
] satisfies AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
const a = assistant.content?.[0] as { id?: string };
const b = assistant.content?.[1] as { id?: string };
expect(typeof a.id).toBe("string");
expect(typeof b.id).toBe("string");
expect(a.id).not.toBe(b.id);
expect(a.id?.length).toBeLessThanOrEqual(40);
expect(b.id?.length).toBeLessThanOrEqual(40);
expect(isValidCloudCodeAssistToolId(a.id as string)).toBe(true);
expect(isValidCloudCodeAssistToolId(b.id as string)).toBe(true);
const r1 = out[1] as Extract<AgentMessage, { role: "toolResult" }>;
const r2 = out[2] as Extract<AgentMessage, { role: "toolResult" }>;
expect(r1.toolCallId).toBe(a.id);
expect(r2.toolCallId).toBe(b.id);
});
});