Files
2026-07-13 13:39:12 +08:00

126 lines
4.8 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
import {
insertNextSection,
extractSection,
versionAfter,
} from "../../scripts/release/sync-next-cycle.mjs";
const SCRIPT_PATH = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"../../scripts/release/sync-next-cycle.mjs"
);
// Pure-function guards for the parallel-cycle sync-back (generate-release
// Phase 5 step 20): main's CHANGELOG must win VERBATIM and the next cycle's
// section must be re-inserted on top without eating any of main's bullets
// (anti-CHANGELOG-eat protocol, design doc
// _tasks/release-flow/2026-07-04_proposta-ciclo-paralelo-v2.md).
const MAIN = `# Changelog
## [Unreleased]
---
## [3.8.44] — 2026-07-04
### ✨ New Features
- **feat(a):** shipped feature one. (#1)
- **feat(b):** shipped feature two. (#2)
### 🙌 Contributors
| Contributor | PRs / Issues |
| --- | --- |
| [@x](https://github.com/x) | #1 |
---
## [3.8.43] — 2026-07-02
- **old:** bullet. (#0)
`;
const CYCLE_SECTION = `## [3.8.45] — TBD
### ✨ New Features
- **feat(new):** cycle bullet accumulated before the sync. (#9)`;
test("insertNextSection puts the cycle section before main's latest version, keeping main intact", () => {
const out = insertNextSection(MAIN, CYCLE_SECTION, "3.8.45");
const i45 = out.indexOf("## [3.8.45]");
const i44 = out.indexOf("## [3.8.44]");
const i43 = out.indexOf("## [3.8.43]");
assert.ok(i45 !== -1 && i44 !== -1 && i43 !== -1, "all three sections present");
assert.ok(i45 < i44 && i44 < i43, "ordering: 3.8.45 → 3.8.44 → 3.8.43");
// main's content survives verbatim (no bullet eaten)
assert.ok(out.includes("shipped feature one"), "main bullet 1 intact");
assert.ok(out.includes("shipped feature two"), "main bullet 2 intact");
assert.ok(out.includes("| [@x](https://github.com/x) | #1 |"), "contributors row intact");
// the cycle's accumulated bullet survives
assert.ok(out.includes("cycle bullet accumulated before the sync"), "cycle bullet intact");
});
test("insertNextSection synthesizes a TBD placeholder when the cycle has no section yet", () => {
const out = insertNextSection(MAIN, null, "3.8.45");
assert.ok(out.includes("## [3.8.45] — TBD"));
assert.ok(out.indexOf("## [3.8.45]") < out.indexOf("## [3.8.44]"));
});
test("insertNextSection replaces a pre-existing copy of the next section instead of duplicating", () => {
const withNext = insertNextSection(MAIN, CYCLE_SECTION, "3.8.45");
const again = insertNextSection(withNext, CYCLE_SECTION, "3.8.45");
const count = (again.match(/## \[3\.8\.45\]/g) || []).length;
assert.equal(count, 1, "exactly one [3.8.45] heading after re-run (idempotent)");
});
test("extractSection returns header through the line before the next heading, trimming the separator", () => {
const withNext = insertNextSection(MAIN, CYCLE_SECTION, "3.8.45");
const section = extractSection(withNext, "3.8.45");
assert.ok(section, "section found");
assert.ok(section.startsWith("## [3.8.45]"));
assert.ok(section.includes("cycle bullet accumulated"));
assert.ok(!section.includes("## [3.8.44]"), "does not bleed into the next section");
assert.ok(!section.trimEnd().endsWith("---"), "trailing separator trimmed");
});
test("extractSection returns null when the version has no section", () => {
assert.equal(extractSection(MAIN, "9.9.9"), null);
});
test("versionAfter finds the heading right below a version's section", () => {
assert.equal(versionAfter(MAIN, "3.8.44"), "3.8.43");
assert.equal(versionAfter(MAIN, "3.8.43"), null, "last section has nothing below");
assert.equal(versionAfter(MAIN, "9.9.9"), null, "missing version → null");
});
// Live-failure guards for the v3.8.45 run (2026-07-06) — source-shape assertions,
// same style as tests/unit/validate-release-green.test.ts.
test("git() passes a widened maxBuffer (ENOBUFS on `git show origin/main:CHANGELOG.md` — the CHANGELOG alone is >1 MiB)", () => {
const src = readFileSync(SCRIPT_PATH, "utf8");
const gitFn = src.slice(src.indexOf("function git("), src.indexOf("function main("));
assert.ok(gitFn.includes("maxBuffer"), "git() helper must set maxBuffer above the 1 MiB default");
});
test("i18n resync also propagates the FINALIZED [prevVersion] section into the mirrors, not just [NEXT]", () => {
const src = readFileSync(SCRIPT_PATH, "utf8");
assert.ok(
src.includes('"release:sync-changelog-i18n", "--", NEXT, prevVersion'),
"syncs the new cycle section"
);
assert.ok(
src.includes("versionAfter(mainChangelog, prevVersion)"),
"computes the boundary below the shipped section"
);
assert.ok(
src.includes('"release:sync-changelog-i18n", "--", prevVersion, belowPrev'),
"syncs the shipped (finalized) section — without this all 42 mirrors keep it as TBD"
);
});