Ctrl K
ComunidadGuías…

test/changelog-counts.test.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// The CHANGELOG `git tag` row must be a fact about the PUBLISHED repository.
//
// WHY THIS FILE EXISTS. On 2026-11-11 the row said 230 and CI said it was wrong: the
// authoring host carried 230 tags, every `fetch-depth: 0` checkout 229, and the delta was
// `v3.141.592659` — a tag created locally, frozen on the unfixed `d5519da3`, never pushed.
// `git tag | wc -l` answers "what did this clone fetch", so the generator wrote 230, the
// guard compared 230 against 229 in CI, and the only way to make both green was to weaken
// the guard. The register (`docs/releases/UNPUBLISHED-TAGS.txt`) names the delta, and this
// file pins the property that matters: the count is unchanged by a tag that exists only
// in one checkout.
//
// The docs guard still asserts the row byte-for-byte against renderCounts(); this file
// asserts the derivation underneath it, with the tag list passed in so the two checkout
// shapes can be compared without creating or deleting a tag.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import {
  UNPUBLISHED_TAGS_FILE,
  changelogCounts,
  countPublishedTags,
  listGitTags,
  unpublishedTagVersions,
} from '../scripts/sync-changelog-counts.mjs';

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');

describe('changelog counts: the git tag row is the published namespace', () => {
  it('a tag that exists only in this checkout does not move the count', () => {
    // The two shapes, as data: the authoring host has the frozen tag, a CI checkout does
    // not. A derivation that returns tags.length passes exactly one of them — which is
    // how 230 shipped while CI measured 229.
    const tags = listGitTags(ROOT);
    assert.ok(tags && tags.length > 0, 'precondition: this checkout must list tags');
    const register = unpublishedTagVersions(ROOT);
    const packaged = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
    const withoutLocalOnly = tags.filter((tag) => !register.includes(tag.replace(/^v/, '')));

    assert.equal(
      countPublishedTags(tags, register, packaged),
      countPublishedTags(withoutLocalOnly, register, packaged),
      'the row must not change when a registered local-only tag is present or absent',
    );
    // The other shape difference: master CI before the release tag is pushed, and the
    // Release checkout at the tag. The packaged tag is excluded by name, so the count
    // cannot move when it appears — that is the second half of the 2026-11-11 failure.
    assert.equal(
      countPublishedTags(tags, register, packaged),
      countPublishedTags(tags.filter((tag) => tag !== `v${packaged}`), register, packaged),
      'the row must not change when the packaged version’s own tag is present or absent',
    );
  });

  it('a registered tag is subtracted only where this checkout has it', () => {
    assert.equal(countPublishedTags(['v1.0.0', 'v1.0.1', 'v1.0.2'], ['1.0.2']), 2);
    // The CI shape: the same register, a namespace that never fetched the tag.
    assert.equal(countPublishedTags(['v1.0.0', 'v1.0.1'], ['1.0.2']), 2);
    // No register at all is not a licence to count refs: the published count is unchanged.
    assert.equal(countPublishedTags(['v1.0.0', 'v1.0.1'], []), 2);
  });

  it('the packaged version tag is excluded by name, before and after it exists', () => {
    // master CI (tag not pushed) and the Release checkout (tag present) must derive the
    // same number; excluding the tag being cut is what makes the row stable across the
    // push. A `< package.json` comparison is invariant too, but it silently drops the
    // three `v3.2.6-rc*` tags this repository carries, so the exclusion is by name.
    assert.equal(countPublishedTags(['v1.0.0', 'v1.0.1', 'v1.0.2'], [], '1.0.2'), 2);
    assert.equal(countPublishedTags(['v1.0.0', 'v1.0.1'], [], '1.0.2'), 2);
    assert.equal(countPublishedTags(['v1.0.0', 'v3.2.6-rc4'], [], '1.0.0'), 1);
  });

  it('no tags at all is "cannot stand behind a count", not zero', () => {
    // A shallow clone lists only the tags its fetched history carries; rewriting the row to
    // 0 there would be a measurement nobody can stand behind. The guard skips on null.
    assert.equal(countPublishedTags([], ['1.0.2']), null);
  });

  it('the row equals the count of a checkout that never fetched the local-only tag', () => {
    // The regression this file exists for: with the derivation reverted to tags.length this
    // is 230 vs 229 on the authoring host, i.e. exactly the row CI rejected.
    const tags = listGitTags(ROOT);
    const register = unpublishedTagVersions(ROOT);
    const packaged = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
    const published = countPublishedTags(tags, register, packaged);
    assert.equal(changelogCounts(ROOT).tags, published, 'the row must equal the published count');
    assert.equal(
      published,
      countPublishedTags(tags.filter((tag) => !register.includes(tag.replace(/^v/, ''))), register, packaged),
      'and that count must be the same in a checkout without the local-only tags',
    );
    assert.equal(
      countPublishedTags(tags.filter((tag) => tag !== `v${packaged}`), register, packaged),
      published,
      'and the same in a checkout that has not created the release tag yet',
    );
  });

  it('a malformed register line is an error, not a silent zero-subtraction', () => {
    // A typo here would drop the subtraction and the count would drift by one on the host
    // only — the failure this whole mechanism exists to prevent. Refuse the line instead.
    const dir = mkdtempSync(join(tmpdir(), 'unpublished-tags-'));
    try {
      mkdirSync(join(dir, 'docs', 'releases'), { recursive: true });
      writeFileSync(join(dir, UNPUBLISHED_TAGS_FILE), '# comment\nv3.141.592659  # frozen\nnot-a-version\n');
      assert.throws(() => unpublishedTagVersions(dir), /not a version: not-a-version/);
      writeFileSync(join(dir, UNPUBLISHED_TAGS_FILE), '# comment only\n\n');
      assert.deepEqual(unpublishedTagVersions(dir), []);
      // The blob is LF, but a Windows checkout under core.autocrlf=true hands the parser
      // CRLF. A comment line must still strip to empty, and the version must still parse.
      writeFileSync(join(dir, UNPUBLISHED_TAGS_FILE), '# comment\r\nv3.141.592659  # frozen\r\n');
      assert.deepEqual(unpublishedTagVersions(dir), ['3.141.592659']);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });
});