import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { GtasksConfig } from '../../config/schema.js'; // Hoisted mocks so vi.mock factories can reference them const { mockTasklistsList, mockTasksList, mockExistsSync, mockReadFileSync } = vi.hoisted(() => ({ mockTasklistsList: vi.fn(), mockTasksList: vi.fn(), mockExistsSync: vi.fn(), mockReadFileSync: vi.fn(), })); vi.mock('googleapis', () => ({ google: { auth: { OAuth2: vi.fn().mockImplementation(() => ({ setCredentials: vi.fn(), })), }, tasks: vi.fn().mockReturnValue({ tasklists: { list: mockTasklistsList, }, tasks: { list: mockTasksList, }, }), }, })); vi.mock('fs', async () => { const actual = await vi.importActual('fs'); return { ...actual, existsSync: mockExistsSync, readFileSync: mockReadFileSync, }; }); import { createGtasksTools } from './gtasks.js'; // ── Test config ───────────────────────────────────────────────────────────── const testConfig: NonNullable = { enabled: true, credentials_file: '/tmp/test-creds.json', token_file: '/tmp/test-token.json', }; const fakeCredentials = { installed: { client_id: 'test-client-id', client_secret: 'test-client-secret', redirect_uris: ['http://localhost'], }, }; const fakeToken = { access_token: 'test-access-token', refresh_token: 'test-refresh-token', }; // ── Helpers ───────────────────────────────────────────────────────────────── function setupValidAuth() { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockImplementation((path: unknown) => { const p = String(path); if (p.includes('creds')) {return JSON.stringify(fakeCredentials);} if (p.includes('token')) {return JSON.stringify(fakeToken);} return ''; }); } // ═════════════════════════════════════════════════════════════════════════════ beforeEach(() => { vi.clearAllMocks(); }); describe('createGtasksTools', () => { it('returns 2 tools with correct names', () => { const tools = createGtasksTools(testConfig); expect(tools).toHaveLength(2); expect(tools.map(t => t.name)).toEqual(['tasks.lists', 'tasks.list']); }); it('tools have descriptions and input schemas', () => { const tools = createGtasksTools(testConfig); for (const tool of tools) { expect(tool.description).toBeTruthy(); expect(tool.inputSchema).toBeDefined(); expect(tool.inputSchema.type).toBe('object'); } }); }); describe('tasks.lists', () => { it('returns error when credentials file missing', async () => { mockExistsSync.mockReturnValue(false); const [listsTool] = createGtasksTools(testConfig); const result = await listsTool.execute({}); expect(result.success).toBe(false); expect(result.error).toContain('Credentials file not found'); }); it('returns error when token file missing', async () => { mockExistsSync.mockImplementation((path: unknown) => { return String(path).includes('creds'); }); mockReadFileSync.mockReturnValue(JSON.stringify(fakeCredentials)); const [listsTool] = createGtasksTools(testConfig); const result = await listsTool.execute({}); expect(result.success).toBe(false); expect(result.error).toContain('Token file not found'); }); it('lists task lists', async () => { setupValidAuth(); mockTasklistsList.mockResolvedValue({ data: { items: [ { id: 'list1', title: 'My Tasks', updated: '2026-02-10T10:00:00Z' }, { id: 'list2', title: 'Work', updated: '2026-02-09T15:00:00Z' }, ], }, }); const [listsTool] = createGtasksTools(testConfig); const result = await listsTool.execute({}); expect(result.success).toBe(true); expect(result.output).toContain('My Tasks'); expect(result.output).toContain('Work'); expect(result.output).toContain('list1'); expect(result.output).toContain('list2'); }); it('handles empty results', async () => { setupValidAuth(); mockTasklistsList.mockResolvedValue({ data: { items: [] } }); const [listsTool] = createGtasksTools(testConfig); const result = await listsTool.execute({}); expect(result.success).toBe(true); expect(result.output).toBe('No task lists found.'); }); it('respects maxResults parameter', async () => { setupValidAuth(); mockTasklistsList.mockResolvedValue({ data: { items: [] } }); const [listsTool] = createGtasksTools(testConfig); await listsTool.execute({ maxResults: 5 }); expect(mockTasklistsList).toHaveBeenCalledWith( expect.objectContaining({ maxResults: 5 }), ); }); it('handles API errors gracefully', async () => { setupValidAuth(); mockTasklistsList.mockRejectedValue(new Error('API quota exceeded')); const [listsTool] = createGtasksTools(testConfig); const result = await listsTool.execute({}); expect(result.success).toBe(false); expect(result.error).toContain('API quota exceeded'); }); }); describe('tasks.list', () => { it('lists tasks from default list', async () => { setupValidAuth(); mockTasksList.mockResolvedValue({ data: { items: [ { id: 'task1', title: 'Buy groceries', status: 'needsAction', due: '2026-02-11T00:00:00Z', notes: 'Milk, bread', parent: '' }, { id: 'task2', title: 'Call dentist', status: 'completed', due: '', notes: '', parent: '' }, ], }, }); const [, listTool] = createGtasksTools(testConfig); const result = await listTool.execute({}); expect(result.success).toBe(true); expect(result.output).toContain('[ ] Buy groceries'); expect(result.output).toContain('Due: 2026-02-11'); expect(result.output).toContain('Notes: Milk, bread'); expect(result.output).toContain('[x] Call dentist'); expect(mockTasksList).toHaveBeenCalledWith( expect.objectContaining({ tasklist: '@default', showCompleted: true, showHidden: false, }), ); }); it('uses specified taskListId', async () => { setupValidAuth(); mockTasksList.mockResolvedValue({ data: { items: [] } }); const [, listTool] = createGtasksTools(testConfig); await listTool.execute({ taskListId: 'list123' }); expect(mockTasksList).toHaveBeenCalledWith( expect.objectContaining({ tasklist: 'list123' }), ); }); it('respects showCompleted parameter', async () => { setupValidAuth(); mockTasksList.mockResolvedValue({ data: { items: [] } }); const [, listTool] = createGtasksTools(testConfig); await listTool.execute({ showCompleted: false }); expect(mockTasksList).toHaveBeenCalledWith( expect.objectContaining({ showCompleted: false }), ); }); it('handles empty results', async () => { setupValidAuth(); mockTasksList.mockResolvedValue({ data: { items: [] } }); const [, listTool] = createGtasksTools(testConfig); const result = await listTool.execute({}); expect(result.success).toBe(true); expect(result.output).toBe('No tasks found.'); }); it('returns error when credentials missing', async () => { mockExistsSync.mockReturnValue(false); const [, listTool] = createGtasksTools(testConfig); const result = await listTool.execute({}); expect(result.success).toBe(false); expect(result.error).toContain('Credentials file not found'); }); it('handles API errors gracefully', async () => { setupValidAuth(); mockTasksList.mockRejectedValue(new Error('Not Found')); const [, listTool] = createGtasksTools(testConfig); const result = await listTool.execute({}); expect(result.success).toBe(false); expect(result.error).toContain('Not Found'); }); it('respects maxResults parameter', async () => { setupValidAuth(); mockTasksList.mockResolvedValue({ data: { items: [] } }); const [, listTool] = createGtasksTools(testConfig); await listTool.execute({ maxResults: 50 }); expect(mockTasksList).toHaveBeenCalledWith( expect.objectContaining({ maxResults: 50 }), ); }); });