import { fireEvent, render, screen } from "@testing-library/react";
import { ReactFlowProvider } from "@xyflow/react";
import { describe, expect, it, vi } from "vitest";
import { CandidateCard } from "@/components/flow/CandidateCard";
import { RoundSummaryCard } from "@/components/flow/RoundSummaryCard";
import { StoryCard } from "@/components/flow/StoryCard";
import { RunResultCard } from "@/components/flow/RunResultCard";
import { AgentDecisionCard } from "@/components/decision/AgentDecisionCard";
import { MobileTimeline } from "@/components/workbench/MobileTimeline";
import { branchSummaryItem, finalResultItem, multipathDecisionItem, multipathReviewItem, roundSummaryItem, storyItem } from "@/lib/layout";
import { makeBranch, makeRound, makeStory, makeView } from "@/tests/fixtures";
const base = { onOpen: vi.fn(), onToggleRound: vi.fn(), onToggleBranch: vi.fn() };
const nodeProps = { selected: false, zIndex: 1, isConnectable: false, positionAbsoluteX: 0, positionAbsoluteY: 0, dragging: false, draggable: true, selectable: true, deletable: false };
const wrap = (node: React.ReactNode) => render({node});
describe("V8 business cards", () => {
it("labels the creation objective action as details while keeping decision actions explicit", () => {
const objective = storyItem(makeView().objective);
const { unmount } = wrap();
expect(screen.getByRole("button", { name: "查看详情" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "查看决策" })).not.toBeInTheDocument();
unmount();
const dataDecision = storyItem(makeStory("data", "data-decision"), 1, 1);
wrap();
expect(screen.getByRole("button", { name: "查看决策" })).toBeInTheDocument();
});
it("shows implementer data decisions as data decisions", () => {
const item = storyItem(makeStory("data", "data-decision"), 1, 1);
wrap();
expect(screen.getByText("数据取舍")).toBeInTheDocument();
expect(screen.getByText("实现 Agent")).toBeInTheDocument();
});
it("distinguishes domain-information output from candidate script", () => {
const branch = makeBranch(1, 4);
wrap();
expect(screen.getByText(/领域信息/)).toBeInTheDocument();
expect(screen.getByText("领域事实")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /展开方案过程/ })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /查看方案汇总/ })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "查看完整候选脚本" })).not.toBeInTheDocument();
});
it("opens the same artifact viewer from collapsed and expanded candidate output", () => {
const onOpen = vi.fn();
const branch = makeBranch(1, 1);
const summaryItem = branchSummaryItem(branch, 1);
const { unmount } = wrap();
fireEvent.click(screen.getByRole("button", { name: "查看完整候选脚本" }));
expect(onOpen).toHaveBeenCalledWith(summaryItem, "artifact");
unmount();
onOpen.mockClear();
const creativeOutput = makeStory("output", "candidate-output", "decision", { artifactRef: "artifact:branch:1" });
const outputItem = storyItem(creativeOutput, 1, 1);
wrap();
fireEvent.click(screen.getByRole("button", { name: "查看完整候选脚本" }));
expect(onOpen).toHaveBeenCalledWith(outputItem, "artifact");
expect(screen.queryByRole("button", { name: "查看实现任务与提示词" })).not.toBeInTheDocument();
});
it("keeps the candidate artifact action on an expanded mobile creative decision", () => {
const view = makeView(1, 1, 1);
const branch = view.rounds[0].branches[0];
branch.output.node = makeStory("mobile-output", "candidate-output", "decision", { artifactRef: "artifact:branch:1" });
const onOpen = vi.fn();
render();
const artifactButtons = screen.getAllByRole("button", { name: "查看完整候选脚本" });
expect(artifactButtons).toHaveLength(2);
fireEvent.click(artifactButtons[1]);
expect(onOpen).toHaveBeenCalledWith(expect.objectContaining({ id: "mobile-output" }), "artifact");
expect(screen.queryByText("查看实现任务与提示词")).not.toBeInTheDocument();
});
it("keeps runtime association in technical detail instead of the business card", () => {
const story = makeStory("evaluation", "round-evaluation");
story.sourceNotice = "runtime-associated";
story.detailRef = "event:987";
wrap();
expect(screen.queryByText("运行记录关联")).not.toBeInTheDocument();
expect(screen.queryByText("event:987")).not.toBeInTheDocument();
});
it("keeps objective and step summaries visible on the mobile timeline", () => {
const view = makeView(1, 1, 1);
view.objective.card = {
primary: { label: "核心目标", value: "形成清晰可执行的创作方向" },
secondary: [
{ key: "inputs", label: "决策前读取", value: "选题 · 账号段落模式 · 账号人设" },
{ key: "shape", label: "目标结构", value: "2 个创作目标 · 1 个领域评估维度" },
],
};
view.objective.decision!.card = view.objective.card;
view.rounds[0].plan.node.card = {
primary: { label: "实现方式", value: "分工 · 2 路实现" },
secondary: [
{ key: "paths", label: "各路任务", value: "方案 1:补充步骤;方案 2:增强信息" },
{ key: "reason", label: "规划理由", value: "两个任务没有强依赖" },
],
};
view.rounds[0].plan.node.decision!.card = view.rounds[0].plan.node.card;
render();
expect(screen.getByText("选题 · 账号段落模式 · 账号人设")).toBeInTheDocument();
expect(screen.getByText("2 个创作目标 · 1 个领域评估维度")).toBeInTheDocument();
expect(screen.getByText("方案 1:补充步骤;方案 2:增强信息")).toBeInTheDocument();
expect(screen.getByText("两个任务没有强依赖")).toBeInTheDocument();
expect(screen.getAllByText("创作目标")).toHaveLength(1);
expect(screen.getAllByText("本轮目标")).toHaveLength(1);
expect(screen.getAllByText("实现规划")).toHaveLength(1);
});
it("keeps candidate review and main decision as two distinct cards", () => {
const batch = makeRound(1, 2, 1).convergenceBatches[0];
const { unmount } = wrap();
expect(screen.getByText("多方案评审 Agent")).toBeInTheDocument();
expect(screen.getAllByText("评审建议").length).toBeGreaterThanOrEqual(1);
unmount();
wrap();
expect(screen.getByText("主 Agent")).toBeInTheDocument();
expect(screen.getAllByText("最终决定").length).toBeGreaterThanOrEqual(1);
expect(screen.getByText("方案 1、方案 2")).toBeInTheDocument();
});
it("describes a stopped open branch as incomplete process instead of missing data", () => {
const branch = makeBranch(1, 1);
branch.status = "open";
branch.outcome.status = "open";
branch.outcome.label = "构建停止时尚未处置";
branch.outcome.sourceNotice = "process-incomplete";
wrap();
expect(screen.getByText("尚未处置")).toBeInTheDocument();
expect(screen.getByText("流程未完成")).toBeInTheDocument();
expect(screen.queryByText("记录缺失")).not.toBeInTheDocument();
});
it("keeps round process and detail actions visible", () => {
const onOpen = vi.fn();
const round = makeRound(1, 3, 2);
const item = roundSummaryItem(round);
wrap();
expect(screen.getByRole("button", { name: /展开本轮/ })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /查看详情/ })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "查看本轮产出脚本表" }));
expect(onOpen).toHaveBeenCalledWith(item, "artifact");
expect(screen.getByText("本轮目标 1")).toBeInTheDocument();
expect(screen.getByText("3 项具体任务 · 完整内容见详情")).toBeInTheDocument();
expect(screen.getByText("分工 · 3 个实现方案")).toBeInTheDocument();
expect(screen.getByText("2 个决策批次 · 涉及方案 1、2、3")).toBeInTheDocument();
expect(screen.getByText("形成第 1 轮内容产出")).toBeInTheDocument();
});
it("opens the round artifact from the expanded round result", () => {
const onOpen = vi.fn();
const result = makeStory("round:1:result", "round-result", "result", { artifactRef: "artifact:round:1" });
const item = storyItem(result, 1);
wrap();
fireEvent.click(screen.getByRole("button", { name: "查看本轮产出脚本表" }));
expect(onOpen).toHaveBeenCalledWith(item, "artifact");
});
it("labels a completed round without an overall evaluation explicitly", () => {
const result = makeStory("round:1:result", "round-result", "result", { status: "not-evaluated" });
wrap();
expect(screen.getByText("未整体评审")).toBeInTheDocument();
expect(screen.queryByText("尚无结论")).not.toBeInTheDocument();
});
it("uses the run-aware decision summary instead of a generic missing-record label", () => {
const round = makeRound(1, 2, 0);
round.summary.decision.summary = "构建停止前尚未形成多路决策";
wrap();
expect(screen.getByText("构建停止前尚未形成多路决策")).toBeInTheDocument();
expect(screen.queryByText("未记录主 Agent 多路决策")).not.toBeInTheDocument();
});
it("opens the dedicated artifact viewer from the final result", () => {
const onOpen = vi.fn();
const view = makeView();
view.finalResult.summary = "脚本构建终结:已经完成结构、内容和表达检查。\n1. 脚本结构概述:四个段落已经形成完整闭环。";
const item = finalResultItem(view);
wrap();
expect(screen.getByText("当前主脚本已经形成")).toBeInTheDocument();
expect(screen.getByText("构建摘要")).toBeInTheDocument();
expect(screen.getByText("3", { selector: ".artifactStats b" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "查看结果详情" }));
expect(onOpen).toHaveBeenCalledWith(item, "business");
onOpen.mockClear();
fireEvent.click(screen.getByRole("button", { name: "查看完整主脚本" }));
expect(onOpen).toHaveBeenCalledWith(item, "artifact");
onOpen.mockClear();
fireEvent.keyDown(screen.getByRole("button", { name: "查看完整主脚本" }), { key: "Enter" });
expect(onOpen).not.toHaveBeenCalledWith(item, "business");
});
});