import assert from 'node:assert/strict'; import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import test from 'node:test'; import * as syncVersion from './sync-version.mjs'; import { collectVersionDrift, inspectBunLockWorkspaceVersions, renderBunLockWorkspaceVersions, renderPackageManifest, renderSdkEntry, } from './sync-version.mjs'; const fixtureManagedPaths = [ 'package.json', 'op-web-sdk/package.json', 'op-web-sdk-react/package.json', 'op-web-sdk-vue/package.json', 'op-web-sdk/src/index.ts', 'op-web-sdk-react/src/index.ts', 'op-web-sdk-vue/src/index.ts', 'bun.lock', ]; function fixtureLockfile(version) { return `{ "lockfileVersion": 1, "workspaces": { "": { "name": "@zseven-w/openpencil-packages" }, "op-web-sdk": { "version": "${version}" }, "op-web-sdk-react": { "version": "${version}" }, "op-web-sdk-vue": { "version": "${version}" }, }, } `; } async function createRepositoryFixture(t, version = '1.0.0') { const repositoryRoot = await mkdtemp(join(tmpdir(), 'sync-version-')); const packagesRoot = join(repositoryRoot, 'packages'); t.after(() => rm(repositoryRoot, { recursive: true, force: true })); const files = { 'package.json': `${JSON.stringify({ name: 'fixture-root', version }, null, 2)}\n`, 'op-web-sdk/package.json': `${JSON.stringify({ name: 'op-web-sdk', version }, null, 2)}\n`, 'op-web-sdk-react/package.json': `${JSON.stringify({ name: 'op-web-sdk-react', version }, null, 2)}\n`, 'op-web-sdk-vue/package.json': `${JSON.stringify({ name: 'op-web-sdk-vue', version }, null, 2)}\n`, 'op-web-sdk/src/index.ts': `export const VERSION = '${version}';\n`, 'op-web-sdk-react/src/index.ts': `export const VERSION = '${version}';\n`, 'op-web-sdk-vue/src/index.ts': `export const VERSION = '${version}';\n`, 'bun.lock': fixtureLockfile(version), }; await Promise.all( Object.entries(files).map(async ([path, contents]) => { const absolutePath = join(packagesRoot, path); await mkdir(dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, contents); }), ); return { repositoryRoot, packagesRoot, managedPaths: fixtureManagedPaths.map((path) => join(packagesRoot, path)), }; } async function readManagedFiles(paths) { return Promise.all(paths.map((path) => readFile(path))); } function createCommandRunner(packagesRoot, version = '2.3.4', { regenerateLock = true } = {}) { const calls = []; return { calls, runCommand: async (command, arguments_) => { calls.push([command, ...arguments_]); if (command === 'sh') { return `${version}\n`; } if (arguments_[0] === '--version') { return '1.3.11\n'; } if (arguments_[0] === 'install') { if (regenerateLock) { await writeFile(join(packagesRoot, 'bun.lock'), fixtureLockfile(version)); } return ''; } throw new Error(`Unexpected command: ${command} ${arguments_.join(' ')}`); }, }; } test('argument parsing selects write or check mode and rejects unknown arguments', () => { assert.equal(syncVersion.parseArguments([]), 'write'); assert.equal(syncVersion.parseArguments(['--check']), 'check'); assert.throws(() => syncVersion.parseArguments(['1.2.3']), /usage/i); assert.throws(() => syncVersion.parseArguments(['--check', '--extra']), /usage/i); }); test('package manifest rendering changes only the top-level version and ends with a newline', () => { const input = `{ "name": "example", "version": "1.0.0", "metadata": { "version": "unchanged" } }`; assert.equal( renderPackageManifest(input, '2.3.4'), `{ "name": "example", "version": "2.3.4", "metadata": { "version": "unchanged" } } `, ); }); test('SDK entry rendering replaces exactly one public VERSION export', () => { const input = `export { mount } from './mount'; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `export { mount } from './mount'; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering replaces only the initializer and preserves its quote style', () => { const input = `export const VERSION: string = "1.0.0"; // keep this formatting `; assert.equal( renderSdkEntry(input, '2.3.4'), `export const VERSION: string = "2.3.4"; // keep this formatting `, ); }); test('SDK entry rendering reports TypeScript parse errors with a source location', () => { assert.throws( () => renderSdkEntry("const broken = ;\nexport const VERSION = '1.0.0';\n", '2.3.4'), /unable to parse SDK entry: 1:\d+:/i, ); }); test('SDK entry rendering rejects a missing VERSION export', () => { assert.throws( () => renderSdkEntry(`export { mount } from './mount';\n`, '2.3.4'), /exactly one.*VERSION export/i, ); }); test('SDK entry rendering rejects multiple VERSION exports', () => { const input = `export const VERSION = '1.0.0'; export const VERSION = '1.0.1'; `; assert.throws(() => renderSdkEntry(input, '2.3.4'), /exactly one.*VERSION export/i); }); test('SDK entry rendering rejects non-exported, non-const, and nested VERSION declarations', () => { assert.throws( () => renderSdkEntry("const VERSION = '1.0.0';\n", '2.3.4'), /exactly one.*VERSION export/i, ); assert.throws( () => renderSdkEntry("export let VERSION = '1.0.0';\n", '2.3.4'), /exactly one.*VERSION export/i, ); assert.throws( () => renderSdkEntry( `namespace Internal { export const VERSION = '1.0.0'; } `, '2.3.4', ), /exactly one.*VERSION export/i, ); }); test('SDK entry rendering does not count declaration text inside a comment', () => { const input = `// export const VERSION = 'documentation-only'; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `// export const VERSION = 'documentation-only'; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering ignores declaration-shaped lines inside block comments', () => { const input = `/* export const VERSION = 'documentation-only'; */ export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `/* export const VERSION = 'documentation-only'; */ export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering ignores declaration-shaped lines inside template literals', () => { const input = `const documentation = \` export const VERSION = 'documentation-only'; \`; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `const documentation = \` export const VERSION = 'documentation-only'; \`; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering ignores declarations inside nested template literals', () => { const input = `const documentation = \` \${\`nested export const VERSION = 'documentation-only'; \`} \`; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `const documentation = \` \${\`nested export const VERSION = 'documentation-only'; \`} \`; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering does not treat comment markers inside quoted strings as comments', () => { const input = `const blockMarker = '/*'; const lineMarker = "//"; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `const blockMarker = '/*'; const lineMarker = "//"; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering ignores escaped block-comment markers in regex literals', () => { const input = String.raw`const pattern = /[\/*]/; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), String.raw`const pattern = /[\/*]/; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering ignores comment markers inside regex character classes', () => { const input = `const pattern = /[/*]/; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `const pattern = /[/*]/; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering ignores escaped line-comment markers in regex literals', () => { const input = String.raw`const pattern = /\//; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), String.raw`const pattern = /\//; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering distinguishes division expressions from comments and regex literals', () => { const input = `const ratio = left / right; /* export const VERSION = 'block-comment-only'; */ const half = total / 2; // export const VERSION = 'line-comment-only'; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `const ratio = left / right; /* export const VERSION = 'block-comment-only'; */ const half = total / 2; // export const VERSION = 'line-comment-only'; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering ignores regex literals after export default', () => { const input = `export default /[/*]/; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `export default /[/*]/; export const VERSION = '2.3.4'; `, ); }); test('SDK entry rendering ignores postfix division inside template interpolation', () => { const input = `const documentation = \` \${count++ / total} export const VERSION = 'documentation-only'; \`; export const VERSION = '1.0.0'; `; assert.equal( renderSdkEntry(input, '2.3.4'), `const documentation = \` \${count++ / total} export const VERSION = 'documentation-only'; \`; export const VERSION = '2.3.4'; `, ); }); test('Bun lock inspection finds every versioned SDK workspace and ignores the root workspace', () => { const lockfile = `{ "workspaces": { "": { "name": "@zseven-w/openpencil-packages", }, "op-web-sdk": { "name": "@zseven-w/op-web-sdk", "version": "1.0.0", "devDependencies": { "fixture": "1.2.3" }, }, "op-web-sdk-react": { "name": "@zseven-w/op-web-sdk-react", "version": "1.0.1", }, "op-web-sdk-vue": { "name": "@zseven-w/op-web-sdk-vue", "version": "1.0.2", }, }, }`; assert.deepEqual(inspectBunLockWorkspaceVersions(lockfile), { 'op-web-sdk': '1.0.0', 'op-web-sdk-react': '1.0.1', 'op-web-sdk-vue': '1.0.2', }); }); test('Bun lock inspection ignores structural delimiters in block comments after primitive values', () => { const lockfile = `{ "lockfileVersion": 1 /* comma, brace }, bracket ] */, "workspaces": { "op-web-sdk": { "version": "1.0.0" }, }, }`; assert.deepEqual(inspectBunLockWorkspaceVersions(lockfile), { 'op-web-sdk': '1.0.0', }); }); test('Bun lock inspection ignores structural delimiters in line comments after primitive values', () => { const lockfile = `{ "lockfileVersion": 1 // comma, brace }, bracket ] , "workspaces": { "op-web-sdk": { "version": "1.0.0" }, }, }`; assert.deepEqual(inspectBunLockWorkspaceVersions(lockfile), { 'op-web-sdk': '1.0.0', }); }); test('Bun lock inspection still rejects unterminated block comments after primitive values', () => { const lockfile = `{ "lockfileVersion": 1 /* unterminated }, ], "workspaces": {}, }`; assert.throws(() => inspectBunLockWorkspaceVersions(lockfile), /unterminated block comment/i); }); test('Bun lock rendering updates only managed workspace version literals', () => { const lockfile = `{ "workspaces": { "": { "name": "@zseven-w/openpencil-packages" }, "op-web-sdk": { "version": "1.0.0", "fixture": "1.0.0" }, "op-web-sdk-react": { "version": "1.0.1" }, "op-web-sdk-vue": { "version": "1.0.2" }, }, }`; const output = renderBunLockWorkspaceVersions(lockfile, '2.3.4'); assert.deepEqual(inspectBunLockWorkspaceVersions(output), { 'op-web-sdk': '2.3.4', 'op-web-sdk-react': '2.3.4', 'op-web-sdk-vue': '2.3.4', }); assert.match(output, /"fixture": "1\.0\.0"/); }); test('drift collection reports each stale path with expected and actual versions', () => { assert.deepEqual( collectVersionDrift('2.3.4', [ { path: 'packages/package.json', actualVersion: '2.3.3' }, { path: 'packages/op-web-sdk/package.json', actualVersion: '2.3.4' }, { path: 'packages/bun.lock#op-web-sdk', actualVersion: undefined }, ]), [ { path: 'packages/package.json', expectedVersion: '2.3.4', actualVersion: '2.3.3', }, { path: 'packages/bun.lock#op-web-sdk', expectedVersion: '2.3.4', actualVersion: undefined, }, ], ); }); test('an already synchronized in-memory repository reports no drift', () => { const version = '2.3.4'; const manifest = renderPackageManifest('{"name":"fixture","version":"1.0.0"}', version); const sdkEntry = renderSdkEntry("export const VERSION = '1.0.0';\n", version); const lockVersions = inspectBunLockWorkspaceVersions(`{ "workspaces": { "op-web-sdk": { "version": "${version}" }, "op-web-sdk-react": { "version": "${version}" }, "op-web-sdk-vue": { "version": "${version}" }, }, }`); assert.deepEqual( collectVersionDrift(version, [ { path: 'packages/package.json', actualVersion: JSON.parse(manifest).version }, { path: 'packages/op-web-sdk/src/index.ts', actualVersion: sdkEntry.match(/VERSION = '([^']+)'/)?.[1], }, ...Object.entries(lockVersions).map(([workspace, actualVersion]) => ({ path: `packages/bun.lock#${workspace}`, actualVersion, })), ]), [], ); }); test('check mode reports stale paths without changing managed files', async (t) => { const fixture = await createRepositoryFixture(t); const before = await readManagedFiles(fixture.managedPaths); const runner = createCommandRunner(fixture.packagesRoot); const result = await syncVersion.synchronizeVersions({ mode: 'check', ...fixture, runCommand: runner.runCommand, }); assert.equal(result.version, '2.3.4'); assert.deepEqual( result.drift.map(({ path }) => path), [ 'packages/package.json', 'packages/op-web-sdk/package.json', 'packages/op-web-sdk-react/package.json', 'packages/op-web-sdk-vue/package.json', 'packages/op-web-sdk/src/index.ts', 'packages/op-web-sdk-react/src/index.ts', 'packages/op-web-sdk-vue/src/index.ts', 'packages/bun.lock#workspaces.op-web-sdk', 'packages/bun.lock#workspaces.op-web-sdk-react', 'packages/bun.lock#workspaces.op-web-sdk-vue', ], ); assert.deepEqual(await readManagedFiles(fixture.managedPaths), before); assert.deepEqual(runner.calls, [['sh', 'scripts/workspace-version.sh', 'Cargo.toml']]); }); test('write mode synchronizes every consumer and is idempotent', async (t) => { const fixture = await createRepositoryFixture(t); const runner = createCommandRunner(fixture.packagesRoot); const first = await syncVersion.synchronizeVersions({ mode: 'write', ...fixture, runCommand: runner.runCommand, }); const afterFirst = await readManagedFiles(fixture.managedPaths); const second = await syncVersion.synchronizeVersions({ mode: 'write', ...fixture, runCommand: runner.runCommand, }); assert.deepEqual(first.drift, []); assert.deepEqual(second.drift, []); assert.deepEqual(await readManagedFiles(fixture.managedPaths), afterFirst); await Promise.all( fixture.managedPaths.slice(0, 4).map(async (path) => { assert.equal(JSON.parse(await readFile(path, 'utf8')).version, '2.3.4'); }), ); await Promise.all( fixture.managedPaths.slice(4, 7).map(async (path) => { assert.match(await readFile(path, 'utf8'), /VERSION = '2\.3\.4'/); }), ); assert.deepEqual( inspectBunLockWorkspaceVersions(await readFile(fixture.managedPaths[7], 'utf8')), { 'op-web-sdk': '2.3.4', 'op-web-sdk-react': '2.3.4', 'op-web-sdk-vue': '2.3.4', }, ); assert.equal(runner.calls.filter(([, argument]) => argument === 'install').length, 2); }); test('write mode performs a Bun preflight before making any changes', async (t) => { const fixture = await createRepositoryFixture(t); const before = await readManagedFiles(fixture.managedPaths); const calls = []; const runCommand = async (command, arguments_) => { calls.push([command, ...arguments_]); if (command === 'sh') { return '2.3.4\n'; } if (arguments_[0] === '--version') { throw new Error('bun unavailable'); } throw new Error('Bun install should not run after a failed preflight'); }; await assert.rejects( syncVersion.synchronizeVersions({ mode: 'write', ...fixture, runCommand, }), /bun.*preflight.*unavailable/i, ); assert.deepEqual(await readManagedFiles(fixture.managedPaths), before); assert.deepEqual(calls, [ ['sh', 'scripts/workspace-version.sh', 'Cargo.toml'], ['bun', '--version'], ]); }); test('write mode restores every managed file when Bun lock regeneration fails', async (t) => { const fixture = await createRepositoryFixture(t); const before = await readManagedFiles(fixture.managedPaths); const runCommand = async (command, arguments_) => { if (command === 'sh') { return '2.3.4\n'; } if (arguments_[0] === '--version') { return '1.3.11\n'; } await writeFile(join(fixture.packagesRoot, 'bun.lock'), 'partially regenerated\n'); throw new Error('install exploded'); }; await assert.rejects( syncVersion.synchronizeVersions({ mode: 'write', ...fixture, runCommand, }), /lockfile regeneration failed.*install exploded/i, ); assert.deepEqual(await readManagedFiles(fixture.managedPaths), before); }); test('write mode repairs workspace versions when Bun leaves them stale', async (t) => { const fixture = await createRepositoryFixture(t); const runner = createCommandRunner(fixture.packagesRoot, '2.3.4', { regenerateLock: false, }); const result = await syncVersion.synchronizeVersions({ mode: 'write', ...fixture, runCommand: runner.runCommand, }); assert.deepEqual(result.drift, []); assert.deepEqual( inspectBunLockWorkspaceVersions(await readFile(fixture.managedPaths[7], 'utf8')), { 'op-web-sdk': '2.3.4', 'op-web-sdk-react': '2.3.4', 'op-web-sdk-vue': '2.3.4', }, ); }); test('write mode waits for sibling writes to settle before rollback', async (t) => { const fixture = await createRepositoryFixture(t); const before = await readManagedFiles(fixture.managedPaths); const runner = createCommandRunner(fixture.packagesRoot); const attempts = new Map(); const writeManagedFile = async (path, contents) => { const attempt = attempts.get(path) ?? 0; attempts.set(path, attempt + 1); if (attempt === 0 && path === fixture.managedPaths[0]) { throw new Error('simulated write failure'); } if (attempt === 0 && path === fixture.managedPaths[1]) { await new Promise((resolvePromise) => setTimeout(resolvePromise, 30)); } await writeFile(path, contents); }; await assert.rejects( syncVersion.synchronizeVersions({ mode: 'write', ...fixture, runCommand: runner.runCommand, writeManagedFile, }), /simulated write failure/i, ); await new Promise((resolvePromise) => setTimeout(resolvePromise, 40)); assert.deepEqual(await readManagedFiles(fixture.managedPaths), before); });