import { mkdtempSync, rmSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import type { ToolContext, ToolResult } from './core.js'; // The cache DB path is resolved from process.cwd() at module load, so stub // cwd BEFORE importing the module to keep the sqlite cache in a temp dir. const tmpRoot = mkdtempSync(join(tmpdir(), 'ms-learn-test-')); let executeTool: (n: string, i: Record, c: ToolContext) => Promise; let TOOL_DEFS: Record; beforeAll(async () => { vi.spyOn(process, 'cwd').mockReturnValue(tmpRoot); const mod = await import('./ms-learn.js'); executeTool = mod.executeTool; TOOL_DEFS = mod.TOOL_DEFS; }); afterAll(() => { rmSync(tmpRoot, { recursive: true, force: true }); }); afterEach(() => { vi.unstubAllGlobals(); }); const ctx = {} as ToolContext; function htmlResponse(body: string, status = 200): Response { return new Response(body, { status, headers: { 'content-type': 'text/html' } }); } function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' }, }); } const LEARN_HTML = ` Durable Functions overview | Microsoft Learn

Durable Functions overview

Durable Functions is an & extension of Azure Functions.

Patterns

  • Function chaining
  • Fan-out/fan-in
var x = 1;

See related docs.

footer chrome
`; describe('TOOL_DEFS', () => { it('registers the four Learn tools', () => { expect(Object.keys(TOOL_DEFS).sort()).toEqual([ 'FetchMicrosoftLearn', 'RefreshMicrosoftLearnCache', 'SearchMicrosoftLearn', 'SearchMicrosoftLearnCache', ]); }); }); describe('executeTool', () => { it('returns null for unknown tool names', async () => { expect(await executeTool('NotALearnTool', {}, ctx)).toBeNull(); }); }); describe('FetchMicrosoftLearn', () => { it('requires a url', async () => { const res = await executeTool('FetchMicrosoftLearn', {}, ctx); expect(res?.isError).toBe(true); expect(res?.output).toContain('url is required'); }); it('rejects URLs outside learn.microsoft.com', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); const res = await executeTool('FetchMicrosoftLearn', { url: 'https://evil.example/docs' }, ctx); expect(res?.isError).toBe(true); expect(res?.output).toContain('not on learn.microsoft.com'); expect(fetchMock).not.toHaveBeenCalled(); }); it('fetches a page, converts to markdown, and caches it', async () => { vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => htmlResponse(LEARN_HTML))); const res = await executeTool( 'FetchMicrosoftLearn', { url: 'https://learn.microsoft.com/en-us/azure/durable?view=latest#anchor' }, ctx, ); expect(res?.isError).toBe(false); expect(res?.output).toContain('Fetched and cached'); // Title cleaned of the "| Microsoft Learn" suffix. expect(res?.output).toContain('# Durable Functions overview'); // Entity decoding, headings, lists, fenced code, links survive. expect(res?.output).toContain('an & extension'); expect(res?.output).toContain('## Patterns'); expect(res?.output).toContain('- Function chaining'); expect(res?.output).toContain('```csharp\nvar x = 1;\n```'); expect(res?.output).toContain('[related docs](https://learn.microsoft.com/en-us/azure/other)'); // Chrome outside
is dropped. expect(res?.output).not.toContain('site nav chrome'); expect(res?.output).not.toContain('footer chrome'); }); it('serves the cached copy (query/hash canonicalized away) without refetching', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); const res = await executeTool( 'FetchMicrosoftLearn', { url: 'https://learn.microsoft.com/en-us/azure/durable?other=param' }, ctx, ); expect(res?.isError).toBe(false); expect(res?.output).toContain('Cached (age='); expect(res?.output).toContain('Durable Functions overview'); expect(fetchMock).not.toHaveBeenCalled(); }); it('reports HTTP errors as tool errors', async () => { vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => htmlResponse('nope', 404))); const res = await executeTool( 'FetchMicrosoftLearn', { url: 'https://learn.microsoft.com/en-us/azure/missing' }, ctx, ); expect(res?.isError).toBe(true); expect(res?.output).toContain('HTTP 404'); }); }); describe('SearchMicrosoftLearnCache', () => { it('requires a query', async () => { const res = await executeTool('SearchMicrosoftLearnCache', {}, ctx); expect(res?.isError).toBe(true); }); it('finds previously cached pages via FTS', async () => { const res = await executeTool('SearchMicrosoftLearnCache', { query: 'durable functions' }, ctx); expect(res?.isError).toBe(false); expect(res?.output).toContain('Cache hits'); expect(res?.output).toContain('https://learn.microsoft.com/en-us/azure/durable'); }); it('reports zero hits without error', async () => { const res = await executeTool('SearchMicrosoftLearnCache', { query: 'zzz-no-such-term' }, ctx); expect(res?.isError).toBe(false); expect(res?.output).toContain('No cache hits'); }); }); describe('RefreshMicrosoftLearnCache', () => { it('force-refetches and overwrites the cache', async () => { const updated = LEARN_HTML.replace('an & extension', 'a refreshed extension'); vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => htmlResponse(updated))); const res = await executeTool( 'RefreshMicrosoftLearnCache', { url: 'https://learn.microsoft.com/en-us/azure/durable' }, ctx, ); expect(res?.isError).toBe(false); expect(res?.output).toContain('Refreshed'); const cached = await executeTool( 'FetchMicrosoftLearn', { url: 'https://learn.microsoft.com/en-us/azure/durable' }, ctx, ); expect(cached?.output).toContain('a refreshed extension'); }); it('surfaces refresh failures', async () => { vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('socket hang up'))); const res = await executeTool( 'RefreshMicrosoftLearnCache', { url: 'https://learn.microsoft.com/en-us/azure/durable' }, ctx, ); expect(res?.isError).toBe(true); expect(res?.output).toContain('socket hang up'); }); }); describe('SearchMicrosoftLearn', () => { it('requires a query', async () => { const res = await executeTool('SearchMicrosoftLearn', {}, ctx); expect(res?.isError).toBe(true); }); it('merges online results with cache hits and clamps top to 25', async () => { const fetchMock = vi.fn().mockImplementation(async () => jsonResponse({ results: [ { title: 'Durable Functions overview', url: 'https://learn.microsoft.com/en-us/azure/durable', description: 'desc' }, { title: 'broken', description: 'no url, filtered out' }, ], }), ); vi.stubGlobal('fetch', fetchMock); const res = await executeTool( 'SearchMicrosoftLearn', { query: 'durable functions', top: 100, products: ['azure'] }, ctx, ); expect(res?.isError).toBe(false); expect(res?.output).toContain('## Online results (1)'); // The cached page is flagged. expect(res?.output).toContain('[cached]'); expect(res?.output).toContain('## Cache hits'); const calledUrl = String(fetchMock.mock.calls[0]?.[0]); expect(calledUrl).toContain('%24top=25'); expect(calledUrl).toContain('products=azure'); }); it('falls back to cache-only results when the online API fails', async () => { vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => jsonResponse({}, 503))); const res = await executeTool('SearchMicrosoftLearn', { query: 'durable functions' }, ctx); expect(res?.isError).toBe(false); expect(res?.output).toContain('## Cache hits'); expect(res?.output).toContain('online search failed'); }); it('errors when online fails and the cache has nothing', async () => { vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => jsonResponse({}, 503))); const res = await executeTool('SearchMicrosoftLearn', { query: 'zzz-no-such-term' }, ctx); expect(res?.isError).toBe(true); expect(res?.output).toContain('Search failed'); }); it('reports no results without error when both sources are empty', async () => { vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => jsonResponse({ results: [] }))); const res = await executeTool('SearchMicrosoftLearn', { query: 'zzz-no-such-term' }, ctx); expect(res?.isError).toBe(false); expect(res?.output).toContain('No results'); }); });