feat(FN-3017): add insights coverage contracts, AgentsOverviewBar component
This merge introduces an `AgentsOverviewBar` component to the agents layout, adds styling enhancements to the planning mode modal and quick chat FAB, and fixes a `useEffect` dependency issue for the insights initial refresh. It also expands test coverage for the insights subsystem with new regressio Fusion-Task-Id: FN-3017
This commit is contained in:
@@ -1195,3 +1195,71 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fn pi extension (runnable structured-output regression slice)", () => {
|
||||
let tmpDir: string;
|
||||
let api: ReturnType<typeof createMockAPI>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.mocked(isGhAvailable).mockReturnValue(true);
|
||||
vi.mocked(isGhAuthenticated).mockReturnValue(true);
|
||||
vi.mocked(runGhJsonAsync).mockReset();
|
||||
vi.mocked(runTaskPlan).mockReset();
|
||||
|
||||
tmpDir = await mkdtemp(join(tmpdir(), "kb-ext-fast-"));
|
||||
await mkdir(join(tmpDir, ".fusion"), { recursive: true });
|
||||
api = createMockAPI();
|
||||
kbExtension(api);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await removeDirWithRetries(tmpDir);
|
||||
});
|
||||
|
||||
it("returns machine-consumable task metadata without assuming FN-* prefixes", async () => {
|
||||
const createTool = api.tools.get("fn_task_create")!;
|
||||
const parent = await createTool.execute("create-1", { description: "parent" }, undefined, undefined, makeCtx(tmpDir));
|
||||
|
||||
const result = await createTool.execute(
|
||||
"create-2",
|
||||
{ description: "child", depends: [parent.details.taskId] },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
expect(result.details.taskId).toMatch(/^[A-Z]+-\d+$/);
|
||||
expect(result.details.dependencies).toEqual([parent.details.taskId]);
|
||||
expect(result.content[0].text).toContain(result.details.taskId);
|
||||
});
|
||||
|
||||
it("returns structured details for invalid task assignment", async () => {
|
||||
const createTool = api.tools.get("fn_task_create")!;
|
||||
const result = await createTool.execute(
|
||||
"create-bad-agent",
|
||||
{ description: "bad assignment", agentId: "agent-does-not-exist" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
expect(result.details.error).toContain("not found");
|
||||
expect(result.content[0].text).toContain("not found");
|
||||
});
|
||||
|
||||
it("returns structured details when assignment targets ephemeral agents", async () => {
|
||||
const ephemeralId = await seedAgent(tmpDir, { ephemeral: true, name: "temp-worker" });
|
||||
const createTool = api.tools.get("fn_task_create")!;
|
||||
|
||||
const result = await createTool.execute(
|
||||
"create-ephemeral",
|
||||
{ description: "ephemeral assignment", agentId: ephemeralId },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
expect(result.details.error).toContain("ephemeral/runtime agent");
|
||||
expect(result.content[0].text).toContain(ephemeralId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,6 +294,44 @@ describe("InsightsView", () => {
|
||||
expect(screen.getByText("Failed to load insights")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render run-level error state from failed insight runs", () => {
|
||||
mockUseInsights.mockReturnValue({
|
||||
sections: mockSections,
|
||||
loading: false,
|
||||
error: null,
|
||||
latestRun: {
|
||||
id: "INSR-10",
|
||||
projectId: "test",
|
||||
trigger: "manual",
|
||||
status: "failed",
|
||||
summary: null,
|
||||
error: "No working memory to analyze",
|
||||
insightsCreated: 0,
|
||||
insightsUpdated: 0,
|
||||
inputMetadata: {},
|
||||
outputMetadata: {},
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
startedAt: "2024-01-01T00:00:01Z",
|
||||
completedAt: "2024-01-01T00:00:10Z",
|
||||
},
|
||||
isRunInFlight: false,
|
||||
runError: "No working memory to analyze",
|
||||
refresh: vi.fn(),
|
||||
runInsights: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
createTask: vi.fn(),
|
||||
dismissStates: new Map(),
|
||||
createTaskStates: new Map(),
|
||||
totalCount: 0,
|
||||
dismissedCount: 0,
|
||||
});
|
||||
|
||||
render(<InsightsView {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("run-error")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("No working memory to analyze").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should render global empty state when all sections are empty", () => {
|
||||
mockUseInsights.mockReturnValue({
|
||||
sections: mockSections,
|
||||
|
||||
@@ -196,6 +196,53 @@ describe("useInsights", () => {
|
||||
});
|
||||
|
||||
describe("runInsights", () => {
|
||||
it("should set latestRun from the newest run returned by fetchInsightRuns during refresh", async () => {
|
||||
mockFetchInsights.mockResolvedValue({ insights: [], count: 0 });
|
||||
mockFetchInsightRuns.mockResolvedValue({
|
||||
runs: [
|
||||
{
|
||||
id: "RUN-2",
|
||||
projectId: "project-1",
|
||||
trigger: "manual" as const,
|
||||
status: "running" as const,
|
||||
summary: null,
|
||||
error: null,
|
||||
insightsCreated: 0,
|
||||
insightsUpdated: 0,
|
||||
inputMetadata: {},
|
||||
outputMetadata: {},
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
startedAt: "2024-01-01T00:00:05Z",
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "RUN-1",
|
||||
projectId: "project-1",
|
||||
trigger: "manual" as const,
|
||||
status: "completed" as const,
|
||||
summary: "done",
|
||||
error: null,
|
||||
insightsCreated: 1,
|
||||
insightsUpdated: 0,
|
||||
inputMetadata: {},
|
||||
outputMetadata: {},
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
startedAt: "2024-01-01T00:00:01Z",
|
||||
completedAt: "2024-01-01T00:00:10Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useInsights("project-1"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.latestRun?.id).toBe("RUN-2");
|
||||
expect(result.current.latestRun?.status).toBe("running");
|
||||
});
|
||||
|
||||
it("should trigger manual insight run and refresh when run completes", async () => {
|
||||
const completedRun = {
|
||||
id: "RUN-1",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Converting insights to tasks
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||
import type { Insight, InsightCategory, InsightStatus, InsightRun } from "@fusion/core";
|
||||
import {
|
||||
fetchInsights,
|
||||
@@ -299,10 +299,9 @@ export function useInsights(projectId?: string): UseInsightsResult {
|
||||
}, [sections]);
|
||||
|
||||
// Initial load - intentionally runs once on mount
|
||||
|
||||
useMemo(() => {
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, []);
|
||||
}, [refresh]);
|
||||
|
||||
return {
|
||||
sections,
|
||||
|
||||
@@ -146,6 +146,15 @@ describe("Insights routes", () => {
|
||||
expect((getRes.body as { id: string }).id).toBe(run.id);
|
||||
});
|
||||
|
||||
it("GET /api/insights/runs/:id returns not-found JSON payload for unknown ids", async () => {
|
||||
const res = await request(app, "GET", "/api/insights/runs/INSR-missing");
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body).toMatchObject({
|
||||
error: expect.stringContaining("Run not found"),
|
||||
});
|
||||
});
|
||||
|
||||
it("GET /api/insights applies category/status/runId filters and pagination", async () => {
|
||||
const insightStore = storeA.getInsightStore();
|
||||
const runA = insightStore.createRun("", { trigger: "manual" });
|
||||
|
||||
Reference in New Issue
Block a user