diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 05979b0..ed0bf40 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -1,4 +1,4 @@ -import { calculateConfigHash, toCamelCaseKeys, generateUniqueFilename } from "../shared/utils"; +import { calculateConfigHash, toCamelCaseKeys, toSnakeCaseKeys, generateUniqueFilename } from "../shared/utils"; import fs from "fs-extra"; import path from "path"; import os from "os"; @@ -365,6 +365,121 @@ describe("Utils", () => { }, }); }); + + it("should preserve request_headers in toSnakeCaseKeys for symmetry", () => { + const input = { + conversationConfig: { + agent: { + prompt: { + tools: [ + { + type: "webhook", + apiSchema: { + url: "https://example.com/webhook", + method: "GET", + requestHeaders: { + "Content-Type": "application/json", + "X-Api-Key": { + secretId: "abc" + }, + "Authorization": { + variableName: "auth_token" + } + } + } + } + ] + } + } + } + }; + + const result = toSnakeCaseKeys(input); + + expect(result).toEqual({ + conversation_config: { + agent: { + prompt: { + tools: [ + { + type: "webhook", + api_schema: { + url: "https://example.com/webhook", + method: "GET", + request_headers: { + "Content-Type": "application/json", + "X-Api-Key": { + secretId: "abc" // Should NOT be converted to secret_id + }, + "Authorization": { + variableName: "auth_token" // Should NOT be converted to variable_name + } + } + } + } + ] + } + } + } + }); + }); + + it("should maintain round-trip conversion symmetry for request_headers", () => { + const original = { + conversation_config: { + agent: { + prompt: { + tools: [ + { + type: "webhook", + api_schema: { + url: "https://example.com/webhook", + method: "POST", + request_headers: { + "Content-Type": "application/json", + "X-Api-Key": { + secretId: "my_secret_123" + } + } + } + } + ] + } + } + } + }; + + // Simulate pull (API → local file): camelCase → snake_case + const afterPull = toSnakeCaseKeys(toCamelCaseKeys(original)); + + // Simulate push (local file → API): snake_case → camelCase + const afterPush = toCamelCaseKeys(afterPull); + + // After round-trip, request_headers internals should be preserved + expect(afterPush).toEqual({ + conversationConfig: { + agent: { + prompt: { + tools: [ + { + type: "webhook", + apiSchema: { + url: "https://example.com/webhook", + method: "POST", + requestHeaders: { + "Content-Type": "application/json", + "X-Api-Key": { + secretId: "my_secret_123" // Should be preserved through round-trip + } + } + } + } + ] + } + } + } + }); + }); }); describe("generateUniqueFilename", () => { diff --git a/src/shared/utils.ts b/src/shared/utils.ts index ba6500f..656c122 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -139,14 +139,21 @@ export function toCamelCaseKeys(value: T, skipHeaderConversion = fa return value; } -export function toSnakeCaseKeys(value: T): T { +export function toSnakeCaseKeys(value: T, skipHeaderConversion = false): T { if (Array.isArray(value)) { - return (value.map((v) => toSnakeCaseKeys(v)) as unknown) as T; + return (value.map((v) => toSnakeCaseKeys(v, skipHeaderConversion)) as unknown) as T; } if (isPlainObject(value)) { const result: Record = {}; for (const [k, v] of Object.entries(value)) { - result[toSnakeCaseKey(k)] = toSnakeCaseKeys(v); + if (skipHeaderConversion) { + // Inside request_headers/requestHeaders: preserve all keys as-is to avoid converting + // header names like "X-Api-Key" or nested keys like "secret_id" + result[k] = toSnakeCaseKeys(v, true); + } else { + // Normal conversion + result[toSnakeCaseKey(k)] = toSnakeCaseKeys(v, k === 'request_headers' || k === 'requestHeaders'); + } } return (result as unknown) as T; }