Files
msd-core/tests/semver-compare.test.cjs
Tom Boucher 85ed50cc4f test(#1972): consolidate 94 command/module regression tests into subject suites
Fold 94 issue-named command/module regression files into the canonical test file
that owns each subject-under-test, across 52 existing suites (state, config, frontmatter,
roadmap-parser, capability-registry, shell-command-projection-dispatch, plan-phase-drift-guard,
health-validation, runtime-converters, commands, etc.). Verbatim block-scoped describe
wrappers; 881 subtests conserved 1:1. No new test files.

Host-env pre-check (per B2): the only GSD_WORKSTREAM/GSD_PROJECT-touching destinations
(intel, planning-workspace) clear those vars hermetically, so folded CLI tests are safe.

Regenerates regression-name allowlist (222->162), ratchets file-count allowlist across
8 buckets (validate entry removed after dropping <=2), makes 34 relocated allow-test-rule
exemptions issue-ref-compliant (ADR-456; prunes 34 stale ids). Repoints CONTEXT.md +
ADR-0002/443/1235/3524 test-file references. lint:ci green.

Part of epic #1969. Closes #1972.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:59:23 -04:00

220 lines
7.4 KiB
JavaScript

/**
* Shared semver compare utility tests.
*
* 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,
semverSatisfies,
} = require('../gsd-core/bin/lib/semver-compare.cjs');
describe('isSemverNewer (shared semver comparison)', () => {
test('newer major version', () => {
assert.strictEqual(isSemverNewer('2.0.0', '1.0.0'), true);
});
test('newer minor version', () => {
assert.strictEqual(isSemverNewer('1.1.0', '1.0.0'), true);
});
test('newer patch version', () => {
assert.strictEqual(isSemverNewer('1.0.1', '1.0.0'), true);
});
test('equal versions', () => {
assert.strictEqual(isSemverNewer('1.0.0', '1.0.0'), false);
});
test('older version returns false', () => {
assert.strictEqual(isSemverNewer('1.0.0', '2.0.0'), false);
});
test('installed ahead of npm (git install scenario)', () => {
assert.strictEqual(isSemverNewer('1.30.0', '1.31.0'), false);
});
test('npm ahead of installed (real update available)', () => {
assert.strictEqual(isSemverNewer('1.31.0', '1.30.0'), true);
});
test('pre-release suffix stripped', () => {
assert.strictEqual(isSemverNewer('1.0.1-beta.1', '1.0.0'), true);
});
test('pre-release on both sides', () => {
assert.strictEqual(isSemverNewer('2.0.0-rc.1', '1.9.0-beta.2'), true);
});
test('null/undefined handled', () => {
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(isSemverNewer('', '1.0.0'), false);
assert.strictEqual(isSemverNewer('1.0.0', ''), true);
});
test('two-segment version (missing patch)', () => {
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);
});
});
describe('semverSatisfies (ADR-1244 engines.gsd range gate)', () => {
const sat = (v, r, expected) =>
assert.strictEqual(semverSatisfies(v, r), expected, `expected satisfies(${JSON.stringify(v)}, ${JSON.stringify(r)}) === ${expected}`);
test('>= comparator', () => {
sat('1.6.0', '>=1.6.0', true);
sat('1.6.1', '>=1.6.0', true);
sat('2.0.0', '>=1.6.0', true);
sat('1.5.9', '>=1.6.0', false);
});
test('> < <= = comparators', () => {
sat('1.6.1', '>1.6.0', true);
sat('1.6.0', '>1.6.0', false);
sat('1.5.0', '<1.6.0', true);
sat('1.6.0', '<1.6.0', false);
sat('1.6.0', '<=1.6.0', true);
sat('1.6.1', '<=1.6.0', false);
sat('1.6.0', '=1.6.0', true);
sat('1.6.1', '=1.6.0', false);
});
test('bare exact full version', () => {
sat('1.6.0', '1.6.0', true);
sat('1.6.1', '1.6.0', false);
});
test('AND-composed range (whitespace)', () => {
sat('1.6.0', '>=1.6.0 <3.0.0', true);
sat('2.9.9', '>=1.6.0 <3.0.0', true);
sat('3.0.0', '>=1.6.0 <3.0.0', false);
sat('1.5.0', '>=1.6.0 <3.0.0', false);
});
test('OR-composed range (||)', () => {
sat('1.6.0', '>=1.6.0 || >=2.0.0', true);
sat('2.0.0', '<1.0.0 || >=2.0.0', true);
sat('1.5.0', '<1.0.0 || >=2.0.0', false);
});
test('caret ranges', () => {
sat('1.2.3', '^1.2.3', true);
sat('1.9.0', '^1.2.3', true);
sat('2.0.0', '^1.2.3', false);
sat('1.2.2', '^1.2.3', false);
sat('0.2.3', '^0.2.3', true);
sat('0.3.0', '^0.2.3', false);
sat('0.0.3', '^0.0.3', true);
sat('0.0.4', '^0.0.3', false);
});
test('tilde ranges', () => {
sat('1.2.3', '~1.2.3', true);
sat('1.2.9', '~1.2.3', true);
sat('1.3.0', '~1.2.3', false);
sat('1.2.0', '~1.2', true);
sat('1.3.0', '~1.2', false);
sat('1.9.0', '~1', true);
sat('2.0.0', '~1', false);
});
test('wildcards and partials', () => {
sat('99.0.0', '*', true);
sat('0.0.1', '*', true);
sat('1.0.0', '1.x', true);
sat('1.9.9', '1.x', true);
sat('2.0.0', '1.x', false);
sat('0.9.9', '1.x', false);
sat('1.2.0', '1.2.x', true);
sat('1.3.0', '1.2.x', false);
sat('1.5.0', '1', true);
sat('2.0.0', '1', false);
});
test('prerelease and v-prefix normalize to numeric core', () => {
sat('1.6.0-rc.1', '>=1.6.0', true);
sat('1.5.1-dev.0', '>=1.6.0', false);
sat('v1.6.0', '>=1.6.0', true);
});
test('FAIL CLOSED on empty/unparseable ranges', () => {
for (const bad of ['', ' ', 'abc', 'not a range', '>=', '>=x', 'foo.bar.baz', '1.2.3.4', '>=1.2.3 garbage']) {
sat('1.6.0', bad, false);
}
});
test('FAIL CLOSED on malformed wildcard tokens (concrete segment after a wildcard)', () => {
for (const bad of ['1.x.2', '>=1.x.2', '1.*.2', '1.X.0']) {
sat('1.5.0', bad, false);
}
});
test('null/undefined inputs do not throw and fail closed', () => {
sat('1.6.0', null, false);
sat('1.6.0', undefined, false);
sat(null, '>=1.6.0', false); // null version -> [0,0,0] -> not >= 1.6.0
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-10-semver-policy-consolidation.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-10-semver-policy-consolidation (consolidation epic #1969 B3 #1972)", () => {
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const {
compareSemverCore,
isStableTripletSemver,
} = require('../gsd-core/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);
});
});
});
}