feat(FN-4620): complete Step 1 — add shared planning board tools
Fusion-Task-Id: FN-4620 Fusion-Task-Lineage: d3d05e54-e137-46ee-bb5d-d71aa03e5a48
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { createPlanningBoardTools } from "../planning-board-tools.js";
|
||||
|
||||
function createStoreMock(overrides?: {
|
||||
listTasks?: TaskStore["listTasks"];
|
||||
getTask?: TaskStore["getTask"];
|
||||
}): TaskStore {
|
||||
return {
|
||||
listTasks: overrides?.listTasks ?? vi.fn(async () => []),
|
||||
getTask: overrides?.getTask ?? vi.fn(async () => {
|
||||
throw new Error("not found");
|
||||
}),
|
||||
} as unknown as TaskStore;
|
||||
}
|
||||
|
||||
describe("createPlanningBoardTools", () => {
|
||||
it("fn_task_list excludes done tasks, includes deps, and handles empty list", async () => {
|
||||
const store = createStoreMock({
|
||||
listTasks: vi.fn(async () => [
|
||||
{
|
||||
id: "FN-1",
|
||||
column: "todo",
|
||||
title: "Task one",
|
||||
description: "Task one description",
|
||||
dependencies: ["FN-0"],
|
||||
},
|
||||
{
|
||||
id: "FN-2",
|
||||
column: "done",
|
||||
title: "Done",
|
||||
description: "Done description",
|
||||
dependencies: [],
|
||||
},
|
||||
]) as TaskStore["listTasks"],
|
||||
});
|
||||
|
||||
const taskList = createPlanningBoardTools(store).find((tool) => tool.name === "fn_task_list");
|
||||
expect(taskList).toBeDefined();
|
||||
const result = await taskList!.execute("c1", {});
|
||||
expect(result.content[0]?.text).toBe("FN-1 (todo): Task one [deps: FN-0]");
|
||||
|
||||
const emptyStore = createStoreMock({ listTasks: vi.fn(async () => []) as TaskStore["listTasks"] });
|
||||
const emptyResult = await createPlanningBoardTools(emptyStore)
|
||||
.find((tool) => tool.name === "fn_task_list")!
|
||||
.execute("c2", {});
|
||||
expect(emptyResult.content[0]?.text).toBe("No active tasks.");
|
||||
});
|
||||
|
||||
it("fn_task_get returns full details and not-found fallback", async () => {
|
||||
const store = createStoreMock({
|
||||
getTask: vi.fn(async (id: string) => ({
|
||||
id,
|
||||
column: "in-progress",
|
||||
description: "Detailed task",
|
||||
dependencies: ["FN-5", "FN-6"],
|
||||
prompt: "# Prompt body",
|
||||
})) as TaskStore["getTask"],
|
||||
});
|
||||
|
||||
const taskGet = createPlanningBoardTools(store).find((tool) => tool.name === "fn_task_get");
|
||||
expect(taskGet).toBeDefined();
|
||||
const result = await taskGet!.execute("c3", { id: "FN-10" });
|
||||
expect(result.content[0]?.text).toContain("ID: FN-10");
|
||||
expect(result.content[0]?.text).toContain("Column: in-progress");
|
||||
expect(result.content[0]?.text).toContain("Description: Detailed task");
|
||||
expect(result.content[0]?.text).toContain("Dependencies: FN-5, FN-6");
|
||||
expect(result.content[0]?.text).toContain("PROMPT.md:");
|
||||
expect(result.content[0]?.text).toContain("# Prompt body");
|
||||
|
||||
const notFoundStore = createStoreMock();
|
||||
const missingResult = await createPlanningBoardTools(notFoundStore)
|
||||
.find((tool) => tool.name === "fn_task_get")!
|
||||
.execute("c4", { id: "FN-404" });
|
||||
expect(missingResult.content[0]?.text).toBe("Task FN-404 not found.");
|
||||
});
|
||||
});
|
||||
75
packages/dashboard/src/planning-board-tools.ts
Normal file
75
packages/dashboard/src/planning-board-tools.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
export function createPlanningBoardTools(store: TaskStore): ToolDefinition[] {
|
||||
const taskGetParams = {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Task ID (e.g. KB-001)" },
|
||||
},
|
||||
required: ["id"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const taskList: ToolDefinition = {
|
||||
name: "fn_task_list",
|
||||
label: "List Tasks",
|
||||
description:
|
||||
"List all tasks that aren't done. Returns ID, description, column, " +
|
||||
"and dependencies for each. Use to check for duplicates before planning.",
|
||||
parameters: { type: "object", properties: {}, additionalProperties: false },
|
||||
execute: async () => {
|
||||
const tasks = await store.listTasks({ slim: true, includeArchived: false });
|
||||
const active = tasks.filter((t) => t.column !== "done");
|
||||
if (active.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "No active tasks." }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
const lines = active.map((t) => {
|
||||
const desc = t.title || t.description.slice(0, 80);
|
||||
const deps = t.dependencies.length ? ` [deps: ${t.dependencies.join(", ")}]` : "";
|
||||
return `${t.id} (${t.column}): ${desc}${deps}`;
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text" as const, text: lines.join("\n") }],
|
||||
details: {},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const taskGet: ToolDefinition = {
|
||||
name: "fn_task_get",
|
||||
label: "Get Task",
|
||||
description:
|
||||
"Get full details of a specific task including its PROMPT.md content. " +
|
||||
"Use to verify duplicates and to read dependency task specs before writing a new PROMPT.md.",
|
||||
parameters: taskGetParams,
|
||||
execute: async (_callId: string, params: { id: string }) => {
|
||||
try {
|
||||
const task = await store.getTask(params.id);
|
||||
const parts = [
|
||||
`ID: ${task.id}`,
|
||||
`Column: ${task.column}`,
|
||||
`Description: ${task.description}`,
|
||||
task.dependencies.length ? `Dependencies: ${task.dependencies.join(", ")}` : null,
|
||||
"",
|
||||
"PROMPT.md:",
|
||||
task.prompt || "(not yet specified)",
|
||||
].filter(Boolean);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: parts.join("\n") }],
|
||||
details: {},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Task ${params.id} not found.` }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return [taskList, taskGet];
|
||||
}
|
||||
Reference in New Issue
Block a user