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", () => {
|
||||
expect(getProjectRootFromWorktree("/repo/.worktrees/fn-001")).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", () => {
|
||||
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\\.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", () => {
|
||||
@@ -43,10 +47,14 @@ describe("resolvePiExtensionProjectRoot", () => {
|
||||
try {
|
||||
mkdirSync(join(root, ".fusion"), { recursive: true });
|
||||
mkdirSync(join(root, ".worktrees", "feature", ".fusion"), { recursive: true });
|
||||
const cwd = join(root, ".worktrees", "feature", "sub");
|
||||
mkdirSync(cwd, { recursive: true });
|
||||
mkdirSync(join(root, ".fusion", "worktrees", "feature", ".fusion"), { 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 {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -40,9 +40,15 @@ export function getProjectRootFromWorktree(
|
||||
cwd: string,
|
||||
opts?: { worktreesDirCandidates?: string[] },
|
||||
): string | null {
|
||||
const legacyMatch = cwd.match(/^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/]|$)/);
|
||||
if (legacyMatch) {
|
||||
return legacyMatch[1]!;
|
||||
const knownWorktreePatterns = [
|
||||
/^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/]|$)/,
|
||||
/^(.+?)[\\/]\.fusion[\\/]worktrees[\\/][^\\/]+(?:[\\/]|$)/,
|
||||
];
|
||||
for (const pattern of knownWorktreePatterns) {
|
||||
const match = cwd.match(pattern);
|
||||
if (match) {
|
||||
return match[1]!;
|
||||
}
|
||||
}
|
||||
|
||||
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 () => {
|
||||
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);
|
||||
dirs.push(secondaryWorktree);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user