From 765ddb816f8483e952cdc354c106c5dffda804a1 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Mon, 14 Sep 2026 17:50:38 +0800 Subject: [PATCH] test(ambilight): [PKG-AMBILIGHT-05] checkpoint installed canvas combinations --- .github/workflows/nodejs.yml | 9 +- .../ARCHITECTURE.md | 14 +- refactor/ambilight-validation.md | 14 + .../ambilight-installed-validation.json | 732 ++++++++++++++++++ ...-PKG-AMBILIGHT-05-installed-combination.md | 50 ++ refactor/plan.md | 6 +- refactor/progress.md | 16 + refactor/scripts/ci-workflow.mjs | 4 + refactor/scripts/ci-workflow.test.mjs | 2 + refactor/tasks.json | 6 +- scripts/installed-artifacts.mjs | 67 ++ scripts/package-check.mjs | 26 +- scripts/performance-artifacts.mjs | 64 +- test/browser/ambilight-proxy.spec.js | 24 +- test/helpers/ambilight.js | 10 +- test/helpers/canvas.js | 11 +- test/package-check.test.js | 33 +- test/package/README.md | 17 +- test/performance-report.test.js | 12 +- 19 files changed, 1032 insertions(+), 85 deletions(-) create mode 100644 refactor/baselines/ambilight-installed-validation.json create mode 100644 refactor/changes/2026-09-14-PKG-AMBILIGHT-05-installed-combination.md create mode 100644 scripts/installed-artifacts.mjs diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 931e5b012..c427dd809 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -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 diff --git a/packages/artplayer-plugin-ambilight/ARCHITECTURE.md b/packages/artplayer-plugin-ambilight/ARCHITECTURE.md index 9913635bd..5f7ea9d0e 100644 --- a/packages/artplayer-plugin-ambilight/ARCHITECTURE.md +++ b/packages/artplayer-plugin-ambilight/ARCHITECTURE.md @@ -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. diff --git a/refactor/ambilight-validation.md b/refactor/ambilight-validation.md index d8893949c..8b8b1f7aa 100644 --- a/refactor/ambilight-validation.md +++ b/refactor/ambilight-validation.md @@ -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逐成员核对,不随候选更新历史哈希。 diff --git a/refactor/baselines/ambilight-installed-validation.json b/refactor/baselines/ambilight-installed-validation.json new file mode 100644 index 000000000..c6d0a338e --- /dev/null +++ b/refactor/baselines/ambilight-installed-validation.json @@ -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" + } +} diff --git a/refactor/changes/2026-09-14-PKG-AMBILIGHT-05-installed-combination.md b/refactor/changes/2026-09-14-PKG-AMBILIGHT-05-installed-combination.md new file mode 100644 index 000000000..9df890c70 --- /dev/null +++ b/refactor/changes/2026-09-14-PKG-AMBILIGHT-05-installed-combination.md @@ -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 组合。 diff --git a/refactor/plan.md b/refactor/plan.md index a5574e776..8fb06b4aa 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -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
整理内部职责与资源 | PKG-AMBILIGHT-02, CORE-03, CORE-12 | 取色计算与绘帧分离,统一 RAF 和 DOM 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | done | | PKG-AMBILIGHT-04 | artplayer-plugin-ambilight
迁移自有源码和公开类型 | PKG-AMBILIGHT-03, ENG-04, ENG-06, CORE-07 | canvas 上下文、参数及 start/stop 的明确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | done | | PKG-AMBILIGHT-PROXY-01 | artplayer-plugin-ambilight, artplayer-proxy-canvas
修复Canvas代理输出尺寸与取色区域 | PKG-AMBILIGHT-04, CORE-22 | 按Canvas实际输出buffer取色,原生video保留intrinsic尺寸,明确5.1.7没有proxy配置 | 旧错误复现、三引擎实际代理九色区域对照及原生视频回归通过;不把本子项当作最终代理/设备验收 | M | done | -| PKG-AMBILIGHT-05 | artplayer-plugin-ambilight
验证新旧核心和组合 | 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
验证新旧核心和组合 | 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
验证分发并同步文档 | 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) diff --git a/refactor/progress.md b/refactor/progress.md index 6c97159b5..3a9977831 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -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、 diff --git a/refactor/scripts/ci-workflow.mjs b/refactor/scripts/ci-workflow.mjs index 0f809b6d6..fb4d03826 100644 --- a/refactor/scripts/ci-workflow.mjs +++ b/refactor/scripts/ci-workflow.mjs @@ -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') diff --git a/refactor/scripts/ci-workflow.test.mjs b/refactor/scripts/ci-workflow.test.mjs index dffbd130b..969a0dd99 100644 --- a/refactor/scripts/ci-workflow.test.mjs +++ b/refactor/scripts/ci-workflow.test.mjs @@ -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'))], diff --git a/refactor/tasks.json b/refactor/tasks.json index c58f0d1a4..24bdc9b18 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -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" ] }, { diff --git a/scripts/installed-artifacts.mjs b/scripts/installed-artifacts.mjs new file mode 100644 index 000000000..9e5e6d7ab --- /dev/null +++ b/scripts/installed-artifacts.mjs @@ -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)) } +} diff --git a/scripts/package-check.mjs b/scripts/package-check.mjs index acfce69ea..55ca9851b 100644 --- a/scripts/package-check.mjs +++ b/scripts/package-check.mjs @@ -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 }) diff --git a/scripts/performance-artifacts.mjs b/scripts/performance-artifacts.mjs index 894f017f7..08cafe601 100644 --- a/scripts/performance-artifacts.mjs +++ b/scripts/performance-artifacts.mjs @@ -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 diff --git a/test/browser/ambilight-proxy.spec.js b/test/browser/ambilight-proxy.spec.js index 72cee6d6c..dafa0225a 100644 --- a/test/browser/ambilight-proxy.spec.js +++ b/test/browser/ambilight-proxy.spec.js @@ -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 }) }) } diff --git a/test/helpers/ambilight.js b/test/helpers/ambilight.js index 8f2b0b28e..a05ee1bf2 100644 --- a/test/helpers/ambilight.js +++ b/test/helpers/ambilight.js @@ -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' } } diff --git a/test/helpers/canvas.js b/test/helpers/canvas.js index 0c19ba842..6e395b70d 100644 --- a/test/helpers/canvas.js +++ b/test/helpers/canvas.js @@ -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' } } diff --git a/test/package-check.test.js b/test/package-check.test.js index 5f0b5b6e9..83ed960c3 100644 --- a/test/package-check.test.js +++ b/test/package-check.test.js @@ -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' } } diff --git a/test/package/README.md b/test/package/README.md index 8b2c00b5e..374348c17 100644 --- a/test/package/README.md +++ b/test/package/README.md @@ -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 diff --git a/test/performance-report.test.js b/test/performance-report.test.js index 6a6991e4c..9bc97c9cd 100644 --- a/test/performance-report.test.js +++ b/test/performance-report.test.js @@ -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/) })