import assert from 'node:assert/strict' import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs' import { basename, join } from 'node:path' import { test } from 'node:test' import { fixture, REPO } from './fixtures.mjs' const SCRIPT = join(REPO, 'scripts/changelog.mjs') const PRETTIER = JSON.parse(readFileSync(join(REPO, 'package.json'))) .devDependencies.prettier const PRODUCT = 'projects/start-os' const HISTORY = Buffer.from( '# Changelog\r\n\r\n## [0.3.0]\r\n\r\n### Fixed\r\n\r\n- Historical bytes.\r\n', ) function compilerFixture(t) { const f = fixture(t) const { root, env, run, git, write } = f git('init', '-q', '-b', 'master') git('config', 'user.name', 'Fixture') git('config', 'user.email', 'fixture@example.com') git('config', 'commit.gpgsign', 'false') git('config', 'tag.gpgsign', 'false') git('config', 'core.hooksPath', '/dev/null') git('remote', 'add', 'origin', root) const fragments = join(root, PRODUCT, 'changelog') mkdirSync(fragments, { recursive: true }) const history = write(`${PRODUCT}/CHANGELOG.md`, HISTORY) write('.prettierrc.json', readFileSync(join(REPO, '.prettierrc.json'))) write( 'package.json', JSON.stringify({ devDependencies: { prettier: PRETTIER } }), ) function commit(date) { git('add', '.') const result = run('git', ['commit', '--allow-empty', '-qm', 'fixture'], { env: date ? { ...env, GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date } : env, }) assert.equal(result.status, 0, result.stderr.toString()) return git('rev-parse', 'HEAD').trim() } function tag(version, annotated = false) { const name = `start-os/v${version}` git('tag', ...(annotated ? ['-a', '-m', 'fixture'] : []), name) return name } function fragment( name = 'patch-fixed-example.md', body = '- Fixed example.\n', releaseNotes = true, ) { return write( `${PRODUCT}/changelog/${name}`, releaseNotes === null ? body : Buffer.concat([ Buffer.from(`---\nrelease-notes: ${releaseNotes}\n---\n\n`), Buffer.from(body), ]), ) } function cli(args, success = true) { const result = run(process.execPath, [SCRIPT, ...args]) if (success) assert.equal(result.status, 0, result.stderr.toString()) else assert.notEqual(result.status, 0) return result } function render(version = '0.4.0.1', ref) { return cli(['render', PRODUCT, version, ...(ref ? ['--ref', ref] : [])]) .stdout } function manifest(version, product = PRODUCT) { const name = basename(product) const path = name === 'start-os' ? 'package.json' : name === 'start-sdk' ? `${product}/package.json` : name === 'start-wrt' ? `${product}/backend/ctrl/Cargo.toml` : `${product}/Cargo.toml` write( path, path.endsWith('.json') ? JSON.stringify({ version, devDependencies: { prettier: PRETTIER } }) : `[package]\nversion = "${version}"\n`, ) } commit() return { ...f, fragments, history, commit, tag, fragment, cli, render, manifest, } } function contains(bytes, value) { assert.ok( bytes.includes(value), `Missing ${JSON.stringify(value.toString())}`, ) } function excludes(bytes, value) { assert.ok( !bytes.includes(value), `Unexpected ${JSON.stringify(value.toString())}`, ) } function count(bytes, value) { return bytes.toString().split(value).length - 1 } test('deterministic grouping and multiline bullets', t => { const { fragment, render } = compilerFixture(t) fragment('major-security-z.md', '- Security.\n') fragment( 'patch-added-z.md', '- Zed.\n Continued **Markdown**.\n\n- Second.\n', ) fragment('minor-added-a.md', '* Alpha.\n') fragment('patch-removed-a.md', '- Removed.\n') fragment('patch-deprecated-a.md', '- Deprecated.\n') fragment('patch-changed-a.md', '- Changed.\n') fragment('patch-fixed-a.md', '- Fixed.\n') const expected = Buffer.from( '# Changelog\n\n## [0.4.0.1]\n\n' + '### Added\n\n- Alpha.\n\n* Zed.\n Continued **Markdown**.\n\n* Second.\n\n' + '### Changed\n\n- Changed.\n\n### Deprecated\n\n- Deprecated.\n\n' + '### Removed\n\n- Removed.\n\n### Fixed\n\n- Fixed.\n\n' + '### Security\n\n- Security.\n\n' + HISTORY.subarray(HISTORY.indexOf('## ')) .toString() .replaceAll('\r\n', '\n'), ) assert.deepEqual(render(), expected) assert.deepEqual(render(), expected) }) test('assembled loose list is formatted and sync is idempotent', t => { const { history, fragment, commit, tag, render, cli, git } = compilerFixture(t) writeFileSync(history, '') fragment('patch-fixed-a.md', '- First.\n\n- Second.\n') fragment('patch-fixed-b.md', '- Third.\n') commit() const release = tag('0.4.0.1') const rendered = render('0.4.0.1', release) assert.deepEqual( rendered, Buffer.from( '## [0.4.0.1]\n\n### Fixed\n\n- First.\n\n- Second.\n\n- Third.\n', ), ) cli(['sync']) assert.deepEqual(readFileSync(history), rendered) cli(['sync']) assert.deepEqual(readFileSync(history), rendered) commit() fragment('patch-fixed-a.md', '- Genuinely changed.\n\n- Second.\n') fragment('patch-fixed-b.md', '- Third.\n') commit() git('tag', '-f', release) cli(['sync'], false) assert.deepEqual(readFileSync(history), rendered) }) test('formatted marker changes are idempotent', t => { const { history, fragment, commit, tag, cli, git } = compilerFixture(t) writeFileSync(history, '') fragment('patch-fixed-a.md', '* Alpha.\n') fragment('patch-fixed-b.md', '- Beta.\n') commit() tag('0.4.0.1') cli(['sync']) const expected = Buffer.from( '## [0.4.0.1]\n\n### Fixed\n\n- Alpha.\n\n* Beta.\n', ) assert.deepEqual(readFileSync(history), expected) commit() cli(['sync']) assert.deepEqual(readFileSync(history), expected) assert.equal(git('diff', 'HEAD'), '') }) test('invalid UTF-8 is rejected', t => { const { fragment, cli, commit, tag } = compilerFixture(t) fragment( undefined, Buffer.concat([ Buffer.from('- Invalid '), Buffer.from([0xff]), Buffer.from('.\n'), ]), ) cli(['validate', PRODUCT], false) commit() const release = tag('0.4.0.1') cli(['render', PRODUCT, '0.4.0.1', '--ref', release], false) }) test('tagged large blob and render stdout are not truncated', t => { const { fragment, commit, tag, render } = compilerFixture(t) const body = `- ${'x'.repeat(1024 * 1024 + 100)}.\n` fragment(undefined, body) commit() const release = tag('0.4.0.1') contains(render('0.4.0.1', release), Buffer.from(body)) }) test('noncompiling commands do not require formatter configuration', t => { const { root, manifest, write, fragment, cli } = compilerFixture(t) unlinkSync(join(root, '.prettierrc.json')) manifest('0.4.0.3') write('package.json', '{"version":"0.4.0.3"}') fragment() cli(['projects']) cli(['version', PRODUCT]) cli(['validate', PRODUCT]) cli(['check-version', PRODUCT]) cli(['changed', 'HEAD']) }) test('command help', async t => { const { cli } = compilerFixture(t) for (const args of [['--help'], ['-h'], ['render', '--help']]) await t.test(JSON.stringify(args), () => { const output = cli(args).stdout contains(output, 'render PRODUCT VERSION [--ref REF]') contains(output, 'check-version PRODUCT [VERSION]') }) }) test('invalid filenames', async t => { const { fragment, cli } = compilerFixture(t) for (const name of [ 'fixed-name.md', 'patch-Fixed-name.md', 'patch-other-name.md', 'patch-fixed-.md', 'patch-fixed-name.txt', 'patch-fixed-name space.md', ]) await t.test(name, () => { const path = fragment(name) cli(['validate', PRODUCT], false) unlinkSync(path) }) }) test('rejected bodies', async t => { const { fragment, cli } = compilerFixture(t) for (const body of [ '', ' \n', 'plain text', '- Item.\n\n## Heading\n', '- Item.\nHeading\n=======\n', '- Item.\n ### Heading\n', ]) await t.test(JSON.stringify(body), () => { fragment(undefined, body) cli(['validate', PRODUCT], false) }) }) test('section extracts exactly one compiled version', t => { const { run } = compilerFixture(t) const history = Buffer.from( '# Changelog\n\n## [1.2.30]\n\n- Newer.\n\n' + '## [1.2.3]\n\n### Fixed\n\n- This release.\n\n' + '## [1.2.2]\n\n- Older.\n', ) const section = version => { const result = run(process.execPath, [SCRIPT, 'section', version], { input: history, }) assert.equal(result.status, 0, result.stderr.toString()) return result.stdout } assert.equal( section('1.2.3').toString(), '## [1.2.3]\n\n### Fixed\n\n- This release.\n\n', ) assert.equal(section('1.2.2').toString(), '## [1.2.2]\n\n- Older.\n') assert.equal(section('1.2.4').length, 0) }) test('empty directory is valid', t => { const { cli, render } = compilerFixture(t) cli(['validate', PRODUCT]) assert.deepEqual(render(), HISTORY) }) test('symlink and nested directory rejected', t => { const { root, fragments, write, cli, commit, tag } = compilerFixture(t) const target = write('outside.md', '- Outside.\n') const path = join(fragments, 'patch-fixed-link.md') symlinkSync(target, path) cli(['validate', PRODUCT], false) commit() const release = tag('0.4.0.1') cli(['render', PRODUCT, '0.4.0.1', '--ref', release], false) unlinkSync(path) mkdirSync(path) cli(['validate', PRODUCT], false) rmdirSync(path) rmdirSync(fragments) symlinkSync(root, fragments, 'dir') cli(['validate', PRODUCT], false) }) test('version injection rejected and prerelease accepted', t => { const { fragment, render, cli } = compilerFixture(t) fragment() render('0.4.0.1-rc.2+build.1') for (const version of ['1.2', '1.2.3\n## Injected', '[1.2.3]', '1.2.3/evil']) cli(['render', PRODUCT, version], false) }) test('render reads tag not newer master', t => { const { fragment, commit, tag, render, history } = compilerFixture(t) const original = fragment(undefined, '- Tagged.\n') commit() const release = tag('0.4.0.1', true) const expected = render('0.4.0.1', release) writeFileSync(original, '- Modified on master.\n') fragment('minor-added-new.md', '- New on master.\n') writeFileSync(history, '# Completely different master history\n') commit() assert.deepEqual(render('0.4.0.1', release), expected) excludes(expected, 'master') contains(expected, '- Tagged.') }) test('sync retains modified and new fragments and retries', t => { const { fragment, commit, tag, render, history, cli } = compilerFixture(t) const unchanged = fragment() const modified = fragment('minor-added-modified.md', '- Tagged added.\n') commit() const release = tag('0.4.0.1') const expected = render('0.4.0.1', release) writeFileSync(modified, '- Revised after tag.\n') const fresh = fragment('patch-fixed-new.md', '- New after tag.\n') commit() cli(['sync']) assert.deepEqual(readFileSync(history), expected) assert.ok(!existsSync(unchanged)) assert.equal(readFileSync(modified, 'utf8'), '- Revised after tag.\n') assert.ok(existsSync(fresh)) cli(['sync']) assert.deepEqual(readFileSync(history), expected) assert.ok(existsSync(modified)) assert.ok(existsSync(fresh)) }) test('overlapping snapshots and pending ancestor sections', t => { const { fragment, commit, tag, render, history, fragments, cli } = compilerFixture(t) const first = fragment(undefined, '- First release.\n') commit() tag('0.4.0.1') fragment('minor-added-next.md', '- Second release.\n') commit() const release = tag('0.4.0.2') const expected = render('0.4.0.2', release) assert.equal(count(expected, '- First release.'), 1) assert.equal(count(expected, '- Second release.'), 1) assert.ok(expected.indexOf('## [0.4.0.2]') < expected.indexOf('## [0.4.0.1]')) cli(['sync']) assert.deepEqual(readFileSync(history), expected) assert.ok(!existsSync(first)) assert.deepEqual(readdirSync(fragments), []) cli(['sync']) assert.deepEqual(readFileSync(history), expected) }) test('topology not semver or commit time orders tags', t => { const { fragment, commit, tag, render, history, cli } = compilerFixture(t) fragment(undefined, '- Ancestor.\n') commit('2026-01-02T00:00:00Z') tag('0.9.0') fragment('patch-fixed-descendant.md', '- Descendant.\n') commit('2026-01-01T00:00:00Z') const release = tag('0.1.0') const expected = render('0.1.0', release) cli(['sync']) assert.deepEqual(readFileSync(history), expected) assert.ok(expected.indexOf('## [0.1.0]') < expected.indexOf('## [0.9.0]')) }) test('unmerged tag excluded', t => { const { git, fragment, commit, tag, render, history, cli } = compilerFixture(t) git('checkout', '-qb', 'unmerged') fragment(undefined, '- Unmerged.\n') commit() tag('0.99.0') git('checkout', '-q', 'master') fragment(undefined, '- Merged.\n') commit() const release = tag('0.4.0.1') const rendered = render('0.4.0.1', release) cli(['sync']) assert.deepEqual(readFileSync(history), rendered) excludes(rendered, 'Unmerged') excludes(rendered, '0.99.0') }) test('legacy tag and existing version heading are preserved', t => { const { tag, cli, history, render, fragment, commit } = compilerFixture(t) tag('0.3.0') cli(['sync']) assert.deepEqual(readFileSync(history), HISTORY) assert.deepEqual(render('0.3.0', 'start-os/v0.3.0'), HISTORY) const path = fragment() const existing = render() writeFileSync(history, existing) commit() const release = tag('0.4.0.1') assert.deepEqual(render('0.4.0.1', release), existing) cli(['sync']) assert.deepEqual(readFileSync(history), existing) assert.ok(!existsSync(path)) cli(['sync']) assert.deepEqual(readFileSync(history), existing) }) test('moved processed tag does not consume updated fragment', t => { const { fragment, commit, tag, cli, history, git } = compilerFixture(t) fragment() commit() tag('0.4.0.1') cli(['sync']) const previous = readFileSync(history) commit() const path = fragment(undefined, '- Replacement release text.\n') commit() git('tag', '-f', 'start-os/v0.4.0.1') cli(['sync'], false) cli(['render', PRODUCT, '0.4.0.1', '--ref', 'start-os/v0.4.0.1'], false) assert.deepEqual(readFileSync(history), previous) assert.ok(existsSync(path)) }) test('reintroduced identical fragment belongs to next release', t => { const { fragment, commit, tag, cli, render, history } = compilerFixture(t) fragment() commit() tag('0.4.0.1') cli(['sync']) commit() const path = fragment() cli(['check-version', PRODUCT, '0.4.0.2']) cli(['sync']) assert.ok(existsSync(path)) commit() const release = tag('0.4.0.2') const rendered = render('0.4.0.2', release) assert.equal(count(rendered, '- Fixed example.'), 2) cli(['sync']) assert.deepEqual(readFileSync(history), rendered) assert.ok(!existsSync(path)) }) test('modified same path at next tag is new ownership', t => { const { fragment, commit, tag, render, cli, history } = compilerFixture(t) const path = fragment(undefined, '- Old text.\n') commit() tag('0.4.0.1') writeFileSync(path, '- New text.\n') commit() const release = tag('0.4.0.2') const rendered = render('0.4.0.2', release) assert.equal(count(rendered, '- Old text.'), 1) assert.equal(count(rendered, '- New text.'), 1) cli(['sync']) assert.deepEqual(readFileSync(history), rendered) assert.ok(!existsSync(path)) }) test('later sync reuses committed version markers', t => { const { fragment, commit, tag, cli, history, render } = compilerFixture(t) fragment(undefined, '- First release.\n') commit() tag('0.4.0.1') cli(['sync']) const firstHistory = readFileSync(history) commit() cli(['sync']) assert.deepEqual(readFileSync(history), firstHistory) fragment('minor-added-next.md', '- Second release.\n') commit() const release = tag('0.4.0.2') const expected = render('0.4.0.2', release) cli(['sync']) assert.deepEqual(readFileSync(history), expected) assert.equal(count(expected, '## [0.4.0.1]'), 1) const suffix = firstHistory.subarray(firstHistory.indexOf('## ')) assert.deepEqual(expected.subarray(-suffix.length), suffix) }) test('product ownership is independent', t => { const { fragment, write, commit, tag, git, cli, history, root } = compilerFixture(t) fragment(undefined, '- OS entry.\n') const other = write( 'projects/start-sdk/changelog/patch-fixed-example.md', '- SDK entry.\n', ) commit() tag('0.4.0.1') git('tag', 'start-sdk/v2.0.0') cli(['sync']) const osHistory = readFileSync(history) const sdkHistory = readFileSync(join(root, 'projects/start-sdk/CHANGELOG.md')) contains(osHistory, 'OS entry') excludes(osHistory, 'SDK entry') contains(sdkHistory, 'SDK entry') excludes(sdkHistory, 'OS entry') assert.ok(!existsSync(other)) }) test('version reads canonical manifests from absolute script', t => { const { manifest, cli } = compilerFixture(t) for (const name of [ 'start-os', 'start-sdk', 'start-cli', 'start-tunnel', 'start-registry', 'start-wrt', ]) { const product = `projects/${name}` const version = name === 'start-os' ? '0.4.0.3' : '1.2.3' manifest(version, product) assert.equal(cli(['version', product]).stdout.toString().trim(), version) } cli(['version', 'projects/unknown'], false) }) test('check version each tier and mismatching manifest', async t => { const { tag, fragment, manifest, cli } = compilerFixture(t) tag('0.4.0.3') for (const [tier, expected] of [ ['patch', '0.4.0.4'], ['minor', '0.4.1'], ['major', '0.5.0'], ]) await t.test(tier, () => { const path = fragment(`${tier}-changed-impact.md`) manifest(expected) cli(['check-version', PRODUCT]) manifest('0.4.0.3') const result = cli(['check-version', PRODUCT], false) contains(result.stderr, expected) unlinkSync(path) }) }) test('changed unrelated PR after ancestor CLI release', t => { const { manifest, commit, write, git, cli } = compilerFixture(t) const product = 'projects/start-cli' manifest('2.3.0', product) const releaseCommit = commit() const path = write( `${product}/changelog/patch-fixed-pending.md`, '---\nrelease-notes: false\n---\n\n- Pending CLI fix.\n', ) const base = commit() git('tag', 'start-cli/v2.3.0', releaseCommit) write('README.md', 'Unrelated PR.\n') const unrelated = commit() assert.deepEqual(cli(['changed', base]).stdout, Buffer.from('\n')) contains(cli(['check-version', product], false).stderr, "expected '2.3.1'") writeFileSync( path, '---\nrelease-notes: false\n---\n\n- Revised pending CLI fix.\n', ) commit() assert.deepEqual( cli(['changed', unrelated]).stdout, Buffer.from('projects/start-cli\n'), ) contains(cli(['check-version', product], false).stderr, "expected '2.3.1'") const fragmentHead = git('rev-parse', 'HEAD').trim() manifest('2.3.0+metadata', product) commit() assert.deepEqual( cli(['changed', fragmentHead]).stdout, Buffer.from('projects/start-cli\n'), ) contains(cli(['check-version', product], false).stderr, "expected '2.3.1'") }) test('changed uses merge base not base tip', t => { const { commit, git, manifest, write, cli, fragment } = compilerFixture(t) const base = commit() git('checkout', '-qb', 'base') manifest('1.2.3', 'projects/start-sdk') const baseTip = commit() git('checkout', '-q', 'master') write('README.md', 'PR-only change.\n') commit() assert.deepEqual(cli(['changed', baseTip]).stdout, Buffer.from('\n')) fragment() commit() assert.deepEqual( cli(['changed', baseTip]).stdout, Buffer.from('projects/start-os\n'), ) assert.deepEqual( cli(['changed', base]).stdout, Buffer.from('projects/start-os\n'), ) }) test('changed deletion rename and multiple products', t => { const { fragment, commit, root, cli, git, manifest } = compilerFixture(t) const deleted = fragment('patch-fixed-deleted.md') const renamed = fragment('patch-fixed-renamed.md') const base = commit() unlinkSync(deleted) const destination = join( root, 'projects/start-sdk/changelog/patch-fixed-renamed.md', ) mkdirSync(join(root, 'projects/start-sdk/changelog'), { recursive: true }) renameSync(renamed, destination) commit() assert.deepEqual( cli(['changed', base]).stdout, Buffer.from('projects/start-os projects/start-sdk\n'), ) const deletionBase = git('rev-parse', 'HEAD').trim() unlinkSync(destination) commit() assert.deepEqual( cli(['changed', deletionBase]).stdout, Buffer.from('projects/start-sdk\n'), ) manifest('1.2.3', 'projects/start-cli') const manifestBase = commit() renameSync( join(root, 'projects/start-cli/Cargo.toml'), join(root, 'removed-manifest.toml'), ) commit() assert.deepEqual( cli(['changed', manifestBase]).stdout, Buffer.from('projects/start-cli\n'), ) }) test('changed selects changelog directory replacements', t => { const { git, fragments, root, commit, cli } = compilerFixture(t) const base = git('rev-parse', 'HEAD').trim() rmdirSync(fragments) symlinkSync(root, fragments, 'dir') commit() assert.deepEqual( cli(['changed', base]).stdout, Buffer.from('projects/start-os\n'), ) unlinkSync(fragments) writeFileSync(fragments, 'Not a directory.\n') const replacementBase = git('rev-parse', 'HEAD').trim() commit() assert.deepEqual( cli(['changed', replacementBase]).stdout, Buffer.from('projects/start-os\n'), ) const deletionBase = git('rev-parse', 'HEAD').trim() unlinkSync(fragments) commit() assert.deepEqual( cli(['changed', deletionBase]).stdout, Buffer.from('projects/start-os\n'), ) }) test('changed matches canonical manifest paths exactly', async t => { const { git, manifest, commit, cli, write } = compilerFixture(t) for (const product of [ 'projects/start-os', 'projects/start-sdk', 'projects/start-cli', 'projects/start-tunnel', 'projects/start-registry', 'projects/start-wrt', ]) await t.test(product, () => { const base = git('rev-parse', 'HEAD').trim() manifest(product === PRODUCT ? '0.4.0.3' : '1.2.3', product) commit() assert.deepEqual( cli(['changed', base]).stdout, Buffer.from(`${product}\n`), ) }) const base = git('rev-parse', 'HEAD').trim() for (const name of [ 'projects/start-os/Cargo.toml', 'projects/start-os/package.json', 'projects/start-wrt/Cargo.toml', 'projects/start-sdk/package.json.backup', 'projects/start-cli/changelog-backup/patch-fixed-example.md', ]) write(name, 'Unrelated path.\n') commit() assert.deepEqual(cli(['changed', base]).stdout, Buffer.from('\n')) }) test('changed handles NUL delimited paths', t => { const { git, fragment, commit, cli } = compilerFixture(t) const base = git('rev-parse', 'HEAD').trim() fragment('patch-fixed-name\nwith-newline.md') commit() assert.deepEqual( cli(['changed', base]).stdout, Buffer.from('projects/start-os\n'), ) cli(['changed', 'nonexistent-base'], false) }) test('projects preserves release caller contract', t => { const { cli } = compilerFixture(t) assert.deepEqual( cli(['projects']).stdout, Buffer.from( 'start-os start-sdk start-cli start-tunnel start-registry start-wrt\n', ), ) }) test('mixed tiers use highest and omit StartOS zero revision', t => { const { tag, fragment, manifest, cli } = compilerFixture(t) tag('0.4.0.3') fragment('patch-fixed-small.md') fragment('minor-added-medium.md') manifest('0.4.1') cli(['check-version', PRODUCT]) cli(['check-version', PRODUCT, '0.4.1.0'], false) fragment('major-removed-large.md') manifest('0.5.0') cli(['check-version', PRODUCT]) cli(['check-version', PRODUCT, '0.4.1'], false) }) test('check version validates malformed fragments even without baseline', async t => { const { manifest, fragment, cli } = compilerFixture(t) manifest('0.4.0.3') for (const [name, body] of [ ['fixed-invalid.md', '- Invalid name.\n'], ['patch-fixed-invalid.md', '## Invalid heading\n'], ]) await t.test(name, () => { const path = fragment(name, body) cli(['check-version', PRODUCT], false) unlinkSync(path) }) fragment('major-added-first-release.md') cli(['check-version', PRODUCT]) cli(['check-version', PRODUCT, '0.9.0']) cli(['check-version', PRODUCT, '1.0.0'], false) }) test('no fragments keeps latest release and override is caller data', t => { const { tag, manifest, cli, root } = compilerFixture(t) tag('0.4.0.3') manifest('0.4.0.3') cli(['check-version', PRODUCT]) manifest('0.4.0.4') cli(['check-version', PRODUCT], false) cli(['check-version', PRODUCT, '0.4.0.3']) unlinkSync(join(root, 'package.json')) cli(['check-version', PRODUCT, '0.4.0.3']) cli(['check-version', PRODUCT], false) }) test('lingering released tiers are not counted again', t => { const { fragment, commit, tag, manifest, cli } = compilerFixture(t) fragment('major-added-released.md', '- Already released.\n') commit() tag('0.4.0.3', true) manifest('0.4.0.3') cli(['check-version', PRODUCT]) fragment('patch-fixed-pending.md') manifest('0.4.0.4') cli(['check-version', PRODUCT]) cli(['check-version', PRODUCT, '0.5.0'], false) }) test('modified path is pending but identical ancestor path is not', t => { const { fragment, commit, tag, manifest, cli } = compilerFixture(t) const path = fragment('minor-added-feature.md', '- Released feature.\n') commit() tag('0.4.0.3') fragment('minor-added-feature.md', '- Revised feature.\n') manifest('0.4.1') cli(['check-version', PRODUCT]) cli(['check-version', PRODUCT, '0.4.0.3'], false) }) test('live origin baseline is fresh and local unpublished tags ignored', t => { const { root, git, tag, fragment, manifest, cli } = compilerFixture(t) const remote = join(root, 'remote.git') git('init', '--bare', '-q', remote) git('remote', 'set-url', 'origin', remote) tag('0.4.0.3') git('push', '-q', 'origin', 'start-os/v0.4.0.3') fragment() manifest('0.4.0.4') tag('0.9.0') cli(['check-version', PRODUCT]) tag('0.4.0.8') git('push', '-q', 'origin', 'start-os/v0.4.0.8') cli(['check-version', PRODUCT], false) manifest('0.4.0.9') cli(['check-version', PRODUCT]) }) test('latest stable is numeric not topological and prereleases ignored', t => { const { tag, commit, fragment, manifest, cli } = compilerFixture(t) tag('0.4.0.10') commit() tag('0.4.0.9') tag('0.5.0-rc.1') fragment() manifest('0.4.0.11') cli(['check-version', PRODUCT]) }) test('check version queries origin once', t => { const { tag, manifest, root, env, cli } = compilerFixture(t) tag('0.4.0.3') manifest('0.4.0.3') const trace = join(root, 'git.trace') env.GIT_TRACE = trace cli(['check-version', PRODUCT]) assert.equal( count(readFileSync(trace), 'built-in: git ls-remote --tags origin'), 1, ) }) test('unknown origin tag namespace is ignored', t => { const { git, tag, manifest, cli } = compilerFixture(t) git('tag', 'unrelated/vnot-a-version') tag('0.4.0.3') manifest('0.4.0.3') cli(['check-version', PRODUCT]) cli(['sync']) }) test('nonancestor release is baseline but does not own pending fragments', t => { const { git, fragment, commit, tag, manifest, cli } = compilerFixture(t) git('checkout', '-qb', 'unmerged') fragment() commit() tag('0.4.0.3') git('checkout', '-q', 'master') fragment() manifest('0.4.0.4') cli(['check-version', PRODUCT]) }) test('check version semver product', t => { const { git, write, manifest, cli } = compilerFixture(t) const product = 'projects/start-sdk' git('tag', 'start-sdk/v2.0.9') write( `${product}/changelog/minor-added-feature.md`, '---\nrelease-notes: false\n---\n\n- Feature.\n', ) manifest('2.1.0', product) cli(['check-version', product]) cli(['check-version', product, '2.0.10'], false) }) test('release-note selection is mandatory and boolean', t => { const { fragment, cli } = compilerFixture(t) for (const body of [ '- Missing metadata.\n', '---\nrelease-notes: yes\n---\n\n- Invalid.\n', '---\nrelease-notes: "false"\n---\n\n- Invalid.\n', '---\nrelease-notes: true\nextra: value\n---\n\n- Invalid.\n', '---\nuser-facing: true\n---\n\n- Visible, not selected.\n', '---\nuser-facing: false\n---\n\n- Legacy metadata.\n', ]) { fragment(undefined, body, null) contains( cli(['validate', PRODUCT], false).stderr, 'release-notes: true or false', ) } for (const selected of [true, false]) { fragment(undefined, '- Valid.\n', selected) cli(['validate', PRODUCT]) } }) test('routine user-visible changes stay in the changelog without generating locales', t => { const { fragment, write, cli, render } = compilerFixture(t) fragment('patch-fixed-routine.md', '- Correct a button label.\n', false) write( `${PRODUCT}/changelog/patch-fixed-routine.pl_PL.md`, '- Poprawiono etykietę przycisku.\n', ) fragment('minor-added-feature.md', '- Notable capability.\n') const notes = cli(['notes', PRODUCT, '0.4.0.1']).stdout.toString() assert.equal(notes, '## Highlights\n\n- Notable capability.\n') contains(render(), 'Correct a button label.') contains(render(), 'Notable capability.') assert.equal(cli(['locales', PRODUCT, '0.4.0.1']).stdout.toString(), '\n') assert.deepEqual( JSON.parse(cli(['notes-bundle', PRODUCT, '0.4.0.1']).stdout), { en_US: notes }, ) }) test('legacy audience metadata retains tagged notes but cannot select pending highlights', t => { const { root, fragment, write, cli, commit, tag, git } = compilerFixture(t) const legacy = '---\nuser-facing: true\n---\n\n' fragment('minor-added-legacy.md', legacy + '- Legacy feature.\n', null) fragment('patch-security-legacy.md', legacy + '- Legacy security.\n', null) fragment( 'patch-fixed-legacy.md', '---\nuser-facing: false\n---\n\n- Legacy routine change.\n', null, ) write( `${PRODUCT}/changelog/minor-added-legacy.pl_PL.md`, '- Dawna funkcja.\n', ) commit() const release = tag('0.4.0.1') const args = ['notes', PRODUCT, '0.4.0.1', '--ref', release] const expected = cli(args).stdout assert.equal( expected.toString(), '## Highlights\n\n- Legacy feature.\n\n## Security\n\n- Legacy security.\n', ) contains( cli(['validate', PRODUCT], false).stderr, 'release-notes: true or false', ) git('rm', '-r', `${PRODUCT}/changelog`) fragment('patch-fixed-pending.md', '- Routine pending change.\n', false) commit() cli(['sync']) assert.deepEqual( readFileSync(join(root, PRODUCT, 'release-notes/0.4.0.1.md')), expected, ) contains( readFileSync(join(root, PRODUCT, 'release-notes/0.4.0.1.pl_PL.md')), 'Dawna funkcja.', ) commit() cli(['sync']) assert.deepEqual(cli(args).stdout, expected) excludes(cli(['notes', PRODUCT, '0.4.0.2']).stdout, 'Legacy feature.') }) test('highlights share fragment bodies and preserve release framing', t => { const { fragment, cli, render, write } = compilerFixture(t) fragment( 'patch-fixed-z.md', '- Visible fix.\n With details.\n\n- Another fix.\n', ) fragment('minor-added-a.md', '- Visible feature.\n') fragment('patch-changed-internal.md', '- Internal change.\n', false) write( `${PRODUCT}/release-notes/0.4.0.1.md`, 'Lede.\n\n## Important\n\nKeep this notice.\n', ) const notes = cli(['notes', PRODUCT, '0.4.0.1']).stdout assert.equal( notes.toString(), 'Lede.\n\n## Highlights\n\n- Visible feature.\n\n- Visible fix.\n With details.\n\n- Another fix.\n\n## Important\n\nKeep this notice.\n', ) contains(render(), 'Internal change.') excludes(notes, 'Internal change.') excludes(notes, 'release-notes:') excludes(render(), 'release-notes:') }) test('notes compile without framing or an origin connection', t => { const { fragment, cli, git } = compilerFixture(t) git('remote', 'set-url', 'origin', '/unavailable-origin') assert.equal( cli(['notes', PRODUCT, '0.4.0.1']).stdout.toString(), '## Highlights\n', ) fragment(undefined, '- Highlight.\n') assert.equal( cli(['notes', PRODUCT, '0.4.0.1']).stdout.toString(), '## Highlights\n\n- Highlight.\n', ) fragment(undefined, '- Internal.\n', false) assert.equal( cli(['notes', PRODUCT, '0.4.0.1']).stdout.toString(), '## Highlights\n', ) }) test('sync archives highlights before consuming tagged fragments', t => { const { root, fragment, write, cli, commit, tag, git } = compilerFixture(t) const path = `${PRODUCT}/release-notes/0.4.0.1.md` const framing = 'Lede.\n\n## Important\n\nImportant notice.\n' write(path, framing) const consumed = fragment(undefined, '- Tagged highlight.\n') fragment('patch-changed-internal.md', '- Internal.\n', false) commit() const release = tag('0.4.0.1', true) const expected = cli(['notes', PRODUCT, '0.4.0.1', '--ref', release]).stdout fragment('minor-added-new.md', '- Future highlight.\n') commit() cli(['sync']) assert.equal(existsSync(consumed), false) assert.deepEqual(readFileSync(join(root, path)), expected) excludes(expected, 'Future highlight.') excludes(expected, 'Internal.') cli(['sync']) assert.deepEqual(readFileSync(join(root, path)), expected) commit() assert.deepEqual( cli(['notes', PRODUCT, '0.4.0.1', '--ref', release]).stdout, expected, ) assert.equal(git('show', `${release}:${path}`), framing) const upcoming = cli(['notes', PRODUCT, '0.4.0.2']).stdout contains(upcoming, 'Future highlight.') excludes(upcoming, 'Tagged highlight.') }) test('notes ownership excludes lingering ancestor fragments and preserves legacy releases', t => { const { root, fragment, write, cli, commit, tag } = compilerFixture(t) const legacy = 'Legacy lede.\r\n\r\n## Highlights\r\n\r\n- Curated legacy highlight.\r\n' const legacyPath = `${PRODUCT}/release-notes/0.4.0.md` write(legacyPath, legacy) fragment(undefined, '- Legacy changelog.\n', null) commit() const old = tag('0.4.0') fragment('patch-added-first.md', '- First highlight.\n') commit() const first = tag('0.4.0.1', true) fragment('patch-fixed-second.md', '- Second highlight.\n') commit() const second = tag('0.4.0.2') const notes = cli(['notes', PRODUCT, '0.4.0.2', '--ref', second]).stdout contains(notes, 'Second highlight.') excludes(notes, 'First highlight.') excludes(notes, 'Legacy changelog.') cli(['sync']) assert.equal(readFileSync(join(root, legacyPath), 'utf8'), legacy) assert.equal( cli(['notes', PRODUCT, '0.4.0', '--ref', old]).stdout.toString(), legacy, ) assert.equal( cli(['notes', PRODUCT, '0.4.0.1', '--ref', first]).stdout.toString(), '## Highlights\n\n- First highlight.\n', ) assert.equal( readFileSync(join(root, `${PRODUCT}/release-notes/0.4.0.2.md`), 'utf8'), '## Highlights\n\n- Second highlight.\n', ) assert.equal( cli(['notes', PRODUCT, '0.4.0.2']).stdout.toString(), '## Highlights\n\n- Second highlight.\n', ) }) test('selection-only tag changes cannot rewrite archived highlights', t => { const { root, fragment, cli, commit, tag, git } = compilerFixture(t) fragment(undefined, '- Released highlight.\n') commit() tag('0.4.0.1') cli(['sync']) const path = join(root, PRODUCT, 'release-notes/0.4.0.1.md') const archived = readFileSync(path) commit() fragment(undefined, '- Released highlight.\n', false) commit() git('tag', '-f', 'start-os/v0.4.0.1') contains(cli(['sync'], false).stderr, 'remove ## Highlights') assert.deepEqual(readFileSync(path), archived) }) test('security fragments form their own section after Highlights and before notices', t => { const { fragment, write, cli, render } = compilerFixture(t) fragment('minor-added-feature.md', '- Visible feature.\n') fragment('patch-security-a.md', '- Security fix.\n') fragment('patch-security-b.md', '- Security fallback.\n') fragment('patch-security-internal.md', '- Internal security detail.\n', false) write( `${PRODUCT}/changelog/patch-security-a.pl_PL.md`, '- Poprawka bezpieczeństwa.\n', ) write( `${PRODUCT}/release-notes/0.4.0.1.md`, '## Important\n\nKeep this notice.\n', ) const english = cli(['notes', PRODUCT, '0.4.0.1']).stdout.toString() assert.equal( english, '## Highlights\n\n- Visible feature.\n\n## Security\n\n- Security fix.\n\n- Security fallback.\n\n## Important\n\nKeep this notice.\n', ) const polish = cli(['notes', PRODUCT, '0.4.0.1', '--locale', 'pl_PL']).stdout contains(polish, '## Nowości\n\n- Visible feature.') contains(polish, '## Bezpieczeństwo\n\n- Poprawka bezpieczeństwa.') contains(polish, '- Security fallback.') contains(polish, '## Important\n\nKeep this notice.') excludes(polish, 'Internal security detail.') const history = render() contains(history, '### Security') contains(history, 'Internal security detail.') }) test('security-only releases need no framing and use catalog headings in every locale', t => { const { root, fragment, cli } = compilerFixture(t) fragment('patch-security-fix.md', '- Security fix.\n') for (const [locale, highlights, security] of [ ['en_US', 'Highlights', 'Security'], ['de_DE', 'Neuerungen', 'Sicherheit'], ['es_ES', 'Novedades', 'Seguridad'], ['fr_FR', 'Nouveautés', 'Sécurité'], ['pl_PL', 'Nowości', 'Bezpieczeństwo'], ]) { assert.equal( cli(['notes', PRODUCT, '0.4.0.1', '--locale', locale]).stdout.toString(), `## ${highlights}\n\n## ${security}\n\n- Security fix.\n`, ) } assert.ok(!existsSync(join(root, PRODUCT, 'release-notes'))) fragment('patch-security-fix.md', '- Internal.\n', false) assert.equal( cli(['notes', PRODUCT, '0.4.0.1']).stdout.toString(), '## Highlights\n', ) }) test('security archives preserve translated sections and pinned titles without framing', t => { const { root, fragment, write, cli, commit, tag, git } = compilerFixture(t) fragment('patch-security-fix.md', '- Security fix.\n') write(`${PRODUCT}/changelog/patch-security-fix.pl_PL.md`, '- Bezpieczniej.\n') const catalog = 'shared-libs/crates/start-core/locales/release-notes.json' const labels = JSON.parse(readFileSync(join(REPO, catalog), 'utf8')) labels['release-notes.security-title'].pl_PL = 'Zapisane bezpieczeństwo' write(catalog, JSON.stringify(labels)) commit() const release = tag('0.4.0.1') const args = [ 'notes', PRODUCT, '0.4.0.1', '--locale', 'pl_PL', '--ref', release, ] const expected = cli(args).stdout contains(expected, '## Zapisane bezpieczeństwo') cli(['sync']) assert.deepEqual( readFileSync(join(root, PRODUCT, 'release-notes/0.4.0.1.pl_PL.md')), expected, ) assert.ok(!existsSync(join(root, PRODUCT, 'changelog/patch-security-fix.md'))) commit() cli(['sync']) assert.deepEqual(cli(args).stdout, expected) assert.ok( cli(['archives', PRODUCT]).stdout.toString().includes('0.4.0.1.pl_PL.md'), ) assert.equal( git( 'ls-tree', '-r', '--name-only', release, '--', `${PRODUCT}/release-notes`, ), '', ) }) test('authored Security sections are rejected before replacing generated notes', t => { const { fragment, write, cli } = compilerFixture(t) fragment('patch-security-fix.md', '- Security fix.\n') for (const [locale, heading] of [ ['en_US', 'Security'], ['pl_PL', 'Bezpieczeństwo'], ]) { const suffix = locale === 'en_US' ? '' : `.${locale}` write( `${PRODUCT}/release-notes/0.4.0.1${suffix}.md`, `## ${heading}\n\n- Hand-written.\n`, ) contains( cli(['notes', PRODUCT, '0.4.0.1', '--locale', locale], false).stderr, `remove ## ${heading}`, ) } }) test('translated Highlights inherit selection, ordering, and English fallback', t => { const { fragment, write, cli, render } = compilerFixture(t) fragment('minor-added-a.md', '- English feature.\n') write(`${PRODUCT}/changelog/minor-added-a.pl_PL.md`, '- Polska funkcja.\n') fragment('patch-fixed-b.md', '- English fallback.\n') fragment('patch-changed-internal.md', '- Internal.\n', false) write( `${PRODUCT}/changelog/patch-changed-internal.pl_PL.md`, '- Wewnętrzne.\n', ) write( `${PRODUCT}/release-notes/0.4.0.1.pl_PL.md`, 'Wstęp.\n\n## Nowości\n\n\n\n## Ważne\n\nInstrukcje.\n', ) const notes = cli(['notes', PRODUCT, '0.4.0.1', '--locale', 'pl_PL']).stdout assert.equal( notes.toString(), 'Wstęp.\n\n## Nowości\n\n- Polska funkcja.\n\n- English fallback.\n\n## Ważne\n\nInstrukcje.\n', ) excludes(notes, 'Internal.') excludes(notes, 'Wewnętrzne.') excludes(notes, '') const history = render('0.4.1') contains(history, 'English feature.') contains(history, 'Internal.') excludes(history, 'Polska funkcja.') const bundle = JSON.parse(cli(['notes-bundle', PRODUCT, '0.4.0.1']).stdout) assert.deepEqual(Object.keys(bundle), ['en_US', 'pl_PL']) assert.equal(bundle.pl_PL, notes.toString()) contains(bundle.en_US, 'English feature.') assert.equal( cli(['locales', PRODUCT, '0.4.0.1']).stdout.toString(), 'pl_PL\n', ) cli(['notes', PRODUCT, '0.4.0.1', '--locale', '../pl_PL'], false) cli(['notes', PRODUCT, '0.4.0.1', '--locale', 'pl_PL\n'], false) }) test('translation companions require canonical bodies and valid bullet Markdown', t => { const { fragment, write, cli } = compilerFixture(t) const path = write( `${PRODUCT}/changelog/patch-fixed-example.pl_PL.md`, '- Polska poprawka.\n', ) contains( cli(['validate', PRODUCT], false).stderr, 'missing canonical fragment', ) fragment() cli(['validate', PRODUCT]) const malformedName = write( `${PRODUCT}/changelog/patch-fixed-example.pl_PL.md\n`, '- Poprawka.\n', ) cli(['validate', PRODUCT], false) unlinkSync(malformedName) writeFileSync(path, '## Nowości\n') contains(cli(['validate', PRODUCT], false).stderr, 'headings are not allowed') writeFileSync(path, '---\nrelease-notes: false\n---\n\n- Wewnętrzne.\n') cli(['validate', PRODUCT], false) unlinkSync(path) write( `${PRODUCT}/changelog/patch-fixed-example.en_US.md`, '- English duplicate.\n', ) contains( cli(['validate', PRODUCT], false).stderr, 'English belongs in the canonical fragment', ) }) test('locale discovery includes framing and visible companions without a formatter', t => { const { root, fragment, write, cli } = compilerFixture(t) fragment() write(`${PRODUCT}/changelog/patch-fixed-example.pl_PL.md`, '- Poprawka.\n') fragment('patch-changed-internal.md', '- Internal.\n', false) write(`${PRODUCT}/changelog/patch-changed-internal.es_ES.md`, '- Interno.\n') write( `${PRODUCT}/release-notes/0.4.0.1.de_DE.md`, '## Neuigkeiten\n\n\n', ) write(`${PRODUCT}/release-notes/0.4.0.2.fr_FR.md`, 'Future.\n') write(`${PRODUCT}/release-notes/0.4.0.1.pre-update.md`, 'Warning.\n') unlinkSync(join(root, '.prettierrc.json')) assert.equal( cli(['locales', PRODUCT, '0.4.0.1']).stdout.toString(), 'de_DE pl_PL\n', ) }) test('localized framing reserves an empty generated section', t => { const { fragment, write, cli } = compilerFixture(t) fragment() const path = `${PRODUCT}/release-notes/0.4.0.1.pl_PL.md` for (const text of [ '## Nowości\n\n- Ręcznie.\n', '## Nowości\n\n- Ręcznie.\n\n\n', '## Nowości\n\n\n\n- Ręcznie.\n', '\n', '## Nowości\n\n\n\n\n', ]) { write(path, text) cli(['notes', PRODUCT, '0.4.0.1', '--locale', 'pl_PL'], false) cli(['notes-bundle', PRODUCT, '0.4.0.1'], false) } fragment(undefined, '- Internal.\n', false) write(path, 'Wstęp.\n\n## Nowości\n\n\n') assert.equal( cli(['notes', PRODUCT, '0.4.0.1', '--locale', 'pl_PL']).stdout.toString(), 'Wstęp.\n\n## Nowości\n', ) }) test('sync archives translations from the tag and consumes unchanged bundles', t => { const { root, fragment, write, cli, commit, tag, git } = compilerFixture(t) const canonical = fragment() const translation = write( `${PRODUCT}/changelog/patch-fixed-example.pl_PL.md`, '- Pierwsza poprawka.\n', ) const framing = `${PRODUCT}/release-notes/0.4.0.1.pl_PL.md` write(framing, 'Wstęp.\n\n## Nowości\n\n\n') commit() const release = tag('0.4.0.1', true) const expected = cli([ 'notes', PRODUCT, '0.4.0.1', '--locale', 'pl_PL', '--ref', release, ]).stdout writeFileSync(translation, '- Zmieniona poprawka.\n') const added = write( `${PRODUCT}/changelog/patch-fixed-example.de_DE.md`, '- Erste Korrektur.\n', ) const unchanged = fragment('patch-fixed-other.md', '- Another English fix.\n') const unchangedTranslation = write( `${PRODUCT}/changelog/patch-fixed-other.pl_PL.md`, '- Inna poprawka.\n', ) commit() tag('0.4.0.2') cli(['sync']) assert.deepEqual(readFileSync(join(root, framing)), expected) assert.ok(!existsSync(canonical)) assert.ok(!existsSync(translation)) assert.ok(!existsSync(added)) assert.ok(!existsSync(unchanged)) assert.ok(!existsSync(unchangedTranslation)) assert.equal( readFileSync( join(root, `${PRODUCT}/release-notes/0.4.0.2.pl_PL.md`), 'utf8', ), '## Nowości\n\n- Zmieniona poprawka.\n\n- Inna poprawka.\n', ) assert.equal( readFileSync( join(root, `${PRODUCT}/release-notes/0.4.0.2.de_DE.md`), 'utf8', ), '## Neuerungen\n\n- Erste Korrektur.\n\n- Another English fix.\n', ) commit() cli(['sync']) assert.deepEqual( cli(['notes', PRODUCT, '0.4.0.1', '--locale', 'pl_PL', '--ref', release]) .stdout, expected, ) assert.deepEqual(readFileSync(join(root, framing)), expected) write(`${PRODUCT}/release-notes/0.4.0.3.fr_FR.md`, 'Unreleased.\n') commit() const archives = cli(['archives', PRODUCT]) .stdout.toString() .trim() .split('\n') assert.ok(archives.includes(`${PRODUCT}/release-notes/0.4.0.1.pl_PL.md`)) assert.ok(archives.includes(`${PRODUCT}/release-notes/0.4.0.2.de_DE.md`)) assert.ok(!archives.includes(`${PRODUCT}/release-notes/0.4.0.3.fr_FR.md`)) assert.equal( git('show', `${release}:${framing}`), 'Wstęp.\n\n## Nowości\n\n\n', ) }) test('post-tag translation edits and additions retain the canonical fragment', t => { const { fragment, write, cli, commit, tag } = compilerFixture(t) const canonical = fragment() const translation = write( `${PRODUCT}/changelog/patch-fixed-example.pl_PL.md`, '- Poprawka.\n', ) commit() tag('0.4.0.1') writeFileSync(translation, '- Zmieniona.\n') const fresh = write( `${PRODUCT}/changelog/patch-fixed-example.de_DE.md`, '- Korrektur.\n', ) commit() cli(['sync']) assert.ok(existsSync(canonical)) assert.ok(existsSync(translation)) assert.ok(existsSync(fresh)) cli(['validate', PRODUCT]) cli(['check-version', PRODUCT, '0.4.0.2']) cli(['sync']) assert.ok(existsSync(canonical)) }) test('deleted and reintroduced translations create new fragment ownership', t => { const { fragment, write, commit, tag, cli, render } = compilerFixture(t) fragment() const translation = write( `${PRODUCT}/changelog/patch-fixed-example.pl_PL.md`, '- Poprawka.\n', ) commit() tag('0.4.0.1') unlinkSync(translation) commit() writeFileSync(translation, '- Poprawka.\n') commit() const release = tag('0.4.0.2') assert.equal(count(render('0.4.0.2', release), '- Fixed example.'), 2) cli(['sync']) assert.ok(!existsSync(translation)) }) test('legacy translated note bytes are preserved', t => { const { root, fragment, write, commit, tag, cli } = compilerFixture(t) const path = `${PRODUCT}/release-notes/0.4.0.1.pl_PL.md` const legacy = 'Wstęp.\r\n\r\n## Nowości\r\n\r\n- Opublikowane.\r\n' write(path, legacy) fragment(undefined, '- Legacy change.\n', null) commit() const release = tag('0.4.0.1') assert.equal( cli([ 'notes', PRODUCT, '0.4.0.1', '--locale', 'pl_PL', '--ref', release, ]).stdout.toString(), legacy, ) cli(['sync']) assert.equal(readFileSync(join(root, path), 'utf8'), legacy) }) test('notes reject authored highlights before release', t => { const { fragment, write, cli } = compilerFixture(t) fragment(undefined, '- Compiled.\n') for (const framing of [ 'Lede.\n\n## Highlights\n\n- Hand-written.\n', 'Lede.\n\n## Highlights\n\n- Hand-written.\n\n## Important\n\n\n', ]) { write(`${PRODUCT}/release-notes/0.4.0.1.md`, framing) contains( cli(['notes', PRODUCT, '0.4.0.1'], false).stderr, 'release-notes/0.4.0.1.md: remove ## Highlights', ) } }) test('notes and fragment consumption are atomic on framing conflicts', t => { const { fragment, write, cli, commit, tag } = compilerFixture(t) const path = `${PRODUCT}/release-notes/0.4.0.1.md` write(path, 'Tagged lede.\n') const pending = fragment() commit() tag('0.4.0.1') write(path, 'Edited after tag.\n') contains(cli(['sync'], false).stderr, 'release notes differ') assert.ok(existsSync(pending)) }) test('consumption does not follow replacement symlink', t => { const { fragment, commit, tag, write, cli } = compilerFixture(t) const path = fragment() commit() tag('0.4.0.1') const outside = write('outside.md', readFileSync(path)) unlinkSync(path) symlinkSync(outside, path) cli(['sync']) assert.ok(lstatSync(path).isSymbolicLink()) assert.ok(existsSync(outside)) }) test('security metadata follows fragment ownership and includes non-highlighted changes', t => { const { fragment, commit, tag, cli } = compilerFixture(t) const security = (version, ref) => cli(['security', PRODUCT, version, ...(ref ? ['--ref', ref] : [])]) .stdout.toString() .trim() assert.equal(security('0.4.0'), 'false') fragment('patch-security-private.md', '- Security correction.\n', false) commit() tag('0.4.0') assert.equal(security('0.4.0', 'start-os/v0.4.0'), 'true') assert.equal(security('0.4.0.1'), 'false') fragment( 'patch-security-private.md', '- Another security correction.\n', false, ) const updated = commit() assert.equal(security('0.4.0.1'), 'true') assert.equal(security('0.4.0.1', updated), 'true') assert.equal(security('0.4.0.1', 'start-os/v0.4.0'), 'false') }) test('StartOS RC tags leave fragments for subsequent candidates and stable release', t => { const { root, fragment, commit, tag, cli, render } = compilerFixture(t) fragment('patch-security-example.md', '- Security correction.\n') const ref = commit() tag('0.4.0-rc.1') contains(render('0.4.0', ref), '- Security correction.') contains( cli(['notes', PRODUCT, '0.4.0', '--ref', ref]).stdout, '- Security correction.', ) assert.equal( cli(['security', PRODUCT, '0.4.0', '--ref', ref]).stdout.toString(), 'true\n', ) cli(['sync']) assert.ok( existsSync(join(root, PRODUCT, 'changelog/patch-security-example.md')), ) tag('0.4.0') cli(['sync']) assert.equal( existsSync(join(root, PRODUCT, 'changelog/patch-security-example.md')), false, ) })