Files
promptfoo--promptfoo/test/scripts/validate-changelog.test.ts
T
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

578 lines
14 KiB
TypeScript

import { describe, expect, it } from 'vitest';
interface ValidationResult {
valid: boolean;
error?: string;
message?: string;
entries?: string[];
footer?: string;
}
const {
findEntriesMissingPrNumber,
findEntriesWithCommitHash,
parseAddedEntries,
findUnreleasedEnd,
findEntriesInVersionedSections,
validateChangelogDiff,
} = require('../../scripts/validate-changelog.cjs') as {
findEntriesMissingPrNumber: (entries: string[]) => string[];
findEntriesWithCommitHash: (entries: string[]) => string[];
parseAddedEntries: (diff: string) => string[];
findUnreleasedEnd: (changelogContent: string) => number;
findEntriesInVersionedSections: (
diff: string,
unreleasedEnd: number,
) => Array<{ line: number; content: string }>;
validateChangelogDiff: (options: {
diff: string;
changelogContent: string;
branchName: string;
prNumber: string;
}) => ValidationResult;
};
describe('validate-changelog', () => {
describe('findEntriesMissingPrNumber', () => {
it('should return empty array when all entries have PR numbers', () => {
const entries = [
'- feat(cli): add new feature (#1234)',
'- fix(api): resolve bug (#5678)',
'- chore: update deps (#9999)',
];
expect(findEntriesMissingPrNumber(entries)).toEqual([]);
});
it('should find entries missing PR numbers', () => {
const entries = [
'- feat(cli): add new feature (#1234)',
'- fix(api): resolve bug',
'- chore: update deps (#9999)',
'- docs: update readme',
];
expect(findEntriesMissingPrNumber(entries)).toEqual([
'- fix(api): resolve bug',
'- docs: update readme',
]);
});
it('should handle empty array', () => {
expect(findEntriesMissingPrNumber([])).toEqual([]);
});
it('should recognize various PR number formats', () => {
const entries = ['- feat: feature (#1)', '- fix: fix (#12345)', '- chore: update (#999999)'];
expect(findEntriesMissingPrNumber(entries)).toEqual([]);
});
it('should not be fooled by hash in text without parentheses', () => {
const entries = ['- feat: implement #1234 feature'];
expect(findEntriesMissingPrNumber(entries)).toEqual(['- feat: implement #1234 feature']);
});
});
describe('findEntriesWithCommitHash', () => {
it('should return empty array when no commit hashes present', () => {
const entries = ['- feat(cli): add new feature (#1234)', '- fix(api): resolve bug (#5678)'];
expect(findEntriesWithCommitHash(entries)).toEqual([]);
});
it('should find entries with commit hashes', () => {
const entries = [
'- feat(cli): add new feature (abc1234)',
'- fix(api): resolve bug (#5678)',
'- chore: update deps (1234567)',
];
expect(findEntriesWithCommitHash(entries)).toEqual([
'- feat(cli): add new feature (abc1234)',
'- chore: update deps (1234567)',
]);
});
it('should flag entries that have both hash and PR number', () => {
// Commit hashes should not be used even if PR number is present
const entries = ['- feat: feature (abc1234) related to (#1234)'];
expect(findEntriesWithCommitHash(entries)).toEqual([
'- feat: feature (abc1234) related to (#1234)',
]);
});
it('should recognize long commit hashes (40 chars)', () => {
const entries = ['- fix: bug (1234567890abcdef1234567890abcdef12345678)'];
expect(findEntriesWithCommitHash(entries)).toEqual([
'- fix: bug (1234567890abcdef1234567890abcdef12345678)',
]);
});
it('should not flag short hashes (less than 7 chars)', () => {
const entries = ['- fix: bug (abc123)'];
expect(findEntriesWithCommitHash(entries)).toEqual([]);
});
});
describe('parseAddedEntries', () => {
it('should extract added entry lines from diff', () => {
const diff = `diff --git a/CHANGELOG.md b/CHANGELOG.md
index abc1234..def5678 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
## [Unreleased]
### Added
+
+- feat(cli): add new feature (#1234)
+- fix(api): resolve bug (#5678)
## [1.0.0] - 2024-01-01`;
const entries = parseAddedEntries(diff);
expect(entries).toEqual([
'- feat(cli): add new feature (#1234)',
'- fix(api): resolve bug (#5678)',
]);
});
it('should ignore non-entry added lines', () => {
const diff = `+### Added
+
+- feat: new feature (#1234)
+Some context line`;
const entries = parseAddedEntries(diff);
expect(entries).toEqual(['- feat: new feature (#1234)']);
});
it('should handle empty diff', () => {
expect(parseAddedEntries('')).toEqual([]);
});
it('should handle diff with only removals', () => {
const diff = `--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,3 +10,2 @@
-- feat: removed feature (#1234)
- feat: kept feature (#5678)`;
expect(parseAddedEntries(diff)).toEqual([]);
});
});
describe('findUnreleasedEnd', () => {
it('should find line number of first versioned section', () => {
const content = `# Changelog
## [Unreleased]
### Added
- feat: new feature (#1234)
## [1.0.0] - 2024-01-01
### Added
- feat: released feature (#100)`;
expect(findUnreleasedEnd(content)).toBe(9);
});
it('should return 999999 when no versioned sections exist', () => {
const content = `# Changelog
## [Unreleased]
### Added
- feat: new feature (#1234)`;
expect(findUnreleasedEnd(content)).toBe(999999);
});
it('should find first versioned section among multiple', () => {
const content = `# Changelog
## [Unreleased]
## [2.0.0] - 2024-06-01
## [1.0.0] - 2024-01-01`;
expect(findUnreleasedEnd(content)).toBe(5);
});
});
describe('findEntriesInVersionedSections', () => {
it('should find entries added after unreleased section', () => {
const diff = `@@ -1,10 +1,12 @@
# Changelog
## [Unreleased]
### Added
## [1.0.0] - 2024-01-01
### Added
+
+- feat: added to old version (#1234)`;
const entries = findEntriesInVersionedSections(diff, 7);
expect(entries).toHaveLength(1);
expect(entries[0].content).toBe('- feat: added to old version (#1234)');
expect(entries[0].line).toBeGreaterThanOrEqual(7);
});
it('should not flag entries in unreleased section', () => {
const diff = `@@ -1,6 +1,8 @@
# Changelog
## [Unreleased]
### Added
+
+- feat: new feature (#1234)`;
const entries = findEntriesInVersionedSections(diff, 10);
expect(entries).toEqual([]);
});
it('should handle multiple hunks', () => {
const diff = `@@ -3,3 +3,4 @@
## [Unreleased]
+- feat: in unreleased (#1)
@@ -10,3 +11,4 @@
## [1.0.0]
+- feat: in versioned (#2)`;
const entries = findEntriesInVersionedSections(diff, 8);
expect(entries).toHaveLength(1);
expect(entries[0].content).toBe('- feat: in versioned (#2)');
});
});
describe('validateChangelogDiff', () => {
// Note: changelogContent should reflect the state AFTER the diff is applied
// (as it would be in the working directory during CI)
it('should pass validation for correct entries', () => {
const diff = `@@ -3,4 +3,6 @@
## [Unreleased]
### Added
+
+- feat: new feature (#1234)`;
// Changelog content after the diff is applied
const changelogAfterDiff = `# Changelog
## [Unreleased]
### Added
- feat: new feature (#1234)
## [1.0.0] - 2024-01-01
### Added
- feat: old feature (#100)`;
const result = validateChangelogDiff({
diff,
changelogContent: changelogAfterDiff,
branchName: 'feat/my-feature',
prNumber: '1234',
});
expect(result.valid).toBe(true);
});
it('should skip validation for version bump branches', () => {
const diff = `+- feat: no pr number`;
const changelogContent = `# Changelog
## [Unreleased]
### Added
- feat: no pr number
## [1.0.0] - 2024-01-01`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'chore/bump-version-12345',
prNumber: '1234',
});
expect(result.valid).toBe(true);
});
it('should fail when PR numbers are missing', () => {
const diff = `@@ -3,4 +3,6 @@
## [Unreleased]
### Added
+
+- feat: new feature without pr`;
const changelogContent = `# Changelog
## [Unreleased]
### Added
- feat: new feature without pr
## [1.0.0] - 2024-01-01`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'feat/my-feature',
prNumber: '1234',
});
expect(result.valid).toBe(false);
expect(result.error).toBe('missing_pr_number');
expect(result.entries).toContain('- feat: new feature without pr');
});
it('should fail when commit hashes are used instead of PR numbers', () => {
// Entry with a valid PR number AND a commit hash - tests that hashes are detected
const diff = `@@ -3,4 +3,6 @@
## [Unreleased]
### Added
+
+- feat: new feature (abc1234)`;
const changelogContent = `# Changelog
## [Unreleased]
### Added
- feat: new feature (abc1234)
## [1.0.0] - 2024-01-01`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'feat/my-feature',
prNumber: '1234',
});
// Note: This fails the "missing PR number" check first, since (abc1234) is not (#1234)
expect(result.valid).toBe(false);
expect(result.error).toBe('missing_pr_number');
});
it('should detect commit hash when entry also has PR-like text', () => {
// Entry with text containing # but using commit hash in parentheses
const diff = `+- feat: fix issue #123 (abc1234)`;
const changelogContent = `# Changelog
## [Unreleased]
### Added
- feat: fix issue #123 (abc1234)
## [1.0.0] - 2024-01-01`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'feat/my-feature',
prNumber: '1234',
});
// This should fail the commit hash check since it has # in text but not (#\d+) format
expect(result.valid).toBe(false);
expect(result.error).toBe('missing_pr_number');
});
it('should detect commit hash even when valid PR number is present', () => {
// Entry has valid PR format (#1234) AND a commit hash - should fail
const diff = `+- feat: new feature (abc1234) (#1234)`;
const changelogContent = `# Changelog
## [Unreleased]
### Added
- feat: new feature (abc1234) (#1234)
## [1.0.0] - 2024-01-01`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'feat/my-feature',
prNumber: '1234',
});
// Should pass PR number check but fail commit hash check
expect(result.valid).toBe(false);
expect(result.error).toBe('commit_hash_instead_of_pr');
});
it('should fail when entries are in versioned sections', () => {
const diff = `@@ -8,4 +8,6 @@
## [1.0.0] - 2024-01-01
### Added
+
+- feat: added to wrong section (#1234)`;
const changelogContent = `# Changelog
## [Unreleased]
### Added
## [1.0.0] - 2024-01-01
### Added
- feat: added to wrong section (#1234)
- feat: old feature (#100)`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'feat/my-feature',
prNumber: '1234',
});
expect(result.valid).toBe(false);
expect(result.error).toBe('entries_in_versioned_section');
});
it('should pass when no entries are added', () => {
const diff = `@@ -1,3 +1,3 @@
# Changelog
-old header
+new header`;
const changelogContent = `# Changelog
new header
## [Unreleased]
## [1.0.0] - 2024-01-01`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'feat/my-feature',
prNumber: '1234',
});
expect(result.valid).toBe(true);
});
it('should include PR number in error message', () => {
const diff = `+- feat: missing pr number`;
const changelogContent = `# Changelog
## [Unreleased]
### Added
- feat: missing pr number
## [1.0.0] - 2024-01-01`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'feat/my-feature',
prNumber: '5678',
});
expect(result.message).toContain('(#5678)');
});
it('should validate multiple entries correctly', () => {
const diff = `@@ -3,4 +3,8 @@
## [Unreleased]
### Added
+
+- feat: first feature (#1234)
+- fix: second fix (#5678)
+- chore: third update (#9999)`;
const changelogContent = `# Changelog
## [Unreleased]
### Added
- feat: first feature (#1234)
- fix: second fix (#5678)
- chore: third update (#9999)
## [1.0.0] - 2024-01-01
### Added
- feat: old feature (#100)`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'feat/my-feature',
prNumber: '1234',
});
expect(result.valid).toBe(true);
});
it('should fail if any entry is missing PR number', () => {
const diff = `@@ -3,4 +3,8 @@
## [Unreleased]
### Added
+
+- feat: first feature (#1234)
+- fix: second fix
+- chore: third update (#9999)`;
const changelogContent = `# Changelog
## [Unreleased]
### Added
- feat: first feature (#1234)
- fix: second fix
- chore: third update (#9999)
## [1.0.0] - 2024-01-01`;
const result = validateChangelogDiff({
diff,
changelogContent,
branchName: 'feat/my-feature',
prNumber: '1234',
});
expect(result.valid).toBe(false);
expect(result.error).toBe('missing_pr_number');
expect(result.entries).toEqual(['- fix: second fix']);
});
});
});