LibreChat/api/app/clients/tools/structured/specs/openweather.test.js
Danny Avila 0855677a36
🌤️ feat: Add OpenWeather Tool for Weather Data Retrieval (#5246)
*  feat: Add OpenWeather Tool for Weather Data Retrieval 🌤️

* chore: linting

* chore: move test files

* fix: tool icon, allow user-provided keys, conform to app key assignment pattern

* chore: linting not included in #5212

---------

Co-authored-by: Jonathan Addington <jonathan.addington@jmaddington.com>
2025-01-10 08:54:08 -05:00

358 lines
9.9 KiB
JavaScript

// __tests__/openweather.test.js
const OpenWeather = require('../OpenWeather');
const fetch = require('node-fetch');
// Mock environment variable
process.env.OPENWEATHER_API_KEY = 'test-api-key';
// Mock the fetch function globally
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 () => {
// Mock geocoding response
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');
});
// Mock forecast response
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 () => {
// Mock geocoding response
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');
});
// Mock historical weather response
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 () => {
// Mock geocoding response
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');
});
// Mock daily aggregation response
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 () => {
// Mock geocoding response
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');
});
// Mock overview response
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 () => {
// Mock geocoding response for all three calls
const geocodingMock = Promise.resolve({
ok: true,
json: async () => [{ lat: 35.9606, lon: -83.9207 }],
});
// Mock weather response for Kelvin
const kelvinMock = Promise.resolve({
ok: true,
json: async () => ({
current: { temp: 293.15 },
}),
});
// Mock weather response for Celsius
const celsiusMock = Promise.resolve({
ok: true,
json: async () => ({
current: { temp: 20 },
}),
});
// Mock weather response for Fahrenheit
const fahrenheitMock = Promise.resolve({
ok: true,
json: async () => ({
current: { temp: 68 },
}),
});
// Test Kelvin
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);
// Test Celsius
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);
// Test Fahrenheit
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 () => {
// Mock geocoding success
fetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
json: async () => [{ lat: 35.9606, lon: -83.9207 }],
}),
);
// Mock weather request failure
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 () => {
// Mock geocoding response first
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');
});
// Mock timestamp API response
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', // Wrong format
});
expect(result).toMatch(/Error: Invalid date format. Expected YYYY-MM-DD./);
});
});