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>
This commit is contained in:
浅念
2026-10-22 14:33:55 +08:00
parent 66ca95d37f
commit c64f6ac48a
11 changed files with 1536 additions and 10 deletions
+35 -4
View File
@@ -182,7 +182,21 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
status: 410,
indexable: false,
}
const extended: RouteManifest = { ...manifest!, routes: [...manifest!.routes, ...probeRows, goneRow] }
// Task 8 实证形态:redirect location 含非 ASCII(全角括号/间隔号——真实 hesupipe
// 1746 redirect 全量校验发现原样发头 ERR_INVALID_CHAR 崩 500 / latin1 乱码)
const nonAsciiRedirectRow: RouteManifestRoute = {
exact_key: '/nonascii-redirect/',
query_policy: 'ignore',
query_key: null,
kind: 'redirect',
status: 301,
indexable: false,
location: '/wp-content/uploads/2024/10/HDPE-carat-pipe-(1)·.jpg',
}
const extended: RouteManifest = {
...manifest!,
routes: [...manifest!.routes, ...probeRows, goneRow, nonAsciiRedirectRow],
}
preview = createPreviewServer({ distDir, routeManifest: extended, host: '127.0.0.1', port: 0 })
const addr = await preview.start()
@@ -228,6 +242,19 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
expect(res.headers.get('location')).toBe('/wp-content/uploads/2026/10/fixture-a.png')
})
it('2b. redirect location 含非 ASCII:Location 头按 UTF-8 百分号编码发(不崩 500/不乱码);迁移页内嵌原串', async () => {
const res = await fetch(new URL('/nonascii-redirect/', `http://127.0.0.1:${port}/`), {
redirect: 'manual',
})
expect(res.status).toBe(301)
// 全角( U+FF08 → %EF%BC%88 / )U+FF09 → %EF%BC%89 / · U+00B7 → %C2%B7
expect(res.headers.get('location')).toBe(
'/wp-content/uploads/2024/10/HDPE-carat-pipe-%EF%BC%881%EF%BC%89%C2%B7.jpg',
)
const text = await res.text()
expect(text).toContain('/wp-content/uploads/2024/10/HDPE-carat-pipe-(1)·.jpg')
})
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')
expect(png.status).toBe(200)
@@ -266,13 +293,17 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
expect(res.text.length).toBeLessThan(1024)
})
it('6. 全响应 X-Robots-Tag: noindex, nofollow;HTML 响应 CSP default-src self(asset 无 CSP)', async () => {
it('6. 全响应 X-Robots-Tag: noindex, nofollow;HTML 响应 CSP(style-src 放开内联——Astro scoped 样式;asset 无 CSP)', async () => {
for (const p of ['/', '/definitely-not-a-route/', '/gone-page/', '/wp-content/uploads/2026/10/fixture-a.png']) {
const res = await get(p)
expect(res.headers.get('x-robots-tag'), p).toBe('noindex, nofollow')
}
expect((await get('/')).headers.get('content-security-policy')).toBe("default-src 'self'")
expect((await get('/definitely-not-a-route/')).headers.get('content-security-policy')).toBe("default-src 'self'")
expect((await get('/')).headers.get('content-security-policy')).toBe(
"default-src 'self'; style-src 'self' 'unsafe-inline'",
)
expect((await get('/definitely-not-a-route/')).headers.get('content-security-policy')).toBe(
"default-src 'self'; style-src 'self' 'unsafe-inline'",
)
expect((await get('/wp-content/uploads/2026/10/fixture-a.png')).headers.get('content-security-policy')).toBeNull()
})