mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
Show agent models on agents page
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
38b57c5d56
commit
47d862b4b2
2 files changed
+174
-2
No files matched your search
@@ -0,0 +1,153 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { Agents } from "./Agents";
|
||||
|
||||
const mockAgentsApi = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
org: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockHeartbeatsApi = vi.hoisted(() => ({
|
||||
liveRunsForCompany: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockOpenNewAgent = vi.hoisted(() => vi.fn());
|
||||
const mockSetBreadcrumbs = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@/lib/router", () => ({
|
||||
Link: ({ children, to, ...props }: { children: ReactNode; to: string }) => (
|
||||
<a href={to} {...props}>{children}</a>
|
||||
),
|
||||
useLocation: () => ({ pathname: "/agents/all", search: "", hash: "", state: null }),
|
||||
useNavigate: () => vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../context/CompanyContext", () => ({
|
||||
useCompany: () => ({ selectedCompanyId: "company-1" }),
|
||||
}));
|
||||
|
||||
vi.mock("../context/DialogContext", () => ({
|
||||
useDialogActions: () => ({ openNewAgent: mockOpenNewAgent }),
|
||||
}));
|
||||
|
||||
vi.mock("../context/BreadcrumbContext", () => ({
|
||||
useBreadcrumbs: () => ({ setBreadcrumbs: mockSetBreadcrumbs }),
|
||||
}));
|
||||
|
||||
vi.mock("../context/SidebarContext", () => ({
|
||||
useSidebar: () => ({ isMobile: false }),
|
||||
}));
|
||||
|
||||
vi.mock("../api/agents", () => ({
|
||||
agentsApi: mockAgentsApi,
|
||||
}));
|
||||
|
||||
vi.mock("../api/heartbeats", () => ({
|
||||
heartbeatsApi: mockHeartbeatsApi,
|
||||
}));
|
||||
|
||||
vi.mock("../adapters/adapter-display-registry", () => ({
|
||||
getAdapterLabel: (type: string) => type,
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
function makeAgent(overrides: Partial<Agent>): Agent {
|
||||
return {
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
name: "Alpha",
|
||||
urlKey: "alpha",
|
||||
role: "engineer",
|
||||
title: null,
|
||||
icon: null,
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
capabilities: null,
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
budgetMonthlyCents: 0,
|
||||
spentMonthlyCents: 0,
|
||||
pauseReason: null,
|
||||
pausedAt: null,
|
||||
permissions: { canCreateAgents: false },
|
||||
lastHeartbeatAt: null,
|
||||
metadata: null,
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function flushReact() {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||
});
|
||||
}
|
||||
|
||||
describe("Agents", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: ReturnType<typeof createRoot> | null;
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = null;
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
mockAgentsApi.list.mockResolvedValue([
|
||||
makeAgent({ adapterConfig: { model: "gpt-5.4" } }),
|
||||
]);
|
||||
mockAgentsApi.org.mockResolvedValue([
|
||||
{
|
||||
id: "agent-1",
|
||||
name: "Alpha",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
reports: [],
|
||||
},
|
||||
]);
|
||||
mockHeartbeatsApi.liveRunsForCompany.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const currentRoot = root;
|
||||
if (currentRoot) {
|
||||
await act(async () => {
|
||||
currentRoot.unmount();
|
||||
});
|
||||
}
|
||||
queryClient.clear();
|
||||
container.remove();
|
||||
document.body.innerHTML = "";
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows the configured model beside the adapter on the all agents page", async () => {
|
||||
root = createRoot(container);
|
||||
await act(async () => {
|
||||
root!.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Agents />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
await flushReact();
|
||||
|
||||
expect(container.textContent).toContain("codex_local");
|
||||
expect(container.textContent).toContain("gpt-5.4");
|
||||
});
|
||||
});
|
||||
+21
-2
@@ -41,6 +41,13 @@ function filterAgents(agents: Agent[], tab: FilterTab, showTerminated: boolean):
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function getConfiguredModel(agent: Agent): string | null {
|
||||
const value = agent.adapterConfig?.model;
|
||||
if (typeof value !== "string") return null;
|
||||
const model = value.trim();
|
||||
return model.length > 0 ? model : null;
|
||||
}
|
||||
|
||||
function filterOrgTree(nodes: OrgNode[], tab: FilterTab, showTerminated: boolean): OrgNode[] {
|
||||
return nodes
|
||||
.reduce<OrgNode[]>((acc, node) => {
|
||||
@@ -253,9 +260,15 @@ export function Agents() {
|
||||
liveCount={liveRunByAgent.get(agent.id)!.liveCount}
|
||||
/>
|
||||
)}
|
||||
<span className="w-28 whitespace-nowrap text-right font-mono text-xs text-muted-foreground">
|
||||
<span className="w-28 whitespace-nowrap text-left font-mono text-xs text-muted-foreground">
|
||||
{getAdapterLabel(agent.adapterType)}
|
||||
</span>
|
||||
<span
|
||||
className="w-36 truncate text-left font-mono text-xs text-muted-foreground"
|
||||
title={getConfiguredModel(agent) ?? undefined}
|
||||
>
|
||||
{getConfiguredModel(agent) ?? "—"}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground w-16 text-right">
|
||||
{agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "—"}
|
||||
</span>
|
||||
@@ -356,9 +369,15 @@ function OrgTreeNode({
|
||||
)}
|
||||
{agent && (
|
||||
<>
|
||||
<span className="w-28 whitespace-nowrap text-right font-mono text-xs text-muted-foreground">
|
||||
<span className="w-28 whitespace-nowrap text-left font-mono text-xs text-muted-foreground">
|
||||
{getAdapterLabel(agent.adapterType)}
|
||||
</span>
|
||||
<span
|
||||
className="w-36 truncate text-left font-mono text-xs text-muted-foreground"
|
||||
title={getConfiguredModel(agent) ?? undefined}
|
||||
>
|
||||
{getConfiguredModel(agent) ?? "—"}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground w-16 text-right">
|
||||
{agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "—"}
|
||||
</span>
|
||||
|
||||
Reference in new issue
Block a user