| |
| const OpenWeather = require('../OpenWeather'); |
| const fetch = require('node-fetch'); |
|
|
| |
| process.env.OPENWEATHER_API_KEY = 'test-api-key'; |
|
|
| |
| jest.mock('node-fetch', () => jest.fn()); |
|
|
| describe('OpenWeather Tool', () => { |
| let tool; |
|
|
| beforeAll(() => { |
| tool = new OpenWeather(); |
| }); |
|
|
| beforeEach(() => { |
| fetch.mockReset(); |
| }); |
|
|
| test('action=help returns help instructions', async () => { |
| const result = await tool.call({ |
| action: 'help', |
| }); |
|
|
| expect(typeof result).toBe('string'); |
| const parsed = JSON.parse(result); |
| expect(parsed.title).toBe('OpenWeather One Call API 3.0 Help'); |
| }); |
|
|
| test('current_forecast with a city and successful geocoding + forecast', async () => { |
| |
| fetch.mockImplementationOnce((url) => { |
| if (url.includes('geo/1.0/direct')) { |
| return Promise.resolve({ |
| ok: true, |
| json: async () => [{ lat: 35.9606, lon: -83.9207 }], |
| }); |
| } |
| return Promise.reject('Unexpected fetch call for geocoding'); |
| }); |
|
|
| |
| fetch.mockImplementationOnce(() => |
| Promise.resolve({ |
| ok: true, |
| json: async () => ({ |
| current: { temp: 293.15, feels_like: 295.15 }, |
| daily: [{ temp: { day: 293.15, night: 283.15 } }], |
| }), |
| }), |
| ); |
|
|
| const result = await tool.call({ |
| action: 'current_forecast', |
| city: 'Knoxville, Tennessee', |
| units: 'Kelvin', |
| }); |
|
|
| const parsed = JSON.parse(result); |
| expect(parsed.current.temp).toBe(293); |
| expect(parsed.current.feels_like).toBe(295); |
| expect(parsed.daily[0].temp.day).toBe(293); |
| expect(parsed.daily[0].temp.night).toBe(283); |
| }); |
|
|
| test('timestamp action with valid date returns mocked historical data', async () => { |
| |
| fetch.mockImplementationOnce((url) => { |
| if (url.includes('geo/1.0/direct')) { |
| return Promise.resolve({ |
| ok: true, |
| json: async () => [{ lat: 35.9606, lon: -83.9207 }], |
| }); |
| } |
| return Promise.reject('Unexpected fetch call for geocoding'); |
| }); |
|
|
| |
| fetch.mockImplementationOnce(() => |
| Promise.resolve({ |
| ok: true, |
| json: async () => ({ |
| data: [ |
| { |
| dt: 1583280000, |
| temp: 283.15, |
| feels_like: 280.15, |
| humidity: 75, |
| weather: [{ description: 'clear sky' }], |
| }, |
| ], |
| }), |
| }), |
| ); |
|
|
| const result = await tool.call({ |
| action: 'timestamp', |
| city: 'Knoxville, Tennessee', |
| date: '2020-03-04', |
| units: 'Kelvin', |
| }); |
|
|
| const parsed = JSON.parse(result); |
| expect(parsed.data[0].temp).toBe(283); |
| expect(parsed.data[0].feels_like).toBe(280); |
| }); |
|
|
| test('daily_aggregation action returns aggregated weather data', async () => { |
| |
| fetch.mockImplementationOnce((url) => { |
| if (url.includes('geo/1.0/direct')) { |
| return Promise.resolve({ |
| ok: true, |
| json: async () => [{ lat: 35.9606, lon: -83.9207 }], |
| }); |
| } |
| return Promise.reject('Unexpected fetch call for geocoding'); |
| }); |
|
|
| |
| fetch.mockImplementationOnce(() => |
| Promise.resolve({ |
| ok: true, |
| json: async () => ({ |
| date: '2020-03-04', |
| temperature: { |
| morning: 283.15, |
| afternoon: 293.15, |
| evening: 288.15, |
| }, |
| humidity: { |
| morning: 75, |
| afternoon: 60, |
| evening: 70, |
| }, |
| }), |
| }), |
| ); |
|
|
| const result = await tool.call({ |
| action: 'daily_aggregation', |
| city: 'Knoxville, Tennessee', |
| date: '2020-03-04', |
| units: 'Kelvin', |
| }); |
|
|
| const parsed = JSON.parse(result); |
| expect(parsed.temperature.morning).toBe(283); |
| expect(parsed.temperature.afternoon).toBe(293); |
| expect(parsed.temperature.evening).toBe(288); |
| }); |
|
|
| test('overview action returns weather summary', async () => { |
| |
| fetch.mockImplementationOnce((url) => { |
| if (url.includes('geo/1.0/direct')) { |
| return Promise.resolve({ |
| ok: true, |
| json: async () => [{ lat: 35.9606, lon: -83.9207 }], |
| }); |
| } |
| return Promise.reject('Unexpected fetch call for geocoding'); |
| }); |
|
|
| |
| fetch.mockImplementationOnce(() => |
| Promise.resolve({ |
| ok: true, |
| json: async () => ({ |
| date: '2024-01-07', |
| lat: 35.9606, |
| lon: -83.9207, |
| tz: '+00:00', |
| units: 'metric', |
| weather_overview: |
| 'Currently, the temperature is 2°C with a real feel of -2°C. The sky is clear with moderate wind.', |
| }), |
| }), |
| ); |
|
|
| const result = await tool.call({ |
| action: 'overview', |
| city: 'Knoxville, Tennessee', |
| units: 'Celsius', |
| }); |
|
|
| const parsed = JSON.parse(result); |
| expect(parsed).toHaveProperty('weather_overview'); |
| expect(typeof parsed.weather_overview).toBe('string'); |
| expect(parsed.weather_overview.length).toBeGreaterThan(0); |
| expect(parsed).toHaveProperty('date'); |
| expect(parsed).toHaveProperty('units'); |
| expect(parsed.units).toBe('metric'); |
| }); |
|
|
| test('temperature units are correctly converted', async () => { |
| |
| const geocodingMock = Promise.resolve({ |
| ok: true, |
| json: async () => [{ lat: 35.9606, lon: -83.9207 }], |
| }); |
|
|
| |
| const kelvinMock = Promise.resolve({ |
| ok: true, |
| json: async () => ({ |
| current: { temp: 293.15 }, |
| }), |
| }); |
|
|
| |
| const celsiusMock = Promise.resolve({ |
| ok: true, |
| json: async () => ({ |
| current: { temp: 20 }, |
| }), |
| }); |
|
|
| |
| const fahrenheitMock = Promise.resolve({ |
| ok: true, |
| json: async () => ({ |
| current: { temp: 68 }, |
| }), |
| }); |
|
|
| |
| fetch.mockImplementationOnce(() => geocodingMock).mockImplementationOnce(() => kelvinMock); |
|
|
| let result = await tool.call({ |
| action: 'current_forecast', |
| city: 'Knoxville, Tennessee', |
| units: 'Kelvin', |
| }); |
| let parsed = JSON.parse(result); |
| expect(parsed.current.temp).toBe(293); |
|
|
| |
| fetch.mockImplementationOnce(() => geocodingMock).mockImplementationOnce(() => celsiusMock); |
|
|
| result = await tool.call({ |
| action: 'current_forecast', |
| city: 'Knoxville, Tennessee', |
| units: 'Celsius', |
| }); |
| parsed = JSON.parse(result); |
| expect(parsed.current.temp).toBe(20); |
|
|
| |
| fetch.mockImplementationOnce(() => geocodingMock).mockImplementationOnce(() => fahrenheitMock); |
|
|
| result = await tool.call({ |
| action: 'current_forecast', |
| city: 'Knoxville, Tennessee', |
| units: 'Fahrenheit', |
| }); |
| parsed = JSON.parse(result); |
| expect(parsed.current.temp).toBe(68); |
| }); |
|
|
| test('timestamp action without a date returns an error message', async () => { |
| const result = await tool.call({ |
| action: 'timestamp', |
| lat: 35.9606, |
| lon: -83.9207, |
| }); |
| expect(result).toMatch( |
| /Error: For timestamp action, a 'date' in YYYY-MM-DD format is required./, |
| ); |
| }); |
|
|
| test('daily_aggregation action without a date returns an error message', async () => { |
| const result = await tool.call({ |
| action: 'daily_aggregation', |
| lat: 35.9606, |
| lon: -83.9207, |
| }); |
| expect(result).toMatch(/Error: date \(YYYY-MM-DD\) is required for daily_aggregation action./); |
| }); |
|
|
| test('unknown action returns an error due to schema validation', async () => { |
| await expect( |
| tool.call({ |
| action: 'unknown_action', |
| }), |
| ).rejects.toThrow(/Received tool input did not match expected schema/); |
| }); |
|
|
| test('geocoding failure returns a descriptive error', async () => { |
| fetch.mockImplementationOnce(() => |
| Promise.resolve({ |
| ok: true, |
| json: async () => [], |
| }), |
| ); |
|
|
| const result = await tool.call({ |
| action: 'current_forecast', |
| city: 'NowhereCity', |
| }); |
| expect(result).toMatch(/Error: Could not find coordinates for city: NowhereCity/); |
| }); |
|
|
| test('API request failure returns an error', async () => { |
| |
| fetch.mockImplementationOnce(() => |
| Promise.resolve({ |
| ok: true, |
| json: async () => [{ lat: 35.9606, lon: -83.9207 }], |
| }), |
| ); |
|
|
| |
| fetch.mockImplementationOnce(() => |
| Promise.resolve({ |
| ok: false, |
| status: 404, |
| json: async () => ({ message: 'Not found' }), |
| }), |
| ); |
|
|
| const result = await tool.call({ |
| action: 'current_forecast', |
| city: 'Knoxville, Tennessee', |
| }); |
| expect(result).toMatch(/Error: OpenWeather API request failed with status 404: Not found/); |
| }); |
|
|
| test('invalid date format returns an error', async () => { |
| |
| fetch.mockImplementationOnce((url) => { |
| if (url.includes('geo/1.0/direct')) { |
| return Promise.resolve({ |
| ok: true, |
| json: async () => [{ lat: 35.9606, lon: -83.9207 }], |
| }); |
| } |
| return Promise.reject('Unexpected fetch call for geocoding'); |
| }); |
|
|
| |
| fetch.mockImplementationOnce((url) => { |
| if (url.includes('onecall/timemachine')) { |
| throw new Error('Invalid date format. Expected YYYY-MM-DD.'); |
| } |
| return Promise.reject('Unexpected fetch call'); |
| }); |
|
|
| const result = await tool.call({ |
| action: 'timestamp', |
| city: 'Knoxville, Tennessee', |
| date: '03-04-2020', |
| }); |
| expect(result).toMatch(/Error: Invalid date format. Expected YYYY-MM-DD./); |
| }); |
| }); |
|
|