diff --git a/packageManager/src/version/Range.ts b/packageManager/src/version/Range.ts index 5c0518c..0b4f985 100644 --- a/packageManager/src/version/Range.ts +++ b/packageManager/src/version/Range.ts @@ -8,6 +8,21 @@ import { Version } from './Version'; export class Range { private range: semver.Range; private rangeSpec: string; + // null = neutral (wildcard/latest — matches both R and LTS) + private isRRange: boolean | null = null; + + /** + * Normalize R-release notation in a range spec to plain semver. + * e.g. '^20R2.3' → '^20.2.3', '20R2.*' → '20.2.x', '20R2' → '20.2.0' + */ + private static normalizeRSpec(spec: string): string { + return spec + .replace(/(\d+)\.R(\d+)\.(\d+)/g, '$1.$2.$3') // 20.R2.3 → 20.2.3 + .replace(/(\d+)\.R(\d+)/g, '$1.$2.0') // 20.R2 → 20.2.0 + .replace(/(\d+)R(\d+)\.(\d+)/g, '$1.$2.$3') // 20R2.3 → 20.2.3 + .replace(/(\d+)R(\d+)\.\*/g, '$1.$2.x') // 20R2.* → 20.2.x + .replace(/(\d+)R(\d+)/g, '$1.$2.0'); // 20R2 → 20.2.0 + } constructor(rangeSpec: string, ideVersion?: Version) { this.rangeSpec = rangeSpec; @@ -15,60 +30,76 @@ export class Range { // Handle special keywords if (rangeSpec === 'latest' || rangeSpec === '*' || !rangeSpec) { this.range = new semver.Range('*'); + this.isRRange = null; // neutral } else if (rangeSpec.toLowerCase() === '4d') { - // Match IDE version + // Match IDE version — type follows the IDE's type const version = ideVersion?.toIDEString(); this.range = new semver.Range(`^${version}`); + this.isRRange = ideVersion?.isR ?? null; } else { - // Parse as semver range - this.range = new semver.Range(rangeSpec); + // Detect R-release notation before normalizing + this.isRRange = /\d+\.?R\d+/.test(rangeSpec); + // Parse as semver range (normalize R notation first if needed) + const normalizedSpec = this.isRRange ? Range.normalizeRSpec(rangeSpec) : rangeSpec; + this.range = new semver.Range(normalizedSpec); } } /** - * Check if a version satisfies this range + * Check if a version satisfies this range. + * Mirrors 4D semantics: R-release and LTS are separate tracks within the same + * major; a typed range (R or LTS) rejects versions from the other track. + * Neutral ranges (*, latest, empty) match both. */ satisfiedBy(version: string | Version): boolean { - const versionString = version instanceof Version - ? version.toString() - : version.replace(/^v/, ''); + const v = version instanceof Version ? version : Version.parse(version); + if (!v) return false; - return semver.satisfies(versionString, this.range); + // Type-compatibility guard + if (this.isRRange !== null && v.isR !== this.isRRange) { + return false; + } + + return semver.satisfies(v.toString(), this.range); } /** * Find the maximum version that satisfies this range */ maxSatisfying(versions: string[]): string | null { - const cleanVersions = versions.map(v => v.replace(/^v/, '')); - const result = semver.maxSatisfying(cleanVersions, this.range); + const filtered = this.isRRange !== null + ? versions.filter(v => { + const parsed = Version.parse(v); + return parsed ? parsed.isR === this.isRRange : true; + }) + : versions; + // Normalize each version string to semver, keeping a map back to original + const normalized = filtered.map(v => (Version.parse(v)?.toString() ?? v.replace(/^v/, ''))); + const result = semver.maxSatisfying(normalized, this.range); if (!result) { return null; } - - // Return with original 'v' prefix if it existed - const originalVersion = versions.find(v => - v.replace(/^v/, '') === result - ); - return originalVersion || result; + const idx = normalized.indexOf(result); + return idx >= 0 ? filtered[idx] : result; } /** * Find the minimum version that satisfies this range */ minSatisfying(versions: string[]): string | null { - const cleanVersions = versions.map(v => v.replace(/^v/, '')); - const result = semver.minSatisfying(cleanVersions, this.range); - + const filtered = this.isRRange !== null + ? versions.filter(v => { + const parsed = Version.parse(v); + return parsed ? parsed.isR === this.isRRange : true; + }) + : versions; + const normalized = filtered.map(v => (Version.parse(v)?.toString() ?? v.replace(/^v/, ''))); + const result = semver.minSatisfying(normalized, this.range); if (!result) { return null; } - - // Return with original 'v' prefix if it existed - const originalVersion = versions.find(v => - v.replace(/^v/, '') === result - ); - return originalVersion || result; + const idx = normalized.indexOf(result); + return idx >= 0 ? filtered[idx] : result; } /** diff --git a/packageManager/src/version/Version.ts b/packageManager/src/version/Version.ts index 0f81112..19c64d3 100644 --- a/packageManager/src/version/Version.ts +++ b/packageManager/src/version/Version.ts @@ -28,6 +28,10 @@ export class Version { // 20R2 → 20.2.0 clean = clean.replace(/R/, '.') + '.0'; this.isR = true; + } else if (/^\d{2}R\d+\.\d+$/.test(clean)) { + // 20R2.1 → 20.2.1 + clean = clean.replace(/R/, '.'); + this.isR = true; } else if (/^\d{2}\.R\d+(\.\d+)?$/.test(clean)) { // 20.R2.3 → 20.2.3 or 20.R2 → 20.2.0 clean = clean.replace(/\.R/, '.'); @@ -85,43 +89,68 @@ export class Version { * Check if this version is greater than another */ gt(other: Version): boolean { - return semver.gt(this.semver, other.semver); + return this.compare(other) > 0; } /** * Check if this version is greater than or equal to another */ gte(other: Version): boolean { - return semver.gte(this.semver, other.semver); + return this.compare(other) >= 0; } /** * Check if this version is less than another */ lt(other: Version): boolean { - return semver.lt(this.semver, other.semver); + return this.compare(other) < 0; } /** * Check if this version is less than or equal to another */ lte(other: Version): boolean { - return semver.lte(this.semver, other.semver); + return this.compare(other) <= 0; } /** * Check if this version equals another */ eq(other: Version): boolean { - return semver.eq(this.semver, other.semver); + return this.compare(other) === 0; } /** - * Compare with another version + * Compare with another version, mirroring 4D's Version.compareTo() logic: + * - compare major first + * - within same major: R-release > LTS (regardless of minor number) + * - then compare minor, patch, prerelease * @returns 0 if equal, 1 if greater, -1 if less */ compare(other: Version): number { - return semver.compare(this.semver, other.semver); + if (this.major > other.major) return 1; + if (this.major < other.major) return -1; + + // Same major: R-release beats LTS unconditionally + if (this.isR && !other.isR) return 1; + if (!this.isR && other.isR) return -1; + + if (this.minor > other.minor) return 1; + if (this.minor < other.minor) return -1; + + if (this.patch > other.patch) return 1; + if (this.patch < other.patch) return -1; + + // Prerelease is lower precedence than release + const thisPre = this.prerelease.length > 0; + const otherPre = other.prerelease.length > 0; + if (thisPre && !otherPre) return -1; + if (!thisPre && otherPre) return 1; + if (thisPre && otherPre) { + return semver.compare(this.semver, other.semver); + } + + return 0; } /** diff --git a/packageManager/test/range.test.ts b/packageManager/test/range.test.ts index e7ef1b4..7d0cfa1 100644 --- a/packageManager/test/range.test.ts +++ b/packageManager/test/range.test.ts @@ -200,4 +200,215 @@ describe('Range', () => { expect(range.maxSatisfying(versions)).toBe('21.3.0'); }); }); + + describe('R-Release range specs', () => { + it('should parse ^20R2 and match R versions only', () => { + const range = new Range('^20R2'); + expect(range.satisfiedBy('20R2')).toBe(true); + expect(range.satisfiedBy('20R2.3')).toBe(true); // compact R with patch + expect(range.satisfiedBy('20R3')).toBe(true); + expect(range.satisfiedBy('20R2.5')).toBe(true); + expect(range.satisfiedBy('20R1')).toBe(false); // below min + expect(range.satisfiedBy('21R2')).toBe(false); // different major + }); + + it('LTS range should not match R versions (^20.2.0 rejects 20R5)', () => { + const range = new Range('^20.2.0'); + expect(range.satisfiedBy(new Version('20R5'))).toBe(false); + expect(range.satisfiedBy('20R5')).toBe(false); + expect(range.satisfiedBy(new Version('20.5.0'))).toBe(true); + }); + + it('R range should not match LTS versions (^20R2 rejects 20.5.0)', () => { + const range = new Range('^20R2'); + expect(range.satisfiedBy(new Version('20.5.0'))).toBe(false); + expect(range.satisfiedBy('20.5.0')).toBe(false); + expect(range.satisfiedBy(new Version('20R3'))).toBe(true); + }); + + it('wildcard range matches both R and LTS', () => { + const range = new Range('*'); + expect(range.satisfiedBy(new Version('20R5'))).toBe(true); + expect(range.satisfiedBy(new Version('20.5.0'))).toBe(true); + }); + + it('maxSatisfying on LTS range excludes R versions from list', () => { + const range = new Range('^20.2.0'); + const versions = ['20.2.0', '20.3.0', '20R5', '20.4.0']; + // 20R5 normalizes to 20.5.0 numerically but must be excluded (wrong type) + expect(range.maxSatisfying(versions)).toBe('20.4.0'); + }); + + it('maxSatisfying on R range excludes LTS versions from list', () => { + const range = new Range('^20R2'); + const versions = ['20.5.0', '20R2', '20R3', '21.0.0']; + expect(range.maxSatisfying(versions)).toBe('20R3'); + }); + + it('minSatisfying on LTS range excludes R versions', () => { + const range = new Range('^20.2.0'); + const versions = ['20R2', '20.2.0', '20.3.0']; + expect(range.minSatisfying(versions)).toBe('20.2.0'); + }); + + it('4d keyword range with R IDE version matches only R versions', () => { + const ide = new Version('21R2'); + const range = new Range('4d', ide); + expect(range.satisfiedBy(new Version('21R2'))).toBe(true); + expect(range.satisfiedBy(new Version('21R3'))).toBe(true); + expect(range.satisfiedBy(new Version('21.5.0'))).toBe(false); + expect(range.satisfiedBy(new Version('22R1'))).toBe(false); + }); + + it('4d keyword range with LTS IDE version matches only LTS versions', () => { + const ide = new Version('21.2.0'); + const range = new Range('4d', ide); + expect(range.satisfiedBy(new Version('21.3.0'))).toBe(true); + expect(range.satisfiedBy(new Version('21R3'))).toBe(false); + }); + + it('should parse ~20R2.3 and match R patch range', () => { + const range = new Range('~20R2.3'); + expect(range.satisfiedBy('20R2.3')).toBe(true); + expect(range.satisfiedBy('20R2.9')).toBe(true); + expect(range.satisfiedBy('20R3')).toBe(false); // different minor + expect(range.satisfiedBy('20.2.3')).toBe(false); // LTS rejected + }); + }); + + describe('less than or equal range (<=)', () => { + it('should handle <=2.0.0 range', () => { + const range = new Range('<=2.0.0'); + expect(range.satisfiedBy('2.0.0')).toBe(true); + expect(range.satisfiedBy('1.9.9')).toBe(true); + expect(range.satisfiedBy('1.0.0')).toBe(true); + expect(range.satisfiedBy('2.0.1')).toBe(false); + expect(range.satisfiedBy('3.0.0')).toBe(false); + }); + + it('should combine >= and <= for a closed range', () => { + const range = new Range('>=1.0.0 <=2.0.0'); + expect(range.satisfiedBy('1.0.0')).toBe(true); + expect(range.satisfiedBy('1.5.0')).toBe(true); + expect(range.satisfiedBy('2.0.0')).toBe(true); + expect(range.satisfiedBy('0.9.9')).toBe(false); + expect(range.satisfiedBy('2.0.1')).toBe(false); + }); + }); + + describe('getSpec() method', () => { + it('should return original range specification string', () => { + expect(new Range('^1.2.3').getSpec()).toBe('^1.2.3'); + expect(new Range('~1.2.3').getSpec()).toBe('~1.2.3'); + expect(new Range('>=1.0.0 <2.0.0').getSpec()).toBe('>=1.0.0 <2.0.0'); + expect(new Range('*').getSpec()).toBe('*'); + expect(new Range('latest').getSpec()).toBe('latest'); + }); + + it('should preserve 4d keyword as spec', () => { + const ide = new Version('21.0.0'); + expect(new Range('4d', ide).getSpec()).toBe('4d'); + }); + }); + + describe('intersect() method', () => { + it('should return a new Range for overlapping ranges', () => { + const r1 = new Range('>=1.0.0 <3.0.0'); + const r2 = new Range('>=2.0.0 <4.0.0'); + const intersection = r1.intersect(r2); + expect(intersection).not.toBeNull(); + expect(intersection!.satisfiedBy('2.0.0')).toBe(true); + expect(intersection!.satisfiedBy('2.9.9')).toBe(true); + expect(intersection!.satisfiedBy('1.0.0')).toBe(false); + expect(intersection!.satisfiedBy('3.0.0')).toBe(false); + }); + + it('should return null for non-overlapping ranges', () => { + const r1 = new Range('^1.0.0'); + const r2 = new Range('^2.0.0'); + expect(r1.intersect(r2)).toBeNull(); + }); + + it('intersection of identical ranges should satisfy same versions', () => { + const r1 = new Range('^1.2.0'); + const r2 = new Range('^1.2.0'); + const intersection = r1.intersect(r2); + expect(intersection).not.toBeNull(); + expect(intersection!.satisfiedBy('1.2.5')).toBe(true); + expect(intersection!.satisfiedBy('2.0.0')).toBe(false); + }); + }); + + describe('Range.parse() static method', () => { + it('should return a Range instance for valid spec', () => { + const r = Range.parse('^1.2.3'); + expect(r).not.toBeNull(); + expect(r!.satisfiedBy('1.2.3')).toBe(true); + expect(r!.satisfiedBy('2.0.0')).toBe(false); + }); + + it('should return null for invalid spec instead of throwing', () => { + expect(Range.parse('not a valid range !@#')).toBeNull(); + }); + + it('should forward ideVersion to constructor', () => { + const ide = new Version('21.0.0'); + const r = Range.parse('4d', ide); + expect(r).not.toBeNull(); + expect(r!.satisfiedBy('21.5.0')).toBe(true); + expect(r!.satisfiedBy('22.0.0')).toBe(false); + }); + }); + + describe('Range.isValid() static method', () => { + it('should return true for valid range specs', () => { + expect(Range.isValid('^1.2.3')).toBe(true); + expect(Range.isValid('~1.0.0')).toBe(true); + expect(Range.isValid('>=1.0.0 <2.0.0')).toBe(true); + expect(Range.isValid('*')).toBe(true); + expect(Range.isValid('latest')).toBe(true); + expect(Range.isValid('1.2.3')).toBe(true); + }); + + it('should return false for invalid range specs', () => { + expect(Range.isValid('not a valid range !@#')).toBe(false); + }); + }); + + describe('Range.satisfies() static helper', () => { + it('should check if version satisfies range spec', () => { + expect(Range.satisfies('1.2.3', '^1.0.0')).toBe(true); + expect(Range.satisfies('2.0.0', '^1.0.0')).toBe(false); + expect(Range.satisfies('1.0.0', '>=1.0.0 <2.0.0')).toBe(true); + expect(Range.satisfies('2.0.0', '>=1.0.0 <2.0.0')).toBe(false); + }); + + it('should support 4d keyword with ideVersion', () => { + const ide = new Version('21.0.0'); + expect(Range.satisfies('21.3.0', '4d', ide)).toBe(true); + expect(Range.satisfies('22.0.0', '4d', ide)).toBe(false); + }); + }); + + describe('Range.maxSatisfying() static helper', () => { + it('should find max satisfying version from a list', () => { + const versions = ['1.0.0', '1.2.0', '1.5.0', '2.0.0']; + expect(Range.maxSatisfying(versions, '^1.0.0')).toBe('1.5.0'); + }); + + it('should return null if nothing satisfies', () => { + expect(Range.maxSatisfying(['1.0.0', '1.5.0'], '^2.0.0')).toBeNull(); + }); + }); + + describe('Range.minSatisfying() static helper', () => { + it('should find min satisfying version from a list', () => { + const versions = ['1.0.0', '1.2.0', '1.5.0', '2.0.0']; + expect(Range.minSatisfying(versions, '^1.0.0')).toBe('1.0.0'); + }); + + it('should return null if nothing satisfies', () => { + expect(Range.minSatisfying(['1.0.0', '1.5.0'], '^2.0.0')).toBeNull(); + }); + }); }); diff --git a/packageManager/test/version.test.ts b/packageManager/test/version.test.ts index 6e89ff5..c2f855b 100644 --- a/packageManager/test/version.test.ts +++ b/packageManager/test/version.test.ts @@ -168,4 +168,270 @@ describe('Version', () => { expect(info.raw).toBe('20R2'); }); }); + + describe('4D R-Release compact format with patch (20R2.1)', () => { + it('should parse compact R-Release with patch (20R2.1)', () => { + const v = new Version('20R2.1'); + expect(v.major).toBe(20); + expect(v.minor).toBe(2); + expect(v.patch).toBe(1); + expect(v.isR).toBe(true); + }); + + it('should parse compact R-Release with multi-digit patch (20R3.42)', () => { + const v = new Version('20R3.42'); + expect(v.major).toBe(20); + expect(v.minor).toBe(3); + expect(v.patch).toBe(42); + expect(v.isR).toBe(true); + }); + + it('should correctly compare R-Release with patch against same minor (20R2 < 20R2.1)', () => { + const v1 = new Version('20R2'); + const v2 = new Version('20R2.1'); + expect(v1.lt(v2)).toBe(true); + expect(v2.gt(v1)).toBe(true); + }); + + it('20R2.3 and 20.R2.3 should be equivalent', () => { + const a = new Version('20R2.3'); + const b = new Version('20.R2.3'); + expect(a.eq(b)).toBe(true); + expect(a.major).toBe(b.major); + expect(a.minor).toBe(b.minor); + expect(a.patch).toBe(b.patch); + }); + }); + + describe('compare() method', () => { + it('should return -1 when less than', () => { + const v1 = new Version('1.0.0'); + const v2 = new Version('2.0.0'); + expect(v1.compare(v2)).toBe(-1); + }); + + it('should return 1 when greater than', () => { + const v1 = new Version('2.0.0'); + const v2 = new Version('1.0.0'); + expect(v1.compare(v2)).toBe(1); + }); + + it('should return 0 when equal', () => { + const v1 = new Version('1.2.3'); + const v2 = new Version('1.2.3'); + expect(v1.compare(v2)).toBe(0); + }); + + it('should compare patch correctly (1.0.1 < 1.0.2)', () => { + const v1 = new Version('1.0.1'); + const v2 = new Version('1.0.2'); + expect(v1.compare(v2)).toBe(-1); + expect(v2.compare(v1)).toBe(1); + }); + + it('should compare minor correctly (1.1.0 < 1.2.0)', () => { + const v1 = new Version('1.1.0'); + const v2 = new Version('1.2.0'); + expect(v1.compare(v2)).toBe(-1); + }); + + it('should compare R-Release versions (20R2 < 20R3)', () => { + const v1 = new Version('20R2'); + const v2 = new Version('20R3'); + expect(v1.compare(v2)).toBe(-1); + expect(v2.compare(v1)).toBe(1); + }); + + it('should compare R-Release with patch (20R2 < 20R2.1)', () => { + const v1 = new Version('20R2'); + const v2 = new Version('20R2.1'); + expect(v1.compare(v2)).toBe(-1); + }); + + it('should treat prerelease as less than release (1.0.0-alpha < 1.0.0)', () => { + const v1 = new Version('1.0.0-alpha'); + const v2 = new Version('1.0.0'); + expect(v1.compare(v2)).toBe(-1); + expect(v2.compare(v1)).toBe(1); + }); + + it('should compare prerelease lexically (1.0.0-alpha < 1.0.0-beta)', () => { + const v1 = new Version('1.0.0-alpha'); + const v2 = new Version('1.0.0-beta'); + expect(v1.compare(v2)).toBe(-1); + }); + + it('should compare prerelease lexically (1.0.0-beta < 1.0.0-rc)', () => { + const v1 = new Version('1.0.0-beta'); + const v2 = new Version('1.0.0-rc'); + expect(v1.compare(v2)).toBe(-1); + }); + }); + + describe('toIDEString() method', () => { + it('should return major.minor.patch string for standard version', () => { + const v = new Version('1.2.3'); + expect(v.toIDEString()).toBe('1.2.3'); + }); + + it('should return major.minor.0 for R-Release without patch (20R2)', () => { + const v = new Version('20R2'); + expect(v.toIDEString()).toBe('20.2.0'); + }); + + it('should return major.minor.patch for R-Release with patch (20.R2.3)', () => { + const v = new Version('20.R2.3'); + expect(v.toIDEString()).toBe('20.2.3'); + }); + + it('should strip prerelease and build metadata', () => { + const v = new Version('1.2.3-alpha.1+build.42'); + expect(v.toIDEString()).toBe('1.2.3'); + }); + }); + + describe('toSemanticString() method', () => { + it('should return plain semver string for basic version', () => { + const v = new Version('1.2.3'); + expect(v.toSemanticString()).toBe('1.2.3'); + }); + + it('should include prerelease in output', () => { + const v = new Version('1.0.0-alpha.1'); + expect(v.toSemanticString()).toBe('1.0.0-alpha.1'); + }); + + it('should include build metadata in output', () => { + const v = new Version('1.0.0+build.123'); + expect(v.toSemanticString()).toBe('1.0.0+build.123'); + }); + + it('should include both prerelease and build metadata', () => { + const v = new Version('1.0.0-alpha+build'); + expect(v.toSemanticString()).toBe('1.0.0-alpha+build'); + }); + }); + + describe('Version.parse() static method', () => { + it('should return a Version instance for valid string', () => { + const v = Version.parse('1.2.3'); + expect(v).not.toBeNull(); + expect(v!.major).toBe(1); + expect(v!.minor).toBe(2); + expect(v!.patch).toBe(3); + }); + + it('should return null for invalid string instead of throwing', () => { + expect(Version.parse('invalid')).toBeNull(); + expect(Version.parse('')).toBeNull(); + expect(Version.parse('not.a.version!')).toBeNull(); + }); + + it('should parse R-Release via static method', () => { + const v = Version.parse('20R2'); + expect(v).not.toBeNull(); + expect(v!.isR).toBe(true); + expect(v!.major).toBe(20); + }); + }); + + describe('Version.isValid() static method', () => { + it('should return true for valid versions', () => { + expect(Version.isValid('1.2.3')).toBe(true); + expect(Version.isValid('v1.2.3')).toBe(true); + expect(Version.isValid('20R2')).toBe(true); + expect(Version.isValid('20.R2.3')).toBe(true); + expect(Version.isValid('1.0.0-alpha')).toBe(true); + }); + + it('should return false for invalid versions', () => { + expect(Version.isValid('invalid')).toBe(false); + expect(Version.isValid('')).toBe(false); + expect(Version.isValid('abc.def.ghi')).toBe(false); + }); + }); + + describe('Prerelease comparisons', () => { + it('prerelease should be less than release (lt)', () => { + const alpha = new Version('1.0.0-alpha'); + const release = new Version('1.0.0'); + expect(alpha.lt(release)).toBe(true); + expect(release.lt(alpha)).toBe(false); + }); + + it('prerelease ordering: alpha < beta (compare)', () => { + const alpha = new Version('1.0.0-alpha'); + const beta = new Version('1.0.0-beta'); + expect(alpha.compare(beta)).toBe(-1); + expect(alpha.eq(beta)).toBe(false); + }); + + it('same prerelease should be equal', () => { + const v1 = new Version('1.0.0-alpha'); + const v2 = new Version('1.0.0-alpha'); + expect(v1.eq(v2)).toBe(true); + expect(v1.compare(v2)).toBe(0); + }); + }); + + describe('R-Release comparisons', () => { + it('20R2 lt 20R3 should be true', () => { + expect(new Version('20R2').lt(new Version('20R3'))).toBe(true); + }); + + it('20R3 gt 20R2 should be true', () => { + expect(new Version('20R3').gt(new Version('20R2'))).toBe(true); + }); + + it('20R2 eq 20.R2 should be true', () => { + expect(new Version('20R2').eq(new Version('20.R2'))).toBe(true); + }); + + it('20R2.3 eq 20.R2.3 should be true', () => { + expect(new Version('20R2.3').eq(new Version('20.R2.3'))).toBe(true); + }); + + it('20R2.3 gt 20R2.2 should be true', () => { + expect(new Version('20R2.3').gt(new Version('20R2.2'))).toBe(true); + }); + + it('20R2.3 gte 20R2.3 should be true', () => { + expect(new Version('20R2.3').gte(new Version('20R2.3'))).toBe(true); + }); + + it('20R2 compare 20R2 should be 0', () => { + expect(new Version('20R2').compare(new Version('20R2'))).toBe(0); + }); + }); + + describe('R-Release vs LTS cross-type comparisons', () => { + it('20R2 gt 20.2.0 (same minor number)', () => { + expect(new Version('20R2').gt(new Version('20.2.0'))).toBe(true); + expect(new Version('20.2.0').lt(new Version('20R2'))).toBe(true); + }); + + it('20R2 gt 20.5.0 (LTS has bigger minor)', () => { + // R-release beats LTS regardless of minor number within same major + expect(new Version('20R2').gt(new Version('20.5.0'))).toBe(true); + expect(new Version('20.5.0').lt(new Version('20R2'))).toBe(true); + }); + + it('20R1 gt 20.9.9 (R minor smaller than LTS minor)', () => { + expect(new Version('20R1').gt(new Version('20.9.9'))).toBe(true); + }); + + it('20R2 not equal to 20.2.0', () => { + expect(new Version('20R2').eq(new Version('20.2.0'))).toBe(false); + }); + + it('21.0.0 gt 20R9 (different major)', () => { + expect(new Version('21.0.0').gt(new Version('20R9'))).toBe(true); + expect(new Version('20R9').lt(new Version('21.0.0'))).toBe(true); + }); + + it('compare returns 1 for 20R2 vs 20.5.0', () => { + expect(new Version('20R2').compare(new Version('20.5.0'))).toBe(1); + expect(new Version('20.5.0').compare(new Version('20R2'))).toBe(-1); + }); + }); });