feat(FN-3338): fix project root resolution in registerExtensionProviders an
The merge adds an auto-reload setting (FN-3334) with UI controls in the settings modal, documentation, and a new version-check module, while also fixing a bug (FN-3338) where extension providers incorrectly resolved the project root when invoked from git worktrees — moving the project-root resolutio Fusion-Task-Id: FN-3338
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix extension provider registration using wrong directory when project runs outside engine's working directory.
|
||||
@@ -22,6 +22,7 @@ const execSyncMock = vi.fn((_cmd?: any, _opts?: any) => "");
|
||||
const existsSyncMock = vi.fn((_path: PathLike) => false);
|
||||
const readFileSyncMock = vi.fn((_path?: any) => "{}");
|
||||
const readCustomProvidersMock = vi.fn(() => []);
|
||||
const packageManagerCwdCapture = vi.fn();
|
||||
|
||||
// Route async `exec` through the `execSync` mock so the promisify bridge works.
|
||||
// Use Symbol.for("nodejs.util.promisify.custom") directly to avoid async imports
|
||||
@@ -96,6 +97,9 @@ vi.mock("@mariozechner/pi-coding-agent", () => ({
|
||||
}
|
||||
},
|
||||
DefaultPackageManager: class {
|
||||
constructor(options: any) {
|
||||
packageManagerCwdCapture(options?.cwd);
|
||||
}
|
||||
async resolve() {
|
||||
return packageManagerResolveMock();
|
||||
}
|
||||
@@ -486,6 +490,46 @@ describe("createFnAgent", () => {
|
||||
expect(createAgentSessionMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("FN-3338: registerExtensionProviders receives resolved project root when cwd is a subdirectory", async () => {
|
||||
// Simulate cwd being a subdirectory of the project. resolvePiExtensionProjectRoot
|
||||
// walks up from /project/src/components checking each dir for .fusion.
|
||||
existsSyncMock.mockImplementation((path) => {
|
||||
const value = String(path);
|
||||
return value === "/project/.fusion";
|
||||
});
|
||||
|
||||
const { createFnAgent } = await import("../pi.js");
|
||||
|
||||
await createFnAgent({
|
||||
cwd: "/project/src/components",
|
||||
systemPrompt: "test",
|
||||
tools: "readonly",
|
||||
});
|
||||
|
||||
// registerExtensionProviders should receive the resolved project root,
|
||||
// not the raw subdirectory cwd. This is verified by checking the
|
||||
// DefaultPackageManager constructor received "/project" as cwd.
|
||||
expect(packageManagerCwdCapture).toHaveBeenCalledWith("/project");
|
||||
expect(createAgentSessionMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("FN-3338: registerExtensionProviders falls back to cwd when no .fusion is found", async () => {
|
||||
// No .fusion directory exists anywhere above cwd.
|
||||
existsSyncMock.mockImplementation(() => false);
|
||||
|
||||
const { createFnAgent } = await import("../pi.js");
|
||||
|
||||
await createFnAgent({
|
||||
cwd: "/unrelated/directory",
|
||||
systemPrompt: "test",
|
||||
tools: "readonly",
|
||||
});
|
||||
|
||||
// Falls back to the raw cwd when no .fusion is found
|
||||
expect(packageManagerCwdCapture).toHaveBeenCalledWith("/unrelated/directory");
|
||||
expect(createAgentSessionMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("registers extension providers before resolving configured models", async () => {
|
||||
packageManagerResolveMock.mockResolvedValueOnce({
|
||||
extensions: [{ enabled: true, path: "/extensions/zai-provider" }],
|
||||
@@ -983,11 +1027,13 @@ describe("createFnAgent", () => {
|
||||
}
|
||||
},
|
||||
DefaultPackageManager: class {
|
||||
constructor(options: any) {
|
||||
packageManagerCwdCapture(options?.cwd);
|
||||
}
|
||||
async resolve() {
|
||||
return packageManagerResolveMock();
|
||||
}
|
||||
},
|
||||
discoverAndLoadExtensions: discoverAndLoadExtensionsMock,
|
||||
getAgentDir: () => "/mock-agent-dir",
|
||||
ModelRegistry: class {
|
||||
static create(...args: unknown[]) {
|
||||
@@ -1068,11 +1114,13 @@ describe("createFnAgent", () => {
|
||||
}
|
||||
},
|
||||
DefaultPackageManager: class {
|
||||
constructor(options: any) {
|
||||
packageManagerCwdCapture(options?.cwd);
|
||||
}
|
||||
async resolve() {
|
||||
return packageManagerResolveMock();
|
||||
}
|
||||
},
|
||||
discoverAndLoadExtensions: discoverAndLoadExtensionsMock,
|
||||
getAgentDir: () => "/mock-agent-dir",
|
||||
ModelRegistry: class {
|
||||
static create(...args: unknown[]) {
|
||||
@@ -1150,11 +1198,13 @@ describe("createFnAgent", () => {
|
||||
}
|
||||
},
|
||||
DefaultPackageManager: class {
|
||||
constructor(options: any) {
|
||||
packageManagerCwdCapture(options?.cwd);
|
||||
}
|
||||
async resolve() {
|
||||
return packageManagerResolveMock();
|
||||
}
|
||||
},
|
||||
discoverAndLoadExtensions: discoverAndLoadExtensionsMock,
|
||||
getAgentDir: () => "/mock-agent-dir",
|
||||
ModelRegistry: class {
|
||||
static create(...args: unknown[]) {
|
||||
|
||||
@@ -1043,7 +1043,12 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
|
||||
piLog.log(`createFnAgent called (tools=${options.tools}, provider=${options.defaultProvider}, model=${options.defaultModelId})`);
|
||||
const authStorage = createFusionAuthStorage();
|
||||
const modelRegistry = ModelRegistry.create(authStorage, getModelRegistryModelsPath());
|
||||
await registerExtensionProviders(options.cwd, modelRegistry);
|
||||
|
||||
// Resolve the project root early so extension providers, skill discovery,
|
||||
// and resource loading all use the correct root when cwd is a worktree,
|
||||
// subdirectory, or any path other than the project root itself.
|
||||
const resolvedProjectRoot = getProjectRootFromWorktree(options.cwd) ?? resolvePiExtensionProjectRoot(options.cwd);
|
||||
await registerExtensionProviders(resolvedProjectRoot, modelRegistry);
|
||||
|
||||
for (const provider of readCustomProviders()) {
|
||||
try {
|
||||
@@ -1109,11 +1114,8 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
|
||||
}
|
||||
const wrappedTools = wrapToolsWithBoundary(tools, worktreePath, worktreeProjectRoot);
|
||||
|
||||
// Resolve the project root for resource discovery (skills, settings, extensions).
|
||||
// When cwd is a worktree (e.g., /project/.worktrees/task-branch) or any other
|
||||
// subdirectory, we walk up to find the project root containing .fusion/.
|
||||
// This ensures skill/settings discovery works regardless of session cwd.
|
||||
const resolvedProjectRoot = worktreeProjectRoot ?? resolvePiExtensionProjectRoot(options.cwd);
|
||||
// resolvedProjectRoot was computed above (before registerExtensionProviders)
|
||||
// and is reused here for resource loader and skill discovery.
|
||||
|
||||
// Compaction is explicitly enabled to prevent context-window overflow during
|
||||
// long-running agent conversations (triage, execution, review, merge).
|
||||
|
||||
Reference in New Issue
Block a user