test(BASE-05): capture published distribution and type consumers

This commit is contained in:
Harvey Zhao committed 2026-09-10 20:34:04 +08:00
1 parent 2b7054d8a5
commit c7a375e146
21 files changed
+5079 -28

No files matched your search

+2 -2
View File
@@ -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`)
+122
View File
@@ -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`)
}
}
+42
View File
@@ -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))
})
+67
View File
@@ -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`)
}
}
+36
View File
@@ -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')
}
}