diff --git a/server/src/__tests__/adapter-routes.test.ts b/server/src/__tests__/adapter-routes.test.ts index e1a730d3ec..eede2af23c 100644 --- a/server/src/__tests__/adapter-routes.test.ts +++ b/server/src/__tests__/adapter-routes.test.ts @@ -45,6 +45,7 @@ const overridingConfigSchemaAdapter: ServerAdapterModule = { let registerServerAdapter: typeof import("../adapters/registry.js").registerServerAdapter; let unregisterServerAdapter: typeof import("../adapters/registry.js").unregisterServerAdapter; +let findServerAdapter: typeof import("../adapters/registry.js").findServerAdapter; let setOverridePaused: typeof import("../adapters/registry.js").setOverridePaused; let adapterRoutes: typeof import("../routes/adapters.js").adapterRoutes; let errorHandler: typeof import("../middleware/index.js").errorHandler; @@ -107,6 +108,7 @@ describe("adapter routes", () => { ]); registerServerAdapter = registry.registerServerAdapter; unregisterServerAdapter = registry.unregisterServerAdapter; + findServerAdapter = registry.findServerAdapter; setOverridePaused = registry.setOverridePaused; adapterRoutes = routes.adapterRoutes; errorHandler = middleware.errorHandler; @@ -236,4 +238,44 @@ describe("adapter routes", () => { expect(res.status, JSON.stringify(res.body)).toBe(403); }); + + it("POST /api/adapters/install preserves module-provided sessionManagement (hot-install parity with init-time IIFE)", async () => { + const HOT_INSTALL_TYPE = "hot_install_session_test"; + const declaredSessionManagement = { + supportsSessionResume: true, + nativeContextManagement: "confirmed" as const, + defaultSessionCompaction: { + enabled: true, + maxSessionRuns: 10, + maxRawInputTokens: 100_000, + maxSessionAgeHours: 24, + }, + }; + const externalModule: ServerAdapterModule = { + type: HOT_INSTALL_TYPE, + execute: async () => ({ exitCode: 0, signal: null, timedOut: false }), + testEnvironment: async () => ({ + adapterType: HOT_INSTALL_TYPE, + status: "pass", + checks: [], + testedAt: new Date(0).toISOString(), + }), + sessionManagement: declaredSessionManagement, + }; + mockPluginLoader.loadExternalAdapterPackage.mockResolvedValue(externalModule); + + const app = createApp({ isInstanceAdmin: true }); + const res = await request(app) + .post("/api/adapters/install") + .send({ packageName: "/tmp/fake-hot-install-adapter", isLocalPath: true }); + + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(res.body.type).toBe(HOT_INSTALL_TYPE); + + const registered = findServerAdapter(HOT_INSTALL_TYPE); + expect(registered).not.toBeNull(); + expect(registered?.sessionManagement).toEqual(declaredSessionManagement); + + unregisterServerAdapter(HOT_INSTALL_TYPE); + }); }); diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 40202edc7c..8268f0629a 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -353,8 +353,10 @@ function getDisabledAdapterTypesFromStore(): string[] { * override a built-in — same `type` — inherit the builtin's policy). If * neither is available, `sessionManagement` remains `undefined`. * - * Exported for unit tests; runtime callers use the IIFE below, which - * applies this transformation during the external-adapter load pass. + * Used by both the init-time IIFE below (external-adapter load pass on + * server start) and the hot-install path in `routes/adapters.ts` + * (`registerWithSessionManagement`), so the two load paths resolve + * `sessionManagement` identically. */ export function resolveExternalAdapterRegistration( externalAdapter: ServerAdapterModule, diff --git a/server/src/routes/adapters.ts b/server/src/routes/adapters.ts index 40e5800ef9..efe52b4870 100644 --- a/server/src/routes/adapters.ts +++ b/server/src/routes/adapters.ts @@ -25,11 +25,11 @@ import { findActiveServerAdapter, listEnabledServerAdapters, registerServerAdapter, + resolveExternalAdapterRegistration, unregisterServerAdapter, isOverridePaused, setOverridePaused, } from "../adapters/registry.js"; -import { getAdapterSessionManagement } from "@paperclipai/adapter-utils"; import { listAdapterPlugins, addAdapterPlugin, @@ -168,15 +168,17 @@ async function normalizeLocalPath(rawPath: string): Promise { } /** - * Register an adapter module into the server registry, filling in - * sessionManagement from the host. + * Register an external adapter module into the server registry via the + * hot-install path, resolving `sessionManagement` identically to how the + * init-time IIFE does. Module-provided `sessionManagement` is honored first, + * with fallback to the host registry by type for builtin-type overrides. + * + * Keeps the hot-install and init-time paths at parity so an adapter installed + * via `POST /api/adapters/install` has the same shape in the registry as the + * same adapter loaded on the next server restart. */ function registerWithSessionManagement(adapter: ServerAdapterModule): void { - const wrapped: ServerAdapterModule = { - ...adapter, - sessionManagement: getAdapterSessionManagement(adapter.type) ?? undefined, - }; - registerServerAdapter(wrapped); + registerServerAdapter(resolveExternalAdapterRegistration(adapter)); } // ---------------------------------------------------------------------------