test(danmuku-mask): [PKG-MASK-02] reproduce historical lifecycle and failure paths

This commit is contained in:
Harvey Zhao committed 2026-09-13 17:43:25 +08:00
1 parent d81b881085
commit ccf3d6ed14
10 files changed
+952 -18

No files matched your search

+259
View File
@@ -0,0 +1,259 @@
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- Historical runner includes fatal isolated Node rejection probes.
import test from 'node:test'
import { danmukuMaskEnvironment, danmukuMaskHistorical, flushMask } from './helpers/danmuku-mask.js'
// These assertions describe frozen historical defects, never candidate acceptance.
const implementations = await danmukuMaskHistorical()
const detailed = implementations.filter(item => ['frozen-source', 'published-1.1.0-main'].includes(item.name))
test('Historical SDK probes replace exactly five call sites and retain verified input identities', () => {
const expected = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-mask-failures-validation.json', 'utf8'))
const actual = implementations.map(({ code: _code, ...identity }) => identity)
assert.deepEqual(actual, expected.inputs, 'SDK seam transformations must remain exactly reproducible')
assert.equal(implementations.length, 7)
for (const item of implementations) {
assert.match(item.inputSha256, /^[a-f0-9]{64}$/u)
assert.match(item.transformedSha256, /^[a-f0-9]{64}$/u)
if (!item.source) {
assert.match(item.archiveSha256, /^[a-f0-9]{64}$/u)
assert.match(item.probeSha256, /^[a-f0-9]{64}$/u)
assert.deepEqual(item.counts, { setBackend: 2, createSegmenter: 1, toBinaryMask: 1, drawMask: 1 })
assert.equal(item.replacements.length, 5)
}
}
})
for (const implementation of implementations) {
test(`${implementation.name}: historical stop during initialization is undone by late initialization`, async () => {
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
const pending = env.result.start()
await flushMask()
assert.equal(env.initGates.length, 1)
env.result.stop()
assert.equal(env.style.maskImage, 'none')
env.initGates[0].resolve()
await pending
await flushMask()
assert.equal(env.draws.length, 1)
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
assert.equal(env.frames.size, 1)
env.result.stop()
})
test(`${implementation.name}: historical inference completion after destroy writes a mask and revives RAF`, async () => {
const env = danmukuMaskEnvironment(implementation, { pendingSegment: true })
await env.result.start()
await flushMask()
assert.equal(env.segmentGates.length, 1)
env.destroy()
env.segmentGates[0].resolve([{}])
await flushMask()
assert.equal(env.draws.length, 1)
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
assert.equal(env.frames.size, 1)
assert.equal(env.created[0].disposed, 0)
assert.deepEqual([...env.listeners].map(([name, callbacks]) => [name, callbacks.size]), [['ready', 1], ['destroy', 1]])
env.result.stop()
})
test(`${implementation.name}: historical repeated start leaves a second RAF chain after stop`, async () => {
const env = danmukuMaskEnvironment(implementation)
await env.result.start()
await flushMask()
await env.result.start()
await flushMask()
assert.equal(env.created.length, 1)
assert.equal(env.frames.size, 2)
env.result.stop()
assert.equal(env.frames.size, 1)
await env.frame()
assert.equal(env.frames.size, 1)
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
env.result.stop()
})
}
for (const implementation of detailed) {
for (const action of ['stop', 'destroy']) {
test(`${implementation.name}: historical ${action} during initialization does not dispose the late model`, async () => {
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
const pending = env.result.start()
await flushMask()
if (action === 'stop')
env.result.stop()
else env.destroy()
env.initGates[0].resolve()
await pending
await flushMask()
assert.equal(env.created[0].disposed, 0)
assert.equal(env.frames.size, 1)
assert.equal(env.style.maskMode, 'alpha')
env.result.stop()
})
}
test(`${implementation.name}: historical stop during inference cannot prevent its pending write`, async () => {
const env = danmukuMaskEnvironment(implementation, { pendingSegment: true })
await env.result.start()
env.result.stop()
assert.deepEqual(env.writes, ['none'])
env.segmentGates[0].resolve([{}])
await flushMask()
assert.deepEqual(env.writes, ['none', 'url(data:image/png;base64,historical-mask)'])
assert.equal(env.frames.size, 1)
env.result.stop()
})
test(`${implementation.name}: historical concurrent starts initialize two models and lose disposal ownership`, async () => {
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
const first = env.result.start()
const second = env.result.start()
await flushMask()
assert.equal(env.created.length, 2)
for (const gate of env.initGates) gate.resolve()
await Promise.all([first, second])
await flushMask()
assert.equal(env.frames.size, 2)
env.destroy()
assert.equal(env.frames.size, 1)
assert.deepEqual(env.created.map(instance => instance.disposed), [0, 0])
})
test(`${implementation.name}: historical inference rejection logs once per frame and keeps retrying`, async () => {
const error = new Error('controlled inference failure')
const env = danmukuMaskEnvironment(implementation, { segmentError: error })
await env.result.start()
await flushMask()
assert.equal(env.frames.size, 1)
assert.equal(env.logs.length, 1)
assert.deepEqual(env.logs[0], ['error', 'Error in segmentBody:', error])
assert.equal(env.draws.length, 0)
await env.frame()
assert.equal(env.logs.length, 2)
assert.equal(env.frames.size, 1)
env.settings.segmentError = null
await env.frame()
assert.equal(env.draws.length, 1)
env.result.stop()
})
test(`${implementation.name}: historical fulfilled false WebGL selection does not trigger CPU fallback`, async () => {
const env = danmukuMaskEnvironment(implementation, { backendResult: { webgl: false } })
await env.result.start()
await flushMask()
assert.deepEqual(env.backends, ['webgl'])
assert.equal(env.created.length, 1)
assert.equal(env.logs.length, 0)
env.result.stop()
})
test(`${implementation.name}: historical rejected WebGL selection tries CPU and ignores its fulfilled boolean`, async () => {
for (const cpu of [true, false]) {
const env = danmukuMaskEnvironment(implementation, { backendError: { webgl: new Error('no WebGL') }, backendResult: { cpu } })
await env.result.start()
await flushMask()
assert.deepEqual(env.backends, ['webgl', 'cpu'])
assert.deepEqual(env.logs, [['warn', 'WebGL backend not available, falling back to CPU', 'no WebGL']])
assert.equal(env.created.length, 1)
env.result.stop()
}
})
test(`${implementation.name}: historical CPU rejection rejects explicit start before creating a model`, async () => {
const error = new Error('controlled CPU failure')
const env = danmukuMaskEnvironment(implementation, { backendError: { webgl: new Error('no WebGL'), cpu: error } })
await assert.rejects(env.result.start(), failure => failure === error)
assert.deepEqual(env.backends, ['webgl', 'cpu'])
assert.equal(env.created.length, 0)
assert.equal(env.frames.size, 0)
})
test(`${implementation.name}: historical model initialization rejection resolves start and polls without a model`, async () => {
const error = new Error('controlled model init failure')
const env = danmukuMaskEnvironment(implementation, { initError: error })
assert.equal(await env.result.start(), undefined)
assert.deepEqual(env.logs, [['error', 'Error initializing segmenter:', error]])
assert.equal(env.frames.size, 1)
await env.frame()
assert.equal(env.created.length, 1)
assert.equal(env.inferences.length, 0)
env.result.stop()
})
test(`${implementation.name}: historical missing danmuku rejects start after allocating the model and makes stop throw`, async () => {
const env = danmukuMaskEnvironment(implementation, { noDanmuku: true })
await assert.rejects(env.result.start(), error => error.name === 'TypeError' && error.message.includes('style'))
assert.throws(() => env.result.stop(), error => error.name === 'TypeError' && error.message.includes('style'))
assert.equal(env.created.length, 1)
assert.equal(env.created[0].disposed, 0)
})
test(`${implementation.name}: historical unavailable 2D context logs read failures and keeps the frame loop`, async () => {
const env = danmukuMaskEnvironment(implementation, { noContext: true })
await env.result.start()
await flushMask()
assert.equal(env.logs.length, 1)
assert.equal(env.logs[0][2].name, 'TypeError')
assert.match(env.logs[0][2].message, /getImageData/u)
assert.equal(env.frames.size, 1)
assert.equal(env.writes.length, 0)
env.result.stop()
})
test(`${implementation.name}: historical canvas allocation failure rejects start but leaves the initialized model owned`, async () => {
const error = new Error('controlled canvas allocation failure')
const env = danmukuMaskEnvironment(implementation, { canvasError: error })
await assert.rejects(env.result.start(), failure => failure === error)
assert.equal(env.created.length, 1)
assert.equal(env.created[0].disposed, 0)
assert.equal(env.frames.size, 0)
env.destroy()
assert.equal(env.created[0].disposed, 0)
})
test(`${implementation.name}: historical canvas read security failure is logged and retried without clearing the previous mask`, async () => {
const error = new Error('controlled canvas taint')
error.name = 'SecurityError'
const env = danmukuMaskEnvironment(implementation, { readError: error })
await env.result.start()
await flushMask()
assert.equal(env.logs[0][2], error)
assert.equal(env.style.maskImage, 'url(prior-mask)')
await env.frame()
assert.equal(env.logs.length, 2)
env.result.stop()
})
test(`${implementation.name}: historical zero RAF id survives stop and retained ready listeners restart after destroy`, async () => {
const env = danmukuMaskEnvironment(implementation, { firstFrame: 0, paused: true })
await env.result.start()
assert.deepEqual([...env.frames.keys()], [0])
env.destroy()
assert.deepEqual([...env.frames.keys()], [0])
assert.deepEqual(env.cancelled, [])
env.emit('ready')
await flushMask()
assert.equal(env.frames.size, 2)
assert.equal(env.created[0].disposed, 0)
})
for (const scenario of ['ready-backend', 'missing-video']) {
test(`${implementation.name}: historical ${scenario} unhandled rejection is observed only in a fatal isolated child`, () => {
const script = `import { danmukuMaskHistorical, danmukuMaskEnvironment, flushMask } from './test/helpers/danmuku-mask.js';
const implementation = (await danmukuMaskHistorical()).find(item => item.name === ${JSON.stringify(implementation.name)});
const settings = ${scenario === 'ready-backend' ? '{backendError: {webgl: new Error("no WebGL"), cpu: new Error("MASK02_READY_BACKEND_REJECTION")}}' : '{noVideo: true}'};
const env = danmukuMaskEnvironment(implementation, settings);
${scenario === 'ready-backend' ? 'env.emit("ready");' : 'await env.result.start();'}
await flushMask();`
const child = spawnSync(process.execPath, ['--unhandled-rejections=strict', '--input-type=module', '-e', script], { encoding: 'utf8', timeout: 15000 })
assert.equal(child.error, undefined)
assert.equal(child.status, 1)
assert.match(child.stderr, scenario === 'ready-backend' ? /MASK02_READY_BACKEND_REJECTION/u : /Cannot read properties of undefined \(reading 'paused'\)/u)
assert.doesNotMatch(child.stderr, /AssertionError|Model network access is forbidden/u)
})
}
}
+239
View File
@@ -0,0 +1,239 @@
import assert from 'node:assert/strict'
import vm from 'node:vm'
import { transform } from 'esbuild'
import ts from 'typescript'
import { verifyDanmukuMaskContract } from '../../refactor/scripts/danmuku-mask-contract.mjs'
import { hash, readMember } from '../../refactor/scripts/releases.mjs'
const sourceFile = 'packages/artplayer-plugin-danmuku-mask/src/index.js'
let historical
export function danmukuMaskHistorical() {
historical ||= loadHistorical()
return historical
}
function evaluate(code, additions = {}) {
const module = { exports: {} }
const context = {
module,
exports: module.exports,
console: { log() {}, warn() {}, error() {} },
fetch() { throw new Error('Model network access is forbidden in historical probes') },
...additions,
}
context.global = context
context.self = context
vm.runInNewContext(code, context, { timeout: 5000 })
return module.exports.default || module.exports
}
function instrument(code, factory, label) {
const text = factory.toString()
const start = code.indexOf(text)
assert(start >= 0 && !code.includes(text, start + 1), 'Exported factory must map uniquely to the evaluated bytes')
const prefix = 'const factory = ('
const parsed = ts.createSourceFile('factory.js', `${prefix}${text});`, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
assert.equal(parsed.parseDiagnostics.length, 0)
const replacements = []
const counts = { setBackend: 0, createSegmenter: 0, toBinaryMask: 0, drawMask: 0 }
const configs = new Set()
function findConfigs(node) {
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
const properties = node.initializer.properties.map(property => property.name?.getText(parsed))
if (properties.includes('runtime') && properties.includes('modelType'))
configs.add(node.name.text)
}
ts.forEachChild(node, findConfigs)
}
findConfigs(parsed)
function visit(node) {
if (ts.isCallExpression(node)) {
const args = node.arguments
const callee = node.expression.getText(parsed)
let target
if (args.length === 1 && ts.isStringLiteral(args[0]) && ['webgl', 'cpu'].includes(args[0].text))
target = 'setBackend'
else if (callee === 'createSegmenter' || callee.endsWith('.createSegmenter') || callee.includes('is not a supported model name.') || (args.length === 2 && ts.isIdentifier(args[1]) && configs.has(args[1].text)))
target = 'createSegmenter'
else if (callee === 'toBinaryMask' || callee.endsWith('.toBinaryMask') || callee.includes('mask.toImageData') || (args.length === 5 && args[3].getText(parsed).endsWith('.drawContour') && args[4].getText(parsed).endsWith('.foregroundThreshold')))
target = 'toBinaryMask'
else if (args.length === 5 && args[3].getText(parsed).endsWith('.opacity') && args[4].getText(parsed).endsWith('.maskBlurAmount'))
target = 'drawMask'
if (target) {
counts[target]++
replacements.push({ start: start + node.expression.getStart(parsed) - prefix.length, end: start + node.expression.end - prefix.length, target, original: callee })
// Inline SDK bodies are replaced as one expression, never traversed as owned lifecycle code.
return
}
}
ts.forEachChild(node, visit)
}
visit(parsed)
assert.deepEqual(counts, { setBackend: 2, createSegmenter: 1, toBinaryMask: 1, drawMask: 1 }, label)
let result = code
for (const replacement of [...replacements].sort((a, b) => b.start - a.start))
result = `${result.slice(0, replacement.start)}globalThis.__maskProbe.${replacement.target}${result.slice(replacement.end)}`
return { code: result, counts, replacements: replacements.map(({ start, end, target, original }) => ({ start, end, target, originalSha256: hash(original) })) }
}
async function loadHistorical() {
const contract = await verifyDanmukuMaskContract()
const implementations = []
const source = contract.sources.get(sourceFile)
const sourceCode = (await transform(source, { format: 'cjs', target: 'es2020' })).code
implementations.push({ name: 'frozen-source', code: sourceCode, inputSha256: hash(source), transformedSha256: hash(sourceCode), source: true, replacements: [] })
for (const release of [contract.baseline.release, ...contract.baseline.previous]) {
const archive = contract.archives.get(release.version)
for (const entry of ['main', 'legacy', 'module']) {
const member = `package/${release.manifest[entry].replace(/^\.\//u, '')}`
const input = readMember(archive, member).toString()
const code = entry === 'module' ? (await transform(input, { format: 'cjs', target: 'es2020' })).code : input
const modified = instrument(code, evaluate(code), `${release.version}/${entry}`)
implementations.push({ name: `published-${release.version}-${entry}`, archiveSha256: release.sha256, member, inputSha256: hash(input), transformedSha256: hash(code), probeSha256: hash(modified.code), ...modified })
}
}
return implementations
}
export const flushMask = () => new Promise(resolve => setImmediate(resolve))
function deferred() {
let resolve
let reject
const promise = new Promise((accept, fail) => {
resolve = accept
reject = fail
})
return { promise, resolve, reject }
}
export function danmukuMaskEnvironment(implementation, settings = {}) {
const env = {
frames: new Map(),
listeners: new Map(),
logs: [],
backends: [],
created: [],
inferences: [],
masks: [],
draws: [],
initGates: [],
segmentGates: [],
writes: [],
cancelled: [],
settings,
}
let nextFrame = settings.firstFrame ?? 1
const style = { maskMode: 'luminance', maskImage: 'url(prior-mask)' }
Object.defineProperty(style, 'maskImage', {
configurable: true,
enumerable: true,
get: () => env.writes.at(-1) ?? 'url(prior-mask)',
set: value => env.writes.push(value),
})
const context = {
getImageData() {
if (settings.readError)
throw settings.readError
return { data: new Uint8ClampedArray([255, 255, 255, 255]) }
},
putImageData() {},
}
env.canvas = {
width: 0,
height: 0,
getContext: () => settings.noContext ? null : context,
toDataURL: () => 'data:image/png;base64,historical-mask',
}
env.video = settings.noVideo ? undefined : { paused: settings.paused ?? false, ended: false, videoWidth: 2, videoHeight: 1 }
env.style = style
env.art = {
isDestroy: false,
template: { $video: env.video, $danmuku: settings.noDanmuku ? undefined : { style } },
on(name, callback) {
if (!env.listeners.has(name))
env.listeners.set(name, new Set())
env.listeners.get(name).add(callback)
},
off(name, callback) { env.listeners.get(name)?.delete(callback) },
}
const probe = {
SupportedModels: { MediaPipeSelfieSegmentation: 'selfie' },
async setBackend(name) {
env.backends.push(name)
if (settings.backendError?.[name])
throw settings.backendError[name]
return settings.backendResult?.[name] ?? true
},
async createSegmenter(model, config) {
const instance = {
model,
config,
disposed: 0,
dispose() { instance.disposed++ },
async segmentPeople(video) {
env.inferences.push({ instance, video })
if (settings.segmentError)
throw settings.segmentError
if (settings.pendingSegment) {
const gate = deferred()
env.segmentGates.push(gate)
return gate.promise
}
return settings.empty ? [] : [{}]
},
}
env.created.push(instance)
if (settings.initError)
throw settings.initError
if (settings.pendingInit) {
const gate = deferred()
env.initGates.push(gate)
await gate.promise
}
return instance
},
async toBinaryMask(...args) {
env.masks.push(args)
return {}
},
async drawMask(...args) { env.draws.push(args) },
}
const factory = evaluate(implementation.code, {
__maskProbe: probe,
require(name) {
assert(['@tensorflow-models/body-segmentation', '@tensorflow/tfjs-core', '@tensorflow/tfjs-backend-webgl', '@tensorflow/tfjs-backend-cpu'].includes(name))
return probe
},
console: { log() {}, warn: (...args) => env.logs.push(['warn', ...args]), error: (...args) => env.logs.push(['error', ...args]) },
document: { createElement(tag) {
assert.equal(tag, 'canvas')
if (settings.canvasError)
throw settings.canvasError
return env.canvas
} },
requestAnimationFrame(callback) {
const id = nextFrame++
env.frames.set(id, callback)
return id
},
cancelAnimationFrame(id) {
env.cancelled.push(id)
env.frames.delete(id)
},
})
env.result = factory(settings.option)(env.art)
env.emit = (name) => {
for (const callback of [...(env.listeners.get(name) || [])]) callback()
}
env.destroy = () => {
env.art.isDestroy = true
env.emit('destroy')
}
env.frame = async () => {
const frames = [...env.frames.values()]
env.frames.clear()
for (const callback of frames) await callback()
await flushMask()
}
return env
}