From b9131616055fded4585ea09cd6520a55b08437ea Mon Sep 17 00:00:00 2001 From: Denys Kukhar Date: Wed, 22 Apr 2026 21:34:11 +0200 Subject: [PATCH 1/3] fix: preserve data_collection child keys from case conversion (#70) data_collection stores user-defined identifiers (e.g. need_callback, call_end_reason, human_reached) that the ElevenLabs platform expects to receive and return verbatim. Without preservation, toCamelCaseKeys silently rewrote them to camelCase on push, which the platform stores as-is and which breaks dashboard features that key on the original snake_case identifiers (e.g. the callback checkbox feature driven by need_callback). Added data_collection and dataCollection to PRESERVE_CHILD_KEYS, so its immediate children are passed through unchanged in both push and pull directions. Tests: three new cases in casing.test.ts cover create / update / get round-trips. Verified that the existing failing reproduction fails without the fix and passes with it. No regressions in the wider suite. Fixes #70 --- src/__tests__/casing.test.ts | 96 ++++++++++++++++++++++++++++++++++++ src/shared/utils.ts | 1 + 2 files changed, 97 insertions(+) diff --git a/src/__tests__/casing.test.ts b/src/__tests__/casing.test.ts index 9b6fc37..91abcb0 100644 --- a/src/__tests__/casing.test.ts +++ b/src/__tests__/casing.test.ts @@ -299,6 +299,102 @@ describe("Key casing normalization", () => { }); }); + it("createAgentApi preserves data_collection child keys (user-defined identifiers)", async () => { + const client = makeMockClient(); + const conversation_config = { + agent: { prompt: { prompt: "hi", temperature: 0 } }, + } as unknown as Record; + const platform_settings = { + data_collection: { + need_callback: { type: "boolean", description: "Whether to call back" }, + call_end_reason: { type: "string", description: "Why the call ended" }, + human_reached: { type: "boolean", description: "Was a human reached" }, + }, + } as unknown as Record; + + await createAgentApi( + client, + "Agent with data_collection", + conversation_config, + platform_settings, + undefined, + [] + ); + + const payload = (client.conversationalAi.agents.create as jest.Mock).mock.calls[0][0]; + + // data_collection top-level key is camelized to dataCollection (envelope convention) + expect(payload.platformSettings).toHaveProperty("dataCollection"); + // Children are user-defined identifiers — must be preserved as-is (snake_case stays snake_case) + expect(payload.platformSettings.dataCollection).toHaveProperty("need_callback"); + expect(payload.platformSettings.dataCollection).toHaveProperty("call_end_reason"); + expect(payload.platformSettings.dataCollection).toHaveProperty("human_reached"); + // Leaf values under each identifier — nested schema fields like 'type'/'description' stay as-is (no underscores to convert) + expect(payload.platformSettings.dataCollection.need_callback).toEqual({ + type: "boolean", + description: "Whether to call back", + }); + }); + + it("updateAgentApi preserves data_collection child keys (user-defined identifiers)", async () => { + const client = makeMockClient(); + const conversation_config = { + agent: { prompt: { prompt: "updated", temperature: 0 } }, + } as unknown as Record; + const platform_settings = { + data_collection: { + need_callback: { type: "boolean", description: "Callback requested" }, + }, + } as unknown as Record; + + await updateAgentApi( + client, + "agent_123", + "Updated", + conversation_config, + platform_settings, + undefined, + [] + ); + + const [, payload] = (client.conversationalAi.agents.update as jest.Mock).mock.calls[0]; + + expect(payload.platformSettings).toHaveProperty("dataCollection"); + expect(payload.platformSettings.dataCollection).toHaveProperty("need_callback"); + }); + + it("getAgentApi preserves data_collection child keys on inbound snake_case conversion", async () => { + const getWithDataCollection = jest.fn().mockResolvedValue({ + agentId: "agent_123", + name: "Test", + conversationConfig: { + agent: { prompt: { prompt: "hi", temperature: 0 } }, + }, + platformSettings: { + dataCollection: { + need_callback: { type: "boolean", description: "Callback" }, + call_end_reason: { type: "string" }, + }, + }, + tags: [], + }); + const client = { + conversationalAi: { agents: { get: getWithDataCollection } }, + } as unknown as ElevenLabsClient; + + const response = await getAgentApi(client, "agent_123") as Record; + + // Envelope snake_cases back for disk + expect(response.platform_settings).toHaveProperty("data_collection"); + // User-defined identifiers preserved as-is — no round-trip corruption + expect(response.platform_settings.data_collection).toHaveProperty("need_callback"); + expect(response.platform_settings.data_collection).toHaveProperty("call_end_reason"); + expect(response.platform_settings.data_collection.need_callback).toEqual({ + type: "boolean", + description: "Callback", + }); + }); + it("createAgentApi preserves 'tools' field when 'tool_ids' is not present", async () => { const client = makeMockClient(); const conversation_config = { diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 7ac85a6..dbf2dcb 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -125,6 +125,7 @@ const PRESERVE_CHILD_KEYS = new Set([ 'dynamic_variables', 'dynamicVariables', 'language_presets', 'languagePresets', 'model_usage', 'modelUsage', + 'data_collection', 'dataCollection', 'nodes', 'edges', ]); From e4635fb9d6d00de8f9519187a2d8a6c9ea0e94c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 6 May 2026 11:52:23 +0200 Subject: [PATCH 2/3] fix: use camelCase mock key in getAgentApi test to actually exercise preservation logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inbound test was using snake_case child keys in the mock response, which would pass even without PRESERVE_CHILD_KEYS. Changed one child key to camelCase (callEndReason) and assert it stays camelCase — proving the preservation logic prevents unwanted conversion to call_end_reason. Co-Authored-By: Claude Opus 4.6 --- src/__tests__/casing.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/__tests__/casing.test.ts b/src/__tests__/casing.test.ts index 91abcb0..cf71117 100644 --- a/src/__tests__/casing.test.ts +++ b/src/__tests__/casing.test.ts @@ -373,7 +373,7 @@ describe("Key casing normalization", () => { platformSettings: { dataCollection: { need_callback: { type: "boolean", description: "Callback" }, - call_end_reason: { type: "string" }, + callEndReason: { type: "string" }, }, }, tags: [], @@ -388,7 +388,9 @@ describe("Key casing normalization", () => { expect(response.platform_settings).toHaveProperty("data_collection"); // User-defined identifiers preserved as-is — no round-trip corruption expect(response.platform_settings.data_collection).toHaveProperty("need_callback"); - expect(response.platform_settings.data_collection).toHaveProperty("call_end_reason"); + // camelCase child key must also be preserved (not converted to call_end_reason) + expect(response.platform_settings.data_collection).toHaveProperty("callEndReason"); + expect(response.platform_settings.data_collection).not.toHaveProperty("call_end_reason"); expect(response.platform_settings.data_collection.need_callback).toEqual({ type: "boolean", description: "Callback", From 303dac180ab898e5adafd78e7612288eca9b85bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 6 May 2026 12:05:27 +0200 Subject: [PATCH 3/3] test: add explicit test for inbound data_collection camelCase key preservation The existing getAgentApi test uses snake_case child keys in the mock, which pass trivially without PRESERVE_CHILD_KEYS. Add a separate test with a camelCase child key (callEndReason) that asserts it is not converted to call_end_reason on the inbound path. Co-Authored-By: Claude Opus 4.6 --- src/__tests__/casing.test.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/__tests__/casing.test.ts b/src/__tests__/casing.test.ts index cf71117..a1e598f 100644 --- a/src/__tests__/casing.test.ts +++ b/src/__tests__/casing.test.ts @@ -373,7 +373,7 @@ describe("Key casing normalization", () => { platformSettings: { dataCollection: { need_callback: { type: "boolean", description: "Callback" }, - callEndReason: { type: "string" }, + call_end_reason: { type: "string" }, }, }, tags: [], @@ -388,15 +388,38 @@ describe("Key casing normalization", () => { expect(response.platform_settings).toHaveProperty("data_collection"); // User-defined identifiers preserved as-is — no round-trip corruption expect(response.platform_settings.data_collection).toHaveProperty("need_callback"); - // camelCase child key must also be preserved (not converted to call_end_reason) - expect(response.platform_settings.data_collection).toHaveProperty("callEndReason"); - expect(response.platform_settings.data_collection).not.toHaveProperty("call_end_reason"); + expect(response.platform_settings.data_collection).toHaveProperty("call_end_reason"); expect(response.platform_settings.data_collection.need_callback).toEqual({ type: "boolean", description: "Callback", }); }); + it("getAgentApi does not snake_case camelCase data_collection child keys", async () => { + const getWithDataCollection = jest.fn().mockResolvedValue({ + agentId: "agent_123", + name: "Test", + conversationConfig: { + agent: { prompt: { prompt: "hi", temperature: 0 } }, + }, + platformSettings: { + dataCollection: { + callEndReason: { type: "string", description: "Why the call ended" }, + }, + }, + tags: [], + }); + const client = { + conversationalAi: { agents: { get: getWithDataCollection } }, + } as unknown as ElevenLabsClient; + + const response = await getAgentApi(client, "agent_123") as Record; + + expect(response.platform_settings).toHaveProperty("data_collection"); + expect(response.platform_settings.data_collection).toHaveProperty("callEndReason"); + expect(response.platform_settings.data_collection).not.toHaveProperty("call_end_reason"); + }); + it("createAgentApi preserves 'tools' field when 'tool_ids' is not present", async () => { const client = makeMockClient(); const conversation_config = {