fix(plugins): guard host cleanup extension metadata

This commit is contained in:
Vincent Koc
2026-06-03 12:35:41 +02:00
parent 2accf3875b
commit 33ec4f273e
2 changed files with 87 additions and 13 deletions
@@ -57,4 +57,53 @@ describe("plugin host cleanup config fallback", () => {
},
]);
});
it("continues cleanup after unreadable session extension metadata", async () => {
const registry = createEmptyPluginRegistry();
const runtimeCleanup = vi.fn();
const brokenExtension = {
pluginId: "broken-extension",
pluginName: "Broken Extension",
source: "test",
} as NonNullable<typeof registry.sessionExtensions>[number];
Object.defineProperty(brokenExtension, "extension", {
get() {
throw new Error("session extension getter exploded");
},
});
registry.sessionExtensions = [brokenExtension];
registry.runtimeLifecycles ??= [];
registry.runtimeLifecycles.push({
pluginId: "healthy-runtime",
pluginName: "Healthy Runtime",
source: "test",
lifecycle: {
id: "healthy-cleanup",
cleanup: runtimeCleanup,
},
});
const result = await runPluginHostCleanup({
cfg: {},
registry,
reason: "disable",
});
expect(runtimeCleanup.mock.calls).toEqual([
[
{
runId: undefined,
reason: "disable",
sessionKey: undefined,
},
],
]);
expect(result.cleanupCount).toBe(1);
expect(result.failures).toHaveLength(1);
expect(result.failures[0]).toMatchObject({
pluginId: "broken-extension",
hookId: "session:unknown",
});
expect(result.failures[0]?.error).toBeInstanceOf(Error);
});
});
+38 -13
View File
@@ -324,17 +324,21 @@ function collectSessionEntrySlotKeys(
): Set<string> {
const slotKeys = new Set<string>();
for (const registration of registry?.sessionExtensions ?? []) {
if (!shouldCleanPlugin(registration.pluginId, pluginId)) {
try {
if (!shouldCleanPlugin(registration.pluginId, pluginId)) {
continue;
}
const slotKey = registration.extension.sessionEntrySlotKey;
if (slotKey === undefined) {
continue;
}
const normalized = normalizeSessionEntrySlotKey(slotKey);
if (normalized.ok) {
slotKeys.add(normalized.key);
}
} catch {
continue;
}
const slotKey = registration.extension.sessionEntrySlotKey;
if (slotKey === undefined) {
continue;
}
const normalized = normalizeSessionEntrySlotKey(slotKey);
if (normalized.ok) {
slotKeys.add(normalized.key);
}
}
return slotKeys;
}
@@ -400,14 +404,35 @@ export async function runPluginHostCleanup(params: {
if (!shouldCleanup()) {
return { cleanupCount, failures };
}
if (!shouldCleanPlugin(registration.pluginId, params.pluginId)) {
let pluginId: string;
let hookId: string;
let cleanup:
| ((params: {
reason: PluginHostCleanupReason;
sessionKey?: string;
}) => void | Promise<void>)
| undefined;
let failurePluginId = params.pluginId ?? "plugin-host";
try {
pluginId = registration.pluginId;
failurePluginId = pluginId;
if (!shouldCleanPlugin(pluginId, params.pluginId)) {
continue;
}
const extension = registration.extension;
cleanup = extension.cleanup;
hookId = `session:${extension.namespace}`;
} catch (error) {
failures.push({
pluginId: failurePluginId,
hookId: "session:unknown",
error,
});
continue;
}
const cleanup = registration.extension.cleanup;
if (!cleanup) {
continue;
}
const hookId = `session:${registration.extension.namespace}`;
try {
await withPluginHostCleanupTimeout(hookId, () =>
cleanup({
@@ -418,7 +443,7 @@ export async function runPluginHostCleanup(params: {
cleanupCount += 1;
} catch (error) {
failures.push({
pluginId: registration.pluginId,
pluginId,
hookId,
error,
});