deer-flow/frontend/tests/unit/core/threads/tool-result.test.ts
Lucien 178a9ab458
fix(web): preserve tool-result identity when provider call IDs repeat (#7672)
* fix(web): preserve independent results when tool call IDs repeat

* fix(web): preserve leading results and idempotent result replay
2026-10-31 08:41:07 +08:00

227 lines
5.8 KiB
TypeScript

import type { Message } from "@langchain/langgraph-sdk";
import { expect, test } from "@rstest/core";
import { getToolResultStatus, hasToolResult } from "@/core/threads/hooks";
test.each(["success", "error", "pending"] as const)(
"a unique setup call resolves its leading result as %s",
(status) => {
const messages: Message[] = [
...(status === "pending"
? []
: [
{
type: "tool",
tool_call_id: "call_0",
name: "setup_agent",
content:
status === "error" ? "Error: save failed" : "Agent saved",
status,
} as Message,
]),
{
type: "ai",
id: "setup",
content: "",
tool_calls: [{ id: "call_0", name: "setup_agent", args: {} }],
},
];
expect(getToolResultStatus(messages, "setup_agent")).toBe(status);
},
);
test("a setup result before a human boundary cannot finish the next turn", () => {
const messages = [
{ type: "tool", tool_call_id: "call_0", content: "Agent saved" },
{ type: "human", content: "Try again" },
{
type: "ai",
content: "",
tool_calls: [{ id: "call_0", name: "setup_agent", args: {} }],
},
] as Message[];
expect(getToolResultStatus(messages, "setup_agent")).toBe("pending");
});
test.each([false, true])(
"an unrelated assistant fences setup status (leading=%s)",
(leading) => {
const owner = {
type: "ai",
content: "",
tool_calls: [{ id: "call_0", name: "setup_agent", args: {} }],
} as Message;
const other = { type: "ai", content: "An unrelated answer" } as Message;
const result = {
type: "tool",
tool_call_id: "call_0",
content: "Agent saved",
} as Message;
expect(
getToolResultStatus(
leading ? [result, other, owner] : [owner, other, result],
"setup_agent",
),
).toBe("pending");
},
);
test("recognizes a completed tool from its ToolMessage", () => {
const messages = [
{
type: "ai",
content: "",
tool_calls: [{ id: "call-1", name: "setup_agent", args: {} }],
},
{
type: "tool",
content: "Agent saved",
name: "setup_agent",
tool_call_id: "call-1",
},
] as Message[];
expect(hasToolResult(messages, "setup_agent")).toBe(true);
});
test("does not treat a pending call or another tool result as completed", () => {
const pending = [
{
type: "ai",
content: "",
tool_calls: [{ id: "call-1", name: "setup_agent", args: {} }],
},
] as Message[];
const otherTool = [
{
type: "tool",
content: "Done",
name: "web_search",
tool_call_id: "call-2",
},
] as Message[];
expect(hasToolResult(pending, "setup_agent")).toBe(false);
expect(hasToolResult(otherTool, "setup_agent")).toBe(false);
});
test("matches a ToolMessage without a name through its tool call id", () => {
const messages = [
{
type: "ai",
content: "",
tool_calls: [{ id: "call-1", name: "setup_agent", args: {} }],
},
{
type: "tool",
content: "Agent saved",
tool_call_id: "call-1",
},
] as Message[];
expect(hasToolResult(messages, "setup_agent")).toBe(true);
});
test("reports a failed tool result instead of treating it as a successful save", () => {
const messages = [
{
type: "ai",
content: "",
tool_calls: [{ id: "call-1", name: "setup_agent", args: {} }],
},
{
type: "tool",
content: "Error: unable to save agent",
name: "setup_agent",
tool_call_id: "call-1",
status: "error",
},
] as Message[];
expect(getToolResultStatus(messages, "setup_agent")).toBe("error");
});
test("honors structured DeerFlow error metadata", () => {
const messages = [
{
type: "ai",
content: "",
tool_calls: [{ id: "call-1", name: "setup_agent", args: {} }],
},
{
type: "tool",
content: "Agent storage is unavailable",
name: "setup_agent",
tool_call_id: "call-1",
additional_kwargs: {
deerflow_tool_meta: { status: "error" },
},
},
] as Message[];
expect(getToolResultStatus(messages, "setup_agent")).toBe("error");
});
test("waits for the latest setup attempt instead of reusing an older success", () => {
const messages = [
{
type: "ai",
content: "",
tool_calls: [{ id: "call-1", name: "setup_agent", args: {} }],
},
{
type: "tool",
content: "Agent saved",
name: "setup_agent",
tool_call_id: "call-1",
},
{
type: "ai",
content: "",
tool_calls: [{ id: "call-2", name: "setup_agent", args: {} }],
},
] as Message[];
expect(getToolResultStatus(messages, "setup_agent")).toBe("pending");
});
test("another tool reusing a provider id cannot replace the setup result", () => {
const messages = [
{
type: "ai",
content: "",
tool_calls: [{ id: "call_0", name: "setup_agent", args: {} }],
},
{ type: "tool", tool_call_id: "call_0", content: "Saved" },
{
type: "ai",
content: "",
tool_calls: [{ id: "call_0", name: "web_search", args: {} }],
},
{
type: "tool",
tool_call_id: "call_0",
content: "Error: failed",
status: "error",
},
] as Message[];
expect(getToolResultStatus(messages, "setup_agent")).toBe("success");
});
test("a reused provider id cannot complete a new pending setup attempt", () => {
const messages = [
{
type: "ai",
content: "",
tool_calls: [{ id: "call_0", name: "setup_agent", args: {} }],
},
{ type: "tool", tool_call_id: "call_0", content: "Saved" },
{
type: "ai",
content: "",
tool_calls: [{ id: "call_0", name: "setup_agent", args: {} }],
},
] as Message[];
expect(getToolResultStatus(messages, "setup_agent")).toBe("pending");
});