Skip to main content
Glama
commit.test.ts21.9 kB
/** * Tests for generateCommitMessage function * Tests the internal pure functions and logic */ import { describe, expect, test } from "bun:test"; describe("commit message generation", () => { describe("analyzeDiff", () => { // Replicate the analyzeDiff function logic for testing interface DiffAnalysis { files: string[]; additions: number; deletions: number; significantChanges: string[]; patterns: { isNewFile: boolean; isDelete: boolean; isFix: boolean; isRefactor: boolean; isTest: boolean; isDocs: boolean; isConfig: boolean; isStyle: boolean; }; } function analyzeDiff(diff: string): DiffAnalysis { const files: string[] = []; const significantChanges: string[] = []; let additions = 0; let deletions = 0; const patterns = { isNewFile: false, isDelete: false, isFix: false, isRefactor: false, isTest: false, isDocs: false, isConfig: false, isStyle: false, }; const lines = diff.split("\n"); for (const line of lines) { if (line.startsWith("diff --git")) { const match = line.match(/b\/(.+)$/); if (match) { files.push(match[1]); } } if (line.startsWith("+") && !line.startsWith("+++")) { additions++; if ( line.match( /^[+]\s*(export\s+)?(function|class|const|interface|type)\s+\w+/, ) ) { significantChanges.push(line.slice(1).trim()); } } if (line.startsWith("-") && !line.startsWith("---")) { deletions++; } if (line.includes("new file mode")) patterns.isNewFile = true; if (line.includes("deleted file mode")) patterns.isDelete = true; if ( line.toLowerCase().includes("fix") || line.toLowerCase().includes("bug") ) patterns.isFix = true; } for (const file of files) { if ( file.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/) || file.includes("__tests__") ) { patterns.isTest = true; } if ( file.match(/\.(md|mdx|txt|rst)$/) || file.toLowerCase().includes("readme") ) { patterns.isDocs = true; } if ( file.match(/\.(json|yaml|yml|toml|ini|env)$/) || file.includes("config") ) { patterns.isConfig = true; } if (file.match(/\.(css|scss|less|styled)/) || file.includes("style")) { patterns.isStyle = true; } } if (deletions > additions * 0.5 && additions > 10) { patterns.isRefactor = true; } return { files, additions, deletions, significantChanges, patterns }; } test("extracts file names from diff", () => { const diff = `diff --git a/src/index.ts b/src/index.ts --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +console.log("hello");`; const result = analyzeDiff(diff); expect(result.files).toContain("src/index.ts"); }); test("counts additions correctly", () => { const diff = `diff --git a/src/index.ts b/src/index.ts +line1 +line2 +line3`; const result = analyzeDiff(diff); expect(result.additions).toBe(3); }); test("counts deletions correctly", () => { const diff = `diff --git a/src/index.ts b/src/index.ts -line1 -line2`; const result = analyzeDiff(diff); expect(result.deletions).toBe(2); }); test("ignores +++ and --- lines in counts", () => { const diff = `diff --git a/src/index.ts b/src/index.ts --- a/src/index.ts +++ b/src/index.ts +actual addition`; const result = analyzeDiff(diff); expect(result.additions).toBe(1); expect(result.deletions).toBe(0); }); test("detects new file", () => { const diff = `diff --git a/src/new.ts b/src/new.ts new file mode 100644 +content`; const result = analyzeDiff(diff); expect(result.patterns.isNewFile).toBe(true); }); test("detects deleted file", () => { const diff = `diff --git a/src/old.ts b/src/old.ts deleted file mode 100644 -content`; const result = analyzeDiff(diff); expect(result.patterns.isDelete).toBe(true); }); test("detects fix in diff content", () => { const diff = `diff --git a/src/index.ts b/src/index.ts +// fix for bug #123`; const result = analyzeDiff(diff); expect(result.patterns.isFix).toBe(true); }); test("detects test files", () => { const diff = `diff --git a/src/index.test.ts b/src/index.test.ts +test content`; const result = analyzeDiff(diff); expect(result.patterns.isTest).toBe(true); }); test("detects spec files", () => { const diff = `diff --git a/src/index.spec.ts b/src/index.spec.ts +test content`; const result = analyzeDiff(diff); expect(result.patterns.isTest).toBe(true); }); test("detects __tests__ folder", () => { const diff = `diff --git a/__tests__/index.ts b/__tests__/index.ts +test content`; const result = analyzeDiff(diff); expect(result.patterns.isTest).toBe(true); }); test("detects markdown docs", () => { const diff = `diff --git a/README.md b/README.md +documentation`; const result = analyzeDiff(diff); expect(result.patterns.isDocs).toBe(true); }); test("detects config files", () => { const diff = `diff --git a/tsconfig.json b/tsconfig.json +config`; const result = analyzeDiff(diff); expect(result.patterns.isConfig).toBe(true); }); test("detects style files", () => { const diff = `diff --git a/styles.css b/styles.css +.class {}`; const result = analyzeDiff(diff); expect(result.patterns.isStyle).toBe(true); }); test("detects refactor (more deletions than additions)", () => { // 15 additions, 10 deletions (10 > 15 * 0.5 = 7.5, and 15 > 10) const additions = Array(15).fill("+line").join("\n"); const deletions = Array(10).fill("-line").join("\n"); const diff = `diff --git a/src/index.ts b/src/index.ts ${additions} ${deletions}`; const result = analyzeDiff(diff); expect(result.patterns.isRefactor).toBe(true); }); test("extracts significant changes (function definitions)", () => { const diff = `diff --git a/src/index.ts b/src/index.ts +function calculateTotal() { + return sum; +}`; const result = analyzeDiff(diff); expect(result.significantChanges.length).toBeGreaterThan(0); expect(result.significantChanges[0]).toContain("function calculateTotal"); }); test("extracts significant changes (class definitions)", () => { const diff = `diff --git a/src/index.ts b/src/index.ts +class UserService { +}`; const result = analyzeDiff(diff); expect(result.significantChanges.length).toBeGreaterThan(0); expect(result.significantChanges[0]).toContain("class UserService"); }); test("extracts significant changes (const exports)", () => { const diff = `diff --git a/src/index.ts b/src/index.ts +export const API_URL = "https://api.example.com";`; const result = analyzeDiff(diff); expect(result.significantChanges.length).toBeGreaterThan(0); }); }); describe("determineCommitType", () => { function determineCommitType( patterns: { isNewFile: boolean; isDelete: boolean; isFix: boolean; isRefactor: boolean; isTest: boolean; isDocs: boolean; isConfig: boolean; isStyle: boolean; }, additions: number, deletions: number, hasBugMemory: boolean, ): string { if (hasBugMemory && !patterns.isNewFile) return "fix"; if (patterns.isTest) return "test"; if (patterns.isDocs) return "docs"; if (patterns.isFix) return "fix"; if (patterns.isConfig) return "chore"; if (patterns.isStyle) return "style"; if (patterns.isRefactor) return "refactor"; if (patterns.isNewFile) return "feat"; if (additions > deletions * 2) return "feat"; if (deletions > additions) return "refactor"; return "feat"; } test("returns test for test files", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: false, isRefactor: false, isTest: true, isDocs: false, isConfig: false, isStyle: false, }, 10, 5, false, ); expect(type).toBe("test"); }); test("returns docs for documentation files", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: false, isRefactor: false, isTest: false, isDocs: true, isConfig: false, isStyle: false, }, 10, 5, false, ); expect(type).toBe("docs"); }); test("returns fix when fix pattern detected", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: true, isRefactor: false, isTest: false, isDocs: false, isConfig: false, isStyle: false, }, 10, 5, false, ); expect(type).toBe("fix"); }); test("returns fix when bug memory exists", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: false, isRefactor: false, isTest: false, isDocs: false, isConfig: false, isStyle: false, }, 10, 5, true, ); expect(type).toBe("fix"); }); test("returns chore for config files", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: false, isRefactor: false, isTest: false, isDocs: false, isConfig: true, isStyle: false, }, 10, 5, false, ); expect(type).toBe("chore"); }); test("returns style for style files", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: false, isRefactor: false, isTest: false, isDocs: false, isConfig: false, isStyle: true, }, 10, 5, false, ); expect(type).toBe("style"); }); test("returns refactor when refactor pattern detected", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: false, isRefactor: true, isTest: false, isDocs: false, isConfig: false, isStyle: false, }, 10, 5, false, ); expect(type).toBe("refactor"); }); test("returns feat for new files", () => { const type = determineCommitType( { isNewFile: true, isDelete: false, isFix: false, isRefactor: false, isTest: false, isDocs: false, isConfig: false, isStyle: false, }, 10, 5, false, ); expect(type).toBe("feat"); }); test("returns feat when additions greatly exceed deletions", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: false, isRefactor: false, isTest: false, isDocs: false, isConfig: false, isStyle: false, }, 100, 10, false, // 100 > 10 * 2 = 20 ); expect(type).toBe("feat"); }); test("returns refactor when deletions exceed additions", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: false, isRefactor: false, isTest: false, isDocs: false, isConfig: false, isStyle: false, }, 10, 20, false, // 20 > 10 ); expect(type).toBe("refactor"); }); test("defaults to feat", () => { const type = determineCommitType( { isNewFile: false, isDelete: false, isFix: false, isRefactor: false, isTest: false, isDocs: false, isConfig: false, isStyle: false, }, 10, 10, false, ); expect(type).toBe("feat"); }); }); describe("determineScope", () => { function determineScope(files: string[]): string | null { if (files.length === 0) return null; const dirs = files .map((f) => { const parts = f.split("/"); return parts.length > 1 ? parts[0] : null; }) .filter(Boolean); if (dirs.length === 0) return null; const uniqueDirs = [...new Set(dirs)]; if (uniqueDirs.length === 1 && uniqueDirs[0]) return uniqueDirs[0]; if (files.some((f) => f.includes("auth"))) return "auth"; if (files.some((f) => f.includes("api"))) return "api"; if (files.some((f) => f.includes("db") || f.includes("database"))) return "db"; if (files.some((f) => f.includes("ui") || f.includes("component"))) return "ui"; return null; } test("returns null for empty files", () => { expect(determineScope([])).toBeNull(); }); test("returns directory when all files in same dir", () => { const files = ["src/index.ts", "src/utils.ts", "src/helper.ts"]; expect(determineScope(files)).toBe("src"); }); test("returns null for root files only", () => { const files = ["index.ts", "utils.ts"]; expect(determineScope(files)).toBeNull(); }); test("returns auth when auth files in different dirs", () => { // When files are in different top-level dirs, keyword matching kicks in const files = ["auth/login.ts", "config/settings.ts"]; expect(determineScope(files)).toBe("auth"); }); test("returns api when api files in different dirs", () => { const files = ["api/users.ts", "config/settings.ts"]; expect(determineScope(files)).toBe("api"); }); test("returns db when database files in different dirs", () => { const files = ["db/schema.ts", "config/settings.ts"]; expect(determineScope(files)).toBe("db"); }); test("returns ui when component files in different dirs", () => { const files = ["components/Button.tsx", "config/settings.ts"]; expect(determineScope(files)).toBe("ui"); }); test("returns common dir when all files share same top-level dir", () => { // Even with 'auth' in path, common dir takes precedence const files = ["src/auth/login.ts", "src/utils.ts"]; expect(determineScope(files)).toBe("src"); }); }); describe("generateSummary", () => { function generateSummary( files: string[], significantChanges: string[], patterns: { isNewFile: boolean; isDelete: boolean }, ): string { if (significantChanges.length > 0) { const first = significantChanges[0]; const match = first?.match( /(function|class|const|interface|type)\s+(\w+)/, ); if (match) { const [, kind, name] = match; if (patterns.isNewFile) return `add ${kind} ${name}`; return `update ${kind} ${name}`; } } if (files.length === 1) { const file = files[0]?.split("/").pop() ?? files[0]; if (patterns.isNewFile) return `add ${file}`; if (patterns.isDelete) return `remove ${file}`; return `update ${file}`; } if (patterns.isNewFile) return `add ${files.length} files`; if (patterns.isDelete) return `remove ${files.length} files`; return `update ${files.length} files`; } test("describes function addition", () => { const summary = generateSummary( ["src/utils.ts"], ["function calculateTotal() {"], { isNewFile: true, isDelete: false }, ); expect(summary).toBe("add function calculateTotal"); }); test("describes function update", () => { const summary = generateSummary( ["src/utils.ts"], ["function calculateTotal() {"], { isNewFile: false, isDelete: false }, ); expect(summary).toBe("update function calculateTotal"); }); test("describes single file addition", () => { const summary = generateSummary(["src/utils.ts"], [], { isNewFile: true, isDelete: false, }); expect(summary).toBe("add utils.ts"); }); test("describes single file deletion", () => { const summary = generateSummary(["src/old.ts"], [], { isNewFile: false, isDelete: true, }); expect(summary).toBe("remove old.ts"); }); test("describes single file update", () => { const summary = generateSummary(["src/utils.ts"], [], { isNewFile: false, isDelete: false, }); expect(summary).toBe("update utils.ts"); }); test("describes multiple file addition", () => { const summary = generateSummary( ["src/a.ts", "src/b.ts", "src/c.ts"], [], { isNewFile: true, isDelete: false }, ); expect(summary).toBe("add 3 files"); }); test("describes multiple file update", () => { const summary = generateSummary(["src/a.ts", "src/b.ts"], [], { isNewFile: false, isDelete: false, }); expect(summary).toBe("update 2 files"); }); }); describe("extractRelatedIssues", () => { function extractRelatedIssues( memories: Array<{ sourcePr?: string; content: string; title: string }>, ): string[] { const issues: string[] = []; const issuePattern = /#(\d+)|([A-Z]+-\d+)/g; for (const memory of memories) { if (memory.sourcePr) { const prMatch = memory.sourcePr.match(/\d+/); if (prMatch) { issues.push(`#${prMatch[0]}`); } } const contentMatches = memory.content.matchAll(issuePattern); for (const match of contentMatches) { const issue = match[1] ? `#${match[1]}` : match[2]; if (issue && !issues.includes(issue)) { issues.push(issue); } } const titleMatches = memory.title.matchAll(issuePattern); for (const match of titleMatches) { const issue = match[1] ? `#${match[1]}` : match[2]; if (issue && !issues.includes(issue)) { issues.push(issue); } } } return [...new Set(issues)].slice(0, 5); } test("extracts issue from sourcePr", () => { const issues = extractRelatedIssues([ { sourcePr: "123", content: "", title: "" }, ]); expect(issues).toContain("#123"); }); test("extracts GitHub issue from content", () => { const issues = extractRelatedIssues([ { content: "Fixes #456", title: "" }, ]); expect(issues).toContain("#456"); }); test("extracts Jira issue from content", () => { const issues = extractRelatedIssues([ { content: "Relates to PROJ-789", title: "" }, ]); expect(issues).toContain("PROJ-789"); }); test("extracts issue from title", () => { const issues = extractRelatedIssues([ { content: "", title: "Fix bug #100" }, ]); expect(issues).toContain("#100"); }); test("deduplicates issues", () => { const issues = extractRelatedIssues([ { content: "See #123 and #123", title: "#123" }, ]); expect(issues.filter((i) => i === "#123").length).toBe(1); }); test("limits to 5 issues", () => { const issues = extractRelatedIssues([ { content: "#1 #2 #3 #4 #5 #6 #7", title: "" }, ]); expect(issues.length).toBeLessThanOrEqual(5); }); }); describe("conventional commit format", () => { test("formats message without scope", () => { const type = "feat"; const scope: string | null = null; const summary = "add new feature"; const body: string | null = null; const scopePart = scope ? `(${scope})` : ""; const message = body ? `${type}${scopePart}: ${summary}\n\n${body}` : `${type}${scopePart}: ${summary}`; expect(message).toBe("feat: add new feature"); }); test("formats message with scope", () => { const type = "fix"; const scope = "auth"; const summary = "resolve login issue"; const body: string | null = null; const scopePart = scope ? `(${scope})` : ""; const message = body ? `${type}${scopePart}: ${summary}\n\n${body}` : `${type}${scopePart}: ${summary}`; expect(message).toBe("fix(auth): resolve login issue"); }); test("formats message with body", () => { const type = "feat"; const scope = "api"; const summary = "add user endpoint"; const body = "This adds a new user endpoint."; const scopePart = scope ? `(${scope})` : ""; const message = body ? `${type}${scopePart}: ${summary}\n\n${body}` : `${type}${scopePart}: ${summary}`; expect(message).toBe( "feat(api): add user endpoint\n\nThis adds a new user endpoint.", ); }); }); });

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