Add mobile services, web proxy, and Gemma4 agent loop
- Mobile: add push notification and calendar sync services with full test suite (calendar, eventStore, notifications, screens) - Mobile: add EAS build config and Jest setup - Web: add API proxy, update useDestinationStation/useJourneys hooks, add middleware tests - Web: update next.config and rebuild - Agent: add Gemma4-based agent loop (agent_base, ttl_agent, ts_agent) - Docs: add privacy policy, post-MVP plan, and aider rules
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import * as Calendar from 'expo-calendar';
|
||||
import { ensureCalendarPermission, fetchNativeEvents } from '../services/calendar';
|
||||
|
||||
// Mock expo-calendar
|
||||
jest.mock('expo-calendar', () => ({
|
||||
requestCalendarPermissionsAsync: jest.fn(),
|
||||
isAvailableAsync: jest.fn(),
|
||||
getCalendarsAsync: jest.fn(),
|
||||
getEventsAsync: jest.fn(),
|
||||
EntityTypes: {
|
||||
EVENTS: 'EVENTS',
|
||||
},
|
||||
}));
|
||||
|
||||
const mockCalendar = Calendar as jest.Mocked<typeof Calendar>;
|
||||
|
||||
describe('calendar service', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('ensureCalendarPermission', () => {
|
||||
it('returns true when permission granted and calendar available', async () => {
|
||||
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
||||
mockCalendar.isAvailableAsync.mockResolvedValue(true);
|
||||
|
||||
const result = await ensureCalendarPermission();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when permission denied', async () => {
|
||||
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'denied' as Calendar.PermissionStatus, granted: false, expires: 'never' as const, canAskAgain: true });
|
||||
|
||||
const result = await ensureCalendarPermission();
|
||||
expect(result).toBe(false);
|
||||
expect(mockCalendar.isAvailableAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns false when calendar not available', async () => {
|
||||
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
||||
mockCalendar.isAvailableAsync.mockResolvedValue(false);
|
||||
|
||||
const result = await ensureCalendarPermission();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchNativeEvents', () => {
|
||||
it('returns empty array when no permission', async () => {
|
||||
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'denied' as Calendar.PermissionStatus, granted: false, expires: 'never' as const, canAskAgain: true });
|
||||
|
||||
const result = await fetchNativeEvents(new Date(), new Date());
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when no calendars', async () => {
|
||||
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
||||
mockCalendar.isAvailableAsync.mockResolvedValue(true);
|
||||
mockCalendar.getCalendarsAsync.mockResolvedValue([]);
|
||||
|
||||
const result = await fetchNativeEvents(new Date(), new Date());
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns mapped events from native calendar', async () => {
|
||||
const startDate = new Date('2025-01-01');
|
||||
const endDate = new Date('2025-01-31');
|
||||
|
||||
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
||||
mockCalendar.isAvailableAsync.mockResolvedValue(true);
|
||||
mockCalendar.getCalendarsAsync.mockResolvedValue([
|
||||
{ id: 'cal1', title: 'Work' },
|
||||
{ id: 'cal2', title: 'Personal' },
|
||||
] as Calendar.Calendar[]);
|
||||
mockCalendar.getEventsAsync.mockResolvedValue([
|
||||
{
|
||||
id: 'evt1',
|
||||
calendarId: 'cal1',
|
||||
title: 'Team Meeting',
|
||||
location: 'Berlin',
|
||||
startDate: new Date('2025-01-15T10:00:00'),
|
||||
},
|
||||
{
|
||||
id: 'evt2',
|
||||
calendarId: 'cal2',
|
||||
title: 'Dentist',
|
||||
location: null,
|
||||
startDate: new Date('2025-01-20T14:00:00'),
|
||||
},
|
||||
] as Calendar.Event[]);
|
||||
|
||||
const result = await fetchNativeEvents(startDate, endDate);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'evt1',
|
||||
title: 'Team Meeting',
|
||||
destination: 'Berlin',
|
||||
eventTime: new Date('2025-01-15T10:00:00'),
|
||||
source: 'native:cal1',
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
id: 'evt2',
|
||||
title: 'Dentist',
|
||||
destination: '',
|
||||
eventTime: new Date('2025-01-20T14:00:00'),
|
||||
source: 'native:cal2',
|
||||
});
|
||||
|
||||
expect(mockCalendar.getEventsAsync).toHaveBeenCalledWith(
|
||||
['cal1', 'cal2'],
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
});
|
||||
|
||||
it('handles events with missing title or startDate', async () => {
|
||||
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
||||
mockCalendar.isAvailableAsync.mockResolvedValue(true);
|
||||
mockCalendar.getCalendarsAsync.mockResolvedValue([{ id: 'cal1', title: 'Default' }] as Calendar.Calendar[]);
|
||||
mockCalendar.getEventsAsync.mockResolvedValue([
|
||||
{
|
||||
id: 'evt1',
|
||||
calendarId: 'cal1',
|
||||
title: null as unknown as string,
|
||||
location: null,
|
||||
startDate: null as unknown as string | Date,
|
||||
},
|
||||
] as Calendar.Event[]);
|
||||
|
||||
const result = await fetchNativeEvents(new Date(), new Date());
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].title).toBe('Untitled Event');
|
||||
expect(result[0].destination).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user