6090508bad
- Add curly braces to all if/else/for/while statements - Fix indentation and trailing spaces - Auto-fixed 372 linting errors using eslint --fix - Remaining issues are warnings only (non-null assertions, explicit any types)
275 lines
8.4 KiB
TypeScript
275 lines
8.4 KiB
TypeScript
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<typeof import('fs')>('fs');
|
|
return {
|
|
...actual,
|
|
existsSync: mockExistsSync,
|
|
readFileSync: mockReadFileSync,
|
|
};
|
|
});
|
|
|
|
import { createGtasksTools } from './gtasks.js';
|
|
|
|
// ── Test config ─────────────────────────────────────────────────────────────
|
|
|
|
const testConfig: NonNullable<GtasksConfig> = {
|
|
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 }),
|
|
);
|
|
});
|
|
});
|