Same-day correction to ee2259f. That commit followed the order literally --
"use the tag's own message" -- and the result was an llm-security v8.0.0
release page reading "llm-security v8.0.0" and nothing else.
The defect is structural, not a typo: --create-tag mints -m "<name>
v<version>", so the tag message is mechanical EXACTLY where this helper made
the tag. The tag message is a good source only for tags written by hand.
The right source was already proven on the instance: llm-security v7.8.3's
release body is byte-for-byte its CHANGELOG `## [7.8.3]` section. So the
CHANGELOG is the org's established source, not a new invention -- and all 10
backfilled repos ship one (measured, 10/10).
- extractChangelogSection / releaseBodyFrom: pure, tested. Priority is
CHANGELOG section -> tag message -> empty, and the source is REPORTED so a
run says where the text came from rather than implying it wrote it.
- Three heading dialects are live and all three are covered: `## [6.0.0] -
date`, `## [0.2.0] -- date` (em-dash), `## v1.0 (date)`, and voyage's
`## v5.10.1 -- date -- trailing prose`. The version token matches exactly,
so 0.1.0-pre is not 0.1.0 and 1.1.0 is not 1.10.0. An empty section (the
standing `## [Unreleased]`) returns null so the caller falls through
instead of publishing a blank body.
- backfill gains --repair for the backlog the first cut created. It PATCHes
a PUBLISHED page, so the bar is strictly more informative, never merely
different: no CHANGELOG section means no update, and a hand-written body at
least as long as the section is left alone. Measured: that rule is what
protects portfolio-optimiser v1.1.0 (4750 hand-written chars vs 3704).
Dry-run over the org: 14 release objects would gain real notes, e.g.
llm-security v8.0.0 19 chars -> 10530, config-audit v6.0.0 19 -> 27309.
18 new tests, written red first. Suite 211/211; check-versions 12/12 OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
112 lines
5.6 KiB
JavaScript
112 lines
5.6 KiB
JavaScript
// Tests for the Forgejo release-object backfill.
|
|
// Pure selector is the unit under test — the API shell (curl via release-plugin.mjs's
|
|
// forgejoApi) is exercised against the live instance, not here.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { planBackfill, planRepair, EXCLUDED_TAGS } from './backfill-forgejo-releases.mjs';
|
|
|
|
const repo = (name, tags, releases = []) => ({ name, tags, releases });
|
|
const tag = (name, message = '') => ({ name, message });
|
|
|
|
test('plans a CREATE for a repo whose newest tag has no release object', () => {
|
|
const r = planBackfill({ repos: [repo('llm-security', [tag('v8.0.0', 'llm-security v8.0.0'), tag('v7.8.3')], ['v7.8.3'])] });
|
|
assert.equal(r.create.length, 1);
|
|
assert.equal(r.create[0].repo, 'llm-security');
|
|
assert.equal(r.create[0].tag, 'v8.0.0');
|
|
});
|
|
|
|
test('the release body is the tag message VERBATIM — the backfill invents no release notes', () => {
|
|
const msg = '0.10.0 — a bundle carries the images its sources declare\n\nFive readers place them.';
|
|
const r = planBackfill({ repos: [repo('llm-ingestion-okf', [tag('v0.10.0', msg)], [])] });
|
|
assert.equal(r.create[0].body, msg);
|
|
});
|
|
|
|
test('skips a repo whose newest tag already has a release object (idempotent re-run)', () => {
|
|
const r = planBackfill({ repos: [repo('ai-psychosis', [tag('v1.2.2')], ['v1.2.2'])] });
|
|
assert.equal(r.create.length, 0);
|
|
assert.equal(r.skip.length, 1);
|
|
assert.match(r.skip[0].reason, /already has a release/);
|
|
});
|
|
|
|
test('skips a repo with no tags at all, and NAMES the reason — never a silent drop', () => {
|
|
const r = planBackfill({ repos: [repo('jobbsok', [])] });
|
|
assert.equal(r.create.length, 0);
|
|
assert.equal(r.skip.length, 1);
|
|
assert.equal(r.skip[0].repo, 'jobbsok');
|
|
assert.match(r.skip[0].reason, /no tags/);
|
|
});
|
|
|
|
test('only the NEWEST tag is backfilled — an older tag without a release stays untouched', () => {
|
|
const r = planBackfill({ repos: [repo('config-audit', [tag('v6.0.0'), tag('v5.9.0'), tag('v5.8.0')], [])] });
|
|
assert.deepEqual(r.create.map(c => c.tag), ['v6.0.0']);
|
|
});
|
|
|
|
test('an excluded tag is reported as a documented EXCEPTION, not dropped silently', () => {
|
|
const r = planBackfill({ repos: [repo('ktg-plugin-marketplace', [tag('pre-polyrepo-archive', 'Archive of the monorepo')], [])] });
|
|
assert.equal(r.create.length, 0);
|
|
assert.equal(r.skip.length, 1);
|
|
assert.equal(r.skip[0].excluded, true);
|
|
assert.match(r.skip[0].reason, /archive/i);
|
|
});
|
|
|
|
test('the exclusion register names the repo AND the tag — it can never blanket-skip a repo', () => {
|
|
const entry = EXCLUDED_TAGS['ktg-plugin-marketplace'];
|
|
assert.ok(entry, 'the archive tag exception must be registered');
|
|
assert.ok(entry['pre-polyrepo-archive'], 'the exception is keyed by TAG, so a future real release in that repo is still backfilled');
|
|
const r = planBackfill({ repos: [repo('ktg-plugin-marketplace', [tag('v9.0.0')], [])] });
|
|
assert.equal(r.create.length, 1, 'a normal tag in an excluded repo is still a release');
|
|
});
|
|
|
|
test('the summary reports the DENOMINATOR, so a run that verified nothing cannot read as clean', () => {
|
|
const r = planBackfill({ repos: [repo('a', [tag('v1')], ['v1']), repo('b', [tag('v2')], []), repo('c', [])] });
|
|
assert.equal(r.total, 3);
|
|
assert.equal(r.tagged, 2);
|
|
});
|
|
|
|
// --- Repair: a release object already filed with a mechanical body ----------
|
|
//
|
|
// The first backfill used the tag message, so llm-security v8.0.0 was filed with the body
|
|
// "llm-security v8.0.0" — the mechanical string `--create-tag` mints. Repair replaces such
|
|
// a body with the CHANGELOG section that should have been there. It is a PATCH to a
|
|
// published page, so the bar is "strictly more informative", never "different".
|
|
|
|
test('repair plans an update when a CHANGELOG section exists and the current body is the mechanical tag message', () => {
|
|
const r = planRepair({ releases: [
|
|
{ repo: 'llm-security', tag: 'v8.0.0', currentBody: 'llm-security v8.0.0', changelogBody: 'Major release. Breaking part is small.' },
|
|
] });
|
|
assert.equal(r.update.length, 1);
|
|
assert.equal(r.update[0].repo, 'llm-security');
|
|
assert.equal(r.update[0].body, 'Major release. Breaking part is small.');
|
|
});
|
|
|
|
test('repair leaves a release whose body ALREADY is the CHANGELOG section', () => {
|
|
const same = 'Security and correctness patch.';
|
|
const r = planRepair({ releases: [{ repo: 'llm-security', tag: 'v7.8.3', currentBody: same, changelogBody: same }] });
|
|
assert.equal(r.update.length, 0);
|
|
assert.equal(r.skip.length, 1);
|
|
assert.match(r.skip[0].reason, /already/);
|
|
});
|
|
|
|
test('repair NEVER blanks a body: no CHANGELOG section means no update', () => {
|
|
const r = planRepair({ releases: [{ repo: 'x', tag: 'v1.0.0', currentBody: 'hand written notes', changelogBody: null }] });
|
|
assert.equal(r.update.length, 0);
|
|
assert.match(r.skip[0].reason, /no CHANGELOG section/);
|
|
});
|
|
|
|
test('repair does not overwrite a body that is LONGER than the CHANGELOG section', () => {
|
|
// A hand-written release page that says more than the CHANGELOG is not a defect to fix.
|
|
const r = planRepair({ releases: [
|
|
{ repo: 'x', tag: 'v1.0.0', currentBody: 'a much longer hand written release note with detail', changelogBody: 'short' },
|
|
] });
|
|
assert.equal(r.update.length, 0);
|
|
assert.match(r.skip[0].reason, /not more informative/);
|
|
});
|
|
|
|
test('repair reports the denominator', () => {
|
|
const r = planRepair({ releases: [
|
|
{ repo: 'a', tag: 'v1', currentBody: 'a v1', changelogBody: 'real notes for a' },
|
|
{ repo: 'b', tag: 'v2', currentBody: 'real notes for b', changelogBody: 'real notes for b' },
|
|
] });
|
|
assert.equal(r.total, 2);
|
|
assert.equal(r.update.length, 1);
|
|
});
|