mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
* ✨ 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>
358 lines
9.9 KiB
JavaScript
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./);
|
|
});
|
|
});
|