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) <noreply@anthropic.com>

* chore: retrigger CI

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: trek-e <trek-e@users.noreply.github.com>
This commit is contained in:
Tom Boucher 2026-04-05 01:04:44 -04:00 committed by GitHub
parent 5d194d8701
commit 5c2d8988bb
2 changed files with 40 additions and 3 deletions

View file

@ -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)
}

View file

@ -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-"));