Files
Aperant/scripts/update-readme.test.mjs
T
André Mikalsen d958fa65cb fix: harden CI/CD with coverage enforcement, eliminate Python, add 22 test files (#1945)
* feat: harden CI/CD with coverage enforcement, eliminate Python, add 22 test files

- Port scripts/update-readme.py to Node.js (update-readme.mjs) and remove
  Python dependency from release workflow
- Add coverage thresholds to vitest.config.ts with @vitest/coverage-v8
- Run coverage on ubuntu in CI, upload artifacts, add PR coverage comments
- Create E2E workflow (.github/workflows/e2e.yml) with Playwright on ubuntu
- Add test:unit and test:integration scripts for separated test execution
- Add 22 new test files (389 tests) covering previously untested AI layer:
  - 6 builtin tool tests (edit, bash, read, write, glob, grep)
  - 4 orchestration tests (recovery-manager, qa-loop, qa-reports, parallel-executor)
  - 5 auth/client/mcp tests (resolver, types, factory, client, registry)
  - 7 runner tests (changelog, commit-message, ideation, insights, insight-extractor,
    merge-resolver, roadmap)

Total: 206 test files, 4594 tests passing. Zero Python dependencies in CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve Windows path separator failures in tests

Use path.join() and path.resolve() for cross-platform path construction
in test assertions instead of hardcoded forward slashes. Also mark E2E
workflow as continue-on-error for pre-existing __dirname ESM issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 19:56:14 +01:00

233 lines
8.7 KiB
JavaScript

/**
* Tests for update-readme.mjs
* Run with: node --test scripts/update-readme.test.mjs
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { chdir, cwd } from 'node:process';
import { validateVersion, updateSection, updateReadme } from './update-readme.mjs';
// ---------------------------------------------------------------------------
// validateVersion
// ---------------------------------------------------------------------------
test('validateVersion - accepts valid stable versions', () => {
assert.equal(validateVersion('2.8.0'), true);
assert.equal(validateVersion('1.0.0'), true);
assert.equal(validateVersion('10.20.30'), true);
});
test('validateVersion - accepts valid prerelease versions', () => {
assert.equal(validateVersion('2.8.0-beta.1'), true);
assert.equal(validateVersion('2.8.0-alpha.10'), true);
assert.equal(validateVersion('1.0.0-rc.3'), true);
});
test('validateVersion - rejects invalid versions', () => {
assert.equal(validateVersion('2.8'), false);
assert.equal(validateVersion('2.8.0.1'), false);
assert.equal(validateVersion('v2.8.0'), false);
assert.equal(validateVersion('2.8.0-beta'), false); // missing .N
assert.equal(validateVersion(''), false);
assert.equal(validateVersion('abc'), false);
assert.equal(validateVersion('2.8.0-win32'), false); // no dot suffix
});
// ---------------------------------------------------------------------------
// updateSection
// ---------------------------------------------------------------------------
test('updateSection - replaces content between markers', () => {
const content = [
'before',
'<!-- START -->',
'tag/v2.7.0)',
'<!-- END -->',
'after',
].join('\n');
const result = updateSection(
content,
'<!-- START -->',
'<!-- END -->',
[[String.raw`tag/v\d+\.\d+\.\d+\)`, 'tag/v2.8.0)']],
);
assert.ok(result.includes('tag/v2.8.0)'), 'should update the version inside the section');
assert.ok(result.includes('before'), 'should keep content before markers');
assert.ok(result.includes('after'), 'should keep content after markers');
});
test('updateSection - applies multiple replacements in order', () => {
const content = [
'<!-- S -->',
'Auto-Claude-2.7.0-mac.dmg download/v2.7.0/file',
'<!-- E -->',
].join('\n');
const semver = String.raw`\d+\.\d+\.\d+(?:-[a-zA-Z]+\.[a-zA-Z0-9.]+)?`;
const result = updateSection(content, '<!-- S -->', '<!-- E -->', [
[`Auto-Claude-${semver}`, 'Auto-Claude-2.8.0'],
[`download/v${semver}/`, 'download/v2.8.0/'],
]);
assert.ok(result.includes('Auto-Claude-2.8.0'), 'should replace filename');
assert.ok(result.includes('download/v2.8.0/'), 'should replace download path');
});
test('updateSection - handles multiline sections (dotAll)', () => {
const content = '<!-- M -->\nline1\nline2\n<!-- M_END -->';
const result = updateSection(content, '<!-- M -->', '<!-- M_END -->', [
['line1', 'replaced'],
]);
assert.ok(result.includes('replaced'));
assert.ok(result.includes('line2'));
});
test('updateSection - no markers leaves text unchanged', () => {
const content = 'no markers here';
const result = updateSection(content, '<!-- A -->', '<!-- B -->', [['x', 'y']]);
assert.equal(result, content);
});
// ---------------------------------------------------------------------------
// updateReadme - stable release
// ---------------------------------------------------------------------------
/**
* Build a minimal README with all section markers used by the script.
*
* Note: The download URL path (download/v${version}/) is what gets tested for
* version replacement. The filename after the version uses "-win" (no dot)
* so the semver regex — which requires a dot inside any prerelease-like suffix
* — does NOT greedily consume the platform suffix as part of the version.
*/
function buildSampleReadme(stableVersion, betaVersion) {
const sv = stableVersion;
const bv = betaVersion;
// Shields.io badge format uses -- for hyphens
const svBadge = sv.replaceAll('-', '--');
const bvBadge = bv.replaceAll('-', '--');
return [
`<!-- TOP_VERSION_BADGE -->`,
`[![version](https://img.shields.io/badge/version-${svBadge}-blue)](https://github.com/example/releases/tag/v${sv})`,
`<!-- TOP_VERSION_BADGE_END -->`,
``,
`[![stable](https://img.shields.io/badge/stable-${svBadge}-blue)](https://example.com)`,
``,
`<!-- STABLE_VERSION_BADGE -->`,
`[v${sv}](https://github.com/example/releases/tag/v${sv})`,
`<!-- STABLE_VERSION_BADGE_END -->`,
``,
`<!-- STABLE_DOWNLOADS -->`,
`https://example.com/download/v${sv}/Auto-Claude-${sv}-win`,
`<!-- STABLE_DOWNLOADS_END -->`,
``,
`[![beta](https://img.shields.io/badge/beta-${bvBadge}-orange)](https://example.com)`,
``,
`<!-- BETA_VERSION_BADGE -->`,
`[v${bv}](https://github.com/example/releases/tag/v${bv})`,
`<!-- BETA_VERSION_BADGE_END -->`,
``,
`<!-- BETA_DOWNLOADS -->`,
`https://example.com/download/v${bv}/Auto-Claude-${bv}-win`,
`<!-- BETA_DOWNLOADS_END -->`,
].join('\n');
}
/** Run updateReadme in a temp directory with a fixture README.md. */
function withTempReadme(readmeContent, fn) {
const tmpDir = mkdtempSync(join(tmpdir(), 'update-readme-test-'));
const original = cwd();
try {
writeFileSync(join(tmpDir, 'README.md'), readmeContent, 'utf8');
chdir(tmpDir);
fn(tmpDir);
} finally {
chdir(original);
rmSync(tmpDir, { recursive: true, force: true });
}
}
test('updateReadme - stable release updates TOP_VERSION_BADGE, STABLE_VERSION_BADGE, STABLE_DOWNLOADS', () => {
const readme = buildSampleReadme('2.7.0', '2.8.0-beta.1');
withTempReadme(readme, (dir) => {
const changed = updateReadme('2.8.0', false);
assert.equal(changed, true, 'should report changes made');
const result = readFileSync(join(dir, 'README.md'), 'utf8');
// TOP_VERSION_BADGE section updated
assert.ok(result.includes('version-2.8.0-blue'), 'top badge version updated');
assert.ok(result.includes('tag/v2.8.0)'), 'top badge link updated');
// Stable badge outside section updated
assert.ok(result.includes('stable-2.8.0-blue'), 'standalone stable badge updated');
// STABLE_VERSION_BADGE section updated
assert.ok(result.includes('tag/v2.8.0)'), 'stable version link updated');
// STABLE_DOWNLOADS section updated
assert.ok(result.includes('download/v2.8.0/'), 'stable download path updated');
assert.ok(result.includes('Auto-Claude-2.8.0'), 'stable download filename updated');
// Beta section NOT modified
assert.ok(result.includes('beta-2.8.0--beta.1-orange'), 'beta badge unchanged');
assert.ok(result.includes('download/v2.8.0-beta.1/'), 'beta download unchanged');
});
});
test('updateReadme - stable release returns false when no changes needed', () => {
const readme = buildSampleReadme('2.8.0', '2.8.0-beta.1');
withTempReadme(readme, () => {
const changed = updateReadme('2.8.0', false);
assert.equal(changed, false, 'should report no changes when already up to date');
});
});
// ---------------------------------------------------------------------------
// updateReadme - prerelease
// ---------------------------------------------------------------------------
test('updateReadme - prerelease updates BETA_VERSION_BADGE and BETA_DOWNLOADS', () => {
const readme = buildSampleReadme('2.7.0', '2.7.0-beta.5');
withTempReadme(readme, (dir) => {
const changed = updateReadme('2.8.0-beta.1', true);
assert.equal(changed, true, 'should report changes made');
const result = readFileSync(join(dir, 'README.md'), 'utf8');
// Beta badge updated (-- escaped)
assert.ok(result.includes('beta-2.8.0--beta.1-orange'), 'beta badge updated');
// BETA_VERSION_BADGE section updated
assert.ok(result.includes('tag/v2.8.0-beta.1)'), 'beta version link updated');
// BETA_DOWNLOADS section updated
assert.ok(result.includes('download/v2.8.0-beta.1/'), 'beta download path updated');
assert.ok(result.includes('Auto-Claude-2.8.0-beta.1'), 'beta download filename updated');
// Stable section NOT modified
assert.ok(result.includes('stable-2.7.0-blue'), 'stable badge unchanged');
assert.ok(result.includes('download/v2.7.0/'), 'stable download unchanged');
});
});
test('updateReadme - prerelease returns false when no changes needed', () => {
const readme = buildSampleReadme('2.7.0', '2.8.0-beta.1');
withTempReadme(readme, () => {
const changed = updateReadme('2.8.0-beta.1', true);
assert.equal(changed, false, 'should report no changes when already up to date');
});
});