Skip to main content
Glama
current.test.ts9.47 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { CurrentTools } from '../../src/tools/current.js'; import { IntervalsClient } from '../../src/clients/intervals.js'; import { WhoopClient } from '../../src/clients/whoop.js'; import { TrainerRoadClient } from '../../src/clients/trainerroad.js'; import type { NormalizedWorkout, RecoveryData, StrainData, PlannedWorkout, WorkoutWithWhoop, StrainActivity } from '../../src/types/index.js'; // Mock the clients vi.mock('../../src/clients/intervals.js'); vi.mock('../../src/clients/whoop.js'); vi.mock('../../src/clients/trainerroad.js'); describe('CurrentTools', () => { let tools: CurrentTools; let mockIntervalsClient: IntervalsClient; let mockWhoopClient: WhoopClient; let mockTrainerRoadClient: TrainerRoadClient; beforeEach(() => { vi.clearAllMocks(); mockIntervalsClient = new IntervalsClient({ apiKey: 'test', athleteId: 'test' }); mockWhoopClient = new WhoopClient({ accessToken: 'test', refreshToken: 'test', clientId: 'test', clientSecret: 'test', }); mockTrainerRoadClient = new TrainerRoadClient({ calendarUrl: 'https://test.com' }); tools = new CurrentTools(mockIntervalsClient, mockWhoopClient, mockTrainerRoadClient); }); describe('getTodaysRecovery', () => { it('should return recovery data from Whoop', async () => { const mockRecovery: RecoveryData = { date: '2024-12-15', recovery_score: 85, hrv_rmssd: 65, resting_heart_rate: 52, sleep_performance_percentage: 90, sleep_duration_hours: 7.5, }; vi.mocked(mockWhoopClient.getTodayRecovery).mockResolvedValue(mockRecovery); const result = await tools.getTodaysRecovery(); expect(result).toEqual(mockRecovery); expect(mockWhoopClient.getTodayRecovery).toHaveBeenCalled(); }); it('should return null when Whoop client is not configured', async () => { const toolsWithoutWhoop = new CurrentTools(mockIntervalsClient, null, mockTrainerRoadClient); const result = await toolsWithoutWhoop.getTodaysRecovery(); expect(result).toBeNull(); }); it('should propagate errors from Whoop client', async () => { vi.mocked(mockWhoopClient.getTodayRecovery).mockRejectedValue(new Error('API Error')); await expect(tools.getTodaysRecovery()).rejects.toThrow('API Error'); }); }); describe('getRecentWorkouts', () => { const mockWorkouts: NormalizedWorkout[] = [ { id: '1', date: '2024-12-15T10:00:00Z', activity_type: 'Cycling', duration_seconds: 3600, distance_km: 45, tss: 85, source: 'intervals.icu', }, { id: '2', date: '2024-12-14T08:00:00Z', activity_type: 'Running', duration_seconds: 2400, distance_km: 8, tss: 45, source: 'intervals.icu', }, ]; it('should return workouts for specified days', async () => { vi.mocked(mockIntervalsClient.getActivities).mockResolvedValue(mockWorkouts); vi.mocked(mockWhoopClient.getWorkouts).mockResolvedValue([]); const result = await tools.getRecentWorkouts({ days: 7 }); expect(result).toHaveLength(2); expect(result[0].id).toBe('1'); expect(result[0].whoop).toBeNull(); // No matching Whoop activity expect(mockIntervalsClient.getActivities).toHaveBeenCalled(); }); it('should pass sport filter to client', async () => { vi.mocked(mockIntervalsClient.getActivities).mockResolvedValue([mockWorkouts[0]]); vi.mocked(mockWhoopClient.getWorkouts).mockResolvedValue([]); const result = await tools.getRecentWorkouts({ days: 7, sport: 'cycling' }); expect(result).toHaveLength(1); expect(mockIntervalsClient.getActivities).toHaveBeenCalledWith( expect.any(String), expect.any(String), 'cycling' ); }); it('should use default days when not specified', async () => { vi.mocked(mockIntervalsClient.getActivities).mockResolvedValue(mockWorkouts); vi.mocked(mockWhoopClient.getWorkouts).mockResolvedValue([]); await tools.getRecentWorkouts({ days: 7 }); // Verify the date range is approximately 7 days const [start, end] = vi.mocked(mockIntervalsClient.getActivities).mock.calls[0]; const startDate = new Date(start); const endDate = new Date(end); const daysDiff = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24); expect(daysDiff).toBeCloseTo(7, 0); }); it('should match and include Whoop data when activity matches', async () => { const whoopActivities: StrainActivity[] = [ { id: 'whoop-1', activity_type: 'Cycling', start_time: '2024-12-15T10:00:00Z', // Same time as workout 1 end_time: '2024-12-15T11:00:00Z', strain_score: 14.5, average_heart_rate: 145, max_heart_rate: 175, calories: 650, }, ]; vi.mocked(mockIntervalsClient.getActivities).mockResolvedValue(mockWorkouts); vi.mocked(mockWhoopClient.getWorkouts).mockResolvedValue(whoopActivities); const result = await tools.getRecentWorkouts({ days: 7 }); expect(result[0].whoop).not.toBeNull(); expect(result[0].whoop?.strain_score).toBe(14.5); expect(result[0].whoop?.match_confidence).toBe('high'); expect(result[1].whoop).toBeNull(); // No matching Whoop activity for running }); it('should return workouts without Whoop data when Whoop is not configured', async () => { const toolsWithoutWhoop = new CurrentTools(mockIntervalsClient, null, mockTrainerRoadClient); vi.mocked(mockIntervalsClient.getActivities).mockResolvedValue(mockWorkouts); const result = await toolsWithoutWhoop.getRecentWorkouts({ days: 7 }); expect(result).toHaveLength(2); expect(result[0].whoop).toBeNull(); expect(result[1].whoop).toBeNull(); }); }); describe('getRecentStrain', () => { const mockStrain: StrainData[] = [ { date: '2024-12-15', strain_score: 15.5, average_heart_rate: 75, max_heart_rate: 185, calories: 2500, activities: [], }, ]; it('should return strain data from Whoop', async () => { vi.mocked(mockWhoopClient.getStrainData).mockResolvedValue(mockStrain); const result = await tools.getRecentStrain({ days: 7 }); expect(result).toEqual(mockStrain); }); it('should return empty array when Whoop client is not configured', async () => { const toolsWithoutWhoop = new CurrentTools(mockIntervalsClient, null, mockTrainerRoadClient); const result = await toolsWithoutWhoop.getRecentStrain({ days: 7 }); expect(result).toEqual([]); }); }); describe('getTodaysPlannedWorkouts', () => { const trainerroadWorkouts: PlannedWorkout[] = [ { id: 'tr-1', date: '2024-12-15T09:00:00Z', name: 'Sweet Spot Base', expected_tss: 88, source: 'trainerroad', }, ]; const intervalsWorkouts: PlannedWorkout[] = [ { id: 'int-1', date: '2024-12-15T17:00:00Z', name: 'Easy Run', expected_tss: 35, source: 'intervals.icu', }, ]; it('should return workouts from both sources', async () => { vi.mocked(mockTrainerRoadClient.getTodayWorkouts).mockResolvedValue(trainerroadWorkouts); vi.mocked(mockIntervalsClient.getPlannedEvents).mockResolvedValue(intervalsWorkouts); const result = await tools.getTodaysPlannedWorkouts(); expect(result.trainerroad).toEqual(trainerroadWorkouts); expect(result.intervals).toEqual(intervalsWorkouts); expect(result.merged).toHaveLength(2); }); it('should deduplicate similar workouts', async () => { const duplicateWorkout: PlannedWorkout = { id: 'int-1', date: '2024-12-15T09:00:00Z', name: 'Sweet Spot Base', // Same name expected_tss: 88, // Same TSS source: 'intervals.icu', }; vi.mocked(mockTrainerRoadClient.getTodayWorkouts).mockResolvedValue(trainerroadWorkouts); vi.mocked(mockIntervalsClient.getPlannedEvents).mockResolvedValue([duplicateWorkout]); const result = await tools.getTodaysPlannedWorkouts(); // Should only have TrainerRoad version (preferred) expect(result.merged).toHaveLength(1); expect(result.merged[0].source).toBe('trainerroad'); }); it('should handle TrainerRoad client not configured', async () => { const toolsWithoutTr = new CurrentTools(mockIntervalsClient, mockWhoopClient, null); vi.mocked(mockIntervalsClient.getPlannedEvents).mockResolvedValue(intervalsWorkouts); const result = await toolsWithoutTr.getTodaysPlannedWorkouts(); expect(result.trainerroad).toEqual([]); expect(result.intervals).toEqual(intervalsWorkouts); expect(result.merged).toHaveLength(1); }); it('should handle errors gracefully', async () => { vi.mocked(mockTrainerRoadClient.getTodayWorkouts).mockRejectedValue(new Error('Failed')); vi.mocked(mockIntervalsClient.getPlannedEvents).mockResolvedValue(intervalsWorkouts); const result = await tools.getTodaysPlannedWorkouts(); expect(result.trainerroad).toEqual([]); expect(result.intervals).toEqual(intervalsWorkouts); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gesteves/domestique'

If you have feedback or need assistance with the MCP directory API, please join our Discord server