mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 13:40:37 +00:00
fix(frontend): reflect tool outcomes in chat steps (#7465)
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
This commit is contained in:
parent
d04ee1d131
commit
a1b89d0432
@ -998,6 +998,9 @@ still retain their error or partial-result status.
|
||||
|
||||
#### MCP Server
|
||||
|
||||
Tool steps mark explicit execution failures. Scheduled-task steps show completed
|
||||
operations only after a valid response, rather than when the request is issued.
|
||||
|
||||
In the chat UI, enable **Token Usage → Debug** to inspect generic/MCP tool calls.
|
||||
Each **Tool details** panel starts collapsed and shows the tool name, call ID,
|
||||
input, and received result or explicit error. Large previews are truncated;
|
||||
|
||||
@ -168,7 +168,7 @@ Edit-and-rerun is deliberately latest-turn-only. `core/messages/utils.ts::getLat
|
||||
|
||||
`MessageGroup` builds its tool-result and browser-preview lookups once per processing group before converting messages to steps. The lookup preserves the first non-empty result and first screenshot-bearing browser view for each tool-call ID, matching the streamed-message display semantics without repeatedly scanning the full group for every tool call.
|
||||
|
||||
Generic tool details receive the explicit Debug flag from `MessageGroup`, independent of token statistics. `tool-call-details.tsx` mounts payload previews only while expanded; `core/messages/tool-detail-preview.ts` caps text output at 12,000 characters, visits at most 12,000 values, and limits nesting to six levels. Serialization budgets include escaped strings, punctuation, indentation, closing delimiters, and truncation markers so structured previews remain valid JSON without a final mid-token slice. Generic calls keep the original ToolMessage content/status. Text is displayed verbatim (or as a bounded prefix), never passed through JSON.parse: reparsing can round numeric IDs, discard duplicate keys, and alter quoted strings. Objects/arrays are formatted only on expansion; specialized renderers retain their existing result conversion. An empty received payload is distinct from a missing message, and neither implies a running/completed state. Copy actions use the shared clipboard fallback, copy the displayed preview, and reset feedback after two seconds. Truncated containers include an inline ellipsis unless an object has already emitted a real ellipsis key; in that case, only the external truncation notice is guaranteed. Arrays stop after a child collapses because the text budget is exhausted, avoiding repeated trailing markers while preserving literal ellipsis elements. Cycle, accessor, and depth-limit markers do not suppress later siblings. Truncated string prefixes preserve complete UTF-16 surrogate pairs. Object traversal stops before a key exceeds the remaining text budget; never shorten property names, since shortened keys can collide with real data. Renderer routing and result conversion share `getToolCallKind`. Disclosure accessible names include the tool name and call ID.
|
||||
Use `core/messages/tool-error.ts` for tool/Debug failures; never classify text or partial results. Scheduler completion needs an object response. Keep details Debug-only via `MessageGroup`. `tool-call-details.tsx` mounts payload previews only while expanded; `core/messages/tool-detail-preview.ts` caps text output at 12,000 characters, visits at most 12,000 values, and limits nesting to six levels. Count escapes, punctuation, indentation, closing delimiters and truncation markers within the budget; never slice structured JSON mid-token. Generic calls keep the original ToolMessage content/status. Text is displayed verbatim (or as a bounded prefix), never passed through JSON.parse: reparsing can round numeric IDs, discard duplicate keys, and alter quoted strings. Format objects/arrays on expansion; retain specialized result conversion. An empty received payload is distinct from a missing message, and neither implies a running/completed state. Copy actions use the shared clipboard fallback, copy the displayed preview, and reset feedback after two seconds. Truncated containers include an inline ellipsis unless an object has already emitted a real ellipsis key; in that case, only the external truncation notice is guaranteed. Arrays stop after a child collapses because the text budget is exhausted, avoiding repeated trailing markers while preserving literal ellipsis elements. Cycle, accessor, and depth-limit markers do not suppress later siblings. Truncated string prefixes preserve complete UTF-16 surrogate pairs. Object traversal stops before a key exceeds the remaining text budget; never shorten property names, since shortened keys can collide with real data. Renderer routing and result conversion share `getToolCallKind`. Disclosure accessible names include the tool name and call ID.
|
||||
|
||||
Array previews coalesce consecutive generated markers only at the end into one omitted-suffix notice. The serializer tracks marker provenance and complete entry boundaries; it must not deduplicate literal ellipsis values or shift the indices of later real elements. A cycle/accessor/depth-limit tail can therefore share one notice while the same markers in the middle retain their positions.
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
resolveArtifactURL,
|
||||
} from "@/core/artifacts/utils";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { isErrorToolMessage } from "@/core/messages/tool-error";
|
||||
import { formatTokenCount } from "@/core/messages/usage";
|
||||
import type { TokenDebugStep } from "@/core/messages/usage-model";
|
||||
import {
|
||||
@ -676,17 +677,24 @@ export function scheduleToolLabel(
|
||||
args: Record<string, unknown> | null | undefined,
|
||||
result: string | Record<string, unknown> | undefined,
|
||||
t: ReturnType<typeof useI18n>["t"],
|
||||
failed = false,
|
||||
): string | null {
|
||||
if (name !== "schedule_task" && name !== "stop_scheduled_task") {
|
||||
return null;
|
||||
}
|
||||
// Both tools always return a JSON object, so text that does not parse is a
|
||||
// tool-level failure (for example rejected arguments), never a success.
|
||||
if (failed) return t.toolCalls.scheduleTaskFailed;
|
||||
if (result == null || (typeof result === "string" && !result.trim())) {
|
||||
return name === "stop_scheduled_task"
|
||||
? t.toolCalls.stopScheduledTaskPending
|
||||
: t.toolCalls.scheduleTaskPending;
|
||||
}
|
||||
// Both tools return an object; a missing or invalid response cannot confirm
|
||||
// that the requested operation succeeded.
|
||||
if (
|
||||
(typeof result === "string" && result.trim() !== "") ||
|
||||
(typeof result === "object" &&
|
||||
result !== null &&
|
||||
("error" in result || "code" in result))
|
||||
typeof result !== "object" ||
|
||||
Array.isArray(result) ||
|
||||
"error" in result ||
|
||||
"code" in result
|
||||
) {
|
||||
return t.toolCalls.scheduleTaskFailed;
|
||||
}
|
||||
@ -765,12 +773,25 @@ function ToolCall({
|
||||
const tokenLabel = tokenDebugStep
|
||||
? formatDebugToken(tokenDebugStep, t)
|
||||
: null;
|
||||
const resolveLabel = (fallback: React.ReactNode) =>
|
||||
tokenDebugStep ? (
|
||||
const failed = resultMessage ? isErrorToolMessage(resultMessage) : false;
|
||||
const schedulerTool =
|
||||
kind === "schedule_task" || kind === "stop_scheduled_task";
|
||||
const resolveLabel = (fallback: React.ReactNode) => {
|
||||
const label = tokenDebugStep ? (
|
||||
<DebugStepLabel label={tokenDebugStep.label} token={tokenLabel} />
|
||||
) : (
|
||||
fallback
|
||||
);
|
||||
if (!failed || (schedulerTool && !tokenDebugStep)) return label;
|
||||
return (
|
||||
<>
|
||||
{label}
|
||||
<span className="text-destructive ml-2 text-xs" role="status">
|
||||
{t.toolCalls.failed}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const writeFilePath =
|
||||
(kind === "write_file" || kind === "str_replace") &&
|
||||
typeof args.path === "string"
|
||||
@ -1114,7 +1135,7 @@ function ToolCall({
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label={resolveLabel(scheduleToolLabel(name, args, result, t))}
|
||||
label={resolveLabel(scheduleToolLabel(name, args, result, t, failed))}
|
||||
icon={stop ? CalendarX2Icon : CalendarClockIcon}
|
||||
className={stop ? "text-sky-700 dark:text-sky-400" : undefined}
|
||||
data-testid={stop ? "scheduled-stop-step" : undefined}
|
||||
|
||||
@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { writeTextToClipboard } from "@/core/clipboard";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { formatToolDetail } from "@/core/messages/tool-detail-preview";
|
||||
import { isErrorToolMessage } from "@/core/messages/tool-error";
|
||||
|
||||
export function ToolCallDetails({
|
||||
name,
|
||||
@ -43,7 +44,7 @@ export function ToolCallDetails({
|
||||
{resultMessage ? (
|
||||
<Payload
|
||||
label={
|
||||
resultMessage.status === "error"
|
||||
isErrorToolMessage(resultMessage)
|
||||
? t.toolCalls.error
|
||||
: t.toolCalls.result
|
||||
}
|
||||
|
||||
@ -1727,6 +1727,9 @@ export const enUS: Translations = {
|
||||
// Tool calls
|
||||
toolCalls: {
|
||||
details: "Tool details",
|
||||
failed: "Failed",
|
||||
scheduleTaskPending: "Scheduled task operation",
|
||||
stopScheduledTaskPending: "Pause this scheduled task",
|
||||
toolName: "Tool name",
|
||||
callId: "Call ID",
|
||||
input: "Input",
|
||||
|
||||
@ -1415,6 +1415,9 @@ export interface Translations {
|
||||
// Tool calls
|
||||
toolCalls: {
|
||||
details: string;
|
||||
failed: string;
|
||||
scheduleTaskPending: string;
|
||||
stopScheduledTaskPending: string;
|
||||
toolName: string;
|
||||
callId: string;
|
||||
input: string;
|
||||
|
||||
@ -1619,6 +1619,9 @@ export const zhCN: Translations = {
|
||||
// Tool calls
|
||||
toolCalls: {
|
||||
details: "工具详情",
|
||||
failed: "执行失败",
|
||||
scheduleTaskPending: "定时任务操作",
|
||||
stopScheduledTaskPending: "暂停此定时任务",
|
||||
toolName: "工具名称",
|
||||
callId: "调用 ID",
|
||||
input: "输入",
|
||||
|
||||
13
frontend/src/core/messages/tool-error.ts
Normal file
13
frontend/src/core/messages/tool-error.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
|
||||
export function isErrorToolMessage(message: Message): boolean {
|
||||
if (message.type !== "tool") return false;
|
||||
if (message.status === "error") return true;
|
||||
const metadata = message.additional_kwargs?.deerflow_tool_meta;
|
||||
return (
|
||||
typeof metadata === "object" &&
|
||||
metadata !== null &&
|
||||
"status" in metadata &&
|
||||
metadata.status === "error"
|
||||
);
|
||||
}
|
||||
@ -20,6 +20,7 @@ import { fetch } from "../api/fetcher";
|
||||
import { getBackendBaseURL } from "../config";
|
||||
import { useI18n } from "../i18n/hooks";
|
||||
import { getMessageRunId } from "../messages/run-duration";
|
||||
import { isErrorToolMessage } from "../messages/tool-error";
|
||||
import {
|
||||
extractContentFromMessage,
|
||||
hasToolCalls,
|
||||
@ -156,22 +157,6 @@ export type PendingPreparedReplayMask = {
|
||||
|
||||
export type ToolResultStatus = "pending" | "success" | "error";
|
||||
|
||||
function isErrorToolMessage(message: Message): boolean {
|
||||
if (message.type !== "tool") {
|
||||
return false;
|
||||
}
|
||||
if (message.status === "error") {
|
||||
return true;
|
||||
}
|
||||
const metadata = message.additional_kwargs?.deerflow_tool_meta;
|
||||
return (
|
||||
typeof metadata === "object" &&
|
||||
metadata !== null &&
|
||||
"status" in metadata &&
|
||||
metadata.status === "error"
|
||||
);
|
||||
}
|
||||
|
||||
export function getToolResultStatus(
|
||||
messages: Message[],
|
||||
toolName: string,
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { createServer } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { mockLangGraphAPI } from "./utils/mock-api";
|
||||
@ -133,3 +136,274 @@ for (const { debug, content, status, label } of [
|
||||
await expect(error).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
|
||||
for (const locale of ["en-US", "zh-CN"] as const) {
|
||||
test(`tool failures and confirmed scheduler labels (${locale})`, async ({
|
||||
page,
|
||||
baseURL,
|
||||
}) => {
|
||||
const zh = locale === "zh-CN";
|
||||
const runId = "00000000-0000-0000-0000-000000004390";
|
||||
await page.setViewportSize({ width: 1440, height: 1000 });
|
||||
await page
|
||||
.context()
|
||||
.addCookies([{ name: "locale", value: locale, url: baseURL! }]);
|
||||
const messages: unknown[] = [
|
||||
{
|
||||
type: "human",
|
||||
id: "human",
|
||||
content: zh
|
||||
? "创建定时任务,然后核对输入文件和账户。"
|
||||
: "Create a scheduled task, then check the input file and account.",
|
||||
},
|
||||
{
|
||||
type: "ai",
|
||||
id: "ai-schedule",
|
||||
content: "",
|
||||
tool_calls: [
|
||||
{ id: "schedule", name: "schedule_task", args: { action: "create" } },
|
||||
],
|
||||
},
|
||||
];
|
||||
const controller = mockLangGraphAPI(page, {
|
||||
threads: [
|
||||
{
|
||||
thread_id: threadId,
|
||||
title: zh ? "工具执行反馈" : "Tool execution feedback",
|
||||
messages: [messages[0]],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.route("**/api/models", (route) =>
|
||||
route.fulfill({ json: { models: [], token_usage: { enabled: true } } }),
|
||||
);
|
||||
let finished = false;
|
||||
const run = () => ({
|
||||
run_id: runId,
|
||||
thread_id: threadId,
|
||||
assistant_id: "lead_agent",
|
||||
status: finished ? "success" : "running",
|
||||
metadata: {},
|
||||
kwargs: {},
|
||||
created_at: "2026-10-25T00:00:00Z",
|
||||
updated_at: "2026-10-25T00:00:01Z",
|
||||
});
|
||||
await page.route(/\/api\/langgraph\/threads\/[^/]+\/runs(\?|$)/, (route) =>
|
||||
route.fulfill({ json: [run()] }),
|
||||
);
|
||||
await page.route(
|
||||
`**/api/langgraph/threads/${threadId}/runs/${runId}`,
|
||||
(route) => route.fulfill({ json: run() }),
|
||||
);
|
||||
// Stream actual SSE bytes so the pending assertion runs before any tool receipt.
|
||||
let finishStream: (() => void) | undefined;
|
||||
const server = createServer((_request, response) => {
|
||||
response.writeHead(200, {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Type": "text/event-stream",
|
||||
});
|
||||
const frame = (event: string, data: unknown) =>
|
||||
`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
||||
response.write(frame("metadata", { run_id: runId, thread_id: threadId }));
|
||||
response.write(frame("values", { messages }));
|
||||
finishStream = () =>
|
||||
response.end(frame("values", { messages }) + frame("end", {}));
|
||||
});
|
||||
await new Promise<void>((resolve) =>
|
||||
server.listen(0, "127.0.0.1", resolve),
|
||||
);
|
||||
const { port } = server.address() as AddressInfo;
|
||||
await page.route(
|
||||
`**/api/langgraph/threads/${threadId}/runs/${runId}/stream*`,
|
||||
(route) => route.continue({ url: `http://127.0.0.1:${port}/stream` }),
|
||||
);
|
||||
try {
|
||||
await page.goto(`/workspace/chats/${threadId}`);
|
||||
await expect(
|
||||
page.getByText(zh ? "定时任务操作" : "Scheduled task operation", {
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(zh ? "已创建定时任务" : "Scheduled a task", {
|
||||
exact: true,
|
||||
}),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("button", {
|
||||
name: zh ? /^工具详情:/ : /^Tool details:/,
|
||||
}),
|
||||
).toHaveCount(0);
|
||||
await page.screenshot({
|
||||
path: test.info().outputPath(`scheduler-pending-${locale}.png`),
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
});
|
||||
messages.push({
|
||||
type: "tool",
|
||||
id: "schedule-result",
|
||||
tool_call_id: "schedule",
|
||||
content: JSON.stringify({ action: "create" }),
|
||||
status: "success",
|
||||
});
|
||||
for (const [id, name, description, content, status, meta] of [
|
||||
[
|
||||
"bash-error",
|
||||
"bash",
|
||||
zh ? "核对输入文件" : "Check input file",
|
||||
"File not found",
|
||||
"error",
|
||||
"error",
|
||||
],
|
||||
[
|
||||
"mcp-error",
|
||||
"mcp_lookup",
|
||||
zh ? "查询账户" : "Look up account",
|
||||
"Provider denied the request",
|
||||
"success",
|
||||
"error",
|
||||
],
|
||||
[
|
||||
"read-ok",
|
||||
"read_file",
|
||||
zh ? "读取本地记录" : "Read local record",
|
||||
'{"error":"ordinary document data"}',
|
||||
"success",
|
||||
"success",
|
||||
],
|
||||
] as const) {
|
||||
messages.push(
|
||||
{
|
||||
type: "ai",
|
||||
id: `ai-${id}`,
|
||||
content: "",
|
||||
tool_calls: [
|
||||
{
|
||||
id,
|
||||
name,
|
||||
args: {
|
||||
description,
|
||||
path: "/mnt/user-data/workspace/input.json",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "tool",
|
||||
id: `result-${id}`,
|
||||
tool_call_id: id,
|
||||
content,
|
||||
status,
|
||||
additional_kwargs: { deerflow_tool_meta: { status: meta } },
|
||||
},
|
||||
);
|
||||
}
|
||||
messages.push({
|
||||
type: "ai",
|
||||
id: "answer",
|
||||
content: zh
|
||||
? "定时任务已创建,文件和账户核对未成功。"
|
||||
: "The scheduled task was created; the file and account checks failed.",
|
||||
});
|
||||
controller.upsertThread({
|
||||
thread_id: threadId,
|
||||
title: zh ? "工具执行反馈" : "Tool execution feedback",
|
||||
messages,
|
||||
});
|
||||
finished = true;
|
||||
finishStream?.();
|
||||
const expandSteps = () =>
|
||||
page.getByRole("button", {
|
||||
name: zh ? /查看其他.*步骤/ : /more steps/,
|
||||
});
|
||||
await expandSteps().click();
|
||||
await expect(
|
||||
page.getByText(zh ? "已创建定时任务" : "Scheduled a task", {
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(zh ? "执行失败" : "Failed", { exact: true }),
|
||||
).toHaveCount(2);
|
||||
await expect(
|
||||
page.getByRole("button", {
|
||||
name: zh ? /^工具详情:/ : /^Tool details:/,
|
||||
}),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByText(zh ? "结果可能不完整" : "Result may be incomplete", {
|
||||
exact: true,
|
||||
}),
|
||||
).toHaveCount(0);
|
||||
const successfulRead = page.getByText(
|
||||
zh ? "读取本地记录" : "Read local record",
|
||||
{ exact: true },
|
||||
);
|
||||
await expect(successfulRead).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
successfulRead.evaluate((element) => {
|
||||
let opacity = 1;
|
||||
for (
|
||||
let node: Element | null = element;
|
||||
node;
|
||||
node = node.parentElement
|
||||
) {
|
||||
opacity *= Number(getComputedStyle(node).opacity);
|
||||
}
|
||||
return opacity;
|
||||
}),
|
||||
)
|
||||
.toBe(1);
|
||||
await page.mouse.move(0, 0);
|
||||
await page.screenshot({
|
||||
path: test.info().outputPath(`normal-errors-${locale}.png`),
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
});
|
||||
await page.reload();
|
||||
await expandSteps().click();
|
||||
await expect(
|
||||
page.getByText(zh ? "执行失败" : "Failed", { exact: true }),
|
||||
).toHaveCount(2);
|
||||
await page.evaluate(() =>
|
||||
localStorage.setItem(
|
||||
"deerflow.local-settings",
|
||||
JSON.stringify({
|
||||
tokenUsage: { headerTotal: true, inlineMode: "step_debug" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
await page.reload();
|
||||
await expandSteps().click();
|
||||
await expect(
|
||||
page.getByRole("button", {
|
||||
name: zh ? /^工具详情:/ : /^Tool details:/,
|
||||
}),
|
||||
).toHaveCount(2);
|
||||
await page
|
||||
.getByRole("button", {
|
||||
name: `${zh ? "工具详情" : "Tool details"}: mcp_lookup (mcp-error)`,
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
await expect(
|
||||
page.getByRole("region", { name: zh ? "错误" : "Error", exact: true }),
|
||||
).toContainText("Provider denied the request");
|
||||
await expect(
|
||||
page.getByRole("region", {
|
||||
name: zh ? "调用 ID" : "Call ID",
|
||||
exact: true,
|
||||
}),
|
||||
).toContainText("mcp-error");
|
||||
await page.screenshot({
|
||||
path: test.info().outputPath(`existing-debug-${locale}.png`),
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
});
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -652,14 +652,28 @@ describe("scheduler tool steps", () => {
|
||||
] as const;
|
||||
|
||||
it.each(actions)("labels schedule_task %s in en and zh", (action, en, zh) => {
|
||||
expect(
|
||||
scheduleToolLabel("schedule_task", { action }, undefined, enUS),
|
||||
).toBe(en);
|
||||
expect(
|
||||
scheduleToolLabel("schedule_task", { action }, undefined, zhCN),
|
||||
).toBe(zh);
|
||||
expect(scheduleToolLabel("schedule_task", { action }, {}, enUS)).toBe(en);
|
||||
expect(scheduleToolLabel("schedule_task", { action }, {}, zhCN)).toBe(zh);
|
||||
});
|
||||
|
||||
it.each([undefined, ""])(
|
||||
"does not announce a completed operation for %j",
|
||||
(result) => {
|
||||
expect(
|
||||
scheduleToolLabel("schedule_task", { action: "create" }, result, enUS),
|
||||
).toBe("Scheduled task operation");
|
||||
expect(
|
||||
scheduleToolLabel("schedule_task", { action: "create" }, result, zhCN),
|
||||
).toBe("定时任务操作");
|
||||
expect(scheduleToolLabel("stop_scheduled_task", {}, result, enUS)).toBe(
|
||||
"Pause this scheduled task",
|
||||
);
|
||||
expect(scheduleToolLabel("stop_scheduled_task", {}, result, zhCN)).toBe(
|
||||
"暂停此定时任务",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("labels a coded error result as a failed change", () => {
|
||||
const error = { error: "Ask which timezone", code: "timezone_required" };
|
||||
expect(
|
||||
@ -680,20 +694,15 @@ describe("scheduler tool steps", () => {
|
||||
"定时任务操作没有成功",
|
||||
);
|
||||
expect(
|
||||
scheduleToolLabel(
|
||||
"schedule_task",
|
||||
{ action: "toString" },
|
||||
undefined,
|
||||
enUS,
|
||||
),
|
||||
scheduleToolLabel("schedule_task", { action: "toString" }, {}, enUS),
|
||||
).toBe("Managed scheduled tasks");
|
||||
});
|
||||
|
||||
it("labels the self-stop and ignores other tools", () => {
|
||||
expect(scheduleToolLabel("stop_scheduled_task", {}, undefined, enUS)).toBe(
|
||||
expect(scheduleToolLabel("stop_scheduled_task", {}, {}, enUS)).toBe(
|
||||
"Paused this scheduled task (takes effect when this run ends)",
|
||||
);
|
||||
expect(scheduleToolLabel("stop_scheduled_task", {}, undefined, zhCN)).toBe(
|
||||
expect(scheduleToolLabel("stop_scheduled_task", {}, {}, zhCN)).toBe(
|
||||
"已暂停此定时任务(本次运行结束后生效)",
|
||||
);
|
||||
expect(scheduleToolLabel("bash", {}, undefined, enUS)).toBeNull();
|
||||
|
||||
@ -108,9 +108,11 @@ describe("generic tool details", () => {
|
||||
{ id: "call-2", name: "mcp_lookup", args: {} },
|
||||
],
|
||||
} as Message,
|
||||
tool("first call failed", "error"),
|
||||
]),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "1 more step" }));
|
||||
expect(screen.getAllByText("Failed", { exact: true })).toHaveLength(1);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Tool details: mcp_lookup (call-2)" }),
|
||||
);
|
||||
@ -182,7 +184,10 @@ describe("generic tool details", () => {
|
||||
render(
|
||||
group([
|
||||
tool(""),
|
||||
tool('{"reason":"denied"}', "error"),
|
||||
{
|
||||
...tool('{"reason":"denied"}'),
|
||||
additional_kwargs: { deerflow_tool_meta: { status: "error" } },
|
||||
} as Message,
|
||||
call,
|
||||
tool("later result"),
|
||||
]),
|
||||
@ -263,6 +268,67 @@ describe("generic tool details", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["bash", "read_file", "mcp_lookup"])(
|
||||
"shows explicit %s failures without adding normal-mode details",
|
||||
(name) => {
|
||||
const invocation = {
|
||||
...call,
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call-1",
|
||||
name,
|
||||
args: {
|
||||
description: "Requested operation",
|
||||
path: "/mnt/user-data/workspace/input.txt",
|
||||
},
|
||||
},
|
||||
],
|
||||
} as Message;
|
||||
const { rerender } = render(
|
||||
group(
|
||||
[
|
||||
invocation,
|
||||
{
|
||||
...tool("provider denied"),
|
||||
additional_kwargs: { deerflow_tool_meta: { status: "error" } },
|
||||
} as Message,
|
||||
],
|
||||
false,
|
||||
),
|
||||
);
|
||||
expect(screen.getByText("Failed", { exact: true })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: /^Tool details:/ })).toBeNull();
|
||||
for (const result of [
|
||||
undefined,
|
||||
tool('{"error":"ordinary document data"}'),
|
||||
{
|
||||
...tool("limited results"),
|
||||
additional_kwargs: {
|
||||
deerflow_tool_meta: { status: "partial_success" },
|
||||
},
|
||||
} as Message,
|
||||
]) {
|
||||
rerender(group(result ? [invocation, result] : [invocation], false));
|
||||
expect(screen.queryByText("Failed", { exact: true })).toBeNull();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("does not announce scheduler success when a received object is marked as failed", () => {
|
||||
const invocation = {
|
||||
...call,
|
||||
tool_calls: [
|
||||
{ id: "call-1", name: "schedule_task", args: { action: "create" } },
|
||||
],
|
||||
} as Message;
|
||||
render(group([invocation, tool("{}", "error")], false));
|
||||
expect(
|
||||
screen.getByText("The scheduled task change didn't go through"),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText("Scheduled a task")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: /^Tool details:/ })).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
"9223372036854775807",
|
||||
'{"run_id":9223372036854775807,"ratio":0.1234567890123456789}',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user