mirror of
https://github.com/doum1004/llmwiki-cli.git
synced 2026-05-02 08:56:12 +02:00
- Enhance git.test.ts with tests for commit messages with special characters, log limits, diff outputs, branch retrieval, remote addition, file deletion staging, conflict detection, and file-specific logs. - Introduce github.test.ts to validate GitHub API functions including repository listing, filtering, error handling, and repository creation with various scenarios. - Expand index-manager.test.ts to cover entry addition, duplicate handling, reading from missing files, entry removal, and case-insensitive category matching. - Update links.test.ts to ensure proper link resolution, handling of self-referential links, and multiple links to the same target. - Add lint.test.ts for frontmatter edge cases, including handling empty frontmatter, invalid YAML, and ensuring parseable output. - Enhance log-manager.test.ts with tests for empty logs, filtering by type and count, log creation, timestamp format, and preserving entry formats. - Extend read-write.test.ts to skip certain directories, handle deeply nested paths, and ensure consistent path formatting across platforms. - Improve search.test.ts to escape regex characters, filter by directory, manage snippet generation, enforce default limits, and handle searches in empty wikis.
285 lines
9.6 KiB
TypeScript
285 lines
9.6 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
import { mkdtemp, rm, writeFile, mkdir } from "fs/promises";
|
|
import { join } from "path";
|
|
import { tmpdir } from "os";
|
|
import * as yaml from "js-yaml";
|
|
|
|
let configDir: string;
|
|
const origConfigDir = process.env.LLMWIKI_CONFIG_DIR;
|
|
const origFetch = globalThis.fetch;
|
|
|
|
beforeEach(async () => {
|
|
configDir = await mkdtemp(join(tmpdir(), "llmwiki-gh-"));
|
|
process.env.LLMWIKI_CONFIG_DIR = configDir;
|
|
|
|
// Save a fake auth token
|
|
await mkdir(configDir, { recursive: true });
|
|
const auth = { token: "ghp_fake123", username: "testuser", created: "2026-01-01T00:00:00.000Z" };
|
|
await writeFile(join(configDir, "auth.yaml"), yaml.dump(auth), "utf-8");
|
|
});
|
|
|
|
afterEach(async () => {
|
|
globalThis.fetch = origFetch;
|
|
await rm(configDir, { recursive: true, force: true });
|
|
if (origConfigDir) {
|
|
process.env.LLMWIKI_CONFIG_DIR = origConfigDir;
|
|
} else {
|
|
delete process.env.LLMWIKI_CONFIG_DIR;
|
|
}
|
|
});
|
|
|
|
function mockFetch(handler: (url: string, opts?: RequestInit) => Response | Promise<Response>) {
|
|
globalThis.fetch = handler as typeof fetch;
|
|
}
|
|
|
|
function jsonResponse(data: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(data), {
|
|
status,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
describe("listRepos", () => {
|
|
it("returns repos from API", async () => {
|
|
const { listRepos } = await import("../src/lib/github.ts");
|
|
const fakeRepos = [
|
|
{ name: "wiki-science", full_name: "user/wiki-science", private: true, html_url: "", clone_url: "", ssh_url: "", pushed_at: "", description: null },
|
|
{ name: "wiki-code", full_name: "user/wiki-code", private: true, html_url: "", clone_url: "", ssh_url: "", pushed_at: "", description: null },
|
|
];
|
|
mockFetch(() => jsonResponse(fakeRepos));
|
|
|
|
const repos = await listRepos();
|
|
expect(repos).toHaveLength(2);
|
|
expect(repos[0]!.name).toBe("wiki-science");
|
|
});
|
|
|
|
it("filters repos by name", async () => {
|
|
const { listRepos } = await import("../src/lib/github.ts");
|
|
const fakeRepos = [
|
|
{ name: "wiki-science", full_name: "user/wiki-science", private: true, html_url: "", clone_url: "", ssh_url: "", pushed_at: "", description: null },
|
|
{ name: "other-project", full_name: "user/other-project", private: false, html_url: "", clone_url: "", ssh_url: "", pushed_at: "", description: null },
|
|
];
|
|
mockFetch(() => jsonResponse(fakeRepos));
|
|
|
|
const repos = await listRepos({ filter: "wiki" });
|
|
expect(repos).toHaveLength(1);
|
|
expect(repos[0]!.name).toBe("wiki-science");
|
|
});
|
|
|
|
it("filter is case insensitive", async () => {
|
|
const { listRepos } = await import("../src/lib/github.ts");
|
|
const fakeRepos = [
|
|
{ name: "MyWiki", full_name: "user/MyWiki", private: true, html_url: "", clone_url: "", ssh_url: "", pushed_at: "", description: null },
|
|
];
|
|
mockFetch(() => jsonResponse(fakeRepos));
|
|
|
|
const repos = await listRepos({ filter: "mywiki" });
|
|
expect(repos).toHaveLength(1);
|
|
});
|
|
|
|
it("throws on API error", async () => {
|
|
const { listRepos } = await import("../src/lib/github.ts");
|
|
mockFetch(() => new Response("", { status: 500, statusText: "Internal Server Error" }));
|
|
|
|
try {
|
|
await listRepos();
|
|
expect(true).toBe(false);
|
|
} catch (err: unknown) {
|
|
expect((err as Error).message).toContain("GitHub API error");
|
|
}
|
|
});
|
|
|
|
it("handles empty response", async () => {
|
|
const { listRepos } = await import("../src/lib/github.ts");
|
|
mockFetch(() => jsonResponse([]));
|
|
|
|
const repos = await listRepos();
|
|
expect(repos).toHaveLength(0);
|
|
});
|
|
|
|
it("uses perPage=20 by default (not all)", async () => {
|
|
const { listRepos } = await import("../src/lib/github.ts");
|
|
let capturedUrl = "";
|
|
mockFetch((url: string) => {
|
|
capturedUrl = url;
|
|
return jsonResponse([]);
|
|
});
|
|
|
|
await listRepos();
|
|
expect(capturedUrl).toContain("per_page=20");
|
|
});
|
|
|
|
it("uses perPage=100 when all=true", async () => {
|
|
const { listRepos } = await import("../src/lib/github.ts");
|
|
let capturedUrl = "";
|
|
mockFetch((url: string) => {
|
|
capturedUrl = url;
|
|
return jsonResponse([]);
|
|
});
|
|
|
|
await listRepos({ all: true });
|
|
expect(capturedUrl).toContain("per_page=100");
|
|
});
|
|
|
|
it("stops pagination when page returns fewer than perPage", async () => {
|
|
const { listRepos } = await import("../src/lib/github.ts");
|
|
let callCount = 0;
|
|
mockFetch(() => {
|
|
callCount++;
|
|
const repos = Array.from({ length: 5 }, (_, i) => ({
|
|
name: `repo-${i}`, full_name: `user/repo-${i}`, private: true,
|
|
html_url: "", clone_url: "", ssh_url: "", pushed_at: "", description: null,
|
|
}));
|
|
return jsonResponse(repos);
|
|
});
|
|
|
|
const repos = await listRepos({ all: true });
|
|
expect(repos).toHaveLength(5);
|
|
expect(callCount).toBe(1); // Should not paginate since 5 < 100
|
|
});
|
|
});
|
|
|
|
describe("getRepo", () => {
|
|
it("returns repo data on success", async () => {
|
|
const { getRepo } = await import("../src/lib/github.ts");
|
|
const fakeRepo = {
|
|
name: "wiki-test", full_name: "user/wiki-test", private: true,
|
|
html_url: "https://github.com/user/wiki-test", clone_url: "", ssh_url: "",
|
|
pushed_at: "2026-01-01", description: "test",
|
|
};
|
|
mockFetch(() => jsonResponse(fakeRepo));
|
|
|
|
const repo = await getRepo("user", "wiki-test");
|
|
expect(repo).not.toBeNull();
|
|
expect(repo!.name).toBe("wiki-test");
|
|
});
|
|
|
|
it("returns null on 404", async () => {
|
|
const { getRepo } = await import("../src/lib/github.ts");
|
|
mockFetch(() => new Response("", { status: 404 }));
|
|
|
|
const repo = await getRepo("user", "nonexistent");
|
|
expect(repo).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("createRepo", () => {
|
|
it("creates repo successfully", async () => {
|
|
const { createRepo } = await import("../src/lib/github.ts");
|
|
const fakeRepo = {
|
|
name: "wiki-new", full_name: "user/wiki-new", private: true,
|
|
html_url: "", clone_url: "", ssh_url: "", pushed_at: "", description: "LLM Wiki",
|
|
};
|
|
mockFetch(() => jsonResponse(fakeRepo, 201));
|
|
|
|
const repo = await createRepo("wiki-new");
|
|
expect(repo.name).toBe("wiki-new");
|
|
});
|
|
|
|
it("throws specific error for duplicate name (422)", async () => {
|
|
const { createRepo } = await import("../src/lib/github.ts");
|
|
mockFetch(() => jsonResponse(
|
|
{ message: "Repository creation failed. name already exists on this account" },
|
|
422,
|
|
));
|
|
|
|
try {
|
|
await createRepo("existing-repo");
|
|
expect(true).toBe(false);
|
|
} catch (err: unknown) {
|
|
expect((err as Error).message).toContain("already exists");
|
|
}
|
|
});
|
|
|
|
it("throws auth error for 401", async () => {
|
|
const { createRepo } = await import("../src/lib/github.ts");
|
|
mockFetch(() => jsonResponse({ message: "Bad credentials" }, 401));
|
|
|
|
try {
|
|
await createRepo("new-repo");
|
|
expect(true).toBe(false);
|
|
} catch (err: unknown) {
|
|
expect((err as Error).message).toContain("Authentication failed");
|
|
}
|
|
});
|
|
|
|
it("throws permission error for 403", async () => {
|
|
const { createRepo } = await import("../src/lib/github.ts");
|
|
mockFetch(() => jsonResponse({ message: "Forbidden" }, 403));
|
|
|
|
try {
|
|
await createRepo("new-repo");
|
|
expect(true).toBe(false);
|
|
} catch (err: unknown) {
|
|
expect((err as Error).message).toContain("Permission denied");
|
|
}
|
|
});
|
|
|
|
it("uses default options (private, LLM Wiki description)", async () => {
|
|
const { createRepo } = await import("../src/lib/github.ts");
|
|
let capturedBody: string = "";
|
|
mockFetch(async (_url, opts) => {
|
|
capturedBody = opts?.body as string ?? "";
|
|
return jsonResponse({
|
|
name: "wiki-new", full_name: "user/wiki-new", private: true,
|
|
html_url: "", clone_url: "", ssh_url: "", pushed_at: "", description: "LLM Wiki",
|
|
}, 201);
|
|
});
|
|
|
|
await createRepo("wiki-new");
|
|
const parsed = JSON.parse(capturedBody);
|
|
expect(parsed.private).toBe(true);
|
|
expect(parsed.description).toBe("LLM Wiki");
|
|
expect(parsed.auto_init).toBe(false);
|
|
});
|
|
|
|
it("respects custom options", async () => {
|
|
const { createRepo } = await import("../src/lib/github.ts");
|
|
let capturedBody: string = "";
|
|
mockFetch(async (_url, opts) => {
|
|
capturedBody = opts?.body as string ?? "";
|
|
return jsonResponse({
|
|
name: "wiki-pub", full_name: "user/wiki-pub", private: false,
|
|
html_url: "", clone_url: "", ssh_url: "", pushed_at: "", description: "My wiki",
|
|
}, 201);
|
|
});
|
|
|
|
await createRepo("wiki-pub", { private: false, description: "My wiki" });
|
|
const parsed = JSON.parse(capturedBody);
|
|
expect(parsed.private).toBe(false);
|
|
expect(parsed.description).toBe("My wiki");
|
|
});
|
|
|
|
it("includes error details in generic failures", async () => {
|
|
const { createRepo } = await import("../src/lib/github.ts");
|
|
mockFetch(() => jsonResponse(
|
|
{ message: "Validation failed", errors: [{ message: "name is invalid" }] },
|
|
422,
|
|
));
|
|
|
|
try {
|
|
await createRepo("bad--name");
|
|
expect(true).toBe(false);
|
|
} catch (err: unknown) {
|
|
expect((err as Error).message).toContain("name is invalid");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("githubFetch auth headers", () => {
|
|
it("includes Authorization header with token", async () => {
|
|
const { listRepos } = await import("../src/lib/github.ts");
|
|
let capturedHeaders: Record<string, string> = {};
|
|
mockFetch((_url, opts) => {
|
|
capturedHeaders = Object.fromEntries(
|
|
Object.entries(opts?.headers ?? {}).map(([k, v]) => [k, String(v)])
|
|
);
|
|
return jsonResponse([]);
|
|
});
|
|
|
|
await listRepos();
|
|
expect(capturedHeaders["Authorization"]).toBe("Bearer ghp_fake123");
|
|
expect(capturedHeaders["Accept"]).toContain("github");
|
|
});
|
|
});
|