test(ci): [CI-01] checkpoint installed adaptive and thumbnail coverage

This commit is contained in:
Harvey Zhao committed 2026-09-15 02:33:16 +08:00
1 parent 7094cf6f9c
commit b90de99bc2
25 files changed
+10494 -45

No files matched your search

+12
View File
@@ -31,6 +31,18 @@ test('Browser invocation rejects ambiguous scopes and output/configuration overr
assert.throws(() => browserInvocation('source', [flag], {}), /Do not override/)
})
test('Installed validation rejects diagnostic SDK substitutions and recovery probes', () => {
const base = { ARTPLAYER_BROWSER_ARTIFACTS: '/installed/map.json' }
for (const mode of ['upstream4', 'bufferlevel4']) {
const env = { ...base, ARTPLAYER_DASH_DIAGNOSTIC_SDK: mode }
assert.throws(() => browserInvocation('installed', [], env), /unchanged DASH SDK/)
assert.equal(browserInvocation('source', [], env).env.ARTPLAYER_DASH_DIAGNOSTIC_SDK, mode)
}
for (const name of ['ARTPLAYER_DASH_DIAGNOSE_STALL', 'ARTPLAYER_DASH_DIAGNOSE_GETTER', 'ARTPLAYER_DASH_DIAGNOSE_METRICS'])
assert.throws(() => browserInvocation('installed', [], { ...base, [name]: '1' }), /recovery diagnostics/)
assert.equal(browserInvocation('installed', [], { ...base, ARTPLAYER_DASH_DIAGNOSTIC_SDK: 'none' }).env.ARTPLAYER_DASH_DIAGNOSTIC_SDK, 'none')
})
for (const code of [0, 17]) {
test(`Browser runner preserves actual child exit ${code} and source input identity`, () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-browser-runner-'))
@@ -108,7 +108,9 @@ for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame
throw new Error('No owned encoding deadline')
probe.encodingDeadline.callback()
}
else probe.art.emit(scenario.replace('frame-', ''))
else {
probe.art.emit(scenario.replace('frame-', ''))
}
if (probe.blobs[0]) {
probe.blobs[0].callback(probe.blobs[0].blob)
probe.blobs[0].callback(probe.blobs[0].blob)
@@ -136,14 +138,16 @@ for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame
probe.restore()
return { before, after, video, canvasDimensions, publishedImage, final, blobs, pendingDeadlines: probe.timers.size, warnings: probe.warnings, heldFrames: probe.frames.length, frameSupport: probe.frameSupport, result: probe.result }
}, scenario)
await testInfo.attach('auto-thumbnail-candidate-native-lifecycle', { contentType: 'application/json', body: JSON.stringify({ scenario, useDefault, sha256: hash(implementation.code), scope: 'Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.', ...state }) })
await testInfo.attach('auto-thumbnail-candidate-native-lifecycle', { contentType: 'application/json', body: JSON.stringify({ scenario, useDefault, sha256: hash(implementation.code), provenance: implementation.provenance, scope: 'Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.', ...state }) })
expect(state.result).toEqual({ name: 'artplayerPluginAutoThumbnail' })
expect(state.pendingDeadlines).toBe(0)
if (scenario === 'encoding-timeout') {
expect(state.warnings).toHaveLength(1)
expect(state.warnings[0][1]).toContain('encoding timed out')
}
else expect(state.warnings).toEqual([])
else {
expect(state.warnings).toEqual([])
}
expect(state.before).toEqual(scenario === 'complete' ? { urls: 1, updates: 2 } : { urls: 0, updates: 0 })
expect(state.after).toEqual(state.before)
expect(state.video).toEqual({ connected: false, src: null, readyState: 0, paused: true, handlers: [true, true, true, true] })
@@ -91,6 +91,7 @@ for (const media of ['auto-thumbnail-timeline', 'pattern']) {
})
await testInfo.attach('auto-thumbnail-candidate-hidden-pixels', { contentType: 'application/json', body: JSON.stringify({
sha256: hash(implementation.code),
provenance: implementation.provenance,
media,
scope: 'All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.',
...state,
+5 -5
View File
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let publishedCode
@@ -14,9 +14,9 @@ test.beforeAll(async () => {
const bytes = readMember(await ensureArchive(release), member)
assert.equal(hash(bytes), release.files[member])
publishedCode = bytes.toString()
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Controlled DASH source tests do not represent installed SDK/package acceptance')
sourceCode = process.env.ARTPLAYER_DASH_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_DASH_ARTIFACT, 'utf8') : await compilePackage(release.name, 'umd')
evidence = { published: release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_DASH_ARTIFACT || 'workspace source build', scope: 'Real ArtPlayer DOM and native MP4 with controlled SDK methods. No dash.js instance, MPD, ABR or adaptive media validation.' }
const candidate = await browserCandidate(release.name, process.env.ARTPLAYER_DASH_ARTIFACT)
sourceCode = candidate.code
evidence = { published: release, sourceSHA256: hash(sourceCode), candidate: candidate.provenance, scope: 'Real ArtPlayer DOM and native MP4 with controlled SDK methods. No dash.js instance, MPD, ABR or adaptive media validation.' }
})
test.afterEach(async ({ page }, testInfo) => {
const state = await page.evaluate(() => ({
@@ -37,7 +37,7 @@ test.afterEach(async ({ page }, testInfo) => {
}
})
async function openDash(page, core, plugin, sdk, testInfo) {
await testInfo.attach('controlled-dash-inputs', { contentType: 'application/json', body: JSON.stringify(evidence) })
await testInfo.attach('controlled-dash-inputs', { contentType: 'application/json', body: JSON.stringify({ ...evidence, core, plugin, selected: plugin === 'published' ? { kind: 'published', sha256: hash(publishedCode) } : evidence.candidate }) })
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: plugin === 'published' ? publishedCode : sourceCode })
await page.evaluate((sdk) => {
+8 -3
View File
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { observeDashBuffers } from './dash-buffer-observer.js'
import { probeBufferGetter, probeBufferMetric } from './dash-seek-diagnostics.js'
import { expect, test } from './fixtures.js'
@@ -10,6 +10,7 @@ import { expect, test } from './fixtures.js'
const sdks = new Map()
const media = new Map()
let candidate
let candidateProvenance
let published
let mediaManifest
let pluginRelease
@@ -18,6 +19,7 @@ assert(['none', 'upstream4', 'bufferlevel4'].includes(diagnosticSDK), 'Unknown d
const recoveryModes = ['ARTPLAYER_DASH_DIAGNOSE_STALL', 'ARTPLAYER_DASH_DIAGNOSE_GETTER', 'ARTPLAYER_DASH_DIAGNOSE_METRICS'].filter(name => process.env[name] === '1')
assert(recoveryModes.length <= 1, 'Choose only one DASH recovery diagnostic')
assert(!recoveryModes.length || diagnosticSDK === 'none', 'Recovery diagnostics require the unchanged published SDK')
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS || (diagnosticSDK === 'none' && recoveryModes.length === 0), 'Installed DASH checks cannot use SDK or recovery diagnostics')
test.beforeAll(async () => {
const baseline = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/dash-sdk.json', import.meta.url)))
@@ -44,7 +46,9 @@ test.beforeAll(async () => {
const bytes = readMember(await ensureArchive(pluginRelease), 'package/dist/artplayer-plugin-dash-control.js')
assert.equal(hash(bytes), pluginRelease.files['package/dist/artplayer-plugin-dash-control.js'])
published = bytes.toString()
candidate = process.env.ARTPLAYER_DASH_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_DASH_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-dash-control', 'umd')
const input = await browserCandidate('artplayer-plugin-dash-control', process.env.ARTPLAYER_DASH_ARTIFACT)
candidate = input.code
candidateProvenance = input.provenance
mediaManifest = JSON.parse(fs.readFileSync(new URL('./media/dash/manifest.json', import.meta.url)))
for (const [name, expected] of Object.entries(mediaManifest.files)) {
assert(/^[\w.-]+$/.test(name))
@@ -116,7 +120,7 @@ async function loadSDK(page, version, core, testInfo) {
await page.evaluate(observeDashBuffers)
await page.addScriptTag({ content: sdk.code })
const capability = await page.evaluate(() => ({ sdk: window.dashjs.supportsMediaSource(), mse: typeof window.MediaSource, managed: typeof window.ManagedMediaSource, avc: Boolean(window.MediaSource?.isTypeSupported('video/mp4; codecs="avc1.42c01e"')), aac: Boolean(window.MediaSource?.isTypeSupported('audio/mp4; codecs="mp4a.40.2"')) }))
await testInfo.attach('dash-sdk-inputs', { contentType: 'application/json', body: JSON.stringify({ sdk: sdk.release, codeMember: sdk.diagnostic?.member || sdk.codeMember, diagnostic: sdk.diagnostic, pluginRelease, candidateSHA256: hash(candidate), candidate: process.env.ARTPLAYER_DASH_ARTIFACT || 'workspace source build', media: mediaManifest, capability }) })
await testInfo.attach('dash-sdk-inputs', { contentType: 'application/json', body: JSON.stringify({ sdk: sdk.release, codeMember: sdk.diagnostic?.member || sdk.codeMember, diagnostic: sdk.diagnostic, pluginRelease, candidateSHA256: hash(candidate), availableCandidate: candidateProvenance, pluginLoaded: false, media: mediaManifest, capability }) })
return capability
}
@@ -124,6 +128,7 @@ async function openDash(page, version, core, plugin, testInfo) {
const capability = await loadSDK(page, version, core, testInfo)
test.skip(!capability.sdk, 'Actual SDK reports no MediaSource support; this engine has no DASH playback acceptance')
await routeMedia(page)
await testInfo.attach('dash-plugin-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, plugin, selected: plugin === 'published' ? { kind: 'published', sha256: hash(published) } : candidateProvenance }) })
await page.addScriptTag({ content: plugin === 'published' ? published : candidate })
await page.evaluate(() => {
window.sdkErrors = []
+5 -8
View File
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let sdkCode
@@ -38,11 +38,8 @@ test.beforeAll(async () => {
}
sdkCode = await code(sdk.release, 'package/dist/hls.min.js')
publishedCode = await code(plugin.release, 'package/dist/artplayer-plugin-hls-control.js')
// This task exercises source builds. Installed artifacts are a separate HLS-06 gate.
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'HLS source fixture cannot masquerade as an installed plugin')
sourceCode = process.env.ARTPLAYER_HLS_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_HLS_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-hls-control', 'umd')
const candidate = await browserCandidate('artplayer-plugin-hls-control', process.env.ARTPLAYER_HLS_ARTIFACT)
sourceCode = candidate.code
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
media = new Map()
for (const [name, expected] of Object.entries(manifest.files)) {
@@ -52,11 +49,11 @@ test.beforeAll(async () => {
assert.equal(bytes.length, expected.bytes)
media.set(name, bytes)
}
evidence = { sdk: sdk.release, plugin: plugin.release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_HLS_ARTIFACT || 'workspace source build', media: manifest, limitations: ['SDK worker disabled', 'local deterministic media', 'not an isolated installed package', 'no physical device certification'] }
evidence = { sdk: sdk.release, plugin: plugin.release, sourceSHA256: hash(sourceCode), candidate: candidate.provenance, media: manifest, limitations: ['SDK worker disabled', 'local deterministic media', 'no physical device certification'] }
})
async function openHls(page, core, plugin, testInfo, manifest = 'master.m3u8') {
await testInfo.attach('hls-inputs', { contentType: 'application/json', body: JSON.stringify(evidence) })
await testInfo.attach('hls-inputs', { contentType: 'application/json', body: JSON.stringify({ ...evidence, core, plugin, selected: plugin === 'published' ? { kind: 'published', sha256: hash(publishedCode) } : evidence.candidate }) })
await page.route('**/hls-fixture/**', async (route) => {
const name = new URL(route.request().url()).pathname.split('/').at(-1)
const bytes = media.get(name)
+6 -6
View File
@@ -3,22 +3,22 @@ import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { observeWorkers } from '../helpers/worker-observer.js'
import { expect, test } from './fixtures.js'
const matrix = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/hls-sdk-matrix.json', import.meta.url)))
const grouped = fs.readFileSync(new URL('./fixtures/hls-grouped.m3u8', import.meta.url), 'utf8')
let candidate
let candidateProvenance
let published
const sdks = new Map()
const media = new Map()
test.beforeAll(async () => {
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'HLS installed artifact map remains a separate HLS-06 gate')
candidate = process.env.ARTPLAYER_HLS_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_HLS_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-hls-control', 'umd')
const input = await browserCandidate('artplayer-plugin-hls-control', process.env.ARTPLAYER_HLS_ARTIFACT)
candidate = input.code
candidateProvenance = input.provenance
const baseline = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/hls-control-release.json', import.meta.url)))
const bytes = readMember(await ensureArchive(baseline.release), 'package/dist/artplayer-plugin-hls-control.js')
assert.equal(hash(bytes), baseline.release.files['package/dist/artplayer-plugin-hls-control.js'])
@@ -55,7 +55,7 @@ test.afterEach(async ({ page }, testInfo) => {
})
async function open(page, testInfo, release, core, plugin = 'candidate', manifest = 'master.m3u8') {
await testInfo.attach('hls-sdk-inputs', { contentType: 'application/json', body: JSON.stringify({ sdk: release, core, plugin, pluginSHA256: hash(plugin === 'candidate' ? candidate : published), candidate: process.env.ARTPLAYER_HLS_ARTIFACT || 'workspace source build', media: Object.fromEntries([...media].map(([name, bytes]) => [name, hash(bytes)])), enableWorker: true }) })
await testInfo.attach('hls-sdk-inputs', { contentType: 'application/json', body: JSON.stringify({ sdk: release, core, plugin, pluginSHA256: hash(plugin === 'candidate' ? candidate : published), selected: plugin === 'candidate' ? candidateProvenance : { kind: 'published', sha256: hash(published) }, media: Object.fromEntries([...media].map(([name, bytes]) => [name, hash(bytes)])), enableWorker: true }) })
await page.route('**/hls-sdk-fixture/**', async (route) => {
const name = new URL(route.request().url()).pathname.split('/').at(-1)
const bytes = media.get(name)
+6 -4
View File
@@ -1,10 +1,10 @@
import fs from 'node:fs'
import assert from 'node:assert/strict'
import process from 'node:process'
import vm from 'node:vm'
import { transform } from 'esbuild'
import { verifyAutoThumbnailContract } from '../../refactor/scripts/auto-thumbnail-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from './load.js'
import { browserCandidate } from './browser-candidate.js'
export async function autoThumbnailHistorical() {
const { baseline, archives, sources } = await verifyAutoThumbnailContract()
@@ -26,9 +26,11 @@ export async function autoThumbnailHistorical() {
}
export async function autoThumbnailCandidate() {
if (process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE === '1')
if (process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE === '1') {
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Installed auto-thumbnail checks cannot use the frozen workspace')
return (await autoThumbnailHistorical()).find(item => item.name === 'frozen-workspace-js')
return { name: 'candidate', code: process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-auto-thumbnail', 'umd') }
}
return { name: 'candidate', ...await browserCandidate('artplayer-plugin-auto-thumbnail', process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT) }
}
// Controlled media events and deferred encodes are not evidence of native decoding.
+14
View File
@@ -8,6 +8,7 @@ import { checkFiles, checkPackages, publishedConsumer } from '../scripts/package
import { removeConsumer, runtimeConsumer } from '../scripts/package-consumer.mjs'
import { emitterContracts } from './contracts/emitter.js'
import { ambilightCandidate } from './helpers/ambilight.js'
import { autoThumbnailCandidate } from './helpers/auto-thumbnail.js'
import { browserCandidate } from './helpers/browser-candidate.js'
import { canvasCandidate } from './helpers/canvas.js'
import { dpipCandidate } from './helpers/dpip.js'
@@ -20,6 +21,7 @@ test('Additional browser packages cannot be mistaken for full release consumer a
test('Explicit installed plugin maps never fall back to source or frozen workspace', async () => {
const keys = ['ARTPLAYER_BROWSER_ARTIFACTS', 'ARTPLAYER_AMBILIGHT_BASELINE', 'ARTPLAYER_CANVAS_BASELINE', 'ARTPLAYER_DPIP_BASELINE', 'ARTPLAYER_DPIP_ARTIFACT', 'ARTPLAYER_VTT_THUMBNAIL_BASELINE', 'ARTPLAYER_VTT_THUMBNAIL_ARTIFACT', 'ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT']
keys.push('ARTPLAYER_AUTO_THUMBNAIL_BASELINE', 'ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT')
const previous = Object.fromEntries(keys.map(key => [key, process.env[key]]))
try {
process.env.ARTPLAYER_BROWSER_ARTIFACTS = path.resolve('refactor/.cache/absent-installed-plugin-map.json')
@@ -31,6 +33,18 @@ test('Explicit installed plugin maps never fall back to source or frozen workspa
delete process.env.ARTPLAYER_VTT_THUMBNAIL_BASELINE
delete process.env.ARTPLAYER_VTT_THUMBNAIL_ARTIFACT
delete process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT
delete process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE
delete process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT
await assert.rejects(autoThumbnailCandidate(), { code: 'ENOENT' })
process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE = '1'
await assert.rejects(autoThumbnailCandidate(), /cannot use the frozen workspace/)
delete process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE
process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT = 'override.js'
await assert.rejects(autoThumbnailCandidate(), /cannot override artplayer-plugin-auto-thumbnail artifact/)
for (const name of ['artplayer-plugin-hls-control', 'artplayer-plugin-dash-control']) {
await assert.rejects(browserCandidate(name), { code: 'ENOENT' })
await assert.rejects(browserCandidate(name, 'override.js'), new RegExp(`cannot override ${name} artifact`))
}
await assert.rejects(ambilightCandidate(), { code: 'ENOENT' })
await assert.rejects(canvasCandidate(), { code: 'ENOENT' })
await assert.rejects(dpipCandidate(), { code: 'ENOENT' })
+2 -2
View File
@@ -9,8 +9,8 @@ step through `ARTPLAYER_BROWSER_ARTIFACTS`. See
browser scope and the remaining package-specific release gates.
Run `yarn test:package` with the pinned Node and Yarn after a frozen install. The
default consumer scope is core and chapter. For Ambilight/Canvas/Document PiP/Ads/Audio Track/VTT Thumbnail/Multiple Subtitles browser work,
run `yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip,artplayer-plugin-ads,artplayer-plugin-audio-track,artplayer-plugin-vtt-thumbnail,artplayer-plugin-multiple-subtitles`.
default consumer scope is core and chapter. For Ambilight/Canvas/Document PiP/Ads/Audio Track/VTT Thumbnail/Multiple Subtitles/HLS Control/DASH Control/Auto Thumbnail browser work,
run `yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip,artplayer-plugin-ads,artplayer-plugin-audio-track,artplayer-plugin-vtt-thumbnail,artplayer-plugin-multiple-subtitles,artplayer-plugin-hls-control,artplayer-plugin-dash-control,artplayer-plugin-auto-thumbnail`.
These four additional packages have reviewed published contracts and use the same
source snapshot/build/pack/offline-install/frozen-reinstall pipeline. Installed
files are copied only after archive hash checks. The report explicitly separates