| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- import { describe, expect, it, vi } from "vitest";
- import { buildHorizontalLayout } from "@/lib/layout";
- import { makeStory, makeView } from "@/tests/fixtures";
- const callbacks = { onOpen: vi.fn(), onToggleRound: vi.fn(), onToggleBranch: vi.fn(), onToggleRetrievalAgent: vi.fn(), expandedRetrievalAgents: new Set<string>() };
- describe("V8 divergence, review, and convergence layout", () => {
- it("places the real planning analysis before the persisted creative objective", () => {
- const view = makeView(1, 1, 1);
- view.planning = makeStory("run:planning-analysis", "planning-analysis", "execution", {
- title: "创作规划解析",
- detailRef: "event:3760",
- });
- const result = buildHorizontalLayout(view, { expandedRounds: new Set(), expandedBranches: new Set(), ...callbacks });
- const planning = result.nodes.find((node) => node.id === "run:planning-analysis")!;
- const objective = result.nodes.find((node) => node.id === view.objective.id)!;
- expect(planning.position.x).toBeLessThan(objective.position.x);
- expect(result.edges.some((edge) => edge.source === planning.id && edge.target === objective.id)).toBe(true);
- });
- it("places collapsed rounds in vertically separated rows", () => {
- const view = makeView(10, 2, 1);
- const result = buildHorizontalLayout(view, { expandedRounds: new Set(), expandedBranches: new Set(), ...callbacks });
- const rounds = view.rounds.map((round) => result.nodes.find((node) => node.id === `round:${round.roundIndex}:summary`)!);
- expect(new Set(rounds.map((node) => node.position.x)).size).toBe(1);
- expect(rounds.map((node) => node.position.y)).toEqual([...rounds.map((node) => node.position.y)].sort((a, b) => a - b));
- rounds.slice(1).forEach((node, index) => expect(node.position.y).toBeGreaterThanOrEqual(rounds[index].position.y + 400 + 104));
- });
- it("places expanded rounds on separate rows without adding their widths together", () => {
- const view = makeView(4, 2, 1);
- const expandedRounds = new Set(view.rounds.map((round) => round.roundIndex));
- const result = buildHorizontalLayout(view, { expandedRounds, expandedBranches: new Set(), ...callbacks });
- const rounds = view.rounds.map((round) => result.nodes.find((node) => node.id === `frame:round:${round.roundIndex}`)!);
- expect(new Set(rounds.map((node) => node.position.x)).size).toBe(1);
- rounds.slice(1).forEach((node, index) => {
- const previous = rounds[index];
- expect(node.position.y).toBeGreaterThanOrEqual(previous.position.y + Number(previous.style?.height) + 104);
- });
- });
- it("fans branches out and converges each branch into its recorded decision batch", () => {
- const view = makeView(1, 6, 2);
- const result = buildHorizontalLayout(view, { expandedRounds: new Set([1]), expandedBranches: new Set(), ...callbacks });
- const candidates = result.nodes.filter((node) => node.type === "candidateCard");
- expect(new Set(candidates.map((node) => node.position.y)).size).toBe(6);
- for (const batch of view.rounds[0].convergenceBatches) {
- expect(result.nodes.find((node) => node.id === batch.review?.id)?.type).toBe("agentDecision");
- expect(result.nodes.find((node) => node.id === batch.decision?.id)?.type).toBe("agentDecision");
- }
- for (const batch of view.rounds[0].convergenceBatches) {
- for (const branchId of batch.branchIds) expect(result.edges.some((edge) => edge.source === `round:1:branch:${branchId}:summary` && edge.target === batch.review?.id)).toBe(true);
- expect(result.edges.some((edge) => edge.source === batch.review?.id && edge.target === batch.decision?.id)).toBe(true);
- }
- });
- it("uses backend roles and ids rather than Chinese titles", () => {
- const view = makeView(1, 1, 1);
- view.rounds[0].goal.title = "completely renamed";
- view.rounds[0].convergenceBatches[0].decision!.node.title = "also renamed";
- const result = buildHorizontalLayout(view, { expandedRounds: new Set([1]), expandedBranches: new Set(), ...callbacks });
- expect(result.nodes.some((node) => node.id === "r1-goal")).toBe(true);
- expect(result.nodes.some((node) => node.id === "multipath-1-1")).toBe(true);
- });
- it("keeps 10 expanded implementation lanes separated", () => {
- const view = makeView(1, 10, 2);
- const expandedBranches = new Set(view.rounds[0].branches.map((branch) => `1:${branch.branchId}`));
- const result = buildHorizontalLayout(view, { expandedRounds: new Set([1]), expandedBranches, ...callbacks });
- const frames = result.nodes.filter((node) => node.type === "branchFrame");
- const boxes = frames.map((node) => ({ top: node.position.y, bottom: node.position.y + Number(node.style?.height || 0) })).sort((a, b) => a.top - b.top);
- expect(boxes).toHaveLength(10);
- boxes.slice(1).forEach((box, index) => expect(box.top).toBeGreaterThan(boxes[index].bottom));
- });
- it("renders a missing decision stage without inventing a batch", () => {
- const view = makeView(1, 2, 0);
- const result = buildHorizontalLayout(view, { expandedRounds: new Set([1]), expandedBranches: new Set(), ...callbacks });
- expect(view.rounds[0].convergenceBatches[0].decision).toBeUndefined();
- expect(result.nodes.some((node) => node.id === "r1-missing-review")).toBe(true);
- expect(result.nodes.some((node) => node.id === "r1-missing-decision")).toBe(true);
- });
- it("tightens expanded-round spacing as semantic zoom hides card details", () => {
- const view = makeView(1, 3, 1);
- view.rounds[0].branches[0].summary.task = "很长的候选任务说明。".repeat(40);
- const heightAt = (detailLevel: "full" | "compact" | "overview") => {
- const result = buildHorizontalLayout(view, { expandedRounds: new Set([1]), expandedBranches: new Set(), detailLevel, ...callbacks });
- return Number(result.nodes.find((node) => node.id === "frame:round:1")?.style?.height || 0);
- };
- expect(heightAt("overview")).toBeLessThan(heightAt("compact"));
- expect(heightAt("compact")).toBeLessThan(heightAt("full"));
- });
- it("keeps compact branch cards separated without hiding agent regions", () => {
- const view = makeView(1, 10, 2);
- const result = buildHorizontalLayout(view, { expandedRounds: new Set([1]), expandedBranches: new Set(), detailLevel: "compact", ...callbacks });
- const candidates = result.nodes.filter((node) => node.type === "candidateCard").sort((a, b) => a.position.y - b.position.y);
- expect(candidates).toHaveLength(10);
- expect(result.nodes.filter((node) => node.type === "agentRegion")).toHaveLength(6);
- candidates.slice(1).forEach((node, index) => {
- const gap = node.position.y - candidates[index].position.y - 232;
- expect(gap).toBeGreaterThanOrEqual(18);
- expect(gap).toBeLessThanOrEqual(24);
- });
- });
- it("keeps implementation and retrieval agents visible in overview mode", () => {
- const view = makeView(1, 1, 1);
- const result = buildHorizontalLayout(view, {
- expandedRounds: new Set([1]),
- expandedBranches: new Set(["1:1"]),
- detailLevel: "overview",
- ...callbacks,
- });
- expect(result.nodes.some((node) => node.type === "roundFrame")).toBe(true);
- expect(result.nodes.some((node) => node.type === "branchFrame")).toBe(true);
- expect(result.nodes.some((node) => node.type === "retrievalAgent")).toBe(true);
- expect(result.nodes.filter((node) => node.type === "agentRegion")).toHaveLength(6);
- expect(result.nodes.some((node) => node.type === "roundSummary")).toBe(false);
- });
- it("centers cards by their own height instead of sharing one top coordinate", () => {
- const view = makeView(1, 1, 1);
- view.rounds[0].goal.card.primary!.value = "这是一个较长的本轮目标,用来验证高卡片依然围绕同一条中线排列。".repeat(8);
- const result = buildHorizontalLayout(view, { expandedRounds: new Set([1]), expandedBranches: new Set(), detailLevel: "full", ...callbacks });
- const goal = result.nodes.find((node) => node.id === "r1-goal")!;
- const output = result.nodes.find((node) => node.id === "r1-result")!;
- expect(goal.position.y).toBeLessThan(output.position.y);
- });
- it("stacks retrieval operations vertically even when they belong to different waves", () => {
- const view = makeView(1, 1, 1);
- const branch = view.rounds[0].branches[0];
- const result = buildHorizontalLayout(view, { expandedRounds: new Set([1]), expandedBranches: new Set(["1:1"]), ...callbacks });
- const operationIds = [...branch.retrievalStage.directToolGroups, ...branch.retrievalStage.agentRuns].map((operation) => operation.id);
- const operations = operationIds.map((id) => result.nodes.find((node) => node.id === id)!);
- const stage = result.nodes.find((node) => node.id === `frame:${branch.retrievalStage.id}`)!;
- expect(new Set(operations.map((node) => node.position.x))).toEqual(new Set([20]));
- expect(operations.map((node) => node.position.y)).toEqual([...operations.map((node) => node.position.y)].sort((a, b) => a - b));
- expect(Number(stage.style?.width)).toBe(380);
- });
- it("grows expanded retrieval frames upward as query details make the stack taller", () => {
- const view = makeView(1, 1, 1);
- const branch = view.rounds[0].branches[0];
- const baseOptions = { expandedRounds: new Set([1]), expandedBranches: new Set(["1:1"]), ...callbacks };
- const collapsed = buildHorizontalLayout(view, baseOptions);
- const expanded = buildHorizontalLayout(view, { ...baseOptions, expandedRetrievalAgents: new Set(branch.retrievalStage.agentRuns.map((run) => run.id)) });
- const collapsedRound = collapsed.nodes.find((node) => node.id === "frame:round:1")!;
- const expandedRound = expanded.nodes.find((node) => node.id === "frame:round:1")!;
- expect(Number(expandedRound.style?.height)).toBeGreaterThan(Number(collapsedRound.style?.height));
- expect(expandedRound.position.y).toBeLessThan(collapsedRound.position.y);
- });
- });
|