fix(daily-briefing): add calendar/tasks scope re-auth guidance

This commit is contained in:
William Valentin
2026-02-23 16:07:12 -08:00
parent 056b8ce515
commit 49fd2b5327
6 changed files with 96 additions and 7 deletions
+14
View File
@@ -185,6 +185,19 @@ describe('calendar.today', () => {
expect(result.success).toBe(false);
expect(result.error).toContain('API quota exceeded');
expect(result.error).not.toContain('flynn gcal-auth');
});
it('surfaces re-auth hint for insufficient scopes', async () => {
setupValidAuth();
mockEventsList.mockRejectedValue(new Error('Request had insufficient authentication scopes.'));
const [todayTool] = createGcalTools(testConfig);
const result = await todayTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('insufficient authentication scopes');
expect(result.error).toContain('flynn gcal-auth');
});
it('respects calendarId parameter', async () => {
@@ -326,6 +339,7 @@ describe('calendar.search', () => {
expect(result.success).toBe(false);
expect(result.error).toContain('API quota exceeded');
expect(result.error).not.toContain('flynn gcal-auth');
});
it('formats events with all fields', async () => {
+20 -3
View File
@@ -109,6 +109,20 @@ function formatEvents(events: EventSummary[]): string {
.join('\n\n');
}
function parseErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
function formatToolError(message: string): string {
const needsScopeHint = /insufficient.*scope|insufficientPermissions|Request had insufficient authentication scopes/i.test(message);
return needsScopeHint
? `${message}. Re-run "flynn gcal-auth" to grant Google Calendar read permissions.`
: message;
}
/**
* Creates Google Calendar query tools bound to the given GcalConfig.
* Tools create their own OAuth2 client per invocation.
@@ -150,10 +164,11 @@ export function createGcalTools(config: NonNullable<GcalConfig>): Tool[] {
output: formatEvents(events),
};
} catch (error) {
const message = parseErrorMessage(error);
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
error: formatToolError(message),
};
}
},
@@ -211,10 +226,11 @@ export function createGcalTools(config: NonNullable<GcalConfig>): Tool[] {
output: formatEvents(events),
};
} catch (error) {
const message = parseErrorMessage(error);
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
error: formatToolError(message),
};
}
},
@@ -268,10 +284,11 @@ export function createGcalTools(config: NonNullable<GcalConfig>): Tool[] {
output: formatEvents(events),
};
} catch (error) {
const message = parseErrorMessage(error);
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
error: formatToolError(message),
};
}
},
+14
View File
@@ -171,6 +171,19 @@ describe('tasks.lists', () => {
expect(result.success).toBe(false);
expect(result.error).toContain('API quota exceeded');
expect(result.error).not.toContain('flynn gtasks-auth');
});
it('surfaces re-auth hint for insufficient scopes', async () => {
setupValidAuth();
mockTasklistsList.mockRejectedValue(new Error('Request had insufficient authentication scopes.'));
const [listsTool] = createGtasksTools(testConfig);
const result = await listsTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('insufficient authentication scopes');
expect(result.error).toContain('flynn gtasks-auth');
});
});
@@ -258,6 +271,7 @@ describe('tasks.list', () => {
expect(result.success).toBe(false);
expect(result.error).toContain('Not Found');
expect(result.error).not.toContain('flynn gtasks-auth');
});
it('respects maxResults parameter', async () => {
+18 -2
View File
@@ -94,6 +94,20 @@ function formatTasks(tasks: TaskSummary[]): string {
.join('\n\n');
}
function parseErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
function formatToolError(message: string): string {
const needsScopeHint = /insufficient.*scope|insufficientPermissions|Request had insufficient authentication scopes/i.test(message);
return needsScopeHint
? `${message}. Re-run "flynn gtasks-auth" to grant Google Tasks read permissions.`
: message;
}
/**
* Creates Google Tasks read-only tools bound to the given GtasksConfig.
* Tools create their own OAuth2 client per invocation.
@@ -137,10 +151,11 @@ export function createGtasksTools(config: NonNullable<GtasksConfig>): Tool[] {
output: formatTaskLists(lists),
};
} catch (error) {
const message = parseErrorMessage(error);
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
error: formatToolError(message),
};
}
},
@@ -204,10 +219,11 @@ export function createGtasksTools(config: NonNullable<GtasksConfig>): Tool[] {
output: formatTasks(taskList),
};
} catch (error) {
const message = parseErrorMessage(error);
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
error: formatToolError(message),
};
}
},