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:
Fusion
2026-05-03 05:12:50 -07:00
committed by gsxdsm
parent eafb078804
commit 4f06f38434
6 changed files with 192 additions and 4 deletions

View File

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

View File

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

View File

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

View File

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

View File

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