feat(FN-4578): complete Step 5 — add fn_milestone_update tool
Fusion-Task-Id: FN-4578 Fusion-Task-Lineage: 1be0fa08-4d83-4988-a218-326225dfe4b5
This commit is contained in:
@@ -203,6 +203,7 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
|
||||
"fn_slice_activate",
|
||||
"fn_feature_link_task",
|
||||
"fn_feature_update",
|
||||
"fn_milestone_update",
|
||||
"fn_agent_stop",
|
||||
"fn_agent_start",
|
||||
"fn_agent_create",
|
||||
@@ -1660,6 +1661,138 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
|
||||
});
|
||||
});
|
||||
|
||||
describe("fn_milestone_update", () => {
|
||||
it("patches title, description, and acceptanceCriteria", async () => {
|
||||
const missionTool = api.tools.get("fn_mission_create")!;
|
||||
const milestoneTool = api.tools.get("fn_milestone_add")!;
|
||||
const updateTool = api.tools.get("fn_milestone_update")!;
|
||||
|
||||
const mission = await missionTool.execute("m1", { title: "Mission" }, undefined, undefined, makeCtx(tmpDir));
|
||||
const milestone = await milestoneTool.execute(
|
||||
"ms1",
|
||||
{ missionId: mission.details.missionId, title: "Milestone", description: "Original" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
const result = await updateTool.execute(
|
||||
"mu1",
|
||||
{ id: milestone.details.milestoneId, title: "Updated Milestone", description: "Updated description", acceptanceCriteria: "AC new" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
expect(result.content[0].text).toContain("Updated");
|
||||
expect(result.details.title).toBe("Updated Milestone");
|
||||
expect(result.details.description).toBe("Updated description");
|
||||
expect(result.details.acceptanceCriteria).toBe("AC new");
|
||||
});
|
||||
|
||||
it("partial patch updates only acceptanceCriteria", async () => {
|
||||
const missionTool = api.tools.get("fn_mission_create")!;
|
||||
const milestoneTool = api.tools.get("fn_milestone_add")!;
|
||||
const updateTool = api.tools.get("fn_milestone_update")!;
|
||||
|
||||
const mission = await missionTool.execute("m1", { title: "Mission" }, undefined, undefined, makeCtx(tmpDir));
|
||||
const milestone = await milestoneTool.execute(
|
||||
"ms1",
|
||||
{ missionId: mission.details.missionId, title: "Milestone", description: "Original" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
await updateTool.execute(
|
||||
"mu2",
|
||||
{ id: milestone.details.milestoneId, acceptanceCriteria: "Only AC" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
const persisted = store.getMissionStore().getMilestone(milestone.details.milestoneId);
|
||||
|
||||
expect(persisted?.title).toBe("Milestone");
|
||||
expect(persisted?.description).toBe("Original");
|
||||
expect(persisted?.acceptanceCriteria).toBe("Only AC");
|
||||
});
|
||||
|
||||
it("returns error when milestone not found", async () => {
|
||||
const updateTool = api.tools.get("fn_milestone_update")!;
|
||||
const result = await updateTool.execute(
|
||||
"mu3",
|
||||
{ id: "MS-999", title: "Updated" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain("Milestone MS-999 not found");
|
||||
});
|
||||
|
||||
it("returns error when no fields supplied", async () => {
|
||||
const missionTool = api.tools.get("fn_mission_create")!;
|
||||
const milestoneTool = api.tools.get("fn_milestone_add")!;
|
||||
const updateTool = api.tools.get("fn_milestone_update")!;
|
||||
|
||||
const mission = await missionTool.execute("m1", { title: "Mission" }, undefined, undefined, makeCtx(tmpDir));
|
||||
const milestone = await milestoneTool.execute(
|
||||
"ms1",
|
||||
{ missionId: mission.details.missionId, title: "Milestone" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
const result = await updateTool.execute(
|
||||
"mu4",
|
||||
{ id: milestone.details.milestoneId },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain("No fields to update");
|
||||
});
|
||||
|
||||
it("trims incoming field values", async () => {
|
||||
const missionTool = api.tools.get("fn_mission_create")!;
|
||||
const milestoneTool = api.tools.get("fn_milestone_add")!;
|
||||
const updateTool = api.tools.get("fn_milestone_update")!;
|
||||
|
||||
const mission = await missionTool.execute("m1", { title: "Mission" }, undefined, undefined, makeCtx(tmpDir));
|
||||
const milestone = await milestoneTool.execute(
|
||||
"ms1",
|
||||
{ missionId: mission.details.missionId, title: "Milestone" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
await updateTool.execute(
|
||||
"mu5",
|
||||
{ id: milestone.details.milestoneId, title: " Trimmed ", description: " Desc ", acceptanceCriteria: " AC " },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
const persisted = store.getMissionStore().getMilestone(milestone.details.milestoneId);
|
||||
|
||||
expect(persisted?.title).toBe("Trimmed");
|
||||
expect(persisted?.description).toBe("Desc");
|
||||
expect(persisted?.acceptanceCriteria).toBe("AC");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHub import tools", () => {
|
||||
it("fn_task_import_github requires gh auth", async () => {
|
||||
const tool = api.tools.get("fn_task_import_github")!;
|
||||
|
||||
@@ -2604,6 +2604,83 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
});
|
||||
|
||||
// ── fn_milestone_update ───────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "fn_milestone_update",
|
||||
label: "fn: Update Milestone",
|
||||
description:
|
||||
"Update an existing milestone's title, description, or acceptance criteria. " +
|
||||
"Partial patches leave untouched fields intact.",
|
||||
promptSnippet: "Update an existing mission milestone",
|
||||
promptGuidelines: [
|
||||
"Use to revise milestone details without re-creating it",
|
||||
"Mission linkage and ordering are preserved",
|
||||
"Provide only the fields you want to change",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
id: Type.String({ description: "Milestone ID to update (e.g., MS-001)" }),
|
||||
title: Type.Optional(Type.String({ description: "Updated milestone title" })),
|
||||
description: Type.Optional(Type.String({ description: "Updated milestone description" })),
|
||||
acceptanceCriteria: Type.Optional(
|
||||
Type.String({ description: "Updated acceptance criteria for completing the milestone" })
|
||||
),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const existingMilestone = missionStore.getMilestone(params.id);
|
||||
if (!existingMilestone) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Milestone ${params.id} not found` }],
|
||||
isError: true,
|
||||
details: { error: "Milestone not found" },
|
||||
};
|
||||
}
|
||||
|
||||
const updates: { title?: string; description?: string; acceptanceCriteria?: string } = {};
|
||||
|
||||
if ("title" in params) {
|
||||
updates.title = params.title?.trim();
|
||||
}
|
||||
if ("description" in params) {
|
||||
updates.description = params.description?.trim();
|
||||
}
|
||||
if ("acceptanceCriteria" in params) {
|
||||
updates.acceptanceCriteria = params.acceptanceCriteria?.trim();
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No fields to update (provide at least one of: title, description, acceptanceCriteria)",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
details: { error: "No fields to update" },
|
||||
};
|
||||
}
|
||||
|
||||
const milestone = missionStore.updateMilestone(params.id, updates);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `Updated ${milestone.id}: "${milestone.title}"` }],
|
||||
details: {
|
||||
milestoneId: milestone.id,
|
||||
missionId: milestone.missionId,
|
||||
title: milestone.title,
|
||||
description: milestone.description,
|
||||
acceptanceCriteria: milestone.acceptanceCriteria,
|
||||
status: milestone.status,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── fn_agent_stop ─────────────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
|
||||
Reference in New Issue
Block a user