import assert from 'node:assert/strict' import { test } from 'node:test' import { fixture } from './fixtures.mjs' import { MANIFESTS, latestRelease, manifestVersion, nextVersion, parseVersion, } from '../changelog-version.mjs' const PROJECTS = [ 'start-os', 'start-cli', 'start-sdk', 'start-tunnel', 'start-registry', 'start-wrt', ] const TIERS = ['patch', 'minor', 'major'] function* combinations(values, length) { if (!length) yield [] else for (const value of values) for (const rest of combinations(values, length - 1)) yield [value, ...rest] } function* permutations(values) { if (!values.length) yield [] else for (let i = 0; i < values.length; i++) for (const rest of permutations(values.filter((_, index) => index !== i))) yield [values[i], ...rest] } test('public parser normalizes StartOS and preserves prerelease', () => { assert.deepEqual(parseVersion('start-os', '0.4.1'), [[4n, 1n, 0n], null]) assert.deepEqual(parseVersion('start-os', '0.4.0.3-rc.1'), [ [4n, 0n, 3n], 'rc.1', ]) assert.deepEqual(parseVersion('start-sdk', '4.0.3+build.1'), [ [4n, 0n, 3n], null, ]) }) test('public parser rejects noncanonical versions', () => { for (const [product, version] of [ ['start-os', '9.0.0'], ['start-sdk', '1.2.3.4'], ['start-sdk', '1.2.3-rc.01'], ['other', '1.2.3'], ]) assert.throws(() => parseVersion(product, version)) }) test('all tier combinations and orders', () => { for (const [project, baseline, results] of [ ['start-os', '0.4.0.3', ['0.4.0.4', '0.4.1', '0.5.0']], ['start-sdk', '4.0.3', ['4.0.4', '4.1.0', '5.0.0']], ]) { for (const [allowed, expected] of [ [['patch'], results[0]], [['patch', 'minor'], results[1]], [['patch', 'minor', 'major'], results[2]], ]) { for (let length = 1; length < 5; length++) for (const tiers of combinations(allowed, length)) if (tiers.includes(allowed.at(-1))) assert.equal( nextVersion(project, baseline, tiers.values()), expected, ) } } }) test('empty fragments preserve exact version', () => { for (const project of PROJECTS) for (const version of project === 'start-os' ? ['0.4.0', '0.4.0.0', '0.4.0.3'] : ['0.0.0', '1.2.3']) assert.equal(nextVersion(project, version, [].values()), version) }) test('carry and reset boundaries', () => { const cases = [ ['start-sdk', '0.0.0', ['0.0.1', '0.1.0', '1.0.0']], ['start-cli', '9.9.9', ['9.9.10', '9.10.0', '10.0.0']], ['start-registry', '1.99.999', ['1.99.1000', '1.100.0', '2.0.0']], ['start-tunnel', '99.1.2', ['99.1.3', '99.2.0', '100.0.0']], ['start-wrt', '1.2.3', ['1.2.4', '1.3.0', '2.0.0']], ['start-os', '0.0.0', ['0.0.0.1', '0.0.1', '0.1.0']], ['start-os', '0.4.0', ['0.4.0.1', '0.4.1', '0.5.0']], ['start-os', '0.4.0.0', ['0.4.0.1', '0.4.1', '0.5.0']], ['start-os', '0.9.9.9', ['0.9.9.10', '0.9.10', '0.10.0']], ['start-os', '0.4.99.999', ['0.4.99.1000', '0.4.100', '0.5.0']], ] for (const [project, baseline, results] of cases) TIERS.forEach((tier, index) => assert.equal(nextVersion(project, baseline, [tier]), results[index]), ) }) test('invalid tier even after major', () => { for (const tier of ['', 'PATCH', 'security', 'minor ', null, 1]) assert.throws(() => nextVersion('start-sdk', '1.2.3', ['major', tier])) }) test('malformed versions', () => { const invalid = [ '', '1', '1.2', 'v1.2.3', '1.2.3.4', '01.2.3', '1.02.3', '1.2.03', '-1.2.3', '1.2.3\n', ' 1.2.3', '1.2.3-', '1.2.3+', '1.2.3-alpha.01', '1.2.3-a_1', '1.2.3+a_1', null, 123, ] for (const project of PROJECTS) { for (const value of invalid.concat( project === 'start-os' ? ['0.4', '0.04.0', '0.4.0.03', '0.4.0.1.2', '1.4.0', '1.4.0.3'] : [], )) assert.throws(() => nextVersion(project, value, [])) } }) test('prereleases are not baselines', () => { for (const [project, version] of [ ['start-sdk', '1.2.3-beta.1'], ['start-os', '0.4.0.3-rc.1'], ]) assert.throws(() => nextVersion(project, version, [])) }) test('build metadata is stable for next version', () => { assert.equal(nextVersion('start-sdk', '1.2.3+build.1', []), '1.2.3+build.1') assert.equal(nextVersion('start-sdk', '1.2.3+build.1', ['patch']), '1.2.4') }) test('numeric order independent of tag order', () => { for (const tags of permutations([ 'start-sdk/v9.99.99', 'start-sdk/v10.0.0', 'start-sdk/v9.100.0', 'start-sdk/v2.0.0', ])) assert.equal(latestRelease('start-sdk', tags.values()), '10.0.0') assert.equal( latestRelease('start-sdk', ['start-sdk/v1.9.9', 'start-sdk/v1.10.0']), '1.10.0', ) assert.equal( latestRelease('start-sdk', ['start-sdk/v1.2.9', 'start-sdk/v1.2.10']), '1.2.10', ) }) test('exact namespace and stable tags', () => { for (const project of PROJECTS) { const version = project === 'start-os' ? '0.4.0.3' : '1.2.3' const tags = [ 'v99.0.0', 'other/v99.0.0', `${project}-extra/v99.0.0`, `refs/tags/${project}/v99.0.0`, `${project}/99.0.0`, `${project}/V99.0.0`, ...['-alpha.1', '-beta.9', '-rc.1', '/extra'].map( suffix => `${project}/v${version}${suffix}`, ), `${project}/v01.2.3`, `${project}/v1.2.3.4.5`, `${project}/v${version}`, ] assert.equal(latestRelease(project, tags), version) } }) test('prereleases do not override stable', () => { assert.equal( latestRelease('start-wrt', ['start-wrt/v1.0.0', 'start-wrt/v2.0.0-beta.4']), '1.0.0', ) assert.equal( latestRelease('start-os', ['start-os/v0.4.0.3', 'start-os/v0.5.0-rc.1']), '0.4.0.3', ) }) test('no stable release', () => { for (const tags of [ [], ['start-sdk/v1.0.0-beta.1'], ['other/v1.0.0'], ['start-sdk/vbad'], ['start-sdk/v1.2.3.4'], ]) assert.equal(latestRelease('start-sdk', tags), null) }) test('StartOS normalizes omitted patch for comparison', () => { for (const [tags, expected] of [ [['0.4.0.99', '0.4.1'], '0.4.1'], [['0.4.0', '0.4.0.1'], '0.4.0.1'], [['0.9.99.99', '0.10.0'], '0.10.0'], [['1.2.3.4', '0.4.0.01'], null], ]) assert.equal( latestRelease( 'start-os', tags.map(version => `start-os/v${version}`), ), expected, ) }) test('build metadata is stable for latest release', () => { assert.equal( latestRelease('start-sdk', ['start-sdk/v1.2.3+build.4']), '1.2.3+build.4', ) }) test('canonical manifest paths', t => { const { root, write } = fixture(t) write('package.json', '{"version":"0.4.0.3"}') write('projects/start-os/Cargo.toml', '[package]\nversion = "0.4.0-rev.3"') write('projects/start-os/package.json', '{"version":"9.9.9"}') write('projects/start-sdk/package.json', '{"version":"3.0.3"}') write('projects/start-sdk/Cargo.toml', '[package]\nversion = "9.9.9"') for (const project of ['start-cli', 'start-tunnel', 'start-registry']) { write( `projects/${project}/Cargo.toml`, '[package]\nversion = "1.2.3" # VERSION_BUMP\n', ) assert.equal(manifestVersion(root, project), '1.2.3') } write('projects/start-wrt/Cargo.toml', '[package]\nversion = "9.9.9"') write( 'projects/start-wrt/backend/ctrl/Cargo.toml', '[package]\nversion = "1.3.0"', ) assert.equal(manifestVersion(root, 'start-os'), '0.4.0.3') assert.equal(manifestVersion(root, 'start-sdk'), '3.0.3') assert.equal(manifestVersion(root, 'start-wrt'), '1.3.0') }) test('package table only and literal quotes', t => { const { root, write } = fixture(t) write( 'projects/start-cli/Cargo.toml', `[dependencies] version = "9.9.9" # VERSION_BUMP [ package ] # manifest name = "start-cli" version = '1.2.3-beta.1+build' # VERSION_BUMP [[bin]] version = "8.8.8" [package.metadata] version = "7.7.7" `, ) assert.equal(manifestVersion(root, 'start-cli'), '1.2.3-beta.1+build') }) test('malformed or missing package version', t => { const { root, write } = fixture(t) for (const text of [ '[dependencies]\nversion = "1.2.3"', '[package]\nname = "cli"', '[package]\nversion.workspace = true', '[package]\nversion = 123', '[package]\nversion = "1.2.3"\nversion = "1.2.4"', '[package]\nversion = "1.2.3.4"', '[package]\nversion = "1.2.03"', '[package]\nversion = "1.2.3-alpha.01"', ]) { write('projects/start-cli/Cargo.toml', text) assert.throws(() => manifestVersion(root, 'start-cli')) } }) test('invalid JSON version', t => { const { root, write } = fixture(t) for (const text of [ '{}', '[]', '{"version":null}', '{"version":123}', '{"version":"1.2.3.4"}', '{', ]) { write('projects/start-sdk/package.json', text) assert.throws(() => manifestVersion(root, 'start-sdk')) } }) test('missing manifest', t => { const { root } = fixture(t) assert.throws(() => manifestVersion(root, 'start-wrt'), { code: 'ENOENT' }) }) test('unknown project', t => { const { root } = fixture(t) for (const project of ['other', '../start-os', 'projects/start-sdk', '']) { assert.throws(() => manifestVersion(root, project)) assert.throws(() => nextVersion(project, '1.2.3', [])) assert.throws(() => latestRelease(project, [])) } }) test('arbitrary sized numeric components retain precision', () => { assert.equal( nextVersion( 'start-sdk', '9007199254740993.9007199254740993.9007199254740993', ['patch'], ), '9007199254740993.9007199254740993.9007199254740994', ) assert.equal( nextVersion('start-os', '0.9007199254740993.1', ['major']), '0.9007199254740994.0', ) assert.equal( latestRelease('start-sdk', [ 'start-sdk/v9007199254740993.0.0', 'start-sdk/v9007199254740992.0.0', ]), '9007199254740993.0.0', ) }) test('manifest UTF-8 decoding is strict', t => { const { root, write } = fixture(t) write(MANIFESTS['start-sdk'], Buffer.from([0xff])) assert.throws(() => manifestVersion(root, 'start-sdk')) })