mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-20 04:01:20 -08:00
test(BASE-05): capture published distribution and type consumers
This commit is contained in:
1 parent
2b7054d8a5
commit
c7a375e146
21 files changed
+5079
-28
No files matched your search
@@ -37,7 +37,7 @@ function sendBytes(req, res, bytes, filename) {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url, `http://127.0.0.1:${port}`)
|
||||
const reportName = { '/reports/api': 'api', '/reports/lifecycle': 'lifecycle', '/reports/dom': 'dom' }[url.pathname]
|
||||
const reportName = { '/reports/api': 'api', '/reports/lifecycle': 'lifecycle', '/reports/dom': 'dom', '/reports/ssr': 'ssr' }[url.pathname]
|
||||
if (req.method === 'POST' && reportName) {
|
||||
assert.equal(req.headers.origin, `http://127.0.0.1:${port}`, 'Unexpected report origin')
|
||||
const chunks = []
|
||||
@@ -62,7 +62,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (reportName === 'lifecycle') {
|
||||
report.capture.media = Object.fromEntries(['video.mp4', 'video2.mp4'].map(name => [name, hash(fs.readFileSync(path.join(root, 'docs/assets/sample', name)))]))
|
||||
}
|
||||
if (reportName === 'dom') report.capture.media = { 'video.mp4': hash(fs.readFileSync(path.join(root, 'docs/assets/sample/video.mp4'))) }
|
||||
if (['dom', 'ssr'].includes(reportName)) report.capture.media = { 'video.mp4': hash(fs.readFileSync(path.join(root, 'docs/assets/sample/video.mp4'))) }
|
||||
const output = path.join(refactorDir, '.cache/reports')
|
||||
fs.mkdirSync(output, { recursive: true })
|
||||
fs.writeFileSync(path.join(output, `${reportName}.json`), `${JSON.stringify(report, null, 2)}\n`)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
import { ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
|
||||
|
||||
const root = path.resolve(refactorDir, '..')
|
||||
const fixtures = path.join(refactorDir, 'fixtures/consumers')
|
||||
const parent = path.join(refactorDir, '.cache/consumers')
|
||||
const reportPath = path.join(refactorDir, 'baselines/consumers.json')
|
||||
const fixtureNames = ['runtime.cjs', 'public.ts', 'language.ts', 'optional-chapter.ts', 'legacy-plugin.ts']
|
||||
export const runtimeChecks = [
|
||||
'DIST.cjs', 'DIST.cjs-legacy', 'DIST.esm', 'DIST.esm-default-only', 'DIST.esm-legacy',
|
||||
'DIST.global-core', 'DIST.amd-core', 'DIST.global-core-legacy', 'DIST.amd-core-legacy',
|
||||
'DIST.global-chapter', 'DIST.amd-chapter', 'DIST.global-chapter-legacy', 'DIST.amd-chapter-legacy',
|
||||
'SSR.import-template', 'SSR.constructor-browser-only', 'DIST.i18n-cjs-esm-global',
|
||||
'DIST.exports-boundary', 'DIST.direct-esm-file',
|
||||
]
|
||||
|
||||
export async function runConsumers() {
|
||||
const releases = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases
|
||||
fs.mkdirSync(parent, { recursive: true })
|
||||
const dir = fs.mkdtempSync(path.join(parent, 'published-'))
|
||||
try {
|
||||
const provenance = []
|
||||
for (const release of releases) {
|
||||
const archive = await ensureArchive(release)
|
||||
for (const [member, digest] of Object.entries(release.files)) {
|
||||
const bytes = readMember(archive, member)
|
||||
assert.equal(hash(bytes), digest, `Published member changed: ${member}`)
|
||||
const target = path.resolve(dir, 'node_modules', release.name, member.slice('package/'.length))
|
||||
assert(target.startsWith(path.join(dir, 'node_modules', release.name) + path.sep), 'Unsafe consumer path')
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true })
|
||||
fs.writeFileSync(target, bytes)
|
||||
}
|
||||
provenance.push({ name: release.name, version: release.version, integrity: release.integrity, sha256: release.sha256 })
|
||||
}
|
||||
for (const name of fixtureNames) fs.copyFileSync(path.join(fixtures, name), path.join(dir, name))
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), '{"private":true,"type":"module"}\n')
|
||||
const runtime = JSON.parse(execFileSync(process.execPath, [path.join(dir, 'runtime.cjs')], { cwd: dir, encoding: 'utf8', timeout: 20000, maxBuffer: 1024 * 1024 }))
|
||||
assert.equal(ts.version, JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).devDependencies.typescript, 'Unexpected TypeScript compiler')
|
||||
const types = []
|
||||
for (const [mode, module, moduleResolution, packageType] of [
|
||||
['node10-commonjs', ts.ModuleKind.CommonJS, ts.ModuleResolutionKind.Node10, 'commonjs'],
|
||||
['nodenext-cjs', ts.ModuleKind.NodeNext, ts.ModuleResolutionKind.NodeNext, 'commonjs'],
|
||||
['nodenext-esm', ts.ModuleKind.NodeNext, ts.ModuleResolutionKind.NodeNext, 'module'],
|
||||
['bundler-esm', ts.ModuleKind.ESNext, ts.ModuleResolutionKind.Bundler, 'module'],
|
||||
]) {
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ private: true, type: packageType }))
|
||||
for (const name of fixtureNames.filter(name => name.endsWith('.ts'))) {
|
||||
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], target: ts.ScriptTarget.ES2020, module, moduleResolution, esModuleInterop: true }
|
||||
const program = ts.createProgram([path.join(dir, name)], options)
|
||||
const declarations = program.getSourceFiles().filter(file => /\/(?:artplayer|artplayer-plugin-chapter)\/types\//.test(file.fileName.replaceAll('\\', '/')) && file.isDeclarationFile)
|
||||
assert(declarations.every(file => file.fileName.startsWith(path.join(dir, 'node_modules').replaceAll('\\', '/'))), 'Types escaped isolated published packages')
|
||||
if (name !== 'legacy-plugin.ts' || mode !== 'node10-commonjs') assert(declarations.length > 0, 'Published declarations were not loaded')
|
||||
const diagnostics = ts.getPreEmitDiagnostics(program).map(d => ({
|
||||
file: d.file ? path.relative(dir, d.file.fileName).replaceAll('\\', '/') : null,
|
||||
line: d.file && d.start !== undefined ? d.file.getLineAndCharacterOfPosition(d.start).line + 1 : null,
|
||||
code: d.code, message: ts.flattenDiagnosticMessageText(d.messageText, '\n').replaceAll(dir.replaceAll('\\', '/'), '<consumer>').replaceAll(dir, '<consumer>'),
|
||||
}))
|
||||
types.push({ mode, packageType, fixture: name, diagnostics, declarations: declarations.map(file => path.relative(dir, file.fileName).replaceAll('\\', '/')).sort() })
|
||||
}
|
||||
}
|
||||
return { schemaVersion: 1, task: 'BASE-05', sourceCommit: '2b7054d8', capturedAt: new Date().toISOString(), node: process.version, typescript: ts.version, releases: provenance,
|
||||
compilerOptions: { strict: true, skipLibCheck: false, noEmit: true, types: [], lib: ['es2020', 'dom'], target: 'es2020', esModuleInterop: true },
|
||||
fixtureHashAlgorithm: 'sha256-lf', fixtures: Object.fromEntries(fixtureNames.map(name => [name, hash(fs.readFileSync(path.join(fixtures, name), 'utf8').replaceAll('\r\n', '\n'))])), runtime, types }
|
||||
}
|
||||
finally {
|
||||
const resolved = path.resolve(dir)
|
||||
assert(resolved.startsWith(path.resolve(parent) + path.sep) && path.basename(resolved).startsWith('published-'), 'Unsafe consumer cleanup')
|
||||
fs.rmSync(resolved, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
export function consumerProfile(report) {
|
||||
return { releases: report.releases, fixtures: report.fixtures, typescript: report.typescript, compilerOptions: report.compilerOptions, runtime: report.runtime, types: report.types }
|
||||
}
|
||||
|
||||
export function validateConsumerReport(report) {
|
||||
assert.equal(report.schemaVersion, 1)
|
||||
assert.equal(report.task, 'BASE-05')
|
||||
assert.deepEqual(report.runtime.checks, runtimeChecks, 'Missing runtime consumer checks')
|
||||
assert.equal(report.fixtureHashAlgorithm, 'sha256-lf')
|
||||
assert.equal(report.compilerOptions.skipLibCheck, false)
|
||||
assert.equal(report.compilerOptions.strict, true)
|
||||
const releases = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases
|
||||
assert.deepEqual(report.releases, releases.map(({ name, version, integrity, sha256 }) => ({ name, version, integrity, sha256 })), 'Wrong published consumers')
|
||||
assert.equal(report.types.length, 16)
|
||||
const cases = new Set(report.types.map(item => `${item.mode}/${item.fixture}`))
|
||||
assert.equal(cases.size, 16)
|
||||
for (const mode of ['node10-commonjs', 'nodenext-cjs', 'nodenext-esm', 'bundler-esm']) {
|
||||
for (const fixture of fixtureNames.filter(name => name.endsWith('.ts'))) {
|
||||
const result = report.types.find(item => item.mode === mode && item.fixture === fixture)
|
||||
assert(result, 'Missing TS consumer case')
|
||||
if (mode !== 'nodenext-esm' && ['public.ts', 'language.ts'].includes(fixture)) assert.deepEqual(result.diagnostics, [], 'Previously valid consumer failed')
|
||||
if (mode !== 'nodenext-esm' && fixture === 'optional-chapter.ts') assert.deepEqual(result.diagnostics.map(d => d.code), [2554], 'Historical optional-argument result changed')
|
||||
if (mode === 'node10-commonjs' && fixture === 'legacy-plugin.ts') assert.deepEqual(result.diagnostics.map(d => d.code), [2307], 'Historical legacy resolution changed')
|
||||
}
|
||||
}
|
||||
for (const name of fixtureNames) assert.equal(report.fixtures[name], hash(fs.readFileSync(path.join(fixtures, name), 'utf8').replaceAll('\r\n', '\n')), 'Consumer fixture changed; review baseline')
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const result = await runConsumers()
|
||||
validateConsumerReport(result)
|
||||
if (process.argv.includes('--capture')) {
|
||||
assert(!fs.existsSync(reportPath), 'Do not overwrite frozen consumer results')
|
||||
fs.writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`)
|
||||
}
|
||||
else if (process.argv.includes('--inspect')) {
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
}
|
||||
else {
|
||||
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use --capture, --inspect or --check')
|
||||
const frozen = JSON.parse(fs.readFileSync(reportPath, 'utf8'))
|
||||
validateConsumerReport(frozen)
|
||||
assert.deepEqual(consumerProfile(result), consumerProfile(frozen), 'Published consumer behavior differs')
|
||||
console.log(`Published consumers rerun: ${result.runtime.checks.length} runtime checks, ${result.types.length} strict TS cases; historical diagnostics remain explicit`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { consumerProfile, runConsumers, validateConsumerReport } from './consumers.mjs'
|
||||
import { validateDistribution } from './distribution.mjs'
|
||||
import { refactorDir } from './releases.mjs'
|
||||
import { validateSsr } from './ssr.mjs'
|
||||
|
||||
test('Published packages rerun isolated runtime and strict TypeScript consumers', async () => {
|
||||
const frozen = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/consumers.json'), 'utf8'))
|
||||
validateConsumerReport(frozen)
|
||||
const result = await runConsumers()
|
||||
validateConsumerReport(result)
|
||||
assert.deepEqual(consumerProfile(result), consumerProfile(frozen))
|
||||
for (const mutate of [
|
||||
value => value.runtime.checks.pop(),
|
||||
value => value.compilerOptions.skipLibCheck = true,
|
||||
value => value.types.find(item => item.mode === 'bundler-esm' && item.fixture === 'public.ts').diagnostics.push({ code: 2322 }),
|
||||
value => value.releases[0].version = 'wrong',
|
||||
]) {
|
||||
const changed = structuredClone(frozen)
|
||||
mutate(changed)
|
||||
assert.throws(() => validateConsumerReport(changed))
|
||||
}
|
||||
})
|
||||
|
||||
test('Distribution inventory preserves all packages and the thumbnail discrepancy', () => {
|
||||
const frozen = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/distribution.json'), 'utf8'))
|
||||
validateDistribution(frozen)
|
||||
const changed = structuredClone(frozen)
|
||||
changed.packages.pop()
|
||||
assert.throws(() => validateDistribution(changed))
|
||||
})
|
||||
|
||||
test('Browser SSR evidence requires node identity, real metadata and cleanup', () => {
|
||||
const frozen = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/ssr.json'), 'utf8'))
|
||||
validateSsr(frozen.report)
|
||||
const changed = structuredClone(frozen.report)
|
||||
changed.observations.videoWidth = 0
|
||||
assert.throws(() => validateSsr(changed))
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { hash, refactorDir } from './releases.mjs'
|
||||
|
||||
const root = path.resolve(refactorDir, '..')
|
||||
const filename = path.join(refactorDir, 'baselines/distribution.json')
|
||||
function files(dir) {
|
||||
if (!fs.existsSync(dir)) return []
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? files(path.join(dir, entry.name)) : [path.join(dir, entry.name)])
|
||||
}
|
||||
function targets(value, field) {
|
||||
if (typeof value === 'string') return [{ field, path: value }]
|
||||
if (value && typeof value === 'object') return Object.entries(value).flatMap(([key, child]) => targets(child, `${field}.${key}`))
|
||||
return []
|
||||
}
|
||||
export function captureDistribution() {
|
||||
const initial = JSON.parse(fs.readFileSync(path.join(refactorDir, 'package-inventory.json'), 'utf8'))
|
||||
return { schemaVersion: 1, task: 'BASE-05', capturedAt: new Date().toISOString(), sourceCommit: '2b7054d8',
|
||||
note: 'Workspace manifest and observed resource inventory. Existing dist files may be stale; presence and hashes are not proof of a fresh build or npm publication. Only core/chapter published tarballs are verified in releases.json and consumers.json.',
|
||||
packages: initial.packages.map(pkg => {
|
||||
const dir = path.join(root, 'packages', pkg.name)
|
||||
const manifestBytes = fs.readFileSync(path.join(dir, 'package.json'))
|
||||
const manifest = JSON.parse(manifestBytes)
|
||||
const resources = ['dist', 'types'].flatMap(name => files(path.join(dir, name))).map(file => ({ path: path.relative(dir, file).replaceAll('\\', '/'), bytes: fs.statSync(file).size, sha256: hash(fs.readFileSync(file)) })).sort((a, b) => a.path.localeCompare(b.path))
|
||||
const declaredTargets = ['main', 'module', 'legacy', 'types', 'typings', 'exports', 'typesVersions', 'browser', 'unpkg', 'jsdelivr'].flatMap(field => targets(manifest[field], field)).map(target => {
|
||||
const normalized = target.path.replace(/^\.\//, '')
|
||||
const regex = new RegExp(`^${normalized.split('*').map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`)
|
||||
return { ...target, observedMatches: resources.filter(resource => regex.test(resource.path)).map(resource => resource.path) }
|
||||
})
|
||||
const site = pkg.name === 'artplayer-vitepress'
|
||||
return { name: pkg.name, kind: site ? 'documentation-site' : 'npm-library', manifest, manifestSha256Lf: hash(manifestBytes.toString().replaceAll('\r\n', '\n')), declaredTargets, resources,
|
||||
sourceAssets: files(path.join(dir, 'src')).filter(file => !/\.[cm]?[jt]sx?$/.test(file)).map(file => path.relative(dir, file).replaceAll('\\', '/')).sort(),
|
||||
buildOutputs: site ? ['docs/document/** via vitepress outDir'] : [`dist/${pkg.name}.js`, `dist/${pkg.name}.legacy.js`, `dist/${pkg.name}.mjs`, `docs/compiled/${pkg.name}.{js,legacy.js,mjs} copied by root build`],
|
||||
distributionStatus: site ? 'Actual workflow builds a static site; no library entrypoints. Manifest lacks private:true; publication intent needs SITE/REL confirmation, not an automatic npm release.' : 'Workspace inventory; per-package published contract remains required unless listed in releases.json.',
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
export function validateDistribution(report) {
|
||||
const initial = JSON.parse(fs.readFileSync(path.join(refactorDir, 'package-inventory.json'), 'utf8'))
|
||||
assert.equal(report.schemaVersion, 1)
|
||||
assert.deepEqual(report.packages.map(pkg => pkg.name), initial.packages.map(pkg => pkg.name), 'Distribution package coverage changed')
|
||||
for (const pkg of report.packages) {
|
||||
assert.equal(pkg.manifest.name, pkg.name)
|
||||
if (pkg.kind === 'npm-library') assert(pkg.resources.length > 0 && pkg.declaredTargets.length > 0, `Missing library resources: ${pkg.name}`)
|
||||
for (const resource of pkg.resources) assert(!resource.path.startsWith('/') && !resource.path.split('/').includes('..') && /^[a-f0-9]{64}$/.test(resource.sha256), 'Invalid resource provenance')
|
||||
}
|
||||
assert.equal(report.packages.filter(pkg => pkg.kind === 'documentation-site').length, 1)
|
||||
const thumbnail = report.packages.find(pkg => pkg.name === 'artplayer-tool-thumbnail')
|
||||
assert(thumbnail.declaredTargets.some(target => target.path.endsWith('.esm.js') && !target.observedMatches.length), 'Missing thumbnail entrypoint discrepancy')
|
||||
}
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
if (process.argv.includes('--capture')) {
|
||||
assert(!fs.existsSync(filename), 'Do not overwrite the historical distribution inventory')
|
||||
const report = captureDistribution()
|
||||
validateDistribution(report)
|
||||
fs.writeFileSync(filename, `${JSON.stringify(report, null, 2)}\n`)
|
||||
}
|
||||
else {
|
||||
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use --capture or --check')
|
||||
const report = JSON.parse(fs.readFileSync(filename, 'utf8'))
|
||||
validateDistribution(report)
|
||||
console.log(`Frozen distribution inventory: ${report.packages.length} packages; current build and npm contents are not revalidated by this command`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { validatePublishedCapture } from './api.mjs'
|
||||
import { hash, refactorDir } from './releases.mjs'
|
||||
|
||||
export function validateSsr(report) {
|
||||
assert.equal(report.kind, 'ssr')
|
||||
validatePublishedCapture(report, 'ssr')
|
||||
assert.deepEqual(report.errors, [])
|
||||
assert.deepEqual(report.unhandled, [])
|
||||
assert.deepEqual(report.checks, ['SSR.browser-reuses-player', 'SSR.browser-reuses-video', 'SSR.browser-ready', 'SSR.browser-plugin', 'SSR.browser-cleanup'].map(id => ({ id, passed: true })))
|
||||
assert.equal(report.capture.media['video.mp4'], hash(fs.readFileSync(path.resolve(refactorDir, '../docs/assets/sample/video.mp4'))))
|
||||
assert(report.observations.videoWidth > 0 && report.observations.duration > 0)
|
||||
assert.equal(report.observations.serverMarker, 'preserved')
|
||||
}
|
||||
export function ssrProfile(report) {
|
||||
return { checks: report.checks, observations: report.observations }
|
||||
}
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const frozen = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/ssr.json'), 'utf8'))
|
||||
validateSsr(frozen.report)
|
||||
const index = process.argv.indexOf('--compare')
|
||||
if (index !== -1) {
|
||||
assert(process.argv[index + 1], '--compare requires a report path')
|
||||
const report = JSON.parse(fs.readFileSync(process.argv[index + 1], 'utf8'))
|
||||
validateSsr(report)
|
||||
assert.deepEqual(ssrProfile(report), ssrProfile(frozen.report))
|
||||
console.log('Published browser template reuse matches SSR baseline')
|
||||
}
|
||||
else {
|
||||
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use --check or --compare <report>')
|
||||
console.log('Frozen browser SSR evidence verified; browser was not rerun')
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user