diff --git a/packages/hdx-eval/src/__tests__/api.test.ts b/packages/hdx-eval/src/__tests__/api.test.ts new file mode 100644 index 0000000000..573659f5ad --- /dev/null +++ b/packages/hdx-eval/src/__tests__/api.test.ts @@ -0,0 +1,105 @@ +import { HyperdxApiClient } from '@/hyperdx/api'; + +const originalFetch = global.fetch; + +function mockMcpContent(text: string): void { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => + JSON.stringify({ + jsonrpc: '2.0', + result: { + content: [{ type: 'text', text }], + }, + }), + }) as unknown as typeof fetch; +} + +afterEach(() => { + global.fetch = originalFetch; +}); + +describe('HyperdxApiClient', () => { + it('reports invalid MCP content as a failed tile query', async () => { + mockMcpContent('not valid JSON'); + + const result = await new HyperdxApiClient( + 'http://localhost', + ).queryTileWithEvidence({ + accessKey: 'test-key', + dashboardId: 'dashboard-id', + tileId: 'tile-id', + startTime: '2026-01-01T00:00:00.000Z', + endTime: '2026-01-01T01:00:00.000Z', + }); + + expect(result.success).toBe(false); + expect(result.hasData).toBe(false); + expect(result.error).toMatch(/^Invalid MCP content:/); + }); + + it('treats empty MCP content as a successful query with no data', async () => { + mockMcpContent(' \n'); + + const result = await new HyperdxApiClient( + 'http://localhost', + ).queryTileWithEvidence({ + accessKey: 'test-key', + dashboardId: 'dashboard-id', + tileId: 'tile-id', + startTime: '2026-01-01T00:00:00.000Z', + endTime: '2026-01-01T01:00:00.000Z', + }); + + expect(result).toEqual({ success: true, hasData: false }); + }); + + it('extracts rows and groups from valid MCP content', async () => { + mockMcpContent( + JSON.stringify({ + result: [ + { ServiceName: 'api', value: 1 }, + { ServiceName: 'worker', value: 2 }, + ], + }), + ); + + const result = await new HyperdxApiClient( + 'http://localhost', + ).queryTileWithEvidence({ + accessKey: 'test-key', + dashboardId: 'dashboard-id', + tileId: 'tile-id', + startTime: '2026-01-01T00:00:00.000Z', + endTime: '2026-01-01T01:00:00.000Z', + }); + + expect(result).toEqual({ + success: true, + hasData: true, + rowCount: 2, + groupCount: 2, + sampleRows: [ + { ServiceName: 'api', value: 1 }, + { ServiceName: 'worker', value: 2 }, + ], + }); + }); + + it('treats valid MCP content without a result as no data', async () => { + mockMcpContent('{}'); + + const result = await new HyperdxApiClient( + 'http://localhost', + ).queryTileWithEvidence({ + accessKey: 'test-key', + dashboardId: 'dashboard-id', + tileId: 'tile-id', + startTime: '2026-01-01T00:00:00.000Z', + endTime: '2026-01-01T01:00:00.000Z', + }); + + expect(result).toEqual({ success: true, hasData: false }); + }); +}); diff --git a/packages/hdx-eval/src/hyperdx/api.ts b/packages/hdx-eval/src/hyperdx/api.ts index b83d148bfc..610c8eb831 100644 --- a/packages/hdx-eval/src/hyperdx/api.ts +++ b/packages/hdx-eval/src/hyperdx/api.ts @@ -325,6 +325,10 @@ export class HyperdxApiClient { }; } + if (!content.text.trim()) { + return { success: true, hasData: false }; + } + try { const inner = JSON.parse(content.text); const result = inner.result; @@ -368,8 +372,14 @@ export class HyperdxApiClient { groupCount, sampleRows: rows.slice(0, 5), }; - } catch { - return { success: true, hasData: content.text.length > 0 }; + } catch (error) { + return { + success: false, + hasData: false, + error: `Invalid MCP content: ${ + error instanceof Error ? error.message : String(error) + }`.slice(0, 300), + }; } } }