From 5c2d8988bb012dcf4d435248929f3b43b1bfcf53 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 5 Apr 2026 01:04:44 -0400 Subject: [PATCH] fix: track remote-questions in managed-resources manifest (#3312) * fix: track remote-questions extension in managed-resources manifest writeManagedResourceManifest only checked for index.js/index.ts when deciding if a subdirectory is an extension. remote-questions uses mod.ts as its entry point and was missed, causing it to be pruned on upgrades. Also check for extension-manifest.json which is the canonical marker for bundled extensions. Fixes #2367 Co-Authored-By: Claude Opus 4.6 (1M context) * chore: retrigger CI --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: trek-e --- src/resource-loader.ts | 8 +++++-- src/tests/resource-loader.test.ts | 35 ++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) 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-"));