test(ambilight): [PKG-AMBILIGHT-01] freeze actual release and invocation contracts

This commit is contained in:
Harvey Zhao committed 2026-09-12 10:54:28 +08:00
1 parent c57c7e4b16
commit 06032a6906
17 files changed
+638 -7

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
export async function verifyAmbilightContract() {
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/ambilight-release.json'), 'utf8'))
const archives = new Map()
for (const release of [baseline.release, ...baseline.previous]) {
const archive = await ensureArchive(release)
assert.deepEqual(archiveFiles(archive), Object.keys(release.files).sort())
for (const [file, expected] of Object.entries(release.files))
assert.equal(hash(readMember(archive, file)), expected, `${release.version}/${file}`)
assert.deepEqual(JSON.parse(readMember(archive, 'package/package.json')), release.manifest)
for (const field of ['main', 'module', 'types', 'legacy']) {
const entry = release.manifest[field]
if (entry)
assert(Object.hasOwn(release.files, `package/${entry.replace(/^\.\//, '')}`))
}
if (release.historicalCore) {
const core = release.historicalCore
const content = execFileSync('git', ['show', `${core.commit}:${core.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
assert.equal(hash(content), core.sha256LF)
assert.equal(JSON.parse(content).version, core.version)
}
archives.set(release.version, archive)
}
const sources = new Map()
for (const [file, expected] of Object.entries(baseline.source)) {
const content = execFileSync('git', ['show', `${baseline.sourceCommit}:${file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
assert.equal(hash(content), expected, `Ambilight frozen source: ${file}`)
sources.set(file, content)
}
return { baseline, archives, sources }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const { baseline } = await verifyAmbilightContract()
console.log(`Ambilight verified: ${[baseline.release, ...baseline.previous].map(release => release.version).join(', ')}; 12 members, frozen workspace and Git core associations`)
}
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Freeze actual published plugin contracts before migration.
import test from 'node:test'
import vm from 'node:vm'
import { ambilightEnvironment, ambilightHistorical } from '../../test/helpers/ambilight.js'
import { verifyAmbilightContract } from './ambilight-contract.mjs'
import { readMember } from './releases.mjs'
const contract = await verifyAmbilightContract()
test('Ambilight archives, manifests and source snapshots retain their exact identities', () => {
assert.equal(contract.baseline.release.version, '1.1.0')
assert.equal(contract.baseline.previous[0].version, '1.0.0')
assert.equal([...contract.archives].length, 2)
assert.equal(contract.baseline.release.historicalCore.version, '5.3.1')
assert.equal(contract.baseline.previous[0].historicalCore.version, '5.1.7')
})
test('Ambilight published CommonJS and script globals preserve their distinct export shapes', () => {
for (const release of [contract.baseline.release, ...contract.baseline.previous]) {
for (const field of ['main', 'legacy']) {
const code = readMember(contract.archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString()
const module = { exports: {} }
const window = {}
vm.runInNewContext(code, { module, exports: module.exports, window }, { timeout: 5000 })
assert.equal(typeof module.exports, release.version === '1.0.0' ? 'object' : 'function')
assert.equal(typeof (module.exports.default || module.exports)(), 'function')
const global = { window: {} }
vm.runInNewContext(code, global, { timeout: 5000 })
assert.equal(typeof (global.artplayerPluginAmbilight || global.window.artplayerPluginAmbilight), 'function')
}
}
})
test('Ambilight published declarations retain the required options argument and ignored zIndex field', () => {
const old = readMember(contract.archives.get('1.0.0'), 'package/types/artplayer-plugin-ambilight.d.ts').toString()
const current = readMember(contract.archives.get('1.1.0'), 'package/types/artplayer-plugin-ambilight.d.ts').toString()
assert.match(old, /export = artplayerPluginAmbilight/)
assert.match(current, /export default artplayerPluginAmbilight/)
for (const source of [old, current]) {
assert.match(source, /\(option: Option\)/)
assert.match(source, /zIndex\??: number/)
}
assert.match(current, /blur\?: string/)
assert.match(old, /blur: string/)
})
test('Ambilight 1.1.0 native ESM import exposes the default factory', async () => {
const source = readMember(contract.archives.get('1.1.0'), 'package/dist/artplayer-plugin-ambilight.mjs').toString()
const namespace = await import(`data:text/javascript,${encodeURIComponent(source)}`)
assert.deepEqual(Object.keys(namespace), ['default'])
assert.equal(typeof namespace.default, 'function')
assert.equal(typeof namespace.default(), 'function')
})
for (const implementation of await ambilightHistorical()) {
test(`Ambilight ${implementation.name}: defaults, option-read timing, styling, grid sampling and public returns`, async () => {
const env = await ambilightEnvironment(implementation)
const option = { opacity: 0.1, zIndex: -99 }
const attach = env.factory(option)
option.opacity = 0.7
const result = attach(env.art)
const element = env.parent.children[0]
assert.equal(element.className, 'artplayer-plugin-ambilight')
assert.equal(element.children.length, 9)
assert.equal(element.style.zIndex, 9, 'Historical zIndex option is ignored')
assert.equal(element.children[0].style.opacity, 0.7)
assert.equal(element.children[0].style.filter, 'blur(50px)')
assert.equal(element.children[0].style.transition, 'background-color 0.3s ease')
assert.deepEqual(Object.keys(result), ['name', 'start', 'stop'])
assert.equal(result.name, 'artplayerPluginAmbilight')
assert.equal(result.start(), undefined)
assert.equal(env.frames.size, 1)
assert.equal(env.draws.length, 0)
env.art.playing = true
env.frame(100)
assert.equal(env.draws.length, 9)
assert.deepEqual([...env.draws[0].slice(1)], [0, 0, 100, 50, 0, 0, 1, 1])
assert.deepEqual([...env.draws[8].slice(1)], [200, 100, 100, 50, 0, 0, 1, 1])
assert.equal(element.children[8].style.backgroundColor, 'rgb(9, 20, 30)')
assert.equal(result.stop(), undefined)
assert.equal(env.frames.size, 0)
assert.equal(typeof env.factory(), 'function', 'Runtime accepts omitted options despite the old declaration')
})
}
+1 -1
View File
@@ -44,7 +44,7 @@ function fixture(t) {
test('Actual contract model assigns all 264 family rows and separates exact published versions from workspace snapshots', () => {
assert.equal(actual.policy.packages.length, 22)
assert.equal(actual.families.length, 12)
assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 7)
assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 9)
assert.equal(actual.policy.versions.filter(version => version.kind === 'workspace-baseline').length, 22)
assert.equal(actual.policy.cases.length, 10)
const markdown = renderContracts(actual)