FN-5851: fix goal tool store resolution
Resolve goal tool lookups to the canonical project store from Fusion worktree directories. - recognize both legacy .worktrees and .fusion/worktrees paths when resolving the project root for pi extensions - add CLI coverage proving fn_goal_list and fn_goal_show return dashboard-created goals from a Fusion worktree cwd - extend core worktree-resolution tests and keep the branch conflict recovery test worktree path isolated in tmpdir - add a patch changeset for the published CLI fix Files changed: .changeset/fn-5851-goal-store-resolution.md | 5 + packages/cli/src/__tests__/goal-store-resolution.test.ts | 104 +++++++++++++++++++++ packages/core/src/__tests__/pi-extensions.test.ts | 14 ++- packages/core/src/pi-extensions.ts | 12 ++- packages/engine/src/__tests__/branch-conflicts-recovery.test.ts | 2 +- 5 files changed, 130 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-5851 Fusion-Task-Lineage: ec4c8239-b953-4df7-9f72-02e17e4b0851
This commit is contained in:
5
.changeset/fn-5851-goal-store-resolution.md
Normal file
5
.changeset/fn-5851-goal-store-resolution.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix `fn_goal_list` and `fn_goal_show` so tool calls made from Fusion worktree directories resolve the canonical project database and return goals created through the dashboard UI.
|
||||||
104
packages/cli/src/__tests__/goal-store-resolution.test.ts
Normal file
104
packages/cli/src/__tests__/goal-store-resolution.test.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { TaskStore, getProjectRootFromWorktree } from "@fusion/core";
|
||||||
|
import kbExtension from "../extension.js";
|
||||||
|
|
||||||
|
interface RegisteredTool {
|
||||||
|
name: string;
|
||||||
|
execute: (
|
||||||
|
toolCallId: string,
|
||||||
|
params: any,
|
||||||
|
signal: AbortSignal | undefined,
|
||||||
|
onUpdate: ((update: any) => void) | undefined,
|
||||||
|
ctx: any,
|
||||||
|
) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockAPI() {
|
||||||
|
const tools = new Map<string, RegisteredTool>();
|
||||||
|
return {
|
||||||
|
registerTool(def: RegisteredTool) {
|
||||||
|
tools.set(def.name, def);
|
||||||
|
},
|
||||||
|
registerCommand() {},
|
||||||
|
registerShortcut() {},
|
||||||
|
registerFlag() {},
|
||||||
|
on() {},
|
||||||
|
tools,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("extension goal tools store resolution", () => {
|
||||||
|
let rootDir: string;
|
||||||
|
let worktreeCwd: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
rootDir = await mkdtemp(join(tmpdir(), "kb-goal-resolution-"));
|
||||||
|
await mkdir(join(rootDir, ".fusion"), { recursive: true });
|
||||||
|
|
||||||
|
const worktreeRoot = join(rootDir, ".fusion", "worktrees", "FN-5851");
|
||||||
|
await mkdir(join(worktreeRoot, ".fusion"), { recursive: true });
|
||||||
|
worktreeCwd = join(worktreeRoot, "packages", "cli");
|
||||||
|
await mkdir(worktreeCwd, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rm(rootDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns canonical project goals when invoked from a .fusion/worktrees cwd", async () => {
|
||||||
|
expect(getProjectRootFromWorktree(worktreeCwd)).toBe(rootDir);
|
||||||
|
|
||||||
|
const store = new TaskStore(rootDir);
|
||||||
|
await store.init();
|
||||||
|
const goal = store.getGoalStore().createGoal({
|
||||||
|
title: "Canonical goal",
|
||||||
|
description: "Created in the project root store",
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = createMockAPI();
|
||||||
|
kbExtension(api);
|
||||||
|
const listTool = api.tools.get("fn_goal_list");
|
||||||
|
const showTool = api.tools.get("fn_goal_show");
|
||||||
|
expect(listTool).toBeDefined();
|
||||||
|
expect(showTool).toBeDefined();
|
||||||
|
|
||||||
|
const listResult = await listTool!.execute(
|
||||||
|
"goal-list-worktree",
|
||||||
|
{ status: "active" },
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{ cwd: worktreeCwd },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(listResult.isError).toBeUndefined();
|
||||||
|
expect(listResult.details.goals).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: goal.id,
|
||||||
|
title: "Canonical goal",
|
||||||
|
description: "Created in the project root store",
|
||||||
|
status: "active",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const showResult = await showTool!.execute(
|
||||||
|
"goal-show-worktree",
|
||||||
|
{ id: goal.id },
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{ cwd: worktreeCwd },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(showResult.isError).toBeUndefined();
|
||||||
|
expect(showResult.details.goal).toMatchObject({
|
||||||
|
id: goal.id,
|
||||||
|
title: "Canonical goal",
|
||||||
|
description: "Created in the project root store",
|
||||||
|
status: "active",
|
||||||
|
});
|
||||||
|
|
||||||
|
store.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,11 +8,15 @@ describe("getProjectRootFromWorktree", () => {
|
|||||||
it("detects POSIX worktree paths", () => {
|
it("detects POSIX worktree paths", () => {
|
||||||
expect(getProjectRootFromWorktree("/repo/.worktrees/fn-001")).toBe("/repo");
|
expect(getProjectRootFromWorktree("/repo/.worktrees/fn-001")).toBe("/repo");
|
||||||
expect(getProjectRootFromWorktree("/repo/.worktrees/fn-001/src/file.ts")).toBe("/repo");
|
expect(getProjectRootFromWorktree("/repo/.worktrees/fn-001/src/file.ts")).toBe("/repo");
|
||||||
|
expect(getProjectRootFromWorktree("/repo/.fusion/worktrees/fn-001")).toBe("/repo");
|
||||||
|
expect(getProjectRootFromWorktree("/repo/.fusion/worktrees/fn-001/src/file.ts")).toBe("/repo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects Windows worktree paths", () => {
|
it("detects Windows worktree paths", () => {
|
||||||
expect(getProjectRootFromWorktree("C:\\repo\\.worktrees\\fn-001")).toBe("C:\\repo");
|
expect(getProjectRootFromWorktree("C:\\repo\\.worktrees\\fn-001")).toBe("C:\\repo");
|
||||||
expect(getProjectRootFromWorktree("C:\\repo\\.worktrees\\fn-001\\src\\file.ts")).toBe("C:\\repo");
|
expect(getProjectRootFromWorktree("C:\\repo\\.worktrees\\fn-001\\src\\file.ts")).toBe("C:\\repo");
|
||||||
|
expect(getProjectRootFromWorktree("C:\\repo\\.fusion\\worktrees\\fn-001")).toBe("C:\\repo");
|
||||||
|
expect(getProjectRootFromWorktree("C:\\repo\\.fusion\\worktrees\\fn-001\\src\\file.ts")).toBe("C:\\repo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("supports configured candidate worktrees dir paths", () => {
|
it("supports configured candidate worktrees dir paths", () => {
|
||||||
@@ -43,10 +47,14 @@ describe("resolvePiExtensionProjectRoot", () => {
|
|||||||
try {
|
try {
|
||||||
mkdirSync(join(root, ".fusion"), { recursive: true });
|
mkdirSync(join(root, ".fusion"), { recursive: true });
|
||||||
mkdirSync(join(root, ".worktrees", "feature", ".fusion"), { recursive: true });
|
mkdirSync(join(root, ".worktrees", "feature", ".fusion"), { recursive: true });
|
||||||
const cwd = join(root, ".worktrees", "feature", "sub");
|
mkdirSync(join(root, ".fusion", "worktrees", "feature", ".fusion"), { recursive: true });
|
||||||
mkdirSync(cwd, { recursive: true });
|
const legacyCwd = join(root, ".worktrees", "feature", "sub");
|
||||||
|
const fusionCwd = join(root, ".fusion", "worktrees", "feature", "sub");
|
||||||
|
mkdirSync(legacyCwd, { recursive: true });
|
||||||
|
mkdirSync(fusionCwd, { recursive: true });
|
||||||
|
|
||||||
expect(resolvePiExtensionProjectRoot(cwd)).toBe(root);
|
expect(resolvePiExtensionProjectRoot(legacyCwd)).toBe(root);
|
||||||
|
expect(resolvePiExtensionProjectRoot(fusionCwd)).toBe(root);
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(root, { recursive: true, force: true });
|
rmSync(root, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,15 @@ export function getProjectRootFromWorktree(
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
opts?: { worktreesDirCandidates?: string[] },
|
opts?: { worktreesDirCandidates?: string[] },
|
||||||
): string | null {
|
): string | null {
|
||||||
const legacyMatch = cwd.match(/^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/]|$)/);
|
const knownWorktreePatterns = [
|
||||||
if (legacyMatch) {
|
/^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/]|$)/,
|
||||||
return legacyMatch[1]!;
|
/^(.+?)[\\/]\.fusion[\\/]worktrees[\\/][^\\/]+(?:[\\/]|$)/,
|
||||||
|
];
|
||||||
|
for (const pattern of knownWorktreePatterns) {
|
||||||
|
const match = cwd.match(pattern);
|
||||||
|
if (match) {
|
||||||
|
return match[1]!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const candidate of opts?.worktreesDirCandidates ?? []) {
|
for (const candidate of opts?.worktreesDirCandidates ?? []) {
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ describe("branch contamination recovery classification", () => {
|
|||||||
|
|
||||||
it("reanchors without checkout -B when detached worktree is already at base on bound branch", async () => {
|
it("reanchors without checkout -B when detached worktree is already at base on bound branch", async () => {
|
||||||
const { repoDir, baseSha } = await setupRepo();
|
const { repoDir, baseSha } = await setupRepo();
|
||||||
const secondaryWorktree = path.join(repoDir, "../feature-secondary");
|
const secondaryWorktree = path.join(tmpdir(), `${path.basename(repoDir)}-feature-secondary`);
|
||||||
await run(`git worktree add --detach ${JSON.stringify(secondaryWorktree)} ${baseSha}`, repoDir);
|
await run(`git worktree add --detach ${JSON.stringify(secondaryWorktree)} ${baseSha}`, repoDir);
|
||||||
dirs.push(secondaryWorktree);
|
dirs.push(secondaryWorktree);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user