mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
test(release): [REL-04] checkpoint exact historical Pages recovery
This commit is contained in:
1 parent
d0c33f6258
commit
ddf3d6ccbc
17 files changed
+3460
-12
No files matched your search
+5
-3
@@ -45,7 +45,7 @@
|
||||
"check:toolchain": "node scripts/check-toolchain.mjs",
|
||||
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build,library,pages,site-vendor,browser-validation}/**/*.ts\" \"scripts/plugin/*.{js,ts}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix",
|
||||
"check:plan": "node refactor/scripts/plan.mjs --check",
|
||||
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js test/plugin-scaffold.test.js test/dev-server.test.js test/pages-artifact.test.js test/site-notices.test.js test/vconsole-lifecycle.test.js test/browser-validation.test.js test/rollback-files.test.js",
|
||||
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js test/plugin-scaffold.test.js test/dev-server.test.js test/pages-artifact.test.js test/site-notices.test.js test/vconsole-lifecycle.test.js test/browser-validation.test.js test/rollback-files.test.js test/pages-recovery.test.js",
|
||||
"test:baseline": "node --test refactor/scripts/*.test.mjs",
|
||||
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn check:release-ledger && yarn check:rollback-inventory && yarn lint && yarn typecheck:library && yarn typecheck:scaffold && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:editor-types && yarn check:llm && yarn check:vconsole && yarn check:site-notices && yarn typecheck:site-assets && yarn check:site-assets && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test",
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:llm && yarn build:vconsole && yarn build:site-notices && yarn build:test && yarn build:docs && yarn test:imports",
|
||||
@@ -141,7 +141,7 @@
|
||||
"release:preflight": "yarn check:toolchain --strict && node refactor/scripts/release-ledger.mjs --report --strict",
|
||||
"test:release-ledger": "node --test refactor/scripts/release-ledger.test.mjs",
|
||||
"prepare:pages": "node scripts/prepare-pages.mjs",
|
||||
"test:pages": "node --test test/pages-artifact.test.js",
|
||||
"test:pages": "node --test test/pages-artifact.test.js test/pages-recovery.test.js",
|
||||
"test:pages:browser": "node test/pages-browser.mjs",
|
||||
"build:site-notices": "node scripts/build-site-notices.mjs",
|
||||
"check:site-notices": "node scripts/build-site-notices.mjs --check",
|
||||
@@ -156,7 +156,9 @@
|
||||
"test:vast-native": "playwright test --config playwright.vast-native.config.js",
|
||||
"test:rollback:iframe": "node scripts/iframe-rollback.mjs",
|
||||
"test:rollback:mainline": "node scripts/mainline-rehearsal.mjs",
|
||||
"check:rollback-inventory": "node refactor/scripts/rollback-inventory.mjs --check"
|
||||
"check:rollback-inventory": "node refactor/scripts/rollback-inventory.mjs --check",
|
||||
"test:rollback:pages": "node scripts/pages-recovery.mjs",
|
||||
"test:rollback:pages:browser": "node test/pages-recovery-browser.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,388 @@
|
||||
{
|
||||
"task": "REL-04",
|
||||
"status": "local-restoration-verified-remote-gate-open",
|
||||
"capturedAt": "2026-09-14T16:41:26.414Z",
|
||||
"source": "d0c33f6258b49eec1937e139789b73a4440cb2e2",
|
||||
"node": "24.21.0",
|
||||
"localRestorationPassed": true,
|
||||
"remoteRestorationVerified": false,
|
||||
"archive": {
|
||||
"path": "refactor/.cache/pages-recovery/run-485vhu/canonical.zip",
|
||||
"commit": "a6d2b81d05c68c8c78cbe62ca0b06761c983f8b8",
|
||||
"command": [
|
||||
"git",
|
||||
"-c",
|
||||
"core.autocrlf=false",
|
||||
"-c",
|
||||
"core.eol=lf",
|
||||
"archive",
|
||||
"--format=zip",
|
||||
"--output=D:\\github\\ArtPlayer\\refactor\\.cache\\pages-recovery\\run-485vhu\\canonical.zip",
|
||||
"a6d2b81d05c68c8c78cbe62ca0b06761c983f8b8"
|
||||
],
|
||||
"bytes": 213337919,
|
||||
"sha256": "692e190df068d584f460a7c29e5b0a9e565a0a793579ade532f36788731f5faf"
|
||||
},
|
||||
"files": {
|
||||
"count": 538,
|
||||
"inventory": "refactor/baselines/pages-recovery-files.json",
|
||||
"sha256": "84cfc7674950cc639d435062e464a2d6b3aca89274adeb9c475070c1c3ca9aa3",
|
||||
"everyFileMatchesGitBlob": true
|
||||
},
|
||||
"rehearsal": {
|
||||
"command": "node scripts/pages-recovery.mjs",
|
||||
"report": "refactor/.cache/pages-recovery/run-485vhu/report.json",
|
||||
"sha256": "65513d6c321ea471ab0213825cf4e1831a2d0fd05d7c2468b959da0b284c8d03",
|
||||
"simulatedChanges": [
|
||||
"modified compiled/artplayer.js",
|
||||
"changed CNAME",
|
||||
"deleted iframe.html",
|
||||
"added candidate-only.txt"
|
||||
],
|
||||
"failedSnapshotRetained": true
|
||||
},
|
||||
"browser": {
|
||||
"command": "node test/pages-recovery-browser.mjs",
|
||||
"report": "refactor/.cache/pages-recovery/run-485vhu/browser.json",
|
||||
"sha256": "59eb442a0f91d925579ceccf0f3884f75e78dca43bc2154c15e492ccfcabc588",
|
||||
"task": "REL-04",
|
||||
"restoredReportSha256": "65513d6c321ea471ab0213825cf4e1831a2d0fd05d7c2468b959da0b284c8d03",
|
||||
"platform": "win32",
|
||||
"reports": [
|
||||
{
|
||||
"engine": "chromium",
|
||||
"version": "153.0.8010.12",
|
||||
"format": "umd",
|
||||
"passed": true,
|
||||
"state": {
|
||||
"version": "5.4.1",
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"time": 3,
|
||||
"error": 0
|
||||
},
|
||||
"inputs": {
|
||||
"core": {
|
||||
"bytes": 134697,
|
||||
"sha256": "b57c083c13b1d16166f4796ce12378975255298c25ee4c32454e365866b8c732"
|
||||
},
|
||||
"chapter": {
|
||||
"bytes": 4973,
|
||||
"sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0"
|
||||
},
|
||||
"media": {
|
||||
"bytes": 5818706,
|
||||
"sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"engine": "chromium",
|
||||
"version": "153.0.8010.12",
|
||||
"format": "esm",
|
||||
"passed": true,
|
||||
"state": {
|
||||
"version": "5.4.1",
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"time": 3,
|
||||
"error": 0
|
||||
},
|
||||
"inputs": {
|
||||
"core": {
|
||||
"bytes": 209954,
|
||||
"sha256": "9546e5aa58bfbf43d10a6dc348e86303dd58300566031a55d4984372f30658e1"
|
||||
},
|
||||
"chapter": {
|
||||
"bytes": 7721,
|
||||
"sha256": "61342a069b84e0731d44a2464d0dfe637d1514d0037c740371fc866c516d2209"
|
||||
},
|
||||
"media": {
|
||||
"bytes": 5818706,
|
||||
"sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"engine": "firefox",
|
||||
"version": "155.0",
|
||||
"format": "umd",
|
||||
"passed": true,
|
||||
"state": {
|
||||
"version": "5.4.1",
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"time": 3,
|
||||
"error": 0
|
||||
},
|
||||
"inputs": {
|
||||
"core": {
|
||||
"bytes": 134697,
|
||||
"sha256": "b57c083c13b1d16166f4796ce12378975255298c25ee4c32454e365866b8c732"
|
||||
},
|
||||
"chapter": {
|
||||
"bytes": 4973,
|
||||
"sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0"
|
||||
},
|
||||
"media": {
|
||||
"bytes": 5818706,
|
||||
"sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"engine": "firefox",
|
||||
"version": "155.0",
|
||||
"format": "esm",
|
||||
"passed": true,
|
||||
"state": {
|
||||
"version": "5.4.1",
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"time": 3,
|
||||
"error": 0
|
||||
},
|
||||
"inputs": {
|
||||
"core": {
|
||||
"bytes": 209954,
|
||||
"sha256": "9546e5aa58bfbf43d10a6dc348e86303dd58300566031a55d4984372f30658e1"
|
||||
},
|
||||
"chapter": {
|
||||
"bytes": 7721,
|
||||
"sha256": "61342a069b84e0731d44a2464d0dfe637d1514d0037c740371fc866c516d2209"
|
||||
},
|
||||
"media": {
|
||||
"bytes": 5818706,
|
||||
"sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"engine": "webkit",
|
||||
"version": "26.6",
|
||||
"format": "umd",
|
||||
"passed": true,
|
||||
"state": {
|
||||
"version": "5.4.1",
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"time": 3,
|
||||
"error": 0
|
||||
},
|
||||
"inputs": {
|
||||
"core": {
|
||||
"bytes": 134697,
|
||||
"sha256": "b57c083c13b1d16166f4796ce12378975255298c25ee4c32454e365866b8c732"
|
||||
},
|
||||
"chapter": {
|
||||
"bytes": 4973,
|
||||
"sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0"
|
||||
},
|
||||
"media": {
|
||||
"bytes": 5818706,
|
||||
"sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"engine": "webkit",
|
||||
"version": "26.6",
|
||||
"format": "esm",
|
||||
"passed": true,
|
||||
"state": {
|
||||
"version": "5.4.1",
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"time": 3,
|
||||
"error": 0
|
||||
},
|
||||
"inputs": {
|
||||
"core": {
|
||||
"bytes": 209954,
|
||||
"sha256": "9546e5aa58bfbf43d10a6dc348e86303dd58300566031a55d4984372f30658e1"
|
||||
},
|
||||
"chapter": {
|
||||
"bytes": 7721,
|
||||
"sha256": "61342a069b84e0731d44a2464d0dfe637d1514d0037c740371fc866c516d2209"
|
||||
},
|
||||
"media": {
|
||||
"bytes": 5818706,
|
||||
"sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"urls": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"status": 200,
|
||||
"contentType": "text/html; charset=utf-8",
|
||||
"sha256": "bd2e7e32efce80725b70f2054c4d9c95465de567118f245924f2e6d17567ff23"
|
||||
},
|
||||
{
|
||||
"file": "mobile.html",
|
||||
"status": 200,
|
||||
"contentType": "text/html; charset=utf-8",
|
||||
"sha256": "f6cf85608023ee57750af773908d4c8ecd95c1065b1aa587e28e2361b2cee7fd"
|
||||
},
|
||||
{
|
||||
"file": "iframe.html",
|
||||
"status": 200,
|
||||
"contentType": "text/html; charset=utf-8",
|
||||
"sha256": "bc262262c66ad32459b7c91ae22a97217414dd5b18cbf68fe105c2269456f879"
|
||||
},
|
||||
{
|
||||
"file": "esm.html",
|
||||
"status": 200,
|
||||
"contentType": "text/html; charset=utf-8",
|
||||
"sha256": "bf9eaaf69b7705d53edab101fade2ce467dd63bb2830a89d4bc4cda44667ff3c"
|
||||
},
|
||||
{
|
||||
"file": "document/index.html",
|
||||
"status": 200,
|
||||
"contentType": "text/html; charset=utf-8",
|
||||
"sha256": "39d893cd5af7f00b569d89a0766f3b3b851c06e0915e7c9d8f22ea2ce54ea11b"
|
||||
},
|
||||
{
|
||||
"file": "document/en/index.html",
|
||||
"status": 200,
|
||||
"contentType": "text/html; charset=utf-8",
|
||||
"sha256": "def18b4f9bee5e8013174b5599859824aae6eff491fb49e56c46ecb23bc2f5bf"
|
||||
},
|
||||
{
|
||||
"file": "compiled/artplayer.js",
|
||||
"status": 200,
|
||||
"contentType": "text/javascript",
|
||||
"sha256": "b57c083c13b1d16166f4796ce12378975255298c25ee4c32454e365866b8c732"
|
||||
},
|
||||
{
|
||||
"file": "uncompiled/artplayer/index.js",
|
||||
"status": 200,
|
||||
"contentType": "text/javascript",
|
||||
"sha256": "a7b9baf492208d8b9b8f56644389b90d23f74df204139ea619efdd667da5ea83"
|
||||
},
|
||||
{
|
||||
"file": "compiled/artplayer-tool-thumbnail.js",
|
||||
"status": 200,
|
||||
"contentType": "text/javascript",
|
||||
"sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98"
|
||||
},
|
||||
{
|
||||
"file": "assets/example/tool.thumbnail.js",
|
||||
"status": 200,
|
||||
"contentType": "text/javascript",
|
||||
"sha256": "ef830a5f499e14c09d51aa31aaa0a194ea1f910b27f14ef97c0ca8f67d529a54"
|
||||
},
|
||||
{
|
||||
"file": "CNAME",
|
||||
"status": 200,
|
||||
"contentType": "application/octet-stream",
|
||||
"sha256": "7b7010e42fc4642ea0cdfcb1b68cd2a6d9248a812876bc0d58d48ebb15952c84"
|
||||
},
|
||||
{
|
||||
"file": ".nojekyll",
|
||||
"status": 200,
|
||||
"contentType": "application/octet-stream",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
],
|
||||
"failures": [],
|
||||
"cancellations": [
|
||||
{
|
||||
"pathname": "/assets/sample/video.mp4",
|
||||
"range": "bytes=0-",
|
||||
"code": "ERR_STREAM_PREMATURE_CLOSE",
|
||||
"aborted": true,
|
||||
"responseDestroyed": true,
|
||||
"responseFinished": false
|
||||
},
|
||||
{
|
||||
"pathname": "/assets/sample/video.mp4",
|
||||
"range": "bytes=0-",
|
||||
"code": "ERR_STREAM_PREMATURE_CLOSE",
|
||||
"aborted": true,
|
||||
"responseDestroyed": true,
|
||||
"responseFinished": false
|
||||
},
|
||||
{
|
||||
"pathname": "/assets/sample/video.mp4",
|
||||
"range": "bytes=0-",
|
||||
"code": "ERR_STREAM_PREMATURE_CLOSE",
|
||||
"aborted": true,
|
||||
"responseDestroyed": true,
|
||||
"responseFinished": false
|
||||
},
|
||||
{
|
||||
"pathname": "/assets/sample/video.mp4",
|
||||
"range": "bytes=0-",
|
||||
"code": "ERR_STREAM_PREMATURE_CLOSE",
|
||||
"aborted": true,
|
||||
"responseDestroyed": true,
|
||||
"responseFinished": false
|
||||
},
|
||||
{
|
||||
"pathname": "/assets/sample/video.mp4",
|
||||
"range": "bytes=0-",
|
||||
"code": "ERR_STREAM_PREMATURE_CLOSE",
|
||||
"aborted": true,
|
||||
"responseDestroyed": true,
|
||||
"responseFinished": false
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Controlled probe loads actual restored scripts/media; not the complete editor or iframe UI.",
|
||||
"No remote Pages source/domain/deployment was changed.",
|
||||
"Thumbnail route bytes verified; extraction and missing historical npm archive are separate gates."
|
||||
]
|
||||
},
|
||||
"previousArchive": {
|
||||
"path": "refactor/.cache/ci02-gh-pages-a6d2b81.zip",
|
||||
"sha256": "e5b949d3db666da647425ef499e94873bb802d645a8bb98eb22db0193ca2c529",
|
||||
"bytes": 213358750,
|
||||
"commit": "a6d2b81d05c68c8c78cbe62ca0b06761c983f8b8",
|
||||
"method": "git archive --format=zip of locally available commit; no remote writes",
|
||||
"strictGitBlobCheckPassed": false,
|
||||
"changedFiles": 403,
|
||||
"allDifferencesCrlfOnly": true,
|
||||
"example": {
|
||||
"file": "CNAME",
|
||||
"bytes": 22,
|
||||
"expectedBytes": 21,
|
||||
"crlfOnly": true
|
||||
},
|
||||
"completeDifferences": "refactor/.cache/rel04-pages-old-archive-differences.json",
|
||||
"exactSingleFileControl": "git -c core.autocrlf=false -c core.eol=lf archive of the same CNAME produces the exact 21-byte Git blob"
|
||||
},
|
||||
"retainedFailures": [
|
||||
{
|
||||
"report": "refactor/.cache/pages-recovery/run-pbolez/report.json",
|
||||
"log": "refactor/.cache/rel04-pages-recovery.log",
|
||||
"result": "Original ZIP CNAME size 22 versus Git blob 21; not counted as restoration pass"
|
||||
},
|
||||
{
|
||||
"report": "refactor/.cache/pages-recovery/run-485vhu/browser-first.json",
|
||||
"log": "refactor/.cache/rel04-pages-recovery-browser.log",
|
||||
"result": "Six playback cases passed, but five premature stream-close events caused overall failure. Final check retains cancellations only for the exact media path with request.aborted=true and ERR_STREAM_PREMATURE_CLOSE."
|
||||
}
|
||||
],
|
||||
"engineering": {
|
||||
"status": "passed",
|
||||
"log": "refactor/.cache/rel04-pages-engineering.log",
|
||||
"exitCode": 0,
|
||||
"commands": [
|
||||
"scoped ESLint for recovery scripts/test and inventory generator",
|
||||
"node --test test/pages-artifact.test.js test/pages-recovery.test.js",
|
||||
"node node_modules/typescript/bin/tsc -p scripts/tsconfig.library.json --noEmit",
|
||||
"yarn check:rollback-inventory",
|
||||
"yarn check:toolchain --strict",
|
||||
"node refactor/scripts/plan.mjs --check",
|
||||
"git diff --check"
|
||||
]
|
||||
},
|
||||
"limitations": [
|
||||
"Local complete old gh-pages Git snapshot recovery, not a remote Pages rollback.",
|
||||
"12 HTTP route checks and six controlled playback pages are not full historical editor, iframe or SDK/device acceptance.",
|
||||
"Restored Thumbnail 4.4.0 website runtime is not the missing original npm 3.5.31 tarball; package rollback alternative remains open.",
|
||||
"Formal publication batches must preserve and revalidate their actual pre-upgrade artifact and final candidate."
|
||||
]
|
||||
}
|
||||
@@ -639,9 +639,14 @@
|
||||
"currentDependencies": {},
|
||||
"currentPeers": {},
|
||||
"currentOptionalDependencies": {},
|
||||
"restore": "Previously verified Pages artifact, exact source SHA and URL map; npm rollback does not apply.",
|
||||
"status": "previous-deployment-artifact-required",
|
||||
"evidence": "refactor/pages-deployment.md"
|
||||
"restore": "Preserve the canonical old Pages ZIP and its Git-blob report; local recovery verified, remote deployment restoration still requires its own gate. npm rollback does not apply.",
|
||||
"status": "local-restore-verified-remote-gate-open",
|
||||
"evidence": "refactor/baselines/pages-recovery-validation.json",
|
||||
"localRecovery": {
|
||||
"commit": "a6d2b81d05c68c8c78cbe62ca0b06761c983f8b8",
|
||||
"archiveSha256": "692e190df068d584f460a7c29e5b0a9e565a0a793579ade532f36788731f5faf",
|
||||
"files": 538
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# REL-04 完整旧站点恢复与归档换行修正
|
||||
|
||||
完整旧站点的本地恢复已验证,REL-04 继续 doing:Thumbnail 原 npm 包/完整替代
|
||||
方案仍缺,远端部署恢复由 CI-04 承担,正式批次仍须绑定其实际产物。
|
||||
|
||||
## 已复现问题与处理
|
||||
|
||||
CI-02 保存的固定 gh-pages ZIP 哈希没有变化,但它受本机 `core.autocrlf=true`
|
||||
影响,403 个文本文件被转换为 CRLF,无法通过原始 Git blob 逐字节核验。
|
||||
例如 CNAME 的 Git 内容为 21 字节,旧 ZIP 为 22 字节。全部 403 处差异经诊断
|
||||
均为 CRLF 转换,没有把该诊断的归一化结果当作精确恢复通过。
|
||||
|
||||
用同一个文件和提交,显式 `core.autocrlf=false`、`core.eol=lf` 生成 ZIP,CNAME
|
||||
恢复为原始 21 字节。最终对完整站点重新归档,538 个文件全部与 Git blob 一致。
|
||||
原 ZIP 和首次失败均保留;没有改本机/仓库 Git 配置。Git 换行配置行为参考
|
||||
[官方属性说明](https://git-scm.com/docs/gitattributes),具体归档差异以本地对照证据为准。
|
||||
|
||||
## 结构与实际演练
|
||||
|
||||
- `scripts/pages-recovery.mjs` 校验旧归档身份,读取固定 Git tree,在命令级固定换行设置生成完整 ZIP;提取前检查成员路径/清单,不执行归档中的代码。
|
||||
- `scripts/pages/recovery.ts` 核对所有文件、字节数和 Git blob;恢复前验证准备目录,再保留失败目录并替换演练站点。第二次 rename 失败会尝试恢复原目录,不声称远端或多请求原子切换。
|
||||
- 先在隔离站点损坏核心脚本、修改 CNAME、删除 iframe.html、新增遗留文件,再证明损坏检查失败并完整恢复。没有编辑工作区 docs/dist。
|
||||
- `test/pages-recovery.test.js` 复现同长度内容损坏和多余文件,验证预检失败不触碰当前站点,成功后保留原失败副本。已接入普通 Node/Pages 测试。
|
||||
- `test/pages-recovery-browser.mjs` 从恢复后的目录提供真实 HTTP/Range 和原始 HTML 字节,12 个历史路径响应哈希一致,三引擎 UMD/ESM 的 6 个真实播放/暂停/seek/章节DOM/销毁检查通过。
|
||||
|
||||
首轮浏览器六项播放均通过,但 HTTP 媒体传输的 premature close 导致总结果失败。
|
||||
最终只把准确媒体路径、`request.aborted=true`、`ERR_STREAM_PREMATURE_CLOSE` 三者
|
||||
同时成立的传输中止单独记录,其他服务错误仍失败;没有放宽播放器断言或增加超时。
|
||||
|
||||
新增 `test:rollback:pages` 和 `test:rollback:pages:browser`,不增加依赖。维护说明
|
||||
见 [Pages 工具](../../scripts/pages/README.md) 和 [恢复流程](../pages-deployment.md)。
|
||||
完整指纹与结果见 [机器证据](../baselines/pages-recovery-validation.json)。
|
||||
|
||||
本次只验证固定旧站点在本地的完整恢复。Thumbnail 网站运行文件存在,不等于找回
|
||||
它的原始 npm 包;本轮没有重跑其图片提取。未切换 Pages source/域名、未运行
|
||||
远端 workflow、未推送或发布。回退工程实现可撤销本提交,远端状态不会变化。
|
||||
@@ -47,6 +47,17 @@ CI-02交付本地配置与预检;CI-04负责实际远端运行和配置切换
|
||||
|
||||
## 恢复
|
||||
|
||||
REL-04 已完成[本地完整恢复演练](changes/2026-09-15-REL-04-pages-recovery.md):
|
||||
固定旧 gh-pages 提交的 538 个文件逐一匹配 Git blob,模拟脚本/域名损坏、文件缺失
|
||||
和新版遗留后恢复成功,12 条 HTTP 路径与六项三引擎播放通过。
|
||||
|
||||
之前的 CI-02 ZIP 受本机换行配置影响,有 403 处 CRLF 文本转换;其原始记录保持
|
||||
不变,但不能用于声称 Git blob 字节完全一致。今后使用命令级
|
||||
`git -c core.autocrlf=false -c core.eol=lf archive` 并核对全部文件,而不是修改
|
||||
全局配置或在核验时忽略换行。新 canonical ZIP 路径和哈希见
|
||||
[机器证据](baselines/pages-recovery-validation.json),与报告一起保存。
|
||||
该本地演练不代替以下远端恢复流程和 CI-04 门槛。
|
||||
|
||||
发生故障先禁用启用变量,避免新部署进入队列;不要取消已经开始的写入或自动force push。
|
||||
保存失败run/部署/请求证据,并重新读取当前source和分支SHA。
|
||||
|
||||
|
||||
+1
-1
@@ -658,7 +658,7 @@
|
||||
- MOD-DEV-01: [记录](changes/2026-09-14-MOD-DEV-01-dev-server.md) [记录](baselines/dev-server-validation.json)
|
||||
- MOD-03: [记录](changes/2026-09-14-MOD-03-slider-updates.md) [记录](baselines/slider-updates-validation.json) [记录](changes/2026-09-14-MOD-03-performance-acceptance.md) [记录](baselines/core-performance-validation.json)
|
||||
- REL-08: [记录](changes/2026-09-14-REL-08-release-ledger.md) [记录](baselines/release-ledger-validation.json) [记录](release-ledger.md) [记录](release-ledger.json)
|
||||
- REL-04: [记录](changes/2026-09-15-REL-04-consumer-checkpoint.md) [记录](rollback-rehearsal.md) [记录](baselines/rollback-consumer-validation.json) [记录](changes/2026-09-15-REL-04-workflows-checkpoint.md) [记录](rollback-inventory.md) [记录](baselines/rollback-workflows-validation.json)
|
||||
- REL-04: [记录](changes/2026-09-15-REL-04-consumer-checkpoint.md) [记录](rollback-rehearsal.md) [记录](baselines/rollback-consumer-validation.json) [记录](changes/2026-09-15-REL-04-workflows-checkpoint.md) [记录](rollback-inventory.md) [记录](baselines/rollback-workflows-validation.json) [记录](changes/2026-09-15-REL-04-pages-recovery.md) [记录](baselines/pages-recovery-validation.json)
|
||||
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md) [记录](type-compatibility-policy.md) [记录](baselines/factory-compatibility-validation.json) [记录](changes/2026-09-13-PKG-FACTORY-01-compatible-types.md)
|
||||
- CORE-25: [记录](changes/2026-09-13-CORE-25-defaults-ssr.md) [记录](baselines/defaults-ssr-validation.json)
|
||||
- ENG-12: [记录](changes/2026-09-13-ENG-12-library-public.md) [记录](baselines/library-public-validation.json)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 进度与证据
|
||||
|
||||
## REL-04 完整旧站点恢复检查点
|
||||
|
||||
修正 Git archive 的本机换行影响:原 ZIP 有 403 处 CRLF 差异,保留失败证据;
|
||||
命令级固定换行设置后,538 个文件与原始 Git blob 全部一致。隔离站点的脚本/
|
||||
域名损坏、缺失和遗留文件恢复通过,12 条 HTTP 路径及三引擎六项播放验证通过。
|
||||
已新增可重跑脚本、预检负例和[维护记录](changes/2026-09-15-REL-04-pages-recovery.md)。
|
||||
远端 Pages 恢复未执行,Thumbnail 原包/完整替代方案仍缺,REL-04 继续 doing。
|
||||
|
||||
## REL-04 更名回退与主线工作流检查点
|
||||
|
||||
完成旧 iframe 包→新 tool 包→恢复旧包的实际冻结安装、应用导入/类型/成员核验,
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
| artplayer-proxy-mediabunny | 2.0.0 | artplayer-proxy-mediabunny@1.2.0 | mediabunny@^1.43.1 | archive-verified-batch-rehearsal-required |
|
||||
| artplayer-tool-iframe | 2.0.0 | artplayer-plugin-iframe@1.0.0 | 无声明 | archive-verified-batch-rehearsal-required |
|
||||
| artplayer-tool-thumbnail | 5.0.0 | 完整旧产物尚缺 | 见原有门槛 | complete-rollback-artifact-required |
|
||||
| artplayer-vitepress | 2.0.0 | 已验证 Pages 产物 | 见原有门槛 | previous-deployment-artifact-required |
|
||||
| artplayer-vitepress | 2.0.0 | 已验证 Pages 产物 | 见原有门槛 | local-restore-verified-remote-gate-open |
|
||||
|
||||
每包优先恢复升级前已验证的应用代码、package.json 和 yarn.lock,再执行 `yarn install --frozen-lockfile`;
|
||||
离线恢复还需保存所需 tarball/依赖缓存。单独把版本改回去并重新解析依赖,不能替代已测试锁文件。
|
||||
|
||||
@@ -109,7 +109,9 @@ HEAD...origin/master` 及修复 SHA,再审查每个上游 diff。找出迁移
|
||||
本次再次 fetch 后 master 仍无新增提交;真实仓库没有需要应用的修复。冻结起点和
|
||||
当前差分记录在[后续检查点证据](baselines/rollback-workflows-validation.json)。
|
||||
|
||||
Thumbnail 的完整旧 npm tarball 缺失、文档站的真实部署恢复继续受原有发布门槛
|
||||
约束,REL-04 保持 doing,继续准备完整回退替代产物/站点恢复证据。正式每批需要
|
||||
文档站的固定旧快照已通过[完整本地恢复](changes/2026-09-15-REL-04-pages-recovery.md),
|
||||
包括全部 538 个文件、12 条 HTTP 路径和六项播放检查。Thumbnail 的完整旧 npm
|
||||
tarball 缺失、文档站的真实远端部署恢复继续受原有发布门槛约束;REL-04 保持 doing,
|
||||
继续准备 Thumbnail 的完整回退替代方案。正式每批需要
|
||||
重新绑定实际 next-major 候选及旧包产物并重跑对应验证。
|
||||
不执行 npm tag 修改、unpublish、推送或部署,不把本演练当作这些动作的授权。
|
||||
@@ -16,7 +16,10 @@ for (const pkg of ledger.packages) {
|
||||
const current = read(`packages/${pkg.name}/package.json`)
|
||||
const row = { name: pkg.name, targetVersion: pkg.targetVersion, distribution: pkg.distribution, history: pkg.history, currentDependencies: current.dependencies || {}, currentPeers: current.peerDependencies || {}, currentOptionalDependencies: current.optionalDependencies || {} }
|
||||
if (pkg.distribution === 'site') {
|
||||
rows.push({ ...row, restore: 'Previously verified Pages artifact, exact source SHA and URL map; npm rollback does not apply.', status: 'previous-deployment-artifact-required', evidence: 'refactor/pages-deployment.md' })
|
||||
const recovery = read('refactor/baselines/pages-recovery-validation.json')
|
||||
assert.equal(recovery.localRestorationPassed, true)
|
||||
assert.equal(recovery.remoteRestorationVerified, false)
|
||||
rows.push({ ...row, restore: 'Preserve the canonical old Pages ZIP and its Git-blob report; local recovery verified, remote deployment restoration still requires its own gate. npm rollback does not apply.', status: 'local-restore-verified-remote-gate-open', evidence: 'refactor/baselines/pages-recovery-validation.json', localRecovery: { commit: recovery.archive.commit, archiveSha256: recovery.archive.sha256, files: recovery.files.count } })
|
||||
continue
|
||||
}
|
||||
if (pkg.distribution === 'recovered-npm') {
|
||||
|
||||
+3
-1
@@ -4808,7 +4808,9 @@
|
||||
"baselines/rollback-consumer-validation.json",
|
||||
"changes/2026-09-15-REL-04-workflows-checkpoint.md",
|
||||
"rollback-inventory.md",
|
||||
"baselines/rollback-workflows-validation.json"
|
||||
"baselines/rollback-workflows-validation.json",
|
||||
"changes/2026-09-15-REL-04-pages-recovery.md",
|
||||
"baselines/pages-recovery-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { readJson, workspace, writeJson } from './package-consumer.mjs'
|
||||
import { digest, inside } from './pages/artifact.ts'
|
||||
import { replaceRehearsalSite, verifyRestoredTree } from './pages/recovery.ts'
|
||||
|
||||
assert.equal(process.versions.node, fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim(), 'Use pinned Node')
|
||||
const baseline = readJson(path.join(workspace, 'refactor/baselines/pages-artifact-validation.json'))
|
||||
const archived = baseline.remote.rollbackArchive
|
||||
assert(/^[a-f0-9]{40}$/.test(archived.commit))
|
||||
const previousArchive = inside(workspace, archived.path)
|
||||
assert.deepEqual(digest(previousArchive), { bytes: archived.bytes, sha256: archived.sha256 }, 'Frozen Pages archive changed')
|
||||
const git = args => execFileSync('git', args, { cwd: workspace, encoding: 'utf8', windowsHide: true })
|
||||
assert.equal(git(['rev-parse', '--show-object-format']).trim(), 'sha1', 'Recovery verifier expects Git SHA-1 object IDs')
|
||||
const tree = git(['ls-tree', '-r', '-l', '-z', archived.commit]).split('\0').filter(Boolean)
|
||||
const expected = {}
|
||||
for (const entry of tree) {
|
||||
const match = /^(100644|100755) blob ([a-f0-9]{40}) +(\d+)\t(.+)$/s.exec(entry)
|
||||
assert(match, 'Site snapshot contains unsupported Git entries')
|
||||
const [, , objectId, bytes, relative] = match
|
||||
inside(workspace, relative)
|
||||
assert(!Object.hasOwn(expected, relative), 'Duplicate Git path')
|
||||
expected[relative] = { objectId, bytes: Number(bytes) }
|
||||
}
|
||||
const parent = path.join(workspace, 'refactor/.cache/pages-recovery')
|
||||
fs.mkdirSync(parent, { recursive: true })
|
||||
const output = fs.mkdtempSync(path.join(parent, 'run-'))
|
||||
const report = { task: 'REL-04', capturedAt: new Date().toISOString(), source: git(['rev-parse', 'HEAD']).trim(), node: process.versions.node, passed: false, archived, gitFiles: expected, scope: 'Local restoration of the complete frozen gh-pages snapshot; no deployment or remote setting changes' }
|
||||
try {
|
||||
const archive = path.join(output, 'canonical.zip')
|
||||
const archiveArgs = ['-c', 'core.autocrlf=false', '-c', 'core.eol=lf', 'archive', '--format=zip', `--output=${archive}`, archived.commit]
|
||||
git(archiveArgs)
|
||||
report.canonicalArchive = { command: ['git', ...archiveArgs], ...digest(archive) }
|
||||
const members = execFileSync('tar', ['-tf', archive], { encoding: 'utf8', windowsHide: true }).trim().split(/\r?\n/)
|
||||
assert.equal(new Set(members).size, members.length)
|
||||
for (const member of members) inside(workspace, member.replace(/\/$/, ''))
|
||||
assert.deepEqual(members.filter(member => !member.endsWith('/')).sort(), Object.keys(expected).sort(), 'Archive inventory differs from frozen Git tree')
|
||||
const original = path.join(output, 'original')
|
||||
const site = path.join(output, 'site')
|
||||
const prepared = path.join(output, 'prepared')
|
||||
fs.mkdirSync(original)
|
||||
execFileSync('tar', ['-xf', archive, '-C', original], { windowsHide: true })
|
||||
const before = verifyRestoredTree(original, expected)
|
||||
assert.equal(fs.readFileSync(path.join(original, 'CNAME'), 'utf8').trim(), 'github.artplayer.org')
|
||||
fs.cpSync(original, site, { recursive: true })
|
||||
fs.writeFileSync(path.join(site, 'compiled/artplayer.js'), 'broken candidate fixture\n')
|
||||
fs.writeFileSync(path.join(site, 'CNAME'), 'recovery-fixture.invalid\n')
|
||||
fs.unlinkSync(path.join(site, 'iframe.html'))
|
||||
fs.writeFileSync(path.join(site, 'candidate-only.txt'), 'must disappear after restoration\n')
|
||||
assert.throws(() => verifyRestoredTree(site, expected), /missing or additional files/)
|
||||
fs.cpSync(original, prepared, { recursive: true })
|
||||
const restored = replaceRehearsalSite(output, expected)
|
||||
assert.deepEqual(restored, before)
|
||||
assert.equal(fs.readFileSync(path.join(site, 'CNAME'), 'utf8').trim(), 'github.artplayer.org')
|
||||
assert.equal(fs.readFileSync(path.join(output, 'failed-site/compiled/artplayer.js'), 'utf8'), 'broken candidate fixture\n')
|
||||
assert(!fs.existsSync(path.join(site, 'candidate-only.txt')))
|
||||
report.files = restored
|
||||
report.simulatedChanges = ['modified compiled/artplayer.js', 'changed CNAME', 'deleted iframe.html', 'added candidate-only.txt']
|
||||
report.failedSnapshotRetained = true
|
||||
report.passed = true
|
||||
}
|
||||
catch (error) {
|
||||
report.error = { name: error.name, message: error.message }
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
writeJson(path.join(output, 'report.json'), report)
|
||||
writeJson(path.join(parent, 'latest.json'), { output: path.relative(workspace, output).replaceAll('\\', '/'), passed: report.passed })
|
||||
console.log(`Pages recovery: ${path.relative(workspace, output)}; passed=${report.passed}`)
|
||||
}
|
||||
@@ -41,3 +41,27 @@ Failed staging builds keep their report once the output directory exists. Earlie
|
||||
preflight failures stay in the CI log. Reports are uploaded even on failure; staged
|
||||
files are uploaded for deployment only after preparation and browser checks pass.
|
||||
See `refactor/pages-deployment.md` for remote state, activation and recovery.
|
||||
|
||||
## Complete historical snapshot recovery
|
||||
|
||||
`yarn test:rollback:pages` validates the CI-02 saved ZIP identity and fixed
|
||||
gh-pages Git objects, creates a new ZIP with command-local `core.autocrlf=false`
|
||||
and `core.eol=lf`, and verifies all extracted paths, sizes and Git blob IDs.
|
||||
The initial ZIP used Windows line conversion; its 403 changed text files remain
|
||||
recorded as a failed exact-Git-byte recovery, not silently normalized.
|
||||
|
||||
`recovery.ts` rejects stale or extra files before replacement. The command
|
||||
damages only an isolated copy, restores it from a fully validated prepared
|
||||
directory, and keeps the failed copy. All outputs stay in
|
||||
`refactor/.cache/pages-recovery/run-*`; there is no recursive cleanup or remote
|
||||
mutation. Both the saved CI-02 ZIP and its Git commit must be available before
|
||||
this explicit rehearsal can run. Preserve the canonical ZIP with its report for
|
||||
future recovery; publishing a new candidate requires a fresh pre-upgrade snapshot.
|
||||
|
||||
`yarn test:rollback:pages:browser` accepts the latest successful recovery only,
|
||||
rechecks every restored Git blob, then serves original HTML without dev reload
|
||||
injection. It verifies 12 route bodies and runs six UMD/ESM playback cases in
|
||||
Chromium/Firefox/WebKit using restored scripts and media. Aborted transfers are
|
||||
retained separately only for the exact sample media path and native aborted
|
||||
request condition. This is local snapshot playback evidence, not a complete
|
||||
editor, iframe, Thumbnail extraction, external SDK or remote deployment check.
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import { inside, inventory } from './artifact.ts'
|
||||
|
||||
export interface GitFile { objectId: string, bytes: number }
|
||||
|
||||
export function verifyRestoredTree(root: string, expected: Record<string, GitFile>) {
|
||||
const files = inventory(root)
|
||||
assert.deepEqual(Object.keys(files).sort(), Object.keys(expected).sort(), 'Restored site has missing or additional files')
|
||||
for (const [relative, identity] of Object.entries(expected)) {
|
||||
const bytes = fs.readFileSync(inside(root, relative))
|
||||
assert.equal(bytes.length, identity.bytes, `Restored file size differs: ${relative}`)
|
||||
const objectId = createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex')
|
||||
assert.equal(objectId, identity.objectId, `Restored Git blob differs: ${relative}`)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
export function replaceRehearsalSite(run: string, expected: Record<string, GitFile>) {
|
||||
const root = fs.realpathSync(run)
|
||||
const live = inside(root, 'site')
|
||||
const prepared = inside(root, 'prepared')
|
||||
const failed = inside(root, 'failed-site')
|
||||
assert(!fs.existsSync(failed), 'Previous failed snapshot must not be overwritten')
|
||||
for (const directory of [live, prepared]) {
|
||||
assert.equal(fs.realpathSync(directory), directory, 'Rehearsal directories must not be links')
|
||||
assert(fs.lstatSync(directory).isDirectory())
|
||||
}
|
||||
verifyRestoredTree(prepared, expected)
|
||||
fs.renameSync(live, failed)
|
||||
try {
|
||||
fs.renameSync(prepared, live)
|
||||
}
|
||||
catch (error) {
|
||||
fs.renameSync(failed, live)
|
||||
throw error
|
||||
}
|
||||
return verifyRestoredTree(live, expected)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable antfu/no-top-level-await -- Sequential browser evidence with explicit server/browser cleanup. */
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { chromium, firefox, webkit } from 'playwright'
|
||||
import { hash } from '../refactor/scripts/releases.mjs'
|
||||
import { serveAsset } from '../scripts/library/assets.ts'
|
||||
import { inside } from '../scripts/pages/artifact.ts'
|
||||
import { verifyRestoredTree } from '../scripts/pages/recovery.ts'
|
||||
|
||||
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
|
||||
const selected = read('refactor/.cache/pages-recovery/latest.json')
|
||||
assert(selected.passed)
|
||||
const output = fs.realpathSync(selected.output)
|
||||
assert.equal(path.dirname(output), fs.realpathSync('refactor/.cache/pages-recovery'))
|
||||
const report = read(path.join(output, 'report.json'))
|
||||
assert(report.passed)
|
||||
assert.equal(report.node, process.versions.node)
|
||||
const site = path.join(output, 'site')
|
||||
verifyRestoredTree(site, report.gitFiles)
|
||||
const reports = []
|
||||
const failures = []
|
||||
const cancellations = []
|
||||
const urls = []
|
||||
const server = http.createServer((request, response) => {
|
||||
const pathname = new URL(request.url, 'http://localhost').pathname
|
||||
// Preserve archived HTML response bytes; the development server injects reload code.
|
||||
const relative = pathname.endsWith('/') ? `${pathname.slice(1)}index.html` : pathname.slice(1)
|
||||
if (relative.endsWith('.html') && Object.hasOwn(report.files, relative)) {
|
||||
const bytes = fs.readFileSync(inside(site, relative))
|
||||
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': bytes.length })
|
||||
response.end(request.method === 'HEAD' ? undefined : bytes)
|
||||
return
|
||||
}
|
||||
serveAsset(site, pathname, request, response).catch((error) => {
|
||||
const observation = { pathname, range: request.headers.range || null, code: error.code, aborted: request.aborted, responseDestroyed: response.destroyed, responseFinished: response.writableFinished }
|
||||
if (pathname === '/assets/sample/video.mp4' && error.code === 'ERR_STREAM_PREMATURE_CLOSE' && request.aborted)
|
||||
cancellations.push(observation)
|
||||
else
|
||||
failures.push({ error: String(error), ...observation })
|
||||
response.destroy()
|
||||
})
|
||||
})
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
const origin = `http://127.0.0.1:${server.address().port}`
|
||||
try {
|
||||
for (const file of ['index.html', 'mobile.html', 'iframe.html', 'esm.html', 'document/index.html', 'document/en/index.html', 'compiled/artplayer.js', 'uncompiled/artplayer/index.js', 'compiled/artplayer-tool-thumbnail.js', 'assets/example/tool.thumbnail.js', 'CNAME', '.nojekyll']) {
|
||||
assert(report.files[file], `Missing frozen route: ${file}`)
|
||||
const response = await fetch(`${origin}/${file}`)
|
||||
assert.equal(response.status, 200)
|
||||
const body = new Uint8Array(await response.arrayBuffer())
|
||||
assert.equal(hash(body), report.files[file].sha256, `Restored HTTP bytes differ: ${file}`)
|
||||
urls.push({ file, status: response.status, contentType: response.headers.get('content-type'), sha256: hash(body) })
|
||||
}
|
||||
for (const [engine, launcher] of Object.entries({ chromium, firefox, webkit })) {
|
||||
const browser = await launcher.launch()
|
||||
try {
|
||||
for (const format of ['umd', 'esm']) {
|
||||
const page = await browser.newPage()
|
||||
page.setDefaultTimeout(10000)
|
||||
page.setDefaultNavigationTimeout(10000)
|
||||
const errors = []
|
||||
page.on('pageerror', error => errors.push(String(error)))
|
||||
const core = `compiled/artplayer.${format === 'umd' ? 'js' : 'mjs'}`
|
||||
const chapter = `compiled/artplayer-plugin-chapter.${format === 'umd' ? 'js' : 'mjs'}`
|
||||
const setup = `window.art = new Artplayer({container:'.player',url:'/assets/sample/video.mp4',muted:true,plugins:[artplayerPluginChapter({chapters:[{start:0,end:Infinity,title:'Restored'}]})]}); document.querySelector('button').onclick=()=>art.play();`
|
||||
const scripts = format === 'umd'
|
||||
? `<script src="/${core}"></script><script src="/${chapter}"></script><script>${setup}</script>`
|
||||
: `<script type="module">import Artplayer from '/${core}'; import artplayerPluginChapter from '/${chapter}'; window.Artplayer=Artplayer; ${setup}</script>`
|
||||
await page.route('**/__recovery_probe.html', route => route.fulfill({ contentType: 'text/html', body: `<!doctype html><button>Play</button><div class="player" style="width:640px;height:360px"></div>${scripts}` }))
|
||||
try {
|
||||
await page.goto(`${origin}/__recovery_probe.html`)
|
||||
await page.waitForFunction(() => window.art?.isReady)
|
||||
await page.locator('button').click()
|
||||
await page.waitForFunction(() => window.art.currentTime > 0.2 && window.art.video.videoWidth > 0)
|
||||
await page.evaluate(() => {
|
||||
window.art.pause()
|
||||
window.art.seek = 3
|
||||
})
|
||||
await page.waitForFunction(() => window.art.video.paused && Math.abs(window.art.currentTime - 3) < 0.1)
|
||||
const state = await page.evaluate(() => ({ version: window.Artplayer.version, width: window.art.video.videoWidth, height: window.art.video.videoHeight, time: window.art.currentTime, error: window.art.video.error?.code || 0 }))
|
||||
assert.equal(state.error, 0)
|
||||
assert.equal(await page.locator('.art-chapter').count(), 1)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
assert.equal(await page.locator('.art-video-player').count(), 0)
|
||||
assert.deepEqual(errors, [])
|
||||
reports.push({ engine, version: browser.version(), format, passed: true, state, inputs: { core: report.files[core], chapter: report.files[chapter], media: report.files['assets/sample/video.mp4'] } })
|
||||
process.stdout.write(`${engine}/${format}: restored playback passed\n`)
|
||||
}
|
||||
catch (error) {
|
||||
failures.push(`${engine}/${format}: ${String(error)}`)
|
||||
reports.push({ engine, version: browser.version(), format, passed: false, error: String(error), errors })
|
||||
await page.screenshot({ path: path.join(output, `${engine}-${format}-failure.png`) })
|
||||
}
|
||||
finally { await page.close() }
|
||||
}
|
||||
}
|
||||
finally { await browser.close() }
|
||||
}
|
||||
}
|
||||
finally {
|
||||
server.closeAllConnections()
|
||||
await new Promise(resolve => server.close(resolve))
|
||||
fs.writeFileSync(path.join(output, 'browser.json'), `${JSON.stringify({ task: 'REL-04', restoredReportSha256: hash(fs.readFileSync(path.join(output, 'report.json'))), platform: process.platform, reports, urls, failures, cancellations, limitations: ['Controlled probe loads actual restored scripts/media; not the complete editor or iframe UI.', 'No remote Pages source/domain/deployment was changed.', 'Thumbnail route bytes verified; extraction and missing historical npm archive are separate gates.'] }, null, 2)}\n`)
|
||||
}
|
||||
assert.deepEqual(failures, [])
|
||||
process.stdout.write(`Restored site: ${urls.length} exact HTTP routes and ${reports.length} browser playback cases passed\n`)
|
||||
@@ -0,0 +1,34 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Validate stale bytes and restoration ownership before directory replacement.
|
||||
import test from 'node:test'
|
||||
import { consumerDirectory, removeConsumer } from '../scripts/package-consumer.mjs'
|
||||
import { replaceRehearsalSite, verifyRestoredTree } from '../scripts/pages/recovery.ts'
|
||||
|
||||
const contents = 'old'
|
||||
const expected = { 'index.html': { bytes: contents.length, objectId: createHash('sha1').update(`blob ${contents.length}\0${contents}`).digest('hex') } }
|
||||
test('site recovery rejects same-size corruption and incomplete preparation before replacing live files', () => {
|
||||
const run = consumerDirectory()
|
||||
try {
|
||||
const site = path.join(run, 'site')
|
||||
const prepared = path.join(run, 'prepared')
|
||||
fs.mkdirSync(site)
|
||||
fs.mkdirSync(prepared)
|
||||
fs.writeFileSync(path.join(site, 'index.html'), 'new')
|
||||
fs.writeFileSync(path.join(prepared, 'index.html'), 'bad')
|
||||
assert.throws(() => verifyRestoredTree(prepared, expected), /Git blob differs/)
|
||||
assert.throws(() => replaceRehearsalSite(run, expected), /Git blob differs/)
|
||||
assert.equal(fs.readFileSync(path.join(site, 'index.html'), 'utf8'), 'new')
|
||||
assert(!fs.existsSync(path.join(run, 'failed-site')))
|
||||
fs.writeFileSync(path.join(prepared, 'index.html'), contents)
|
||||
fs.writeFileSync(path.join(prepared, 'extra.js'), '')
|
||||
assert.throws(() => replaceRehearsalSite(run, expected), /missing or additional/)
|
||||
fs.unlinkSync(path.join(prepared, 'extra.js'))
|
||||
replaceRehearsalSite(run, expected)
|
||||
assert.equal(fs.readFileSync(path.join(site, 'index.html'), 'utf8'), 'old')
|
||||
assert.equal(fs.readFileSync(path.join(run, 'failed-site/index.html'), 'utf8'), 'new')
|
||||
}
|
||||
finally { removeConsumer(run) }
|
||||
})
|
||||
Reference in new issue
Block a user