diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index f9e059693..2bdb753c2 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -130,6 +130,11 @@ let testGatewayBind: "auto" | "lan" | "tailnet" | "loopback" | undefined; let testGatewayAuth: Record | undefined; let testHooksConfig: Record | undefined; let testCanvasHostPort: number | undefined; +let testLegacyIssues: Array<{ path: string; message: string }> = []; +let testLegacyParsed: Record = {}; +let testMigrationConfig: Record | null = null; +let testMigrationChanges: string[] = []; +let testIsNixMode = false; const sessionStoreSaveDelayMs = vi.hoisted(() => ({ value: 0 })); vi.mock("../config/sessions.js", async () => { const actual = await vi.importActual( @@ -151,6 +156,21 @@ vi.mock("../config/config.js", () => { path.join(os.homedir(), ".clawdis", "clawdis.json"); const readConfigFileSnapshot = async () => { + if (testLegacyIssues.length > 0) { + return { + path: resolveConfigPath(), + exists: true, + raw: JSON.stringify(testLegacyParsed ?? {}), + parsed: testLegacyParsed ?? {}, + valid: false, + config: {}, + issues: testLegacyIssues.map((issue) => ({ + path: issue.path, + message: issue.message, + })), + legacyIssues: testLegacyIssues, + }; + } const configPath = resolveConfigPath(); try { await fs.access(configPath); @@ -193,20 +213,20 @@ vi.mock("../config/config.js", () => { } }; - const writeConfigFile = async (cfg: Record) => { + const writeConfigFile = vi.fn(async (cfg: Record) => { const configPath = resolveConfigPath(); await fs.mkdir(path.dirname(configPath), { recursive: true }); const raw = JSON.stringify(cfg, null, 2).trimEnd().concat("\n"); await fs.writeFile(configPath, raw, "utf-8"); - }; + }); return { CONFIG_PATH_CLAWDIS: resolveConfigPath(), STATE_DIR_CLAWDIS: path.dirname(resolveConfigPath()), - isNixMode: false, + isNixMode: testIsNixMode, migrateLegacyConfig: (raw: unknown) => ({ - config: raw as Record, - changes: [], + config: testMigrationConfig ?? (raw as Record), + changes: testMigrationChanges, }), loadConfig: () => ({ agent: { @@ -286,6 +306,11 @@ beforeEach(async () => { testGatewayAuth = undefined; testHooksConfig = undefined; testCanvasHostPort = undefined; + testLegacyIssues = []; + testLegacyParsed = {}; + testMigrationConfig = null; + testMigrationChanges = []; + testIsNixMode = false; cronIsolatedRun.mockClear(); drainSystemEvents(); resetAgentRunContextForTest(); @@ -523,6 +548,40 @@ describe("gateway server", () => { }, ); + test("auto-migrates legacy config on startup", async () => { + (writeConfigFile as unknown as { mockClear?: () => void })?.mockClear?.(); + testLegacyIssues = [ + { + path: "routing.allowFrom", + message: "legacy", + }, + ]; + testLegacyParsed = { routing: { allowFrom: ["+15555550123"] } }; + testMigrationConfig = { whatsapp: { allowFrom: ["+15555550123"] } }; + testMigrationChanges = ["Moved routing.allowFrom → whatsapp.allowFrom."]; + + const port = await getFreePort(); + const server = await startGatewayServer(port); + expect(writeConfigFile).toHaveBeenCalledWith(testMigrationConfig); + await server.close(); + }); + + test("fails in Nix mode when legacy config is present", async () => { + testLegacyIssues = [ + { + path: "routing.allowFrom", + message: "legacy", + }, + ]; + testLegacyParsed = { routing: { allowFrom: ["+15555550123"] } }; + testIsNixMode = true; + + const port = await getFreePort(); + await expect(startGatewayServer(port)).rejects.toThrow( + "Legacy config entries detected while running in Nix mode", + ); + }); + test("models.list returns model catalog", async () => { piSdkMock.enabled = true; piSdkMock.models = [