* test(#2359): failing-first regression for the 1.4.0 Cursor commands citation The `## [1.4.0]` CHANGELOG entry for the `.cursor/commands/` surface carries the trailing reference (#803) — the Cline PR, cited correctly by the entry two lines below. The Cursor surface shipped in #805 (feat(#785)). Adds four behavioral cases to the owning module's test file, asserting through parseChangelog (the same parser cmdExtract/cmdVerify/cmdRender use) rather than raw text matching: 1. the Cursor bullet cites 805 - FAILS before the fix 2. the adjacent Cline bullet still cites 803 - guards a global s/803/805/ 3. exactly one Cursor bullet exists - guards drop/duplicate 4. the two bullets cite different PRs - the defect as an invariant Folded into tests/changeset-serialize.test.cjs rather than a new bug-2359-*.test.cjs file, per scripts/lint-regression-test-names.cjs. Refs #2359 * fix(#2359): CHANGELOG 1.4.0 Cursor commands entry cites #805, not #803 The `## [1.4.0]` entry for `gsd install --cursor` writing `.cursor/commands/gsd-<name>.md` carried the trailing reference (#803). That PR is `feat(#787): elevate Cline` — cited correctly by the entry two lines below. The Cursor slash-command surface shipped in #805 (`feat(#785): write .cursor/commands/ Cursor 1.6 slash-command surface`). Two adjacent bullets therefore claimed one PR, and only the second was right. The trailing (#NNNN) is machine-parsed by parseChangelog and consumed by `changeset extract`, so the wrong number is live data, not only prose — and it had already propagated to a human reporter (#2341, quoted there as "#785 / #803"). Single-occurrence, line-anchored edit. The Cline entry's (#803) is correct and is deliberately untouched; the regression test added in the preceding commit asserts both, so a global s/803/805/ fails. Prose is unchanged, including the "both surfaces are written on every install" clause the issue explicitly certifies as accurate. Fixes #2359 * chore(#2359): changeset fragment for the Cursor commands citation fix * test(#2359): assert bullet cardinality before reading it Review findings, one root cause: each of the four cases re-derived the bullet list, re-filtered by marker, then destructured `const [x] = ...` and read `.pr` unguarded. A reworded or deleted entry therefore died with TypeError: Cannot read properties of undefined (reading 'pr') instead of naming what was missing. The repetition was also duplicated logic across all four cases. Adds locateBullet(version, marker, label), which asserts exactly one match before returning, and folds the two cardinality guards into one case covering both entries. Failure mode verified: a marker matching nothing now raises AssertionError "expected exactly one bogus bullet in the 1.4.0 section, found 0". Refs #2359 * chore(#2359): backfill changeset pr number (#3252) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -3,6 +3,7 @@ process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { serializeChangelog, parseChangelog } = require(path.join(__dirname, '..', 'scripts', 'changeset', 'serialize.cjs'));
|
||||
@@ -181,3 +182,64 @@ describe('#3001: multi-paragraph changeset body round-trips without truncation',
|
||||
`second paragraph must survive; got: ${JSON.stringify(bullet.body)}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeset serialize: shipped CHANGELOG citation integrity (#2359)', () => {
|
||||
// Regression: the `## [1.4.0]` Cursor commands-surface entry carried the
|
||||
// trailing reference (#803) — the *Cline* PR, cited correctly by the entry two
|
||||
// lines below it. The Cursor surface actually shipped in #805. Two adjacent
|
||||
// bullets therefore claimed one PR, and only the second was right.
|
||||
//
|
||||
// This is not cosmetic: the trailing (#NNNN) is machine-parsed here and
|
||||
// consumed by `cmdExtract`, so a wrong number propagates into extracted
|
||||
// release-note ranges. It had already propagated to a human reporter (#2341,
|
||||
// quoted there as "#785 / #803").
|
||||
//
|
||||
// The obvious bad fix is a global s/(#803)/(#805)/, which repairs the Cursor
|
||||
// entry and silently corrupts the Cline one. The Cline assertion below is the
|
||||
// guard for exactly that, and is expected to pass both before and after.
|
||||
const CHANGELOG = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md'), 'utf8');
|
||||
const CURSOR_MARKER = /`\.cursor\/commands\/gsd-<name>\.md`/;
|
||||
const CLINE_MARKER = /Elevate the Cline runtime to hook parity/;
|
||||
|
||||
function bulletsOf(version) {
|
||||
const release = parseChangelog(CHANGELOG).releases.find((r) => r.version === version);
|
||||
assert.ok(release, `CHANGELOG.md must still carry a \`## [${version}]\` release section`);
|
||||
return release.sections.flatMap((s) => s.bullets);
|
||||
}
|
||||
|
||||
// Asserts cardinality before returning, so a reworded or deleted entry fails
|
||||
// with a readable message instead of a TypeError on an undefined bullet.
|
||||
function locateBullet(version, marker, label) {
|
||||
const matches = bulletsOf(version).filter((b) => marker.test(b.body));
|
||||
assert.equal(matches.length, 1,
|
||||
`expected exactly one ${label} bullet in the ${version} section, found ${matches.length}`);
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
test('the 1.4.0 Cursor commands-surface bullet cites PR 805', () => {
|
||||
const cursor = locateBullet('1.4.0', CURSOR_MARKER, 'Cursor commands-surface');
|
||||
assert.equal(cursor.pr, 805,
|
||||
'the .cursor/commands/ surface shipped in PR #805 (feat(#785)), not #803 (the Cline PR)');
|
||||
});
|
||||
|
||||
test('the adjacent 1.4.0 Cline bullet still cites PR 803', () => {
|
||||
const cline = locateBullet('1.4.0', CLINE_MARKER, 'Cline hook-parity');
|
||||
assert.equal(cline.pr, 803,
|
||||
"the Cline entry's #803 is correct and must survive the Cursor correction");
|
||||
});
|
||||
|
||||
test('both 1.4.0 entries survive the correction exactly once each', () => {
|
||||
const bullets = bulletsOf('1.4.0');
|
||||
assert.equal(bullets.filter((b) => CURSOR_MARKER.test(b.body)).length, 1,
|
||||
'the Cursor entry must be corrected in place — neither dropped nor duplicated');
|
||||
assert.equal(bullets.filter((b) => CLINE_MARKER.test(b.body)).length, 1,
|
||||
'the Cline entry must be left alone — neither dropped nor duplicated');
|
||||
});
|
||||
|
||||
test('the adjacent Cursor and Cline bullets do not cite the same PR', () => {
|
||||
const cursor = locateBullet('1.4.0', CURSOR_MARKER, 'Cursor commands-surface');
|
||||
const cline = locateBullet('1.4.0', CLINE_MARKER, 'Cline hook-parity');
|
||||
assert.notEqual(cursor.pr, cline.pr,
|
||||
'two adjacent entries describing different runtimes cannot share one PR');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user