feat(FN-4904): complete Step 3 — align engine and CLI root resolution
Fusion-Task-Id: FN-4904 Fusion-Task-Lineage: 5da52c8c-0ed6-4e3a-920e-aa1b56d0b719
This commit is contained in:
committed by
gsxdsm
parent
b40a58d693
commit
47eaf089bf
60
packages/cli/src/__tests__/extension-task-tools.test.ts
Normal file
60
packages/cli/src/__tests__/extension-task-tools.test.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
vi.mock("@fusion/dashboard", () => ({
|
||||||
|
registerGithubTrackingHook: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@fusion/engine", () => ({
|
||||||
|
createFnAgent: vi.fn(),
|
||||||
|
fetchWebContent: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import kbExtension from "../extension.js";
|
||||||
|
import { TaskStore } from "@fusion/core";
|
||||||
|
|
||||||
|
function makeCtx(cwd: string) {
|
||||||
|
return { cwd } as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("extension task tools resolve repo root from worktrees", () => {
|
||||||
|
it("uses canonical project root for fn_task_show and fn_task_list from worktree cwd", async () => {
|
||||||
|
const repoRoot = await mkdtemp(join(tmpdir(), "fn-4904-cli-"));
|
||||||
|
const worktreeRoot = join(repoRoot, ".worktrees", "feature");
|
||||||
|
try {
|
||||||
|
await mkdir(join(repoRoot, ".fusion"), { recursive: true });
|
||||||
|
await mkdir(join(worktreeRoot, ".fusion"), { recursive: true });
|
||||||
|
|
||||||
|
const store = new TaskStore(repoRoot);
|
||||||
|
await store.init();
|
||||||
|
await store.createTask({ description: "Task from canonical root" });
|
||||||
|
|
||||||
|
const tools = new Map<string, any>();
|
||||||
|
kbExtension({
|
||||||
|
registerTool(def: any) {
|
||||||
|
tools.set(def.name, def);
|
||||||
|
},
|
||||||
|
registerCommand: vi.fn(),
|
||||||
|
registerShortcut: vi.fn(),
|
||||||
|
registerFlag: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const showTool = tools.get("fn_task_show");
|
||||||
|
const listTool = tools.get("fn_task_list");
|
||||||
|
expect(showTool).toBeTruthy();
|
||||||
|
expect(listTool).toBeTruthy();
|
||||||
|
|
||||||
|
const show = await showTool.execute("show", { id: "FN-001" }, undefined, undefined, makeCtx(worktreeRoot));
|
||||||
|
const list = await listTool.execute("list", {}, undefined, undefined, makeCtx(worktreeRoot));
|
||||||
|
|
||||||
|
expect(show.content[0].text).toContain("FN-001");
|
||||||
|
expect(show.content[0].text).toContain("Task from canonical root");
|
||||||
|
expect(list.content[0].text).toContain("FN-001");
|
||||||
|
} finally {
|
||||||
|
await rm(repoRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
resolveAgentProvisioningPolicy,
|
resolveAgentProvisioningPolicy,
|
||||||
TASK_PRIORITIES,
|
TASK_PRIORITIES,
|
||||||
resolveSecretAccessPolicy,
|
resolveSecretAccessPolicy,
|
||||||
|
getProjectRootFromWorktree,
|
||||||
type SecretScope,
|
type SecretScope,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
@@ -67,6 +68,11 @@ const MIME_TYPES: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function resolveProjectRoot(cwd: string): string {
|
function resolveProjectRoot(cwd: string): string {
|
||||||
|
const worktreeProjectRoot = getProjectRootFromWorktree(cwd);
|
||||||
|
if (worktreeProjectRoot && existsSync(join(worktreeProjectRoot, ".fusion"))) {
|
||||||
|
return worktreeProjectRoot;
|
||||||
|
}
|
||||||
|
|
||||||
let current = resolve(cwd);
|
let current = resolve(cwd);
|
||||||
while (true) {
|
while (true) {
|
||||||
if (existsSync(join(current, ".fusion"))) {
|
if (existsSync(join(current, ".fusion"))) {
|
||||||
|
|||||||
@@ -74,12 +74,21 @@ describe("resolveProjectRoot", () => {
|
|||||||
expect(resolveProjectRoot(dir)).toBe(dir);
|
expect(resolveProjectRoot(dir)).toBe(dir);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("walks up from worktree path to find project root", () => {
|
it("prefers parent repo root for worktree paths when both parent and worktree have .fusion", () => {
|
||||||
const projectDir = `/tmp/skill-resolver-mock-${++mockDirCounter}`;
|
const projectDir = `/tmp/skill-resolver-mock-${++mockDirCounter}`;
|
||||||
const worktreeDir = `${projectDir}/.worktrees/swift-falcon`;
|
const worktreeDir = `${projectDir}/.worktrees/swift-falcon`;
|
||||||
mockDirs.add(`${projectDir}/.fusion`);
|
mockDirs.add(`${projectDir}/.fusion`);
|
||||||
|
mockDirs.add(`${worktreeDir}/.fusion`);
|
||||||
|
|
||||||
expect(resolveProjectRoot(worktreeDir)).toBe(projectDir);
|
expect(resolveProjectRoot(`${worktreeDir}/sub`)).toBe(projectDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to legacy walk when parent repo .fusion is missing", () => {
|
||||||
|
const projectDir = `/tmp/skill-resolver-mock-${++mockDirCounter}`;
|
||||||
|
const worktreeDir = `${projectDir}/.worktrees/swift-falcon`;
|
||||||
|
mockDirs.add(`${worktreeDir}/.fusion`);
|
||||||
|
|
||||||
|
expect(resolveProjectRoot(`${worktreeDir}/sub`)).toBe(worktreeDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("walks up from deeply nested path", () => {
|
it("walks up from deeply nested path", () => {
|
||||||
|
|||||||
@@ -12,20 +12,25 @@
|
|||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
import type { ResourceDiagnostic, Skill } from "@mariozechner/pi-coding-agent";
|
import type { ResourceDiagnostic, Skill } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { getProjectRootFromWorktree } from "@fusion/core";
|
||||||
import { piLog } from "./logger.js";
|
import { piLog } from "./logger.js";
|
||||||
|
|
||||||
// ── Project Root Resolution ──────────────────────────────────────────────────
|
// ── Project Root Resolution ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the project root directory by walking up from `cwd` looking for
|
* Resolve the project root directory by preferring the parent repo when
|
||||||
* a directory containing `.fusion/`. This handles worktree paths (e.g.,
|
* `cwd` is inside a `.worktrees/<name>/...` path, then falling back to the
|
||||||
* `/project/.worktrees/task-branch`) and any other subdirectory by walking
|
* legacy `.fusion` ancestor walk.
|
||||||
* up to the actual project root.
|
|
||||||
*
|
*
|
||||||
* Falls back to `cwd` if no `.fusion/` directory is found (mirrors
|
* Falls back to `cwd` if no `.fusion/` directory is found (mirrors
|
||||||
* `resolvePiExtensionProjectRoot` from `@fusion/core`).
|
* `resolvePiExtensionProjectRoot` from `@fusion/core`).
|
||||||
*/
|
*/
|
||||||
export function resolveProjectRoot(cwd: string): string {
|
export function resolveProjectRoot(cwd: string): string {
|
||||||
|
const worktreeProjectRoot = getProjectRootFromWorktree(cwd);
|
||||||
|
if (worktreeProjectRoot && existsSync(join(worktreeProjectRoot, ".fusion"))) {
|
||||||
|
return worktreeProjectRoot;
|
||||||
|
}
|
||||||
|
|
||||||
let current = resolve(cwd);
|
let current = resolve(cwd);
|
||||||
while (true) {
|
while (true) {
|
||||||
if (existsSync(join(current, ".fusion"))) {
|
if (existsSync(join(current, ".fusion"))) {
|
||||||
|
|||||||
Reference in New Issue
Block a user