Skip to main content
Glama
init.test.ts25.4 kB
/** * Tests for initProject helper functions * Tests stack detection, git scanning, documentation scanning, code patterns */ import { describe, expect, test } from "bun:test"; describe("initProject", () => { describe("InitInput validation", () => { function isValidScanCommits(value: number): boolean { return value >= 10 && value <= 2000; } test("validates minimum scan commits", () => { expect(isValidScanCommits(10)).toBe(true); }); test("validates maximum scan commits", () => { expect(isValidScanCommits(2000)).toBe(true); }); test("rejects below minimum", () => { expect(isValidScanCommits(5)).toBe(false); }); test("rejects above maximum", () => { expect(isValidScanCommits(3000)).toBe(false); }); test("validates default value", () => { expect(isValidScanCommits(500)).toBe(true); }); }); describe("InitResult structure", () => { interface InitResult { configCreated: boolean; memoriesCreated: number; decisions: number; solutions: number; patterns: number; notes: number; architecture: number; scannedFiles: number; scannedCommits: number; } function createDefaultResult(): InitResult { return { configCreated: false, memoriesCreated: 0, decisions: 0, solutions: 0, patterns: 0, notes: 0, architecture: 0, scannedFiles: 0, scannedCommits: 0, }; } test("creates default result", () => { const result = createDefaultResult(); expect(result.configCreated).toBe(false); expect(result.memoriesCreated).toBe(0); }); test("all counts start at zero", () => { const result = createDefaultResult(); expect(result.decisions).toBe(0); expect(result.solutions).toBe(0); expect(result.patterns).toBe(0); expect(result.notes).toBe(0); expect(result.architecture).toBe(0); }); }); describe("runtime detection", () => { function detectRuntime(lockFiles: string[]): string | null { if (lockFiles.includes("bun.lockb") || lockFiles.includes("bun.lock")) { return "Bun"; } if (lockFiles.includes("pnpm-lock.yaml")) { return "Node.js (pnpm)"; } if (lockFiles.includes("yarn.lock")) { return "Node.js (yarn)"; } if (lockFiles.includes("package-lock.json")) { return "Node.js (npm)"; } if (lockFiles.includes("deno.json")) { return "Deno"; } return null; } test("detects Bun from bun.lockb", () => { expect(detectRuntime(["bun.lockb"])).toBe("Bun"); }); test("detects Bun from bun.lock", () => { expect(detectRuntime(["bun.lock"])).toBe("Bun"); }); test("detects pnpm", () => { expect(detectRuntime(["pnpm-lock.yaml"])).toBe("Node.js (pnpm)"); }); test("detects yarn", () => { expect(detectRuntime(["yarn.lock"])).toBe("Node.js (yarn)"); }); test("detects npm", () => { expect(detectRuntime(["package-lock.json"])).toBe("Node.js (npm)"); }); test("detects Deno", () => { expect(detectRuntime(["deno.json"])).toBe("Deno"); }); test("returns null for no lock files", () => { expect(detectRuntime([])).toBeNull(); }); test("prioritizes Bun over npm", () => { expect(detectRuntime(["bun.lockb", "package-lock.json"])).toBe("Bun"); }); }); describe("framework detection", () => { function detectFramework( deps: string[], hasAppDir: boolean, ): string | null { if (deps.includes("next")) { return hasAppDir ? "Next.js (App Router)" : "Next.js (Pages Router)"; } if (deps.includes("nuxt")) return "Nuxt"; if ( deps.includes("@remix-run/react") || deps.includes("@remix-run/node") ) { return "Remix"; } if (deps.includes("@sveltejs/kit")) return "SvelteKit"; if (deps.includes("svelte")) return "Svelte"; if (deps.includes("astro")) return "Astro"; if (deps.includes("@angular/core")) return "Angular"; if (deps.includes("vue")) return "Vue"; if (deps.includes("react")) return "React"; if (deps.includes("hono")) return "Hono"; if (deps.includes("fastify")) return "Fastify"; if (deps.includes("express")) return "Express"; if (deps.includes("@nestjs/core")) return "NestJS"; if (deps.includes("elysia")) return "Elysia"; return null; } test("detects Next.js App Router", () => { expect(detectFramework(["next"], true)).toBe("Next.js (App Router)"); }); test("detects Next.js Pages Router", () => { expect(detectFramework(["next"], false)).toBe("Next.js (Pages Router)"); }); test("detects Remix", () => { expect(detectFramework(["@remix-run/react"], false)).toBe("Remix"); }); test("detects SvelteKit", () => { expect(detectFramework(["@sveltejs/kit"], false)).toBe("SvelteKit"); }); test("detects base Svelte", () => { expect(detectFramework(["svelte"], false)).toBe("Svelte"); }); test("detects Vue", () => { expect(detectFramework(["vue"], false)).toBe("Vue"); }); test("detects React", () => { expect(detectFramework(["react"], false)).toBe("React"); }); test("detects Express", () => { expect(detectFramework(["express"], false)).toBe("Express"); }); test("detects Hono", () => { expect(detectFramework(["hono"], false)).toBe("Hono"); }); test("detects Elysia", () => { expect(detectFramework(["elysia"], false)).toBe("Elysia"); }); test("returns null for no framework", () => { expect(detectFramework([], false)).toBeNull(); }); }); describe("database detection", () => { function detectDatabase(deps: string[]): string | null { if (deps.includes("prisma") || deps.includes("@prisma/client")) { return "Prisma"; } if (deps.includes("drizzle-orm")) return "Drizzle"; if (deps.includes("typeorm")) return "TypeORM"; if (deps.includes("sequelize")) return "Sequelize"; if (deps.includes("mongoose")) return "Mongoose (MongoDB)"; if (deps.includes("@libsql/client") || deps.includes("libsql")) { return "LibSQL/Turso"; } if (deps.includes("better-sqlite3") || deps.includes("sql.js")) { return "SQLite"; } if (deps.includes("pg") || deps.includes("postgres")) return "PostgreSQL"; if (deps.includes("mysql2") || deps.includes("mysql")) return "MySQL"; if (deps.includes("redis") || deps.includes("ioredis")) return "Redis"; if (deps.includes("@supabase/supabase-js")) return "Supabase"; if (deps.includes("firebase") || deps.includes("firebase-admin")) { return "Firebase"; } return null; } test("detects Prisma", () => { expect(detectDatabase(["prisma"])).toBe("Prisma"); }); test("detects Prisma client", () => { expect(detectDatabase(["@prisma/client"])).toBe("Prisma"); }); test("detects Drizzle", () => { expect(detectDatabase(["drizzle-orm"])).toBe("Drizzle"); }); test("detects TypeORM", () => { expect(detectDatabase(["typeorm"])).toBe("TypeORM"); }); test("detects Mongoose", () => { expect(detectDatabase(["mongoose"])).toBe("Mongoose (MongoDB)"); }); test("detects LibSQL", () => { expect(detectDatabase(["@libsql/client"])).toBe("LibSQL/Turso"); }); test("detects SQLite", () => { expect(detectDatabase(["better-sqlite3"])).toBe("SQLite"); }); test("detects PostgreSQL", () => { expect(detectDatabase(["pg"])).toBe("PostgreSQL"); }); test("detects Redis", () => { expect(detectDatabase(["redis"])).toBe("Redis"); }); test("detects Supabase", () => { expect(detectDatabase(["@supabase/supabase-js"])).toBe("Supabase"); }); test("returns null for no database", () => { expect(detectDatabase([])).toBeNull(); }); }); describe("auth detection", () => { function detectAuth(deps: string[]): string | null { if (deps.includes("better-auth")) return "Better Auth"; if (deps.includes("next-auth") || deps.includes("@auth/core")) { return "Auth.js (NextAuth)"; } if ( deps.includes("@clerk/nextjs") || deps.includes("@clerk/clerk-sdk-node") ) { return "Clerk"; } if (deps.includes("lucia")) return "Lucia"; if (deps.includes("@supabase/auth-helpers-nextjs")) return "Supabase Auth"; if (deps.includes("passport")) return "Passport.js"; if (deps.includes("@kinde-oss/kinde-auth-nextjs")) return "Kinde"; if (deps.includes("auth0")) return "Auth0"; return null; } test("detects Better Auth", () => { expect(detectAuth(["better-auth"])).toBe("Better Auth"); }); test("detects NextAuth", () => { expect(detectAuth(["next-auth"])).toBe("Auth.js (NextAuth)"); }); test("detects Clerk", () => { expect(detectAuth(["@clerk/nextjs"])).toBe("Clerk"); }); test("detects Lucia", () => { expect(detectAuth(["lucia"])).toBe("Lucia"); }); test("detects Passport", () => { expect(detectAuth(["passport"])).toBe("Passport.js"); }); test("detects Auth0", () => { expect(detectAuth(["auth0"])).toBe("Auth0"); }); test("returns null for no auth", () => { expect(detectAuth([])).toBeNull(); }); }); describe("testing detection", () => { function detectTesting(deps: string[]): string | null { if (deps.includes("vitest")) return "Vitest"; if (deps.includes("jest")) return "Jest"; if (deps.includes("@playwright/test")) return "Playwright"; if (deps.includes("cypress")) return "Cypress"; if (deps.includes("@testing-library/react")) { return "React Testing Library"; } if (deps.includes("mocha")) return "Mocha"; if (deps.includes("ava")) return "AVA"; return null; } test("detects Vitest", () => { expect(detectTesting(["vitest"])).toBe("Vitest"); }); test("detects Jest", () => { expect(detectTesting(["jest"])).toBe("Jest"); }); test("detects Playwright", () => { expect(detectTesting(["@playwright/test"])).toBe("Playwright"); }); test("detects Cypress", () => { expect(detectTesting(["cypress"])).toBe("Cypress"); }); test("detects Mocha", () => { expect(detectTesting(["mocha"])).toBe("Mocha"); }); test("returns null for no testing", () => { expect(detectTesting([])).toBeNull(); }); }); describe("commit categorization", () => { function categorizeCommit(message: string): "decision" | "solution" | null { const messageLower = message.toLowerCase(); const firstLine = message.split("\n")[0] ?? ""; const decisionKeywords = [ "decision", "chose", "decided", "migrat", "refactor", "architect", "restructur", "redesign", "overhaul", "breaking", "deprecat", "upgrade", "switch to", "replace", ]; const solutionKeywords = [ "fix", "bug", "resolve", "issue", "error", "crash", "patch", "hotfix", "correct", ]; if (firstLine.match(/^fix(\(.+\))?!?:/i)) { return "solution"; } if (firstLine.match(/^refactor(\(.+\))?!?:/i)) { return "decision"; } for (const keyword of decisionKeywords) { if (messageLower.includes(keyword)) return "decision"; } for (const keyword of solutionKeywords) { if (messageLower.includes(keyword)) return "solution"; } return null; } test("categorizes fix: as solution", () => { expect(categorizeCommit("fix: resolve login issue")).toBe("solution"); }); test("categorizes fix(scope): as solution", () => { expect(categorizeCommit("fix(auth): fix token refresh")).toBe("solution"); }); test("categorizes refactor: as decision", () => { expect(categorizeCommit("refactor: restructure auth module")).toBe( "decision", ); }); test("categorizes migration as decision", () => { expect(categorizeCommit("Migrate from Jest to Vitest")).toBe("decision"); }); test("categorizes breaking change as decision", () => { expect(categorizeCommit("Breaking: remove deprecated API")).toBe( "decision", ); }); test("categorizes bug fix as solution", () => { expect(categorizeCommit("Fixed bug in authentication")).toBe("solution"); }); test("categorizes error fix as solution", () => { expect(categorizeCommit("Resolve error in payment flow")).toBe( "solution", ); }); test("returns null for generic commit", () => { expect(categorizeCommit("Update README")).toBeNull(); }); test("returns null for docs commit", () => { expect(categorizeCommit("docs: add API documentation")).toBeNull(); }); }); describe("title extraction", () => { function extractTitle(commitMessage: string): string { const firstLine = commitMessage.split("\n")[0] ?? ""; return firstLine .replace( /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?!?:\s*/i, "", ) .slice(0, 100); } test("extracts title from fix commit", () => { expect(extractTitle("fix: resolve login bug")).toBe("resolve login bug"); }); test("extracts title from scoped commit", () => { expect(extractTitle("feat(auth): add SSO support")).toBe( "add SSO support", ); }); test("handles breaking change marker", () => { expect(extractTitle("feat!: major API change")).toBe("major API change"); }); test("handles plain message", () => { expect(extractTitle("Update dependencies")).toBe("Update dependencies"); }); test("truncates long titles", () => { const longMessage = "a".repeat(150); expect(extractTitle(longMessage).length).toBe(100); }); }); describe("summary extraction", () => { function extractSummary(commitMessage: string): string | undefined { const lines = commitMessage.split("\n").filter((l) => l.trim()); if (lines.length <= 1) return undefined; const body = lines.slice(1).join(" ").trim(); if (body.length < 20) return undefined; return body.slice(0, 200); } test("extracts body as summary", () => { const message = "fix: login bug\n\nThis fixes the issue where users..."; expect(extractSummary(message)).toBe( "This fixes the issue where users...", ); }); test("returns undefined for single line", () => { expect(extractSummary("fix: simple fix")).toBeUndefined(); }); test("returns undefined for short body", () => { const message = "fix: bug\n\nSmall fix"; expect(extractSummary(message)).toBeUndefined(); }); test("truncates long summaries", () => { const message = "fix: bug\n\n" + "a".repeat(300); const summary = extractSummary(message); expect(summary?.length).toBe(200); }); }); describe("tag building from commit", () => { function buildTags( message: string, type: string, isBreaking: boolean, ): string[] { const tags = ["git-extracted", type]; if (isBreaking) tags.push("breaking-change"); const scopeMatch = message.match(/^\w+\(([^)]+)\):/); if (scopeMatch) tags.push(scopeMatch[1]); const messageLower = message.toLowerCase(); if (messageLower.includes("auth")) tags.push("auth"); if (messageLower.includes("api")) tags.push("api"); if (messageLower.includes("database") || messageLower.includes("db")) { tags.push("database"); } if (messageLower.includes("ui") || messageLower.includes("frontend")) { tags.push("ui"); } if (messageLower.includes("test")) tags.push("testing"); if (messageLower.includes("security")) tags.push("security"); return [...new Set(tags)]; } test("includes git-extracted tag", () => { const tags = buildTags("fix: bug", "solution", false); expect(tags).toContain("git-extracted"); }); test("includes type tag", () => { const tags = buildTags("fix: bug", "solution", false); expect(tags).toContain("solution"); }); test("includes breaking-change tag", () => { const tags = buildTags("feat!: major change", "decision", true); expect(tags).toContain("breaking-change"); }); test("extracts scope as tag", () => { const tags = buildTags("fix(auth): login bug", "solution", false); expect(tags).toContain("auth"); }); test("adds auth tag from content", () => { const tags = buildTags("fix: authentication issue", "solution", false); expect(tags).toContain("auth"); }); test("adds api tag from content", () => { const tags = buildTags("fix: API endpoint error", "solution", false); expect(tags).toContain("api"); }); test("adds database tag from content", () => { const tags = buildTags("fix: database connection", "solution", false); expect(tags).toContain("database"); }); test("deduplicates tags", () => { const tags = buildTags("fix(auth): auth bug", "solution", false); const authCount = tags.filter((t) => t === "auth").length; expect(authCount).toBe(1); }); }); describe("breaking change detection", () => { function isBreakingChange(message: string): boolean { const messageLower = message.toLowerCase(); return ( message.includes("!:") || messageLower.includes("breaking change") || messageLower.includes("breaking:") ); } test("detects conventional breaking marker", () => { expect(isBreakingChange("feat!: major change")).toBe(true); }); test("detects BREAKING CHANGE", () => { expect( isBreakingChange("feat: new API\n\nBREAKING CHANGE: old removed"), ).toBe(true); }); test("detects breaking: prefix", () => { expect(isBreakingChange("breaking: remove deprecated")).toBe(true); }); test("returns false for normal commit", () => { expect(isBreakingChange("fix: minor bug")).toBe(false); }); }); describe("issue reference extraction", () => { function extractIssues(message: string): string[] { const issues: string[] = []; const matches = message.matchAll(/#(\d+)|([A-Z]+-\d+)/g); for (const match of matches) { const issue = match[1] ? `#${match[1]}` : match[2]; if (issue && !issues.includes(issue)) { issues.push(issue); } } return issues; } test("extracts GitHub issue reference", () => { const issues = extractIssues("fix: resolve #123"); expect(issues).toContain("#123"); }); test("extracts multiple GitHub issues", () => { const issues = extractIssues("fix: resolve #123 and #456"); expect(issues).toEqual(["#123", "#456"]); }); test("extracts Jira issue reference", () => { const issues = extractIssues("fix: resolve PROJ-123"); expect(issues).toContain("PROJ-123"); }); test("extracts mixed references", () => { const issues = extractIssues("fix: #123 and JIRA-456"); expect(issues).toEqual(["#123", "JIRA-456"]); }); test("returns empty for no issues", () => { expect(extractIssues("fix: simple bug")).toEqual([]); }); test("deduplicates issues", () => { const issues = extractIssues("fix #123: related to #123"); expect(issues).toEqual(["#123"]); }); }); describe("documentation importance scoring", () => { function getDocImportance(filePath: string): number { const pathLower = filePath.toLowerCase(); if (pathLower.includes("readme")) return 0.9; if (pathLower.includes("contributing")) return 0.8; if (pathLower.includes("architecture")) return 0.85; if (pathLower.includes("adr")) return 0.8; if (pathLower.includes("decision")) return 0.8; if (pathLower.includes("changelog")) return 0.6; return 0.5; } test("README has highest importance", () => { expect(getDocImportance("README.md")).toBe(0.9); }); test("CONTRIBUTING has high importance", () => { expect(getDocImportance("CONTRIBUTING.md")).toBe(0.8); }); test("architecture docs have high importance", () => { expect(getDocImportance("docs/architecture.md")).toBe(0.85); }); test("ADR docs have high importance", () => { expect(getDocImportance("docs/adr/001-use-typescript.md")).toBe(0.8); }); test("changelog has medium importance", () => { expect(getDocImportance("CHANGELOG.md")).toBe(0.6); }); test("other docs have default importance", () => { expect(getDocImportance("docs/api.md")).toBe(0.5); }); }); describe("doc tag building", () => { function buildDocTags(filePath: string): string[] { const tags = ["documentation", "imported"]; const pathLower = filePath.toLowerCase(); if (pathLower.includes("readme")) tags.push("readme"); if (pathLower.includes("contributing")) tags.push("contributing"); if (pathLower.includes("architecture")) tags.push("architecture"); if (pathLower.includes("adr") || pathLower.includes("decision")) { tags.push("adr"); } if (pathLower.includes("changelog")) tags.push("changelog"); if (pathLower.includes("api")) tags.push("api-docs"); if (pathLower.includes("guide")) tags.push("guide"); if (pathLower.includes("tutorial")) tags.push("tutorial"); return tags; } test("includes documentation tag", () => { const tags = buildDocTags("docs/api.md"); expect(tags).toContain("documentation"); }); test("includes imported tag", () => { const tags = buildDocTags("docs/api.md"); expect(tags).toContain("imported"); }); test("includes readme tag", () => { const tags = buildDocTags("README.md"); expect(tags).toContain("readme"); }); test("includes architecture tag", () => { const tags = buildDocTags("docs/architecture/overview.md"); expect(tags).toContain("architecture"); }); test("includes adr tag for decision docs", () => { const tags = buildDocTags("docs/decision/001.md"); expect(tags).toContain("adr"); }); test("includes api-docs tag", () => { const tags = buildDocTags("docs/api-reference.md"); expect(tags).toContain("api-docs"); }); test("includes guide tag", () => { const tags = buildDocTags("docs/getting-started-guide.md"); expect(tags).toContain("guide"); }); }); describe("doc title extraction", () => { function extractDocTitle(content: string, filename: string): string { const titleMatch = content.match(/^#\s+(.+)$/m); if (titleMatch) return titleMatch[1].slice(0, 100); return filename.replace(/\.(md|mdx|txt)$/, "").replace(/[-_]/g, " "); } test("extracts markdown heading", () => { expect(extractDocTitle("# Welcome\n\nContent", "file.md")).toBe( "Welcome", ); }); test("falls back to filename", () => { expect(extractDocTitle("No heading here", "getting-started.md")).toBe( "getting started", ); }); test("replaces underscores in filename", () => { expect(extractDocTitle("Content", "api_reference.md")).toBe( "api reference", ); }); test("removes extension", () => { expect(extractDocTitle("Content", "guide.mdx")).toBe("guide"); }); test("truncates long titles", () => { const longTitle = "# " + "a".repeat(150); expect(extractDocTitle(longTitle, "file.md").length).toBe(100); }); }); describe("file finding with depth limit", () => { function shouldTraverse(depth: number, maxDepth: number): boolean { return depth < maxDepth; } function shouldIncludeDir(name: string, excludes: string[]): boolean { return !excludes.includes(name) && !name.startsWith("."); } test("allows traversal within depth", () => { expect(shouldTraverse(3, 5)).toBe(true); }); test("blocks traversal at depth limit", () => { expect(shouldTraverse(5, 5)).toBe(false); }); test("blocks traversal beyond depth", () => { expect(shouldTraverse(6, 5)).toBe(false); }); test("excludes node_modules", () => { expect(shouldIncludeDir("node_modules", ["node_modules", ".git"])).toBe( false, ); }); test("excludes .git", () => { expect(shouldIncludeDir(".git", ["node_modules", ".git"])).toBe(false); }); test("excludes hidden directories", () => { expect(shouldIncludeDir(".cache", [])).toBe(false); }); test("includes normal directories", () => { expect(shouldIncludeDir("src", ["node_modules", ".git"])).toBe(true); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docleaai/doclea-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server