fix(coding-agent): cache extension imports for session switches

Closes #5905.
This commit is contained in:
Armin Ronacher
2026-06-20 21:05:55 +02:00
parent a1da88aed4
commit 5505316ea2
4 changed files with 226 additions and 10 deletions

View File

@@ -0,0 +1,131 @@
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { clearExtensionCache, loadExtensions, loadExtensionsCached } from "../../../src/core/extensions/loader.ts";
import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts";
interface TestState {
moduleLoads?: number;
factoryRuns?: number;
}
function state(): TestState {
const global = globalThis as typeof globalThis & { __extensionFactoryCacheTest?: TestState };
if (!global.__extensionFactoryCacheTest) {
global.__extensionFactoryCacheTest = {};
}
return global.__extensionFactoryCacheTest;
}
function resetState(): void {
delete (globalThis as typeof globalThis & { __extensionFactoryCacheTest?: TestState }).__extensionFactoryCacheTest;
}
function writeCountingExtension(filePath: string): void {
writeFileSync(
filePath,
`
const state = (globalThis.__extensionFactoryCacheTest ??= {});
state.moduleLoads = (state.moduleLoads ?? 0) + 1;
export default function () {
state.factoryRuns = (state.factoryRuns ?? 0) + 1;
}
`,
"utf-8",
);
}
describe("extension factory cache", () => {
const roots: string[] = [];
function fixture(name: string) {
const root = join(tmpdir(), `pi-extension-cache-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
const cwd = join(root, "project");
const agentDir = join(root, "agent");
mkdirSync(cwd, { recursive: true });
mkdirSync(agentDir, { recursive: true });
roots.push(root);
return { root, cwd, agentDir };
}
beforeEach(() => {
resetState();
clearExtensionCache();
});
afterEach(() => {
while (roots.length > 0) {
const root = roots.pop();
if (root && existsSync(root)) {
rmSync(root, { recursive: true, force: true });
}
}
resetState();
clearExtensionCache();
});
it("caches extension modules for cached same-cwd loads but reruns factories", async () => {
const { root, cwd } = fixture("same-cwd");
const extensionPath = join(root, "counting.ts");
writeCountingExtension(extensionPath);
const first = await loadExtensionsCached([extensionPath], cwd);
const second = await loadExtensionsCached([extensionPath], cwd);
expect(state().moduleLoads).toBe(1);
expect(state().factoryRuns).toBe(2);
expect(first.extensions[0]).not.toBe(second.extensions[0]);
expect(first.runtime).not.toBe(second.runtime);
});
it("does not cache direct loadExtensions calls", async () => {
const { root, cwd } = fixture("direct");
const extensionPath = join(root, "counting.ts");
writeCountingExtension(extensionPath);
await loadExtensions([extensionPath], cwd);
await loadExtensions([extensionPath], cwd);
expect(state().moduleLoads).toBe(2);
expect(state().factoryRuns).toBe(2);
});
it("clears the cache on resource loader reload", async () => {
const { cwd, agentDir } = fixture("reload");
const extensionDir = join(agentDir, "extensions");
mkdirSync(extensionDir, { recursive: true });
writeCountingExtension(join(extensionDir, "counting.ts"));
const loader = new DefaultResourceLoader({
cwd,
agentDir,
noSkills: true,
noPromptTemplates: true,
noThemes: true,
});
await loader.reload();
await loader.reload();
expect(state().moduleLoads).toBe(2);
expect(state().factoryRuns).toBe(2);
});
it("keeps the cache scoped to one cwd", async () => {
const { root } = fixture("cross-cwd");
const firstCwd = join(root, "first");
const secondCwd = join(root, "second");
mkdirSync(firstCwd, { recursive: true });
mkdirSync(secondCwd, { recursive: true });
const extensionPath = join(root, "counting.ts");
writeCountingExtension(extensionPath);
await loadExtensionsCached([extensionPath], firstCwd);
await loadExtensionsCached([extensionPath], secondCwd);
await loadExtensionsCached([extensionPath], secondCwd);
expect(state().moduleLoads).toBe(2);
expect(state().factoryRuns).toBe(3);
});
});