mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-18 10:41:20 -08:00
test(release): [REL-04] checkpoint renamed rollback and mainline conflict recovery
This commit is contained in:
1 parent
aa29dc9212
commit
d0c33f6258
14 files changed
+1718
-8
No files matched your search
@@ -0,0 +1,106 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import ts from 'typescript'
|
||||
import { verifyIframeContract } from '../refactor/scripts/iframe-contract.mjs'
|
||||
import { hash, readMember } from '../refactor/scripts/releases.mjs'
|
||||
import { checkFiles, packedFiles } from './package-check.mjs'
|
||||
import { consumerDirectory, removeConsumer, run, workspace, writeJson } from './package-consumer.mjs'
|
||||
import { verifyRollbackFiles } from './rollback-files.mjs'
|
||||
|
||||
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use pinned Yarn')
|
||||
assert.equal(process.versions.node, fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim(), 'Use pinned Node')
|
||||
const yarn = process.env.npm_execpath
|
||||
assert(yarn && fs.existsSync(yarn))
|
||||
const name = 'artplayer-tool-iframe'
|
||||
const parent = path.join(workspace, 'refactor/.cache/iframe-rollback')
|
||||
fs.mkdirSync(parent, { recursive: true })
|
||||
const output = fs.mkdtempSync(path.join(parent, 'run-'))
|
||||
const directory = consumerDirectory()
|
||||
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspace, encoding: 'utf8', windowsHide: true }).trim()
|
||||
const report = { task: 'REL-04', capturedAt: new Date().toISOString(), source, node: process.versions.node, yarn: '1.22.22', passed: false, scope: 'Old package name and application imports restored together; independent renamed-package installation rehearsal', profiles: {}, steps: [] }
|
||||
function checkTypes(consumer, source) {
|
||||
const file = path.join(consumer, 'consumer.ts')
|
||||
fs.writeFileSync(file, source)
|
||||
const program = ts.createProgram([file], { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: true, target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS, moduleResolution: ts.ModuleResolutionKind.NodeJs, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'] })
|
||||
for (const input of program.getSourceFiles()) {
|
||||
const actual = fs.realpathSync(input.fileName)
|
||||
assert(actual.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(input) && path.dirname(actual) === fs.realpathSync(path.dirname(ts.sys.getExecutingFilePath()))), 'Type resolution escaped isolated consumer')
|
||||
}
|
||||
const diagnostics = ts.getPreEmitDiagnostics(program).map(item => ts.flattenDiagnosticMessageText(item.messageText, '\n'))
|
||||
assert.deepEqual(diagnostics, [])
|
||||
return { compiler: ts.version, mode: 'node10-commonjs', diagnostics }
|
||||
}
|
||||
try {
|
||||
const { baseline, archive } = await verifyIframeContract()
|
||||
const snapshot = path.join(output, 'build')
|
||||
fs.mkdirSync(snapshot)
|
||||
for (const file of ['package.json', 'yarn.lock', 'tsconfig.json', 'tsconfig.base.json']) fs.copyFileSync(path.join(workspace, file), path.join(snapshot, file))
|
||||
fs.cpSync(path.join(workspace, 'scripts'), path.join(snapshot, 'scripts'), { recursive: true })
|
||||
fs.symlinkSync(path.join(workspace, 'node_modules'), path.join(snapshot, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
|
||||
fs.cpSync(path.join(workspace, 'packages', name), path.join(snapshot, 'packages', name), { recursive: true, filter: file => !['dist', 'node_modules'].includes(path.basename(file)) })
|
||||
fs.writeFileSync(path.join(output, 'build.log'), run(['scripts/build.js', name], snapshot))
|
||||
const candidateArchive = path.join(output, 'candidate.tgz')
|
||||
fs.writeFileSync(path.join(output, 'pack.log'), run([yarn, 'pack', '--filename', candidateArchive], path.join(snapshot, 'packages', name)))
|
||||
const files = packedFiles(candidateArchive)
|
||||
const manifest = JSON.parse(readMember(candidateArchive, 'package/package.json'))
|
||||
checkFiles(manifest, files)
|
||||
assert.equal(manifest.name, name)
|
||||
assert(!['preinstall', 'install', 'postinstall'].some(hook => manifest.scripts?.[hook]), 'Installation hooks need a separate fixture')
|
||||
const profiles = {
|
||||
old: { ...baseline.release, archive },
|
||||
candidate: { name, version: manifest.version, archive: candidateArchive, sha256: hash(fs.readFileSync(candidateArchive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(candidateArchive, member))])) },
|
||||
}
|
||||
for (const [id, pkg] of Object.entries(profiles)) {
|
||||
const consumer = consumerDirectory()
|
||||
try {
|
||||
const application = id === 'old'
|
||||
? `const assert = require('node:assert/strict');\nconst Iframe = require('${pkg.name}').default;\nconst Helper = require('${pkg.name}/dist/artplayer-helper-iframe.js').default;\nassert.equal(typeof Iframe, 'function');\nassert.equal(typeof Helper.destroy, 'function');\nconsole.log('old import and helper restored');\n`
|
||||
: `const assert = require('node:assert/strict');\nconst Iframe = require('${pkg.name}');\nassert.equal(typeof Iframe, 'function');\nassert.equal(typeof require('${pkg.name}/legacy'), 'function');\nconsole.log('candidate import and legacy verified');\n`
|
||||
writeJson(path.join(consumer, 'package.json'), { name: 'iframe-rollback-consumer', private: true, dependencies: { [pkg.name]: `file:${pkg.archive.replaceAll('\\', '/')}` } })
|
||||
fs.writeFileSync(path.join(output, `${id}-prepare.log`), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer))
|
||||
verifyRollbackFiles(consumer, pkg)
|
||||
for (const file of ['package.json', 'yarn.lock']) fs.copyFileSync(path.join(consumer, file), path.join(output, `${id}-${file}`))
|
||||
fs.writeFileSync(path.join(output, `${id}-application.cjs`), application)
|
||||
report.profiles[id] = { name: pkg.name, version: pkg.version, archive: path.relative(workspace, pkg.archive).replaceAll('\\', '/'), sha256: pkg.sha256, lockSha256: hash(fs.readFileSync(path.join(consumer, 'yarn.lock'))), applicationSha256: hash(application), files: pkg.files }
|
||||
}
|
||||
finally { removeConsumer(consumer) }
|
||||
}
|
||||
for (const [id, action] of [['old', 'install-predecessor'], ['candidate', 'upgrade-renamed-package'], ['old', 'restore-predecessor']]) {
|
||||
const pkg = profiles[id]
|
||||
for (const file of ['package.json', 'yarn.lock']) fs.copyFileSync(path.join(output, `${id}-${file}`), path.join(directory, file))
|
||||
fs.copyFileSync(path.join(output, `${id}-application.cjs`), path.join(directory, 'application.cjs'))
|
||||
fs.writeFileSync(path.join(output, `${action}-install.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--force', '--ignore-scripts', '--non-interactive'], directory))
|
||||
assert.equal(hash(fs.readFileSync(path.join(directory, 'yarn.lock'))), report.profiles[id].lockSha256)
|
||||
const installed = verifyRollbackFiles(directory, pkg)
|
||||
assert(!fs.existsSync(path.join(directory, 'node_modules', id === 'old' ? name : baseline.release.name)), 'Renamed package remained after restoration')
|
||||
assert.equal(hash(fs.readFileSync(path.join(directory, 'application.cjs'))), report.profiles[id].applicationSha256)
|
||||
const runtime = run(['application.cjs'], directory).trim()
|
||||
const types = checkTypes(directory, `import Iframe from '${pkg.name}';\nconst iframe = new Iframe({ iframe: document.createElement('iframe'), url: '/child.html' });\niframe.commit(() => 42);\niframe.destroy();\n`)
|
||||
report.steps.push({ action, profile: id, installed, runtime, types, otherPackageRemoved: true, applicationRestored: true })
|
||||
}
|
||||
const aliasConsumer = consumerDirectory()
|
||||
try {
|
||||
writeJson(path.join(aliasConsumer, 'package.json'), { name: 'iframe-alias-negative', private: true, dependencies: { [name]: `file:${archive.replaceAll('\\', '/')}` } })
|
||||
fs.writeFileSync(path.join(output, 'alias-install.log'), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], aliasConsumer))
|
||||
const installed = path.join(aliasConsumer, 'node_modules', name)
|
||||
for (const [member, digest] of Object.entries(baseline.release.files)) assert.equal(hash(fs.readFileSync(path.join(installed, member.slice(8)))), digest)
|
||||
fs.writeFileSync(path.join(aliasConsumer, 'alias.cjs'), `const assert = require('node:assert/strict');\nconst Iframe = require('${name}');\nassert.equal(typeof Iframe, 'object');\nassert.equal(typeof Iframe.default, 'function');\nassert.throws(() => new Iframe({}), TypeError);\nassert.throws(() => require('${name}/legacy'), error => error.code === 'MODULE_NOT_FOUND');\nconsole.log('old tarball under new dependency key does not preserve candidate constructor/legacy');\n`)
|
||||
report.aliasNegative = { passed: true, result: run(['alias.cjs'], aliasConsumer).trim(), scope: 'Local file dependency key alias, not npm registry alias resolution' }
|
||||
}
|
||||
finally { removeConsumer(aliasConsumer) }
|
||||
report.passed = true
|
||||
}
|
||||
catch (error) {
|
||||
report.error = { name: error.name, message: error.message }
|
||||
fs.writeFileSync(path.join(output, 'failure.log'), `${error.stack}\n${error.stdout || ''}\n${error.stderr || ''}`)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
writeJson(path.join(output, 'report.json'), report)
|
||||
writeJson(path.join(parent, 'latest.json'), { output: path.relative(workspace, output).replaceAll('\\', '/'), passed: report.passed })
|
||||
removeConsumer(directory)
|
||||
console.log(`Iframe rollback: ${path.relative(workspace, output)}; passed=${report.passed}`)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync, spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { consumerDirectory, removeConsumer, workspace, writeJson } from './package-consumer.mjs'
|
||||
|
||||
assert.equal(process.versions.node, fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim(), 'Use pinned Node')
|
||||
const parent = path.join(workspace, 'refactor/.cache/mainline-rehearsal')
|
||||
fs.mkdirSync(parent, { recursive: true })
|
||||
const output = fs.mkdtempSync(path.join(parent, 'run-'))
|
||||
const directory = consumerDirectory()
|
||||
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspace, encoding: 'utf8', windowsHide: true }).trim()
|
||||
const report = { task: 'REL-04', capturedAt: new Date().toISOString(), source, node: process.versions.node, passed: false, scope: 'Synthetic isolated JS-to-TS modify/delete conflict; no production fix or remote mutation', steps: [] }
|
||||
function command(executable, args, expected = 0) {
|
||||
const result = spawnSync(executable, args, { cwd: directory, encoding: 'utf8', timeout: 30000, windowsHide: true, env: { ...process.env, NODE_PATH: '', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null', GIT_TERMINAL_PROMPT: '0' } })
|
||||
report.steps.push({ executable: executable === process.execPath ? 'node' : executable, args, status: result.status, stdout: result.stdout, stderr: result.stderr })
|
||||
assert(!result.error, result.error?.message)
|
||||
assert.equal(result.status, expected, `${executable} ${args.join(' ')}: ${result.stderr}`)
|
||||
if (executable === process.execPath && expected === 1)
|
||||
assert(result.stderr.includes('ERR_ASSERTION') && result.stderr.includes('actual: -2') && result.stderr.includes('expected: 0'), 'Expected the negative-limit regression, not an unrelated Node failure')
|
||||
return result.stdout.trim()
|
||||
}
|
||||
const git = (...args) => command('git', args)
|
||||
function write(file, text) {
|
||||
const target = path.join(directory, file)
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true })
|
||||
fs.writeFileSync(target, text)
|
||||
}
|
||||
try {
|
||||
git('init', '--initial-branch=master')
|
||||
git('config', 'user.name', 'ArtPlayer isolated rehearsal')
|
||||
git('config', 'user.email', 'rehearsal@example.invalid')
|
||||
git('config', 'commit.gpgsign', 'false')
|
||||
git('config', 'core.autocrlf', 'false')
|
||||
write('package.json', '{"private":true,"type":"module"}\n')
|
||||
const original = 'export default function retryLimit(value) {\n return Math.trunc(value);\n}\n'
|
||||
write('src/retry.js', original)
|
||||
const consumer = extension => `import assert from 'node:assert/strict';\nimport retryLimit from './src/retry.${extension}';\nassert.equal(retryLimit(2.9), 2);\nassert.equal(retryLimit(-2.9), 0);\nconsole.log('retry limit regression passed');\n`
|
||||
write('consumer.mjs', consumer('js'))
|
||||
git('add', '.')
|
||||
git('commit', '-m', 'fixture: original JavaScript implementation')
|
||||
const base = git('rev-parse', 'HEAD')
|
||||
command(process.execPath, ['consumer.mjs'], 1)
|
||||
git('switch', '-c', 'codex/rehearsal')
|
||||
// This fixture deliberately moves the calculation into another responsibility.
|
||||
fs.unlinkSync(path.join(directory, 'src/retry.js'))
|
||||
write('src/retry.ts', 'import { normalizeLimit } from \'./normalize.ts\';\nexport default (value: number): number => normalizeLimit(value);\n')
|
||||
write('src/normalize.ts', 'export const normalizeLimit = (value: number): number => Math.trunc(value);\n')
|
||||
write('consumer.mjs', consumer('ts'))
|
||||
git('add', '.')
|
||||
git('commit', '-m', 'fixture: split retry policy into TypeScript modules')
|
||||
const migrated = git('rev-parse', 'HEAD')
|
||||
command(process.execPath, ['consumer.mjs'], 1)
|
||||
git('switch', 'master')
|
||||
write('src/retry.js', original.replace('Math.trunc(value)', 'Math.max(0, Math.trunc(value))'))
|
||||
git('add', 'src/retry.js')
|
||||
git('commit', '-m', 'fixture: clamp negative retry limits')
|
||||
const fix = git('rev-parse', 'HEAD')
|
||||
command(process.execPath, ['consumer.mjs'])
|
||||
git('switch', 'codex/rehearsal')
|
||||
assert.equal(git('status', '--porcelain'), '')
|
||||
const beforeTree = git('rev-parse', 'HEAD^{tree}')
|
||||
command('git', ['cherry-pick', '-x', fix], 1)
|
||||
const conflict = git('ls-files', '--unmerged')
|
||||
assert(conflict.includes('src/retry.js'), 'Expected the removed JavaScript file to conflict')
|
||||
assert.equal(git('rev-parse', 'CHERRY_PICK_HEAD'), fix)
|
||||
git('cherry-pick', '--abort')
|
||||
assert.equal(git('rev-parse', 'HEAD'), migrated)
|
||||
assert.equal(git('rev-parse', 'HEAD^{tree}'), beforeTree)
|
||||
assert.equal(git('status', '--porcelain'), '')
|
||||
assert(!fs.existsSync(path.join(directory, 'src/retry.js')), 'Abort left an obsolete JavaScript file')
|
||||
command('git', ['cherry-pick', '-x', fix], 1)
|
||||
git('rm', 'src/retry.js')
|
||||
write('src/normalize.ts', 'export const normalizeLimit = (value: number): number => Math.max(0, Math.trunc(value));\n')
|
||||
git('add', 'src/normalize.ts')
|
||||
command(process.execPath, ['consumer.mjs'])
|
||||
git('-c', 'core.editor=true', 'cherry-pick', '--continue')
|
||||
const resolved = git('rev-parse', 'HEAD')
|
||||
assert(git('log', '-1', '--format=%B').includes(fix), 'Resolved commit must retain upstream attribution')
|
||||
assert.equal(git('status', '--porcelain'), '')
|
||||
assert.equal(git('ls-files', 'src/retry.js'), '')
|
||||
command(process.execPath, ['consumer.mjs'])
|
||||
report.commits = { base, migrated, fix, resolved }
|
||||
report.abort = { restoredCommit: migrated, restoredTree: beforeTree, clean: true }
|
||||
report.resolution = { source: 'src/retry.js', destination: 'src/normalize.ts', obsoleteFileAbsent: true, regressionPassed: true, attributed: true }
|
||||
fs.writeFileSync(path.join(output, 'resolved.patch'), `${git('show', '--format=fuller', '--binary', resolved)}\n`)
|
||||
command('git', ['bundle', 'create', path.join(output, 'rehearsal.bundle'), '--all'])
|
||||
git('bundle', 'verify', path.join(output, 'rehearsal.bundle'))
|
||||
report.passed = true
|
||||
}
|
||||
catch (error) {
|
||||
report.error = { name: error.name, message: error.message }
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
writeJson(path.join(output, 'report.json'), report)
|
||||
writeJson(path.join(parent, 'latest.json'), { output: path.relative(workspace, output).replaceAll('\\', '/'), passed: report.passed })
|
||||
removeConsumer(directory)
|
||||
console.log(`Mainline rehearsal: ${path.relative(workspace, output)}; passed=${report.passed}`)
|
||||
}
|
||||
Reference in new issue
Block a user