diff --git a/tests/unit/code-chunker.test.ts b/tests/unit/code-chunker.test.ts index 482bb0c..28a09b0 100644 --- a/tests/unit/code-chunker.test.ts +++ b/tests/unit/code-chunker.test.ts @@ -2,6 +2,60 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { TreeSitterChunker } from "../../src/lite/chunker-treesitter.js"; import { ValidationError } from "../../src/errors.js"; +interface MockNode { + type: string; + text: string; + startPosition: { row: number; column: number }; + endPosition: { row: number; column: number }; + childCount: number; + child: (i: number) => MockNode | null; + namedChildCount: number; + namedChild: (i: number) => MockNode | null; +} + +/** + * Helper: create a mock TSNode that simulates tree-sitter node shape. + */ +function makeMockNode( + type: string, + text: string, + startRow: number, + endRow: number, + children: MockNode[] = [], +): MockNode { + return { + type, + text, + startPosition: { row: startRow, column: 0 }, + endPosition: { row: endRow, column: 0 }, + childCount: children.length, + child: (i: number) => children[i] ?? null, + namedChildCount: children.length, + namedChild: (i: number) => children[i] ?? null, + }; +} + +/** + * Create a chunker with mocked tree-sitter internals for testing + * the algorithm without requiring tree-sitter to be installed. + */ +function createMockedChunker(rootChildren: ReturnType[]): TreeSitterChunker { + const instance = new TreeSitterChunker(); + + const rootNode = makeMockNode("program", "", 0, 100, rootChildren); + + // Mock the private getParser and loadGrammar methods + // @ts-expect-error — accessing private method for testing + instance.getParser = vi.fn().mockResolvedValue({ + setLanguage: vi.fn(), + parse: vi.fn().mockReturnValue({ rootNode }), + }); + // @ts-expect-error — accessing private method for testing + instance.loadGrammar = vi.fn().mockResolvedValue({}); + + return instance; +} + describe("TreeSitterChunker", () => { let chunker: TreeSitterChunker; @@ -73,62 +127,6 @@ describe("TreeSitterChunker", () => { }); describe("chunk() — with mocked tree-sitter", () => { - interface MockNode { - type: string; - text: string; - startPosition: { row: number; column: number }; - endPosition: { row: number; column: number }; - childCount: number; - child: (i: number) => MockNode | null; - namedChildCount: number; - namedChild: (i: number) => MockNode | null; - } - - /** - * Helper: create a mock TSNode that simulates tree-sitter node shape. - */ - function makeMockNode( - type: string, - text: string, - startRow: number, - endRow: number, - children: MockNode[] = [], - ): MockNode { - return { - type, - text, - startPosition: { row: startRow, column: 0 }, - endPosition: { row: endRow, column: 0 }, - childCount: children.length, - child: (i: number) => children[i] ?? null, - namedChildCount: children.length, - namedChild: (i: number) => children[i] ?? null, - }; - } - - /** - * Create a chunker with mocked tree-sitter internals for testing - * the algorithm without requiring tree-sitter to be installed. - */ - function createMockedChunker( - rootChildren: ReturnType[], - ): TreeSitterChunker { - const instance = new TreeSitterChunker(); - - const rootNode = makeMockNode("program", "", 0, 100, rootChildren); - - // Mock the private getParser and loadGrammar methods - // @ts-expect-error — accessing private method for testing - instance.getParser = vi.fn().mockResolvedValue({ - setLanguage: vi.fn(), - parse: vi.fn().mockReturnValue({ rootNode }), - }); - // @ts-expect-error — accessing private method for testing - instance.loadGrammar = vi.fn().mockResolvedValue({}); - - return instance; - } - it("should chunk TypeScript code at function boundaries", async () => { const importNode = makeMockNode("import_statement", 'import { foo } from "bar";', 0, 0); const fn1 = makeMockNode( diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index 2a54616..fa34f2e 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -71,6 +71,22 @@ describe("config", () => { }); }); +function makeConfig(overrides: Partial = {}): LibScopeConfig { + return { + embedding: { + provider: "local", + ollamaUrl: "http://localhost:11434", + ollamaModel: "nomic-embed-text", + openaiModel: "text-embedding-3-small", + ...overrides.embedding, + }, + database: { path: "/tmp/test-libscope/libscope.db", ...overrides.database }, + indexing: { maxDocumentSize: 100 * 1024 * 1024, ...overrides.indexing }, + logging: { level: "info", ...overrides.logging }, + ...("llm" in overrides ? { llm: overrides.llm } : {}), + }; +} + describe("validateConfig", () => { let warnSpy: ReturnType; const savedEnv: Record = {}; @@ -104,22 +120,6 @@ describe("validateConfig", () => { } }); - function makeConfig(overrides: Partial = {}): LibScopeConfig { - return { - embedding: { - provider: "local", - ollamaUrl: "http://localhost:11434", - ollamaModel: "nomic-embed-text", - openaiModel: "text-embedding-3-small", - ...overrides.embedding, - }, - database: { path: "/tmp/test-libscope/libscope.db", ...overrides.database }, - indexing: { maxDocumentSize: 100 * 1024 * 1024, ...overrides.indexing }, - logging: { level: "info", ...overrides.logging }, - ...("llm" in overrides ? { llm: overrides.llm } : {}), - }; - } - it("should pass validation silently for local provider", () => { const config = makeConfig(); const warnings = validateConfig(config); diff --git a/tests/unit/confluence.test.ts b/tests/unit/confluence.test.ts index 3afbad2..91d6b4b 100644 --- a/tests/unit/confluence.test.ts +++ b/tests/unit/confluence.test.ts @@ -46,6 +46,26 @@ function makePageDetail(overrides: Record = {}) { }; } +function mockFetchResponse(body: unknown, ok = true, status = 200): Response { + return { + ok, + status, + statusText: ok ? "OK" : "Error", + json: () => Promise.resolve(body), + text: () => Promise.resolve(JSON.stringify(body)), + headers: new Headers(), + redirected: false, + type: "basic", + url: "", + clone: () => mockFetchResponse(body, ok, status), + body: null, + bodyUsed: false, + arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), + blob: () => Promise.resolve(new Blob()), + formData: () => Promise.resolve(new FormData()), + } as Response; +} + describe("Confluence connector", () => { let db: Database.Database; let provider: MockEmbeddingProvider; @@ -63,26 +83,6 @@ describe("Confluence connector", () => { db.close(); }); - function mockFetchResponse(body: unknown, ok = true, status = 200): Response { - return { - ok, - status, - statusText: ok ? "OK" : "Error", - json: () => Promise.resolve(body), - text: () => Promise.resolve(JSON.stringify(body)), - headers: new Headers(), - redirected: false, - type: "basic", - url: "", - clone: () => mockFetchResponse(body, ok, status), - body: null, - bodyUsed: false, - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - blob: () => Promise.resolve(new Blob()), - formData: () => Promise.resolve(new FormData()), - } as Response; - } - const baseConfig: ConfluenceConfig = { baseUrl: "https://acme.atlassian.net", email: "user@example.com", diff --git a/tests/unit/onenote.test.ts b/tests/unit/onenote.test.ts index 4c12337..81d29b6 100644 --- a/tests/unit/onenote.test.ts +++ b/tests/unit/onenote.test.ts @@ -59,6 +59,37 @@ function makeConfig(overrides?: Partial): OneNoteConfig { }; } +function setupGraphMocks( + notebooks: Array<{ id: string; displayName: string }> = [ + { id: "nb1", displayName: "Work Notebook" }, + ], + sections: Array<{ id: string; displayName: string }> = [ + { id: "sec1", displayName: "Project Notes" }, + ], + pages: Array<{ id: string; title: string; lastModifiedDateTime: string }> = [ + { id: "pg1", title: "Meeting Notes", lastModifiedDateTime: "2024-01-15T10:00:00Z" }, + ], + pageHtml: string = "

Meeting Notes

Discussion points

", +): void { + mockFetch((url: string, init?: RequestInit) => { + const accept = (init?.headers as Record)?.Accept ?? ""; + + if (url.includes("/me/onenote/notebooks") && !url.includes("/sections")) { + return jsonResponse({ value: notebooks }); + } + if (url.includes("/sections") && !url.includes("/pages")) { + return jsonResponse({ value: sections }); + } + if (url.includes("/pages") && !url.includes("/content")) { + return jsonResponse({ value: pages }); + } + if (url.includes("/content") || accept === "text/html") { + return htmlResponse(pageHtml); + } + return new Response("Not found", { status: 404 }); + }); +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -253,37 +284,6 @@ describe("OneNote Connector", () => { // ------------------------------------------------------------------------- describe("syncOneNote", () => { - function setupGraphMocks( - notebooks: Array<{ id: string; displayName: string }> = [ - { id: "nb1", displayName: "Work Notebook" }, - ], - sections: Array<{ id: string; displayName: string }> = [ - { id: "sec1", displayName: "Project Notes" }, - ], - pages: Array<{ id: string; title: string; lastModifiedDateTime: string }> = [ - { id: "pg1", title: "Meeting Notes", lastModifiedDateTime: "2024-01-15T10:00:00Z" }, - ], - pageHtml: string = "

Meeting Notes

Discussion points

", - ): void { - mockFetch((url: string, init?: RequestInit) => { - const accept = (init?.headers as Record)?.Accept ?? ""; - - if (url.includes("/me/onenote/notebooks") && !url.includes("/sections")) { - return jsonResponse({ value: notebooks }); - } - if (url.includes("/sections") && !url.includes("/pages")) { - return jsonResponse({ value: sections }); - } - if (url.includes("/pages") && !url.includes("/content")) { - return jsonResponse({ value: pages }); - } - if (url.includes("/content") || accept === "text/html") { - return htmlResponse(pageHtml); - } - return new Response("Not found", { status: 404 }); - }); - } - it("performs full sync creating topics and indexing pages", async () => { setupGraphMocks(); diff --git a/tests/unit/slack.test.ts b/tests/unit/slack.test.ts index 3f5bdde..79a53d5 100644 --- a/tests/unit/slack.test.ts +++ b/tests/unit/slack.test.ts @@ -82,6 +82,26 @@ describe("convertSlackMrkdwn", () => { }); }); +function setupChannelList(channels: Array<{ id: string; name: string }> = []): void { + mockFetch.mockImplementationOnce(() => + Promise.resolve(slackOk({ channels, response_metadata: {} })), + ); +} + +function setupMessages(messages: Array> = []): void { + mockFetch.mockImplementationOnce(() => + Promise.resolve(slackOk({ messages, response_metadata: {} })), + ); +} + +function setupUserInfo(user: Record): void { + mockFetch.mockImplementationOnce(() => Promise.resolve(slackOk({ user }))); +} + +function setupThreadReplies(messages: Array> = []): void { + mockFetch.mockImplementationOnce(() => Promise.resolve(slackOk({ messages }))); +} + describe("syncSlack", () => { let db: Database.Database; let provider: MockEmbeddingProvider; @@ -104,26 +124,6 @@ describe("syncSlack", () => { threadMode: "aggregate", }; - function setupChannelList(channels: Array<{ id: string; name: string }> = []): void { - mockFetch.mockImplementationOnce(() => - Promise.resolve(slackOk({ channels, response_metadata: {} })), - ); - } - - function setupMessages(messages: Array> = []): void { - mockFetch.mockImplementationOnce(() => - Promise.resolve(slackOk({ messages, response_metadata: {} })), - ); - } - - function setupUserInfo(user: Record): void { - mockFetch.mockImplementationOnce(() => Promise.resolve(slackOk({ user }))); - } - - function setupThreadReplies(messages: Array> = []): void { - mockFetch.mockImplementationOnce(() => Promise.resolve(slackOk({ messages }))); - } - it("lists and filters channels", async () => { setupChannelList([ { id: "C001", name: "general" },