test: refactor to run asynchronously (#174)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { describe } from 'manten';
|
import { describe } from 'manten';
|
||||||
|
|
||||||
describe('aicommits', ({ runTestSuite }) => {
|
describe('aicommits', ({ runTestSuite }) => {
|
||||||
runTestSuite(import('./specs/cli.js'));
|
runTestSuite(import('./specs/cli/index.js'));
|
||||||
runTestSuite(import('./specs/config.js'));
|
runTestSuite(import('./specs/config.js'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
import { testSuite, expect } from 'manten';
|
|
||||||
import { createFixture } from 'fs-fixture';
|
|
||||||
import { createAicommits, createGit } from '../utils.js';
|
|
||||||
|
|
||||||
const { OPENAI_KEY } = process.env;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!OPENAI_KEY) {
|
|
||||||
console.warn('⚠️ process.env.OPENAI_KEY is necessary to run these tests. Skipping...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('CLI', async ({ test }) => {
|
|
||||||
const data: Record<string, string> = {
|
|
||||||
firstName: 'Hiroki',
|
|
||||||
};
|
|
||||||
const fixture = await createFixture({
|
|
||||||
'data.json': JSON.stringify(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
const aicommits = createAicommits({
|
|
||||||
cwd: fixture.path,
|
|
||||||
home: fixture.path,
|
|
||||||
});
|
|
||||||
|
|
||||||
await test('Fails on non-Git project', async () => {
|
|
||||||
const { stdout, exitCode } = await aicommits([], { reject: false });
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(stdout).toMatch('The current directory must be a Git repository!');
|
|
||||||
});
|
|
||||||
|
|
||||||
const git = await createGit(fixture.path);
|
|
||||||
|
|
||||||
await test('Fails on no staged files', async () => {
|
|
||||||
const { stdout, exitCode } = await aicommits([], { reject: false });
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(stdout).toMatch('No staged changes found. Make sure to stage your changes with `git add`.');
|
|
||||||
});
|
|
||||||
|
|
||||||
await aicommits([
|
|
||||||
'config',
|
|
||||||
'set',
|
|
||||||
`OPENAI_KEY=${OPENAI_KEY}`,
|
|
||||||
]);
|
|
||||||
|
|
||||||
await test('Excludes files', async () => {
|
|
||||||
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. Make sure to stage your changes with `git add`.');
|
|
||||||
});
|
|
||||||
|
|
||||||
await test('Generates commit message', async () => {
|
|
||||||
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 } = await git('log', ['--oneline']);
|
|
||||||
console.log('Committed with:', stdout);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test('Accepts --generate flag, overriding config', async () => {
|
|
||||||
data.lastName = 'Osame';
|
|
||||||
await fixture.writeJson('data.json', data);
|
|
||||||
|
|
||||||
await git('add', ['data.json']);
|
|
||||||
|
|
||||||
const statusBefore = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
||||||
expect(statusBefore.stdout).toBe('M data.json');
|
|
||||||
|
|
||||||
await aicommits([
|
|
||||||
'config',
|
|
||||||
'set',
|
|
||||||
'generate=4',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Generate flag should override generate config
|
|
||||||
const committing = aicommits(['--generate', '2']);
|
|
||||||
|
|
||||||
committing.stdout!.on('data', function onPrompt(buffer: Buffer) {
|
|
||||||
const stdout = buffer.toString();
|
|
||||||
if (stdout.match('└')) {
|
|
||||||
const countChoices = stdout.match(/ {2}[●○]/g)?.length ?? 0;
|
|
||||||
|
|
||||||
// 2 choices should be generated
|
|
||||||
expect(countChoices).toBe(2);
|
|
||||||
|
|
||||||
committing.stdin!.write('\r');
|
|
||||||
committing.stdin!.end();
|
|
||||||
committing.stdout?.off('data', onPrompt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await committing;
|
|
||||||
|
|
||||||
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
||||||
expect(statusAfter.stdout).toBe('');
|
|
||||||
|
|
||||||
const { stdout } = await git('log', ['--oneline']);
|
|
||||||
console.log('Committed with:', stdout);
|
|
||||||
|
|
||||||
await aicommits([
|
|
||||||
'config',
|
|
||||||
'set',
|
|
||||||
'generate=1',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
await 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]/;
|
|
||||||
|
|
||||||
data.username = 'privatenumber';
|
|
||||||
await fixture.writeJson('data.json', data);
|
|
||||||
|
|
||||||
await git('add', ['data.json']);
|
|
||||||
|
|
||||||
const statusBefore = await git('status', ['--porcelain', '--untracked-files=no']);
|
|
||||||
expect(statusBefore.stdout).toBe('M data.json');
|
|
||||||
|
|
||||||
await aicommits([
|
|
||||||
'config',
|
|
||||||
'set',
|
|
||||||
'locale=ja',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Generate flag should override generate config
|
|
||||||
const committing = aicommits(['--generate', '1']);
|
|
||||||
|
|
||||||
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 } = await git('log', ['--oneline']);
|
|
||||||
console.log('Committed with:', stdout);
|
|
||||||
|
|
||||||
expect(stdout).toMatch(japanesePattern);
|
|
||||||
});
|
|
||||||
|
|
||||||
await fixture.rm();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
137
tests/specs/cli/commits.ts
Normal file
137
tests/specs/cli/commits.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { testSuite, expect } from 'manten';
|
||||||
|
import { createFixture, createGit } from '../../utils.js';
|
||||||
|
|
||||||
|
const { OPENAI_KEY } = process.env;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OPENAI_KEY) {
|
||||||
|
console.warn('⚠️ process.env.OPENAI_KEY is necessary to run these tests. Skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CLI', async ({ test }) => {
|
||||||
|
const files = {
|
||||||
|
'.aicommits': `OPENAI_KEY=${OPENAI_KEY}`,
|
||||||
|
'data.json': 'Lorem ipsum dolor sit amet '.repeat(10),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
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. Make sure to stage your changes with `git add`.');
|
||||||
|
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', ['--oneline']);
|
||||||
|
console.log('Committed with:', commitMessage);
|
||||||
|
|
||||||
|
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', ['--oneline']);
|
||||||
|
console.log('Committed with:', commitMessage);
|
||||||
|
|
||||||
|
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', ['--oneline']);
|
||||||
|
console.log('Committed with:', commitMessage);
|
||||||
|
expect(commitMessage).toMatch(japanesePattern);
|
||||||
|
|
||||||
|
await fixture.rm();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
24
tests/specs/cli/error-cases.ts
Normal file
24
tests/specs/cli/error-cases.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { testSuite, expect } from 'manten';
|
||||||
|
import { createFixture, createGit } from '../../utils.js';
|
||||||
|
|
||||||
|
export default testSuite(({ describe }) => {
|
||||||
|
describe('Error cases', async ({ test }) => {
|
||||||
|
test('Fails on non-Git project', async () => {
|
||||||
|
const { fixture, aicommits } = await createFixture();
|
||||||
|
const { stdout, exitCode } = await aicommits([], { reject: false });
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(stdout).toMatch('The current directory must be a Git repository!');
|
||||||
|
await fixture.rm();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fails on no staged files', async () => {
|
||||||
|
const { fixture, aicommits } = await createFixture();
|
||||||
|
await createGit(fixture.path);
|
||||||
|
|
||||||
|
const { stdout, exitCode } = await aicommits([], { reject: false });
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(stdout).toMatch('No staged changes found. Make sure to stage your changes with `git add`.');
|
||||||
|
await fixture.rm();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
8
tests/specs/cli/index.ts
Normal file
8
tests/specs/cli/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { testSuite } from 'manten';
|
||||||
|
|
||||||
|
export default testSuite(({ describe }) => {
|
||||||
|
describe('CLI', ({ runTestSuite }) => {
|
||||||
|
runTestSuite(import('./error-cases.js'));
|
||||||
|
runTestSuite(import('./commits.js'));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { testSuite, expect } from 'manten';
|
import { testSuite, expect } from 'manten';
|
||||||
import { createFixture } from 'fs-fixture';
|
import { createFixture } from '../utils.js';
|
||||||
import { createAicommits } from '../utils.js';
|
|
||||||
|
|
||||||
export default testSuite(({ describe }) => {
|
export default testSuite(({ describe }) => {
|
||||||
describe('config', async ({ test }) => {
|
describe('config', async ({ test }) => {
|
||||||
const fixture = await createFixture();
|
const { fixture, aicommits } = await createFixture();
|
||||||
const aicommits = createAicommits({
|
|
||||||
home: fixture.path,
|
|
||||||
});
|
|
||||||
const configPath = path.join(fixture.path, '.aicommits');
|
const configPath = path.join(fixture.path, '.aicommits');
|
||||||
const openAiToken = 'OPENAI_KEY=sk-abc';
|
const openAiToken = 'OPENAI_KEY=sk-abc';
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { execa, execaNode, type Options } from 'execa';
|
import { execa, execaNode, type Options } from 'execa';
|
||||||
|
import {
|
||||||
|
createFixture as createFixtureBase,
|
||||||
|
type FileTree,
|
||||||
|
type FsFixture,
|
||||||
|
} from 'fs-fixture';
|
||||||
|
|
||||||
const aicommitsPath = path.resolve('./dist/cli.mjs');
|
const aicommitsPath = path.resolve('./dist/cli.mjs');
|
||||||
|
|
||||||
export const createAicommits = ({
|
const createAicommits = (fixture: FsFixture) => {
|
||||||
cwd,
|
|
||||||
home,
|
|
||||||
}: {
|
|
||||||
cwd?: string;
|
|
||||||
home: string;
|
|
||||||
}) => {
|
|
||||||
const homeEnv = {
|
const homeEnv = {
|
||||||
HOME: home, // Linux
|
HOME: fixture.path, // Linux
|
||||||
USERPROFILE: home, // Windows
|
USERPROFILE: fixture.path, // Windows
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,7 +19,7 @@ export const createAicommits = ({
|
|||||||
options?: Options,
|
options?: Options,
|
||||||
) => execaNode(aicommitsPath, args, {
|
) => execaNode(aicommitsPath, args, {
|
||||||
...options,
|
...options,
|
||||||
cwd,
|
cwd: fixture.path,
|
||||||
extendEnv: false,
|
extendEnv: false,
|
||||||
env: {
|
env: {
|
||||||
...homeEnv,
|
...homeEnv,
|
||||||
@@ -61,3 +60,15 @@ export const createGit = async (cwd: string) => {
|
|||||||
|
|
||||||
return git;
|
return git;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createFixture = async (
|
||||||
|
source?: string | FileTree,
|
||||||
|
) => {
|
||||||
|
const fixture = await createFixtureBase(source);
|
||||||
|
const aicommits = createAicommits(fixture);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fixture,
|
||||||
|
aicommits,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user