branch:
client-tools-reconnect.test.ts
4599 bytesRaw
import { env } from "cloudflare:workers";
import { describe, it, expect } from "vitest";
import { MessageType } from "../types";
import type { UIMessage as ChatMessage } from "ai";
import { connectChatWS } from "./test-utils";
import { getAgentByName } from "agents";
describe("Client tools after reconnect", () => {
it("should use client tools from CF_AGENT_TOOL_RESULT for continuation", async () => {
const room = crypto.randomUUID();
// Step 1: Set up a conversation with a pending tool call (simulates state before refresh)
const agentStub = await getAgentByName(env.TestChatAgent, room);
const userMessage: ChatMessage = {
id: "msg1",
role: "user",
parts: [{ type: "text", text: "Change the background" }]
};
const toolCallId = "call_reconnect_test";
const assistantMessage: ChatMessage = {
id: "assistant-1",
role: "assistant",
parts: [
{
type: "tool-changeBackgroundColor",
toolCallId,
state: "input-available",
input: { color: "blue" }
}
] as ChatMessage["parts"]
};
// Persist messages directly (simulates state loaded from SQLite after DO restart)
await agentStub.persistMessages([userMessage, assistantMessage]);
// Step 2: Connect (simulates reconnect after refresh)
// Note: We intentionally do NOT send a CF_AGENT_USE_CHAT_REQUEST first,
// so _lastClientTools is never set — this simulates DO restart.
const { ws } = await connectChatWS(`/agents/test-chat-agent/${room}`);
// Wait for connection to be established
await new Promise((resolve) => setTimeout(resolve, 200));
// Clear any captured context from connection setup
await agentStub.clearCapturedContext();
// Step 3: Send tool result WITH clientTools (simulates client approval after reconnect)
const clientTools = [
{
name: "changeBackgroundColor",
description: "Changes the background color",
parameters: {
type: "object",
properties: { color: { type: "string" } }
}
},
{
name: "changeTextColor",
description: "Changes the text color",
parameters: {
type: "object",
properties: { color: { type: "string" } }
}
}
];
ws.send(
JSON.stringify({
type: MessageType.CF_AGENT_TOOL_RESULT,
toolCallId,
toolName: "changeBackgroundColor",
output: { success: true },
autoContinue: true,
clientTools
})
);
// Wait for tool result to be applied + 500ms stream wait + continuation
await new Promise((resolve) => setTimeout(resolve, 1500));
// Step 4: Verify continuation received client tools
const capturedClientTools = await agentStub.getCapturedClientTools();
expect(capturedClientTools).toBeDefined();
expect(capturedClientTools).toHaveLength(2);
expect(capturedClientTools![0].name).toBe("changeBackgroundColor");
expect(capturedClientTools![1].name).toBe("changeTextColor");
ws.close(1000);
});
it("should work without clientTools in CF_AGENT_TOOL_RESULT (backwards compat)", async () => {
const room = crypto.randomUUID();
const agentStub = await getAgentByName(env.TestChatAgent, room);
// Persist a conversation with a pending tool call
await agentStub.persistMessages([
{
id: "msg1",
role: "user",
parts: [{ type: "text", text: "Do something" }]
},
{
id: "assistant-1",
role: "assistant",
parts: [
{
type: "tool-testTool",
toolCallId: "call_compat_test",
state: "input-available",
input: {}
}
] as ChatMessage["parts"]
}
]);
const { ws } = await connectChatWS(`/agents/test-chat-agent/${room}`);
await new Promise((resolve) => setTimeout(resolve, 200));
await agentStub.clearCapturedContext();
// Send tool result WITHOUT clientTools (old client behavior)
ws.send(
JSON.stringify({
type: MessageType.CF_AGENT_TOOL_RESULT,
toolCallId: "call_compat_test",
toolName: "testTool",
output: { success: true },
autoContinue: true
// No clientTools field
})
);
await new Promise((resolve) => setTimeout(resolve, 1500));
// Continuation should still fire, just without client tools
const capturedClientTools = await agentStub.getCapturedClientTools();
expect(capturedClientTools).toBeUndefined();
ws.close(1000);
});
});