import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createReportsServer } from '../reports/reports.server'; import type { ReportsDeps } from '../shared/types'; type ToolResult = { content: { type: string; text: string }[]; isError?: boolean }; function makeDeps(): ReportsDeps { return { aiServiceBaseUrl: 'http://localhost:8000' }; } function getToolHandler(server: ReturnType, name: string) { const tools = (server as unknown as { _registeredTools: Record Promise }> })._registeredTools; const entry = tools[name]; if (!entry) throw new Error(`Tool "${name}" not found`); return entry.handler; } function parseResult(result: ToolResult) { return JSON.parse(result.content[0].text) as Record; } describe('ReportsServer', () => { let server: ReturnType; beforeEach(() => { server = createReportsServer(makeDeps()); vi.restoreAllMocks(); }); it('creates server with correct name', () => { expect(server).toBeDefined(); }); describe('generate_report', () => { it('generates a market overview report', async () => { const mockResponse = { report_id: 'rpt-001', report_type: 'market_overview', title: 'Báo cáo thị trường BĐS Quận 7', location: 'Quận 7, Hồ Chí Minh', generated_at: '2026-04-16T10:00:00Z', summary: 'Thị trường Q7 tiếp tục tăng trưởng ổn định...', sections: [ { title: 'Tổng quan', content: 'Quận 7 ghi nhận 1,200 giao dịch...', charts: [{ type: 'line', title: 'Giá trung bình', data: [] }], }, ], key_metrics: { avgPriceVND: 4_500_000_000, totalListings: 1200, avgPricePerM2: 55_000_000, }, }; vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true, json: async () => mockResponse, } as Response); const handler = getToolHandler(server, 'generate_report'); const result = await handler({ reportType: 'market_overview', location: 'Quận 7, Hồ Chí Minh', period: '1y', includeForecasts: false, includeMacro: false, language: 'vi', }); const data = parseResult(result); expect(data.reportId).toBe('rpt-001'); expect(data.reportType).toBe('market_overview'); expect(data.title).toContain('Quận 7'); expect(data.sections).toHaveLength(1); expect((data.keyMetrics as Record).totalListings).toBe(1200); }); it('generates report with forecasts', async () => { const mockResponse = { report_id: 'rpt-002', report_type: 'price_forecast', title: 'Price Forecast - District 2', location: 'Quận 2', generated_at: '2026-04-16T10:00:00Z', summary: 'Prices expected to increase 5-8% over next 12 months.', sections: [], key_metrics: {}, forecasts: { price_trend: [ { period: 'Q3 2026', predicted_change_pct: 2.5 }, { period: 'Q4 2026', predicted_change_pct: 3.1 }, ], demand_trend: [ { period: 'Q3 2026', predicted_change_pct: 1.8 }, ], confidence: 0.82, }, }; vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true, json: async () => mockResponse, } as Response); const handler = getToolHandler(server, 'generate_report'); const result = await handler({ reportType: 'price_forecast', location: 'Quận 2', includeForecasts: true, includeMacro: false, period: '1y', language: 'en', }); const data = parseResult(result); expect(data.forecasts).toBeDefined(); const forecasts = data.forecasts as Record; expect(forecasts.confidence).toBe(0.82); expect((forecasts.price_trend as unknown[]).length).toBe(2); }); it('sends correct request body to AI service', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true, json: async () => ({ report_id: 'rpt-003', report_type: 'industrial_zone', title: 'Test', location: 'Bình Dương', generated_at: '2026-04-16T10:00:00Z', summary: 'Test', }), } as Response); const handler = getToolHandler(server, 'generate_report'); await handler({ reportType: 'industrial_zone', location: 'Bình Dương', propertyType: 'industrial', period: '6m', includeForecasts: true, includeMacro: true, language: 'vi', }); expect(fetchSpy).toHaveBeenCalledWith( 'http://localhost:8000/reports/generate', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, }), ); const body = JSON.parse(fetchSpy.mock.calls[0][1]!.body as string) as Record; expect(body.report_type).toBe('industrial_zone'); expect(body.location).toBe('Bình Dương'); expect(body.property_type).toBe('industrial'); expect(body.period).toBe('6m'); expect(body.include_forecasts).toBe(true); expect(body.include_macro).toBe(true); expect(body.language).toBe('vi'); }); it('returns error on service failure', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: false, status: 503, text: async () => 'Service Unavailable', } as Response); const handler = getToolHandler(server, 'generate_report'); const result = await handler({ reportType: 'market_overview', location: 'Test', period: '1y', includeForecasts: false, includeMacro: false, language: 'vi', }); expect(result.isError).toBe(true); const data = parseResult(result); expect(data.error).toContain('Report generation error (503)'); }); }); describe('get_macro_data', () => { it('retrieves GDP and population data', async () => { const mockResponse = { province: 'Bình Dương', data: { gdp: [ { year: 2023, value: 18.5, unit: 'billion USD', yoy_change: 7.2 }, { year: 2024, value: 20.1, unit: 'billion USD', yoy_change: 8.6 }, ], population: [ { year: 2023, value: 2800000, unit: 'people', yoy_change: 3.1 }, { year: 2024, value: 2890000, unit: 'people', yoy_change: 3.2 }, ], }, highlights: [ 'GDP growth exceeds national average', 'Strong FDI inflow from electronics sector', ], }; vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true, json: async () => mockResponse, } as Response); const handler = getToolHandler(server, 'get_macro_data'); const result = await handler({ province: 'Bình Dương', categories: ['gdp', 'population'], fromYear: 2023, toYear: 2024, }); const data = parseResult(result); expect(data.province).toBe('Bình Dương'); const macroData = data.data as Record; expect(macroData.gdp).toHaveLength(2); expect(macroData.population).toHaveLength(2); expect((macroData.gdp[0] as Record).year).toBe(2023); expect((macroData.gdp[0] as Record).yoyChange).toBe(7.2); expect(data.highlights).toHaveLength(2); }); it('sends correct request body', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true, json: async () => ({ province: 'Hồ Chí Minh', data: {}, highlights: [] }), } as Response); const handler = getToolHandler(server, 'get_macro_data'); await handler({ province: 'Hồ Chí Minh', categories: ['fdi', 'infrastructure'], fromYear: 2020, toYear: 2025, }); const body = JSON.parse(fetchSpy.mock.calls[0][1]!.body as string) as Record; expect(body.province).toBe('Hồ Chí Minh'); expect(body.categories).toEqual(['fdi', 'infrastructure']); expect(body.from_year).toBe(2020); expect(body.to_year).toBe(2025); }); it('returns error on service failure', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: false, status: 404, text: async () => 'Province not found', } as Response); const handler = getToolHandler(server, 'get_macro_data'); const result = await handler({ province: 'Unknown', categories: ['gdp'], fromYear: 2020, toYear: 2025, }); expect(result.isError).toBe(true); const data = parseResult(result); expect(data.error).toContain('Macro data error (404)'); }); }); });