131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
/**
|
|
* Tests for github/pr module
|
|
*
|
|
* Tests buildPrBody formatting and findExistingPr logic.
|
|
* createPullRequest/pushBranch/commentOnPr call `gh`/`git` CLI, not unit-tested here.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const mockExecFileSync = vi.fn();
|
|
vi.mock('node:child_process', () => ({
|
|
execFileSync: (...args: unknown[]) => mockExecFileSync(...args),
|
|
}));
|
|
|
|
vi.mock('../infra/github/issue.js', () => ({
|
|
checkGhCli: vi.fn().mockReturnValue({ available: true }),
|
|
}));
|
|
|
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|
...(await importOriginal<Record<string, unknown>>()),
|
|
createLogger: () => ({
|
|
info: vi.fn(),
|
|
debug: vi.fn(),
|
|
error: vi.fn(),
|
|
}),
|
|
getErrorMessage: (e: unknown) => String(e),
|
|
}));
|
|
|
|
import { buildPrBody, findExistingPr } from '../infra/github/pr.js';
|
|
import type { GitHubIssue } from '../infra/github/types.js';
|
|
|
|
describe('findExistingPr', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('オープンな PR がある場合はその PR を返す', () => {
|
|
mockExecFileSync.mockReturnValue(JSON.stringify([{ number: 42, url: 'https://github.com/org/repo/pull/42' }]));
|
|
|
|
const result = findExistingPr('/project', 'task/fix-bug');
|
|
|
|
expect(result).toEqual({ number: 42, url: 'https://github.com/org/repo/pull/42' });
|
|
});
|
|
|
|
it('PR がない場合は undefined を返す', () => {
|
|
mockExecFileSync.mockReturnValue(JSON.stringify([]));
|
|
|
|
const result = findExistingPr('/project', 'task/fix-bug');
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('gh CLI が失敗した場合は undefined を返す', () => {
|
|
mockExecFileSync.mockImplementation(() => { throw new Error('gh: command not found'); });
|
|
|
|
const result = findExistingPr('/project', 'task/fix-bug');
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('buildPrBody', () => {
|
|
it('should build body with single issue and report', () => {
|
|
const issue: GitHubIssue = {
|
|
number: 99,
|
|
title: 'Add login feature',
|
|
body: 'Implement username/password authentication.',
|
|
labels: [],
|
|
comments: [],
|
|
};
|
|
|
|
const result = buildPrBody([issue], 'Piece `default` completed.');
|
|
|
|
expect(result).toContain('## Summary');
|
|
expect(result).toContain('Implement username/password authentication.');
|
|
expect(result).toContain('## Execution Report');
|
|
expect(result).toContain('Piece `default` completed.');
|
|
expect(result).toContain('Closes #99');
|
|
});
|
|
|
|
it('should use title when body is empty', () => {
|
|
const issue: GitHubIssue = {
|
|
number: 10,
|
|
title: 'Fix bug',
|
|
body: '',
|
|
labels: [],
|
|
comments: [],
|
|
};
|
|
|
|
const result = buildPrBody([issue], 'Done.');
|
|
|
|
expect(result).toContain('Fix bug');
|
|
expect(result).toContain('Closes #10');
|
|
});
|
|
|
|
it('should build body without issue', () => {
|
|
const result = buildPrBody(undefined, 'Task completed.');
|
|
|
|
expect(result).toContain('## Summary');
|
|
expect(result).toContain('## Execution Report');
|
|
expect(result).toContain('Task completed.');
|
|
expect(result).not.toContain('Closes');
|
|
});
|
|
|
|
it('should support multiple issues', () => {
|
|
const issues: GitHubIssue[] = [
|
|
{
|
|
number: 1,
|
|
title: 'First issue',
|
|
body: 'First issue body.',
|
|
labels: [],
|
|
comments: [],
|
|
},
|
|
{
|
|
number: 2,
|
|
title: 'Second issue',
|
|
body: 'Second issue body.',
|
|
labels: [],
|
|
comments: [],
|
|
},
|
|
];
|
|
|
|
const result = buildPrBody(issues, 'Done.');
|
|
|
|
expect(result).toContain('## Summary');
|
|
expect(result).toContain('First issue body.');
|
|
expect(result).toContain('Closes #1');
|
|
expect(result).toContain('Closes #2');
|
|
});
|
|
});
|