test(refactor): [BASE-03] capture media and lifecycle behavior

This commit is contained in:
Harvey Zhao committed 2026-09-10 19:55:55 +08:00
1 parent b382ec08f7
commit 9489392905
15 files changed
+1333 -20

No files matched your search

+10 -6
View File
@@ -12,10 +12,7 @@ export const checkIds = [
'API-05.destroy-return', 'API-05.instance-cleanup',
]
export function validateApiReport(report) {
assert.equal(report.kind, 'public-api')
assert.deepEqual(report.errors, [], 'Browser capture failed')
assert.deepEqual(report.checks, checkIds.map(id => ({ id, passed: true })), 'Missing or failed API checks')
export function validatePublishedCapture(report, fixture) {
assert(report.environment.userAgent && report.environment.language, 'Missing browser environment')
assert(Number.isFinite(Date.parse(report.capture.capturedAt)), 'Missing capture timestamp')
const releases = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases
@@ -24,12 +21,19 @@ export function validateApiReport(report) {
return { name: release.name, version: release.version, integrity: release.integrity, member, sha256: release.files[member] }
})
assert.deepEqual(report.capture.releases, expectedSources, 'Published sources differ')
assert.deepEqual(report.scripts, [...expectedSources.map(release => `/releases/${release.name}/${release.member.slice('package/'.length)}`), '/fixtures/api.js'], 'Unexpected loaded scripts')
assert.deepEqual(report.scripts, [...expectedSources.map(release => `/releases/${release.name}/${release.member.slice('package/'.length)}`), `/fixtures/${fixture}.js`], 'Unexpected loaded scripts')
assert.equal(report.capture.fixtureHashAlgorithm, 'sha256-lf')
for (const name of ['api.html', 'api.js']) {
for (const name of [`${fixture}.html`, `${fixture}.js`]) {
const source = fs.readFileSync(path.join(refactorDir, 'fixtures', name), 'utf8').replaceAll('\r\n', '\n')
assert.equal(report.capture.fixtures[name], hash(source), `Fixture changed: ${name}; review the baseline explicitly`)
}
}
export function validateApiReport(report) {
assert.equal(report.kind, 'public-api')
assert.deepEqual(report.errors, [], 'Browser capture failed')
assert.deepEqual(report.checks, checkIds.map(id => ({ id, passed: true })), 'Missing or failed API checks')
validatePublishedCapture(report, 'api')
for (const section of ['static', 'prototype', 'emitterPrototype', 'defaults', 'constants', 'config', 'utilityDescriptors', 'instance', 'instanceResolved', 'chapterFactory', 'chapterResult', 'integrations']) {
assert(report.snapshot?.[section] && Object.keys(report.snapshot[section]).length, `Missing API section: ${section}`)
}
+8 -4
View File
@@ -37,7 +37,8 @@ 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}`)
if (req.method === 'POST' && url.pathname === '/reports/api') {
const reportName = { '/reports/api': 'api', '/reports/lifecycle': 'lifecycle' }[url.pathname]
if (req.method === 'POST' && reportName) {
assert.equal(req.headers.origin, `http://127.0.0.1:${port}`, 'Unexpected report origin')
const chunks = []
let size = 0
@@ -47,7 +48,7 @@ const server = http.createServer(async (req, res) => {
chunks.push(chunk)
}
const report = JSON.parse(Buffer.concat(chunks).toString('utf8'))
assert.equal(report.kind, 'public-api')
assert.equal(report.kind, reportName === 'api' ? 'public-api' : 'lifecycle')
report.capture = {
capturedAt: new Date().toISOString(),
releases: baseline.releases.map(release => ({
@@ -56,11 +57,14 @@ const server = http.createServer(async (req, res) => {
sha256: release.files[`package/${release.manifest.main.replace(/^\.\//, '')}`],
})),
fixtureHashAlgorithm: 'sha256-lf',
fixtures: Object.fromEntries(['api.html', 'api.js'].map(name => [name, hash(fs.readFileSync(path.join(refactorDir, 'fixtures', name), 'utf8').replaceAll('\r\n', '\n'))])),
fixtures: Object.fromEntries([`${reportName}.html`, `${reportName}.js`].map(name => [name, hash(fs.readFileSync(path.join(refactorDir, 'fixtures', name), 'utf8').replaceAll('\r\n', '\n'))])),
}
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)))]))
}
const output = path.join(refactorDir, '.cache/reports')
fs.mkdirSync(output, { recursive: true })
fs.writeFileSync(path.join(output, 'api.json'), `${JSON.stringify(report, null, 2)}\n`)
fs.writeFileSync(path.join(output, `${reportName}.json`), `${JSON.stringify(report, null, 2)}\n`)
res.writeHead(204).end()
return
}
+89
View File
@@ -0,0 +1,89 @@
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 const lifecycleChecks = [
'LIFE.version', 'EVENT.chain', 'EVENT.context-arguments', 'EVENT.once-reentry',
'EVENT.dispatch-snapshot', 'EVENT.off-original-once', 'EVENT.throw-propagation',
'LIFE.ready-callback', 'MEDIA.metadata', 'MEDIA.play-promise', 'MEDIA.play-result',
'MEDIA.time-advances', 'MEDIA.pause-sync-stable', 'MEDIA.seek',
'MEDIA.quality-preserves-time', 'MEDIA.url-resets-resumes',
'LIFE.ready-once-after-switches', 'LIFE.restart-url', 'MEDIA.play-rejects-original',
'PLUGIN.async-promise', 'PLUGIN.async-registry', 'PLUGIN.duplicate-throws',
'MEDIA.latest-source', 'LIFE.destroy-retains-markup', 'LIFE.destroy-cleans-proxy',
'LIFE.destroy-removes-markup', 'LIFE.constructor-plugin-error',
]
export function validateLifecycleReport(report) {
assert.equal(report.kind, 'lifecycle')
assert.deepEqual(report.errors, [], 'Lifecycle capture failed')
assert.deepEqual(report.unhandled, [], 'Unexpected unhandled rejection')
assert.deepEqual(report.checks, lifecycleChecks.map(id => ({ id, passed: true })), 'Missing or failed lifecycle checks')
validatePublishedCapture(report, 'lifecycle')
for (const name of ['video.mp4', 'video2.mp4']) {
assert.equal(report.capture.media[name], hash(fs.readFileSync(path.resolve(refactorDir, '../docs/assets/sample', name))), `Media fixture changed: ${name}`)
}
const o = report.observations
assert(o.playback.progressed >= o.playback.start + 0.1 && o.playback.width > 0 && o.playback.duration > 2, 'No real-media advancement evidence')
assert(o.pause.observedMs >= 250 && o.pause.maxDelta < 0.02, 'No stable pause evidence')
assert.equal(o.readyCallbacks, 1, 'Ready callback repeated')
assert.equal(o.remainingInstances, 0, 'Probe leaked registered instances')
const order = (phase, earlier, later) => {
const events = report.trace.filter(item => item.phase === phase).map(item => item.event)
assert(events.indexOf(earlier) >= 0 && events.indexOf(earlier) < events.indexOf(later), `Unexpected ${phase} order: ${earlier} -> ${later}`)
}
order('initial', 'video:loadedmetadata', 'ready')
order('play', 'video:play', 'video:playing')
order('play', 'video:playing', 'play')
order('pause', 'pause', 'video:pause')
order('quality-paused', 'video:canplay', 'restart')
for (const item of o.expectedUnhandled) {
assert.deepEqual(item, { phase: 'controlled-resume-rejection', message: 'controlled switch resume rejection' }, 'Unexpected controlled rejection')
}
const tasks = JSON.parse(fs.readFileSync(path.join(refactorDir, 'tasks.json'), 'utf8')).tasks
assert.equal(new Set(report.findings.map(item => item.id)).size, report.findings.length, 'Duplicate finding IDs')
for (const finding of report.findings) assert(tasks.some(task => task.id === finding.owner), `Unknown finding owner: ${finding.owner}`)
}
export function lifecycleProfile(report) {
const o = report.observations
return {
checks: report.checks,
emitterOrder: o.emitterOrder,
readyCallbacks: o.readyCallbacks,
restarts: o.restarts,
concurrentSwitch: { outcomes: o.concurrentSwitch.outcomes, finalSource: o.concurrentSwitch.finalSource },
destroyPending: { switchState: o.destroyPending.switchState, latePluginRegistered: o.destroyPending.latePluginRegistered },
repeatedDestroy: o.repeatedDestroy,
failedConstructor: { hasMarkup: o.failedConstructor.hasMarkup, retainedListeners: o.failedConstructor.listeners > 0, registered: o.failedConstructor.registered },
rejectedResume: { switchState: o.rejectedResume.switchState, expectedUnhandled: o.rejectedResume.expectedUnhandled },
findings: report.findings.map(({ id, owner }) => ({ id, owner })).sort((a, b) => a.id.localeCompare(b.id)),
}
}
export function verifyFrozenLifecycle() {
const frozen = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/lifecycle.json'), 'utf8'))
assert.equal(frozen.schemaVersion, 1)
validateLifecycleReport(frozen.report)
assert.equal(hash(JSON.stringify(lifecycleProfile(frozen.report))), frozen.verification.profileSha256, 'Frozen lifecycle profile changed')
return frozen
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const frozen = verifyFrozenLifecycle()
const index = process.argv.indexOf('--compare')
if (index !== -1) {
assert(process.argv[index + 1], '--compare requires a report path')
const captured = JSON.parse(fs.readFileSync(process.argv[index + 1], 'utf8'))
validateLifecycleReport(captured)
assert.deepEqual(lifecycleProfile(captured), lifecycleProfile(frozen.report), 'Published lifecycle profile differs')
console.log('Published lifecycle capture matches semantic baseline; historical findings remain recorded')
}
else {
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use --check or --compare <report>')
console.log(`Frozen lifecycle report verified: ${lifecycleChecks.length} checks; this command does not rerun the browser`)
}
}
+31
View File
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { lifecycleProfile, validateLifecycleReport, verifyFrozenLifecycle } from './lifecycle.mjs'
test('lifecycle evidence rejects fake progress, unexpected errors and event reordering', () => {
const { report } = verifyFrozenLifecycle()
const stopped = structuredClone(report)
stopped.observations.playback.progressed = stopped.observations.playback.start
assert.throws(() => validateLifecycleReport(stopped), /No real-media advancement/)
const error = structuredClone(report)
error.unhandled.push('unexpected error')
assert.throws(() => validateLifecycleReport(error), /Unexpected unhandled/)
const reordered = structuredClone(report)
const phase = reordered.trace.filter(item => item.phase === 'play')
const native = phase.find(item => item.event === 'video:playing')
const publicEvent = phase.find(item => item.event === 'play')
native.event = 'play'
publicEvent.event = 'video:playing'
assert.throws(() => validateLifecycleReport(reordered), /Unexpected play order/)
})
test('lifecycle profile ignores media clock jitter but retains lifecycle findings', () => {
const { report } = verifyFrozenLifecycle()
const jitter = structuredClone(report)
jitter.observations.playback.progressed += 0.03
jitter.observations.pause.observedMs += 10
assert.deepEqual(lifecycleProfile(jitter), lifecycleProfile(report))
const changed = structuredClone(report)
changed.observations.repeatedDestroy.survivorAfter = true
assert.notDeepEqual(lifecycleProfile(changed), lifecycleProfile(report))
})