Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 74 additions & 17 deletions src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ describe("Utils", () => {
});
});

it("should preserve secret_id and other keys in request_headers object format", () => {
it("should preserve header names but convert nested object keys in request_headers", () => {
const input = {
conversation_config: {
agent: {
Expand Down Expand Up @@ -333,11 +333,11 @@ describe("Utils", () => {
url: "https://example.com/webhook",
method: "GET",
requestHeaders: {
"Content-Type": "application/json",
"X-Api-Key": {
secret_id: "abc" // Should NOT be converted to secretId
"Content-Type": "application/json", // Header name preserved
"X-Api-Key": { // Header name preserved
secretId: "abc" // BUT nested key IS converted
},
"foo_bar": "baz" // Should NOT be converted to fooBar
"foo_bar": "baz" // Header name preserved (string value, no nested conversion)
}
}
}
Expand Down Expand Up @@ -366,7 +366,64 @@ describe("Utils", () => {
});
});

it("should preserve request_headers in toSnakeCaseKeys for symmetry", () => {
it("should preserve header names but convert secretId to snake_case in workspace_overrides", () => {
const input = {
workspace_overrides: {
conversation_initiation_client_data_webhook: {
request_headers: {
"x-elevenlabs-hoxhunt-token": {
secret_id: "test-secret-123"
}
}
}
}
};

const result = toCamelCaseKeys(input);

expect(result).toEqual({
workspaceOverrides: {
conversationInitiationClientDataWebhook: {
requestHeaders: {
"x-elevenlabs-hoxhunt-token": { // Header name preserved
secretId: "test-secret-123" // Nested key converted to camelCase
}
}
}
}
});
});

it("should preserve header names but convert secretId to snake_case when converting from API", () => {
// Simulating API response (camelCase)
const input = {
workspaceOverrides: {
conversationInitiationClientDataWebhook: {
requestHeaders: {
"x-elevenlabs-hoxhunt-token": {
secretId: "test-secret-123"
}
}
}
}
};

const result = toSnakeCaseKeys(input);

expect(result).toEqual({
workspace_overrides: {
conversation_initiation_client_data_webhook: {
request_headers: {
"x-elevenlabs-hoxhunt-token": { // Header name preserved
secret_id: "test-secret-123" // Nested key converted to snake_case
}
}
}
}
});
});

it("should preserve header names but convert nested object keys in requestHeaders for toSnakeCaseKeys", () => {
const input = {
conversationConfig: {
agent: {
Expand Down Expand Up @@ -407,12 +464,12 @@ describe("Utils", () => {
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
"Content-Type": "application/json", // Header name preserved
"X-Api-Key": { // Header name preserved
secret_id: "abc" // BUT nested key IS converted
},
"Authorization": {
variableName: "auth_token" // Should NOT be converted to variable_name
"Authorization": { // Header name preserved
variable_name: "auth_token" // BUT nested key IS converted
}
}
}
Expand All @@ -424,7 +481,7 @@ describe("Utils", () => {
});
});

it("should maintain round-trip conversion symmetry for request_headers", () => {
it("should maintain round-trip conversion symmetry for request_headers with proper key conversion", () => {
const original = {
conversation_config: {
agent: {
Expand All @@ -438,7 +495,7 @@ describe("Utils", () => {
request_headers: {
"Content-Type": "application/json",
"X-Api-Key": {
secretId: "my_secret_123"
secret_id: "my_secret_123"
}
}
}
Expand All @@ -455,7 +512,7 @@ describe("Utils", () => {
// Simulate push (local file → API): snake_case → camelCase
const afterPush = toCamelCaseKeys(afterPull);

// After round-trip, request_headers internals should be preserved
// After round-trip, header names preserved but nested keys converted
expect(afterPush).toEqual({
conversationConfig: {
agent: {
Expand All @@ -467,9 +524,9 @@ describe("Utils", () => {
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
"Content-Type": "application/json", // Header name preserved
"X-Api-Key": { // Header name preserved
secretId: "my_secret_123" // Nested key converted through round-trip
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/agents/ui/PullView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,21 +216,28 @@ export const PullView: React.FC<PullViewProps> = ({
conversation_config?: Record<string, unknown>;
platformSettings?: Record<string, unknown>;
platform_settings?: Record<string, unknown>;
workflow?: unknown;
tags?: string[];
};

const conversationConfig = agentDetailsTyped.conversationConfig || agentDetailsTyped.conversation_config || {};
const platformSettings = agentDetailsTyped.platformSettings || agentDetailsTyped.platform_settings || {};
const workflow = agentDetailsTyped.workflow;
const tags = agentDetailsTyped.tags || [];

// Create agent config structure (without agent_id - it goes in index file)
const agentConfig = {
const agentConfig: any = {
name: agentName,
conversation_config: conversationConfig,
platform_settings: platformSettings,
tags
};

// Only include workflow if it exists
if (workflow !== undefined && workflow !== null) {
agentConfig.workflow = workflow;
}

let configPath: string;

if (agent.action === 'update' && existingEntry && existingEntry.config) {
Expand Down
24 changes: 14 additions & 10 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,41 +118,45 @@ function toSnakeCaseKey(key: string): string {
.toLowerCase();
}

export function toCamelCaseKeys<T = unknown>(value: T, skipHeaderConversion = false): T {
export function toCamelCaseKeys<T = unknown>(value: T, skipHeaderConversion: boolean | 'names-only' = false): T {
if (Array.isArray(value)) {
return (value.map((v) => toCamelCaseKeys(v, skipHeaderConversion)) as unknown) as T;
}
if (isPlainObject(value)) {
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value)) {
if (skipHeaderConversion) {
// Inside request_headers: preserve all keys as-is to avoid converting
// header names like "X-Api-Key" or nested keys like "secret_id"
if (skipHeaderConversion === true) {
// Deep inside request_headers: preserve all keys (for backwards compatibility with arrays)
result[k] = toCamelCaseKeys(v, true);
} else if (skipHeaderConversion === 'names-only') {
// Inside request_headers object: preserve header names (keys) but convert nested objects
result[k] = toCamelCaseKeys(v, false);
} else {
// Normal conversion
result[toCamelCaseKey(k)] = toCamelCaseKeys(v, k === 'request_headers');
result[toCamelCaseKey(k)] = toCamelCaseKeys(v, k === 'request_headers' ? 'names-only' : false);
}
}
return (result as unknown) as T;
}
return value;
}

export function toSnakeCaseKeys<T = unknown>(value: T, skipHeaderConversion = false): T {
export function toSnakeCaseKeys<T = unknown>(value: T, skipHeaderConversion: boolean | 'names-only' = false): T {
if (Array.isArray(value)) {
return (value.map((v) => toSnakeCaseKeys(v, skipHeaderConversion)) as unknown) as T;
}
if (isPlainObject(value)) {
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value)) {
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"
if (skipHeaderConversion === true) {
// Deep inside request_headers: preserve all keys (for backwards compatibility with arrays)
result[k] = toSnakeCaseKeys(v, true);
} else if (skipHeaderConversion === 'names-only') {
// Inside request_headers object: preserve header names (keys) but convert nested objects
result[k] = toSnakeCaseKeys(v, false);
} else {
// Normal conversion
result[toSnakeCaseKey(k)] = toSnakeCaseKeys(v, k === 'request_headers' || k === 'requestHeaders');
result[toSnakeCaseKey(k)] = toSnakeCaseKeys(v, k === 'request_headers' || k === 'requestHeaders' ? 'names-only' : false);
}
}
return (result as unknown) as T;
Expand Down
Loading