fix: reject duplicate session entry slots

This commit is contained in:
Eva
2026-05-01 20:02:06 +07:00
committed by Josh Lehman
parent 757a8a9a43
commit 6fe535cb87
2 changed files with 78 additions and 0 deletions

View File

@@ -142,6 +142,56 @@ describe("plugin session extension SessionEntry projection", () => {
);
});
it("rejects duplicate promoted SessionEntry slot keys across registrations", () => {
const { config, registry } = createPluginRegistryFixture();
registerTestPlugin({
registry,
config,
record: createPluginRecord({ id: "slot-owner", name: "Slot Owner" }),
register(api) {
api.registerSessionExtension({
namespace: "workflow",
description: "first promoted slot",
sessionEntrySlotKey: "approvalSnapshot",
});
api.registerSessionExtension({
namespace: "recovery",
description: "same plugin duplicate slot",
sessionEntrySlotKey: " approvalSnapshot ",
});
},
});
registerTestPlugin({
registry,
config,
record: createPluginRecord({ id: "slot-colliding-plugin", name: "Slot Colliding" }),
register(api) {
api.registerSessionExtension({
namespace: "workflow",
description: "cross-plugin duplicate slot",
sessionEntrySlotKey: "approvalSnapshot",
});
},
});
expect(registry.registry.sessionExtensions ?? []).toHaveLength(1);
expect(registry.registry.sessionExtensions?.[0]?.extension.sessionEntrySlotKey).toBe(
"approvalSnapshot",
);
expect(registry.registry.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
pluginId: "slot-owner",
message: "sessionEntrySlotKey already registered: approvalSnapshot",
}),
expect.objectContaining({
pluginId: "slot-colliding-plugin",
message: "sessionEntrySlotKey already registered: approvalSnapshot",
}),
]),
);
});
it("clears promoted SessionEntry slots with plugin-owned session state", async () => {
const { config, registry } = createPluginRegistryFixture();
registerTestPlugin({

View File

@@ -1626,6 +1626,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
const namespace = normalizeHostHookString(extension.namespace);
const description = normalizeHostHookString(extension.description);
const project = extension.project;
let normalizedSessionEntrySlotKey: string | undefined;
let invalidMessage: string | undefined;
if (!namespace || !description) {
invalidMessage = "session extension registration requires namespace and description";
@@ -1639,6 +1640,8 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
const slotKey = normalizeSessionEntrySlotKey(extension.sessionEntrySlotKey);
if (!slotKey.ok) {
invalidMessage = slotKey.error;
} else {
normalizedSessionEntrySlotKey = slotKey.key;
}
}
if (invalidMessage) {
@@ -1662,6 +1665,28 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
});
return;
}
if (normalizedSessionEntrySlotKey) {
const existingSlot = (registry.sessionExtensions ?? []).find((entry) => {
const existingSlotKey = entry.extension.sessionEntrySlotKey;
if (existingSlotKey === undefined) {
return false;
}
const normalizedExistingSlotKey = normalizeSessionEntrySlotKey(existingSlotKey);
return (
normalizedExistingSlotKey.ok &&
normalizedExistingSlotKey.key === normalizedSessionEntrySlotKey
);
});
if (existingSlot) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `sessionEntrySlotKey already registered: ${normalizedSessionEntrySlotKey}`,
});
return;
}
}
(registry.sessionExtensions ??= []).push({
pluginId: record.id,
pluginName: record.name,
@@ -1669,6 +1694,9 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
...extension,
namespace,
description,
...(normalizedSessionEntrySlotKey
? { sessionEntrySlotKey: normalizedSessionEntrySlotKey }
: {}),
},
source: record.source,
rootDir: record.rootDir,