feat(KB-662): complete Step 2 — add visibility refresh tests for useProjects

This commit is contained in:
gsxdsm
2026-04-01 21:36:22 -07:00
parent 79340711fa
commit a19912af9a
17 changed files with 4054 additions and 151 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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");
});

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});