From e6865ec7e1f7d0aa703d8c5e18d9f422c56f51be Mon Sep 17 00:00:00 2001 From: Pier Carlo Cadoppi Date: Wed, 4 Nov 2026 10:04:24 +0000 Subject: [PATCH] fix(coding-agent): realpath extension entries before jiti import (#8112) Closes #8092 --- .../src/core/extensions/loader.ts | 4 +- .../8092-extension-realpath-jiti.test.ts | 85 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/8092-extension-realpath-jiti.test.ts diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 91c85ead4..e26aebabf 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -12,7 +12,7 @@ import { mcpNamespace } from "@earendil-works/pi-mcp"; import type { KeyId } from "@earendil-works/pi-tui"; import type { createJiti } from "jiti"; import { CONFIG_DIR_NAME, getAgentDir, isBunBinary, isBundledNode } from "../../config.ts"; -import { pathsEqual, resolvePath } from "../../utils/paths.ts"; +import { canonicalizePath, pathsEqual, resolvePath } from "../../utils/paths.ts"; import { createEventBus, type EventBus } from "../event-bus.ts"; import type { ExecOptions } from "../exec.ts"; import { execCommand } from "../exec.ts"; @@ -590,7 +590,7 @@ async function loadExtensionModule(extensionPath: string, cacheToken?: Extension ...resolutionOptions, }); - const module = await jiti.import(extensionPath, { default: true }); + const module = await jiti.import(canonicalizePath(extensionPath), { default: true }); const factory = module as ExtensionFactory; if (typeof factory !== "function") { return undefined; diff --git a/packages/coding-agent/test/suite/regressions/8092-extension-realpath-jiti.test.ts b/packages/coding-agent/test/suite/regressions/8092-extension-realpath-jiti.test.ts new file mode 100644 index 000000000..154e2e92f --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/8092-extension-realpath-jiti.test.ts @@ -0,0 +1,85 @@ +import { existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { clearExtensionCache, loadExtensions } from "../../../src/core/extensions/loader.ts"; + +interface TestState { + depValue?: unknown; +} + +function state(): TestState { + const global = globalThis as typeof globalThis & { __extensionRealpathTest?: TestState }; + if (!global.__extensionRealpathTest) { + global.__extensionRealpathTest = {}; + } + return global.__extensionRealpathTest; +} + +function resetState(): void { + delete (globalThis as typeof globalThis & { __extensionRealpathTest?: TestState }).__extensionRealpathTest; +} + +const roots: string[] = []; + +const DIRECTORY_LINK_TYPE = process.platform === "win32" ? "junction" : "dir"; + +function fixture(): string { + const root = join(tmpdir(), `pi-ext-realpath-${Date.now()}-${Math.random().toString(36).slice(2)}`); + mkdirSync(root, { recursive: true }); + roots.push(root); + return root; +} + +afterEach(() => { + while (roots.length > 0) { + const root = roots.pop(); + if (root && existsSync(root)) { + rmSync(root, { recursive: true, force: true }); + } + } + resetState(); + clearExtensionCache(); +}); + +describe("regression #8092: extension entries are realpath'd before jiti load", () => { + it("loads an extension whose declared dep is only reachable via the pnpm store", async () => { + const root = fixture(); + + // pnpm isolated layout: node_modules/ is a symlink into the + // virtual store, and declared deps exist only as store siblings. + const storePkgDir = join(root, "node_modules", ".pnpm", "@test+ext@1.0.0", "node_modules", "@test"); + const extDir = join(storePkgDir, "ext"); + const depDir = join(storePkgDir, "dep"); + mkdirSync(extDir, { recursive: true }); + mkdirSync(depDir, { recursive: true }); + + writeFileSync(join(depDir, "index.js"), `export const value = 42;\n`, "utf-8"); + writeFileSync( + join(extDir, "index.ts"), + ` +import { value } from "@test/dep"; + +export default function () { + (globalThis.__extensionRealpathTest ??= {}).depValue = value; +} +`, + "utf-8", + ); + + const topLevel = join(root, "node_modules", "@test"); + mkdirSync(topLevel, { recursive: true }); + // Junctions (win32) require an absolute target; pnpm's own relative + // link targets are equivalent here since the store path is absolute. + symlinkSync(join(storePkgDir, "ext"), join(topLevel, "ext"), DIRECTORY_LINK_TYPE); + + const entry = join(topLevel, "ext", "index.ts"); + expect(existsSync(entry)).toBe(true); + + const result = await loadExtensions([entry], root); + + expect(result.errors).toEqual([]); + expect(result.extensions).toHaveLength(1); + expect(state().depValue).toBe(42); + }); +});