- Created skills/ directory - Moved 272 skills to skills/ subfolder - Kept agents/ at root level - Kept installation scripts and docs at root level Repository structure: - skills/ - All 272 skills from skills.sh - agents/ - Agent definitions - *.sh, *.ps1 - Installation scripts - README.md, etc. - Documentation Co-Authored-By: Claude <noreply@anthropic.com>
359 lines
10 KiB
TypeScript
359 lines
10 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import {
|
|
type CommandRunner,
|
|
formatReleaseNotes,
|
|
generateChangelog,
|
|
getContributors,
|
|
getContributorsForRepo,
|
|
getLatestReleasedTag,
|
|
isIncludedCommit,
|
|
runChangelog,
|
|
} from '../scripts/generate-changelog';
|
|
|
|
type RunnerResponse = string | (() => string) | (() => Promise<string>);
|
|
|
|
function createRunner(responses: Record<string, RunnerResponse>): CommandRunner {
|
|
return (strings, ...values) => {
|
|
const command = strings.reduce(
|
|
(acc, part, index) => `${acc}${part}${String(values[index] ?? '')}`,
|
|
'',
|
|
);
|
|
return {
|
|
text: async () => {
|
|
const response = responses[command];
|
|
if (response === undefined) {
|
|
throw new Error(`Unexpected command: ${command}`);
|
|
}
|
|
if (typeof response === 'function') {
|
|
return await response();
|
|
}
|
|
return response;
|
|
},
|
|
};
|
|
};
|
|
}
|
|
|
|
describe('isIncludedCommit', () => {
|
|
describe('simple prefixes', () => {
|
|
test('includes feat: commits', () => {
|
|
expect(isIncludedCommit('feat: add new feature')).toBe(true);
|
|
});
|
|
|
|
test('includes fix: commits', () => {
|
|
expect(isIncludedCommit('fix: resolve bug')).toBe(true);
|
|
});
|
|
|
|
test('excludes chore: commits', () => {
|
|
expect(isIncludedCommit('chore: update deps')).toBe(false);
|
|
});
|
|
|
|
test('excludes docs: commits', () => {
|
|
expect(isIncludedCommit('docs: update readme')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('scoped prefixes', () => {
|
|
test('includes feat(scope): commits', () => {
|
|
expect(isIncludedCommit('feat(api): add endpoint')).toBe(true);
|
|
});
|
|
|
|
test('includes fix(scope): commits', () => {
|
|
expect(isIncludedCommit('fix(commands): resolve issue')).toBe(true);
|
|
});
|
|
|
|
test('includes feat(multi-word): commits', () => {
|
|
expect(isIncludedCommit('feat(user-auth): add login')).toBe(true);
|
|
});
|
|
|
|
test('excludes chore(scope): commits', () => {
|
|
expect(isIncludedCommit('chore(deps): update')).toBe(false);
|
|
});
|
|
|
|
test('excludes docs(scope): commits', () => {
|
|
expect(isIncludedCommit('docs(readme): update')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('with git hash prefix', () => {
|
|
test('includes abc1234 feat: commits', () => {
|
|
expect(isIncludedCommit('abc1234 feat: add feature')).toBe(true);
|
|
});
|
|
|
|
test('includes abc1234 fix(scope): commits', () => {
|
|
expect(isIncludedCommit('abc1234 fix(commands): fix bug')).toBe(true);
|
|
});
|
|
|
|
test('excludes abc1234 chore: commits', () => {
|
|
expect(isIncludedCommit('abc1234 chore: update')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('case insensitivity', () => {
|
|
test('includes FEAT: commits', () => {
|
|
expect(isIncludedCommit('FEAT: add feature')).toBe(true);
|
|
});
|
|
|
|
test('includes FIX(scope): commits', () => {
|
|
expect(isIncludedCommit('FIX(commands): fix bug')).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getLatestReleasedTag', () => {
|
|
test('returns latest tag', async () => {
|
|
const runner = createRunner({
|
|
"gh release list --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty'":
|
|
'v1.2.3\n',
|
|
});
|
|
|
|
await expect(getLatestReleasedTag(runner)).resolves.toBe('v1.2.3');
|
|
});
|
|
|
|
test('returns null on failure', async () => {
|
|
const runner = createRunner({});
|
|
|
|
await expect(getLatestReleasedTag(runner)).resolves.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('formatReleaseNotes', () => {
|
|
test('renders sections and contributors', () => {
|
|
const notes = formatReleaseNotes(
|
|
{
|
|
core: ['- abc123 feat: core change'],
|
|
claudeCode: ['- def456 fix(commands): adjust'],
|
|
openCode: ['- ghi789 fix(opencode): tweak'],
|
|
},
|
|
['', '**Thank you to 1 community contributor:**', '- @alice:', ' - feat: add thing'],
|
|
);
|
|
|
|
expect(notes).toEqual([
|
|
'## Core',
|
|
'- abc123 feat: core change',
|
|
'',
|
|
'## Claude Code',
|
|
'- def456 fix(commands): adjust',
|
|
'',
|
|
'## OpenCode',
|
|
'- ghi789 fix(opencode): tweak',
|
|
'',
|
|
'**Thank you to 1 community contributor:**',
|
|
'- @alice:',
|
|
' - feat: add thing',
|
|
]);
|
|
});
|
|
|
|
test('renders empty sections without contributors', () => {
|
|
const notes = formatReleaseNotes({ core: [], claudeCode: [], openCode: [] }, []);
|
|
|
|
expect(notes).toEqual([
|
|
'## Core',
|
|
'No changes in this release',
|
|
'',
|
|
'## Claude Code',
|
|
'No changes in this release',
|
|
'',
|
|
'## OpenCode',
|
|
'No changes in this release',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('generateChangelog', () => {
|
|
test('categorizes commits by changed files', async () => {
|
|
const runner = createRunner({
|
|
'git log v1.0.0..HEAD --oneline --format="%h %s"': [
|
|
'abc123 feat: core change',
|
|
'bcd234 fix(commands): adjust',
|
|
'cde345 fix(opencode): tweak',
|
|
'eee111 feat: missing files',
|
|
'fff222 chore: skip',
|
|
].join('\n'),
|
|
'git diff-tree --no-commit-id --name-only -r abc123': 'src/core/analyze.ts\n',
|
|
'git diff-tree --no-commit-id --name-only -r bcd234': 'commands/example.json\n',
|
|
'git diff-tree --no-commit-id --name-only -r cde345': '.opencode/config.json\n',
|
|
'git diff-tree --no-commit-id --name-only -r eee111': () => {
|
|
throw new Error('boom');
|
|
},
|
|
});
|
|
|
|
const changelog = await generateChangelog('v1.0.0', runner);
|
|
|
|
expect(changelog).toEqual({
|
|
core: ['- abc123 feat: core change', '- eee111 feat: missing files'],
|
|
claudeCode: ['- bcd234 fix(commands): adjust'],
|
|
openCode: ['- cde345 fix(opencode): tweak'],
|
|
});
|
|
});
|
|
|
|
test('returns empty categories when git log fails', async () => {
|
|
const runner = createRunner({});
|
|
|
|
const changelog = await generateChangelog('v1.0.0', runner);
|
|
|
|
expect(changelog).toEqual({
|
|
core: [],
|
|
claudeCode: [],
|
|
openCode: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getContributorsForRepo', () => {
|
|
test('includes unique contributors and their commits', async () => {
|
|
const compare = [
|
|
JSON.stringify({
|
|
login: 'alice',
|
|
message: 'feat: add thing\n\nBody',
|
|
}),
|
|
JSON.stringify({
|
|
login: 'bob',
|
|
message: 'fix: resolve issue',
|
|
}),
|
|
JSON.stringify({
|
|
login: 'alice',
|
|
message: 'feat: follow-up',
|
|
}),
|
|
JSON.stringify({
|
|
login: 'kenryu42',
|
|
message: 'feat: excluded author',
|
|
}),
|
|
JSON.stringify({
|
|
login: null,
|
|
message: 'feat: missing author',
|
|
}),
|
|
JSON.stringify({
|
|
login: 'carol',
|
|
message: 'chore: ignore',
|
|
}),
|
|
].join('\n');
|
|
|
|
const runner = createRunner({
|
|
'gh api "/repos/example/repo/compare/v1.0.0...HEAD" --jq \'.commits[] | {login: .author.login, message: .commit.message}\'':
|
|
compare,
|
|
});
|
|
|
|
const notes = await getContributorsForRepo('v1.0.0', 'example/repo', runner);
|
|
|
|
expect(notes).toEqual([
|
|
'',
|
|
'**Thank you to 2 community contributors:**',
|
|
'- @alice:',
|
|
' - feat: add thing',
|
|
' - feat: follow-up',
|
|
'- @bob:',
|
|
' - fix: resolve issue',
|
|
]);
|
|
});
|
|
|
|
test('returns empty list when no contributors qualify', async () => {
|
|
const compare = [
|
|
JSON.stringify({
|
|
login: 'kenryu42',
|
|
message: 'feat: excluded author',
|
|
}),
|
|
JSON.stringify({
|
|
login: 'carol',
|
|
message: 'chore: ignore',
|
|
}),
|
|
].join('\n');
|
|
|
|
const runner = createRunner({
|
|
'gh api "/repos/example/repo/compare/v1.0.0...HEAD" --jq \'.commits[] | {login: .author.login, message: .commit.message}\'':
|
|
compare,
|
|
});
|
|
|
|
const notes = await getContributorsForRepo('v1.0.0', 'example/repo', runner);
|
|
|
|
expect(notes).toEqual([]);
|
|
});
|
|
|
|
test('returns empty list on command failure', async () => {
|
|
const runner = createRunner({});
|
|
|
|
const notes = await getContributorsForRepo('v1.0.0', 'example/repo', runner);
|
|
|
|
expect(notes).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('getContributors', () => {
|
|
test('uses default repo wrapper', async () => {
|
|
const runner = createRunner({
|
|
'gh api "/repos/kenryu42/claude-code-safety-net/compare/v1.0.0...HEAD" --jq \'.commits[] | {login: .author.login, message: .commit.message}\'':
|
|
JSON.stringify({
|
|
login: 'alice',
|
|
message: 'feat: add thing',
|
|
}),
|
|
});
|
|
|
|
const notes = await getContributors('v1.0.0', runner);
|
|
|
|
expect(notes).toEqual([
|
|
'',
|
|
'**Thank you to 1 community contributor:**',
|
|
'- @alice:',
|
|
' - feat: add thing',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('runChangelog', () => {
|
|
test('prints initial release when no tag exists', async () => {
|
|
const runner = createRunner({
|
|
"gh release list --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty'":
|
|
'\n',
|
|
});
|
|
const logs: string[] = [];
|
|
|
|
await runChangelog({
|
|
runner,
|
|
log: (message) => {
|
|
logs.push(message);
|
|
},
|
|
});
|
|
|
|
expect(logs).toEqual(['Initial release']);
|
|
});
|
|
|
|
test('prints changelog and contributors for tagged releases', async () => {
|
|
const compare = JSON.stringify({
|
|
login: 'alice',
|
|
message: 'feat: add thing',
|
|
});
|
|
const runner = createRunner({
|
|
"gh release list --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty'":
|
|
'v1.0.0\n',
|
|
'git log v1.0.0..HEAD --oneline --format="%h %s"': 'abc123 feat: core change',
|
|
'git diff-tree --no-commit-id --name-only -r abc123': 'src/core/analyze.ts\n',
|
|
'gh api "/repos/kenryu42/claude-code-safety-net/compare/v1.0.0...HEAD" --jq \'.commits[] | {login: .author.login, message: .commit.message}\'':
|
|
compare,
|
|
});
|
|
const logs: string[] = [];
|
|
|
|
await runChangelog({
|
|
runner,
|
|
log: (message) => {
|
|
logs.push(message);
|
|
},
|
|
});
|
|
|
|
expect(logs).toEqual([
|
|
[
|
|
'## Core',
|
|
'- abc123 feat: core change',
|
|
'',
|
|
'## Claude Code',
|
|
'No changes in this release',
|
|
'',
|
|
'## OpenCode',
|
|
'No changes in this release',
|
|
'',
|
|
'**Thank you to 1 community contributor:**',
|
|
'- @alice:',
|
|
' - feat: add thing',
|
|
].join('\n'),
|
|
]);
|
|
});
|
|
});
|