'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const test = require('node:test'); const semver = require('semver'); const { CONTAINER_ROOTS, DEPLOYMENT_FILES, DEPLOYMENT_ROOTS, applyVersionTransitionPlan, auditReleaseVersionContract, createVersionTransitionPlan, parseArguments, runCli, validatePlan, } = require('../../scripts/ql3-version-transition.cjs'); const { readReleaseIdentity, } = require('../../scripts/lib/ql3-release-identity.cjs'); const root = path.resolve(__dirname, '../..'); const SOURCE_VERSION = readReleaseIdentity(root).version; const TARGET_VERSION = semver.inc(SOURCE_VERSION, 'prerelease'); const LEGACY_VERSION = JSON.parse( fs.readFileSync(path.join(root, 'package.json')), ).version; function copyFile(sourceRoot, targetRoot, relativePath) { const source = path.join(sourceRoot, relativePath); const target = path.join(targetRoot, relativePath); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.copyFileSync(source, target); fs.chmodSync(target, fs.statSync(source).mode & 0o777); } function createFixture(t) { const fixture = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-version-')), ); t.after(() => fs.rmSync(fixture, { recursive: true, force: true })); copyFile(root, fixture, 'package.json'); copyFile(root, fixture, 'ql3-release.json'); for (const entry of fs.readdirSync(path.join(root, 'packages'), { withFileTypes: true, })) { if (entry.isDirectory() && entry.name.startsWith('ql3-')) { copyFile(root, fixture, `packages/${entry.name}/package.json`); } } for (const containerRoot of CONTAINER_ROOTS) { for (const relativePath of [ 'Dockerfile', 'package.json', 'package-lock.json', 'runtime-dependencies/package.json', 'runtime-dependencies/package-lock.json', ]) { copyFile(root, fixture, `${containerRoot}/${relativePath}`); } } for (const deploymentRoot of DEPLOYMENT_ROOTS) { fs.cpSync( path.join(root, deploymentRoot), path.join(fixture, deploymentRoot), { recursive: true, dereference: false }, ); } for (const relativePath of DEPLOYMENT_FILES) { copyFile(root, fixture, relativePath); } return fixture; } function replaceVersion( filePath, source = SOURCE_VERSION, target = TARGET_VERSION, ) { const contents = fs.readFileSync(filePath, 'utf8'); assert.equal(contents.includes(source), true); fs.writeFileSync(filePath, contents.split(source).join(target)); } test('audits one source-derived QingLong 3 release identity', () => { assert.deepEqual(auditReleaseVersionContract(root), { schemaVersion: 1, schema: 'qinglong/release-identity@v1', version: SOURCE_VERSION, nodeVersion: '24.18.0', nodeEngine: '>=24.18.0 <25', legacyRootPackageVersion: LEGACY_VERSION, legacyRootExcluded: true, workspacePackageCount: 18, containerRootCount: 4, deploymentFileCount: 242, deploymentImageReferences: 32, deploymentVersionOccurrences: 36, compatible: true, }); }); test('plans the exact governed version surface without touching legacy 2.x', () => { const plan = createVersionTransitionPlan({ root, sourceVersion: SOURCE_VERSION, targetVersion: TARGET_VERSION, }); assert.equal(plan.fileCount, 65); assert.equal(plan.replacementCount, 83); assert.equal(plan.legacyRootPackageVersion, LEGACY_VERSION); assert.equal(plan.legacyRootExcluded, true); assert.equal( plan.entries.some((entry) => entry.path === 'package.json'), false, ); assert.equal( plan.entries.filter((entry) => entry.path.startsWith('packages/')).length, 18, ); assert.match(plan.planDigest, /^sha256:[a-f0-9]{64}$/u); assert.equal(validatePlan(plan), plan); }); test('applies and exactly replays a complete version transition', (t) => { const fixture = createFixture(t); const plan = createVersionTransitionPlan({ root: fixture, sourceVersion: SOURCE_VERSION, targetVersion: TARGET_VERSION, }); const report = applyVersionTransitionPlan(plan, { root: fixture, report: path.join(fixture, 'first-report.json'), }); assert.equal(report.changedFiles, plan.fileCount); assert.equal(report.alreadyCurrentFiles, 0); assert.equal(report.exactReplay, false); assert.equal(auditReleaseVersionContract(fixture).version, TARGET_VERSION); assert.equal( JSON.parse(fs.readFileSync(path.join(fixture, 'package.json'))).version, LEGACY_VERSION, ); const replay = applyVersionTransitionPlan(plan, { root: fixture, report: path.join(fixture, 'replay-report.json'), }); assert.equal(replay.changedFiles, 0); assert.equal(replay.alreadyCurrentFiles, plan.fileCount); assert.equal(replay.exactReplay, true); }); test('resumes a partial transition using before and after digests', (t) => { const fixture = createFixture(t); const plan = createVersionTransitionPlan({ root: fixture, sourceVersion: SOURCE_VERSION, targetVersion: TARGET_VERSION, }); replaceVersion(path.join(fixture, 'ql3-release.json')); const report = applyVersionTransitionPlan(plan, { root: fixture, report: path.join(fixture, 'recovered-report.json'), }); assert.equal(report.changedFiles, plan.fileCount - 1); assert.equal(report.alreadyCurrentFiles, 1); assert.equal(auditReleaseVersionContract(fixture).version, TARGET_VERSION); }); test('preflights every governed file before making a partial mutation', (t) => { const fixture = createFixture(t); const plan = createVersionTransitionPlan({ root: fixture, sourceVersion: SOURCE_VERSION, targetVersion: TARGET_VERSION, }); const drifted = path.join(fixture, plan.entries.at(-1).path); fs.appendFileSync(drifted, '\n'); assert.throws( () => applyVersionTransitionPlan(plan, { root: fixture, report: path.join(fixture, 'must-not-exist.json'), }), /drifted after plan creation/, ); assert.equal( JSON.parse(fs.readFileSync(path.join(fixture, 'ql3-release.json'))).version, SOURCE_VERSION, ); assert.equal(fs.existsSync(path.join(fixture, 'must-not-exist.json')), false); }); test('rejects invalid SemVer, downgrade, plan mutation and a symbolic-link identity', (t) => { assert.throws( () => createVersionTransitionPlan({ root, sourceVersion: SOURCE_VERSION, targetVersion: '3.0.0-alpha', }), /monotonically newer/, ); const plan = createVersionTransitionPlan({ root, sourceVersion: SOURCE_VERSION, targetVersion: TARGET_VERSION, }); const mutatedPlan = JSON.parse(JSON.stringify(plan)); mutatedPlan.entries[0].path = 'package.json'; assert.throws(() => validatePlan(mutatedPlan), /plan entry is invalid/); const fixture = createFixture(t); const invalidIdentityPath = path.join(fixture, 'ql3-release.json'); const invalidIdentity = JSON.parse(fs.readFileSync(invalidIdentityPath)); invalidIdentity.version = '3.0.0-alpha.01'; fs.writeFileSync( invalidIdentityPath, `${JSON.stringify(invalidIdentity, null, 2)}\n`, ); assert.throws( () => auditReleaseVersionContract(fixture), /identity shape or value is incompatible/, ); copyFile(root, fixture, 'ql3-release.json'); fs.renameSync( path.join(fixture, 'ql3-release.json'), path.join(fixture, 'identity-target.json'), ); fs.symlinkSync( 'identity-target.json', path.join(fixture, 'ql3-release.json'), ); assert.throws( () => auditReleaseVersionContract(fixture), /canonical regular file/, ); }); test('CLI writes no-replace plans and accepts only closed modes', (t) => { const directory = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-version-cli-')), ); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); const planPath = path.join(directory, 'plan.json'); const output = { write() {} }; runCli( [ '--mode=plan', `--from=${SOURCE_VERSION}`, `--to=${TARGET_VERSION}`, `--output=${planPath}`, ], root, output, ); assert.equal(fs.statSync(planPath).mode & 0o777, 0o600); assert.throws( () => runCli( [ '--mode=plan', `--from=${SOURCE_VERSION}`, `--to=${TARGET_VERSION}`, `--output=${planPath}`, ], root, output, ), /output must be unused/, ); assert.deepEqual(parseArguments(['--mode=audit']), { mode: 'audit' }); assert.throws( () => parseArguments(['--mode=audit', '--extra=true']), /arguments are invalid/, ); });