447 lines
13 KiB
TypeScript
447 lines
13 KiB
TypeScript
import { testSuite, expect } from 'manten';
|
|
import {
|
|
assertOpenAiToken,
|
|
createFixture,
|
|
createGit,
|
|
files,
|
|
} from '../../utils.js';
|
|
|
|
export default testSuite(({ describe }) => {
|
|
if (process.platform === 'win32') {
|
|
// https://github.com/nodejs/node/issues/31409
|
|
console.warn('Skipping tests on Windows because Node.js spawn cant open TTYs');
|
|
return;
|
|
}
|
|
|
|
assertOpenAiToken();
|
|
|
|
describe('Commits', async ({ test, describe }) => {
|
|
test('Excludes files', async () => {
|
|
const { fixture, aicommits } = await createFixture(files);
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
const statusBefore = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusBefore.stdout).toBe('A data.json');
|
|
|
|
const { stdout, exitCode } = await aicommits(['--exclude', 'data.json'], { reject: false });
|
|
expect(exitCode).toBe(1);
|
|
expect(stdout).toMatch('No staged changes found.');
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Generates commit message', async () => {
|
|
const { fixture, aicommits } = await createFixture(files);
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
const committing = aicommits();
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusAfter.stdout).toBe('');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--pretty=format:%s']);
|
|
console.log({
|
|
commitMessage,
|
|
length: commitMessage.length,
|
|
});
|
|
expect(commitMessage.length).toBeLessThanOrEqual(50);
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Generated commit message must be under 20 characters', async () => {
|
|
const { fixture, aicommits } = await createFixture({
|
|
...files,
|
|
'.aicommits': `${files['.aicommits']}\nmax-length=20`,
|
|
});
|
|
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
const committing = aicommits();
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--pretty=format:%s']);
|
|
console.log({
|
|
commitMessage,
|
|
length: commitMessage.length,
|
|
});
|
|
expect(commitMessage.length).toBeLessThanOrEqual(20);
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Accepts --all flag, staging all changes before commit', async () => {
|
|
const { fixture, aicommits } = await createFixture(files);
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
await git('commit', ['-m', 'wip']);
|
|
|
|
// Change tracked file
|
|
await fixture.writeFile('data.json', 'Test');
|
|
|
|
const statusBefore = await git('status', ['--short']);
|
|
expect(statusBefore.stdout).toBe(' M data.json\n?? .aicommits');
|
|
|
|
const committing = aicommits(['--all']);
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const statusAfter = await git('status', ['--short']);
|
|
expect(statusAfter.stdout).toBe('?? .aicommits');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['-n1', '--pretty=format:%s']);
|
|
console.log({
|
|
commitMessage,
|
|
length: commitMessage.length,
|
|
});
|
|
expect(commitMessage.length).toBeLessThanOrEqual(50);
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Accepts --generate flag, overriding config', async ({ onTestFail }) => {
|
|
const { fixture, aicommits } = await createFixture({
|
|
...files,
|
|
'.aicommits': `${files['.aicommits']}\ngenerate=4`,
|
|
});
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
// Generate flag should override generate config
|
|
const committing = aicommits([
|
|
'--generate', '2',
|
|
]);
|
|
|
|
// Hit enter to accept the commit message
|
|
committing.stdout!.on('data', function onPrompt(buffer: Buffer) {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('\r');
|
|
committing.stdin!.end();
|
|
committing.stdout?.off('data', onPrompt);
|
|
}
|
|
});
|
|
|
|
const { stdout } = await committing;
|
|
const countChoices = stdout.match(/ {2}[●○]/g)?.length ?? 0;
|
|
|
|
onTestFail(() => console.log({ stdout }));
|
|
expect(countChoices).toBe(2);
|
|
|
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusAfter.stdout).toBe('');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--pretty=format:%s']);
|
|
console.log({
|
|
commitMessage,
|
|
length: commitMessage.length,
|
|
});
|
|
expect(commitMessage.length).toBeLessThanOrEqual(50);
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Generates Japanese commit message via locale config', async () => {
|
|
// https://stackoverflow.com/a/15034560/911407
|
|
const japanesePattern = /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\uFF00-\uFF9F\u4E00-\u9FAF\u3400-\u4DBF]/;
|
|
|
|
const { fixture, aicommits } = await createFixture({
|
|
...files,
|
|
'.aicommits': `${files['.aicommits']}\nlocale=ja`,
|
|
});
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
const committing = aicommits();
|
|
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusAfter.stdout).toBe('');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--pretty=format:%s']);
|
|
console.log({
|
|
commitMessage,
|
|
length: commitMessage.length,
|
|
});
|
|
expect(commitMessage).toMatch(japanesePattern);
|
|
expect(commitMessage.length).toBeLessThanOrEqual(50);
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
describe('commit types', ({ test }) => {
|
|
test('Should not use conventional commits by default', async () => {
|
|
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
|
|
const { fixture, aicommits } = await createFixture({
|
|
...files,
|
|
});
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
const committing = aicommits();
|
|
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusAfter.stdout).toBe('');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--oneline']);
|
|
console.log('Committed with:', commitMessage);
|
|
expect(commitMessage).not.toMatch(conventionalCommitPattern);
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Conventional commits', async () => {
|
|
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
|
|
const { fixture, aicommits } = await createFixture({
|
|
...files,
|
|
'.aicommits': `${files['.aicommits']}\ntype=conventional`,
|
|
});
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
const committing = aicommits();
|
|
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusAfter.stdout).toBe('');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--oneline']);
|
|
console.log('Committed with:', commitMessage);
|
|
expect(commitMessage).toMatch(conventionalCommitPattern);
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Accepts --type flag, overriding config', async () => {
|
|
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
|
|
const { fixture, aicommits } = await createFixture({
|
|
...files,
|
|
'.aicommits': `${files['.aicommits']}\ntype=other`,
|
|
});
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
// Generate flag should override generate config
|
|
const committing = aicommits([
|
|
'--type', 'conventional',
|
|
]);
|
|
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusAfter.stdout).toBe('');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--oneline']);
|
|
console.log('Committed with:', commitMessage);
|
|
expect(commitMessage).toMatch(conventionalCommitPattern);
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Accepts empty --type flag', async () => {
|
|
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
|
|
const { fixture, aicommits } = await createFixture({
|
|
...files,
|
|
'.aicommits': `${files['.aicommits']}\ntype=conventional`,
|
|
});
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
const committing = aicommits([
|
|
'--type', '',
|
|
]);
|
|
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusAfter.stdout).toBe('');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--oneline']);
|
|
console.log('Committed with:', commitMessage);
|
|
expect(commitMessage).not.toMatch(conventionalCommitPattern);
|
|
|
|
await fixture.rm();
|
|
});
|
|
});
|
|
|
|
describe('proxy', ({ test }) => {
|
|
test('Fails on invalid proxy', async () => {
|
|
const { fixture, aicommits } = await createFixture({
|
|
...files,
|
|
'.aicommits': `${files['.aicommits']}\nproxy=http://localhost:1234`,
|
|
});
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
const committing = aicommits([], {
|
|
reject: false,
|
|
});
|
|
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
const { stdout, exitCode } = await committing;
|
|
|
|
expect(exitCode).toBe(1);
|
|
expect(stdout).toMatch('connect ECONNREFUSED');
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Connects with config', async () => {
|
|
const { fixture, aicommits } = await createFixture({
|
|
...files,
|
|
'.aicommits': `${files['.aicommits']}\nproxy=http://localhost:8888`,
|
|
});
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
const committing = aicommits();
|
|
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusAfter.stdout).toBe('');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--pretty=format:%s']);
|
|
console.log({
|
|
commitMessage,
|
|
length: commitMessage.length,
|
|
});
|
|
expect(commitMessage.length).toBeLessThanOrEqual(50);
|
|
|
|
await fixture.rm();
|
|
});
|
|
|
|
test('Connects with env variable', async () => {
|
|
const { fixture, aicommits } = await createFixture(files);
|
|
const git = await createGit(fixture.path);
|
|
|
|
await git('add', ['data.json']);
|
|
|
|
const committing = aicommits([], {
|
|
env: {
|
|
HTTP_PROXY: 'http://localhost:8888',
|
|
},
|
|
});
|
|
|
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
|
const stdout = buffer.toString();
|
|
if (stdout.match('└')) {
|
|
committing.stdin!.write('y');
|
|
committing.stdin!.end();
|
|
}
|
|
});
|
|
|
|
await committing;
|
|
|
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
expect(statusAfter.stdout).toBe('');
|
|
|
|
const { stdout: commitMessage } = await git('log', ['--pretty=format:%s']);
|
|
console.log({
|
|
commitMessage,
|
|
length: commitMessage.length,
|
|
});
|
|
expect(commitMessage.length).toBeLessThanOrEqual(50);
|
|
|
|
await fixture.rm();
|
|
});
|
|
});
|
|
});
|
|
});
|