test(ambilight): [PKG-AMBILIGHT-05] checkpoint installed canvas combinations

This commit is contained in:
Harvey Zhao committed 2026-09-14 17:50:38 +08:00
1 parent e7f31b6fc8
commit 765ddb816f
19 files changed
+1032 -85

No files matched your search

+8 -1
View File
@@ -135,7 +135,14 @@ jobs:
run: yarn test:react-consumer 2>&1 | tee refactor/.cache/ci/react-consumer.log
- name: Verify Vue installed consumers
run: yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log
- name: Run all three engines
- name: Prepare installed browser plugin packages
# Node/framework consumers above retain their two-package fixture scope.
# These extra installed packages are required by the browser helpers.
# Other ecosystem installation/SDK gates remain tracked by CI-01.
run: |
yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas 2>&1 | tee refactor/.cache/ci/browser-packages.log
node --input-type=module -e 'import fs from "node:fs"; const { output } = JSON.parse(fs.readFileSync("refactor/.cache/packages/latest.json")); fs.appendFileSync(process.env.GITHUB_ENV, `ARTPLAYER_BROWSER_ARTIFACTS=${output}/browser-artifacts.json\n`);'
- name: Run installed browser checks
run: yarn test:browser 2>&1 | tee refactor/.cache/ci/browser.log
- name: Verify iframe cached history and interrupted navigation
run: yarn test:iframe-history 2>&1 | tee refactor/.cache/ci/iframe-history.log
@@ -89,5 +89,15 @@ resized output geometry. Browser tests play real video with the actual workspace
proxy and paint a uniform nine-color palette through its public post-processing
callback; getImageData is native. Actual core 5.1.7 has no proxy option and uses
VIDEO, so its case checks that capability boundary and native playback. Only 5.4.0
and candidate cases validate the canvas proxy combination. These tests do not
establish that the unrefactored proxy's own async draw loop is terminally safe.
and candidate cases validate the canvas proxy combination. The combination now
observes both native RAF owners and verifies that no proxy callback runs after
destroy. Canvas-specific tests additionally cover paused seeks, source switching,
native pixels and destruction inside the post-processing callback.
For installed-package verification run
`yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas`,
set `ARTPLAYER_BROWSER_ARTIFACTS` to its `browser-artifacts.json`, then run the
Ambilight lifecycle/proxy and Canvas lifecycle browser suites. Helpers verify the
installed hashes and current package/build inputs; an explicit artifact map never
falls back to a source build. This supplements source tests. It does not replace
physical mobile devices, npm entrypoint/type matrices or the remaining 05/06 gates.
+14
View File
@@ -1,5 +1,19 @@
# Ambilight 测试维护
## 已安装产物检查点
`yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas`
生成包含两包的实际隔离安装及 browser-artifacts.json。把该路径传给
ARTPLAYER_BROWSER_ARTIFACTS 后,Ambilight/Canvas helper 校验当前源码与构建快照
及安装文件摘要,不回退内存构建。原生生命周期/代理组合测试可直接复用;同目录
的 legacy map 必须指向 tarball 中已登记的 legacy 文件。
现代45项、增强组合销毁断言后的现代9项、legacy45项三引擎全部通过;49+57+10项
专项/工程检查通过。两个原生 RAF owner 的销毁后归零和 proxy 绘制停止都有断言。
见[安装证据](baselines/ambilight-installed-validation.json)与
[检查点记录](changes/2026-09-14-PKG-AMBILIGHT-05-installed-combination.md)。
05仍doing,物理移动设备与完整分发验收未完成;以下早期记录保留其当时范围。
先读 [发布契约](baselines/ambilight-contract.md)。ambilight-release.json冻结两个真实
发布及工作区Git来源,verifyAmbilightContract逐成员核对,不随候选更新历史哈希。
@@ -0,0 +1,732 @@
{
"schemaVersion": 1,
"task": "PKG-AMBILIGHT-05",
"status": "checkpoint",
"environment": {
"node": "24.21.0",
"yarn": "1.22.22",
"browsers": {
"chromium": "153.0.8010.12",
"firefox": "155.0",
"webkit": "26.6"
},
"physicalDevices": false,
"remoteCI": false
},
"packageRuns": [
{
"path": "refactor/.cache/packages/run-IstjHZ",
"reportSha256": "ac1d90ef3fe400d81bfc3dda9ee3098c611d6bf0f8a51c59f233210b40459568"
},
{
"path": "refactor/.cache/packages/run-IIEOt4",
"reportSha256": "6efd5011592a1f2d81b785934c5365f6bb3cd4d05568a40c8465e9255b2df2a1"
}
],
"packages": [
{
"name": "artplayer",
"archive": "artplayer.tgz",
"sha256": "a4e53c0c08850e02585c025581af30c8a91f3c8871471375d2228ffc246fd454",
"changedSinceInitialRun": [],
"runtime": {
"package/dist/artplayer.js": "9148a369636f814b03e6a1ed2a7db222a31b159f93e8a94d52c5a9dde2dfe6a6",
"package/dist/artplayer.legacy.js": "9bde35a6d64234cae15300e37b03204c075ef26126058f48bb3bf97d6e002449",
"package/dist/artplayer.mjs": "265bca0ad3f8373acc84333c19cdded0cbd6b309ede9e51c0bec35685c77eebd",
"package/dist/i18n/ar.js": "bd0bef65f4f7dc3f8ed562904cc6fa9071ff4537fb64c1e0fc3d554141bcd3cb",
"package/dist/i18n/ar.mjs": "59310b7bbeea83bbb7d73aadc5ca82a6ec269c3530d19aa741c94d5c64fc5da8",
"package/dist/i18n/cs.js": "6e2c93daba24c2e48dfa206bcf4f2f2a8a02713b856e404d2e4e4ecd8309ca2f",
"package/dist/i18n/cs.mjs": "c73fba18797d62d7b2581e67fe7340c35fe84510b2250ff1ecaad253d9cabbac",
"package/dist/i18n/es.js": "f4133ac6ea06b69a474b33c4a1809a4066e2376bb9464a3f3aa8330f01f1944a",
"package/dist/i18n/es.mjs": "1390bca3def2be115e811de337df0a0c3f63998e51359f283023246245c552a7",
"package/dist/i18n/fa.js": "8d3ef2e0f0cc0ea480c2167b6fcb87cdf444e90759c3519e8daa139fec576da4",
"package/dist/i18n/fa.mjs": "ab441e8a2d79043b29ceca87c783bf06874bb9d3a149c1604bc167eb481be2a9",
"package/dist/i18n/fr.js": "89dc4194a6c0a962b9ce756b52c90401c1acf3b159f265f3bc0d7d6d7d987f03",
"package/dist/i18n/fr.mjs": "99491878430fb7b6f2b7bf93da0588a831704b412e01116f23ba1d9d45863816",
"package/dist/i18n/id.js": "4c6dc86db741bcc2857df55a9d037e82e91d375fa6fd22e0562809ff621b92db",
"package/dist/i18n/id.mjs": "383e5803a6b6686c6de4e867770241de75b6f517dcca2bdedc6f2004bf6e1fb3",
"package/dist/i18n/pl.js": "3a38828b1c3d719c0aca9906cf6a46c23555a8411f8d57645f0e0ce8147dc664",
"package/dist/i18n/pl.mjs": "079ed96732979035297af387c64a7b99519b5a43107fb86735a654ea4fe8e9b5",
"package/dist/i18n/ru.js": "62fac9dddb302eb16382e6e9301fc4b2c66f5f69d4a358a26b67026a5fa4e787",
"package/dist/i18n/ru.mjs": "a893aeaee1d79409db72e324fbff200e3864f33ed168f04d91d25f7cf2efa765",
"package/dist/i18n/tr.js": "1492c36b069a7d73db4f3913bd18085faff7afd8b3c158138fe264e31600e38c",
"package/dist/i18n/tr.mjs": "a90478cac078836b76c2a98261907610fee07f3e4b28671b8e7dbf533778f918",
"package/dist/i18n/vi.js": "c7216ef7e608a79b7e2c479a07bf310d4ab79577947ac4a62d676faaced0e9cf",
"package/dist/i18n/vi.mjs": "66270fdfa0b5da396322d1769169b7df37a030ba15ac18eb30bd87b3af8074ec",
"package/dist/i18n/zh-tw.js": "11c49857d6d40a5ffd224ffa59c9026e71621b345f63c5a086326db80a22371a",
"package/dist/i18n/zh-tw.mjs": "a38e630cb96de96c15d824686aec750e25cf6c8ada01285a292442a81566314f"
}
},
{
"name": "artplayer-plugin-chapter",
"archive": "artplayer-plugin-chapter.tgz",
"sha256": "9c0a399b4b48db3f30bef69bfedd24245e9328f39151b1e1f670c4d357f4fb05",
"changedSinceInitialRun": [],
"runtime": {
"package/dist/artplayer-plugin-chapter.js": "179c3ea90ddc85da27b46f000110e32d464dfa5c8c394e98316dc1b75d6f2a72",
"package/dist/artplayer-plugin-chapter.legacy.js": "dade919ed044cbf46ac16e505f538f86a86edccf177da2e14d79d111559d4bef",
"package/dist/artplayer-plugin-chapter.mjs": "754fbcfc60add74d6b6ad3bc8db253df3eb14ace13ac8a5439be0592bd5512d0"
}
},
{
"name": "artplayer-plugin-ambilight",
"archive": "artplayer-plugin-ambilight.tgz",
"sha256": "a72d5805ca34ebcad615cfd155bafdbe8ddbdeb03e10a154142f8927be80f3b7",
"changedSinceInitialRun": [
"package/ARCHITECTURE.md"
],
"runtime": {
"package/dist/artplayer-plugin-ambilight.js": "21ee5227f26cfff6e82ee6932f6eafc9741fa3be0ec2f5252a47b53ceea14599",
"package/dist/artplayer-plugin-ambilight.legacy.js": "c3ed1a54e44a0202811d70378cb19edf87b1ebd010d05fe96ebe64d0b4301d5d",
"package/dist/artplayer-plugin-ambilight.mjs": "ef126959819c4973da27dbb1e60ccccc54386c23f8c7750ce1ba4d38f306aaaa"
}
},
{
"name": "artplayer-proxy-canvas",
"archive": "artplayer-proxy-canvas.tgz",
"sha256": "09936e35e18188cab011f933d8063cac5e0f491a1960353ca9d5c65b9b42b65d",
"changedSinceInitialRun": [],
"runtime": {
"package/dist/artplayer-proxy-canvas.js": "a901e5c5fc00a64a6c2ae9d4b2b846950412710ec9672d30640d40987d4f9e73",
"package/dist/artplayer-proxy-canvas.legacy.js": "052263fdd7ab657a0183d874a706c2cf82527233c5e60b58158caa0677718b97",
"package/dist/artplayer-proxy-canvas.mjs": "66ca7271f52545a8796f563f2d5ca04581bed8d60d218463923325acf5b73ccf"
}
}
],
"runtimeTypeScope": [
"artplayer",
"artplayer-plugin-chapter"
],
"additionalBrowserPackages": [
"artplayer-plugin-ambilight",
"artplayer-proxy-canvas"
],
"runtimeChecks": 36,
"legacyTypeModes": 5,
"preciseTypeModes": 8,
"sourceInputs": [
{
"path": "scripts/package-check.mjs",
"sha256": "d55266708fd2f6d23743daaf0a54c6543b8c2987503eb1fd54a05bbb11420a1a"
},
{
"path": "scripts/installed-artifacts.mjs",
"sha256": "0b1c85a48117551c7a3f06f5df8426fea19e302f874dd6ca2e44418dfed92b3b"
},
{
"path": "scripts/performance-artifacts.mjs",
"sha256": "4e9b9b3149b710eed20a1513f8bc1e13772ebcf14d84424b780addccdace8519"
},
{
"path": "test/helpers/ambilight.js",
"sha256": "56cd57e6cc52a4ef233492c45d9a51d16713e9fb40de123df1619395d0fb1723"
},
{
"path": "test/helpers/canvas.js",
"sha256": "fb1e86db48e098aa5cb0469241f814ddfaed2c35151f58028bcda19d8feed14a"
},
{
"path": "test/browser/ambilight-proxy.spec.js",
"sha256": "9ef43bb84ef4440bfa4257ec3998018a71e857d7bcb61c415923a8c0b2291e20"
},
{
"path": "test/browser/ambilight-lifecycle.spec.js",
"sha256": "321693a24d33f0c7aee02fd4137b55175386562a645ba138e15c3a51d6348d2c"
},
{
"path": "test/browser/canvas-lifecycle.spec.js",
"sha256": "5c50ad4f11d1df91c4247dc24c07edd2c9540e128e00fd07ee096406174512a7"
},
{
"path": ".github/workflows/nodejs.yml",
"sha256": "5e7d3cbaee3d96e9088b53205657614d8386a528a1a2fd9cc0687c90b4e7dfd3"
},
{
"path": "refactor/scripts/ci-workflow.mjs",
"sha256": "35148f6c9b75f3a4ee0fd20fd13211acddd45ca2f187f1a5c2581ce903796521"
},
{
"path": "refactor/scripts/ci-workflow.test.mjs",
"sha256": "923718fcc4897088ed92554cf4db5b95b82b8e1099db9c8b897806a87b08095a"
},
{
"path": "test/package-check.test.js",
"sha256": "e4e2010a00c6b6319ad36340a656e9d5f8821d76a30e87df6de7375a54cc2f35"
},
{
"path": "test/performance-report.test.js",
"sha256": "0304a90a07a86b2f71bde01ae28bf535ec7b9922cd463e16e066cea47a9af1d9"
}
],
"browserRuns": [
{
"path": "refactor/.cache/ambilight05-installed-main-initial-browser/report.json",
"sha256": "bde43cc3bc56c2ff7ff128a85364e5d2f83fdc1a6dc5ae4bf869f78537b5281f",
"stats": {
"startTime": "2026-09-14T09:37:03.054Z",
"duration": 92475.65999999999,
"expected": 45,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"cases": [
{
"browser": "chromium",
"title": "published-5.1.7: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "chromium",
"title": "published-5.1.7: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "firefox",
"title": "published-5.1.7: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "firefox",
"title": "published-5.1.7: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "webkit",
"title": "published-5.1.7: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "webkit",
"title": "published-5.1.7: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "chromium",
"title": "published-5.1.7: historical core has no proxy option and retains native Ambilight playback",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "firefox",
"title": "published-5.1.7: historical core has no proxy option and retains native Ambilight playback",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "webkit",
"title": "published-5.1.7: historical core has no proxy option and retains native Ambilight playback",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
}
]
},
{
"path": "refactor/.cache/ambilight05-installed-legacy-browser/report.json",
"sha256": "7f8ffd2ddee4ecd026a674161f5055e367ffed2681f9a7a06b806a3a4ec9c797",
"stats": {
"startTime": "2026-09-14T09:40:52.738Z",
"duration": 93430.18999999999,
"expected": 45,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"cases": [
{
"browser": "chromium",
"title": "published-5.1.7: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "chromium",
"title": "published-5.1.7: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "firefox",
"title": "published-5.1.7: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "firefox",
"title": "published-5.1.7: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "webkit",
"title": "published-5.1.7: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "webkit",
"title": "published-5.1.7: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: Ambilight ready starts native sampling and destruction releases only its frames and view",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: Ambilight recovers from native cross-origin taint after a same-origin source switch",
"status": "expected"
},
{
"browser": "chromium",
"title": "published-5.1.7: historical core has no proxy option and retains native Ambilight playback",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "firefox",
"title": "published-5.1.7: historical core has no proxy option and retains native Ambilight playback",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "webkit",
"title": "published-5.1.7: historical core has no proxy option and retains native Ambilight playback",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: candidate canvas paints native pixels, resizes and switches media",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: candidate paused seek draws the new video frame without restarting RAF",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: candidate destroy inside callback leaves no media or native RAF and escaped play is inert",
"status": "expected"
}
]
},
{
"path": "refactor/.cache/ambilight05-installed-main-final-browser/report.json",
"sha256": "606ac1f446d62c379bd37aff2ef2e23804ffc59572ec59ad6e8462a43e77bf13",
"stats": {
"startTime": "2026-09-14T09:43:17.253Z",
"duration": 18682.41,
"expected": 9,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"cases": [
{
"browser": "chromium",
"title": "published-5.1.7: historical core has no proxy option and retains native Ambilight playback",
"status": "expected"
},
{
"browser": "chromium",
"title": "published: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "chromium",
"title": "candidate: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "firefox",
"title": "published-5.1.7: historical core has no proxy option and retains native Ambilight playback",
"status": "expected"
},
{
"browser": "firefox",
"title": "published: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "firefox",
"title": "candidate: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "webkit",
"title": "published-5.1.7: historical core has no proxy option and retains native Ambilight playback",
"status": "expected"
},
{
"browser": "webkit",
"title": "published: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
},
{
"browser": "webkit",
"title": "candidate: Ambilight samples all nine canvas proxy regions after output resizing",
"status": "expected"
}
]
}
],
"unit": {
"ambilight": 49,
"canvas": 57,
"packageAndPerformance": 10,
"failures": 0,
"ciWorkflow": 32
},
"maps": [
{
"path": "refactor/.cache/packages/run-IIEOt4/browser-artifacts.json",
"sha256": "8a62e2910193654b510c948aece932b3328966650b27b997e20c80776909b36b"
},
{
"path": "refactor/.cache/packages/run-IIEOt4/browser-artifacts-legacy.json",
"sha256": "9eca95c02a0abb16cb9397b71ab71ca4c7080865c791eb7730a3a48cebd1d42d"
}
],
"limits": [
"Only the Ambilight ARCHITECTURE.md changed between the two installations; runtime files are byte-identical.",
"36 runtime and 13 type mode groups are core/chapter consumers, not whole-ecosystem consumer acceptance.",
"Nine-color Canvas palette is painted through the public post-processing callback, not inferred from natural video frames.",
"Physical mobile devices, full package distribution, Canvas Document PiP/subtitle combinations and remote CI remain open."
],
"checks": {
"strictToolchain": "passed",
"rootLint": {
"errors": 0,
"warnings": 1
},
"ciConfiguration": "passed",
"engineeringLog": "refactor/.cache/ambilight05-final-engineering.log"
}
}
@@ -0,0 +1,50 @@
# PKG-AMBILIGHT-05 已安装产物组合检查点
## 解决的证据缺口
此前 Ambilight/Canvas 回归通过 helper 在内存中构建源码。即使指定核心的已安装
artifact map,插件仍会构建源码,不能据此宣称整个组合验证了 npm 安装内容。
本次扩展既有隔离打包流程,纳入有冻结发布契约的 Ambilight 和 Canvas:源快照
重建、Yarn pack、仓库外离线安装、冻结重装、逐文件摘要验证后保存浏览器产物。
使用 `yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas`。
未审核的额外包与重复包名被拒绝;额外包模式不允许 `--release`。报告明确 36 项
runtime 与 5 组旧/8 组精确类型检查仍只覆盖 core/chapter;另外两包的打包安装
成功不自动成为其全部运行时/类型/发布验收。原有逐包类型矩阵仍独立保留。
## 结构与兼容
把性能模块中通用的安装来源验证提取到 `scripts/installed-artifacts.mjs`,原
`verifyPerformanceArtifacts` 名称作为兼容导出保留。通用验证可指定包集合,检查
安装文件摘要、当前 src/public/package.json 与构建快照,以及锁和构建工具输入。
Ambilight/Canvas helper 收到明确 map 后只加载这些已验证文件;缺包、文件变化、
源码漂移或同时启用 frozen-workspace 都失败,不再回退源码。未设置 map 的原源码
测试保留。没有新增依赖、改变 bundler、库生产代码、公开 API 或声明。
CI 在 Node/React/Vue 消费者之后、浏览器之前额外准备两包的安装产物并更新 map;
避免默认 core/chapter map 触发缺包。性能兼容导出只返回其已校验的两包,不把
额外插件自动加入旧性能基线。配置反例保护新增步骤不能缺失或被跳过;其他插件的
installed/SDK 全矩阵仍归 CI-01,不宣称这次能使整个远端 CI 全绿。
组合测试额外观察两个独立 RAF owner。真实视频解码后通过公开 Canvas callback
绘制九色区域,验证输出 resize 和九格取色,再检查销毁后两队列为空且没有新绘制。
这仍是真实 DOM/Canvas/getImageData/RAF;颜色来自显式后处理 callback,不声称是
自然视频画面的取色结果。5.1.7 核心没有 proxy 配置,明确只测原生 video 能力边界。
## 验证与余项
结果、两次打包差异、产物映射与报告摘要见
[机器证据](../baselines/ambilight-installed-validation.json)。现代首轮 45 项,增强
组合销毁断言后的现代 9 项与 legacy 45 项均使用实际安装产物;legacy 指向同一
安装目录的真实 legacy 文件,其摘要仍须匹配 tarball。三个引擎的旧核心是冻结
已发布版本,新核心/插件/代理来自该次安装;没有用浏览器模拟替代 SDK 或真机。
Ambilight 49、Canvas 57、打包/来源/性能回归 10 项通过。新增反例保护额外包不能
误标 release-ready、显式 map 不得回退源码以及额外插件的源码漂移。包维护说明与
测试流程同批更新。另32项CI配置回归通过,工程合计42项。首轮 lint 的新导入
顺序/未用 import 等已修正;根lint保留1条既有生成声明unused-disable warning。
PKG-AMBILIGHT-05 继续 doing:物理移动设备、剩余完整组合/分发仍缺证据;本次
没有关闭 PKG-CANVAS-05 的字幕/Document PiP/真机要求,也不关闭 CI-01 的全包
消费者与远端矩阵。不推送、部署或发布。回退本检查点可恢复原默认 core/chapter
安装流程与源码 helper;注意不能再把旧 helper 的运行当成完整 installed 组合。
+3 -3
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 256 项,范围 22 个包及工作区/示例。
状态:todo 51 / doing 18 / blocked 0 / done 187 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 50 / doing 19 / blocked 0 / done 187 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -155,7 +155,7 @@
| PKG-AMBILIGHT-03 | artplayer-plugin-ambilight<br>整理内部职责与资源 | PKG-AMBILIGHT-02, CORE-03, CORE-12 | 取色计算与绘帧分离,统一 RAF 和 DOM 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | done |
| PKG-AMBILIGHT-04 | artplayer-plugin-ambilight<br>迁移自有源码和公开类型 | PKG-AMBILIGHT-03, ENG-04, ENG-06, CORE-07 | canvas 上下文、参数及 start/stop 的明确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | done |
| PKG-AMBILIGHT-PROXY-01 | artplayer-plugin-ambilight, artplayer-proxy-canvas<br>修复Canvas代理输出尺寸与取色区域 | PKG-AMBILIGHT-04, CORE-22 | 按Canvas实际输出buffer取色,原生video保留intrinsic尺寸,明确5.1.7没有proxy配置 | 旧错误复现、三引擎实际代理九色区域对照及原生视频回归通过;不把本子项当作最终代理/设备验收 | M | done |
| PKG-AMBILIGHT-05 | artplayer-plugin-ambilight<br>验证新旧核心和组合 | PKG-AMBILIGHT-04, CORE-22, PKG-AMBILIGHT-PROXY-01, PKG-CANVAS-04, PKG-FACTORY-01 | 原生 video、canvas proxy 的能力边界及销毁无帧循环 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo |
| PKG-AMBILIGHT-05 | artplayer-plugin-ambilight<br>验证新旧核心和组合 | PKG-AMBILIGHT-04, CORE-22, PKG-AMBILIGHT-PROXY-01, PKG-CANVAS-04, PKG-FACTORY-01 | 原生 video、canvas proxy 的能力边界及销毁无帧循环 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | doing |
| PKG-AMBILIGHT-06 | artplayer-plugin-ambilight<br>验证分发并同步文档 | PKG-AMBILIGHT-05, ENG-07 | ambilight.js 示例及样式/分发验证 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | M | todo |
## 5 包迁移:artplayer-plugin-audio-track
@@ -534,7 +534,7 @@
- PKG-AMBILIGHT-03: [记录](changes/2026-09-12-PKG-AMBILIGHT-03-lifecycle.md) [记录](ambilight-validation.md) [记录](baselines/ambilight-lifecycle-validation.json)
- PKG-AMBILIGHT-04: [记录](changes/2026-09-12-PKG-AMBILIGHT-04-types.md) [记录](ambilight-validation.md) [记录](baselines/ambilight-types-validation.json)
- PKG-AMBILIGHT-PROXY-01: [记录](changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md) [记录](ambilight-validation.md) [记录](baselines/ambilight-proxy-validation.json)
- PKG-AMBILIGHT-05: [记录](changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md) [记录](ambilight-validation.md)
- PKG-AMBILIGHT-05: [记录](changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md) [记录](ambilight-validation.md) [记录](changes/2026-09-14-PKG-AMBILIGHT-05-installed-combination.md) [记录](baselines/ambilight-installed-validation.json)
- PKG-AUDIO-01: [记录](changes/2026-09-12-PKG-AUDIO-01-contract.md) [记录](baselines/audio-track-contract.md) [记录](baselines/audio-track-release.json)
- PKG-AUDIO-02: [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md) [记录](audio-validation.md) [记录](baselines/audio-validation.json)
- PKG-AUDIO-03: [记录](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) [记录](baselines/audio-lifecycle-validation.json)
+16
View File
@@ -1,5 +1,21 @@
# 进度与证据
## PKG-AMBILIGHT-05 已安装产物组合检查点(仍doing)
隔离构建/打包流程显式纳入 Ambilight/Canvas,仓库外离线安装与冻结重装后校验
全部文件;加载器指定 map 时校验安装摘要和源码/构建输入,拒绝缺包或源码 fallback。
现代首轮45项、增强销毁检查后现代9项、legacy45项三引擎回归全部通过;覆盖实际
播放/取色、尺寸变化、跨域恢复、暂停寻址、切源与销毁。组合额外验证插件/代理两
个 RAF 队列均归零且无后续绘制。49+57+10项专项/工程测试、工具链与lint通过。
CI在Node/框架消费者后准备额外浏览器安装,新增顺序/不可跳过校验;另32项CI回归
通过。性能检查保持core/chapter两包范围,不向其他插件自动扩张性能基线。
两次安装仅 Ambilight 架构文档变化,运行时文件一致;36 runtime、5旧+8精确类型
组只属于 core/chapter,报告已明确范围,额外包模式禁止误用严格 release 准入。
见[变更](changes/2026-09-14-PKG-AMBILIGHT-05-installed-combination.md)与
[证据](baselines/ambilight-installed-validation.json)。256项:187 done、19 doing、50 todo。
Ambilight移动真机、Canvas字幕/Document PiP/真机、完整分发与远端CI仍未完成;
本检查点不关闭其他任务,不推送/部署/发布。下一步继续剩余组合与分包消费者验收。
## SITE-VCONSOLE-01 站点控制台生命周期修复完成
冻结 vConsole 3.15.0 原始 bundle,以严格 TS 和六个唯一构建替换点修复日志 RAF、
+4
View File
@@ -90,6 +90,10 @@ export function validateCIWorkflow(source) {
const vueIndex = browser.steps.findIndex(step => step.run === 'yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log')
assert(vueIndex > consumerIndex && !Object.hasOwn(browser.steps[vueIndex], 'if'), 'Run Vue installed consumers after restoring canonical Node')
assert(browser.steps.some(step => step.uses?.startsWith('actions/upload-artifact@') && step.if === 'always()' && step.with.path.split('\n').includes('refactor/.cache/vue-consumer-*/')), 'Retain Vue consumer failure evidence')
const extraIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas '))
const engineIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:browser '))
assert(extraIndex > Math.max(consumerIndex, reactIndex, vueIndex) && extraIndex < engineIndex, 'Prepare installed Ambilight/Canvas after consumer probes and before browser checks')
assert(!Object.hasOwn(browser.steps[extraIndex], 'if') && browser.steps[extraIndex].run.includes('GITHUB_ENV') && browser.steps[extraIndex].run.includes('ARTPLAYER_BROWSER_ARTIFACTS='), 'Always select the additional installed browser artifact map')
const pages = workflow.jobs.checks.steps.find(step => step.uses?.startsWith('actions/upload-pages-artifact@'))
assert.equal(pages?.if, 'inputs.pages-artifact && github.ref == \'refs/heads/master\' && matrix.os == \'ubuntu-latest\'', 'Only one trusted matrix leg can prepare Pages')
const prepareIndex = workflow.jobs.checks.steps.findIndex(step => step.id === 'pages')
+2
View File
@@ -33,6 +33,8 @@ for (const [name, mutate] of [
['conditional Node setup', w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/setup-node@')).if = 'false'],
['wrong consumer Node version', w => w.jobs['browser-smoke'].steps.find(s => s.id === 'consumer-node-20').with['node-version'] = '24'],
['skipped installed consumer', w => w.jobs['browser-smoke'].steps.find(s => s.run?.includes('--expected-node 22.12.0')).if = 'false'],
['missing browser plugin installation', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:package --include=')).run = 'echo omitted'],
['skipped browser plugin installation', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:package --include=')).if = 'false'],
['browser runtime not restored', w => w.jobs['browser-smoke'].steps.find(s => s.id === 'restore-canonical-node').with = { 'node-version': '22.12.0', 'package-manager-cache': false }],
['skipped React consumer', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:react-consumer')).if = 'false'],
['removed React consumer', w => w.jobs['browser-smoke'].steps = w.jobs['browser-smoke'].steps.filter(s => !s.run?.startsWith('yarn test:react-consumer'))],
+4 -2
View File
@@ -1584,13 +1584,15 @@
"PKG-CANVAS-04",
"PKG-FACTORY-01"
],
"status": "todo",
"status": "doing",
"risk": "M",
"deliverable": "原生 video、canvas proxy 的能力边界及销毁无帧循环",
"acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成",
"evidence": [
"changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md",
"ambilight-validation.md"
"ambilight-validation.md",
"changes/2026-09-14-PKG-AMBILIGHT-05-installed-combination.md",
"baselines/ambilight-installed-validation.json"
]
},
{
+67
View File
@@ -0,0 +1,67 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { hash } from '../refactor/scripts/releases.mjs'
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
const walk = directory => fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)])
function digest(file) {
const bytes = fs.readFileSync(file)
return hash(/\.(?:[cm]?[jt]s|json|css|less|html|svg|txt)$/.test(file) ? bytes.toString('utf8').replaceAll('\r\n', '\n') : bytes)
}
export function verifyInstalledArtifacts(root, mapFile, names = ['artplayer', 'artplayer-plugin-chapter']) {
assert(mapFile, 'Run yarn test:package and set ARTPLAYER_BROWSER_ARTIFACTS to its browser-artifacts.json')
const directory = path.dirname(path.resolve(mapFile))
const artifacts = read(mapFile)
const report = read(path.join(directory, 'report.json'))
assert.equal(report.task, 'ENG-07', 'Browser checks require isolated installation evidence')
assert.equal(report.knownTypeBlockers, 0)
assert.equal(report.node, process.versions.node, 'Build and browser-check Node versions differ')
const toolchain = {}
const currentManifest = read(path.join(root, 'package.json'))
const buildManifest = read(path.join(directory, 'build/package.json'))
for (const key of ['packageManager', 'devDependencies', 'resolutions'])
assert.deepEqual(buildManifest[key], currentManifest[key], `Build toolchain changed: ${key}`)
for (const file of ['yarn.lock', 'scripts/build.js', 'scripts/utils.js', 'scripts/projects.js']) {
toolchain[file] = digest(path.join(root, file))
assert.equal(digest(path.join(directory, 'build', file)), toolchain[file], `Stale build input: ${file}`)
}
const libraryInputs = location => Object.fromEntries(walk(path.join(location, 'scripts/library')).filter(file => /\.(?:[cm]?[jt]s|json)$/.test(file)).sort().map(file => [path.relative(location, file).replaceAll('\\', '/'), digest(file)]))
const currentLibrary = libraryInputs(root)
assert.deepEqual(libraryInputs(path.join(directory, 'build')), currentLibrary, 'Stale library build inputs; rerun yarn test:package')
Object.assign(toolchain, currentLibrary)
const inputs = []
for (const name of names) {
const pkg = report.packages.find(pkg => pkg.name === name)
assert(pkg && artifacts[name], `Missing installed package: ${name}`)
const file = path.resolve(directory, artifacts[name])
const member = `package/dist/${path.basename(file)}`
assert.equal(hash(fs.readFileSync(file)), pkg.files[member], `Installed browser artifact changed: ${name}`)
const sourceHashes = {}
for (const folder of ['src', 'public']) {
const current = path.join(root, 'packages', name, folder)
const snapshot = path.join(directory, 'build/packages', name, folder)
if (!fs.existsSync(current)) {
assert(!fs.existsSync(snapshot), `Source directory was removed: ${name}/${folder}`)
continue
}
const inventory = location => Object.fromEntries(walk(location).map(file => [path.relative(location, file).replaceAll('\\', '/'), digest(file)]))
const expected = inventory(current)
assert.deepEqual(inventory(snapshot), expected, `Stale package source: ${name}/${folder}; rerun yarn test:package`)
Object.assign(sourceHashes, Object.fromEntries(Object.entries(expected).map(([file, digest]) => [`${folder}/${file}`, digest])))
}
for (const file of ['package.json', 'THIRD_PARTY_NOTICES', 'tsconfig.json']) {
const current = path.join(root, 'packages', name, file)
const snapshot = path.join(directory, 'build/packages', name, file)
assert.equal(fs.existsSync(current), fs.existsSync(snapshot), `Package input removed or added: ${name}/${file}`)
if (fs.existsSync(current)) {
sourceHashes[file] = digest(current)
assert.equal(digest(snapshot), sourceHashes[file], `Stale package input: ${name}/${file}`)
}
}
inputs.push({ name, file, member, sha256: pkg.files[member], archiveSha256: pkg.sha256, sourceHashes })
}
return { directory, inputs, toolchain, packages: report.packages.filter(pkg => names.includes(pkg.name)) }
}
+21 -5
View File
@@ -5,7 +5,7 @@ import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { ensureArchive, hash, readMember } from '../refactor/scripts/releases.mjs'
import { consumerDirectory, names, readJson, removeConsumer, run, runtimeConsumer, typeConsumers, workspace, writeJson } from './package-consumer.mjs'
import { consumerDirectory, names as defaultNames, readJson, removeConsumer, run, runtimeConsumer, typeConsumers, workspace, writeJson } from './package-consumer.mjs'
export function checkFiles(manifest, files, historical = []) {
for (const field of ['main', 'module', 'types', 'legacy']) {
@@ -67,10 +67,23 @@ export async function publishedConsumer() {
}
}
export async function checkPackages({ release = false } = {}) {
export async function checkPackages({ release = false, include = [] } = {}) {
assert(!release || include.length === 0, 'Additional package browser preparation is not full release acceptance')
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Run yarn test:package with the pinned Yarn')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn), 'Missing Yarn executable')
const supported = {
'artplayer-plugin-ambilight': async () => (await import('../refactor/scripts/ambilight-contract.mjs')).verifyAmbilightContract(),
'artplayer-proxy-canvas': async () => (await import('../refactor/scripts/canvas-contract.mjs')).verifyCanvasContract(),
}
assert(new Set(include).size === include.length, 'Duplicate additional package')
const historicalFiles = new Map()
for (const name of include) {
assert(Object.hasOwn(supported, name), `Additional package needs a reviewed contract: ${name}`)
const contract = await supported[name]()
historicalFiles.set(name, Object.keys(contract.baseline.release.files))
}
const names = [...defaultNames, ...include]
const parent = path.join(workspace, 'refactor/.cache/packages')
fs.mkdirSync(parent, { recursive: true })
const output = fs.mkdtempSync(path.join(parent, 'run-'))
@@ -98,7 +111,7 @@ export async function checkPackages({ release = false } = {}) {
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
assert.equal(manifest.name, name)
assert(!['preinstall', 'install', 'postinstall'].some(hook => manifest.scripts?.[hook]), 'Add lifecycle fixtures before accepting install hooks')
checkFiles(manifest, files, Object.keys(baseline.releases.find(r => r.name === name).files))
checkFiles(manifest, files, historicalFiles.get(name) || Object.keys(baseline.releases.find(r => r.name === name).files))
packages.push({ name, version: manifest.version, archive: path.basename(archive), sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(file => [file, hash(readMember(archive, file))])) })
}
writeJson(path.join(installed, 'package.json'), { private: true, name: 'artplayer-isolated-consumer', dependencies: Object.fromEntries(packages.map(pkg => [pkg.name, `file:${path.join(output, pkg.archive).replaceAll('\\', '/')}`])) })
@@ -123,11 +136,13 @@ export async function checkPackages({ release = false } = {}) {
const preciseTypes = typeConsumers(installed, { precise: true })
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspace, encoding: 'utf8' }).trim()
const publishedPackages = baseline.releases.map(({ name, version, files }) => ({ name, version, files }))
const report = { task: 'ENG-07', capturedAt: new Date().toISOString(), source, toolchain: { yarn: '1.22.22', yarnPath: yarn }, node: process.versions.node, packages, runtime, publishedPackages, publishedRuntime: oldRuntime, types, preciseTypes, knownRuntimeBlockers: runtime.observations.defaultsWithoutNavigator.resolved ? 0 : 1, knownTypeBlockers: [...types, ...preciseTypes].reduce((sum, result) => sum + result.diagnostics.length, 0) }
const report = { task: 'ENG-07', capturedAt: new Date().toISOString(), source, toolchain: { yarn: '1.22.22', yarnPath: yarn }, node: process.versions.node, packages, runtimeScope: defaultNames, typeScope: defaultNames, additionalBrowserPackages: include, runtime, publishedPackages, publishedRuntime: oldRuntime, types, preciseTypes, knownRuntimeBlockers: runtime.observations.defaultsWithoutNavigator.resolved ? 0 : 1, knownTypeBlockers: [...types, ...preciseTypes].reduce((sum, result) => sum + result.diagnostics.length, 0) }
writeJson(path.join(output, 'report.json'), report)
writeJson(path.join(output, 'browser-artifacts.json'), artifacts)
writeJson(path.join(parent, 'latest.json'), { output: path.relative(workspace, output).replaceAll('\\', '/') })
console.log(`Installed tarball contracts passed: ${runtime.checks.length} runtime checks; ${types.filter(t => !t.diagnostics.length).length}/${types.length} legacy type modes; ${preciseTypes.filter(t => !t.diagnostics.length).length}/${preciseTypes.length} precise type modes. Report: ${output}`)
if (include.length)
console.log(`Runtime/type fixture scope: ${defaultNames.join(', ')}. Additional packages require their own consumer and browser acceptance: ${include.join(', ')}.`)
if (release) {
assert.equal(report.knownTypeBlockers, 0, 'Known type blockers remain; this candidate is not release-ready')
assert.equal(report.knownRuntimeBlockers, 0, 'Known runtime blockers remain; this candidate is not release-ready')
@@ -141,7 +156,8 @@ export async function checkPackages({ release = false } = {}) {
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
checkPackages({ release: process.argv.includes('--release') }).catch((error) => {
const include = process.argv.find(arg => arg.startsWith('--include='))?.slice('--include='.length).split(',') || []
checkPackages({ release: process.argv.includes('--release'), include }).catch((error) => {
console.error(error)
process.exitCode = 1
})
+2 -62
View File
@@ -1,71 +1,11 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../refactor/scripts/releases.mjs'
import { ensureArchive, readMember } from '../refactor/scripts/releases.mjs'
import { measureBytes } from '../refactor/scripts/sizes.mjs'
export { verifyInstalledArtifacts as verifyPerformanceArtifacts } from './installed-artifacts.mjs'
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
const walk = directory => fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)])
function digest(file) {
const bytes = fs.readFileSync(file)
return hash(/\.(?:[cm]?[jt]s|json|css|less|html|svg|txt)$/.test(file) ? bytes.toString('utf8').replaceAll('\r\n', '\n') : bytes)
}
export function verifyPerformanceArtifacts(root, mapFile) {
assert(mapFile, 'Run yarn test:package and set ARTPLAYER_BROWSER_ARTIFACTS to its browser-artifacts.json')
const directory = path.dirname(path.resolve(mapFile))
const artifacts = read(mapFile)
const report = read(path.join(directory, 'report.json'))
assert.equal(report.task, 'ENG-07', 'Performance requires isolated installation evidence')
assert.equal(report.knownTypeBlockers, 0)
assert.equal(report.node, process.versions.node, 'Build and benchmark Node versions differ')
const toolchain = {}
const currentManifest = read(path.join(root, 'package.json'))
const buildManifest = read(path.join(directory, 'build/package.json'))
for (const key of ['packageManager', 'devDependencies', 'resolutions'])
assert.deepEqual(buildManifest[key], currentManifest[key], `Build toolchain changed: ${key}`)
for (const file of ['yarn.lock', 'scripts/build.js', 'scripts/utils.js', 'scripts/projects.js']) {
toolchain[file] = digest(path.join(root, file))
assert.equal(digest(path.join(directory, 'build', file)), toolchain[file], `Stale build input: ${file}`)
}
const libraryInputs = location => Object.fromEntries(walk(path.join(location, 'scripts/library')).filter(file => /\.(?:[cm]?[jt]s|json)$/.test(file)).sort().map(file => [path.relative(location, file).replaceAll('\\', '/'), digest(file)]))
const currentLibrary = libraryInputs(root)
assert.deepEqual(libraryInputs(path.join(directory, 'build')), currentLibrary, 'Stale library build inputs; rerun yarn test:package')
Object.assign(toolchain, currentLibrary)
const inputs = []
for (const name of ['artplayer', 'artplayer-plugin-chapter']) {
const pkg = report.packages.find(pkg => pkg.name === name)
assert(pkg && artifacts[name], `Missing installed package: ${name}`)
const file = path.resolve(directory, artifacts[name])
const member = `package/dist/${path.basename(file)}`
assert.equal(hash(fs.readFileSync(file)), pkg.files[member], `Installed performance artifact changed: ${name}`)
const sourceHashes = {}
for (const folder of ['src', 'public']) {
const current = path.join(root, 'packages', name, folder)
const snapshot = path.join(directory, 'build/packages', name, folder)
if (!fs.existsSync(current)) {
assert(!fs.existsSync(snapshot), `Source directory was removed: ${name}/${folder}`)
continue
}
const inventory = location => Object.fromEntries(walk(location).map(file => [path.relative(location, file).replaceAll('\\', '/'), digest(file)]))
const expected = inventory(current)
assert.deepEqual(inventory(snapshot), expected, `Stale package source: ${name}/${folder}; rerun yarn test:package`)
Object.assign(sourceHashes, Object.fromEntries(Object.entries(expected).map(([file, digest]) => [`${folder}/${file}`, digest])))
}
for (const file of ['package.json', 'THIRD_PARTY_NOTICES', 'tsconfig.json']) {
const current = path.join(root, 'packages', name, file)
const snapshot = path.join(directory, 'build/packages', name, file)
assert.equal(fs.existsSync(current), fs.existsSync(snapshot), `Package input removed or added: ${name}/${file}`)
if (fs.existsSync(current)) {
sourceHashes[file] = digest(current)
assert.equal(digest(snapshot), sourceHashes[file], `Stale package input: ${name}/${file}`)
}
}
inputs.push({ name, file, member, sha256: pkg.files[member], archiveSha256: pkg.sha256, sourceHashes })
}
return { directory, inputs, toolchain, packages: report.packages }
}
export async function compareArtifactSizes(root, artifacts) {
const releases = read(path.join(root, 'refactor/baselines/releases.json')).releases
+21 -3
View File
@@ -11,8 +11,20 @@ for (const core of ['published-5.1.7', 'published', 'candidate']) {
const scenario = core === 'published-5.1.7' ? 'historical core has no proxy option and retains native Ambilight playback' : 'Ambilight samples all nine canvas proxy regions after output resizing'
test(`${core}: ${scenario}`, async ({ page }, testInfo) => {
await page.goto(`/test/player.html?core=${core}`)
for (const [name, source] of [['ambilightFactory', implementation.source], ['canvasProxy', proxy]])
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${source}; window.${name} = module.exports.default || module.exports; })();` })
for (const [name, source] of [['ambilightFactory', implementation.source], ['canvasProxy', proxy]]) {
await page.addScriptTag({ content: `(() => {
const pending = new Set();
const requestAnimationFrame = callback => {
const id = window.requestAnimationFrame(time => { pending.delete(id); callback(time); });
pending.add(id); return id;
};
const cancelAnimationFrame = id => { pending.delete(id); window.cancelAnimationFrame(id); };
const module = { exports: {} }; const exports = module.exports;
${source};
window.${name} = module.exports.default || module.exports;
(window.combinationFrames ||= {})['${name}'] = pending;
})();` })
}
await page.evaluate(() => {
window.palette = ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)', 'rgb(255, 255, 0)', 'rgb(0, 255, 255)', 'rgb(255, 0, 255)', 'rgb(120, 0, 0)', 'rgb(0, 120, 0)', 'rgb(0, 0, 120)']
window.drawCount = 0
@@ -61,8 +73,14 @@ for (const core of ['published-5.1.7', 'published', 'candidate']) {
expect(dimensions.canvas).not.toEqual(dimensions.video)
await testInfo.attach('ambilight-proxy-inputs', { contentType: 'application/json', body: JSON.stringify({ core, dimensions, plugin: hash(implementation.source), proxy: hash(proxySource), media: '/test/pattern.mp4', scope: 'real video decode and proxy post-processing callback paints a nine-cell palette using native Canvas' }) })
await expect.poll(() => page.evaluate(() => [...document.querySelectorAll('.artplayer-plugin-ambilight > div')].map(node => node.style.backgroundColor))).toEqual(await page.evaluate(() => window.palette))
await page.evaluate(() => window.art.destroy())
const drawsAtDestroy = await page.evaluate(async () => {
window.art.destroy()
const draws = window.drawCount
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
return draws
})
expect(await page.locator('.artplayer-plugin-ambilight').count()).toBe(0)
expect(await page.evaluate(() => ({ draws: window.drawCount, pluginFrames: window.combinationFrames.ambilightFactory.size, proxyFrames: window.combinationFrames.canvasProxy.size }))).toEqual({ draws: drawsAtDestroy, pluginFrames: 0, proxyFrames: 0 })
})
}
+9 -1
View File
@@ -1,4 +1,5 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
@@ -6,12 +7,19 @@ import vm from 'node:vm'
import { build, transform } from 'esbuild'
import { verifyAmbilightContract } from '../../refactor/scripts/ambilight-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
import { verifyInstalledArtifacts } from '../../scripts/installed-artifacts.mjs'
import { getEntryFile } from '../../scripts/projects.js'
export async function ambilightCandidate() {
const root = fileURLToPath(new URL('../../', import.meta.url))
if (process.env.ARTPLAYER_BROWSER_ARTIFACTS) {
assert.notEqual(process.env.ARTPLAYER_AMBILIGHT_BASELINE, '1', 'Installed candidate cannot use the frozen workspace')
const { inputs } = verifyInstalledArtifacts(root, process.env.ARTPLAYER_BROWSER_ARTIFACTS, ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight'])
const input = inputs.find(item => item.name === 'artplayer-plugin-ambilight')
return { name: 'candidate-installed', source: fs.readFileSync(input.file, 'utf8'), format: 'artifact', provenance: input }
}
if (process.env.ARTPLAYER_AMBILIGHT_BASELINE === '1')
return (await ambilightHistorical()).find(item => item.name === 'frozen-workspace')
const root = fileURLToPath(new URL('../../', import.meta.url))
const result = await build({ entryPoints: [getEntryFile(path.join(root, 'packages/artplayer-plugin-ambilight'))], bundle: true, write: false, platform: 'browser', format: 'cjs', target: 'es2020' })
return { name: 'candidate-source', source: result.outputFiles[0].text, format: 'artifact' }
}
+10 -1
View File
@@ -1,3 +1,5 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
@@ -5,12 +7,19 @@ import vm from 'node:vm'
import { build, transform } from 'esbuild'
import { verifyCanvasContract } from '../../refactor/scripts/canvas-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
import { verifyInstalledArtifacts } from '../../scripts/installed-artifacts.mjs'
import { getEntryFile } from '../../scripts/projects.js'
export async function canvasCandidate() {
const root = fileURLToPath(new URL('../../', import.meta.url))
if (process.env.ARTPLAYER_BROWSER_ARTIFACTS) {
assert.notEqual(process.env.ARTPLAYER_CANVAS_BASELINE, '1', 'Installed candidate cannot use the frozen workspace')
const { inputs } = verifyInstalledArtifacts(root, process.env.ARTPLAYER_BROWSER_ARTIFACTS, ['artplayer', 'artplayer-plugin-chapter', 'artplayer-proxy-canvas'])
const input = inputs.find(item => item.name === 'artplayer-proxy-canvas')
return { name: 'candidate-installed', source: fs.readFileSync(input.file, 'utf8'), format: 'artifact', provenance: input }
}
if (process.env.ARTPLAYER_CANVAS_BASELINE === '1')
return (await canvasHistorical()).find(item => item.name === 'frozen-workspace')
const root = fileURLToPath(new URL('../../', import.meta.url))
const result = await build({ entryPoints: [getEntryFile(path.join(root, 'packages/artplayer-proxy-canvas'))], bundle: true, write: false, platform: 'browser', format: 'cjs', target: 'es2020' })
return { name: 'candidate-source', source: result.outputFiles[0].text, format: 'artifact' }
}
+32 -1
View File
@@ -1,11 +1,42 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- This fixture exercises the repository's Node test runner.
import { test } from 'node:test'
import { checkFiles, publishedConsumer } from '../scripts/package-check.mjs'
import { checkFiles, checkPackages, publishedConsumer } from '../scripts/package-check.mjs'
import { removeConsumer, runtimeConsumer } from '../scripts/package-consumer.mjs'
import { emitterContracts } from './contracts/emitter.js'
import { ambilightCandidate } from './helpers/ambilight.js'
import { canvasCandidate } from './helpers/canvas.js'
test('Additional browser packages cannot be mistaken for full release consumer acceptance', async () => {
await assert.rejects(checkPackages({ release: true, include: ['artplayer-plugin-ambilight'] }), /not full release acceptance/)
})
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']
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')
assert(!fs.existsSync(process.env.ARTPLAYER_BROWSER_ARTIFACTS))
delete process.env.ARTPLAYER_AMBILIGHT_BASELINE
delete process.env.ARTPLAYER_CANVAS_BASELINE
await assert.rejects(ambilightCandidate(), { code: 'ENOENT' })
await assert.rejects(canvasCandidate(), { code: 'ENOENT' })
process.env.ARTPLAYER_AMBILIGHT_BASELINE = '1'
process.env.ARTPLAYER_CANVAS_BASELINE = '1'
await assert.rejects(ambilightCandidate(), /cannot use the frozen workspace/)
await assert.rejects(canvasCandidate(), /cannot use the frozen workspace/)
}
finally {
for (const key of keys) {
if (previous[key] === undefined)
delete process.env[key]
else process.env[key] = previous[key]
}
}
})
test('Package checks reject missing files, missing wildcard exports and internal configuration', () => {
const manifest = { name: 'fixture', main: './dist/main.js', module: './dist/main.mjs', types: './types/main.d.ts', legacy: './dist/legacy.js', exports: { '.': './dist/main.js', './lang/*': './dist/lang/*.js' } }
+15 -2
View File
@@ -1,8 +1,14 @@
# Installed package checks
Run `yarn test:package` with the pinned Node and Yarn after a frozen install. The
initial scope is core and chapter; extend `names` in `scripts/package-consumer.mjs`
and add package-specific consumers when migrating another package.
default consumer scope is core and chapter. For Ambilight/Canvas browser work,
run `yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas`.
These two 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
the core/chapter runtime/type scope from the additional browser package scope;
`--release` rejects additional packages until complete consumer coverage exists.
Other packages need a reviewed contract and their own consumers before inclusion.
`scripts/package-check.mjs` copies source into an ignored build snapshot, rebuilds
all three formats and core languages using repository scripts, and runs Yarn pack.
@@ -72,6 +78,13 @@ and run `yarn test:browser`. The service then uses installed package bytes witho
falling back to source. CI performs this sequence and uploads only reports and
artifacts, excluding the build snapshot's node_modules link.
`scripts/installed-artifacts.mjs` verifies selected packages against the installed
report and the current source/build inputs; performance retains its existing
two-package wrapper. Ambilight and Canvas browser helpers use this verifier when
an artifact map is supplied, including optional legacy maps pointing to checked
legacy members in the same installation. Missing packages, stale source, modified
bundles or contradictory frozen-workspace flags fail instead of rebuilding source.
`test/package-check.test.js` verifies missing targets, leaked configuration and
removed historical files, then deliberately removes an actual published default
export and required entry in an isolated consumer to prove runtime failures are
+10 -2
View File
@@ -6,6 +6,7 @@ import process from 'node:process'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'
import { hash } from '../refactor/scripts/releases.mjs'
import { verifyInstalledArtifacts } from '../scripts/installed-artifacts.mjs'
import { verifyPerformanceArtifacts } from '../scripts/performance-artifacts.mjs'
import { performanceHtml, performanceScript, waitForObservation } from '../scripts/performance-fixture.mjs'
import { validatePairedPerformance } from '../scripts/performance-report.mjs'
@@ -128,7 +129,7 @@ test('performance refuses stale sources, build tools and altered installed bundl
write(path.join(root, file), content)
write(path.join(snapshot, file), content)
}
const names = ['artplayer', 'artplayer-plugin-chapter']
const names = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight']
const packages = names.map((name) => {
const entry = `packages/${name}/src/index.ts`
write(path.join(root, entry), 'export default 1\n')
@@ -144,6 +145,13 @@ test('performance refuses stale sources, build tools and altered installed bundl
write(map, JSON.stringify(Object.fromEntries(names.map(name => [name, `artifacts/${name}/dist/${name}.js`]))))
write(path.join(output, 'report.json'), JSON.stringify({ task: 'ENG-07', knownTypeBlockers: 0, node: process.versions.node, packages }))
assert.equal(verifyPerformanceArtifacts(root, map).inputs.length, 2)
assert.equal(verifyPerformanceArtifacts(root, map).packages.length, 2)
assert.equal(verifyInstalledArtifacts(root, map, names).inputs.length, 3)
assert.throws(() => verifyInstalledArtifacts(root, map, [...names, 'artplayer-proxy-canvas']), /Missing installed package/)
const pluginSource = path.join(root, 'packages/artplayer-plugin-ambilight/src/index.ts')
write(pluginSource, 'export default 2\n')
assert.throws(() => verifyInstalledArtifacts(root, map, names), /Stale package source/)
write(pluginSource, 'export default 1\n')
const source = path.join(root, 'packages/artplayer/src/index.ts')
write(source, 'export default 2\n')
assert.throws(() => verifyPerformanceArtifacts(root, map), /Stale package source/)
@@ -158,5 +166,5 @@ test('performance refuses stale sources, build tools and altered installed bundl
assert.throws(() => verifyPerformanceArtifacts(root, map), /Stale library build inputs/)
fs.unlinkSync(path.join(snapshot, 'scripts/library/removed.ts'))
write(path.join(output, 'artifacts/artplayer/dist/artplayer.js'), 'changed bundle\n')
assert.throws(() => verifyPerformanceArtifacts(root, map), /Installed performance artifact changed/)
assert.throws(() => verifyPerformanceArtifacts(root, map), /Installed browser artifact changed/)
})