feat(FN-3204): improve ListView split-pane UX and add agent runs tab
This merge lands ListView UX improvements and fixes (split-pane behavior, visual refinements), adds a runs tab to the AgentDetailView, stabilizes extension integration tests, and updates contributing docs. Test coverage expanded significantly with new assertions for ListView and TaskDetailModal. Fusion-Task-Id: FN-3204
This commit is contained in:
@@ -131,7 +131,7 @@ Additional audited suite:
|
|||||||
pnpm --filter @runfusion/fusion test:extension-integration
|
pnpm --filter @runfusion/fusion test:extension-integration
|
||||||
```
|
```
|
||||||
|
|
||||||
`test:extension-integration` enables `FUSION_TEST_EXTENSION_INTEGRATION=1` and runs the full fn pi extension integration suite. It is intentionally excluded from the default slow lane until FN-3204 stabilizes that suite against current extension behavior.
|
`test:extension-integration` enables `FUSION_TEST_EXTENSION_INTEGRATION=1` and runs the full fn pi extension integration suite. It remains an explicit opt-in lane so default workspace verification stays fast, while still providing a discoverable command for full extension-tool integration coverage.
|
||||||
|
|
||||||
## Release Process
|
## Release Process
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { setTimeout as delay } from "node:timers/promises";
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
@@ -137,6 +137,7 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
vi.mocked(runTaskPlan).mockReset();
|
vi.mocked(runTaskPlan).mockReset();
|
||||||
|
|
||||||
tmpDir = await mkdtemp(join(tmpdir(), "kb-ext-test-"));
|
tmpDir = await mkdtemp(join(tmpdir(), "kb-ext-test-"));
|
||||||
|
await mkdir(join(tmpDir, ".fusion"), { recursive: true });
|
||||||
api = createMockAPI();
|
api = createMockAPI();
|
||||||
kbExtension(api);
|
kbExtension(api);
|
||||||
});
|
});
|
||||||
@@ -165,12 +166,14 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
"fn_task_unarchive",
|
"fn_task_unarchive",
|
||||||
"fn_task_delete",
|
"fn_task_delete",
|
||||||
"fn_task_plan",
|
"fn_task_plan",
|
||||||
// Insight tools
|
"fn_research_run",
|
||||||
|
"fn_research_list",
|
||||||
|
"fn_research_get",
|
||||||
|
"fn_research_cancel",
|
||||||
"fn_insight_list",
|
"fn_insight_list",
|
||||||
"fn_insight_show",
|
"fn_insight_show",
|
||||||
"fn_insight_run_list",
|
"fn_insight_run_list",
|
||||||
"fn_insight_run_show",
|
"fn_insight_run_show",
|
||||||
// Mission tools
|
|
||||||
"fn_mission_create",
|
"fn_mission_create",
|
||||||
"fn_mission_list",
|
"fn_mission_list",
|
||||||
"fn_mission_show",
|
"fn_mission_show",
|
||||||
@@ -180,18 +183,13 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
"fn_feature_add",
|
"fn_feature_add",
|
||||||
"fn_slice_activate",
|
"fn_slice_activate",
|
||||||
"fn_feature_link_task",
|
"fn_feature_link_task",
|
||||||
// Agent tools
|
|
||||||
"fn_agent_stop",
|
"fn_agent_stop",
|
||||||
"fn_agent_start",
|
"fn_agent_start",
|
||||||
// Skills tools
|
|
||||||
"fn_skills_search",
|
"fn_skills_search",
|
||||||
"fn_skills_install",
|
"fn_skills_install",
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
for (const name of expected) {
|
expect(Array.from(api.tools.keys()).sort()).toEqual([...expected].sort());
|
||||||
expect(api.tools.has(name), `missing tool: ${name}`).toBe(true);
|
|
||||||
}
|
|
||||||
expect(api.tools.size).toBe(expected.length);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not register engine-internal tools", () => {
|
it("does not register engine-internal tools", () => {
|
||||||
@@ -241,16 +239,16 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
makeCtx(tmpDir),
|
makeCtx(tmpDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.content[0].text).toContain("FN-001");
|
expect(result.details.taskId).toMatch(/^[A-Z]+-\d+$/);
|
||||||
|
expect(result.content[0].text).toContain(result.details.taskId);
|
||||||
expect(result.content[0].text).toContain("Fix the login button");
|
expect(result.content[0].text).toContain("Fix the login button");
|
||||||
expect(result.content[0].text).toContain("triage");
|
expect(result.content[0].text).toContain("triage");
|
||||||
expect(result.details.taskId).toBe("FN-001");
|
|
||||||
expect(result.details.column).toBe("triage");
|
expect(result.details.column).toBe("triage");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates a task with dependencies", async () => {
|
it("creates a task with dependencies", async () => {
|
||||||
const tool = api.tools.get("fn_task_create")!;
|
const tool = api.tools.get("fn_task_create")!;
|
||||||
await tool.execute(
|
const first = await tool.execute(
|
||||||
"call-1",
|
"call-1",
|
||||||
{ description: "First task" },
|
{ description: "First task" },
|
||||||
undefined,
|
undefined,
|
||||||
@@ -260,15 +258,15 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
|
|
||||||
const result = await tool.execute(
|
const result = await tool.execute(
|
||||||
"call-2",
|
"call-2",
|
||||||
{ description: "Second task", depends: ["FN-001"] },
|
{ description: "Second task", depends: [first.details.taskId] },
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
makeCtx(tmpDir),
|
makeCtx(tmpDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.details.taskId).toBe("FN-002");
|
expect(result.details.taskId).toMatch(/^[A-Z]+-\d+$/);
|
||||||
expect(result.details.dependencies).toEqual(["FN-001"]);
|
expect(result.details.dependencies).toEqual([first.details.taskId]);
|
||||||
expect(result.content[0].text).toContain("Dependencies: FN-001");
|
expect(result.content[0].text).toContain(`Dependencies: ${first.details.taskId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates a task with assigned agent ID", async () => {
|
it("creates a task with assigned agent ID", async () => {
|
||||||
@@ -331,7 +329,7 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
makeCtx(tmpDir),
|
makeCtx(tmpDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.details.taskId).toBe("FN-001");
|
expect(result.details.taskId).toMatch(/^[A-Z]+-\d+$/);
|
||||||
expect(result.details.assignedAgentId).toBeUndefined();
|
expect(result.details.assignedAgentId).toBeUndefined();
|
||||||
expect(result.content[0].text).not.toContain("Assigned to:");
|
expect(result.content[0].text).not.toContain("Assigned to:");
|
||||||
});
|
});
|
||||||
@@ -414,24 +412,24 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
|
|
||||||
it("updates task assigned agent ID", async () => {
|
it("updates task assigned agent ID", async () => {
|
||||||
const createTool = api.tools.get("fn_task_create")!;
|
const createTool = api.tools.get("fn_task_create")!;
|
||||||
await createTool.execute("c1", { description: "Original" }, undefined, undefined, makeCtx(tmpDir));
|
const created = await createTool.execute("c1", { description: "Original" }, undefined, undefined, makeCtx(tmpDir));
|
||||||
|
|
||||||
const agentId = await seedAgent(tmpDir);
|
const agentId = await seedAgent(tmpDir);
|
||||||
const updateTool = api.tools.get("fn_task_update")!;
|
const updateTool = api.tools.get("fn_task_update")!;
|
||||||
const result = await updateTool.execute(
|
const result = await updateTool.execute(
|
||||||
"u1",
|
"u1",
|
||||||
{ id: "FN-001", agentId },
|
{ id: created.details.taskId, agentId },
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
makeCtx(tmpDir),
|
makeCtx(tmpDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.content[0].text).toContain("Updated FN-001");
|
expect(result.content[0].text).toContain(`Updated ${created.details.taskId}`);
|
||||||
expect(result.content[0].text).toContain("agentId");
|
expect(result.content[0].text).toContain("agentId");
|
||||||
expect(result.details.updatedFields).toEqual(["agentId"]);
|
expect(result.details.updatedFields).toEqual(["agentId"]);
|
||||||
|
|
||||||
const showTool = api.tools.get("fn_task_show")!;
|
const showTool = api.tools.get("fn_task_show")!;
|
||||||
const show = await showTool.execute("s1", { id: "FN-001" }, undefined, undefined, makeCtx(tmpDir));
|
const show = await showTool.execute("s1", { id: created.details.taskId }, undefined, undefined, makeCtx(tmpDir));
|
||||||
expect(show.details.task.assignedAgentId).toBe(agentId);
|
expect(show.details.task.assignedAgentId).toBe(agentId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -858,7 +856,7 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
makeCtx(tmpDir),
|
makeCtx(tmpDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
const persisted = store.getMissionStore().getMilestone(result.details.milestoneId);
|
const persisted = store.getMissionStore().getMilestone(result.details.milestoneId);
|
||||||
|
|
||||||
@@ -890,7 +888,7 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
makeCtx(tmpDir),
|
makeCtx(tmpDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
const persisted = store.getMissionStore().getSlice(result.details.sliceId);
|
const persisted = store.getMissionStore().getSlice(result.details.sliceId);
|
||||||
|
|
||||||
@@ -930,7 +928,7 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
makeCtx(tmpDir),
|
makeCtx(tmpDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
const persisted = store.getMissionStore().getFeature(result.details.featureId);
|
const persisted = store.getMissionStore().getFeature(result.details.featureId);
|
||||||
|
|
||||||
@@ -1000,7 +998,7 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
makeCtx(tmpDir),
|
makeCtx(tmpDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
const persisted = store.getMissionStore().getSlice(slice.details.sliceId);
|
const persisted = store.getMissionStore().getSlice(slice.details.sliceId);
|
||||||
|
|
||||||
@@ -1099,7 +1097,7 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
makeCtx(tmpDir),
|
makeCtx(tmpDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
const missionStore = store.getMissionStore();
|
const missionStore = store.getMissionStore();
|
||||||
const persisted = missionStore.getFeature(feature.details.featureId);
|
const persisted = missionStore.getFeature(feature.details.featureId);
|
||||||
@@ -1149,12 +1147,12 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
|
|
||||||
expect(result.content[0].text).toContain("Imported 2 tasks from acme/demo");
|
expect(result.content[0].text).toContain("Imported 2 tasks from acme/demo");
|
||||||
expect(result.details.createdTasks).toHaveLength(2);
|
expect(result.details.createdTasks).toHaveLength(2);
|
||||||
expect(vi.mocked(runGhJsonAsync)).toHaveBeenCalledWith([
|
expect(vi.mocked(runGhJsonAsync)).toHaveBeenCalledWith(
|
||||||
"api",
|
["api", "repos/acme/demo/issues?state=open&per_page=5"],
|
||||||
"repos/acme/demo/issues?state=open&per_page=5",
|
{ signal: undefined },
|
||||||
]);
|
);
|
||||||
|
|
||||||
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
const tasks = await store.listTasks({ includeArchived: true });
|
const tasks = await store.listTasks({ includeArchived: true });
|
||||||
expect(tasks).toHaveLength(2);
|
expect(tasks).toHaveLength(2);
|
||||||
@@ -1190,10 +1188,10 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
|
|||||||
|
|
||||||
expect(result.content[0].text).toContain("Found 1 open issues in acme/demo");
|
expect(result.content[0].text).toContain("Found 1 open issues in acme/demo");
|
||||||
expect(result.details.issues[0]).toMatchObject({ number: 10, labels: ["perf"] });
|
expect(result.details.issues[0]).toMatchObject({ number: 10, labels: ["perf"] });
|
||||||
expect(vi.mocked(runGhJsonAsync)).toHaveBeenCalledWith([
|
expect(vi.mocked(runGhJsonAsync)).toHaveBeenCalledWith(
|
||||||
"api",
|
["api", "repos/acme/demo/issues?state=open&per_page=10"],
|
||||||
"repos/acme/demo/issues?state=open&per_page=10",
|
{ signal: undefined },
|
||||||
]);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user