fix(preview+pipeline): 资产键编码归一——7 张全角名图片预览 404 修复;证据文档叙述修正
spec 复审发现(浏览器可见缺陷):manifest 资产行 exact_key 有 7 例为裸形 (文件名含全角括号/顿号/间隔号)——html/redirect 键源数据侧已编码、asset 键来自文件名本体是裸形;浏览器对裸非 ASCII 一律百分号编码后发请求,预览 按请求字节逐字匹配 → 裸键永不命中 → 两页 3 张图破图(dist 文件在盘; 生产面 nginx 先解码不受影响)。修补 pipeline.toManifestRoute:exact_key 归一为 URL 线形(可打印 ASCII 原样、其余 UTF-8 百分号编码大写 hex), target_file 仍盘上裸名、DB/渲染快照域不变。 回归三层:build-pipeline 注入裸全角资产行(红验证);preview-server 2c 非 ASCII redirect → Location → 资产终端 200;e2e 断言 ④c(7 条全查 跟随到终端 200)+ ⑦c(manifest 2772 键全 ASCII 不变量)。spec 复审复跑 (新库全链 job e059bf41)23/23 PASS,浏览器实测 3 张原破图全部加载。 证据文档:§5.2 失败计数修正(5×500+2×mojibake=7 例)、§2 ①c 措辞对齐 断言实义(抽前 3 标题)、§5.5 修补记录、计时/产物/job 面更新至复跑值。 验证:vitest 382 + 主题 26 全绿(408);typecheck 过;lint 裸跑零告警。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -344,6 +344,25 @@ function buildRobots(origin: string): string {
|
|||||||
return `User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`
|
return `User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* exact_key 线形归一(URL wire form):可打印 ASCII(含既有 %xx 序列的字面 %)原样,
|
||||||
|
* 其余码位按 UTF-8 百分号编码(大写 hex——WHATWG URL 解析器/浏览器的编码形态;空格
|
||||||
|
* 同路编码为 %20)。Task 8 spec 复审修补:asset 路由的 routePath 来自文件名本体(真实
|
||||||
|
* hesupipe 7 例含全角括号/顿号/间隔号),html/redirect 键在源数据侧已是编码形、asset
|
||||||
|
* 键是裸形——预览按请求字节逐字匹配(浏览器对裸非 ASCII 一律百分号编码后发送),裸键
|
||||||
|
* 永不命中 → 图片 404、redirect Location 跟随断裂。归一只动 manifest 键(服务权威面);
|
||||||
|
* target_file 仍是盘上文件名(裸形——文件系统域不解码),route_registry/渲染快照域
|
||||||
|
* 不变(DB 键保持导入原串)。
|
||||||
|
*/
|
||||||
|
function wireKeyOf(key: string): string {
|
||||||
|
let out = ''
|
||||||
|
for (const ch of key) {
|
||||||
|
const code = ch.codePointAt(0)!
|
||||||
|
out += code > 0x20 && code <= 0x7e ? ch : encodeURIComponent(ch)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
/** route-manifest 行映射:exact_key 带查询形态;target_file 派生映射;redirect 带 location */
|
/** route-manifest 行映射:exact_key 带查询形态;target_file 派生映射;redirect 带 location */
|
||||||
function toManifestRoute(r: RouteEntry, assetsByPath: Map<string, AssetEntry>): RouteManifestRoute {
|
function toManifestRoute(r: RouteEntry, assetsByPath: Map<string, AssetEntry>): RouteManifestRoute {
|
||||||
// redirect 行 redirectStatus 是权威(targetStatus 同值兜底——数据面两处冗余)
|
// redirect 行 redirectStatus 是权威(targetStatus 同值兜底——数据面两处冗余)
|
||||||
@@ -355,7 +374,7 @@ function toManifestRoute(r: RouteEntry, assetsByPath: Map<string, AssetEntry>):
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
const row: RouteManifestRoute = {
|
const row: RouteManifestRoute = {
|
||||||
exact_key: r.queryKey === null ? r.routePath : `${r.routePath}?${r.queryKey}`,
|
exact_key: wireKeyOf(r.queryKey === null ? r.routePath : `${r.routePath}?${r.queryKey}`),
|
||||||
query_policy: r.queryPolicy,
|
query_policy: r.queryPolicy,
|
||||||
query_key: r.queryKey,
|
query_key: r.queryKey,
|
||||||
kind: r.routeKind,
|
kind: r.routeKind,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"verdict": "PASS",
|
"verdict": "PASS",
|
||||||
"generatedAt": "2026-10-22T06:21:14.951Z",
|
"generatedAt": "2026-10-22T07:20:15.759Z",
|
||||||
"env": {
|
"env": {
|
||||||
"node": "v24.13.0",
|
"node": "v24.13.0",
|
||||||
"platform": "win32",
|
"platform": "win32",
|
||||||
@@ -10,55 +10,58 @@
|
|||||||
},
|
},
|
||||||
"chain": {
|
"chain": {
|
||||||
"register": {
|
"register": {
|
||||||
"runId": "20261022T142105-hiapw6",
|
"runId": "20261022T151903-lhppmd",
|
||||||
"siteId": "599b435b-0778-4676-8a95-b134546a1eb8",
|
"siteId": "763a6544-dcdd-43c8-b1a9-11769f4b720a",
|
||||||
"siteCreated": false,
|
"siteCreated": true,
|
||||||
"connectionId": "3c6b9a48-0426-4f01-b822-56c20b5e8208",
|
"connectionId": "05a9f6a7-cf67-4f0f-ac11-a4899378bd08",
|
||||||
"snapshots": [
|
"snapshots": [
|
||||||
{
|
{
|
||||||
"kind": "wxr",
|
"kind": "wxr",
|
||||||
"sha256": "f9bc5d4497a1",
|
"sha256": "f9bc5d4497a1",
|
||||||
"byteSize": 5490399,
|
"byteSize": 5490399,
|
||||||
"reused": true,
|
"reused": false,
|
||||||
"copied": false,
|
"copied": false,
|
||||||
"durationMs": 15.189599999999928
|
"durationMs": 16.857200000000034
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "media_bundle",
|
"kind": "media_bundle",
|
||||||
"sha256": "8b674261c104",
|
"sha256": "8b674261c104",
|
||||||
"byteSize": 172532000,
|
"byteSize": 172532000,
|
||||||
"reused": true,
|
"reused": false,
|
||||||
"copied": false,
|
"copied": false,
|
||||||
"durationMs": 1288.2132000000001
|
"durationMs": 2684.0553999999997
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "manual_url_list",
|
"kind": "manual_url_list",
|
||||||
"sha256": "c935d444f52b",
|
"sha256": "c935d444f52b",
|
||||||
"byteSize": 143945,
|
"byteSize": 143945,
|
||||||
"reused": true,
|
"reused": false,
|
||||||
"copied": false,
|
"copied": false,
|
||||||
"durationMs": 20.976500000000215
|
"durationMs": 24.289099999999962
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"jobId": "9d82df35-5c8a-4345-8156-3319c7ac9d67",
|
"jobId": "c7b4fd9d-61d6-46d5-8022-86259a1675ac",
|
||||||
"status": "succeeded_with_issues",
|
"status": "succeeded_with_issues",
|
||||||
"reused": true,
|
"reused": false,
|
||||||
"reportPath": "D:\\project\\cms-one\\artifacts\\import\\9d82df35-5c8a-4345-8156-3319c7ac9d67\\reports\\import.json"
|
"reportPath": "D:\\project\\cms-one\\artifacts\\import\\c7b4fd9d-61d6-46d5-8022-86259a1675ac\\reports\\import.json"
|
||||||
},
|
},
|
||||||
"publish": {
|
"publish": {
|
||||||
"dryRunCandidates": 0,
|
"dryRunCandidates": 192,
|
||||||
"publishedCount": 0,
|
"publishedCount": 192,
|
||||||
"upToDateCount": 192,
|
"upToDateCount": 192,
|
||||||
"siteConfigRevision": null
|
"siteConfigRevision": {
|
||||||
|
"created": true,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"jobId": "619c440c-f579-4647-99dc-2840dcd36f19",
|
"jobId": "e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e",
|
||||||
"status": "succeeded",
|
"status": "succeeded",
|
||||||
"reused": true,
|
"reused": false,
|
||||||
"inputDir": "D:\\project\\cms-one\\artifacts\\jobs\\619c440c-f579-4647-99dc-2840dcd36f19\\input",
|
"inputDir": "D:\\project\\cms-one\\artifacts\\jobs\\e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e\\input",
|
||||||
"reportPath": "D:\\project\\cms-one\\artifacts\\jobs\\619c440c-f579-4647-99dc-2840dcd36f19\\reports\\build.json",
|
"reportPath": "D:\\project\\cms-one\\artifacts\\jobs\\e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e\\reports\\build.json",
|
||||||
"counts": {
|
"counts": {
|
||||||
"contents": 192,
|
"contents": 192,
|
||||||
"taxonomies": 6,
|
"taxonomies": 6,
|
||||||
@@ -71,14 +74,23 @@
|
|||||||
},
|
},
|
||||||
"issues": [],
|
"issues": [],
|
||||||
"artifacts": {
|
"artifacts": {
|
||||||
"distPath": "D:\\project\\cms-one\\artifacts\\jobs\\619c440c-f579-4647-99dc-2840dcd36f19\\dist",
|
"distPath": "D:\\project\\cms-one\\artifacts\\jobs\\e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e\\dist",
|
||||||
|
"distSha256": "b97ecbdaa37de096a5b780b9b3b2c889e9156a1c2bb06457cff4686436aa6276",
|
||||||
|
"distByteSize": 122929197,
|
||||||
"distFileCount": 1029,
|
"distFileCount": 1029,
|
||||||
"distSha256": "6883e70a1ae31f1f5244fef3315bd57a6c46eb08f078ac84d99373f21c621832",
|
"routeManifestPath": "D:\\project\\cms-one\\artifacts\\jobs\\e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e\\route-manifest.json",
|
||||||
"routeManifestPath": "D:\\project\\cms-one\\artifacts\\jobs\\619c440c-f579-4647-99dc-2840dcd36f19\\route-manifest.json",
|
"routeManifestSha256": "5b11f7f11da66dff6dcfac97bc0324178e8ed146227dc5b3df90c2cd49f2486d",
|
||||||
|
"routeManifestByteSize": 812072,
|
||||||
"sitemapEntries": 190,
|
"sitemapEntries": 190,
|
||||||
"mediaCopied": 836
|
"mediaCopied": 836
|
||||||
},
|
},
|
||||||
"marks": []
|
"marks": [
|
||||||
|
"queued",
|
||||||
|
"snapshotting",
|
||||||
|
"building",
|
||||||
|
"validating",
|
||||||
|
"succeeded"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sqlCounts": {
|
"sqlCounts": {
|
||||||
@@ -116,14 +128,17 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"timings": {
|
"timings": {
|
||||||
"registerWallMs": 1771,
|
"registerWallMs": 2886,
|
||||||
"registerServiceMs": 1750,
|
"registerServiceMs": 2874,
|
||||||
"importMs": 94,
|
"importMs": 57199,
|
||||||
"publishDryRunMs": 87,
|
"publishDryRunMs": 6,
|
||||||
"publishConfirmMs": 77,
|
"publishConfirmMs": 17,
|
||||||
"buildTotalMs": 11875,
|
"buildTotalMs": 9498,
|
||||||
"astroProbeMs": 7486,
|
"buildSnapshotMs": 431,
|
||||||
"smokeMs": 1225
|
"buildBuildingMs": 8880,
|
||||||
|
"buildValidatingReportMs": 174,
|
||||||
|
"astroProbeMs": 4865,
|
||||||
|
"smokeMs": 285
|
||||||
},
|
},
|
||||||
"assertions": [
|
"assertions": [
|
||||||
{
|
{
|
||||||
@@ -196,6 +211,13 @@
|
|||||||
"pass": true,
|
"pass": true,
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "④c 非 ASCII redirect → 资产终端(7 条全查)",
|
||||||
|
"expect": "301 → 跟随 Location 编码形 → 终端 200 + 图片 MIME(exact_key 编码线形命中)",
|
||||||
|
"actual": "[{\"request\":\"/?attachment_id=2835\",\"location\":\"/wp-content/uploads/2024/01/PE-water-supply-pipe-engineering-drawing-of-Wuxi-Wuai-Primary-School-%EF%BC%881%EF%BC%89.jpg\",\"hopStatus\":301,\"terminalStatus\":200,\"terminalContentType\":\"image/jpeg\"},{\"request\":\"/?attachment_id=3125\",\"location\":\"/wp-content/uploads/2024/03/Papua-New-Guinea-Water-Supply-Project-PE-Water-Supply-Pipe%EF%BC%88hesu%EF%BC%89.png\",\"hopStatus\":301,\"terminalStatus\":200,\"terminalContentType\":\"image/png\"},{\"request\":\"/changqing%c2%b7lixiang-cultural-tourism-street-project-steel-mesh-skeleton-pipe-1/\",\"location\":\"/wp-content/uploads/2024/04/Changqing%C2%B7Lixiang-Cultural-Tourism-Street-Project-Steel-Mesh-Skeleton-Pipe-1.png\",\"hopStatus\":301,\"terminalStatus\":200,\"terminalContentType\":\"image/png\"},{\"request\":\"/changqing%c2%b7lixiang-cultural-tourism-street-project-steel-mesh-skeleton-pipe/\",\"location\":\"/wp-content/uploads/2024/04/Changqing%C2%B7Lixiang-Cultural-Tourism-Street-Project-Steel-Mesh-Skeleton-Pipe.jpg\",\"hopStatus\":301,\"terminalStatus\":200,\"terminalContentType\":\"image/jpeg\"},{\"request\":\"/hdpe-carat-pipe/hdpe-carat-pipe-%ef%bc%881%ef%bc%89/\",\"location\":\"/wp-content/uploads/2024/10/HDPE-carat-pipe-%EF%BC%881%EF%BC%89.jpg\",\"hopStatus\":301,\"terminalStatus\":200,\"terminalContentType\":\"image/jpeg\"},{\"request\":\"/product/steel-belt-reinforced-pe-corrugated-pipe/steel-belt-reinforced-pe-corrugated-pipe%e3%80%81-4/\",\"location\":\"/wp-content/uploads/2023/12/Steel-Belt-Reinforced-PE-Corrugated-Pipe%E3%80%81-4.jpg\",\"hopStatus\":301,\"terminalStatus\":200,\"terminalContentType\":\"image/jpeg\"},{\"request\":\"/product/steel-belt-reinforced-pe-corrugated-pipe/steel-belt-reinforced-pe-corrugated-pipe%e3%80%81-5/\",\"location\":\"/wp-content/uploads/2023/12/Steel-Belt-Reinforced-PE-Corrugated-Pipe%E3%80%81-5.jpg\",\"hopStatus\":301,\"terminalStatus\":200,\"terminalContentType\":\"image/jpeg\"}]",
|
||||||
|
"pass": true,
|
||||||
|
"note": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "⑤sitemap 条目数",
|
"id": "⑤sitemap 条目数",
|
||||||
"expect": "= manifest indexable html 数(190)",
|
"expect": "= manifest indexable html 数(190)",
|
||||||
@@ -224,10 +246,17 @@
|
|||||||
"pass": true,
|
"pass": true,
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "⑦manifest 键线形",
|
||||||
|
"expect": "全 2772 键纯 ASCII(编码线形——裸非 ASCII 键 = 预览永不命中)",
|
||||||
|
"actual": "nonAsciiKeys=0",
|
||||||
|
"pass": true,
|
||||||
|
"note": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "⑨门禁 validation_runs",
|
"id": "⑨门禁 validation_runs",
|
||||||
"expect": "job succeeded + validation_run PASS(build succeeded 即含门禁通过)",
|
"expect": "job succeeded + validation_run PASS(build succeeded 即含门禁通过)",
|
||||||
"actual": "job=succeeded runs=[{\"id\":\"6dc3776b-7bd5-4199-aa95-2db8e8a5ea41\",\"status\":\"PASS\",\"rule_version\":\"min-gates-v1\"}] issues=[]",
|
"actual": "job=succeeded runs=[{\"id\":\"6459e0df-dd53-4158-b300-7c610100eeef\",\"status\":\"PASS\",\"rule_version\":\"min-gates-v1\"}] issues=[]",
|
||||||
"pass": true,
|
"pass": true,
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
@@ -536,6 +565,57 @@
|
|||||||
"locationMatch": true
|
"locationMatch": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"a4c": [
|
||||||
|
{
|
||||||
|
"request": "/?attachment_id=2835",
|
||||||
|
"location": "/wp-content/uploads/2024/01/PE-water-supply-pipe-engineering-drawing-of-Wuxi-Wuai-Primary-School-%EF%BC%881%EF%BC%89.jpg",
|
||||||
|
"hopStatus": 301,
|
||||||
|
"terminalStatus": 200,
|
||||||
|
"terminalContentType": "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"request": "/?attachment_id=3125",
|
||||||
|
"location": "/wp-content/uploads/2024/03/Papua-New-Guinea-Water-Supply-Project-PE-Water-Supply-Pipe%EF%BC%88hesu%EF%BC%89.png",
|
||||||
|
"hopStatus": 301,
|
||||||
|
"terminalStatus": 200,
|
||||||
|
"terminalContentType": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"request": "/changqing%c2%b7lixiang-cultural-tourism-street-project-steel-mesh-skeleton-pipe-1/",
|
||||||
|
"location": "/wp-content/uploads/2024/04/Changqing%C2%B7Lixiang-Cultural-Tourism-Street-Project-Steel-Mesh-Skeleton-Pipe-1.png",
|
||||||
|
"hopStatus": 301,
|
||||||
|
"terminalStatus": 200,
|
||||||
|
"terminalContentType": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"request": "/changqing%c2%b7lixiang-cultural-tourism-street-project-steel-mesh-skeleton-pipe/",
|
||||||
|
"location": "/wp-content/uploads/2024/04/Changqing%C2%B7Lixiang-Cultural-Tourism-Street-Project-Steel-Mesh-Skeleton-Pipe.jpg",
|
||||||
|
"hopStatus": 301,
|
||||||
|
"terminalStatus": 200,
|
||||||
|
"terminalContentType": "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"request": "/hdpe-carat-pipe/hdpe-carat-pipe-%ef%bc%881%ef%bc%89/",
|
||||||
|
"location": "/wp-content/uploads/2024/10/HDPE-carat-pipe-%EF%BC%881%EF%BC%89.jpg",
|
||||||
|
"hopStatus": 301,
|
||||||
|
"terminalStatus": 200,
|
||||||
|
"terminalContentType": "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"request": "/product/steel-belt-reinforced-pe-corrugated-pipe/steel-belt-reinforced-pe-corrugated-pipe%e3%80%81-4/",
|
||||||
|
"location": "/wp-content/uploads/2023/12/Steel-Belt-Reinforced-PE-Corrugated-Pipe%E3%80%81-4.jpg",
|
||||||
|
"hopStatus": 301,
|
||||||
|
"terminalStatus": 200,
|
||||||
|
"terminalContentType": "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"request": "/product/steel-belt-reinforced-pe-corrugated-pipe/steel-belt-reinforced-pe-corrugated-pipe%e3%80%81-5/",
|
||||||
|
"location": "/wp-content/uploads/2023/12/Steel-Belt-Reinforced-PE-Corrugated-Pipe%E3%80%81-5.jpg",
|
||||||
|
"hopStatus": 301,
|
||||||
|
"terminalStatus": 200,
|
||||||
|
"terminalContentType": "image/jpeg"
|
||||||
|
}
|
||||||
|
],
|
||||||
"smoke": {
|
"smoke": {
|
||||||
"htmlTotal": 190,
|
"htmlTotal": 190,
|
||||||
"htmlFailures": 0,
|
"htmlFailures": 0,
|
||||||
@@ -544,14 +624,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buildReport": {
|
"buildReport": {
|
||||||
"distPath": "D:\\project\\cms-one\\artifacts\\jobs\\619c440c-f579-4647-99dc-2840dcd36f19\\dist",
|
"distPath": "D:\\project\\cms-one\\artifacts\\jobs\\e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e\\dist",
|
||||||
"distFileCount": 1029,
|
"distFileCount": 1029,
|
||||||
"mediaCopied": 836,
|
"mediaCopied": 836,
|
||||||
"sitemapEntries": 190,
|
"sitemapEntries": 190,
|
||||||
"validation": {
|
"validation": {
|
||||||
"ruleVersion": "min-gates-v1",
|
"ruleVersion": "min-gates-v1",
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
"runId": "6dc3776b-7bd5-4199-aa95-2db8e8a5ea41",
|
"runId": "6459e0df-dd53-4158-b300-7c610100eeef",
|
||||||
"p0Count": 0,
|
"p0Count": 0,
|
||||||
"p2Count": 0
|
"p2Count": 0
|
||||||
},
|
},
|
||||||
@@ -559,6 +639,6 @@
|
|||||||
"rewritten": 386,
|
"rewritten": 386,
|
||||||
"unresolved": 317
|
"unresolved": 317
|
||||||
},
|
},
|
||||||
"durationMs": 11875.096499999992
|
"durationMs": 9485.525300000001
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 阶段 4 真实 hesupipe 端到端验收证据(Plan 4 · Task 8 —— 浏览器时刻)
|
# 阶段 4 真实 hesupipe 端到端验收证据(Plan 4 · Task 8 —— 浏览器时刻)
|
||||||
|
|
||||||
**verdict:PASS**(断言总表 21 项脚本断言全过 + 浏览器时刻 6 截图无破版无横向溢出;两处验收内修补(预览服务器 Location 头 ASCII 化、CSP 放行内联样式)均有回归测试锁定,见 §5;观察项未触发主题补规则,见 §5.1)
|
**verdict:PASS**(断言总表 23 项脚本断言全过 + 浏览器时刻 6 截图无破版无横向溢出;三处验收内修补——预览服务器 Location 头 ASCII 化(§5.2)、CSP 放行内联样式(§5.3)、管线资产键编码归一(§5.5,spec 复审)——均有回归测试锁定;观察项未触发主题补规则,见 §5.1)
|
||||||
|
|
||||||
| 项 | 值 |
|
| 项 | 值 |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
| 环境 | Windows 11 · Node v24.13.0 · pnpm 10.7.1 · tsx 直吃 `.mjs` 编排脚本(`@sf/*` 经根 tsconfig paths 解析,与 sf CLI 同机制) |
|
| 环境 | Windows 11 · Node v24.13.0 · pnpm 10.7.1 · tsx 直吃 `.mjs` 编排脚本(`@sf/*` 经根 tsconfig paths 解析,与 sf CLI 同机制) |
|
||||||
| 数据库 | 本项目 PG(docker `sf-postgres`,postgres:16,localhost:5434)临时库 `sf_phase4_hesupipe`(验后已 DROP,见 §7) |
|
| 数据库 | 本项目 PG(docker `sf-postgres`,postgres:16,localhost:5434)临时库 `sf_phase4_hesupipe`(验后已 DROP,见 §7) |
|
||||||
| 站点/输入 | 复用阶段 3 同源副本(`storage/incoming/` 的 WXR + 媒体 + 侧车;storage/ 不入库)——site `hesupipe-real`、origin=https://hesupipe.com、locale=en |
|
| 站点/输入 | 复用阶段 3 同源副本(`storage/incoming/` 的 WXR + 媒体 + 侧车;storage/ 不入库)——site `hesupipe-real`、origin=https://hesupipe.com、locale=en |
|
||||||
| 全链 job | import `9d82df35-5c8a-4345-8156-3319c7ac9d67`(succeeded_with_issues,51,123.8ms 首跑)· publish 192 → upToDate · build `619c440c-f579-4647-99dc-2840dcd36f19`(succeeded,11,885.4ms 首跑)· validation run `6dc3776b-7bd5-4199-aa95-2db8e8a5ea41` PASS(min-gates-v1,p0=0 p2=0) |
|
| 全链 job(spec 复审复跑,权威) | import `c7b4fd9d-61d6-46d5-8022-86259a1675ac`(succeeded_with_issues,57,198.6ms)· publish 192 → upToDate · build `e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e`(succeeded,9,497.9ms)· validation run `6459e0df-dd53-4158-b300-7c610100eeef` PASS(min-gates-v1,p0=0 p2=0)。首轮 job:import `9d82df35…`(51,123.8ms)· build `619c440c…`(11,885.4ms,artifacts/ 本地留档) |
|
||||||
| 产物面 | dist 1029 文件(190 html + 836 媒体 + sitemap.xml + robots.txt + 404.html)· route-manifest 2772 行(html 190 / redirect 1746 / asset 836)· dist SHA-256 `6883e70a…621832` |
|
| 产物面 | dist 1029 文件(190 html + 836 媒体 + sitemap.xml + robots.txt + 404.html)· route-manifest 2772 行(html 190 / redirect 1746 / asset 836)· dist SHA-256 `b97ecbda…aa6276` |
|
||||||
| 断言明细 | `docs/evidence/phase4-assertions.json`(脚本逐断言落盘,verdict=PASS 21/21) |
|
| 断言明细 | `docs/evidence/phase4-assertions.json`(脚本逐断言落盘,verdict=PASS 23/23) |
|
||||||
| 截图 | `docs/evidence/screenshots/phase4-{mobile-375,desktop-1440}-{home,product-detail,news-detail}.png` ×6(§3) |
|
| 截图 | `docs/evidence/screenshots/phase4-{mobile-375,desktop-1440}-{home,product-detail,news-detail}.png` ×6(§3) |
|
||||||
| 编排脚本 | `scripts/e2e-real.mjs`(`chain` 全链+断言 / `serve` 截图会话常驻预览,两个子命令) |
|
| 编排脚本 | `scripts/e2e-real.mjs`(`chain` 全链+断言 / `serve` 截图会话常驻预览,两个子命令) |
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_h
|
|||||||
|
|
||||||
# 截图会话(serve:对既有 succeeded build 起常驻预览 127.0.0.1:4571,Playwright MCP 对其取证)
|
# 截图会话(serve:对既有 succeeded build 起常驻预览 127.0.0.1:4571,Playwright MCP 对其取证)
|
||||||
DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_hesupipe" \
|
DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_hesupipe" \
|
||||||
pnpm exec tsx scripts/e2e-real.mjs serve --build 619c440c-f579-4647-99dc-2840dcd36f19 --port 4571
|
pnpm exec tsx scripts/e2e-real.mjs serve --build e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e --port 4571
|
||||||
```
|
```
|
||||||
|
|
||||||
互斥纪律(Task 5 备忘):链路执行期间零并发测试/零并发构建(vitest 全套在链路完成后单独跑,见 §5.3);astro 计时探针冻结输入重跑(`SF_BUILD_INPUT` 指向 job input 目录,前后清主题暂存 dist)。
|
互斥纪律(Task 5 备忘):链路执行期间零并发测试/零并发构建(vitest 全套在链路完成后单独跑,见 §5.3);astro 计时探针冻结输入重跑(`SF_BUILD_INPUT` 指向 job input 目录,前后清主题暂存 dist)。
|
||||||
@@ -41,7 +41,7 @@ DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_h
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| ①a | 首页 `/` | HTTP 200 + 真实内容 | 200;站点名在体 + 最新 3 条新闻标题在体(Chile/Tanzania/Burkina Faso 三篇实题) | PASS |
|
| ①a | 首页 `/` | HTTP 200 + 真实内容 | 200;站点名在体 + 最新 3 条新闻标题在体(Chile/Tanzania/Burkina Faso 三篇实题) | PASS |
|
||||||
| ①b | 编码内容页 `/how-to-choose-pe-pipe%ef%bc%9f/` | 同上 | 200;标题"Before choosing a PE pipe, be sure to confirm the purpose"在体 | PASS |
|
| ①b | 编码内容页 `/how-to-choose-pe-pipe%ef%bc%9f/` | 同上 | 200;标题"Before choosing a PE pipe, be sure to confirm the purpose"在体 | PASS |
|
||||||
| ①c | 产品分类列表 `/product-category/hdpe-pipe-fittings/` | 同上 | 200;term 名 + 14 个产品标题在体 | PASS |
|
| ①c | 产品分类列表 `/product-category/hdpe-pipe-fittings/` | 同上 | 200;term 名在体;term 含 14 产品,断言抽前 3 标题在体 | PASS |
|
||||||
| ①d | 产品详情 `/product/ecological-wooden-door/` | 同上 | 200;标题"ECOLOGICAL WOOD DOORS"在体 + 12 个本地化媒体引用 | PASS |
|
| ①d | 产品详情 `/product/ecological-wooden-door/` | 同上 | 200;标题"ECOLOGICAL WOOD DOORS"在体 + 12 个本地化媒体引用 | PASS |
|
||||||
| ①e | 新闻列表 `/news/` | 同上 | 200;最新 5 条标题在体 | PASS |
|
| ①e | 新闻列表 `/news/` | 同上 | 200;最新 5 条标题在体 | PASS |
|
||||||
| ①f | 新闻详情 `/2023-pipe-market-analysis/` | 同上 | 200;标题 + 日期"2023-11-19 13:32:01"在体 | PASS |
|
| ①f | 新闻详情 `/2023-pipe-market-analysis/` | 同上 | 200;标题 + 日期"2023-11-19 13:32:01"在体 | PASS |
|
||||||
@@ -49,12 +49,14 @@ DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_h
|
|||||||
| ③ | SEO 三件套 ×5 页 | title/description 非空 + canonical === origin+路径 | 5/5 逐页精确(含编码路径 canonical `…/how-to-choose-pe-pipe%ef%bc%9f/` 原样保持) | PASS |
|
| ③ | SEO 三件套 ×5 页 | title/description 非空 + canonical === origin+路径 | 5/5 逐页精确(含编码路径 canonical `…/how-to-choose-pe-pipe%ef%bc%9f/` 原样保持) | PASS |
|
||||||
| ④a | 未命中 404 | 404 + noindex | status=404 + `X-Robots-Tag: noindex, nofollow`(自定义 404.html) | PASS |
|
| ④a | 未命中 404 | 404 + noindex | status=404 + `X-Robots-Tag: noindex, nofollow`(自定义 404.html) | PASS |
|
||||||
| ④b | 301 抽检 | `?attachment_id=` 原串 3 + 路径形态 2,逐条 301 + Location 与 manifest 原串一致 | 5/5:全 301、locationMatch 全真(ASCII 位置串逐字节等;非 ASCII 原串的头域派生形见 §5.2) | PASS |
|
| ④b | 301 抽检 | `?attachment_id=` 原串 3 + 路径形态 2,逐条 301 + Location 与 manifest 原串一致 | 5/5:全 301、locationMatch 全真(ASCII 位置串逐字节等;非 ASCII 原串的头域派生形见 §5.2) | PASS |
|
||||||
|
| ④c | 非 ASCII redirect → 资产终端(7 条全查) | 301 → 跟随 Location 编码形 → 终端 200 + 图片 MIME | 7/7(如 `/?attachment_id=2835` → `…School-%EF%BC%881%EF%BC%89.jpg` → 200 image/jpeg);§5.5 修补回归 | PASS |
|
||||||
| ⑤a | sitemap.xml(盘读) | 条目 = manifest indexable html 数 | loc=190 = indexable=190 | PASS |
|
| ⑤a | sitemap.xml(盘读) | 条目 = manifest indexable html 数 | loc=190 = indexable=190 | PASS |
|
||||||
| ⑤b | robots.txt(盘读) | 存在 + Sitemap 行指向 origin | 存在,`Sitemap: https://hesupipe.com/sitemap.xml` | PASS |
|
| ⑤b | robots.txt(盘读) | 存在 + Sitemap 行指向 origin | 存在,`Sitemap: https://hesupipe.com/sitemap.xml` | PASS |
|
||||||
| ⑥ | 浏览器时刻(375/1440 ×3 页) | 无横向溢出 + 视觉无破版 | scrollWidth 360≤375 / 1425≤1440 ×6 组合全过;6 截图样式生效无破版(§3) | PASS |
|
| ⑥ | 浏览器时刻(375/1440 ×3 页) | 无横向溢出 + 视觉无破版 | scrollWidth 360≤375 / 1425≤1440 ×6 组合全过;6 截图样式生效无破版(§3) | PASS |
|
||||||
| ⑦a | html 全量 | 190 行全 200 | ok=190/190(keep-alive 并发 8;content-type=text/html; charset=utf-8) | PASS |
|
| ⑦a | html 全量 | 190 行全 200 | ok=190/190(keep-alive 并发 8;content-type=text/html; charset=utf-8) | PASS |
|
||||||
| ⑦b | redirect 全量 | 1746 行逐条 301 + Location 一致 | ok=1746/1746(含 24 条 `?attachment_id=` exact 行按原串发请求;非 ASCII Location 修复后全过,§5.2) | PASS |
|
| ⑦b | redirect 全量 | 1746 行逐条 301 + Location 一致 | ok=1746/1746(含 24 条 `?attachment_id=` exact 行按原串发请求;非 ASCII Location 修复后全过,§5.2) | PASS |
|
||||||
| ⑧ | 计时对账 | 分钟级预算内 + 与阶段 3 基线同量级 | 全链首跑 ~64s(import 51.1s + build 11.9s);复跑链断言 ~15s(§4) | PASS |
|
| ⑦c | manifest 键线形 | 全 2772 键纯 ASCII(编码线形) | nonAsciiKeys=0(§5.5 修补不变量——裸非 ASCII 键永不匹配浏览器请求形) | PASS |
|
||||||
|
| ⑧ | 计时对账 | 分钟级预算内 + 与阶段 3 基线同量级 | 首轮 ~64s(import 51.1s + build 11.9s);spec 复审复跑 ~70s(import 57.2s + build 9.5s);幂等复跑 ~15s(§4) | PASS |
|
||||||
| ⑨ | 门禁 | validation_runs PASS | job=succeeded · run PASS · min-gates-v1 · p0=0 p2=0 · issues=[] | PASS |
|
| ⑨ | 门禁 | validation_runs PASS | job=succeeded · run PASS · min-gates-v1 · p0=0 p2=0 · issues=[] | PASS |
|
||||||
| 计数 | content_entries | 192(129+48+15) | SQL=192 {news_article:129, product:48, basic_page:15} | PASS |
|
| 计数 | content_entries | 192(129+48+15) | SQL=192 {news_article:129, product:48, basic_page:15} | PASS |
|
||||||
| 计数 | published 指针 | 192 | SQL=192 | PASS |
|
| 计数 | published 指针 | 192 | SQL=192 | PASS |
|
||||||
@@ -77,21 +79,22 @@ DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_h
|
|||||||
| `screenshots/phase4-desktop-1440-news-detail.png` | 1440×900 | 1425 | 标题/日期/正文/头图排版正常 |
|
| `screenshots/phase4-desktop-1440-news-detail.png` | 1440×900 | 1425 | 标题/日期/正文/头图排版正常 |
|
||||||
|
|
||||||
- 溢出判据:`document.body.scrollWidth <= window.innerWidth`(六组合全 false 无横向溢出;360/1425 与视口的 15px 差是滚动条槽位)。
|
- 溢出判据:`document.body.scrollWidth <= window.innerWidth`(六组合全 false 无横向溢出;360/1425 与视口的 15px 差是滚动条槽位)。
|
||||||
|
- 截图取自首轮 build(`619c440c`)产物;spec 复审复跑(`e059bf41`,§5.5)后浏览器复验过同一会话流程(两页 3 张原破图加载成功,见 §5.5)。
|
||||||
- 控制台仅 1 条噪声:`/favicon.ico` 404(源数据无 favicon,路由表无此行——如期 404,装饰性,不修,§5.4)。
|
- 控制台仅 1 条噪声:`/favicon.ico` 404(源数据无 favicon,路由表无此行——如期 404,装饰性,不修,§5.4)。
|
||||||
|
|
||||||
## 4. 耗时分解(vs 基线)
|
## 4. 耗时分解(vs 基线)
|
||||||
|
|
||||||
| 阶段 | 首跑实测 | 对账 |
|
| 阶段 | spec 复审复跑(权威) | 首轮 | 对账 |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| register(快照副本复用) | 服务内 1,750ms(wxr 15.2 / media_bundle 1,288.2 / manual_url_list 21.0——哈希命中即复用,内容寻址副本不再重拷) | 首登 19.3s(阶段 3)的重哈希路径,符合内容寻址预期 |
|
| register(快照副本复用) | 服务内 2,874ms | 1,750ms | 首登 19.3s(阶段 3)的重哈希路径,符合内容寻址预期 |
|
||||||
| import | **51,123.8ms** | 阶段 3 基线 51,768.0ms(+0.7% 机噪,同量级) |
|
| import | **57,198.6ms** | **51,123.8ms** | 阶段 3 基线 51,768.0ms——同量级(-1.2% / +10.5%,机噪区间) |
|
||||||
| publish(dry-run + confirm) | 首跑 192 发布;复跑 dry-run 87ms + confirm 77ms(upToDate=192 幂等) | — |
|
| publish(dry-run + confirm) | 6ms + 17ms(upToDate=192 幂等) | 首跑 192 发布;复跑 87ms + 77ms | — |
|
||||||
| build 总 | **11,885.4ms** | 分相 marks:snapshot=450ms / building=11,240ms / validating+report=183ms |
|
| build 总 | **9,497.9ms** | **11,885.4ms** | 分相 marks:snapshot=431 / building=8,880 / validating+report=174(首轮 450 / 11,240 / 183) |
|
||||||
| └ astro 纯构建探针 | 冻结输入重跑三次 4,567 / 5,650 / 7,486ms | building 相与探针差 ≈ 媒体复制 836 文件(~164.5MiB)+ 主题运行器编排(快照装载/产物整理),量级自洽 |
|
| └ astro 纯构建探针 | 4,865ms | 4,567 / 5,650 / 7,486ms(冻结输入三跑) | building 相与探针差 ≈ 媒体复制 836 文件(~164.5MiB)+ 主题运行器编排,量级自洽 |
|
||||||
| 全量冒烟(190 html + 1746 redirect) | **1,225ms** | keep-alive 并发 8,quiet 服务器 |
|
| 全量冒烟(190 html + 1746 redirect) | **285ms** | 1,225ms | keep-alive 并发 8,quiet 服务器 |
|
||||||
| 复跑链(幂等命中) | register 1.8s + import 复用 94ms + publish 164ms + build 复用(不重跑,读原报告) | 断言二次全过(21/21) |
|
| 幂等复跑链(同库重提) | — | register 1.8s + import 复用 94ms + publish 164ms + build 复用(不重跑,读原报告) | 断言二次全过(首轮 21/21) |
|
||||||
|
|
||||||
分钟级预算内:全链首跑 ~64s(含发布);验收复跑 ~15s。
|
分钟级预算内:首轮全链 ~64s、spec 复审复跑(新库全量,含 §5.5 修补验证)~70s、幂等复跑 ~15s。
|
||||||
|
|
||||||
## 5. 观察项兑现(验收内修补与判定)
|
## 5. 观察项兑现(验收内修补与判定)
|
||||||
|
|
||||||
@@ -106,7 +109,7 @@ DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_h
|
|||||||
|
|
||||||
### 5.2 预览服务器修补一:Location 头 ASCII 化(真实缺陷,HTTP 级全量校验显影)
|
### 5.2 预览服务器修补一:Location 头 ASCII 化(真实缺陷,HTTP 级全量校验显影)
|
||||||
|
|
||||||
⑦b 首轮 1739/1746:manifest location 含非 ASCII 码位(全角括号`(1)`、间隔号`·`)时,`res.setHeader('Location', 原串)` 触发 Node `ERR_INVALID_CHAR` 崩 500(2 例)或被按 latin1 写出 mojibake `·`(3 例)。修补:`apps/preview-server/server.mjs` 新增 `asciiLocation()`——非 ASCII 码位按 UTF-8 百分号编码(与 WP/nginx 形态一致,大写 hex),控制字符同路编码(头注入面封死);manifest 原串保持权威,HTML 迁移页内嵌转义原文。断言侧比较改用同规则镜像(`asciiLocationMirror`),预期口径更新为"manifest 原串的 RFC 9110 头域 ASCII 派生形"。回归测试:`tests/integration/preview-server.test.ts` 测试 2b(非 ASCII redirect 行 → 301 + 编码 Location + 迁移页原文)。修后 1746/1746。
|
⑦b 首轮 1739/1746(7 例失败):manifest location 含非 ASCII 码位(全角括号`(1)`、间隔号`·`)时,`res.setHeader('Location', 原串)` 触发 Node `ERR_INVALID_CHAR` 崩 500(5×500)或被按 latin1 写出 mojibake(2×301,`·` → `·`)。修补:`apps/preview-server/server.mjs` 新增 `asciiLocation()`——非 ASCII 码位按 UTF-8 百分号编码(与 WP/nginx 形态一致,大写 hex),控制字符同路编码(头注入面封死);manifest 原串保持权威,HTML 迁移页内嵌转义原文。断言侧比较改用同规则镜像(`asciiLocationMirror`),预期口径更新为"manifest 原串的 RFC 9110 头域 ASCII 派生形"。回归测试:`tests/integration/preview-server.test.ts` 测试 2b(非 ASCII redirect 行 → 301 + 编码 Location + 迁移页原文)。修后 1746/1746。
|
||||||
|
|
||||||
### 5.3 预览服务器修补二:CSP 放行内联样式(浏览器时刻显影,HTTP 级不可见)
|
### 5.3 预览服务器修补二:CSP 放行内联样式(浏览器时刻显影,HTTP 级不可见)
|
||||||
|
|
||||||
@@ -115,7 +118,20 @@ DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_h
|
|||||||
### 5.4 不修项(如实记录)
|
### 5.4 不修项(如实记录)
|
||||||
|
|
||||||
- `/favicon.ico` 404:源数据无 favicon、路由表无行,预览 404 是正确行为;控制台一条装饰性噪声。
|
- `/favicon.ico` 404:源数据无 favicon、路由表无行,预览 404 是正确行为;控制台一条装饰性噪声。
|
||||||
- 集成测试复跑:`pnpm vitest run tests/integration/preview-server.test.ts` 14/14 过(含 2b/6),与链路互斥执行。
|
- 集成测试复跑:`pnpm vitest run tests/integration/preview-server.test.ts` 15/15 过(含 2b/2c/6),与链路互斥执行。
|
||||||
|
|
||||||
|
### 5.5 管线修补三:资产 exact_key 编码归一(spec 复审发现,浏览器可见缺陷)
|
||||||
|
|
||||||
|
**缺陷**:manifest 资产行的 exact_key 有 7 例为裸形(文件名含全角括号/顿号/间隔号,如 `/wp-content/uploads/2024/10/HDPE-carat-pipe-(1).jpg`)——html/redirect 键在源数据侧已是百分号编码形,asset 键来自文件名本体是裸形。浏览器对裸非 ASCII 一律按 WHATWG URL 规则百分号编码(大写 hex)后发请求,预览按请求字节逐字匹配 → 裸键永不命中 → `/hdpe-carat-pipe/` 与 `/product/steel-belt-reinforced-pe-corrugated-pipe/` 共 3 张图破图(dist 文件实际在盘;生产面 nginx 先解码再查文件,不受影响——预览面缺陷)。
|
||||||
|
|
||||||
|
**修补**(`apps/api/src/modules/build/pipeline.ts` `wireKeyOf()`):manifest 生成(`toManifestRoute`)对 exact_key 归一为 URL 线形——可打印 ASCII(含既有 %xx 序列的字面 `%`)原样,其余码位按 UTF-8 百分号编码(大写 hex,空格同路编码)——与 §5.2 `asciiLocation` 同族规则、与 URL 解析器/浏览器编码形态逐字节一致(首轮 manifest 2772 键实测仅这 7 键异形)。**归一只动 manifest 键**(服务权威面):`target_file` 仍是盘上裸文件名(文件系统域不解码),route_registry/渲染快照/DB 键保持导入原串。
|
||||||
|
|
||||||
|
**回归锁定**(三层):
|
||||||
|
1. `tests/integration/build-pipeline.test.ts`(主题夹具直跑):注入裸全角名资产行 → manifest exact_key = `%EF%BC%881%EF%BC%89` 编码形 + target_file 裸形 + 全键纯 ASCII 不变量(红验证:修补前该用例失败);
|
||||||
|
2. `tests/integration/preview-server.test.ts` 测试 2c:非 ASCII redirect → Location 编码形 → 资产行命中 → 终端 200(+裸形直接请求亦命中);
|
||||||
|
3. `scripts/e2e-real.mjs` 验收断言 ④c(7 条非 ASCII redirect 全查:301 → 跟随 → 终端 200 image/*)+ ⑦c(manifest 2772 键全 ASCII 不变量)。
|
||||||
|
|
||||||
|
**复验**(spec 复审复跑,新库全链 job `e059bf41`):断言 23/23 PASS;manifest 7 资产键全部编码形(`nonAsciiKeys=0`);Playwright 浏览器实测两页 3 张原破图全部加载成功(img.complete=true,naturalWidth 750/800/800),控制台仅余 favicon 404 装饰性噪声。
|
||||||
|
|
||||||
## 6. 差异分析(如实记录,无硬凑)
|
## 6. 差异分析(如实记录,无硬凑)
|
||||||
|
|
||||||
@@ -126,6 +142,6 @@ DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_h
|
|||||||
|
|
||||||
## 7. 清理与保留声明
|
## 7. 清理与保留声明
|
||||||
|
|
||||||
- **serve 会话与临时库**:截图会话(端口 4571)已停;`docker exec sf-postgres psql -U siteforge -d postgres -c "DROP DATABASE sf_phase4_hesupipe WITH (FORCE);"` 验后执行。本项目开发库 `siteforge` 全程零写入(所有命令经 `DATABASE_URL_TEST` 指临时库)。
|
- **serve 会话与临时库**:截图/复验会话(端口 4571)已停;`docker exec sf-postgres psql -U siteforge -d postgres -c "DROP DATABASE sf_phase4_hesupipe WITH (FORCE);"` 验后执行(spec 复审复跑后二次 DROP)。本项目开发库 `siteforge` 全程零写入(所有命令经 `DATABASE_URL_TEST` 指临时库)。
|
||||||
- **保留**:`storage/incoming/` 副本(阶段 3 已声明,约 345MB,gitignore);`artifacts/jobs/619c440c…/`(dist + route-manifest + 报告,artifacts/ 不入库,本地留档);`docs/evidence/screenshots/*.png` ×6 入库。
|
- **保留**:`storage/incoming/` 副本(阶段 3 已声明,约 345MB,gitignore);`artifacts/jobs/619c440c…/` 与 `artifacts/jobs/e059bf41…/`(dist + route-manifest + 报告,artifacts/ 不入库,本地留档);`docs/evidence/screenshots/*.png` ×6 入库。
|
||||||
- cms 源侧(`D:\project\cms`)本任务零访问(输入全用阶段 3 副本)。
|
- cms 源侧(`D:\project\cms`)本任务零访问(输入全用阶段 3 副本)。
|
||||||
|
|||||||
+32
-1
@@ -608,6 +608,28 @@ async function cmdChain(opts) {
|
|||||||
JSON.stringify(a4),
|
JSON.stringify(a4),
|
||||||
a4.every((x) => x.status === 301 && x.locationMatch),
|
a4.every((x) => x.status === 301 && x.locationMatch),
|
||||||
)
|
)
|
||||||
|
// spec 复审修补回归:非 ASCII redirect 跟随到资产终端——Location 编码形请求须命中
|
||||||
|
// manifest 资产行(exact_key 已归一为同一编码线形)→ 200 + 图片 MIME(浏览器真实路径)
|
||||||
|
const nonAsciiRedirects = redirectRows.filter((r) => /\P{ASCII}/u.test(r.location ?? ''))
|
||||||
|
const a4c = []
|
||||||
|
for (const row of nonAsciiRedirects) {
|
||||||
|
const hop = await client.get(row.exact_key)
|
||||||
|
const terminalKey = asciiLocationMirror(row.location ?? '')
|
||||||
|
const terminal = await client.get(terminalKey)
|
||||||
|
a4c.push({
|
||||||
|
request: row.exact_key,
|
||||||
|
location: terminalKey,
|
||||||
|
hopStatus: hop.status,
|
||||||
|
terminalStatus: terminal.status,
|
||||||
|
terminalContentType: terminal.headers['content-type'] ?? null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
record(
|
||||||
|
`④c 非 ASCII redirect → 资产终端(${String(a4c.length)} 条全查)`,
|
||||||
|
'301 → 跟随 Location 编码形 → 终端 200 + 图片 MIME(exact_key 编码线形命中)',
|
||||||
|
JSON.stringify(a4c),
|
||||||
|
a4c.length > 0 && a4c.every((x) => x.hopStatus === 301 && x.terminalStatus === 200 && String(x.terminalContentType).startsWith('image/')),
|
||||||
|
)
|
||||||
|
|
||||||
// ===== 断言 ⑤ sitemap/robots(盘读) =====
|
// ===== 断言 ⑤ sitemap/robots(盘读) =====
|
||||||
console.log('[assert] ⑤ sitemap/robots …')
|
console.log('[assert] ⑤ sitemap/robots …')
|
||||||
@@ -654,6 +676,15 @@ async function cmdChain(opts) {
|
|||||||
`ok=${String(redirectResults.length - redirectFailures.length)}/${String(redirectRows.length)}${redirectFailures.length === 0 ? '' : ` 首失败=${JSON.stringify(redirectFailures.slice(0, 5))}`}`,
|
`ok=${String(redirectResults.length - redirectFailures.length)}/${String(redirectRows.length)}${redirectFailures.length === 0 ? '' : ` 首失败=${JSON.stringify(redirectFailures.slice(0, 5))}`}`,
|
||||||
redirectFailures.length === 0 && redirectRows.length === 1746,
|
redirectFailures.length === 0 && redirectRows.length === 1746,
|
||||||
)
|
)
|
||||||
|
// spec 复审修补不变量:manifest 全键编码线形(纯 ASCII)——裸非 ASCII 键永不匹配
|
||||||
|
// 浏览器请求形(浏览器对裸非 ASCII 一律百分号编码后发送)
|
||||||
|
const nonAsciiKeys = manifest.routes.filter((r) => /\P{ASCII}/u.test(r.exact_key)).map((r) => r.exact_key)
|
||||||
|
record(
|
||||||
|
'⑦manifest 键线形',
|
||||||
|
`全 ${String(manifest.routes.length)} 键纯 ASCII(编码线形——裸非 ASCII 键 = 预览永不命中)`,
|
||||||
|
nonAsciiKeys.length === 0 ? 'nonAsciiKeys=0' : `nonAsciiKeys=${JSON.stringify(nonAsciiKeys.slice(0, 5))}`,
|
||||||
|
nonAsciiKeys.length === 0,
|
||||||
|
)
|
||||||
|
|
||||||
// ===== 断言 ⑨ 门禁全绿(validation_runs PASS) =====
|
// ===== 断言 ⑨ 门禁全绿(validation_runs PASS) =====
|
||||||
console.log('[assert] ⑨ 门禁 …')
|
console.log('[assert] ⑨ 门禁 …')
|
||||||
@@ -698,7 +729,7 @@ async function cmdChain(opts) {
|
|||||||
sqlCounts,
|
sqlCounts,
|
||||||
timings: Object.fromEntries(Object.entries(timings).map(([k, v]) => [k, Math.round(v)])),
|
timings: Object.fromEntries(Object.entries(timings).map(([k, v]) => [k, Math.round(v)])),
|
||||||
assertions: rows,
|
assertions: rows,
|
||||||
detail: { a1, a2, a3, a4, smoke: { htmlTotal: htmlRows.length, htmlFailures: htmlFailures.length, redirectTotal: redirectRows.length, redirectFailures: redirectFailures.length } },
|
detail: { a1, a2, a3, a4, a4c, smoke: { htmlTotal: htmlRows.length, htmlFailures: htmlFailures.length, redirectTotal: redirectRows.length, redirectFailures: redirectFailures.length } },
|
||||||
buildReport: {
|
buildReport: {
|
||||||
distPath: buildReport.artifacts.distPath,
|
distPath: buildReport.artifacts.distPath,
|
||||||
distFileCount: buildReport.artifacts.distFileCount,
|
distFileCount: buildReport.artifacts.distFileCount,
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ describe('构建管线全链(mini 夹具 DB 物化输入)', () => {
|
|||||||
// 哈希链复用:generated_from = 输入 render-manifest 的 route 快照哈希
|
// 哈希链复用:generated_from = 输入 render-manifest 的 route 快照哈希
|
||||||
expect(manifest.generated_from_route_snapshot_hash).toBe(inputManifest.sourceHashes.route)
|
expect(manifest.generated_from_route_snapshot_hash).toBe(inputManifest.sourceHashes.route)
|
||||||
expect(manifest.routes).toHaveLength(routes.length)
|
expect(manifest.routes).toHaveLength(routes.length)
|
||||||
// 序 = routes.json 序(物化排序确定)
|
// 序 = routes.json 序(物化排序确定);键为线形(可打印 ASCII 原样——mini 夹具
|
||||||
|
// 全 ASCII,与非 ASCII 归一规则的同构镜像见"主题夹具直跑"用例的字面断言)
|
||||||
expect(manifest.routes.map((r: any) => r.exact_key)).toStrictEqual(
|
expect(manifest.routes.map((r: any) => r.exact_key)).toStrictEqual(
|
||||||
routes.map((r: any) => (r.queryKey === null ? r.routePath : `${r.routePath}?${r.queryKey}`)),
|
routes.map((r: any) => (r.queryKey === null ? r.routePath : `${r.routePath}?${r.queryKey}`)),
|
||||||
)
|
)
|
||||||
@@ -408,7 +409,7 @@ describe('主题夹具直跑管线(Task 3 render-input 作 SF_BUILD_INPUT)',
|
|||||||
// 输入 = 主题夹具六文件 + 真媒体注入(copyPath/sha256/byteSize 换真值——夹具占位哈希无对应实体)
|
// 输入 = 主题夹具六文件 + 真媒体注入(copyPath/sha256/byteSize 换真值——夹具占位哈希无对应实体)
|
||||||
const inputDir = join(w, 'input')
|
const inputDir = join(w, 'input')
|
||||||
await mkdir(inputDir, { recursive: true })
|
await mkdir(inputDir, { recursive: true })
|
||||||
for (const name of ['site.json', 'content.json', 'taxonomies.json', 'routes.json', 'render-manifest.json']) {
|
for (const name of ['site.json', 'content.json', 'taxonomies.json', 'render-manifest.json']) {
|
||||||
await copyFile(join(THEME_FIXTURE_INPUT, name), join(inputDir, name))
|
await copyFile(join(THEME_FIXTURE_INPUT, name), join(inputDir, name))
|
||||||
}
|
}
|
||||||
const mediaFiles: Array<{ pub: string; buf: Buffer }> = [
|
const mediaFiles: Array<{ pub: string; buf: Buffer }> = [
|
||||||
@@ -416,27 +417,63 @@ describe('主题夹具直跑管线(Task 3 render-input 作 SF_BUILD_INPUT)',
|
|||||||
{ pub: '/wp-content/uploads/2026/10/fixture-b.jpg', buf: mediaBytes(`fx-b-${rand()}`) },
|
{ pub: '/wp-content/uploads/2026/10/fixture-b.jpg', buf: mediaBytes(`fx-b-${rand()}`) },
|
||||||
{ pub: '/wp-content/uploads/2026/10/fixture-c.jpg', buf: mediaBytes(`fx-c-${rand()}`) },
|
{ pub: '/wp-content/uploads/2026/10/fixture-c.jpg', buf: mediaBytes(`fx-c-${rand()}`) },
|
||||||
]
|
]
|
||||||
|
// 非 ASCII 资产键回归(Task 8 spec 复审修补):真实 hesupipe 有 7 例文件名含全角
|
||||||
|
// 括号/顿号/间隔号的资产——routePath 是裸形(DB/文件系统域),而浏览器对裸非 ASCII
|
||||||
|
// 一律百分号编码后发请求,manifest exact_key 若保持裸形则永不命中(预览 404、
|
||||||
|
// redirect Location 跟随断裂)。此处注入裸形资产行,断言 manifest 键归一为编码线形。
|
||||||
|
const rawAssetPath = '/wp-content/uploads/2026/10/HDPE-carat-pipe-(1).jpg'
|
||||||
|
mediaFiles.push({ pub: rawAssetPath, buf: mediaBytes(`fx-fw-${rand()}`) })
|
||||||
|
const routes = JSON.parse(await readFile(join(THEME_FIXTURE_INPUT, 'routes.json'), 'utf8'))
|
||||||
|
routes.push({
|
||||||
|
host: '',
|
||||||
|
routePath: rawAssetPath,
|
||||||
|
routeKind: 'asset',
|
||||||
|
queryPolicy: 'ignore',
|
||||||
|
queryKey: null,
|
||||||
|
targetStatus: '200',
|
||||||
|
targetContentId: null,
|
||||||
|
targetFile: rawAssetPath,
|
||||||
|
redirectStatus: null,
|
||||||
|
redirectLocation: null,
|
||||||
|
indexable: false,
|
||||||
|
priority: 0,
|
||||||
|
verificationStatus: 'verified_asset',
|
||||||
|
})
|
||||||
|
await writeFile(join(inputDir, 'routes.json'), `${canonicalJson(routes)}\n`, 'utf8')
|
||||||
const binDir = join(w, 'media-bin')
|
const binDir = join(w, 'media-bin')
|
||||||
await mkdir(binDir, { recursive: true })
|
await mkdir(binDir, { recursive: true })
|
||||||
const assets = JSON.parse(await readFile(join(THEME_FIXTURE_INPUT, 'assets.json'), 'utf8'))
|
const assets = JSON.parse(await readFile(join(THEME_FIXTURE_INPUT, 'assets.json'), 'utf8'))
|
||||||
expect(assets).toHaveLength(3)
|
expect(assets).toHaveLength(3)
|
||||||
const rewritten: any[] = []
|
const rewritten: any[] = []
|
||||||
for (let i = 0; i < assets.length; i++) {
|
for (let i = 0; i < mediaFiles.length; i++) {
|
||||||
const m = mediaFiles[i]!
|
const m = mediaFiles[i]!
|
||||||
const file = join(binDir, `media-${i}.bin`)
|
const file = join(binDir, `media-${i}.bin`)
|
||||||
await writeFile(file, m.buf)
|
await writeFile(file, m.buf)
|
||||||
rewritten.push({ ...assets[i], copyPath: file, sha256: sha256of(m.buf), byteSize: m.buf.byteLength })
|
rewritten.push(
|
||||||
|
i < assets.length
|
||||||
|
? { ...assets[i], copyPath: file, sha256: sha256of(m.buf), byteSize: m.buf.byteLength }
|
||||||
|
: {
|
||||||
|
publicPath: m.pub,
|
||||||
|
storagePath: '',
|
||||||
|
copyPath: file,
|
||||||
|
sha256: sha256of(m.buf),
|
||||||
|
detectedMimeType: 'image/jpeg',
|
||||||
|
byteSize: m.buf.byteLength,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
altText: null,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
await writeFile(join(inputDir, 'assets.json'), `${canonicalJson(rewritten)}\n`, 'utf8')
|
await writeFile(join(inputDir, 'assets.json'), `${canonicalJson(rewritten)}\n`, 'utf8')
|
||||||
|
|
||||||
const artifactsRoot = join(w, 'artifacts')
|
const artifactsRoot = join(w, 'artifacts')
|
||||||
const result = await runBuildPipeline(db, { jobId, siteId: siteFix.siteId, inputDir, artifactsRoot })
|
const result = await runBuildPipeline(db, { jobId, siteId: siteFix.siteId, inputDir, artifactsRoot })
|
||||||
expect(result.mediaCopied).toBe(3)
|
expect(result.mediaCopied).toBe(4)
|
||||||
expect(result.sitemapEntries).toBe(14)
|
expect(result.sitemapEntries).toBe(14)
|
||||||
|
|
||||||
const distDir = join(artifactsRoot, 'jobs', jobId, 'dist')
|
const distDir = join(artifactsRoot, 'jobs', jobId, 'dist')
|
||||||
expect(result.distPath).toBe(distDir)
|
expect(result.distPath).toBe(distDir)
|
||||||
const routes = JSON.parse(await readFile(join(inputDir, 'routes.json'), 'utf8'))
|
|
||||||
|
|
||||||
// 全量落盘映射:14 html 行 target_file 存在且 isFile(含编码目录与 .html 字面)
|
// 全量落盘映射:14 html 行 target_file 存在且 isFile(含编码目录与 .html 字面)
|
||||||
const htmlRoutes = routes.filter((r: any) => r.routeKind === 'html' && r.queryKey === null)
|
const htmlRoutes = routes.filter((r: any) => r.routeKind === 'html' && r.queryKey === null)
|
||||||
@@ -448,7 +485,8 @@ describe('主题夹具直跑管线(Task 3 render-input 作 SF_BUILD_INPUT)',
|
|||||||
expect(statSync(join(distDir, encoded, 'index.html')).isFile()).toBe(true)
|
expect(statSync(join(distDir, encoded, 'index.html')).isFile()).toBe(true)
|
||||||
expect(statSync(join(distDir, 'news', 'fixture-news-a.html')).isFile()).toBe(true)
|
expect(statSync(join(distDir, 'news', 'fixture-news-a.html')).isFile()).toBe(true)
|
||||||
|
|
||||||
// 3 媒体全量 sha256 校验(非抽检——夹具 3 条全对)
|
// 4 媒体全量 sha256 校验(非抽检——夹具 3 条 + 注入裸全角名 1 条全对;
|
||||||
|
// dist 落盘用 public_path 裸形——文件系统域不解码)
|
||||||
for (const m of mediaFiles) {
|
for (const m of mediaFiles) {
|
||||||
expect(sha256of(await readFile(join(distDir, m.pub)))).toBe(sha256of(m.buf))
|
expect(sha256of(await readFile(join(distDir, m.pub)))).toBe(sha256of(m.buf))
|
||||||
}
|
}
|
||||||
@@ -465,13 +503,30 @@ describe('主题夹具直跑管线(Task 3 render-input 作 SF_BUILD_INPUT)',
|
|||||||
`User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`,
|
`User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`,
|
||||||
)
|
)
|
||||||
|
|
||||||
// route-manifest:15 行全量(html 14 + redirect 1)+ redirect 字段面
|
// route-manifest:16 行全量(html 14 + redirect 1 + asset 1)+ redirect 字段面
|
||||||
const manifest = JSON.parse(await readFile(result.routeManifestPath, 'utf8'))
|
const manifest = JSON.parse(await readFile(result.routeManifestPath, 'utf8'))
|
||||||
expect(isRouteManifest(manifest)).toBe(true)
|
expect(isRouteManifest(manifest)).toBe(true)
|
||||||
expect(manifest.routes).toHaveLength(15)
|
expect(manifest.routes).toHaveLength(16)
|
||||||
expect(manifest.site_id).toBe(siteFix.siteId)
|
expect(manifest.site_id).toBe(siteFix.siteId)
|
||||||
expect(manifest.build_job_id).toBe(jobId)
|
expect(manifest.build_job_id).toBe(jobId)
|
||||||
expect(manifest.origin).toBe(origin)
|
expect(manifest.origin).toBe(origin)
|
||||||
|
// 键线形不变量:全 manifest exact_key 纯 ASCII(裸非 ASCII 键永不匹配浏览器请求形)
|
||||||
|
expect(manifest.routes.every((r: any) => /^[\x20-\x7e]*$/.test(r.exact_key))).toBe(true)
|
||||||
|
// 非 ASCII 资产键归一:exact_key = UTF-8 百分号编码线形(大写 hex——WHATWG URL/
|
||||||
|
// 浏览器编码形态,既有 %xx 序列原样保留不二次编码);target_file 保持裸形(盘上名)
|
||||||
|
const nonAsciiAsset = manifest.routes.find(
|
||||||
|
(r: any) => r.exact_key === '/wp-content/uploads/2026/10/HDPE-carat-pipe-%EF%BC%881%EF%BC%89.jpg',
|
||||||
|
)
|
||||||
|
expect(nonAsciiAsset).toMatchObject({
|
||||||
|
exact_key: '/wp-content/uploads/2026/10/HDPE-carat-pipe-%EF%BC%881%EF%BC%89.jpg',
|
||||||
|
query_policy: 'ignore',
|
||||||
|
query_key: null,
|
||||||
|
kind: 'asset',
|
||||||
|
target_file: rawAssetPath,
|
||||||
|
status: 200,
|
||||||
|
content_type: 'image/jpeg',
|
||||||
|
indexable: false,
|
||||||
|
})
|
||||||
const redirect = manifest.routes.find((r: any) => r.exact_key === '/old-path/')
|
const redirect = manifest.routes.find((r: any) => r.exact_key === '/old-path/')
|
||||||
expect(redirect).toStrictEqual({
|
expect(redirect).toStrictEqual({
|
||||||
exact_key: '/old-path/',
|
exact_key: '/old-path/',
|
||||||
|
|||||||
@@ -193,9 +193,26 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
|
|||||||
indexable: false,
|
indexable: false,
|
||||||
location: '/wp-content/uploads/2024/10/HDPE-carat-pipe-(1)·.jpg',
|
location: '/wp-content/uploads/2024/10/HDPE-carat-pipe-(1)·.jpg',
|
||||||
}
|
}
|
||||||
|
// Task 8 spec 复审修补回归:非 ASCII 文件名资产——盘上/dist 是裸形文件名(文件系统
|
||||||
|
// 域不解码),manifest exact_key 须是编码线形(管线生成侧归一——浏览器对裸非 ASCII
|
||||||
|
// 一律百分号编码后发请求)。与上一行 redirect 构成真实链:跟随 Location 的编码形
|
||||||
|
// 请求必须命中本资产行(2c 断言全链 200)。
|
||||||
|
const nonAsciiAssetFile = 'wp-content/uploads/2024/10/HDPE-carat-pipe-(1)·.jpg'
|
||||||
|
await mkdir(dirname(join(distDir, nonAsciiAssetFile)), { recursive: true })
|
||||||
|
await writeFile(join(distDir, nonAsciiAssetFile), 'nonascii-asset-body\n', 'utf8')
|
||||||
|
const nonAsciiAssetRow: RouteManifestRoute = {
|
||||||
|
exact_key: '/wp-content/uploads/2024/10/HDPE-carat-pipe-%EF%BC%881%EF%BC%89%C2%B7.jpg',
|
||||||
|
query_policy: 'ignore',
|
||||||
|
query_key: null,
|
||||||
|
kind: 'asset',
|
||||||
|
target_file: `/${nonAsciiAssetFile}`,
|
||||||
|
status: 200,
|
||||||
|
content_type: 'image/jpeg',
|
||||||
|
indexable: false,
|
||||||
|
}
|
||||||
const extended: RouteManifest = {
|
const extended: RouteManifest = {
|
||||||
...manifest!,
|
...manifest!,
|
||||||
routes: [...manifest!.routes, ...probeRows, goneRow, nonAsciiRedirectRow],
|
routes: [...manifest!.routes, ...probeRows, goneRow, nonAsciiRedirectRow, nonAsciiAssetRow],
|
||||||
}
|
}
|
||||||
|
|
||||||
preview = createPreviewServer({ distDir, routeManifest: extended, host: '127.0.0.1', port: 0 })
|
preview = createPreviewServer({ distDir, routeManifest: extended, host: '127.0.0.1', port: 0 })
|
||||||
@@ -255,6 +272,24 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
|
|||||||
expect(text).toContain('/wp-content/uploads/2024/10/HDPE-carat-pipe-(1)·.jpg')
|
expect(text).toContain('/wp-content/uploads/2024/10/HDPE-carat-pipe-(1)·.jpg')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('2c. 非 ASCII redirect → Location 编码形 → 资产终端 200(spec 复审修补回归:exact_key 编码线形命中)', async () => {
|
||||||
|
const res = await fetch(new URL('/nonascii-redirect/', `http://127.0.0.1:${port}/`), {
|
||||||
|
redirect: 'manual',
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(301)
|
||||||
|
const location = res.headers.get('location')
|
||||||
|
expect(location).toBe('/wp-content/uploads/2024/10/HDPE-carat-pipe-%EF%BC%881%EF%BC%89%C2%B7.jpg')
|
||||||
|
// 跟随 Location(浏览器/重定向客户端的真实路径——发编码形请求)→ 命中资产行 → 200
|
||||||
|
const terminal = await get(location!)
|
||||||
|
expect(terminal.status).toBe(200)
|
||||||
|
expect(terminal.headers.get('content-type')).toBe('image/jpeg')
|
||||||
|
expect(terminal.bytes).toStrictEqual(Buffer.from('nonascii-asset-body\n', 'utf8'))
|
||||||
|
// 裸形直接请求(未编码客户端形态):URL 解析器编码后同键命中——不可 404
|
||||||
|
const rawForm = await get('/wp-content/uploads/2024/10/HDPE-carat-pipe-(1)·.jpg')
|
||||||
|
expect(rawForm.status).toBe(200)
|
||||||
|
expect(rawForm.bytes).toStrictEqual(Buffer.from('nonascii-asset-body\n', 'utf8'))
|
||||||
|
})
|
||||||
|
|
||||||
it('3. asset 行 200 + MIME:png 真实行 content_type;jpg/webp/pdf 扩展名映射;未知扩展 octet-stream', async () => {
|
it('3. asset 行 200 + MIME:png 真实行 content_type;jpg/webp/pdf 扩展名映射;未知扩展 octet-stream', async () => {
|
||||||
const png = await get('/wp-content/uploads/2026/10/fixture-a.png')
|
const png = await get('/wp-content/uploads/2026/10/fixture-a.png')
|
||||||
expect(png.status).toBe(200)
|
expect(png.status).toBe(200)
|
||||||
|
|||||||
Reference in New Issue
Block a user