mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 22:17:48 +02:00
The header live chip derived its active-task state from the workspace-wide agent-task-snapshot, while the right-panel Execution log read the per-issue task list. Two queries, two endpoints, two independent refetches: the heavier workspace snapshot lands later than the per-issue list, so the log could show a running task while the header chip had not started yet. Point the chip at the same `issueKeys.tasks(issueId)` cache the Execution log uses (identical query options). Both surfaces now observe one cache entry and update atomically. Drop the now-redundant workspace-id lookup and client-side issue_id filter, since the endpoint is already issue-scoped. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
198 lines
6.0 KiB
TypeScript
198 lines
6.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { cleanup, screen } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { AgentTask } from "@multica/core/types";
|
|
import { renderWithI18n } from "../../test/i18n";
|
|
|
|
const mockState = vi.hoisted(() => ({
|
|
tasks: [] as unknown[],
|
|
taskMessagesOptions: vi.fn(),
|
|
// Captures the props the chip passes to PopoverTrigger so a test can assert
|
|
// the card is wired to open on hover, not only on click.
|
|
triggerProps: undefined as Record<string, unknown> | undefined,
|
|
}));
|
|
|
|
vi.mock("@multica/core/workspace/hooks", () => ({
|
|
useActorName: () => ({
|
|
getActorName: (_type: string, id: string) =>
|
|
({
|
|
"agent-1": "Walt",
|
|
"agent-2": "Gus",
|
|
})[id] ?? "Unknown Agent",
|
|
getActorInitials: (_type: string, id: string) =>
|
|
({
|
|
"agent-1": "WA",
|
|
"agent-2": "GU",
|
|
})[id] ?? "UA",
|
|
getActorAvatarUrl: () => null,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@multica/core/chat/queries", () => ({
|
|
taskMessagesOptions: mockState.taskMessagesOptions,
|
|
}));
|
|
|
|
vi.mock("@multica/ui/components/ui/popover", async () => {
|
|
const React = await vi.importActual<typeof import("react")>("react");
|
|
return {
|
|
Popover: ({ children }: { children: React.ReactNode }) => (
|
|
<div data-testid="agent-popover">{children}</div>
|
|
),
|
|
PopoverTrigger: ({
|
|
render,
|
|
children,
|
|
...props
|
|
}: {
|
|
render: React.ReactElement;
|
|
children: React.ReactNode;
|
|
} & Record<string, unknown>) => {
|
|
mockState.triggerProps = props;
|
|
return React.cloneElement(render, undefined, children);
|
|
},
|
|
PopoverContent: ({ children }: { children: React.ReactNode }) => (
|
|
<div data-testid="agent-popover-content">{children}</div>
|
|
),
|
|
};
|
|
});
|
|
|
|
vi.mock("./execution-log-section", () => ({
|
|
ActiveTaskRow: ({ task }: { task: AgentTask }) => (
|
|
<div data-testid="active-task-row">{task.id}</div>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@tanstack/react-query", async () => {
|
|
const actual =
|
|
await vi.importActual<typeof import("@tanstack/react-query")>(
|
|
"@tanstack/react-query",
|
|
);
|
|
|
|
return {
|
|
...actual,
|
|
useQuery: (opts: { queryKey?: readonly unknown[] }) => {
|
|
// Per-issue task list: issueKeys.tasks(issueId) === ["issues","tasks",id]
|
|
if (opts.queryKey?.[0] === "issues" && opts.queryKey?.[1] === "tasks") {
|
|
return { data: mockState.tasks };
|
|
}
|
|
return { data: undefined };
|
|
},
|
|
};
|
|
});
|
|
|
|
import { IssueAgentHeaderChip } from "./issue-agent-header-chip";
|
|
|
|
function makeTask(overrides: Partial<AgentTask>): AgentTask {
|
|
return {
|
|
id: "task-1",
|
|
agent_id: "agent-1",
|
|
runtime_id: "runtime-1",
|
|
issue_id: "issue-1",
|
|
status: "running",
|
|
priority: 0,
|
|
dispatched_at: null,
|
|
started_at: "2026-06-08T08:00:00Z",
|
|
completed_at: null,
|
|
result: null,
|
|
error: null,
|
|
created_at: "2026-06-08T08:00:00Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
mockState.tasks = [];
|
|
mockState.triggerProps = undefined;
|
|
});
|
|
|
|
describe("IssueAgentHeaderChip", () => {
|
|
it("shows the active agent name without event count or elapsed time", () => {
|
|
mockState.tasks = [makeTask({})];
|
|
|
|
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />);
|
|
|
|
expect(
|
|
screen.getByRole("button", { name: "Walt is working" }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText("Walt is working")).toBeInTheDocument();
|
|
expect(screen.queryByText(/events?/i)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/\d+[smh]/i)).not.toBeInTheDocument();
|
|
expect(mockState.taskMessagesOptions).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps the header popover card with active task rows", () => {
|
|
mockState.tasks = [makeTask({ id: "task-running" })];
|
|
|
|
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />);
|
|
|
|
expect(screen.getByTestId("agent-popover-content")).toBeInTheDocument();
|
|
expect(screen.getByTestId("active-task-row")).toHaveTextContent(
|
|
"task-running",
|
|
);
|
|
expect(mockState.taskMessagesOptions).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("opens the activity card on hover, not only on click", () => {
|
|
mockState.tasks = [makeTask({})];
|
|
|
|
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />);
|
|
|
|
// Base UI gates hover-to-open on `openOnHover` on the trigger. Without it
|
|
// the chip would be click-only, which is the behavior MUL-3507 replaces.
|
|
// The trigger stays a real <button>, so click/keyboard access is retained.
|
|
expect(mockState.triggerProps?.openOnHover).toBe(true);
|
|
expect(
|
|
screen.getByRole("button", { name: "Walt is working" }),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("uses the concise multi-agent working label", () => {
|
|
mockState.tasks = [
|
|
makeTask({ id: "task-1", agent_id: "agent-1" }),
|
|
makeTask({ id: "task-2", agent_id: "agent-2" }),
|
|
];
|
|
|
|
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />);
|
|
|
|
expect(
|
|
screen.getByRole("button", { name: "2 agents working" }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getAllByText("2 agents working")).toHaveLength(2);
|
|
expect(screen.getAllByTestId("active-task-row")).toHaveLength(2);
|
|
expect(mockState.taskMessagesOptions).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses the requested Chinese single-agent copy", () => {
|
|
mockState.tasks = [makeTask({})];
|
|
|
|
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />, {
|
|
locale: "zh-Hans",
|
|
});
|
|
|
|
expect(screen.getByText("Walt 在工作")).toBeInTheDocument();
|
|
});
|
|
|
|
it("does not render when the issue has only terminal tasks", () => {
|
|
// The list is issue-scoped by the endpoint, so the chip's only job is to
|
|
// ignore terminal statuses (those are the execution log's story).
|
|
mockState.tasks = [
|
|
makeTask({
|
|
id: "task-done",
|
|
status: "completed",
|
|
completed_at: "2026-06-08T08:05:00Z",
|
|
}),
|
|
makeTask({
|
|
id: "task-cancelled",
|
|
status: "cancelled",
|
|
completed_at: "2026-06-08T08:06:00Z",
|
|
}),
|
|
];
|
|
|
|
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />);
|
|
|
|
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
});
|
|
});
|