Files
aryx/tests/scripts/release.test.ts
T
David KayaandCopilot f8fe9d866e fix: use platform-native paths in release tests for cross-OS compatibility
The release workflow tests hardcoded Windows-style backslash paths
(C:\workspace\...) which caused path.relative() to produce incorrect
results on Linux and macOS, where backslashes are not path separators.

Replace hardcoded paths with resolve()/join() calls that produce
platform-native paths, fixing CI failures on Linux and macOS.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-16 13:53:49 +02:00

227 lines
6.2 KiB
TypeScript

import { join, resolve } from 'node:path';
import { describe, expect, test } from 'bun:test';
import {
incrementVersion,
parseReleaseBump,
runReleaseWorkflow,
type GitCommandResult,
type ReleaseDependencies,
} from '../../scripts/release';
const testRepositoryRoot = resolve('/test-workspace/aryx');
const testPackageJsonPath = join(testRepositoryRoot, 'package.json');
function gitSuccess(stdout = '', stderr = ''): GitCommandResult {
return {
exitCode: 0,
stdout,
stderr,
};
}
function gitFailure(exitCode = 1, stdout = '', stderr = ''): GitCommandResult {
return {
exitCode,
stdout,
stderr,
};
}
function createReleaseDependencies(options?: {
packageJsonText?: string;
gitResults?: GitCommandResult[];
}) {
let packageJsonText =
options?.packageJsonText ??
`{
"name": "aryx",
"version": "0.0.26"
}
`;
const gitResults = [...(options?.gitResults ?? [])];
const commands: string[][] = [];
const writes: Array<{ path: string; content: string }> = [];
const logs: string[] = [];
const dependencies: ReleaseDependencies = {
readText: async () => packageJsonText,
writeText: async (path, content) => {
packageJsonText = content;
writes.push({ path, content });
},
runGit: async (args) => {
commands.push(args);
const result = gitResults.shift();
if (!result) {
throw new Error(`Unexpected git command: ${args.join(' ')}`);
}
return result;
},
log: (message) => {
logs.push(message);
},
};
return {
dependencies,
commands,
writes,
logs,
getPackageJsonText: () => packageJsonText,
};
}
describe('release bump parsing', () => {
test('defaults to patch when no bump argument is provided', () => {
expect(parseReleaseBump()).toBe('patch');
});
test('rejects unsupported bump arguments', () => {
expect(() => parseReleaseBump('prerelease')).toThrow(
'Unsupported release bump "prerelease". Use patch, minor, or major.',
);
});
});
describe('version incrementing', () => {
test('supports patch, minor, and major bumps', () => {
expect(incrementVersion('0.0.26', 'patch')).toBe('0.0.27');
expect(incrementVersion('0.0.26', 'minor')).toBe('0.1.0');
expect(incrementVersion('0.0.26', 'major')).toBe('1.0.0');
});
});
describe('release workflow', () => {
test('bumps package.json, commits, tags, and pushes the tracked branch', async () => {
const { dependencies, commands, writes, logs, getPackageJsonText } = createReleaseDependencies({
gitResults: [
gitSuccess(),
gitSuccess('main\n'),
gitSuccess('origin\n'),
gitSuccess('refs/heads/main\n'),
gitSuccess(),
gitSuccess(),
gitSuccess(),
gitSuccess(),
gitSuccess(),
],
});
const result = await runReleaseWorkflow(
{
repositoryRoot: testRepositoryRoot,
packageJsonPath: testPackageJsonPath,
},
dependencies,
);
expect(result).toMatchObject({
bump: 'patch',
currentVersion: '0.0.26',
nextVersion: '0.0.27',
tagName: 'v0.0.27',
localBranch: 'main',
remote: 'origin',
upstreamBranch: 'main',
});
expect(writes).toHaveLength(1);
expect(getPackageJsonText()).toContain('"version": "0.0.27"');
expect(commands).toEqual([
['status', '--porcelain'],
['symbolic-ref', '--quiet', '--short', 'HEAD'],
['config', '--get', 'branch.main.remote'],
['config', '--get', 'branch.main.merge'],
['tag', '--list', 'v0.0.27'],
['add', 'package.json'],
['commit', '-m', 'chore: release v0.0.27'],
['tag', '-a', 'v0.0.27', '-m', 'Release v0.0.27'],
['push', '--follow-tags', 'origin', 'HEAD:main'],
]);
expect(logs).toEqual(['Released v0.0.27 from main to origin/main.']);
});
test('fails before writing package.json when the worktree is dirty', async () => {
const { dependencies, commands, writes } = createReleaseDependencies({
gitResults: [gitSuccess(' M README.md\n')],
});
await expect(
runReleaseWorkflow(
{
repositoryRoot: testRepositoryRoot,
packageJsonPath: testPackageJsonPath,
},
dependencies,
),
).rejects.toThrow('Release requires a clean git worktree. Commit, stash, or discard changes first.');
expect(writes).toHaveLength(0);
expect(commands).toEqual([['status', '--porcelain']]);
});
test('fails before writing package.json when the next release tag already exists', async () => {
const { dependencies, commands, writes } = createReleaseDependencies({
gitResults: [
gitSuccess(),
gitSuccess('main\n'),
gitSuccess('origin\n'),
gitSuccess('refs/heads/main\n'),
gitSuccess('v0.1.0\n'),
],
});
await expect(
runReleaseWorkflow(
{
repositoryRoot: testRepositoryRoot,
packageJsonPath: testPackageJsonPath,
bumpArg: 'minor',
},
dependencies,
),
).rejects.toThrow('Tag v0.1.0 already exists. Choose a different release version.');
expect(writes).toHaveLength(0);
expect(commands).toEqual([
['status', '--porcelain'],
['symbolic-ref', '--quiet', '--short', 'HEAD'],
['config', '--get', 'branch.main.remote'],
['config', '--get', 'branch.main.merge'],
['tag', '--list', 'v0.1.0'],
]);
});
test('fails when the current branch does not have an upstream', async () => {
const { dependencies, commands, writes } = createReleaseDependencies({
gitResults: [
gitSuccess(),
gitSuccess('release\n'),
gitFailure(),
gitFailure(),
],
});
await expect(
runReleaseWorkflow(
{
repositoryRoot: testRepositoryRoot,
packageJsonPath: testPackageJsonPath,
},
dependencies,
),
).rejects.toThrow('Release requires an upstream branch for release. Configure tracking before running the release helper.');
expect(writes).toHaveLength(0);
expect(commands).toEqual([
['status', '--porcelain'],
['symbolic-ref', '--quiet', '--short', 'HEAD'],
['config', '--get', 'branch.release.remote'],
['config', '--get', 'branch.release.merge'],
]);
});
});