Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

398 lines
12 KiB
TypeScript

import { createEvent, fireEvent, render, screen } from "@testing-library/react";
import { TooltipProvider } from "@tutti-os/ui-system";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AgentGUIAgentTarget } from "../../../types";
import { AgentTargetInfoRendererProvider } from "../../../shared/AgentTargetInfoRendererContext";
import {
AGENT_GUI_PROVIDER_RAIL_PREFERENCES_STORAGE_KEY,
parseAgentGUIProviderRailPreferences
} from "../model/agentGuiProviderRailOrder";
import type { AgentGUIViewLabels } from "./AgentGUINodeView.types";
import { AgentGUIProviderRail } from "./AgentGUIProviderRail";
describe("AgentGUIProviderRail selection", () => {
beforeEach(() => {
globalThis.localStorage.clear();
});
it("derives mutually exclusive aria and visual selection only from conversationFilter", () => {
const readyTarget = target({
agentTargetId: "agent:ready",
label: "Ready Agent",
provider: "ready-provider",
targetId: "legacy:ready"
});
const unavailableTarget = target({
agentTargetId: "agent:offline",
availability: { status: "unavailable" },
disabled: true,
label: "Offline Agent",
provider: "offline-provider",
targetId: "legacy:offline"
});
const placeholderTarget = target({
agentTargetId: "agent:placeholder",
disabled: true,
label: "Placeholder Agent",
provider: "placeholder-provider",
targetId: "local:placeholder-provider"
});
const props = providerRailProps({
agentTargets: [readyTarget, unavailableTarget, placeholderTarget],
conversationFilter: {
kind: "agentTarget",
agentTargetId: "agent:offline"
},
selectedAgentTarget: placeholderTarget
});
const view = render(providerRail(props));
expectOnlySelectedTab("Offline Agent");
view.rerender(
providerRail({
...props,
conversationFilter: { kind: "all" },
selectedAgentTarget: unavailableTarget
})
);
expectOnlySelectedTab("All agents");
view.rerender(
providerRail({
...props,
conversationFilter: {
kind: "agentTarget" as const,
agentTargetId: "agent:placeholder"
},
selectedAgentTarget: readyTarget
})
);
expectOnlySelectedTab("Placeholder Agent");
});
it("uses canonical agentTargetId for tile clicks and fails closed without it", () => {
const onSelectConversationFilterTarget = vi.fn();
const canonicalTarget = target({
agentTargetId: " agent:canonical ",
disabled: true,
label: "Canonical Agent",
provider: "canonical-provider",
targetId: "legacy-provider-target"
});
const compatibilityTarget = target({
label: "Compatibility Agent",
provider: "compatibility-provider",
targetId: "legacy-only-target"
});
render(
providerRail(
providerRailProps({
agentTargets: [canonicalTarget, compatibilityTarget],
onSelectConversationFilterTarget
})
)
);
fireEvent.click(screen.getByRole("tab", { name: "Canonical Agent" }));
expect(onSelectConversationFilterTarget).toHaveBeenCalledTimes(1);
expect(onSelectConversationFilterTarget).toHaveBeenLastCalledWith({
provider: "canonical-provider",
agentTargetId: "agent:canonical"
});
fireEvent.click(screen.getByRole("tab", { name: "Compatibility Agent" }));
expect(onSelectConversationFilterTarget).toHaveBeenCalledTimes(1);
});
it("keeps All clicks aggregate and uses canonical identity for manager fallback", () => {
const onSelectConversationFilterTarget = vi.fn();
const onSelectHomeComposerAgentTarget = vi.fn();
const onUpdateConversationFilter = vi.fn();
const selectedTarget = target({
agentTargetId: "agent:selected",
disabled: true,
label: "Selected Agent",
provider: "selected-provider",
targetId: "legacy:selected"
});
const fallbackTarget = target({
agentTargetId: "agent:fallback",
label: "Fallback Agent",
provider: "fallback-provider",
targetId: "legacy:fallback"
});
const props = providerRailProps({
agentTargets: [selectedTarget, fallbackTarget],
conversationFilter: { kind: "all" },
onSelectConversationFilterTarget,
onSelectHomeComposerAgentTarget,
onUpdateConversationFilter,
selectedAgentTarget: selectedTarget
});
const view = render(providerRail(props));
fireEvent.click(screen.getByRole("tab", { name: "All agents" }));
expect(onUpdateConversationFilter).toHaveBeenLastCalledWith({
kind: "all"
});
expect(onSelectConversationFilterTarget).not.toHaveBeenCalled();
view.rerender(
providerRail({
...props,
conversationFilter: {
kind: "agentTarget" as const,
agentTargetId: "agent:selected"
},
managerOpen: true
})
);
fireEvent.contextMenu(
screen.getByRole("listitem", { name: "Reorder Selected Agent" })
);
fireEvent.click(
screen.getByRole("button", { name: "Remove Selected Agent" })
);
expect(onSelectHomeComposerAgentTarget).toHaveBeenCalledWith({
provider: "fallback-provider",
agentTargetId: "agent:fallback"
});
expect(onUpdateConversationFilter).toHaveBeenLastCalledWith({
kind: "all"
});
});
it("resolves rail container drop targets from provider tile identities", () => {
const codex = target({
agentTargetId: "agent:codex",
label: "Codex",
provider: "codex",
targetId: "local:codex"
});
const claude = target({
agentTargetId: "agent:claude",
label: "Claude",
provider: "claude-code",
targetId: "local:claude"
});
const cursor = target({
agentTargetId: "agent:cursor",
label: "Cursor",
provider: "cursor",
targetId: "local:cursor"
});
render(
providerRail(providerRailProps({ agentTargets: [codex, claude, cursor] }))
);
const codexTile = screen.getByRole("tab", { name: "Codex" });
const claudeTile = screen.getByRole("tab", { name: "Claude" });
const cursorTile = screen.getByRole("tab", { name: "Cursor" });
mockVerticalBounds(codexTile, 0);
mockVerticalBounds(claudeTile, 40);
mockVerticalBounds(cursorTile, 80);
const dataTransfer = createDataTransfer();
fireEvent.dragStart(cursorTile, { dataTransfer });
const tablist = screen.getByRole("tablist");
const dragOverEvent = createEvent.dragOver(tablist, { dataTransfer });
Object.defineProperty(dragOverEvent, "clientY", { value: 5 });
fireEvent(tablist, dragOverEvent);
expect(codexTile).toHaveAttribute("data-drag-over", "before");
const dropEvent = createEvent.drop(tablist, { dataTransfer });
Object.defineProperty(dropEvent, "clientY", { value: 5 });
fireEvent(tablist, dropEvent);
expect(
parseAgentGUIProviderRailPreferences(
globalThis.localStorage.getItem(
AGENT_GUI_PROVIDER_RAIL_PREFERENCES_STORAGE_KEY
)
).order
).toEqual(["local:cursor", "local:codex", "local:claude"]);
expect(
screen.getAllByRole("tab").map((tab) => tab.getAttribute("aria-label"))
).toEqual(["All agents", "Cursor", "Codex", "Claude"]);
});
it("renders a shared owner badge through the Avatar primitive", () => {
render(
providerRail(
providerRailProps({
agentTargets: [
target({
agentTargetId: "agent:shared",
badge: {
iconUrl: "https://cdn.example.test/jackson.png",
label: "Jackson"
},
label: "Shared Codex",
provider: "codex",
targetId: "shared:codex"
})
]
})
)
);
const badge = screen
.getByRole("tab", { name: "Shared Codex, Jackson" })
.querySelector('[data-agent-owner-badge="true"]');
expect(badge).not.toBeNull();
expect(badge?.querySelector('[data-slot="avatar"]')).not.toBeNull();
});
it("lazily renders exact Host target information for the hovered rail tile", async () => {
const sharedTarget = target({
agentTargetId: "agent:shared",
label: "Shared Codex",
ownerDeviceLabel: "Vector's MacBook Pro",
provider: "codex",
targetId: "shared:codex"
});
const renderAgentTargetInfo = vi.fn(({ surface, target }) => (
<div>{`${surface}:${target.agentTargetId}`}</div>
));
render(
<TooltipProvider>
<AgentTargetInfoRendererProvider
agentTargets={[sharedTarget]}
renderer={renderAgentTargetInfo}
>
<AgentGUIProviderRail
{...providerRailProps({ agentTargets: [sharedTarget] })}
/>
</AgentTargetInfoRendererProvider>
</TooltipProvider>
);
expect(renderAgentTargetInfo).not.toHaveBeenCalled();
fireEvent.pointerMove(screen.getByRole("tab", { name: "Shared Codex" }), {
pointerType: "mouse"
});
expect(await screen.findByRole("tooltip")).toHaveTextContent(
"provider-rail:agent:shared"
);
expect(renderAgentTargetInfo).toHaveBeenLastCalledWith({
surface: "provider-rail",
target: sharedTarget
});
});
});
function mockVerticalBounds(element: HTMLElement, top: number): void {
vi.spyOn(element, "getBoundingClientRect").mockReturnValue({
bottom: top + 32,
height: 32,
left: 0,
right: 52,
toJSON: () => ({}),
top,
width: 52,
x: 0,
y: top
});
}
function createDataTransfer(): DataTransfer {
const values = new Map<string, string>();
return {
dropEffect: "none",
effectAllowed: "uninitialized",
getData: (format: string) => values.get(format) ?? "",
setData: (format: string, value: string) => {
values.set(format, value);
}
} as unknown as DataTransfer;
}
function expectOnlySelectedTab(name: string): void {
const tabs = screen.getAllByRole("tab");
const selectedTabs = tabs.filter(
(tab) => tab.getAttribute("aria-selected") === "true"
);
expect(selectedTabs).toHaveLength(1);
expect(selectedTabs[0]).toBe(screen.getByRole("tab", { name }));
for (const tab of tabs) {
const selected = tab === selectedTabs[0];
expect(tab).toHaveAttribute("aria-selected", selected ? "true" : "false");
expect(tab).toHaveAttribute("data-selected", selected ? "true" : "false");
}
}
function target(
input: Pick<AgentGUIAgentTarget, "label" | "provider" | "targetId"> &
Partial<AgentGUIAgentTarget>
): AgentGUIAgentTarget {
return {
ref: { kind: "local", provider: input.provider },
...input
};
}
function providerRailProps(
overrides: Partial<React.ComponentProps<typeof AgentGUIProviderRail>> = {}
): React.ComponentProps<typeof AgentGUIProviderRail> {
return {
activeConversation: null,
activeConversationId: null,
agentTargets: [],
agentTargetsLoading: false,
comingSoonProviders: [],
conversationFilter: { kind: "all" },
conversations: [],
labels: LABELS,
managerOpen: false,
onManagerOpenChange: vi.fn(),
onRequestComposerFocus: vi.fn(),
onSelectConversationFilterTarget: vi.fn(),
onSelectHomeComposerAgentTarget: vi.fn(),
onUpdateConversationFilter: vi.fn(),
providerRailMode: "exact",
selectedAgentTarget: target({
agentTargetId: "agent:default",
label: "Default Agent",
provider: "default-provider",
targetId: "legacy:default"
}),
...overrides
};
}
function providerRail(
props: React.ComponentProps<typeof AgentGUIProviderRail>
): React.ReactElement {
return (
<TooltipProvider>
<AgentGUIProviderRail {...props} />
</TooltipProvider>
);
}
const LABELS = {
addAgentToSidebar: (agent: string) => `Add ${agent}`,
conversationFilterAll: "All agents",
dragAgentToReorder: (agent: string) => `Reorder ${agent}`,
manageAgentsAvailable: "Available",
manageAgentsDescription: "Manage rail agents",
manageAgentsDisabled: "Hidden",
manageAgentsKeepOneAvailable: "Keep one available",
manageAgentsNoAvailable: "No available agents",
manageAgentsNoDisabled: "No hidden agents",
manageAgentsRunningBlocked: (agent: string) => `Cannot remove ${agent}`,
manageAgentsTitle: "Manage agents",
providerSwitchLabel: "Agent filters",
removeAgentFromSidebar: (agent: string) => `Remove ${agent}`
} as AgentGUIViewLabels;