fix(10): centralize semver comparison policy across hooks and changeset
This commit is contained in:
30
tests/bug-10-semver-policy-consolidation.test.cjs
Normal file
30
tests/bug-10-semver-policy-consolidation.test.cjs
Normal file
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
compareSemverCore,
|
||||
isStableTripletSemver,
|
||||
} = require('../get-shit-done/bin/lib/semver-compare.cjs');
|
||||
const { isInstalledAheadOfLatest } = require('../hooks/gsd-statusline.js');
|
||||
|
||||
describe('bug #10: semver policy consolidation', () => {
|
||||
test('shared comparator treats prerelease patch increment as newer than previous stable', () => {
|
||||
assert.ok(compareSemverCore('1.2.1-beta.1', '1.2.0') > 0);
|
||||
});
|
||||
|
||||
test('shared comparator ignores prerelease suffix for equal base versions', () => {
|
||||
assert.equal(compareSemverCore('1.2.0-rc.1', '1.2.0'), 0);
|
||||
});
|
||||
|
||||
test('stable-triplet validator excludes prerelease tags', () => {
|
||||
assert.equal(isStableTripletSemver('1.2.0-rc.1'), false);
|
||||
assert.equal(isStableTripletSemver('1.2.0'), true);
|
||||
});
|
||||
|
||||
test('statusline dev-install detection uses shared comparator semantics', () => {
|
||||
assert.equal(isInstalledAheadOfLatest('1.2.1-beta.1', '1.2.0'), true);
|
||||
assert.equal(isInstalledAheadOfLatest('1.2.0-rc.1', '1.2.0'), false);
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,12 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { parseStateMd, formatGsdState, readGsdState } = require('../hooks/gsd-statusline.js');
|
||||
const {
|
||||
parseStateMd,
|
||||
formatGsdState,
|
||||
readGsdState,
|
||||
isInstalledAheadOfLatest,
|
||||
} = require('../hooks/gsd-statusline.js');
|
||||
|
||||
// ─── parseStateMd ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -217,6 +222,16 @@ describe('formatGsdState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInstalledAheadOfLatest', () => {
|
||||
test('treats prerelease patch increment as ahead of prior stable', () => {
|
||||
assert.equal(isInstalledAheadOfLatest('1.2.1-beta.1', '1.2.0'), true);
|
||||
});
|
||||
|
||||
test('treats equal base version prerelease as not ahead', () => {
|
||||
assert.equal(isInstalledAheadOfLatest('1.2.0-rc.1', '1.2.0'), false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── readGsdState ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('readGsdState', () => {
|
||||
|
||||
@@ -1,81 +1,79 @@
|
||||
/**
|
||||
* Tests for the isNewer() semver comparison function used in gsd-check-update.js.
|
||||
* Shared semver compare utility tests.
|
||||
*
|
||||
* WHY DUPLICATED: isNewer() lives inside a template literal string passed to
|
||||
* spawn(process.execPath, ['-e', `...`]) — it runs in a detached child process
|
||||
* that has no access to the parent module scope. This means it cannot be
|
||||
* require()'d or imported from a shared module. The function is intentionally
|
||||
* inlined in the spawn string so it works in the child process context.
|
||||
*
|
||||
* We mirror the implementation here so the logic is testable. If the hook's
|
||||
* implementation diverges from this copy, the fix is to update this mirror —
|
||||
* not to restructure the hook (which would require changing the spawn pattern
|
||||
* across the entire hook architecture).
|
||||
* These assertions lock the normalization policy used by update-check,
|
||||
* statusline dev-install detection, and changeset extraction range compare.
|
||||
*/
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
compareSemverCore,
|
||||
isSemverNewer,
|
||||
toNumericTuple,
|
||||
} = require('../get-shit-done/bin/lib/semver-compare.cjs');
|
||||
|
||||
// Mirror of isNewer() from hooks/gsd-check-update.js (inside spawn template)
|
||||
function isNewer(a, b) {
|
||||
const pa = (a || '').split('.').map(s => Number(s.replace(/-.*/, '')) || 0);
|
||||
const pb = (b || '').split('.').map(s => Number(s.replace(/-.*/, '')) || 0);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (pa[i] > pb[i]) return true;
|
||||
if (pa[i] < pb[i]) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
describe('isNewer (semver comparison)', () => {
|
||||
describe('isSemverNewer (shared semver comparison)', () => {
|
||||
test('newer major version', () => {
|
||||
assert.strictEqual(isNewer('2.0.0', '1.0.0'), true);
|
||||
assert.strictEqual(isSemverNewer('2.0.0', '1.0.0'), true);
|
||||
});
|
||||
|
||||
test('newer minor version', () => {
|
||||
assert.strictEqual(isNewer('1.1.0', '1.0.0'), true);
|
||||
assert.strictEqual(isSemverNewer('1.1.0', '1.0.0'), true);
|
||||
});
|
||||
|
||||
test('newer patch version', () => {
|
||||
assert.strictEqual(isNewer('1.0.1', '1.0.0'), true);
|
||||
assert.strictEqual(isSemverNewer('1.0.1', '1.0.0'), true);
|
||||
});
|
||||
|
||||
test('equal versions', () => {
|
||||
assert.strictEqual(isNewer('1.0.0', '1.0.0'), false);
|
||||
assert.strictEqual(isSemverNewer('1.0.0', '1.0.0'), false);
|
||||
});
|
||||
|
||||
test('older version returns false', () => {
|
||||
assert.strictEqual(isNewer('1.0.0', '2.0.0'), false);
|
||||
assert.strictEqual(isSemverNewer('1.0.0', '2.0.0'), false);
|
||||
});
|
||||
|
||||
test('installed ahead of npm (git install scenario)', () => {
|
||||
assert.strictEqual(isNewer('1.30.0', '1.31.0'), false);
|
||||
assert.strictEqual(isSemverNewer('1.30.0', '1.31.0'), false);
|
||||
});
|
||||
|
||||
test('npm ahead of installed (real update available)', () => {
|
||||
assert.strictEqual(isNewer('1.31.0', '1.30.0'), true);
|
||||
assert.strictEqual(isSemverNewer('1.31.0', '1.30.0'), true);
|
||||
});
|
||||
|
||||
test('pre-release suffix stripped', () => {
|
||||
assert.strictEqual(isNewer('1.0.1-beta.1', '1.0.0'), true);
|
||||
assert.strictEqual(isSemverNewer('1.0.1-beta.1', '1.0.0'), true);
|
||||
});
|
||||
|
||||
test('pre-release on both sides', () => {
|
||||
assert.strictEqual(isNewer('2.0.0-rc.1', '1.9.0-beta.2'), true);
|
||||
assert.strictEqual(isSemverNewer('2.0.0-rc.1', '1.9.0-beta.2'), true);
|
||||
});
|
||||
|
||||
test('null/undefined handled', () => {
|
||||
assert.strictEqual(isNewer(null, '1.0.0'), false);
|
||||
assert.strictEqual(isNewer('1.0.0', null), true);
|
||||
assert.strictEqual(isNewer(null, null), false);
|
||||
assert.strictEqual(isSemverNewer(null, '1.0.0'), false);
|
||||
assert.strictEqual(isSemverNewer('1.0.0', null), true);
|
||||
assert.strictEqual(isSemverNewer(null, null), false);
|
||||
});
|
||||
|
||||
test('empty string handled', () => {
|
||||
assert.strictEqual(isNewer('', '1.0.0'), false);
|
||||
assert.strictEqual(isNewer('1.0.0', ''), true);
|
||||
assert.strictEqual(isSemverNewer('', '1.0.0'), false);
|
||||
assert.strictEqual(isSemverNewer('1.0.0', ''), true);
|
||||
});
|
||||
|
||||
test('two-segment version (missing patch)', () => {
|
||||
assert.strictEqual(isNewer('1.1', '1.0'), true);
|
||||
assert.strictEqual(isNewer('1.0', '1.1'), false);
|
||||
assert.strictEqual(isSemverNewer('1.1', '1.0'), true);
|
||||
assert.strictEqual(isSemverNewer('1.0', '1.1'), false);
|
||||
});
|
||||
|
||||
test('v-prefixed versions normalize consistently', () => {
|
||||
assert.strictEqual(isSemverNewer('v1.2.1', '1.2.0'), true);
|
||||
assert.strictEqual(isSemverNewer('1.2.0', 'v1.2.0'), false);
|
||||
assert.deepStrictEqual(toNumericTuple('v1.2.3-rc.1'), [1, 2, 3]);
|
||||
});
|
||||
|
||||
test('core comparator uses three-way ordering', () => {
|
||||
assert.strictEqual(compareSemverCore('1.2.0', '1.2.0'), 0);
|
||||
assert.strictEqual(compareSemverCore('1.2.1', '1.2.0'), 1);
|
||||
assert.strictEqual(compareSemverCore('1.2.0', '1.2.1'), -1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user