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
+64 -9
View File
@@ -244,7 +244,8 @@ describe('构建管线全链(mini 夹具 DB 物化输入)', () => {
// 哈希链复用:generated_from = 输入 render-manifest 的 route 快照哈希
expect(manifest.generated_from_route_snapshot_hash).toBe(inputManifest.sourceHashes.route)
expect(manifest.routes).toHaveLength(routes.length)
// 序 = routes.json 序(物化排序确定)
// 序 = routes.json 序(物化排序确定);键为线形(可打印 ASCII 原样——mini 夹具
// 全 ASCII,与非 ASCII 归一规则的同构镜像见"主题夹具直跑"用例的字面断言)
expect(manifest.routes.map((r: any) => r.exact_key)).toStrictEqual(
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 换真值——夹具占位哈希无对应实体)
const inputDir = join(w, 'input')
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))
}
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-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')
await mkdir(binDir, { recursive: true })
const assets = JSON.parse(await readFile(join(THEME_FIXTURE_INPUT, 'assets.json'), 'utf8'))
expect(assets).toHaveLength(3)
const rewritten: any[] = []
for (let i = 0; i < assets.length; i++) {
for (let i = 0; i < mediaFiles.length; i++) {
const m = mediaFiles[i]!
const file = join(binDir, `media-${i}.bin`)
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')
const artifactsRoot = join(w, 'artifacts')
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)
const distDir = join(artifactsRoot, 'jobs', jobId, 'dist')
expect(result.distPath).toBe(distDir)
const routes = JSON.parse(await readFile(join(inputDir, 'routes.json'), 'utf8'))
// 全量落盘映射:14 html 行 target_file 存在且 isFile(含编码目录与 .html 字面)
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, 'news', 'fixture-news-a.html')).isFile()).toBe(true)
// 3 媒体全量 sha256 校验(非抽检——夹具 3 条全对)
// 4 媒体全量 sha256 校验(非抽检——夹具 3 条 + 注入裸全角名 1 条全对;
// dist 落盘用 public_path 裸形——文件系统域不解码)
for (const m of mediaFiles) {
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`,
)
// 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'))
expect(isRouteManifest(manifest)).toBe(true)
expect(manifest.routes).toHaveLength(15)
expect(manifest.routes).toHaveLength(16)
expect(manifest.site_id).toBe(siteFix.siteId)
expect(manifest.build_job_id).toBe(jobId)
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/')
expect(redirect).toStrictEqual({
exact_key: '/old-path/',