feat(KB-662): complete Step 2 — add visibility refresh tests for useProjects
This commit is contained in:
1287
packages/dashboard/app/components/AgentDetailView.tsx
Normal file
1287
packages/dashboard/app/components/AgentDetailView.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -117,7 +117,7 @@ describe("FileBrowserModal", () => {
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /kb/i }));
|
||||
await user.click(screen.getByRole("button", { name: /KB-002 Task Two/i }));
|
||||
await user.click(screen.getByRole("button", { name: /FN-002 Task Two/i }));
|
||||
|
||||
expect(mockOnWorkspaceChange).toHaveBeenCalledWith("FN-002");
|
||||
});
|
||||
|
||||
1305
packages/dashboard/app/components/MissionManager.tsx
Normal file
1305
packages/dashboard/app/components/MissionManager.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -37,8 +37,10 @@ export function QuickScriptsDropdown({
|
||||
return Object.entries(scripts).sort(([a], [b]) => a.localeCompare(b));
|
||||
}, [scripts]);
|
||||
|
||||
const showFooter = scriptEntries.length > 0;
|
||||
|
||||
// Total items for keyboard navigation (scripts + "Manage Scripts...")
|
||||
const totalItems = scriptEntries.length + 1;
|
||||
const totalItems = scriptEntries.length + (showFooter ? 1 : 0);
|
||||
|
||||
// Fetch scripts when dropdown opens
|
||||
useEffect(() => {
|
||||
@@ -135,7 +137,7 @@ export function QuickScriptsDropdown({
|
||||
// Run script
|
||||
const [name, command] = scriptEntries[highlightedIndex];
|
||||
handleRunScript(name, command);
|
||||
} else {
|
||||
} else if (showFooter && highlightedIndex === scriptEntries.length) {
|
||||
// Manage Scripts...
|
||||
handleManageScripts();
|
||||
}
|
||||
@@ -151,7 +153,7 @@ export function QuickScriptsDropdown({
|
||||
break;
|
||||
}
|
||||
},
|
||||
[highlightedIndex, totalItems, scriptEntries]
|
||||
[highlightedIndex, totalItems, scriptEntries, showFooter]
|
||||
);
|
||||
|
||||
// Toggle dropdown
|
||||
@@ -179,7 +181,7 @@ export function QuickScriptsDropdown({
|
||||
{/* Trigger button */}
|
||||
<button
|
||||
ref={triggerRef}
|
||||
className={`quick-scripts-dropdown__trigger ${isOpen ? "open" : ""}`}
|
||||
className={`btn-icon quick-scripts-dropdown__trigger ${isOpen ? "open btn-icon--active" : ""}`}
|
||||
onClick={toggleDropdown}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="listbox"
|
||||
@@ -212,9 +214,12 @@ export function QuickScriptsDropdown({
|
||||
</div>
|
||||
) : scriptEntries.length === 0 ? (
|
||||
<div className="quick-scripts-dropdown__empty" data-testid="quick-scripts-empty">
|
||||
<div className="quick-scripts-dropdown__empty-icon">
|
||||
<Terminal size={16} />
|
||||
</div>
|
||||
<p>No scripts configured</p>
|
||||
<button
|
||||
className="quick-scripts-dropdown__empty-action"
|
||||
className="quick-scripts-dropdown__empty-action btn"
|
||||
onClick={handleManageScripts}
|
||||
>
|
||||
Add your first script
|
||||
@@ -250,7 +255,7 @@ export function QuickScriptsDropdown({
|
||||
<div className="quick-scripts-dropdown__footer">
|
||||
<button
|
||||
className={`quick-scripts-dropdown__manage ${
|
||||
highlightedIndex === scriptEntries.length ? "highlighted" : ""
|
||||
showFooter && highlightedIndex === scriptEntries.length ? "highlighted" : ""
|
||||
}`}
|
||||
onClick={handleManageScripts}
|
||||
data-testid="quick-scripts-manage"
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "@testing-library/jest-dom";
|
||||
import { AgentDetailView } from "../AgentDetailView";
|
||||
import type { AgentCapability, AgentDetail } from "../../api";
|
||||
|
||||
// Mock the API functions
|
||||
vi.mock("../../api", () => ({
|
||||
fetchAgent: vi.fn(),
|
||||
updateAgentState: vi.fn(),
|
||||
deleteAgent: vi.fn(),
|
||||
fetchAgentLogs: vi.fn(),
|
||||
}));
|
||||
|
||||
import { fetchAgent, updateAgentState } from "../../api";
|
||||
|
||||
const mockFetchAgent = vi.mocked(fetchAgent);
|
||||
const mockUpdateAgentState = vi.mocked(updateAgentState);
|
||||
|
||||
describe("AgentDetailView", () => {
|
||||
const createMockAgent = (overrides: Partial<{
|
||||
id: string;
|
||||
name: string;
|
||||
role: AgentCapability;
|
||||
state: "idle" | "active" | "paused" | "terminated";
|
||||
taskId?: string;
|
||||
}> = {}): AgentDetail => ({
|
||||
id: "agent-001",
|
||||
name: "Test Agent",
|
||||
role: "executor" as AgentCapability,
|
||||
state: "active",
|
||||
taskId: "FN-001",
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
lastHeartbeatAt: "2024-01-01T00:05:00.000Z",
|
||||
metadata: {},
|
||||
heartbeatHistory: [],
|
||||
activeRun: {
|
||||
id: "run-001",
|
||||
agentId: "agent-001",
|
||||
startedAt: "2024-01-01T00:00:00.000Z",
|
||||
endedAt: null,
|
||||
status: "active",
|
||||
},
|
||||
completedRuns: [
|
||||
{
|
||||
id: "run-002",
|
||||
agentId: "agent-001",
|
||||
startedAt: "2023-12-31T00:00:00.000Z",
|
||||
endedAt: "2023-12-31T00:05:00.000Z",
|
||||
status: "completed",
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
} as AgentDetail);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockFetchAgent.mockResolvedValue(createMockAgent());
|
||||
mockUpdateAgentState.mockResolvedValue(createMockAgent({ state: "paused" }));
|
||||
});
|
||||
|
||||
it("shows loading state initially", () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Loading agent/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays agent name in header after loading", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// Wait for the h2 element specifically (the header title)
|
||||
await waitFor(() => {
|
||||
const headings = screen.getAllByRole("heading", { level: 2 });
|
||||
expect(headings.some(h => h.textContent === "Test Agent")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("displays role badge", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("executor")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays state badge", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// There should be at least one element with "active" (could be in badge or inline-badge)
|
||||
expect(screen.getAllByText("active").length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows all tabs", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Dashboard")).toBeInTheDocument();
|
||||
expect(screen.getByText("Logs")).toBeInTheDocument();
|
||||
expect(screen.getByText("Runs")).toBeInTheDocument();
|
||||
expect(screen.getByText("Settings")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows Pause button for active agent", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows Resume button for paused agent", async () => {
|
||||
mockFetchAgent.mockResolvedValue(createMockAgent({ state: "paused" }));
|
||||
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Resume")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows Delete button for terminated agent", async () => {
|
||||
mockFetchAgent.mockResolvedValue(createMockAgent({ state: "terminated" }));
|
||||
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Delete")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows statistics section on dashboard", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Total Runs")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays agent ID in footer", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("agent-001")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("calls API with correct agentId", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgent).toHaveBeenCalledWith("agent-001");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays health status indicator", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// Health status should be either Healthy, Unresponsive, or Idle
|
||||
const healthTexts = ["Healthy", "Unresponsive", "Idle"];
|
||||
const hasHealthStatus = healthTexts.some(text =>
|
||||
document.body.textContent?.includes(text)
|
||||
);
|
||||
expect(hasHealthStatus).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows Live Run on runs tab when agent has active run", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Dashboard")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText("Runs"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Live Run")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,13 +3,13 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { TaskComments } from "../TaskComments";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
addComment: vi.fn(),
|
||||
addSteeringComment: vi.fn(),
|
||||
addTaskComment: vi.fn(),
|
||||
updateTaskComment: vi.fn(),
|
||||
deleteTaskComment: vi.fn(),
|
||||
}));
|
||||
|
||||
import { addComment, addTaskComment, updateTaskComment, deleteTaskComment } from "../../api";
|
||||
import { addSteeringComment, addTaskComment, updateTaskComment, deleteTaskComment } from "../../api";
|
||||
|
||||
const makeTask = (overrides: any = {}) => ({
|
||||
id: "FN-001",
|
||||
@@ -155,8 +155,8 @@ describe("TaskComments", () => {
|
||||
expect(screen.getByText(/AI Guidance comments are injected into the task execution context/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses addComment API when AI Guidance type is selected", async () => {
|
||||
vi.mocked(addComment).mockResolvedValue(makeTask({
|
||||
it("uses addSteeringComment API when AI Guidance type is selected", async () => {
|
||||
vi.mocked(addSteeringComment).mockResolvedValue(makeTask({
|
||||
comments: [{ id: "c1", text: "Guidance text", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
|
||||
}));
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("TaskComments", () => {
|
||||
fireEvent.click(screen.getByText("Add Guidance"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addComment).toHaveBeenCalledWith("FN-001", "Guidance text");
|
||||
expect(addSteeringComment).toHaveBeenCalledWith("FN-001", "Guidance text");
|
||||
expect(addTaskComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -188,7 +188,7 @@ describe("TaskComments", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "User text", "user");
|
||||
expect(addComment).not.toHaveBeenCalled();
|
||||
expect(addSteeringComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user