From 3abe798e673622d80dac2eee49cda7cf68a1864f Mon Sep 17 00:00:00 2001 From: rgaunt Date: Thu, 1 May 2025 23:41:09 +1000 Subject: [PATCH 1/5] Fix template path resolution issue - Changed template path resolution to only use the application installation directory - Added fallback to default template when template file doesn't exist - Added debug logging for template resolution - Improved error messaging --- index.js | 74 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 3bc37c8..e0ace0c 100755 --- a/index.js +++ b/index.js @@ -80,15 +80,49 @@ export function checkGhCli() { } } -// Get the template path -export function getTemplatePath() { - const templatePath = path.join(process.cwd(), 'templates', 'PULL_REQUEST_TEMPLATE.twig'); +// Default PR template content as a fallback +const DEFAULT_TEMPLATE = `{% if has_ticket %} +## Ticket +{{ ticket_number }} +{% endif %} + +## Changes +{% for change in changes %} +- {{ change }} +{% endfor %} + +{% if has_tests %} +## Tests +- āœ… Includes tests +{% else %} +## Tests +- āŒ No tests included +{% endif %} +`; + +// Get the directory where the script is installed +function getScriptDir() { + // Use import.meta.url to get the full URL of the current module + const fileUrl = import.meta.url; + // Convert the file URL to a system path and get the directory + return path.dirname(new URL(fileUrl).pathname); +} +// Get the template path or create default template +export function getTemplatePath() { + // Get template ONLY from the script's installation directory + const scriptDir = getScriptDir(); + const templatePath = path.join(scriptDir, 'templates', 'PULL_REQUEST_TEMPLATE.twig'); + + // Check if template exists in the app installation directory if (!existsSync(templatePath)) { - throw new Error('PR template not found: ' + templatePath); + console.log(`šŸ” Template not found in application directory: ${templatePath}`); + console.log('āš ļø Using default template'); + return { isDefault: true, content: DEFAULT_TEMPLATE }; } - return templatePath; + console.log(`šŸ“‹ Using template from application directory: ${templatePath}`); + return { isDefault: false, path: templatePath }; } // Create PR using GitHub CLI @@ -173,15 +207,27 @@ export async function main() { } } - // Render template - const templatePath = getTemplatePath(); - - const renderedTemplate = await renderFileAsync(templatePath, { - ticket_number: ticketNumber || '', - changes, - has_tests: hasTests, - has_ticket: !!ticketNumber - }); + // Get template and render it + const template = getTemplatePath(); + + let renderedTemplate; + if (template.isDefault) { + // Render the default template string + renderedTemplate = twig.twig({ data: template.content }).render({ + ticket_number: ticketNumber || '', + changes, + has_tests: hasTests, + has_ticket: !!ticketNumber + }); + } else { + // Render from file + renderedTemplate = await renderFileAsync(template.path, { + ticket_number: ticketNumber || '', + changes, + has_tests: hasTests, + has_ticket: !!ticketNumber + }); + } console.log('\nšŸ“‹ PR Preview:'); console.log(`Title: ${ticketNumber ? `[${ticketNumber}] ` : ''}${prTitle}`); From 3adc4b5fe5eb41c6f4d492a8aa52e6cc7eacbdf6 Mon Sep 17 00:00:00 2001 From: rgaunt Date: Thu, 1 May 2025 23:41:47 +1000 Subject: [PATCH 2/5] Added .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66d62f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/.claude/settings.local.json From 4a1fc17aa718a4076908a67382efb69992f9a1ef Mon Sep 17 00:00:00 2001 From: rgaunt Date: Fri, 2 May 2025 23:38:51 +1000 Subject: [PATCH 3/5] Add feature to allow selecting target branch for PR - Add getDefaultBranch function to get the repository default branch - Add getRemoteBranches function to get list of available branches - Update createPR function to accept a target branch parameter - Modify main function to prompt user for target branch - Add basic tests for the new functionality --- index.js | 70 +++++++++++++++++++++++++++++++++++--- jest.config.mjs | 2 +- test/basic.test.js | 12 +++++-- test/target-branch.test.js | 46 +++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 test/target-branch.test.js diff --git a/index.js b/index.js index e0ace0c..157b249 100755 --- a/index.js +++ b/index.js @@ -48,6 +48,44 @@ export function getCurrentBranch() { } } +// Get default branch name +export function getDefaultBranch() { + try { + // Try to get the remote's default branch + // First get the default remote (usually origin) + const remote = execSync('git remote').toString().trim().split('\n')[0]; + + // Then get the default branch (what HEAD points to) + const output = execSync(`git remote show ${remote} | grep "HEAD branch"`).toString().trim(); + const match = output.match(/HEAD branch:\s*(.+)$/); + + if (match && match[1]) { + return match[1]; + } + + // Fallback to 'main' or 'master' if we can't determine it + return 'main'; + } catch { + // Fallback to a sensible default + return 'main'; + } +} + +// Get list of remote branches +export function getRemoteBranches() { + try { + // Get all remote branches, excluding HEAD reference + const output = execSync('git branch -r | grep -v HEAD').toString().trim(); + + // Parse and clean branch names + return output.split('\n') + .map(branch => branch.trim().replace(/^origin\//, '')) + .filter(branch => branch !== ''); + } catch { + return []; + } +} + // Check if branch is pushed to remote export function isBranchPushedToRemote(branchName) { try { @@ -126,9 +164,16 @@ export function getTemplatePath() { } // Create PR using GitHub CLI -export async function createPR(title, body) { +export async function createPR(title, body, targetBranch = null) { try { - const command = `gh pr create --title "${title}" --body "${body.replace(/"/g, '\\"')}"`; + // Build the command with optional target branch + let command = `gh pr create --title "${title}" --body "${body.replace(/"/g, '\\"')}"`; + + // Add target branch if specified + if (targetBranch) { + command += ` --base "${targetBranch}"`; + } + const output = execSync(command).toString().trim(); return { success: true, @@ -263,9 +308,26 @@ export async function main() { console.log(`āœ… Branch '${currentBranch}' successfully pushed to remote.`); } - // Create PR + // Get default branch for PR target + const defaultBranch = getDefaultBranch(); + + // Get list of available remote branches for selection + const remoteBranches = getRemoteBranches(); + + // Ask user which branch to target for PR + // Default to the repository's default branch + console.log('\n🌿 Select target branch for PR:'); + const targetBranch = await input({ + message: 'šŸŽÆ Target branch for PR:', + default: defaultBranch, + // Optionally we could add validation that the branch exists in remoteBranches + }); + + console.log(`šŸ“Œ Creating PR targeting branch: ${targetBranch}`); + + // Create PR with specified target branch const fullTitle = ticketNumber ? `[${ticketNumber}] ${prTitle}` : prTitle; - const result = await createPR(fullTitle, renderedTemplate); + const result = await createPR(fullTitle, renderedTemplate, targetBranch); if (result.success) { console.log(`\nāœ… Pull Request created successfully: ${result.url}`); diff --git a/jest.config.mjs b/jest.config.mjs index 43cea5e..48e8a98 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -1,6 +1,6 @@ export default { testEnvironment: 'node', - testMatch: ['**/test/basic.test.js'], + testMatch: ['**/test/*.test.js'], collectCoverage: true, coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov'], diff --git a/test/basic.test.js b/test/basic.test.js index 665f10d..9d53696 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -24,8 +24,16 @@ describe('GitHub PR Maker', () => { }); test('Template path is correctly resolved', () => { - // This test only verifies the path structure, not actual file existence - expect(getTemplatePath().endsWith('PULL_REQUEST_TEMPLATE.twig')).toBe(true); + // This test only verifies the template path object + const template = getTemplatePath(); + + // Check for default template case + if (template.isDefault) { + expect(template.content).toBeTruthy(); + } else { + // Check for file path case + expect(template.path.endsWith('PULL_REQUEST_TEMPLATE.twig')).toBe(true); + } }); test('PR title formatting with and without ticket number', () => { diff --git a/test/target-branch.test.js b/test/target-branch.test.js new file mode 100644 index 0000000..11642be --- /dev/null +++ b/test/target-branch.test.js @@ -0,0 +1,46 @@ +import { + getDefaultBranch, + getRemoteBranches, + createPR +} from '../index.js'; + +describe('Target Branch Selection', () => { + // Simple tests for the new functions - these mostly verify the functions exist + // and return the expected types, not their detailed behavior since that would require + // complex mocking + + describe('getDefaultBranch', () => { + test('returns a string value', () => { + // Just verify the function exists and returns a string + expect(typeof getDefaultBranch).toBe('function'); + + // Since this connects to git, we don't test the actual return value + // Just verify it returns something that looks reasonable (a string) + const result = getDefaultBranch(); + expect(typeof result).toBe('string'); + }); + }); + + describe('getRemoteBranches', () => { + test('returns an array', () => { + // Just verify the function exists and returns an array + expect(typeof getRemoteBranches).toBe('function'); + + // This also connects to git, so we just verify it returns an array + const branches = getRemoteBranches(); + expect(Array.isArray(branches)).toBe(true); + }); + }); + + describe('createPR', () => { + test('accepts a target branch parameter', () => { + // Verify the function accepts a target branch parameter + expect(typeof createPR).toBe('function'); + + // This is just a type check - we don't actually call the function + // since that would create a real PR + const pr = {title: 'Test PR', body: 'Test body', targetBranch: 'main'}; + expect(() => createPR(pr.title, pr.body, pr.targetBranch)).not.toThrow(); + }); + }); +}); \ No newline at end of file From 4035f7f20e8504a3451eee841e712238a53d4019 Mon Sep 17 00:00:00 2001 From: rgaunt Date: Tue, 13 May 2025 12:44:14 +1000 Subject: [PATCH 4/5] Updated tests, updated target branch selector. --- index.js | 38 +++++++++++++++---------------- package.json | 1 + test/basic.test.js | 12 ++-------- test/target-branch.test.js | 46 -------------------------------------- 4 files changed, 22 insertions(+), 75 deletions(-) delete mode 100644 test/target-branch.test.js diff --git a/index.js b/index.js index 157b249..a05a2fa 100755 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ #!/usr/bin/env node import { execSync } from 'child_process'; -import { input, confirm } from '@inquirer/prompts'; +import { input, confirm, search } from '@inquirer/prompts'; import twig from 'twig'; import { promisify } from 'util'; import { existsSync } from 'fs'; @@ -54,15 +54,15 @@ export function getDefaultBranch() { // Try to get the remote's default branch // First get the default remote (usually origin) const remote = execSync('git remote').toString().trim().split('\n')[0]; - + // Then get the default branch (what HEAD points to) const output = execSync(`git remote show ${remote} | grep "HEAD branch"`).toString().trim(); const match = output.match(/HEAD branch:\s*(.+)$/); - + if (match && match[1]) { return match[1]; } - + // Fallback to 'main' or 'master' if we can't determine it return 'main'; } catch { @@ -76,7 +76,7 @@ export function getRemoteBranches() { try { // Get all remote branches, excluding HEAD reference const output = execSync('git branch -r | grep -v HEAD').toString().trim(); - + // Parse and clean branch names return output.split('\n') .map(branch => branch.trim().replace(/^origin\//, '')) @@ -151,7 +151,7 @@ export function getTemplatePath() { // Get template ONLY from the script's installation directory const scriptDir = getScriptDir(); const templatePath = path.join(scriptDir, 'templates', 'PULL_REQUEST_TEMPLATE.twig'); - + // Check if template exists in the app installation directory if (!existsSync(templatePath)) { console.log(`šŸ” Template not found in application directory: ${templatePath}`); @@ -168,12 +168,12 @@ export async function createPR(title, body, targetBranch = null) { try { // Build the command with optional target branch let command = `gh pr create --title "${title}" --body "${body.replace(/"/g, '\\"')}"`; - + // Add target branch if specified if (targetBranch) { command += ` --base "${targetBranch}"`; } - + const output = execSync(command).toString().trim(); return { success: true, @@ -254,7 +254,7 @@ export async function main() { // Get template and render it const template = getTemplatePath(); - + let renderedTemplate; if (template.isDefault) { // Render the default template string @@ -308,24 +308,24 @@ export async function main() { console.log(`āœ… Branch '${currentBranch}' successfully pushed to remote.`); } - // Get default branch for PR target const defaultBranch = getDefaultBranch(); - - // Get list of available remote branches for selection - const remoteBranches = getRemoteBranches(); - + // Default branch gets added to the start of the array. + const remoteBranches = getRemoteBranches() + .sort((branchA, branchB) => { + return (branchA === defaultBranch) ? -1 : (branchB === defaultBranch) ? 1 : branchA.localeCompare(branchB); + }) + .map(branch => ({ title: branch, value: branch })); // Ask user which branch to target for PR // Default to the repository's default branch console.log('\n🌿 Select target branch for PR:'); - const targetBranch = await input({ + const targetBranch = await search({ message: 'šŸŽÆ Target branch for PR:', default: defaultBranch, - // Optionally we could add validation that the branch exists in remoteBranches + source: (input = '') => { return remoteBranches.filter(branch => branch.title.includes(input)); }, }); - + console.log(`šŸ“Œ Creating PR targeting branch: ${targetBranch}`); - // Create PR with specified target branch const fullTitle = ticketNumber ? `[${ticketNumber}] ${prTitle}` : prTitle; const result = await createPR(fullTitle, renderedTemplate, targetBranch); @@ -349,4 +349,4 @@ if (import.meta.url === `file://${process.argv[1]}`) { console.error('An error occurred:', error); process.exit(1); }); -} \ No newline at end of file +} diff --git a/package.json b/package.json index 6a1f973..cfbfc7f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "lint": "eslint .", + "lint:fix": "eslint . --fix", "start": "node index.js" }, "dependencies": { diff --git a/test/basic.test.js b/test/basic.test.js index 9d53696..665f10d 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -24,16 +24,8 @@ describe('GitHub PR Maker', () => { }); test('Template path is correctly resolved', () => { - // This test only verifies the template path object - const template = getTemplatePath(); - - // Check for default template case - if (template.isDefault) { - expect(template.content).toBeTruthy(); - } else { - // Check for file path case - expect(template.path.endsWith('PULL_REQUEST_TEMPLATE.twig')).toBe(true); - } + // This test only verifies the path structure, not actual file existence + expect(getTemplatePath().endsWith('PULL_REQUEST_TEMPLATE.twig')).toBe(true); }); test('PR title formatting with and without ticket number', () => { diff --git a/test/target-branch.test.js b/test/target-branch.test.js deleted file mode 100644 index 11642be..0000000 --- a/test/target-branch.test.js +++ /dev/null @@ -1,46 +0,0 @@ -import { - getDefaultBranch, - getRemoteBranches, - createPR -} from '../index.js'; - -describe('Target Branch Selection', () => { - // Simple tests for the new functions - these mostly verify the functions exist - // and return the expected types, not their detailed behavior since that would require - // complex mocking - - describe('getDefaultBranch', () => { - test('returns a string value', () => { - // Just verify the function exists and returns a string - expect(typeof getDefaultBranch).toBe('function'); - - // Since this connects to git, we don't test the actual return value - // Just verify it returns something that looks reasonable (a string) - const result = getDefaultBranch(); - expect(typeof result).toBe('string'); - }); - }); - - describe('getRemoteBranches', () => { - test('returns an array', () => { - // Just verify the function exists and returns an array - expect(typeof getRemoteBranches).toBe('function'); - - // This also connects to git, so we just verify it returns an array - const branches = getRemoteBranches(); - expect(Array.isArray(branches)).toBe(true); - }); - }); - - describe('createPR', () => { - test('accepts a target branch parameter', () => { - // Verify the function accepts a target branch parameter - expect(typeof createPR).toBe('function'); - - // This is just a type check - we don't actually call the function - // since that would create a real PR - const pr = {title: 'Test PR', body: 'Test body', targetBranch: 'main'}; - expect(() => createPR(pr.title, pr.body, pr.targetBranch)).not.toThrow(); - }); - }); -}); \ No newline at end of file From 77744796780a8b9df48faab33f38cb53d1e62af7 Mon Sep 17 00:00:00 2001 From: rgaunt Date: Sat, 6 Dec 2025 13:17:13 +1100 Subject: [PATCH 5/5] Remove mocked tests. --- package-lock.json | 3 + test/basic.test.js | 8 ++- test/git.test.js | 125 --------------------------------- test/index.test.js | 156 ------------------------------------------ test/template.test.js | 45 ------------ 5 files changed, 10 insertions(+), 327 deletions(-) delete mode 100644 test/git.test.js delete mode 100644 test/index.test.js delete mode 100644 test/template.test.js diff --git a/package-lock.json b/package-lock.json index ac0b657..279421b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,9 @@ "@inquirer/prompts": "^7.5.0", "twig": "^1.17.1" }, + "bin": { + "gh-pr": "index.js" + }, "devDependencies": { "@eslint/js": "^9.24.0", "eslint": "^9.25.1", diff --git a/test/basic.test.js b/test/basic.test.js index 665f10d..ceb2326 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -25,7 +25,13 @@ describe('GitHub PR Maker', () => { test('Template path is correctly resolved', () => { // This test only verifies the path structure, not actual file existence - expect(getTemplatePath().endsWith('PULL_REQUEST_TEMPLATE.twig')).toBe(true); + const template = getTemplatePath(); + // getTemplatePath returns an object with either { isDefault: true, content } or { isDefault: false, path } + if (template.isDefault) { + expect(template.content).toBeDefined(); + } else { + expect(template.path.endsWith('PULL_REQUEST_TEMPLATE.twig')).toBe(true); + } }); test('PR title formatting with and without ticket number', () => { diff --git a/test/git.test.js b/test/git.test.js deleted file mode 100644 index 9adeb07..0000000 --- a/test/git.test.js +++ /dev/null @@ -1,125 +0,0 @@ -// @ts-check -import { execSync } from 'child_process'; - -// Mock child_process -jest.mock('child_process', () => ({ - execSync: jest.fn() -})); - -// Import our functions -import { getRecentCommits, checkGitRepository, checkGhCli } from '../index'; - -describe('Git utility functions', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getRecentCommits', () => { - it('should parse git log output correctly', () => { - // Mock the git log output - execSync.mockReturnValueOnce(Buffer.from( - 'abc123|||Fix navigation bar|||Fixed styling issues in the navigation bar\n' + - 'def456|||Add user authentication|||Implemented JWT authentication\n' + - 'ghi789|||Update README|||Updated installation instructions' - )); - - const commits = getRecentCommits(3); - - // Verify the command that was executed - expect(execSync).toHaveBeenCalledWith('git log -3 --pretty=format:%h|||%s|||%b'); - - // Verify the parsed output - expect(commits).toEqual([ - { - hash: 'abc123', - subject: 'Fix navigation bar', - body: 'Fixed styling issues in the navigation bar' - }, - { - hash: 'def456', - subject: 'Add user authentication', - body: 'Implemented JWT authentication' - }, - { - hash: 'ghi789', - subject: 'Update README', - body: 'Updated installation instructions' - } - ]); - }); - - it('should handle commits with empty bodies', () => { - execSync.mockReturnValueOnce(Buffer.from( - 'abc123|||Fix bug|||\n' + - 'def456|||Add feature|||This is a description' - )); - - const commits = getRecentCommits(2); - - expect(commits).toEqual([ - { - hash: 'abc123', - subject: 'Fix bug', - body: '' - }, - { - hash: 'def456', - subject: 'Add feature', - body: 'This is a description' - } - ]); - }); - - it('should return empty array on error', () => { - execSync.mockImplementationOnce(() => { - throw new Error('git command failed'); - }); - - const commits = getRecentCommits(); - - expect(commits).toEqual([]); - }); - }); - - describe('checkGitRepository', () => { - it('should return true when in a git repo', () => { - execSync.mockReturnValueOnce(Buffer.from('true')); - - const result = checkGitRepository(); - - expect(execSync).toHaveBeenCalledWith('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); - expect(result).toBe(true); - }); - - it('should return false when not in a git repo', () => { - execSync.mockImplementationOnce(() => { - throw new Error('not a git repository'); - }); - - const result = checkGitRepository(); - - expect(result).toBe(false); - }); - }); - - describe('checkGhCli', () => { - it('should return true when gh is installed', () => { - execSync.mockReturnValueOnce(Buffer.from('gh version 2.0.0')); - - const result = checkGhCli(); - - expect(execSync).toHaveBeenCalledWith('gh --version', { stdio: 'ignore' }); - expect(result).toBe(true); - }); - - it('should return false when gh is not installed', () => { - execSync.mockImplementationOnce(() => { - throw new Error('command not found: gh'); - }); - - const result = checkGhCli(); - - expect(result).toBe(false); - }); - }); -}); diff --git a/test/index.test.js b/test/index.test.js deleted file mode 100644 index 0ae0c4b..0000000 --- a/test/index.test.js +++ /dev/null @@ -1,156 +0,0 @@ -import { execSync } from 'child_process'; - -jest.mock('child_process', () => ({ - execSync: jest.fn() -})); - -jest.mock('fs', () => ({ - existsSync: jest.fn() -})); - -jest.mock('@inquirer/prompts', () => ({ - input: jest.fn(), - confirm: jest.fn() -})); - -jest.mock('twig', () => ({ - default: { - renderFile: jest.fn() - } -})); - -// Import our functions after mocking -const { getRecentCommits, checkGitRepository, checkGhCli, createPR } = require('../index.js'); - -describe('Git utility functions', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getRecentCommits', () => { - it('should parse git log output correctly', () => { - // Mock the git log output - execSync.mockReturnValueOnce(Buffer.from( - 'abc123|||Fix navigation bar|||Fixed styling issues\n' + - 'def456|||Add user auth|||Implemented JWT auth' - )); - - const commits = getRecentCommits(2); - - expect(execSync).toHaveBeenCalledWith('git log -2 --pretty=format:%h|||%s|||%b'); - expect(commits).toEqual([ - { - hash: 'abc123', - subject: 'Fix navigation bar', - body: 'Fixed styling issues' - }, - { - hash: 'def456', - subject: 'Add user auth', - body: 'Implemented JWT auth' - } - ]); - }); - - it('should handle commits with empty bodies', () => { - execSync.mockReturnValueOnce(Buffer.from( - 'abc123|||Fix bug|||\n' + - 'def456|||Add feature|||This is a description' - )); - - const commits = getRecentCommits(2); - - expect(commits).toEqual([ - { - hash: 'abc123', - subject: 'Fix bug', - body: '' - }, - { - hash: 'def456', - subject: 'Add feature', - body: 'This is a description' - } - ]); - }); - - it('should return empty array on error', () => { - execSync.mockImplementationOnce(() => { - throw new Error('git command failed'); - }); - - const commits = getRecentCommits(); - - expect(commits).toEqual([]); - }); - }); - - describe('checkGitRepository', () => { - it('should return true when in a git repo', () => { - execSync.mockReturnValueOnce(Buffer.from('true')); - - const result = checkGitRepository(); - - expect(execSync).toHaveBeenCalledWith('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); - expect(result).toBe(true); - }); - - it('should return false when not in a git repo', () => { - execSync.mockImplementationOnce(() => { - throw new Error('not a git repository'); - }); - - const result = checkGitRepository(); - - expect(result).toBe(false); - }); - }); - - describe('checkGhCli', () => { - it('should return true when gh is installed', () => { - execSync.mockReturnValueOnce(Buffer.from('gh version 2.0.0')); - - const result = checkGhCli(); - - expect(execSync).toHaveBeenCalledWith('gh --version', { stdio: 'ignore' }); - expect(result).toBe(true); - }); - - it('should return false when gh is not installed', () => { - execSync.mockImplementationOnce(() => { - throw new Error('command not found: gh'); - }); - - const result = checkGhCli(); - - expect(result).toBe(false); - }); - }); - - describe('createPR', () => { - it('should create a PR successfully', async () => { - execSync.mockReturnValueOnce(Buffer.from('https://github.com/user/repo/pull/123')); - - const result = await createPR('Test PR', 'PR body'); - - expect(execSync).toHaveBeenCalledWith('gh pr create --title "Test PR" --body "PR body"'); - expect(result).toEqual({ - success: true, - url: 'https://github.com/user/repo/pull/123' - }); - }); - - it('should handle PR creation errors', async () => { - execSync.mockImplementationOnce(() => { - throw new Error('Failed to create PR'); - }); - - const result = await createPR('Test PR', 'PR body'); - - expect(result).toEqual({ - success: false, - error: 'Failed to create PR' - }); - }); - }); -}); diff --git a/test/template.test.js b/test/template.test.js deleted file mode 100644 index 957ab20..0000000 --- a/test/template.test.js +++ /dev/null @@ -1,45 +0,0 @@ -import { existsSync } from 'fs'; -import path from 'path'; - -jest.mock('fs', () => ({ - existsSync: jest.fn() -})); - -jest.mock('twig', () => ({ - default: { - renderFile: jest.fn((templatePath, data, callback) => { - // Simple mock implementation to verify template engine integration - const rendered = `Rendered template with ticket ${data.ticket_number}`; - callback(null, rendered); - }) - } -})); - -// Import our function after mocking -const { getTemplatePath } = require('../index.js'); - -describe('Template functions', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getTemplatePath', () => { - it('should return the template path when it exists', () => { - // Set up fs mock to return true for existsSync - existsSync.mockReturnValueOnce(true); - - const templatePath = getTemplatePath(); - const expectedPath = path.join(process.cwd(), 'templates', 'PULL_REQUEST_TEMPLATE.twig'); - - expect(existsSync).toHaveBeenCalledWith(expectedPath); - expect(templatePath).toBe(expectedPath); - }); - - it('should throw an error when template does not exist', () => { - // Set up fs mock to return false for existsSync - existsSync.mockReturnValueOnce(false); - - expect(() => getTemplatePath()).toThrow('PR template not found'); - }); - }); -});