diff --git a/src/components/panels/agent-detail-tabs.tsx b/src/components/panels/agent-detail-tabs.tsx index 7e9b106..1ae2f4f 100644 --- a/src/components/panels/agent-detail-tabs.tsx +++ b/src/components/panels/agent-detail-tabs.tsx @@ -1443,7 +1443,7 @@ export function ConfigTab({ const updateModelConfig = (updater: (current: { primary?: string; fallbacks?: string[] }) => { primary?: string; fallbacks?: string[] }) => { setConfig((prev: any) => { const nextModel = updater({ ...(prev?.model || {}) }) - const dedupedFallbacks = [...new Set((nextModel.fallbacks || []).map((value) => value.trim()).filter(Boolean))] + const dedupedFallbacks = [...new Set((nextModel.fallbacks || []).map((value) => (value || '').trim()).filter(Boolean))] return { ...prev, model: { @@ -2788,8 +2788,8 @@ export function ModelsTab({ agent }: { agent: Agent }) { body: JSON.stringify({ gateway_config: { model: { - primary: primary.trim(), - fallbacks: fallbacks.filter(f => f.trim()), + primary: (primary || '').trim(), + fallbacks: fallbacks.filter(f => f && f.trim()), }, }, write_to_gateway: true, diff --git a/src/lib/__tests__/agent-model-trim.test.ts b/src/lib/__tests__/agent-model-trim.test.ts new file mode 100644 index 0000000..08b57b7 --- /dev/null +++ b/src/lib/__tests__/agent-model-trim.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest' + +/** + * Tests for the null-safe trim patterns used in agent model config editing. + * These mirror the logic in agent-detail-tabs.tsx (updateModelConfig + handleSave). + */ + +function dedupFallbacks(fallbacks: (string | undefined | null)[]): string[] { + return [...new Set((fallbacks || []).map((value) => (value || '').trim()).filter(Boolean))] +} + +function safePrimary(primary: string | undefined | null): string { + return (primary || '').trim() +} + +function filterFallbacks(fallbacks: (string | undefined | null)[]): string[] { + return fallbacks.filter(f => f && f.trim()) as string[] +} + +describe('agent model config trim safety', () => { + describe('dedupFallbacks (updateModelConfig pattern)', () => { + it('handles normal string values', () => { + expect(dedupFallbacks(['gpt-4', 'claude-3'])).toEqual(['gpt-4', 'claude-3']) + }) + + it('handles undefined values without throwing', () => { + expect(dedupFallbacks([undefined, 'gpt-4', undefined])).toEqual(['gpt-4']) + }) + + it('handles null values without throwing', () => { + expect(dedupFallbacks([null, 'gpt-4'])).toEqual(['gpt-4']) + }) + + it('filters out empty strings', () => { + expect(dedupFallbacks(['', ' ', 'gpt-4'])).toEqual(['gpt-4']) + }) + + it('deduplicates models', () => { + expect(dedupFallbacks(['gpt-4', 'gpt-4', 'claude-3'])).toEqual(['gpt-4', 'claude-3']) + }) + + it('handles empty array', () => { + expect(dedupFallbacks([])).toEqual([]) + }) + }) + + describe('safePrimary (handleSave pattern)', () => { + it('trims normal string', () => { + expect(safePrimary(' gpt-4 ')).toBe('gpt-4') + }) + + it('handles undefined without throwing', () => { + expect(safePrimary(undefined)).toBe('') + }) + + it('handles null without throwing', () => { + expect(safePrimary(null)).toBe('') + }) + + it('handles empty string', () => { + expect(safePrimary('')).toBe('') + }) + }) + + describe('filterFallbacks (handleSave pattern)', () => { + it('filters valid values', () => { + expect(filterFallbacks(['gpt-4', 'claude-3'])).toEqual(['gpt-4', 'claude-3']) + }) + + it('filters out undefined without throwing', () => { + expect(filterFallbacks([undefined, 'gpt-4'])).toEqual(['gpt-4']) + }) + + it('filters out null without throwing', () => { + expect(filterFallbacks([null, 'gpt-4'])).toEqual(['gpt-4']) + }) + + it('filters out empty strings', () => { + expect(filterFallbacks(['', 'gpt-4'])).toEqual(['gpt-4']) + }) + + it('filters out whitespace-only strings', () => { + expect(filterFallbacks([' ', 'gpt-4'])).toEqual(['gpt-4']) + }) + }) +}) diff --git a/tests/agent-model-config.spec.ts b/tests/agent-model-config.spec.ts new file mode 100644 index 0000000..ccb25d3 --- /dev/null +++ b/tests/agent-model-config.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test' +import { API_KEY_HEADER, createTestAgent, deleteTestAgent } from './helpers' + +/** + * E2E tests for agent model configuration updates. + * Verifies the API handles edge cases like empty/null model values. + */ + +test.describe('Agent Model Config', () => { + const cleanup: number[] = [] + + test.afterEach(async ({ request }) => { + for (const id of cleanup) { + await deleteTestAgent(request, id).catch(() => {}) + } + cleanup.length = 0 + }) + + test('PUT with valid model config succeeds', async ({ request }) => { + const { id } = await createTestAgent(request) + cleanup.push(id) + + const res = await request.put(`/api/agents/${id}`, { + headers: API_KEY_HEADER, + data: { + gateway_config: { + model: { + primary: 'claude-3-5-sonnet-20241022', + fallbacks: ['gpt-4o'], + }, + }, + }, + }) + + expect(res.status()).toBe(200) + }) + + test('PUT with empty fallbacks array succeeds', async ({ request }) => { + const { id } = await createTestAgent(request) + cleanup.push(id) + + const res = await request.put(`/api/agents/${id}`, { + headers: API_KEY_HEADER, + data: { + gateway_config: { + model: { + primary: 'claude-3-5-sonnet-20241022', + fallbacks: [], + }, + }, + }, + }) + + expect(res.status()).toBe(200) + }) + + test('PUT with empty string primary returns appropriate response', async ({ request }) => { + const { id } = await createTestAgent(request) + cleanup.push(id) + + const res = await request.put(`/api/agents/${id}`, { + headers: API_KEY_HEADER, + data: { + gateway_config: { + model: { + primary: '', + fallbacks: [], + }, + }, + }, + }) + + // Server should accept the update (model config is optional gateway config) + expect([200, 400]).toContain(res.status()) + }) + + test('PUT with whitespace-only fallbacks filters them', async ({ request }) => { + const { id } = await createTestAgent(request) + cleanup.push(id) + + const res = await request.put(`/api/agents/${id}`, { + headers: API_KEY_HEADER, + data: { + gateway_config: { + model: { + primary: 'claude-3-5-sonnet-20241022', + fallbacks: [' ', '', 'gpt-4o'], + }, + }, + }, + }) + + expect(res.status()).toBe(200) + }) +})