import { describe, expect, it } from "vitest"; import type { Db } from "@paperclipai/db"; import { applyManagedExperimentalOverlay, instanceSettingsService, normalizeExperimentalSettings, } from "../services/instance-settings.js"; import { parseManagedConfigEnv } from "../services/managed-config.js"; const MANAGED_RAW = JSON.stringify({ v: 1, mode: "cloud", catalogVersion: "2026.720.0", // enableApps is retained only for compatibility and must be ignored; // enablePipelines remains a live managed feature. features: { enableApps: false, enablePipelines: true }, plugins: { autoInstall: [] }, }); function managedEnv(raw: string | undefined = MANAGED_RAW) { return { PAPERCLIP_MANAGED_CONFIG: raw }; } /** * Minimal stand-in for the drizzle query chains instanceSettingsService uses. * Captures every `update().set()` payload so tests can assert what would be * persisted. */ function stubDb(row: Record) { const persistedSets: Array> = []; const db = { select: () => ({ from: () => ({ where: () => Promise.resolve([row]) }) }), insert: () => { throw new Error("unexpected insert in test"); }, update: () => ({ set: (values: Record) => { persistedSets.push(values); return { where: () => ({ returning: () => Promise.resolve([{ ...row, ...values }]) }) }; }, }), } as unknown as Db; return { db, persistedSets }; } function settingsRow(experimental: Record) { return { id: "row-1", singletonKey: "default", defaultEnvironmentId: null, general: {}, experimental, createdAt: new Date("2026-06-20T00:00:00.000Z"), updatedAt: new Date("2026-06-20T00:00:00.000Z"), }; } describe("applyManagedExperimentalOverlay", () => { it("is the identity with no managed config (self-hosted)", () => { const experimental = normalizeExperimentalSettings({ enableApps: true }); const result = applyManagedExperimentalOverlay(experimental, null); expect(result.experimental).toEqual(experimental); expect(result.managedKeys).toEqual({}); }); it("ignores the retired Apps flag while overlaying live managed values", () => { const config = parseManagedConfigEnv(managedEnv())!; const stored = normalizeExperimentalSettings({ enableApps: true }); const { experimental, managedKeys } = applyManagedExperimentalOverlay(stored, config); expect(experimental.enableApps).toBe(true); // managed overlay > schema default expect(experimental.enablePipelines).toBe(true); // unmanaged keys keep their stored/default values expect(experimental.enableCases).toBe(false); expect(managedKeys).toEqual({ enablePipelines: { managed: true, managedBy: "paperclip-cloud" }, }); // input is not mutated expect(stored.enableApps).toBe(true); }); }); describe("instanceSettingsService managed overlay", () => { it("persists chat connector opt-in and reads it back after service reconstruction", async () => { const row = settingsRow({}); const { db, persistedSets } = stubDb(row); const service = instanceSettingsService(db, { runtimeEnv: {} }); expect((await service.getExperimental()).enableChatConnectors).toBe(false); for (const enabled of [true, false]) { const updated = await service.updateExperimental({ enableChatConnectors: enabled }); Object.assign(row, persistedSets.at(-1)); expect(updated.experimental.enableChatConnectors).toBe(enabled); const restored = await instanceSettingsService(db, { runtimeEnv: {} }).getExperimental(); expect(restored).toMatchObject({ enableApps: true, enableChatConnectors: enabled }); } }); it("overlays the managed chat connector gate without changing stored data", async () => { const { db, persistedSets } = stubDb(settingsRow({ enableChatConnectors: true })); const service = instanceSettingsService(db, { runtimeEnv: managedEnv(JSON.stringify({ v: 1, mode: "cloud", catalogVersion: "test", features: { enableChatConnectors: false }, plugins: { autoInstall: [] }, })) }); expect(await service.getExperimental()).toMatchObject({ enableChatConnectors: false, managedKeys: { enableChatConnectors: { managed: true, managedBy: "paperclip-cloud" } }, }); expect(persistedSets).toHaveLength(0); }); it("fails closed at construction on a malformed managed config", () => { const { db } = stubDb(settingsRow({})); expect(() => instanceSettingsService(db, { runtimeEnv: managedEnv("{bad") })).toThrow( /PAPERCLIP_MANAGED_CONFIG is not valid JSON/, ); }); it("overlays managed values on getExperimental and exposes managedKeys", async () => { const { db } = stubDb(settingsRow({ enableApps: true })); const svc = instanceSettingsService(db, { runtimeEnv: managedEnv() }); const experimental = await svc.getExperimental(); expect(experimental.enableApps).toBe(true); expect(experimental.enablePipelines).toBe(true); expect(experimental.managedKeys).toEqual({ enablePipelines: { managed: true, managedBy: "paperclip-cloud" }, }); }); it("overlays managed values on get()", async () => { const { db } = stubDb(settingsRow({ enableApps: true })); const svc = instanceSettingsService(db, { runtimeEnv: managedEnv() }); const settings = await svc.get(); expect(settings.experimental.enableApps).toBe(true); expect(settings.experimental.managedKeys?.enableApps).toBeUndefined(); }); it("leaves the self-hosted read path unchanged (no managedKeys field)", async () => { const { db } = stubDb(settingsRow({ enableApps: true })); const svc = instanceSettingsService(db, { runtimeEnv: {} }); const experimental = await svc.getExperimental(); expect(experimental.enableApps).toBe(true); expect(Object.prototype.hasOwnProperty.call(experimental, "managedKeys")).toBe(false); expect(experimental).toEqual(normalizeExperimentalSettings({ enableApps: true })); const settings = await svc.get(); expect(Object.prototype.hasOwnProperty.call(settings.experimental, "managedKeys")).toBe(false); }); it("never persists the overlay: updates write stored values, responses show managed ones", async () => { const { db, persistedSets } = stubDb(settingsRow({ enableApps: true })); const svc = instanceSettingsService(db, { runtimeEnv: managedEnv() }); const updated = await svc.updateExperimental({ enableCases: true }); expect(persistedSets).toHaveLength(1); const persisted = persistedSets[0]!.experimental as Record; // The retired compatibility key normalizes on, independent of the // managed document's historical value. expect(persisted.enableApps).toBe(true); // The overlay-added value is not written. expect(persisted.enablePipelines).toBe(false); expect(persisted.enableCases).toBe(true); expect(persisted).not.toHaveProperty("managedKeys"); // The response still reflects the overlay. expect(updated.experimental.enableApps).toBe(true); expect(updated.experimental.enablePipelines).toBe(true); expect(updated.experimental.managedKeys?.enableApps).toBeUndefined(); }); it("does not let managed metadata leak into self-hosted writes", async () => { const { db, persistedSets } = stubDb(settingsRow({})); const svc = instanceSettingsService(db, { runtimeEnv: {} }); const updated = await svc.updateExperimental({ enableCases: true }); expect(persistedSets).toHaveLength(1); expect(persistedSets[0]!.experimental).not.toHaveProperty("managedKeys"); expect(Object.prototype.hasOwnProperty.call(updated.experimental, "managedKeys")).toBe(false); }); });