Skip to main content
Glama
task-retrieval.spec.ts57.8 kB
import axios from 'axios'; import type { MockedFunction, Mocked } from 'vitest'; import { vi, describe, it, expect, beforeEach } from 'vitest'; import { getTicklerTasks, getWaitingTasks, getTasksDueThisWeek, getTasksDueToday, getTasksDueTomorrow, getRecentMedia, getChoresDueToday, listPersonalInboxTasks, listBrianInboxPerBeckyTasks, listBeckyInboxPerBrianTasks, listNextActions, listGtdProjects, getTaskById, getTasksWithLabel, getAreasOfFocus, getShoppingList, listBrianTimeSensitiveTasks, listBeckyTimeSensitiveTasks, } from './task-retrieval'; import { getTodoistClient } from '../client'; import * as taskCache from '../cache/task-cache'; import fs from 'fs'; import path from 'path'; // Mock the client module vi.mock('../client'); // Mock fs module vi.mock('fs'); vi.mock('path'); // Mock task-cache module vi.mock('../cache/task-cache'); const mockGetTodoistClient = vi.mocked(getTodoistClient); const mockTaskCache = taskCache as Mocked<typeof taskCache>; const mockFs = fs as Mocked<typeof fs>; const mockPath = path as Mocked<typeof path>; describe('Task Retrieval Functions', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('getTicklerTasks', () => { it('should return tickler tasks that are due today or overdue', async () => { // arrange const mockTicklerTasks = [ { id: '1', content: 'Review insurance policies', due: { date: '2024-01-15' }, labels: ['Tickler'], }, { id: '2', content: 'Schedule dentist appointment', due: { date: '2024-01-14' }, labels: ['Ansonia Tickler'], }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTicklerTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTicklerTasks(); // assert expect(result).toEqual(mockTicklerTasks); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent( '(today | overdue) & (#Tickler | #Ansonia Tickler | #Brian tickler)' )}` ); }); it('should handle empty results when no tickler tasks are due', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTicklerTasks(); // assert expect(result).toEqual([]); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent( '(today | overdue) & (#Tickler | #Ansonia Tickler | #Brian tickler)' )}` ); }); it('should throw error when API call fails', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getTicklerTasks(); // assert await expect(promise).rejects.toThrow( 'Failed to get tickler tasks: API Error' ); }); }); describe('getWaitingTasks', () => { const WAITING_FILTER = '#Waiting | #Brian waiting | #Ansonia Waiting'; it('should fetch waiting tasks with the correct filter', async () => { // arrange const mockTasks = [ { id: '123', content: 'Wait for client feedback', description: 'Project is on hold until client responds', is_completed: false, labels: ['Waiting', 'work'], priority: 2, due: null, url: 'https://todoist.com/showTask?id=123', comment_count: 0, created_at: '2024-01-10T10:00:00Z', updated_at: '2024-01-10T10:00:00Z', }, { id: '456', content: 'Wait for vendor quote', description: 'Need pricing before proceeding', is_completed: false, labels: ['Brian waiting', 'business'], priority: 3, due: null, url: 'https://todoist.com/showTask?id=456', comment_count: 1, created_at: '2024-01-10T10:00:00Z', updated_at: '2024-01-10T10:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getWaitingTasks(); // assert expect(result.tasks).toHaveLength(2); expect(result.total_count).toBe(2); expect(result.tasks[0].content).toBe('Wait for client feedback'); expect(result.tasks[1].content).toBe('Wait for vendor quote'); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(WAITING_FILTER)}` ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getWaitingTasks(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); }); it('should handle API error', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getWaitingTasks(); // assert await expect(promise).rejects.toThrow( 'Failed to get waiting tasks: API Error' ); }); it('should handle axios error with response data', async () => { // arrange const axiosError = { isAxiosError: true, response: { data: { error: 'Rate limit exceeded' }, }, message: 'Request failed', }; const mockClient = { get: vi.fn().mockRejectedValue(axiosError), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getWaitingTasks(); // assert await expect(promise).rejects.toThrow( 'Failed to get waiting tasks: Rate limit exceeded' ); }); it('should map task properties correctly', async () => { // arrange const mockTasks = [ { id: '789', content: 'Test waiting task', description: 'Test description', is_completed: false, labels: ['Ansonia Waiting', 'test'], priority: 1, due: null, url: 'https://todoist.com/showTask?id=789', comment_count: 5, created_at: '2024-01-10T10:00:00Z', updated_at: '2024-01-10T10:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getWaitingTasks(); // assert expect(result.tasks[0]).toEqual({ id: 789, content: 'Test waiting task', description: 'Test description', is_completed: false, labels: ['Ansonia Waiting', 'test'], priority: 1, due_date: null, url: 'https://todoist.com/showTask?id=789', comment_count: 5, }); }); }); describe('getTasksDueThisWeek', () => { it('should get tasks due this week with complex filter', async () => { // arrange const mockTasks = [ { id: 1, content: 'Task 1', due: { date: '2024-01-15' } }, { id: 2, content: 'Task 2', due: { date: '2024-01-16' } }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksDueThisWeek(); // assert expect(result).toEqual(mockTasks); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent( 'next 7 days & (!##Tickler) & (!##Ansonia Tickler) & (!##Project - Meal prep) & ' + '(!shared | assigned to: Brian | ##Brian inbox - per Becky | ##Brian acknowledged | ##Project - Meal prep | ##Shopping list) & ' + '(!##BABY & !###BrianBabyFocus & !##Home Preparation & !##Cards & !##Hospital Preparation & !##Baby Care Book & ' + '!##To Pack & !##Hospital Stay & !##Post Partum & !##Questions and Concerns & !##Research & !##BabyClassNotes & ' + '!##CarPreparation & !##Food & !##Before Hospital Stay)' )}` ); }); it('should handle API errors gracefully', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getTasksDueThisWeek(); // assert await expect(promise).rejects.toThrow( 'Failed to get tasks due this week: API Error' ); }); it('should return empty array when no tasks found', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksDueThisWeek(); // assert expect(result).toEqual([]); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent( 'next 7 days & (!##Tickler) & (!##Ansonia Tickler) & (!##Project - Meal prep) & ' + '(!shared | assigned to: Brian | ##Brian inbox - per Becky | ##Brian acknowledged | ##Project - Meal prep | ##Shopping list) & ' + '(!##BABY & !###BrianBabyFocus & !##Home Preparation & !##Cards & !##Hospital Preparation & !##Baby Care Book & ' + '!##To Pack & !##Hospital Stay & !##Post Partum & !##Questions and Concerns & !##Research & !##BabyClassNotes & ' + '!##CarPreparation & !##Food & !##Before Hospital Stay)' )}` ); }); }); describe('getTasksDueToday', () => { const DUE_TODAY_FILTER = [ '(today | overdue)', '& !##Tickler', '& !##Brian tickler', '& !##Ansonia Tickler', '& !##Someday', '& !##Brian someday', '& !##Brian inbox - per Becky', '& !##Becky inbox - per Brian', '& !##Shopping list', '& !##Becky acknowledged', '& !##Chores', '& !##rent', '& (!##BABY', '& !###BrianBabyFocus', '& !##Home Preparation', '& !##Cards', '& !##Hospital Preparation', '& !##Baby Care Book', '& !##To Pack', '& !##Hospital Stay', '& !##Post Partum', '& !##Questions and Concerns', '& !##Research', '& !##BabyClassNotes', '& !##CarPreparation', '& !##Food', '& !##Before Hospital Stay)', '& !##Daily Chores', '& !##Baby Research', '& !##Becky someday', ].join(' '); it('should fetch tasks due today with the correct filter', async () => { // arrange const mockTasks = [ { id: '123', content: 'Complete project report', description: 'Finish the quarterly report', is_completed: false, labels: ['work', 'priority'], priority: 3, due: { date: '2024-01-15', string: 'today', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/showTask?id=123', comment_count: 0, created_at: '2024-01-10T10:00:00Z', updated_at: '2024-01-10T10:00:00Z', }, { id: '456', content: 'Buy groceries', description: 'Milk, bread, eggs', is_completed: false, labels: ['personal'], priority: 2, due: { date: '2024-01-15', string: 'today', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/showTask?id=456', comment_count: 1, created_at: '2024-01-10T10:00:00Z', updated_at: '2024-01-10T10:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksDueToday(); // assert expect(result.tasks).toHaveLength(2); expect(result.total_count).toBe(2); expect(result.tasks[0].content).toBe('Complete project report'); expect(result.tasks[1].content).toBe('Buy groceries'); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(DUE_TODAY_FILTER)}` ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksDueToday(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); }); it('should handle API error', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getTasksDueToday(); // assert await expect(promise).rejects.toThrow( 'Failed to get tasks due today: API Error' ); }); it('should handle axios error with response data', async () => { // arrange const axiosError = { isAxiosError: true, response: { data: { error: 'Rate limit exceeded' }, }, message: 'Request failed', }; const mockClient = { get: vi.fn().mockRejectedValue(axiosError), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getTasksDueToday(); // assert await expect(promise).rejects.toThrow( 'Failed to get tasks due today: Rate limit exceeded' ); }); it('should map task properties correctly', async () => { // arrange const mockTasks = [ { id: '789', content: 'Test task', description: 'Test description', is_completed: true, labels: ['test'], priority: 1, due: { date: '2024-01-15', string: 'today', lang: 'en', is_recurring: true, }, url: 'https://todoist.com/showTask?id=789', comment_count: 5, created_at: '2024-01-10T10:00:00Z', updated_at: '2024-01-10T10:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksDueToday(); // assert expect(result.tasks[0]).toEqual({ id: 789, content: 'Test task', description: 'Test description', is_completed: true, labels: ['test'], priority: 1, due_date: '2024-01-15', url: 'https://todoist.com/showTask?id=789', comment_count: 5, }); }); }); describe('getTasksDueTomorrow', () => { const EXPECTED_FILTER = [ 'tomorrow', '& (!##Tickler)', '& (!##Chores)', '& (!##Brian tickler)', '& (!##Ansonia Tickler)', '& (!##Project - Meal prep)', '& (!shared | assigned to: Brian | ##Brian acknowledged | ##Project - Meal prep | ##Shopping list)', '& (!##BABY & !###BrianBabyFocus & !##Home Preparation & !##Cards & !##Hospital Preparation & !##Baby Care Book & !##To Pack & !##Hospital Stay & !##Post Partum & !##Questions and Concerns & !##Research & !##BabyClassNotes & !##CarPreparation & !##Food & !##Before Hospital Stay)', ].join(' '); it('should return tasks due tomorrow with the correct filter', async () => { // arrange const mockTasks = [ { id: '123', content: 'Review project proposal', description: 'Go through the Q4 proposal', is_completed: false, labels: ['work', 'priority'], priority: 3, due: { date: '2024-01-16', string: 'tomorrow', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/showTask?id=123', comment_count: 2, created_at: '2024-01-14T10:00:00Z', updated_at: '2024-01-14T10:00:00Z', project_id: '456', }, { id: '124', content: 'Team meeting prep', description: "Prepare agenda for tomorrow's meeting", is_completed: false, labels: ['work'], priority: 2, due: { date: '2024-01-16', string: 'tomorrow', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/showTask?id=124', comment_count: 0, created_at: '2024-01-14T11:00:00Z', updated_at: '2024-01-14T11:00:00Z', project_id: '789', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksDueTomorrow(); // assert expect(result).toEqual(mockTasks); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(EXPECTED_FILTER)}` ); }); it('should handle API errors gracefully', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getTasksDueTomorrow(); // assert await expect(promise).rejects.toThrow( 'Failed to get tasks due tomorrow: API Error' ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksDueTomorrow(); // assert expect(result).toEqual([]); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(EXPECTED_FILTER)}` ); }); }); describe('getRecentMedia', () => { it('should return recent media tasks from last 30 days', async () => { // arrange const mockTasks = [ { id: '1', content: 'Watch The Matrix', project_id: '123', labels: ['Media', 'Movie'], created: '2024-01-15T10:00:00Z', due: null, parent_id: null, }, { id: '2', content: 'Read Dune', project_id: '124', labels: ['Media', 'Book'], created: '2024-01-10T10:00:00Z', due: null, parent_id: null, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getRecentMedia(); // assert expect(result).toEqual(mockTasks); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent( '##Media & !subtask & (created after: 30 days ago) & !@watched' )}` ); }); it('should handle empty results', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getRecentMedia(); // assert expect(result).toEqual([]); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent( '##Media & !subtask & (created after: 30 days ago) & !@watched' )}` ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getRecentMedia(); // assert await expect(promise).rejects.toThrow( 'Failed to get recent media: API Error' ); }); }); describe('getChoresDueToday', () => { it('should return chores due today or overdue', async () => { // arrange const mockTasks = [ { id: '1', content: 'Do laundry', due: { date: '2024-01-15' }, project_id: '123', labels: ['chore'], }, { id: '2', content: 'Clean kitchen', due: { date: '2024-01-14' }, project_id: '123', labels: ['chore'], }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getChoresDueToday(); // assert expect(result).toEqual(mockTasks); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent('(today | overdue) & ##Chores')}` ); }); it('should return no chores message when no chores are due', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getChoresDueToday(); // assert expect(result).toEqual([]); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent('(today | overdue) & ##Chores')}` ); }); it('should handle API errors gracefully', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getChoresDueToday(); // assert await expect(promise).rejects.toThrow( 'Failed to get chores due today: API Error' ); }); }); describe('listPersonalInboxTasks', () => { it('should return personal inbox tasks when API call succeeds', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Test personal inbox task', description: 'Test description', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listPersonalInboxTasks(); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Test personal inbox task'); expect(result.total_count).toBe(1); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=%23%23Inbox%20%26%20!subtask' ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listPersonalInboxTasks(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=%23%23Inbox%20%26%20!subtask' ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = listPersonalInboxTasks(); // assert await expect(promise).rejects.toThrow( 'Failed to list personal inbox tasks: API Error' ); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=%23%23Inbox%20%26%20!subtask' ); }); }); describe('listBrianInboxPerBeckyTasks', () => { it('should return Brian inbox per Becky tasks when API call succeeds', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Test Brian inbox per Becky task', description: 'Test description', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listBrianInboxPerBeckyTasks(); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Test Brian inbox per Becky task'); expect(result.total_count).toBe(1); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=%23%23Brian%20inbox%20-%20per%20Becky%20%26%20!subtask' ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listBrianInboxPerBeckyTasks(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=%23%23Brian%20inbox%20-%20per%20Becky%20%26%20!subtask' ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = listBrianInboxPerBeckyTasks(); // assert await expect(promise).rejects.toThrow( 'Failed to list Brian inbox per Becky tasks: API Error' ); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=%23%23Brian%20inbox%20-%20per%20Becky%20%26%20!subtask' ); }); it('should store task names in cache when fetching Brian inbox per Becky tasks', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Test Brian inbox per Becky task 1', description: 'Test description 1', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, { id: '2', project_id: '456', content: 'Test Brian inbox per Becky task 2', description: 'Test description 2', is_completed: false, labels: ['label2'], priority: 2, due: null, url: 'https://todoist.com/task/2', comment_count: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockTaskCache.setTaskName = vi.fn(); // act await listBrianInboxPerBeckyTasks(); // assert expect(mockTaskCache.setTaskName).toHaveBeenCalledWith( '1', 'Test Brian inbox per Becky task 1' ); expect(mockTaskCache.setTaskName).toHaveBeenCalledWith( '2', 'Test Brian inbox per Becky task 2' ); }); }); describe('listBeckyInboxPerBrianTasks', () => { it('should return Becky inbox per Brian tasks when API call succeeds', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Test Becky inbox per Brian task', description: 'Test description', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listBeckyInboxPerBrianTasks(); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Test Becky inbox per Brian task'); expect(result.total_count).toBe(1); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=%23%23Becky%20inbox%20-%20per%20Brian%20%26%20!subtask' ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listBeckyInboxPerBrianTasks(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=%23%23Becky%20inbox%20-%20per%20Brian%20%26%20!subtask' ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = listBeckyInboxPerBrianTasks(); // assert await expect(promise).rejects.toThrow( 'Failed to list Becky inbox per Brian tasks: API Error' ); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=%23%23Becky%20inbox%20-%20per%20Brian%20%26%20!subtask' ); }); }); describe('listNextActions', () => { it('should return next actions tasks when API call succeeds', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Test next action task', description: 'Test description', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listNextActions(); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Test next action task'); expect(result.total_count).toBe(1); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=(%23%23Next%20actions%20%7C%20%23%23Brian%20acknowledged)%20%26%20!subtask' ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listNextActions(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=(%23%23Next%20actions%20%7C%20%23%23Brian%20acknowledged)%20%26%20!subtask' ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = listNextActions(); // assert await expect(promise).rejects.toThrow( 'Failed to list next actions: API Error' ); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=(%23%23Next%20actions%20%7C%20%23%23Brian%20acknowledged)%20%26%20!subtask' ); }); }); describe('listGtdProjects', () => { it('should return GTD projects tasks when API call succeeds', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Test GTD project task', description: 'Test description', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listGtdProjects(); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Test GTD project task'); expect(result.total_count).toBe(1); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=(%23Projects%20%7C%20%23Brian%20projects%20%7C%20%23Ansonia%20Projects)%20%26%20!subtask%20%26%20(!%23%23BABY%20%26%20!%23%23%23BrianBabyFocus%20%26%20!%23%23Home%20Preparation%20%26%20!%23%23Cards%20%26%20!%23%23Hospital%20Preparation%20%26%20!%23%23Baby%20Care%20Book%20%26%20!%23%23To%20Pack%20%26%20!%23%23Hospital%20Stay%20%26%20!%23%23Post%20Partum%20%26%20!%23%23Questions%20and%20Concerns%20%26%20!%23%23Research%20%26%20!%23%23BabyClassNotes%20%26%20!%23%23CarPreparation%20%26%20!%23%23Food%20%26%20!%23%23Before%20Hospital%20Stay)' ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listGtdProjects(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=(%23Projects%20%7C%20%23Brian%20projects%20%7C%20%23Ansonia%20Projects)%20%26%20!subtask%20%26%20(!%23%23BABY%20%26%20!%23%23%23BrianBabyFocus%20%26%20!%23%23Home%20Preparation%20%26%20!%23%23Cards%20%26%20!%23%23Hospital%20Preparation%20%26%20!%23%23Baby%20Care%20Book%20%26%20!%23%23To%20Pack%20%26%20!%23%23Hospital%20Stay%20%26%20!%23%23Post%20Partum%20%26%20!%23%23Questions%20and%20Concerns%20%26%20!%23%23Research%20%26%20!%23%23BabyClassNotes%20%26%20!%23%23CarPreparation%20%26%20!%23%23Food%20%26%20!%23%23Before%20Hospital%20Stay)' ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = listGtdProjects(); // assert await expect(promise).rejects.toThrow( 'Failed to list GTD projects: API Error' ); expect(mockClient.get).toHaveBeenCalledWith( '/tasks?filter=(%23Projects%20%7C%20%23Brian%20projects%20%7C%20%23Ansonia%20Projects)%20%26%20!subtask%20%26%20(!%23%23BABY%20%26%20!%23%23%23BrianBabyFocus%20%26%20!%23%23Home%20Preparation%20%26%20!%23%23Cards%20%26%20!%23%23Hospital%20Preparation%20%26%20!%23%23Baby%20Care%20Book%20%26%20!%23%23To%20Pack%20%26%20!%23%23Hospital%20Stay%20%26%20!%23%23Post%20Partum%20%26%20!%23%23Questions%20and%20Concerns%20%26%20!%23%23Research%20%26%20!%23%23BabyClassNotes%20%26%20!%23%23CarPreparation%20%26%20!%23%23Food%20%26%20!%23%23Before%20Hospital%20Stay)' ); }); }); describe('getTaskById', () => { it('should fetch task by id successfully', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: { id: '123', content: 'Test Task', description: 'Test Description', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/123', comment_count: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTaskById('123'); // assert expect(result).toEqual({ id: '123', content: 'Test Task', description: 'Test Description', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/123', comment_count: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }); expect(mockClient.get).toHaveBeenCalledWith('/tasks/123'); }); it('should handle API error when task not found', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('Task not found')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getTaskById('999'); // assert await expect(promise).rejects.toThrow( 'Failed to get task by id: Task not found' ); expect(mockClient.get).toHaveBeenCalledWith('/tasks/999'); }); }); describe('getTasksWithLabel', () => { it('should return tasks with specific label', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Test task with label', description: 'Test description', is_completed: false, labels: ['urgent'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksWithLabel('urgent'); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Test task with label'); expect(result.tasks[0].labels).toContain('urgent'); expect(result.total_count).toBe(1); const expectedFilter = '@urgent & !##Brian projects & !##Projects'; expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(expectedFilter)}` ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksWithLabel('urgent'); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); const expectedFilter = '@urgent & !##Brian projects & !##Projects'; expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(expectedFilter)}` ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getTasksWithLabel('urgent'); // assert await expect(promise).rejects.toThrow( 'Failed to get tasks with label: API Error' ); const expectedFilter = '@urgent & !##Brian projects & !##Projects'; expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(expectedFilter)}` ); }); describe('context label behavior', () => { it('should use filter without project exclusions when label starts with context:', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Project task with context label', description: 'Test description', is_completed: false, labels: ['context:work'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksWithLabel('context:work'); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Project task with context label'); expect(result.tasks[0].labels).toContain('context:work'); expect(result.total_count).toBe(1); const expectedFilter = '@context:work'; expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(expectedFilter)}` ); }); it('should exclude project tasks when label does not start with context:', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Regular task with label', description: 'Test description', is_completed: false, labels: ['work'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksWithLabel('work'); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Regular task with label'); expect(result.tasks[0].labels).toContain('work'); expect(result.total_count).toBe(1); const expectedFilter = '@work & !##Brian projects & !##Projects'; expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(expectedFilter)}` ); }); it('should use filter without project exclusions for context labels with special characters', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Task with complex context label', description: 'Test description', is_completed: false, labels: ['context:home-office'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getTasksWithLabel('context:home-office'); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Task with complex context label'); expect(result.tasks[0].labels).toContain('context:home-office'); expect(result.total_count).toBe(1); const expectedFilter = '@context:home-office'; expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(expectedFilter)}` ); }); }); }); describe('getAreasOfFocus', () => { it('should return tasks from Areas of focus project', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Review quarterly goals', description: 'Strategic planning task', is_completed: false, labels: ['focus'], priority: 1, due: { date: '2024-01-15', string: 'Jan 15', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, { id: '2', project_id: '123', content: 'Plan strategic initiatives', description: 'Long-term planning', is_completed: false, labels: ['planning'], priority: 2, due: { date: '2024-01-20', string: 'Jan 20', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/2', comment_count: 1, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getAreasOfFocus(); // assert expect(result.tasks).toHaveLength(2); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Review quarterly goals'); expect(result.tasks[1].id).toBe(2); expect(result.tasks[1].content).toBe('Plan strategic initiatives'); expect(result.total_count).toBe(2); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent('##Areas of focus')}` ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getAreasOfFocus(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent('##Areas of focus')}` ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getAreasOfFocus(); // assert await expect(promise).rejects.toThrow( 'Failed to get tasks from Areas of focus project: API Error' ); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent('##Areas of focus')}` ); }); }); describe('getShoppingList', () => { it('should return tasks from Shopping list project', async () => { // arrange const mockTasks = [ { id: '1', project_id: '456', content: 'Milk', description: 'Organic whole milk', is_completed: false, labels: ['dairy'], priority: 1, due: { date: '2024-01-15', string: 'Jan 15', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, { id: '2', project_id: '456', content: 'Bread', description: 'Whole grain bread', is_completed: false, labels: ['bakery'], priority: 2, due: { date: '2024-01-15', string: 'Jan 15', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/2', comment_count: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getShoppingList(); // assert expect(result.tasks).toHaveLength(2); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Milk'); expect(result.tasks[1].id).toBe(2); expect(result.tasks[1].content).toBe('Bread'); expect(result.total_count).toBe(2); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent('##Shopping list')}` ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getShoppingList(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent('##Shopping list')}` ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getShoppingList(); // assert await expect(promise).rejects.toThrow( 'Failed to get tasks from Shopping list project: API Error' ); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent('##Shopping list')}` ); }); }); describe('listBrianTimeSensitiveTasks', () => { const BRIAN_TIME_SENSITIVE_FILTER = '##Brian time sensitive \\(per BP\\) & !subtask'; it('should return Brian time sensitive tasks when API call succeeds', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Test Brian time sensitive task', description: 'Test description', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listBrianTimeSensitiveTasks(); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Test Brian time sensitive task'); expect(result.total_count).toBe(1); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(BRIAN_TIME_SENSITIVE_FILTER)}` ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listBrianTimeSensitiveTasks(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(BRIAN_TIME_SENSITIVE_FILTER)}` ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = listBrianTimeSensitiveTasks(); // assert await expect(promise).rejects.toThrow( 'Failed to list Brian time sensitive tasks: API Error' ); }); }); describe('listBeckyTimeSensitiveTasks', () => { const BECKY_TIME_SENSITIVE_FILTER = '##Becky time sensitive \\(per BK\\) & !subtask'; it('should return Becky time sensitive tasks when API call succeeds', async () => { // arrange const mockTasks = [ { id: '1', project_id: '123', content: 'Test Becky time sensitive task', description: 'Test description', is_completed: false, labels: ['label1'], priority: 1, due: { date: '2024-01-01', string: 'Jan 1', lang: 'en', is_recurring: false, }, url: 'https://todoist.com/task/1', comment_count: 2, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockTasks }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listBeckyTimeSensitiveTasks(); // assert expect(result.tasks).toHaveLength(1); expect(result.tasks[0].id).toBe(1); expect(result.tasks[0].content).toBe('Test Becky time sensitive task'); expect(result.total_count).toBe(1); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(BECKY_TIME_SENSITIVE_FILTER)}` ); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await listBeckyTimeSensitiveTasks(); // assert expect(result.tasks).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(BECKY_TIME_SENSITIVE_FILTER)}` ); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = listBeckyTimeSensitiveTasks(); // assert await expect(promise).rejects.toThrow( 'Failed to list Becky time sensitive tasks: API Error' ); }); }); });

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/bkotos/todoist-mcp'

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