Files
waggle-os/packages/agent/tests/connectors/connectors-microsoft.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

712 lines
28 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { MSTeamsConnector } from '../../src/connectors/ms-teams-connector.js';
import { OutlookConnector } from '../../src/connectors/outlook-connector.js';
import { OneDriveConnector } from '../../src/connectors/onedrive-connector.js';
import { OneNoteConnector } from '../../src/connectors/onenote-connector.js';
import type { VaultStore } from '@waggle/core';
function createMockVault(
connectorId: string,
cred?: { value: string; isExpired: boolean },
): VaultStore {
return {
getConnectorCredential: vi.fn((id: string) => {
if (id === connectorId && cred) return { ...cred, type: 'bearer' };
return null;
}),
get: vi.fn(() => null),
set: vi.fn(),
delete: vi.fn(),
list: vi.fn(() => []),
has: vi.fn(() => false),
setConnectorCredential: vi.fn(),
migrateFromConfig: vi.fn(() => 0),
} as unknown as VaultStore;
}
// ── Microsoft Teams Connector ─────────────────────────────────────────
describe('MSTeamsConnector', () => {
let connector: MSTeamsConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new MSTeamsConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('has correct id, name, service, and authType', () => {
expect(connector.id).toBe('ms-teams');
expect(connector.name).toBe('Microsoft Teams');
expect(connector.service).toBe('teams.microsoft.com');
expect(connector.authType).toBe('bearer');
expect(connector.substrate).toBe('waggle');
});
it('has expected number of actions', () => {
expect(connector.actions).toHaveLength(6);
expect(connector.actions.map(a => a.name)).toEqual([
'list_teams', 'list_channels', 'get_messages', 'send_message', 'list_chats', 'send_chat_message',
]);
});
it('action risk levels are correct', () => {
const risks = Object.fromEntries(connector.actions.map(a => [a.name, a.riskLevel]));
expect(risks.list_teams).toBe('low');
expect(risks.list_channels).toBe('low');
expect(risks.get_messages).toBe('low');
expect(risks.send_message).toBe('medium');
expect(risks.list_chats).toBe('low');
expect(risks.send_chat_message).toBe('medium');
});
it('execute returns error when not connected', async () => {
const result = await connector.execute('list_teams', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Not connected');
});
it('healthCheck returns disconnected without token', async () => {
const health = await connector.healthCheck();
expect(health.status).toBe('disconnected');
expect(health.id).toBe('ms-teams');
});
it('connect() retrieves token from vault', async () => {
const vault = createMockVault('ms-teams', { value: 'graph-token-123', isExpired: false });
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('ms-teams');
});
it('healthCheck() returns connected when Graph API responds OK', async () => {
const vault = createMockVault('ms-teams', { value: 'graph-token-123', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ displayName: 'User' }) }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
});
it('healthCheck() returns error when Graph API fails', async () => {
const vault = createMockVault('ms-teams', { value: 'graph-token-123', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: false, status: 401 }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('error');
expect(health.error).toContain('401');
});
it('execute(list_teams) calls Graph API', async () => {
const vault = createMockVault('ms-teams', { value: 'graph-token-123', isExpired: false });
await connector.connect(vault);
const mockTeams = { value: [{ id: 't1', displayName: 'Engineering' }] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockTeams }) as unknown as typeof fetch;
const result = await connector.execute('list_teams', {});
expect(result.success).toBe(true);
expect(result.data).toEqual(mockTeams);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/joinedTeams');
});
it('execute(send_message) posts to channel', async () => {
const vault = createMockVault('ms-teams', { value: 'graph-token-123', isExpired: false });
await connector.connect(vault);
const mockMsg = { id: 'msg1' };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockMsg }) as unknown as typeof fetch;
const result = await connector.execute('send_message', {
team_id: 't1', channel_id: 'c1', content: 'Hello Teams!',
});
expect(result.success).toBe(true);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/teams/t1/channels/c1/messages');
expect(fetchCall[1].method).toBe('POST');
});
it('execute() returns error for unknown action', async () => {
const vault = createMockVault('ms-teams', { value: 'graph-token-123', isExpired: false });
await connector.connect(vault);
const result = await connector.execute('unknown_action', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Unknown action');
});
it('toDefinition() maps correctly', () => {
const def = connector.toDefinition('connected');
expect(def.id).toBe('ms-teams');
expect(def.tools).toContain('connector_ms-teams_list_teams');
expect(def.tools).toContain('connector_ms-teams_send_message');
expect(def.tools).toHaveLength(6);
expect(def.actions).toHaveLength(6);
});
});
// ── Outlook Connector ─────────────────────────────────────────────────
describe('OutlookConnector', () => {
let connector: OutlookConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new OutlookConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('has correct id, name, service, and authType', () => {
expect(connector.id).toBe('outlook');
expect(connector.name).toBe('Outlook Calendar & Email');
expect(connector.service).toBe('outlook.office365.com');
expect(connector.authType).toBe('bearer');
expect(connector.substrate).toBe('waggle');
});
it('has expected number of actions', () => {
expect(connector.actions).toHaveLength(6);
expect(connector.actions.map(a => a.name)).toEqual([
'list_events', 'create_event', 'list_emails', 'send_email', 'search_emails', 'get_email',
]);
});
it('action risk levels are correct', () => {
const risks = Object.fromEntries(connector.actions.map(a => [a.name, a.riskLevel]));
expect(risks.list_events).toBe('low');
expect(risks.create_event).toBe('medium');
expect(risks.list_emails).toBe('low');
expect(risks.send_email).toBe('medium');
expect(risks.search_emails).toBe('low');
expect(risks.get_email).toBe('low');
});
it('execute returns error when not connected', async () => {
const result = await connector.execute('list_events', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Not connected');
});
it('healthCheck returns disconnected without token', async () => {
const health = await connector.healthCheck();
expect(health.status).toBe('disconnected');
expect(health.id).toBe('outlook');
});
it('connect() retrieves token from vault', async () => {
const vault = createMockVault('outlook', { value: 'graph-token-456', isExpired: false });
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('outlook');
});
it('healthCheck() returns connected when Graph API responds OK', async () => {
const vault = createMockVault('outlook', { value: 'graph-token-456', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ displayName: 'User' }) }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
});
it('execute(list_events) calls Graph API', async () => {
const vault = createMockVault('outlook', { value: 'graph-token-456', isExpired: false });
await connector.connect(vault);
const mockEvents = { value: [{ id: 'ev1', subject: 'Standup' }] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockEvents }) as unknown as typeof fetch;
const result = await connector.execute('list_events', { $top: 10 });
expect(result.success).toBe(true);
expect(result.data).toEqual(mockEvents);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/events');
});
it('execute(create_event) creates event with attendees', async () => {
const vault = createMockVault('outlook', { value: 'graph-token-456', isExpired: false });
await connector.connect(vault);
const mockEvent = { id: 'ev2', subject: 'Team Sync' };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockEvent }) as unknown as typeof fetch;
const result = await connector.execute('create_event', {
subject: 'Team Sync',
start: '2026-03-20T10:00:00',
end: '2026-03-20T11:00:00',
attendees: ['alice@example.com'],
});
expect(result.success).toBe(true);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/events');
expect(fetchCall[1].method).toBe('POST');
const body = JSON.parse(fetchCall[1].body);
expect(body.subject).toBe('Team Sync');
expect(body.attendees).toHaveLength(1);
expect(body.attendees[0].emailAddress.address).toBe('alice@example.com');
});
it('execute(send_email) sends email', async () => {
const vault = createMockVault('outlook', { value: 'graph-token-456', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, text: async () => '' }) as unknown as typeof fetch;
const result = await connector.execute('send_email', {
to: ['bob@example.com'],
subject: 'Hello',
body: '<p>Hi Bob!</p>',
});
expect(result.success).toBe(true);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/sendMail');
expect(fetchCall[1].method).toBe('POST');
});
it('execute(search_emails) searches with $search', async () => {
const vault = createMockVault('outlook', { value: 'graph-token-456', isExpired: false });
await connector.connect(vault);
const mockResults = { value: [{ id: 'm1', subject: 'Project Update' }] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockResults }) as unknown as typeof fetch;
const result = await connector.execute('search_emails', { query: 'project' });
expect(result.success).toBe(true);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/messages');
// URLSearchParams encodes $ as %24
expect(decodeURIComponent(fetchCall[0])).toContain('$search');
});
it('execute() returns error for unknown action', async () => {
const vault = createMockVault('outlook', { value: 'graph-token-456', isExpired: false });
await connector.connect(vault);
const result = await connector.execute('unknown_action', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Unknown action');
});
it('toDefinition() maps correctly', () => {
const def = connector.toDefinition('connected');
expect(def.id).toBe('outlook');
expect(def.tools).toContain('connector_outlook_list_events');
expect(def.tools).toContain('connector_outlook_send_email');
expect(def.tools).toHaveLength(6);
expect(def.actions).toHaveLength(6);
});
});
// ── OneDrive Connector ────────────────────────────────────────────────
describe('OneDriveConnector', () => {
let connector: OneDriveConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new OneDriveConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('has correct id, name, service, and authType', () => {
expect(connector.id).toBe('onedrive');
expect(connector.name).toBe('OneDrive');
expect(connector.service).toBe('onedrive.live.com');
expect(connector.authType).toBe('bearer');
expect(connector.substrate).toBe('waggle');
});
it('has expected number of actions', () => {
expect(connector.actions).toHaveLength(5);
expect(connector.actions.map(a => a.name)).toEqual([
'list_files', 'get_file', 'search_files', 'upload_file', 'list_recent',
]);
});
it('action risk levels are correct', () => {
const risks = Object.fromEntries(connector.actions.map(a => [a.name, a.riskLevel]));
expect(risks.list_files).toBe('low');
expect(risks.get_file).toBe('low');
expect(risks.search_files).toBe('low');
expect(risks.upload_file).toBe('medium');
expect(risks.list_recent).toBe('low');
});
it('execute returns error when not connected', async () => {
const result = await connector.execute('list_files', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Not connected');
});
it('healthCheck returns disconnected without token', async () => {
const health = await connector.healthCheck();
expect(health.status).toBe('disconnected');
expect(health.id).toBe('onedrive');
});
it('connect() retrieves token from vault', async () => {
const vault = createMockVault('onedrive', { value: 'graph-token-789', isExpired: false });
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('onedrive');
});
it('healthCheck() returns connected when Graph API responds OK', async () => {
const vault = createMockVault('onedrive', { value: 'graph-token-789', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ driveType: 'personal' }) }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
});
it('healthCheck() returns error when Graph API fails', async () => {
const vault = createMockVault('onedrive', { value: 'graph-token-789', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: false, status: 403 }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('error');
expect(health.error).toContain('403');
});
it('execute(list_files) calls root children endpoint', async () => {
const vault = createMockVault('onedrive', { value: 'graph-token-789', isExpired: false });
await connector.connect(vault);
const mockFiles = { value: [{ id: 'f1', name: 'document.docx' }] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockFiles }) as unknown as typeof fetch;
const result = await connector.execute('list_files', {});
expect(result.success).toBe(true);
expect(result.data).toEqual(mockFiles);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/drive/root/children');
});
it('execute(list_files) with folder_path uses path-based endpoint', async () => {
const vault = createMockVault('onedrive', { value: 'graph-token-789', isExpired: false });
await connector.connect(vault);
const mockFiles = { value: [{ id: 'f2', name: 'report.xlsx' }] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockFiles }) as unknown as typeof fetch;
const result = await connector.execute('list_files', { folder_path: 'Documents/Work' });
expect(result.success).toBe(true);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/drive/root:/Documents/Work:/children');
});
it('execute(search_files) searches via Graph API', async () => {
const vault = createMockVault('onedrive', { value: 'graph-token-789', isExpired: false });
await connector.connect(vault);
const mockResults = { value: [{ id: 'f3', name: 'notes.txt' }] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockResults }) as unknown as typeof fetch;
const result = await connector.execute('search_files', { query: 'notes' });
expect(result.success).toBe(true);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/drive/root/search');
expect(fetchCall[0]).toContain('notes');
});
it('execute(upload_file) uploads via PUT', async () => {
const vault = createMockVault('onedrive', { value: 'graph-token-789', isExpired: false });
await connector.connect(vault);
const mockFile = { id: 'f4', name: 'notes.txt' };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockFile }) as unknown as typeof fetch;
const result = await connector.execute('upload_file', {
path: 'Documents/notes.txt',
content: 'Hello World',
});
expect(result.success).toBe(true);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/drive/root:/Documents/notes.txt:/content');
expect(fetchCall[1].method).toBe('PUT');
});
it('execute(get_file) downloads file content', async () => {
const vault = createMockVault('onedrive', { value: 'graph-token-789', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
text: async () => 'file content here',
}) as unknown as typeof fetch;
const result = await connector.execute('get_file', { item_id: 'f1' });
expect(result.success).toBe(true);
expect((result.data as Record<string, unknown>).content).toBe('file content here');
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/me/drive/items/f1/content');
});
it('execute() returns error for unknown action', async () => {
const vault = createMockVault('onedrive', { value: 'graph-token-789', isExpired: false });
await connector.connect(vault);
const result = await connector.execute('unknown_action', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Unknown action');
});
it('toDefinition() maps correctly', () => {
const def = connector.toDefinition('connected');
expect(def.id).toBe('onedrive');
expect(def.tools).toContain('connector_onedrive_list_files');
expect(def.tools).toContain('connector_onedrive_upload_file');
expect(def.tools).toHaveLength(5);
expect(def.actions).toHaveLength(5);
});
});
// ── OneNote Connector (E-6) ────────────────────────────────────────────
describe('OneNoteConnector', () => {
let connector: OneNoteConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new OneNoteConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('has correct id, name, service, and authType', () => {
expect(connector.id).toBe('onenote');
expect(connector.name).toBe('Microsoft OneNote');
expect(connector.service).toBe('onenote.com');
expect(connector.authType).toBe('bearer');
expect(connector.substrate).toBe('waggle');
expect(connector.category).toBe('productivity');
});
it('exposes the harvest-focused action surface', () => {
expect(connector.actions).toHaveLength(5);
expect(connector.actions.map((a) => a.name)).toEqual([
'list_notebooks',
'list_sections',
'list_pages',
'get_page',
'search_pages',
]);
});
it('every action is low risk (read-only surface)', () => {
for (const action of connector.actions) {
expect(action.riskLevel).toBe('low');
}
});
it('list_sections requires notebook_id', () => {
const action = connector.actions.find((a) => a.name === 'list_sections')!;
expect(action.inputSchema.required).toContain('notebook_id');
});
it('get_page requires page_id', () => {
const action = connector.actions.find((a) => a.name === 'get_page')!;
expect(action.inputSchema.required).toContain('page_id');
});
it('search_pages requires query', () => {
const action = connector.actions.find((a) => a.name === 'search_pages')!;
expect(action.inputSchema.required).toContain('query');
});
it('execute returns error when not connected', async () => {
const result = await connector.execute('list_notebooks', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Not connected');
expect(result.error).toContain('Notes.Read');
});
it('healthCheck returns disconnected without token', async () => {
const health = await connector.healthCheck();
expect(health.status).toBe('disconnected');
expect(health.id).toBe('onenote');
});
it('connect() retrieves token from vault', async () => {
const vault = createMockVault('onenote', { value: 'graph-token-onenote', isExpired: false });
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('onenote');
});
it('healthCheck() probes /me/onenote/notebooks to exercise the Notes.Read scope', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ value: [] }),
});
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
const calledUrl = String(fetchMock.mock.calls[0][0]);
expect(calledUrl).toContain('/me/onenote/notebooks');
expect(calledUrl).toContain('$top=1');
});
it('execute(list_notebooks) calls Graph API with OData params', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ value: [{ id: 'n1', displayName: 'Marko Notebook' }] }),
});
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
const result = await connector.execute('list_notebooks', { $top: 10, $orderby: 'displayName' });
expect(result.success).toBe(true);
const calledUrl = String(fetchMock.mock.calls[0][0]);
expect(calledUrl).toContain('/me/onenote/notebooks');
expect(calledUrl).toContain('%24top=10');
expect(calledUrl).toContain('%24orderby=displayName');
});
it('execute(list_sections) binds notebook_id into the URL path (not query)', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ value: [] }) });
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
await connector.execute('list_sections', { notebook_id: 'nb-1' });
const calledUrl = String(fetchMock.mock.calls[0][0]);
expect(calledUrl).toContain('/me/onenote/notebooks/nb-1/sections');
// notebook_id must NOT appear in the query string.
expect(calledUrl).not.toContain('notebook_id=');
});
it('execute(list_sections) errors when notebook_id is missing', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const result = await connector.execute('list_sections', {});
expect(result.success).toBe(false);
expect(result.error).toContain('notebook_id');
});
it('execute(list_pages) without section_id lists user-wide pages', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ value: [] }) });
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
await connector.execute('list_pages', { $top: 5 });
const calledUrl = String(fetchMock.mock.calls[0][0]);
expect(calledUrl).toContain('/me/onenote/pages');
// section-scoped URL must NOT appear.
expect(calledUrl).not.toContain('/sections/');
});
it('execute(list_pages) with section_id binds it into the URL path', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ value: [] }) });
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
await connector.execute('list_pages', { section_id: 's-7' });
const calledUrl = String(fetchMock.mock.calls[0][0]);
expect(calledUrl).toContain('/me/onenote/sections/s-7/pages');
expect(calledUrl).not.toContain('section_id=');
});
it('execute(get_page) returns HTML body for harvest ingestion', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const fakeHtml = '<html><body><h1>Note</h1><p>Body</p></body></html>';
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
text: async () => fakeHtml,
headers: { get: (k: string) => (k === 'content-type' ? 'text/html' : null) },
});
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
const result = await connector.execute('get_page', { page_id: 'p-1' });
expect(result.success).toBe(true);
const data = result.data as { html: string; contentType: string | null };
expect(data.html).toBe(fakeHtml);
expect(data.contentType).toBe('text/html');
const calledUrl = String(fetchMock.mock.calls[0][0]);
expect(calledUrl).toContain('/me/onenote/pages/p-1/content');
});
it('execute(get_page) appends includeIDs=true when requested', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
text: async () => '',
headers: { get: () => null },
});
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
await connector.execute('get_page', { page_id: 'p-1', includeIDs: true });
const calledUrl = String(fetchMock.mock.calls[0][0]);
expect(calledUrl).toContain('includeIDs=true');
});
it('execute(search_pages) wraps query in quotes for phrase search', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ value: [] }) });
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
await connector.execute('search_pages', { query: 'kvark roadmap', $top: 10 });
const calledUrl = String(fetchMock.mock.calls[0][0]);
expect(calledUrl).toContain('/me/onenote/pages');
// $search="kvark roadmap" — URL-encoded as %22kvark+roadmap%22 or %22kvark%20roadmap%22.
expect(calledUrl).toMatch(/%24search=%22kvark[+%20]roadmap%22/);
expect(calledUrl).toContain('%24top=10');
});
it('rejects unknown actions', async () => {
const vault = createMockVault('onenote', { value: 'graph-token', isExpired: false });
await connector.connect(vault);
const result = await connector.execute('made-up-action', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Unknown action');
});
});