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:
浅念
2026-10-22 15:31:14 +08:00
parent c64f6ac48a
commit 236f3dc4d3
6 changed files with 306 additions and 70 deletions
+36 -1
View File
@@ -193,9 +193,26 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
indexable: false,
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 = {
...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 })
@@ -255,6 +272,24 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
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 () => {
const png = await get('/wp-content/uploads/2026/10/fixture-a.png')
expect(png.status).toBe(200)