Files
cms-website/docs/evidence/phase4-preview.md
T
浅念 c64f6ac48a test(evidence): 阶段4端到端预览验收
Plan 4 Task 8:真实 hesupipe 全链(register 副本复用 → import → publish
→ build → preview)临时库端到端验收,断言 21/21 PASS + 浏览器时刻 6 截图
无破版无横向溢出。新增 scripts/e2e-real.mjs(chain/serve 两子命令)与
docs/evidence/phase4-preview.md + phase4-assertions.json + screenshots/。

验收内修补(预览服务器,均有回归测试锁定):
- Location 头 ASCII 化:非 ASCII 码位按 UTF-8 百分号编码——原样 setHeader
  触发 ERR_INVALID_CHAR 崩 500 或 latin1 mojibake(1746 redirect 全量
  校验显影 2+3 例);manifest 原串保持权威,测试 2b 锁定。
- CSP 放行内联样式:Astro 5 scoped 样式内联 <style> 被 default-src 'self'
  拦截(浏览器时刻显影,HTTP 级不可见);测试 6 断言同步。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 14:33:55 +08:00

132 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段 4 真实 hesupipe 端到端验收证据(Plan 4 · Task 8 —— 浏览器时刻)
**verdict:PASS**(断言总表 21 项脚本断言全过 + 浏览器时刻 6 截图无破版无横向溢出;两处验收内修补(预览服务器 Location 头 ASCII 化、CSP 放行内联样式)均有回归测试锁定,见 §5;观察项未触发主题补规则,见 §5.1)
| 项 | 值 |
|---|---|
| 日期 | 2026-10-22 |
| 环境 | 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) |
| 站点/输入 | 复用阶段 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) |
| 产物面 | 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` |
| 断言明细 | `docs/evidence/phase4-assertions.json`(脚本逐断言落盘,verdict=PASS 21/21) |
| 截图 | `docs/evidence/screenshots/phase4-{mobile-375,desktop-1440}-{home,product-detail,news-detail}.png` ×6(§3) |
| 编排脚本 | `scripts/e2e-real.mjs`(`chain` 全链+断言 / `serve` 截图会话常驻预览,两个子命令) |
## 1. 全链命令原文(仓库根 `D:\project\cms-one`,Git Bash)
```bash
# 临时库(阶段 3 的 sf_phase3_hesupipe 已在其任务末 DROP;本任务新建同构库)
docker exec sf-postgres psql -U siteforge -d postgres -c "CREATE DATABASE sf_phase4_hesupipe;"
DATABASE_URL="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_hesupipe" pnpm --filter @sf/api db:migrate
# 全链 + 断言(register 副本复用 → import → publish dry-run/confirm → build → 进程内预览
# 断言 ①-⑤⑦⑧⑨ + SQL/产物计数对拍 → docs/evidence/phase4-assertions.json)
DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_hesupipe" \
pnpm exec tsx scripts/e2e-real.mjs chain --site hesupipe-real
# 截图会话(serve:对既有 succeeded build 起常驻预览 127.0.0.1:4571,Playwright MCP 对其取证)
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
```
互斥纪律(Task 5 备忘):链路执行期间零并发测试/零并发构建(vitest 全套在链路完成后单独跑,见 §5.3);astro 计时探针冻结输入重跑(`SF_BUILD_INPUT` 指向 job input 目录,前后清主题暂存 dist)。
## 2. 断言总表(预期 vs 实测 vs 判定)
预期来源:Plan 4 Task 8 节 + 快线 §8.7。①-⑤⑦⑨ 与计数对拍由 `chain` 子命令逐项断言(明细 JSON 同步落盘);⑥⑧ 在浏览器时刻/计时节单独取证(§3/§4)。
| # | 断言项 | 预期 | 实测 | 判定 |
|---|---|---|---|---|
| ①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 |
| ①c | 产品分类列表 `/product-category/hdpe-pipe-fittings/` | 同上 | 200;term 名 + 14 个产品标题在体 | PASS |
| ①d | 产品详情 `/product/ecological-wooden-door/` | 同上 | 200;标题"ECOLOGICAL WOOD DOORS"在体 + 12 个本地化媒体引用 | PASS |
| ①e | 新闻列表 `/news/` | 同上 | 200;最新 5 条标题在体 | PASS |
| ①f | 新闻详情 `/2023-pipe-market-analysis/` | 同上 | 200;标题 + 日期"2023-11-19 13:32:01"在体 | PASS |
| ② | 图片本地化抽检 10 张 | 根相对引用 + dist 文件存在 + preview 200 + assets.json 在册 | 10/10:全部 `/wp-content/uploads/…` 根相对、dist 文件在、HTTP 200(11KB-1MB 实尺寸)、assets.json 全在册 | 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 |
| ④b | 301 抽检 | `?attachment_id=` 原串 3 + 路径形态 2,逐条 301 + Location 与 manifest 原串一致 | 5/5:全 301、locationMatch 全真(ASCII 位置串逐字节等;非 ASCII 原串的头域派生形见 §5.2) | PASS |
| ⑤a | sitemap.xml(盘读) | 条目 = manifest indexable html 数 | loc=190 = indexable=190 | 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 |
| ⑦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 |
| ⑧ | 计时对账 | 分钟级预算内 + 与阶段 3 基线同量级 | 全链首跑 ~64s(import 51.1s + build 11.9s);复跑链断言 ~15s(§4) | 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 |
| 计数 | published 指针 | 192 | SQL=192 | PASS |
| 计数 | routes | 2772(190/1746/836) | SQL 分组逐桶吻合 | PASS |
| 计数 | media ready | 836 | SQL=836 | PASS |
| 计数 | audit content.publish | ≥1 | =1(confirm 写入) | PASS |
| 计数 | 构建产物面 | dist≈1029 | distFileCount=1029 · mediaCopied=836 · sitemapEntries=190 | PASS |
## 3. 浏览器时刻(Playwright MCP × 常驻预览 127.0.0.1:4571)
三页取样(与 ① 断言页一致):首页 `/`、产品详情 `/product/ecological-wooden-door/`(12 媒体引用最多)、新闻详情 `/2023-pipe-market-analysis/`。每页双视口截图(整页)。
| 截图 | 视口 | scrollWidth(≤视口) | 视觉判定 |
|---|---|---|---|
| `screenshots/phase4-mobile-375-home.png` | 375×812 | 360 | 样式生效(标题层级/列表/图片自适应),无破版 |
| `screenshots/phase4-mobile-375-product-detail.png` | 375×812 | 360 | 产品标题 + 木门样图横排自适应缩放,无溢出无破图 |
| `screenshots/phase4-mobile-375-news-detail.png` | 375×812 | 360 | 文章标题/日期/正文排版正常,头图自适应 |
| `screenshots/phase4-desktop-1440-home.png` | 1440×900 | 1425 | 桌面版式正常,新闻列表/产品分节无错位 |
| `screenshots/phase4-desktop-1440-product-detail.png` | 1440×900 | 1425 | 图廊 + 描述正文排版正常 |
| `screenshots/phase4-desktop-1440-news-detail.png` | 1440×900 | 1425 | 标题/日期/正文/头图排版正常 |
- 溢出判据:`document.body.scrollWidth <= window.innerWidth`(六组合全 false 无横向溢出;360/1425 与视口的 15px 差是滚动条槽位)。
- 控制台仅 1 条噪声:`/favicon.ico` 404(源数据无 favicon,路由表无此行——如期 404,装饰性,不修,§5.4)。
## 4. 耗时分解(vs 基线)
| 阶段 | 首跑实测 | 对账 |
|---|---|---|
| register(快照副本复用) | 服务内 1,750ms(wxr 15.2 / media_bundle 1,288.2 / manual_url_list 21.0——哈希命中即复用,内容寻址副本不再重拷) | 首登 19.3s(阶段 3)的重哈希路径,符合内容寻址预期 |
| import | **51,123.8ms** | 阶段 3 基线 51,768.0ms(+0.7% 机噪,同量级) |
| publish(dry-run + confirm) | 首跑 192 发布;复跑 dry-run 87ms + confirm 77ms(upToDate=192 幂等) | — |
| build 总 | **11,885.4ms** | 分相 marks:snapshot=450ms / building=11,240ms / validating+report=183ms |
| └ astro 纯构建探针 | 冻结输入重跑三次 4,567 / 5,650 / 7,486ms | building 相与探针差 ≈ 媒体复制 836 文件(~164.5MiB)+ 主题运行器编排(快照装载/产物整理),量级自洽 |
| 全量冒烟(190 html + 1746 redirect) | **1,225ms** | keep-alive 并发 8,quiet 服务器 |
| 复跑链(幂等命中) | register 1.8s + import 复用 94ms + publish 164ms + build 复用(不重跑,读原报告) | 断言二次全过(21/21) |
分钟级预算内:全链首跑 ~64s(含发布);验收复跑 ~15s。
## 5. 观察项兑现(验收内修补与判定)
### 5.1 主题分发观察项:**未触发**
计划预告的三类分发观察项在真实 190 路由集上均未出现,无需补主题规则:
- `/news/` 兄弟特例:无冲突(`/news/` 列表行唯一);
- 嵌套分类前缀:数据集 product_cat 无嵌套(10 个分类全一级);
- 裸 `/product-category/` 前缀:10 个归档页均有 term 且有产物(`hdpe-pipe-fittings` 14 产品为最大者);
- `/products/` 走 basic_page 内容页(阶段 3 既有 exact 冲突事实的延续,generic 模板正常渲染)。
### 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。
### 5.3 预览服务器修补二:CSP 放行内联样式(浏览器时刻显影,HTTP 级不可见)
截图会话首开时页面在浏览器里裸奔(无样式)——Astro 5 把 scoped 样式内联为 `<style>` 元素,`default-src 'self'` 拦掉全部内联样式(控制台 CSP violation)。HTTP 级断言看不到此面(样式不在头域语义内)。修补:CSP 常量改为 `default-src 'self'; style-src 'self' 'unsafe-inline'`(仅样式面放开;脚本/连接/图片仍走 default-src)。回归测试:测试 6 断言更新(/ 与 404 双路径)。修后 6 截图全部样式生效。
### 5.4 不修项(如实记录)
- `/favicon.ico` 404:源数据无 favicon、路由表无行,预览 404 是正确行为;控制台一条装饰性噪声。
- 集成测试复跑:`pnpm vitest run tests/integration/preview-server.test.ts` 14/14 过(含 2b/6),与链路互斥执行。
## 6. 差异分析(如实记录,无硬凑)
1. **编码 html 路由仅 1 条(vs 计划假设的编码内容页更多)**:数据集 ~54 条含中文/全角的 permalink 全部是 attachment 降级 redirect 行(媒体文件目标),有内容的编码路由只有 `/how-to-choose-pe-pipe%ef%bc%9f/` 一条——①b 取样即为该条,编码链路(routes.json → dist 文件名 → manifest → 预览匹配逐字节对齐)在其身上完整验证;⑦a 190 行全 200 亦覆盖全部编码形态。
2. **urlRewrite unresolved=317 非死链**:该计数是"body 内 src/href 有可比对路径但未命中 ready 媒体索引"的引用数——主体是**站内页面链接**(`<a href="https://hesupipe.com/…">`,非媒体、本就不在改写索引内,保持原样正确)+ 外站热链(cn-hdpe.com 等 19+)+ 已知缺失媒体 19 与 WP 尺寸变体 2(阶段 3 §5.2/5.3 同源)。门禁 ①internal-links 对根相对链接全量校验 0 死链(p0=0);dist 实测 460 个根相对图片引用全部命中产物文件。
3. **build 复用路径的 durationMs**:复用命中返回的是原 job 行的 durationMs(11,875.1ms,非本次调用墙钟)——服务口径"未重跑、未改行",产物/计数读原 job 报告,断言不受影响。
4. ** astro 探针三次波动 4.6-7.5s**:同冻结输入,机噪区间;取区间而非单点呈报。
## 7. 清理与保留声明
- **serve 会话与临时库**:截图会话(端口 4571)已停;`docker exec sf-postgres psql -U siteforge -d postgres -c "DROP DATABASE sf_phase4_hesupipe WITH (FORCE);"` 验后执行。本项目开发库 `siteforge` 全程零写入(所有命令经 `DATABASE_URL_TEST` 指临时库)。
- **保留**:`storage/incoming/` 副本(阶段 3 已声明,约 345MB,gitignore);`artifacts/jobs/619c440c…/`(dist + route-manifest + 报告,artifacts/ 不入库,本地留档);`docs/evidence/screenshots/*.png` ×6 入库。
- cms 源侧(`D:\project\cms`)本任务零访问(输入全用阶段 3 副本)。