deer-flow/frontend/tests/unit/core/tasks/subtask-result.test.ts
AochenShen99 66b9e7f212
feat: emit structured runtime metadata (follow-up#3887) (#3906)
* feat: emit structured runtime metadata

* fix: avoid subagent import cycle in replay gateway

* fix: preserve legacy subtask result parsing

* refactor: tighten runtime metadata contracts

* fix(middleware): keep recovery hint on task exception wrapper content

The structured-metadata stamp overwrote the wrapper text with the bare
task-failure message, dropping the model-facing 'Continue with available
context, or choose an alternative tool.' guidance that every other tool
exception keeps. Append the shared hint after the formatted message.

* fix(subagents): require lowercase hex for result_sha256 reader

Length-only validation accepted any 64-char string; a faulty serializer
or relaying wrapper could store a non-digest value in the delegation
ledger. Enforce the producer's hexdigest shape with a fullmatch.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-04 11:27:19 +08:00

263 lines
8.2 KiB
TypeScript

import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import type { Message } from "@langchain/langgraph-sdk";
import { describe, expect, it } from "@rstest/core";
import {
SUBAGENT_ERROR_KEY,
SUBAGENT_RESULT_BRIEF_KEY,
SUBAGENT_STATUS_KEY,
derivePendingSubtaskStatus,
hasSubtaskToolResult,
parseSubtaskResult,
} from "@/core/tasks/subtask-result";
interface ContractFile {
valid_status_values: string[];
}
const CONTRACT_PATH = resolve(
__dirname,
"../../../../../contracts/subagent_status_contract.json",
);
const CONTRACT: ContractFile = JSON.parse(
readFileSync(CONTRACT_PATH, "utf-8"),
) as ContractFile;
describe("parseSubtaskResult", () => {
it("uses legacy task result text when structured metadata is absent", () => {
expect(
parseSubtaskResult(
"Task Succeeded. Result: investigated and produced a 3-page report",
),
).toEqual({
status: "completed",
result: "investigated and produced a 3-page report",
});
expect(
parseSubtaskResult(
"Task failed. Error: underlying tool raised RuntimeError",
),
).toEqual({
status: "failed",
error: "Error: underlying tool raised RuntimeError",
});
expect(parseSubtaskResult("Task cancelled by user.")).toEqual({
status: "failed",
error: "Task cancelled by user.",
});
expect(parseSubtaskResult("Task timed out. Error: 900 seconds")).toEqual({
status: "failed",
error: "Task timed out. Error: 900 seconds",
});
expect(
parseSubtaskResult(
"Task polling timed out after 15 minutes. Status: RUNNING",
),
).toEqual({
status: "failed",
error: "Task polling timed out after 15 minutes. Status: RUNNING",
});
expect(
parseSubtaskResult("Error: Tool 'task' failed with TypeError: boom"),
).toEqual({
status: "failed",
error: "Error: Tool 'task' failed with TypeError: boom",
});
});
it("keeps unknown content-only task results in progress", () => {
const parsed = parseSubtaskResult("partial streaming chunk");
expect(parsed.status).toBe("in_progress");
expect(parsed.error).toBeUndefined();
expect(parsed.result).toBeUndefined();
});
});
describe("hasSubtaskToolResult", () => {
it("matches a task tool call to its ToolMessage", () => {
const messages = [
{ type: "ai" },
{ type: "tool", tool_call_id: "call_task_1" },
] as Message[];
expect(hasSubtaskToolResult("call_task_1", messages)).toBe(true);
});
it("returns false when a task tool call has no ToolMessage", () => {
const messages = [
{ type: "ai" },
{ type: "tool", tool_call_id: "call_other" },
] as Message[];
expect(hasSubtaskToolResult("call_task_1", messages)).toBe(false);
});
});
describe("derivePendingSubtaskStatus", () => {
it("keeps a task in progress while its own assistant turn is loading", () => {
const messages = [{ type: "ai" }] as Message[];
expect(derivePendingSubtaskStatus("call_task_1", messages, true)).toBe(
"in_progress",
);
});
it("does not revive an earlier unfinished task during a later turn", () => {
const messages = [{ type: "ai" }] as Message[];
expect(derivePendingSubtaskStatus("call_task_1", messages, false)).toBe(
"failed",
);
});
it("leaves result parsing to the ToolMessage path when a result exists", () => {
const messages = [
{ type: "ai" },
{ type: "tool", tool_call_id: "call_task_1" },
] as Message[];
expect(derivePendingSubtaskStatus("call_task_1", messages, false)).toBe(
"in_progress",
);
});
});
/**
* Structured-status path (bytedance/deer-flow#3146).
*
* The backend stamps `ToolMessage.additional_kwargs.subagent_status`
* directly. The frontend should prefer that over reverse-engineering it
* from the content string.
*/
describe("parseSubtaskResult — structured additional_kwargs (preferred path)", () => {
it("uses additional_kwargs.subagent_status when present", () => {
const parsed = parseSubtaskResult("Task Succeeded. Result: foo", {
[SUBAGENT_STATUS_KEY]: "completed",
});
expect(parsed.status).toBe("completed");
});
it("collapses cancelled / timed_out / polling_timed_out to failed for the card UI", () => {
for (const backendStatus of [
"cancelled",
"timed_out",
"polling_timed_out",
]) {
const parsed = parseSubtaskResult("anything at all", {
[SUBAGENT_STATUS_KEY]: backendStatus,
});
expect(parsed.status).toBe("failed");
}
});
it("uses subagent_error when supplied", () => {
const parsed = parseSubtaskResult("ignored content", {
[SUBAGENT_STATUS_KEY]: "failed",
[SUBAGENT_ERROR_KEY]: "boom from backend",
});
expect(parsed.status).toBe("failed");
expect(parsed.error).toBe("boom from backend");
});
it("ignores empty / non-string subagent_error", () => {
const parsed = parseSubtaskResult("ignored content", {
[SUBAGENT_STATUS_KEY]: "failed",
[SUBAGENT_ERROR_KEY]: "",
});
expect(parsed.status).toBe("failed");
expect(parsed.error).toBeUndefined();
});
it("ignores terminal-looking content when partial structured metadata is present", () => {
const parsed = parseSubtaskResult("Task Succeeded. Result: foo", {
[SUBAGENT_RESULT_BRIEF_KEY]: "structured result without status",
});
expect(parsed.status).toBe("in_progress");
expect(parsed.result).toBeUndefined();
});
it("ignores terminal-looking content when the structured status is unknown", () => {
const parsed = parseSubtaskResult("Task Succeeded. Result: foo", {
[SUBAGENT_STATUS_KEY]: "renamed_in_v3",
});
expect(parsed.status).toBe("in_progress");
});
it("structured status overrides misleading content", () => {
const parsed = parseSubtaskResult("Task Succeeded. Result: this is a lie", {
[SUBAGENT_STATUS_KEY]: "failed",
});
expect(parsed.status).toBe("failed");
expect(parsed.result).toBeUndefined();
expect(parsed.error).toBeUndefined();
});
it("does not back-fill result from content when structured result metadata is missing", () => {
const parsed = parseSubtaskResult("Task Succeeded. Result: text-only", {
[SUBAGENT_STATUS_KEY]: "completed",
});
expect(parsed.status).toBe("completed");
expect(parsed.result).toBeUndefined();
});
it("uses bounded structured result metadata when present for completed task", () => {
const parsed = parseSubtaskResult("Task Succeeded. Result: text body", {
[SUBAGENT_STATUS_KEY]: "completed",
subagent_result_brief: "structured",
subagent_result_sha256: "a".repeat(64),
});
expect(parsed.status).toBe("completed");
expect(parsed.result).toBe("structured");
});
it("does not back-fill error from content when structured error metadata is missing", () => {
const parsed = parseSubtaskResult(
"Error: Tool 'task' failed with TypeError: boom",
{
[SUBAGENT_STATUS_KEY]: "failed",
},
);
expect(parsed.status).toBe("failed");
expect(parsed.error).toBeUndefined();
});
it("leaves `error` undefined when structured says failed with no error and unrecognised text", () => {
// Don't dump arbitrary content into the error field — better to render
// an empty `failed` pill than to surface noise.
const parsed = parseSubtaskResult("partial streaming chunk", {
[SUBAGENT_STATUS_KEY]: "failed",
});
expect(parsed.status).toBe("failed");
expect(parsed.error).toBeUndefined();
});
});
/**
* Cross-language contract test for the structured subagent status field.
* The backend and frontend share the enum values, but task result text is
* no longer part of the wire contract.
*/
describe("parseSubtaskResult — shared contract fixture", () => {
const expectedCardStatus = (backendStatus: string): string => {
if (backendStatus === "completed") return "completed";
return "failed";
};
for (const status of CONTRACT.valid_status_values) {
it(`maps structured status: ${status}`, () => {
const parsed = parseSubtaskResult("ignored content", {
[SUBAGENT_STATUS_KEY]: status,
});
expect(parsed.status).toBe(expectedCardStatus(status));
});
}
});