diff --git a/src/resource-loader.ts b/src/resource-loader.ts index ad60e1c03..deb6e1d87 100644 --- a/src/resource-loader.ts +++ b/src/resource-loader.ts @@ -87,9 +87,13 @@ function writeManagedResourceManifest(agentDir: string): void { installedExtensionDirs = entries .filter(e => e.isDirectory()) .filter(e => { - // Only track directories that are actual extensions (contain index.js or index.ts) + // Track directories that are actual extensions — identified by an + // index.js/index.ts entry point OR an extension-manifest.json (e.g. + // remote-questions which uses mod.ts instead of index.ts). const dirPath = join(bundledExtensionsDir, e.name) - return existsSync(join(dirPath, 'index.js')) || existsSync(join(dirPath, 'index.ts')) + return existsSync(join(dirPath, 'index.js')) + || existsSync(join(dirPath, 'index.ts')) + || existsSync(join(dirPath, 'extension-manifest.json')) }) .map(e => e.name) } diff --git a/src/tests/resource-loader.test.ts b/src/tests/resource-loader.test.ts index 12622a1ad..637b9088a 100644 --- a/src/tests/resource-loader.test.ts +++ b/src/tests/resource-loader.test.ts @@ -1,6 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join, parse } from "node:path"; import { tmpdir } from "node:os"; @@ -98,6 +98,39 @@ test("buildResourceLoader excludes duplicate top-level pi extensions when bundle ); }); +test("initResources manifest tracks all bundled extension subdirectories including remote-questions (#2367)", async () => { + const { initResources } = await import("../resource-loader.ts"); + const tmp = mkdtempSync(join(tmpdir(), "gsd-resource-loader-manifest-")); + const fakeAgentDir = join(tmp, "agent"); + + try { + initResources(fakeAgentDir); + + const manifestPath = join(fakeAgentDir, "managed-resources.json"); + assert.equal(existsSync(manifestPath), true, "managed-resources.json should exist after initResources"); + + const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); + const installedDirs: string[] = manifest.installedExtensionDirs ?? []; + + // remote-questions uses mod.ts (not index.ts) as its entry point and has an + // extension-manifest.json — it must still appear in the manifest so that + // pruneRemovedBundledExtensions can track it across upgrades. + assert.ok( + installedDirs.includes("remote-questions"), + `installedExtensionDirs should include remote-questions but got: [${installedDirs.join(", ")}]`, + ); + + // Also verify that the synced remote-questions directory actually exists in the agent dir + assert.equal( + existsSync(join(fakeAgentDir, "extensions", "remote-questions")), + true, + "remote-questions directory should be synced to agent extensions", + ); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } +}); + test("initResources prunes stale top-level extension siblings next to bundled compiled extensions", async (t) => { const { initResources } = await import("../resource-loader.ts"); const tmp = mkdtempSync(join(tmpdir(), "gsd-resource-loader-sync-"));