feat(FN-1794): merge fusion/fn-1794
This commit is contained in:
@@ -26,6 +26,7 @@ vi.mock("../../api", () => ({
|
||||
resetAgentBudget: vi.fn(),
|
||||
fetchWorkspaceFileContent: vi.fn(),
|
||||
saveWorkspaceFileContent: vi.fn(),
|
||||
fetchDiscoveredSkills: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../AgentLogViewer", () => ({
|
||||
@@ -36,7 +37,18 @@ vi.mock("../AgentLogViewer", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
import { fetchAgent, updateAgent, updateAgentState, fetchAgentChildren, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, updateAgentInstructions, fetchWorkspaceFileContent, saveWorkspaceFileContent } from "../../api";
|
||||
// Mock SkillMultiselect
|
||||
vi.mock("../SkillMultiselect", () => ({
|
||||
SkillMultiselect: ({ value, onChange, id }: { value: string[]; onChange: (v: string[]) => void; id?: string }) => (
|
||||
<div data-testid="skill-multiselect">
|
||||
<span data-testid="skill-multiselect-value">{JSON.stringify(value)}</span>
|
||||
<button data-testid="add-skill-test" onClick={() => onChange([...value, "test-skill"])}>Add Test Skill</button>
|
||||
<button data-testid="remove-skill-test" onClick={() => onChange(value.filter(s => s !== "test-skill"))}>Remove Test Skill</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
import { fetchAgent, updateAgent, updateAgentState, fetchAgentChildren, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, updateAgentInstructions, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchDiscoveredSkills } from "../../api";
|
||||
|
||||
const mockFetchAgent = vi.mocked(fetchAgent);
|
||||
const mockUpdateAgent = vi.mocked(updateAgent);
|
||||
@@ -52,6 +64,12 @@ const mockResetAgentBudget = vi.mocked(resetAgentBudget);
|
||||
const mockUpdateAgentInstructions = vi.mocked(updateAgentInstructions);
|
||||
const mockFetchWorkspaceFileContent = vi.mocked(fetchWorkspaceFileContent);
|
||||
const mockSaveWorkspaceFileContent = vi.mocked(saveWorkspaceFileContent);
|
||||
const mockFetchDiscoveredSkills = vi.mocked(fetchDiscoveredSkills);
|
||||
|
||||
const MOCK_SKILLS = [
|
||||
{ id: "skill-1", name: "Skill One", path: "/path/skill-1", relativePath: "skills/skill-1", enabled: true, metadata: { source: "*", scope: "user" as const, origin: "top-level" as const } },
|
||||
{ id: "skill-2", name: "Skill Two", path: "/path/skill-2", relativePath: "skills/skill-2", enabled: true, metadata: { source: "*", scope: "user" as const, origin: "top-level" as const } },
|
||||
];
|
||||
|
||||
describe("AgentDetailView", () => {
|
||||
const createMockAgent = (overrides: Partial<{
|
||||
@@ -124,6 +142,8 @@ describe("AgentDetailView", () => {
|
||||
mockFetchWorkspaceFileContent.mockResolvedValue({ content: "", mtime: "2024-01-01T00:00:00.000Z", size: 0 });
|
||||
mockSaveWorkspaceFileContent.mockResolvedValue({ success: true, mtime: "2024-01-01T00:00:00.000Z", size: 0 });
|
||||
mockUpdateAgentInstructions.mockResolvedValue({} as any);
|
||||
// Default: return skills
|
||||
mockFetchDiscoveredSkills.mockResolvedValue(MOCK_SKILLS);
|
||||
});
|
||||
|
||||
it("shows loading state initially", () => {
|
||||
@@ -2418,4 +2438,189 @@ describe("AgentDetailView", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Skills ─────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("Skills", () => {
|
||||
it("renders skill badges in Dashboard tab when agent has skills", async () => {
|
||||
const agentWithSkills = createMockAgent({
|
||||
metadata: { skills: ["skill-1", "skill-2"] },
|
||||
});
|
||||
mockFetchAgent.mockResolvedValue(agentWithSkills);
|
||||
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("skill-1")).toBeTruthy();
|
||||
expect(screen.getAllByText("skill-2")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows dash when agent has no skills in Dashboard tab", async () => {
|
||||
const agentWithNoSkills = createMockAgent({
|
||||
metadata: {},
|
||||
});
|
||||
mockFetchAgent.mockResolvedValue(agentWithNoSkills);
|
||||
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Skills")).toBeInTheDocument();
|
||||
// Should show dash when no skills
|
||||
const skillsLabel = screen.getByText("Skills");
|
||||
const parent = skillsLabel.closest(".info-item");
|
||||
expect(parent?.textContent).toContain("—");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows SkillMultiselect in Config tab", async () => {
|
||||
const user = userEvent.setup();
|
||||
const agentWithSkills = createMockAgent({
|
||||
metadata: { skills: ["skill-1"] },
|
||||
});
|
||||
mockFetchAgent.mockResolvedValue(agentWithSkills);
|
||||
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Navigate to Settings tab
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Settings")).toBeInTheDocument();
|
||||
});
|
||||
await user.click(screen.getByText("Settings"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("skill-multiselect")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Should show pre-selected skill
|
||||
expect(screen.getByTestId("skill-multiselect-value").textContent).toContain("skill-1");
|
||||
});
|
||||
|
||||
it("pre-fills skills from agent metadata in Config tab", async () => {
|
||||
const agentWithSkills = createMockAgent({
|
||||
metadata: { skills: ["skill-1", "skill-2"] },
|
||||
});
|
||||
mockFetchAgent.mockResolvedValue(agentWithSkills);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Settings")).toBeInTheDocument();
|
||||
});
|
||||
await user.click(screen.getByText("Settings"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("skill-multiselect")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Should have both skills pre-selected
|
||||
expect(screen.getByTestId("skill-multiselect-value").textContent).toContain("skill-1");
|
||||
expect(screen.getByTestId("skill-multiselect-value").textContent).toContain("skill-2");
|
||||
});
|
||||
|
||||
it("includes skills in metadata when saving Config tab", async () => {
|
||||
const agentWithSkills = createMockAgent({
|
||||
metadata: { skills: ["skill-1"] },
|
||||
});
|
||||
mockFetchAgent.mockResolvedValue(agentWithSkills);
|
||||
mockUpdateAgent.mockResolvedValue(createMockAgent({ metadata: { skills: ["skill-1", "new-skill"] } }) as any);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Settings")).toBeInTheDocument();
|
||||
});
|
||||
await user.click(screen.getByText("Settings"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("skill-multiselect")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Add a skill
|
||||
await user.click(screen.getByTestId("add-skill-test"));
|
||||
|
||||
// Save settings
|
||||
await user.click(screen.getByText("Save Settings"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAgent).toHaveBeenCalledWith(
|
||||
"agent-001",
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
skills: ["skill-1", "test-skill"],
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("enables Save Settings when skills change", async () => {
|
||||
const agentWithSkills = createMockAgent({
|
||||
metadata: { skills: [] },
|
||||
});
|
||||
mockFetchAgent.mockResolvedValue(agentWithSkills);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Settings")).toBeInTheDocument();
|
||||
});
|
||||
await user.click(screen.getByText("Settings"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("skill-multiselect")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Initially no changes
|
||||
expect(screen.getByText("Save Settings")).toBeDisabled();
|
||||
|
||||
// Add a skill
|
||||
await user.click(screen.getByTestId("add-skill-test"));
|
||||
|
||||
// Save button should now be enabled
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Save Settings")).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ vi.mock("../../api", () => ({
|
||||
startAgentRun: vi.fn(),
|
||||
fetchOrgTree: vi.fn(),
|
||||
fetchModels: vi.fn().mockResolvedValue({ models: [] }),
|
||||
fetchDiscoveredSkills: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.mock("../AgentDetailView", () => ({
|
||||
|
||||
@@ -9,6 +9,19 @@ vi.mock("../../api", () => ({
|
||||
createAgent: vi.fn(),
|
||||
fetchModels: vi.fn(),
|
||||
updateGlobalSettings: vi.fn(),
|
||||
fetchDiscoveredSkills: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock SkillMultiselect
|
||||
vi.mock("../SkillMultiselect", () => ({
|
||||
SkillMultiselect: ({ value, onChange, id }: { value: string[]; onChange: (v: string[]) => void; id?: string }) => (
|
||||
<div data-testid="skill-multiselect">
|
||||
<span data-testid="skill-multiselect-value">{JSON.stringify(value)}</span>
|
||||
<button data-testid="add-skill-1" onClick={() => onChange([...value, "skill-1"])}>Add Skill 1</button>
|
||||
<button data-testid="add-skill-2" onClick={() => onChange([...value, "skill-2"])}>Add Skill 2</button>
|
||||
<button data-testid="remove-skill-1" onClick={() => onChange(value.filter(s => s !== "skill-1"))}>Remove Skill 1</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock CustomModelDropdown to simplify interaction testing
|
||||
@@ -93,6 +106,7 @@ vi.mock("../AgentGenerationModal", () => ({
|
||||
const mockCreateAgent = vi.mocked(apiModule.createAgent);
|
||||
const mockFetchModels = vi.mocked(apiModule.fetchModels);
|
||||
const mockUpdateGlobalSettings = vi.mocked(apiModule.updateGlobalSettings);
|
||||
const mockFetchDiscoveredSkills = vi.mocked(apiModule.fetchDiscoveredSkills);
|
||||
|
||||
const MOCK_MODELS_RESPONSE = {
|
||||
models: [
|
||||
@@ -103,6 +117,11 @@ const MOCK_MODELS_RESPONSE = {
|
||||
favoriteModels: ["anthropic/claude-sonnet-4-5"],
|
||||
};
|
||||
|
||||
const MOCK_SKILLS_RESPONSE = [
|
||||
{ id: "skill-1", name: "Skill One", path: "/path/skill-1", relativePath: "skills/skill-1", enabled: true, metadata: { source: "*", scope: "user" as const, origin: "top-level" as const } },
|
||||
{ id: "skill-2", name: "Skill Two", path: "/path/skill-2", relativePath: "skills/skill-2", enabled: true, metadata: { source: "*", scope: "user" as const, origin: "top-level" as const } },
|
||||
];
|
||||
|
||||
describe("NewAgentDialog", () => {
|
||||
const mockOnClose = vi.fn();
|
||||
const mockOnCreated = vi.fn();
|
||||
@@ -112,6 +131,7 @@ describe("NewAgentDialog", () => {
|
||||
mockFetchModels.mockResolvedValue(MOCK_MODELS_RESPONSE);
|
||||
mockCreateAgent.mockResolvedValue({} as any);
|
||||
mockUpdateGlobalSettings.mockResolvedValue({});
|
||||
mockFetchDiscoveredSkills.mockResolvedValue(MOCK_SKILLS_RESPONSE);
|
||||
});
|
||||
|
||||
describe("modal visibility", () => {
|
||||
@@ -1134,4 +1154,98 @@ describe("NewAgentDialog", () => {
|
||||
expect(mockUpdateGlobalSettings).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("skill selection", () => {
|
||||
it("renders SkillMultiselect in Step 1", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
|
||||
|
||||
// Navigate to step 1
|
||||
const nameInput = screen.getByLabelText(/Name/);
|
||||
await user.type(nameInput, "Test Agent");
|
||||
await user.click(screen.getByText("Next"));
|
||||
|
||||
// SkillMultiselect should be visible in step 1
|
||||
expect(screen.getByTestId("skill-multiselect")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows selected skills in summary on step 2", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
|
||||
|
||||
// Navigate to step 1 and add a skill
|
||||
const nameInput = screen.getByLabelText(/Name/);
|
||||
await user.type(nameInput, "Test Agent");
|
||||
await user.click(screen.getByText("Next"));
|
||||
|
||||
// Add a skill using the mocked button
|
||||
await user.click(screen.getByTestId("add-skill-1"));
|
||||
|
||||
// Navigate to step 2
|
||||
await user.click(screen.getByText("Next"));
|
||||
|
||||
// Summary should show skill count
|
||||
expect(screen.getByText(/1 skill/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("includes metadata.skills in createAgent call when skills are selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
|
||||
|
||||
// Navigate to step 1
|
||||
const nameInput = screen.getByLabelText(/Name/);
|
||||
await user.type(nameInput, "Test Agent");
|
||||
await user.click(screen.getByText("Next"));
|
||||
|
||||
// Add skills
|
||||
await user.click(screen.getByTestId("add-skill-1"));
|
||||
await user.click(screen.getByTestId("add-skill-2"));
|
||||
|
||||
// Navigate to step 2 and create
|
||||
await user.click(screen.getByText("Next"));
|
||||
await user.click(screen.getByText("Create"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
const createCall = mockCreateAgent.mock.calls[0][0];
|
||||
expect(createCall.metadata).toEqual({ skills: ["skill-1", "skill-2"] });
|
||||
});
|
||||
|
||||
it("does not include metadata when no skills are selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
|
||||
|
||||
// Navigate through steps without adding skills
|
||||
const nameInput = screen.getByLabelText(/Name/);
|
||||
await user.type(nameInput, "Test Agent");
|
||||
await user.click(screen.getByText("Next"));
|
||||
await user.click(screen.getByText("Next"));
|
||||
await user.click(screen.getByText("Create"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
const createCall = mockCreateAgent.mock.calls[0][0];
|
||||
expect(createCall.metadata).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user