Skip to content

Commit f3e129d

Browse files
[fix] snake case adjustments (#25)
* snake case adjustments * pull workflows in interactive mode
1 parent 765e2d5 commit f3e129d

File tree

3 files changed

+96
-28
lines changed

3 files changed

+96
-28
lines changed

src/__tests__/utils.test.ts

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ describe("Utils", () => {
294294
});
295295
});
296296

297-
it("should preserve secret_id and other keys in request_headers object format", () => {
297+
it("should preserve header names but convert nested object keys in request_headers", () => {
298298
const input = {
299299
conversation_config: {
300300
agent: {
@@ -333,11 +333,11 @@ describe("Utils", () => {
333333
url: "https://example.com/webhook",
334334
method: "GET",
335335
requestHeaders: {
336-
"Content-Type": "application/json",
337-
"X-Api-Key": {
338-
secret_id: "abc" // Should NOT be converted to secretId
336+
"Content-Type": "application/json", // Header name preserved
337+
"X-Api-Key": { // Header name preserved
338+
secretId: "abc" // BUT nested key IS converted
339339
},
340-
"foo_bar": "baz" // Should NOT be converted to fooBar
340+
"foo_bar": "baz" // Header name preserved (string value, no nested conversion)
341341
}
342342
}
343343
}
@@ -366,7 +366,64 @@ describe("Utils", () => {
366366
});
367367
});
368368

369-
it("should preserve request_headers in toSnakeCaseKeys for symmetry", () => {
369+
it("should preserve header names but convert secretId to snake_case in workspace_overrides", () => {
370+
const input = {
371+
workspace_overrides: {
372+
conversation_initiation_client_data_webhook: {
373+
request_headers: {
374+
"x-elevenlabs-hoxhunt-token": {
375+
secret_id: "test-secret-123"
376+
}
377+
}
378+
}
379+
}
380+
};
381+
382+
const result = toCamelCaseKeys(input);
383+
384+
expect(result).toEqual({
385+
workspaceOverrides: {
386+
conversationInitiationClientDataWebhook: {
387+
requestHeaders: {
388+
"x-elevenlabs-hoxhunt-token": { // Header name preserved
389+
secretId: "test-secret-123" // Nested key converted to camelCase
390+
}
391+
}
392+
}
393+
}
394+
});
395+
});
396+
397+
it("should preserve header names but convert secretId to snake_case when converting from API", () => {
398+
// Simulating API response (camelCase)
399+
const input = {
400+
workspaceOverrides: {
401+
conversationInitiationClientDataWebhook: {
402+
requestHeaders: {
403+
"x-elevenlabs-hoxhunt-token": {
404+
secretId: "test-secret-123"
405+
}
406+
}
407+
}
408+
}
409+
};
410+
411+
const result = toSnakeCaseKeys(input);
412+
413+
expect(result).toEqual({
414+
workspace_overrides: {
415+
conversation_initiation_client_data_webhook: {
416+
request_headers: {
417+
"x-elevenlabs-hoxhunt-token": { // Header name preserved
418+
secret_id: "test-secret-123" // Nested key converted to snake_case
419+
}
420+
}
421+
}
422+
}
423+
});
424+
});
425+
426+
it("should preserve header names but convert nested object keys in requestHeaders for toSnakeCaseKeys", () => {
370427
const input = {
371428
conversationConfig: {
372429
agent: {
@@ -407,12 +464,12 @@ describe("Utils", () => {
407464
url: "https://example.com/webhook",
408465
method: "GET",
409466
request_headers: {
410-
"Content-Type": "application/json",
411-
"X-Api-Key": {
412-
secretId: "abc" // Should NOT be converted to secret_id
467+
"Content-Type": "application/json", // Header name preserved
468+
"X-Api-Key": { // Header name preserved
469+
secret_id: "abc" // BUT nested key IS converted
413470
},
414-
"Authorization": {
415-
variableName: "auth_token" // Should NOT be converted to variable_name
471+
"Authorization": { // Header name preserved
472+
variable_name: "auth_token" // BUT nested key IS converted
416473
}
417474
}
418475
}
@@ -424,7 +481,7 @@ describe("Utils", () => {
424481
});
425482
});
426483

427-
it("should maintain round-trip conversion symmetry for request_headers", () => {
484+
it("should maintain round-trip conversion symmetry for request_headers with proper key conversion", () => {
428485
const original = {
429486
conversation_config: {
430487
agent: {
@@ -438,7 +495,7 @@ describe("Utils", () => {
438495
request_headers: {
439496
"Content-Type": "application/json",
440497
"X-Api-Key": {
441-
secretId: "my_secret_123"
498+
secret_id: "my_secret_123"
442499
}
443500
}
444501
}
@@ -455,7 +512,7 @@ describe("Utils", () => {
455512
// Simulate push (local file → API): snake_case → camelCase
456513
const afterPush = toCamelCaseKeys(afterPull);
457514

458-
// After round-trip, request_headers internals should be preserved
515+
// After round-trip, header names preserved but nested keys converted
459516
expect(afterPush).toEqual({
460517
conversationConfig: {
461518
agent: {
@@ -467,9 +524,9 @@ describe("Utils", () => {
467524
url: "https://example.com/webhook",
468525
method: "POST",
469526
requestHeaders: {
470-
"Content-Type": "application/json",
471-
"X-Api-Key": {
472-
secretId: "my_secret_123" // Should be preserved through round-trip
527+
"Content-Type": "application/json", // Header name preserved
528+
"X-Api-Key": { // Header name preserved
529+
secretId: "my_secret_123" // Nested key converted through round-trip
473530
}
474531
}
475532
}

src/agents/ui/PullView.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,21 +216,28 @@ export const PullView: React.FC<PullViewProps> = ({
216216
conversation_config?: Record<string, unknown>;
217217
platformSettings?: Record<string, unknown>;
218218
platform_settings?: Record<string, unknown>;
219+
workflow?: unknown;
219220
tags?: string[];
220221
};
221222

222223
const conversationConfig = agentDetailsTyped.conversationConfig || agentDetailsTyped.conversation_config || {};
223224
const platformSettings = agentDetailsTyped.platformSettings || agentDetailsTyped.platform_settings || {};
225+
const workflow = agentDetailsTyped.workflow;
224226
const tags = agentDetailsTyped.tags || [];
225227

226228
// Create agent config structure (without agent_id - it goes in index file)
227-
const agentConfig = {
229+
const agentConfig: any = {
228230
name: agentName,
229231
conversation_config: conversationConfig,
230232
platform_settings: platformSettings,
231233
tags
232234
};
233235

236+
// Only include workflow if it exists
237+
if (workflow !== undefined && workflow !== null) {
238+
agentConfig.workflow = workflow;
239+
}
240+
234241
let configPath: string;
235242

236243
if (agent.action === 'update' && existingEntry && existingEntry.config) {

src/shared/utils.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,41 +118,45 @@ function toSnakeCaseKey(key: string): string {
118118
.toLowerCase();
119119
}
120120

121-
export function toCamelCaseKeys<T = unknown>(value: T, skipHeaderConversion = false): T {
121+
export function toCamelCaseKeys<T = unknown>(value: T, skipHeaderConversion: boolean | 'names-only' = false): T {
122122
if (Array.isArray(value)) {
123123
return (value.map((v) => toCamelCaseKeys(v, skipHeaderConversion)) as unknown) as T;
124124
}
125125
if (isPlainObject(value)) {
126126
const result: Record<string, unknown> = {};
127127
for (const [k, v] of Object.entries(value)) {
128-
if (skipHeaderConversion) {
129-
// Inside request_headers: preserve all keys as-is to avoid converting
130-
// header names like "X-Api-Key" or nested keys like "secret_id"
128+
if (skipHeaderConversion === true) {
129+
// Deep inside request_headers: preserve all keys (for backwards compatibility with arrays)
131130
result[k] = toCamelCaseKeys(v, true);
131+
} else if (skipHeaderConversion === 'names-only') {
132+
// Inside request_headers object: preserve header names (keys) but convert nested objects
133+
result[k] = toCamelCaseKeys(v, false);
132134
} else {
133135
// Normal conversion
134-
result[toCamelCaseKey(k)] = toCamelCaseKeys(v, k === 'request_headers');
136+
result[toCamelCaseKey(k)] = toCamelCaseKeys(v, k === 'request_headers' ? 'names-only' : false);
135137
}
136138
}
137139
return (result as unknown) as T;
138140
}
139141
return value;
140142
}
141143

142-
export function toSnakeCaseKeys<T = unknown>(value: T, skipHeaderConversion = false): T {
144+
export function toSnakeCaseKeys<T = unknown>(value: T, skipHeaderConversion: boolean | 'names-only' = false): T {
143145
if (Array.isArray(value)) {
144146
return (value.map((v) => toSnakeCaseKeys(v, skipHeaderConversion)) as unknown) as T;
145147
}
146148
if (isPlainObject(value)) {
147149
const result: Record<string, unknown> = {};
148150
for (const [k, v] of Object.entries(value)) {
149-
if (skipHeaderConversion) {
150-
// Inside request_headers/requestHeaders: preserve all keys as-is to avoid converting
151-
// header names like "X-Api-Key" or nested keys like "secret_id"
151+
if (skipHeaderConversion === true) {
152+
// Deep inside request_headers: preserve all keys (for backwards compatibility with arrays)
152153
result[k] = toSnakeCaseKeys(v, true);
154+
} else if (skipHeaderConversion === 'names-only') {
155+
// Inside request_headers object: preserve header names (keys) but convert nested objects
156+
result[k] = toSnakeCaseKeys(v, false);
153157
} else {
154158
// Normal conversion
155-
result[toSnakeCaseKey(k)] = toSnakeCaseKeys(v, k === 'request_headers' || k === 'requestHeaders');
159+
result[toSnakeCaseKey(k)] = toSnakeCaseKeys(v, k === 'request_headers' || k === 'requestHeaders' ? 'names-only' : false);
156160
}
157161
}
158162
return (result as unknown) as T;

0 commit comments

Comments
 (0)