Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 54 additions & 56 deletions tests/unit/code-chunker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof makeMockNode>[]): 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;

Expand Down Expand Up @@ -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<typeof makeMockNode>[],
): 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(
Expand Down
32 changes: 16 additions & 16 deletions tests/unit/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ describe("config", () => {
});
});

function makeConfig(overrides: Partial<LibScopeConfig> = {}): 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<typeof vi.fn>;
const savedEnv: Record<string, string | undefined> = {};
Expand Down Expand Up @@ -104,22 +120,6 @@ describe("validateConfig", () => {
}
});

function makeConfig(overrides: Partial<LibScopeConfig> = {}): 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);
Expand Down
40 changes: 20 additions & 20 deletions tests/unit/confluence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
import type { ConfluenceConfig } from "../../src/connectors/confluence.js";
import type Database from "better-sqlite3";

function makeSpacesResponse(spaces: Array<{ id: string; key: string; name: string }>) {

Check warning on line 14 in tests/unit/confluence.test.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Missing return type on function
return { results: spaces, _links: {} };
}

function makePagesResponse(

Check warning on line 18 in tests/unit/confluence.test.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Missing return type on function
pages: Array<{
id: string;
title: string;
Expand All @@ -33,7 +33,7 @@
};
}

function makePageDetail(overrides: Record<string, unknown> = {}) {

Check warning on line 36 in tests/unit/confluence.test.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Missing return type on function
return {
id: "page-1",
title: "Test Page",
Expand All @@ -46,6 +46,26 @@
};
}

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;
Expand All @@ -63,26 +83,6 @@
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",
Expand Down
62 changes: 31 additions & 31 deletions tests/unit/onenote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,37 @@ function makeConfig(overrides?: Partial<OneNoteConfig>): 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 = "<h1>Meeting Notes</h1><p>Discussion points</p>",
): void {
mockFetch((url: string, init?: RequestInit) => {
const accept = (init?.headers as Record<string, string>)?.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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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 = "<h1>Meeting Notes</h1><p>Discussion points</p>",
): void {
mockFetch((url: string, init?: RequestInit) => {
const accept = (init?.headers as Record<string, string>)?.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();

Expand Down
40 changes: 20 additions & 20 deletions tests/unit/slack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown>> = []): void {
mockFetch.mockImplementationOnce(() =>
Promise.resolve(slackOk({ messages, response_metadata: {} })),
);
}

function setupUserInfo(user: Record<string, unknown>): void {
mockFetch.mockImplementationOnce(() => Promise.resolve(slackOk({ user })));
}

function setupThreadReplies(messages: Array<Record<string, unknown>> = []): void {
mockFetch.mockImplementationOnce(() => Promise.resolve(slackOk({ messages })));
}

describe("syncSlack", () => {
let db: Database.Database;
let provider: MockEmbeddingProvider;
Expand All @@ -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<Record<string, unknown>> = []): void {
mockFetch.mockImplementationOnce(() =>
Promise.resolve(slackOk({ messages, response_metadata: {} })),
);
}

function setupUserInfo(user: Record<string, unknown>): void {
mockFetch.mockImplementationOnce(() => Promise.resolve(slackOk({ user })));
}

function setupThreadReplies(messages: Array<Record<string, unknown>> = []): void {
mockFetch.mockImplementationOnce(() => Promise.resolve(slackOk({ messages })));
}

it("lists and filters channels", async () => {
setupChannelList([
{ id: "C001", name: "general" },
Expand Down
Loading