gongxiangp2/lib/__tests__/desktop-template-packs.test.ts
WuYongJie 93fc28f1bb feat(client): add native template slots and safe cloud version restore
Support explicit current-output A/B fill, protected edited drafts, scoped cloud update offers and atomic native restore with stale-open CAS. Preserve original unknown requests, improve Pi conversation continuity and native DPI/navigation reads, and publish the authenticated template metadata contract.

Validated: native backend 172 passed/5 environment tests ignored, UI 110, Pi 53, template/rule API 24 and native cloud contract 6; strict Clippy, layering and wasm. Real S01 selected B, edited/undo-redo, PCraft reopen and layered PSD roundtrip pass with exact pixels. Full nine-output generation and Photoshop acceptance remain open.
2026-10-23 21:27:06 +08:00

96 lines
7.1 KiB
TypeScript
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.

/** @jest-environment node */
import { createHash } from 'node:crypto'
import gujia from '../desktop-client/template-packs/gj-desk-dnz0385-native-v1.json'
import yijiada from '../desktop-client/template-packs/yjd-desk-dnz0385-native-v1.json'
import { desktopTemplatePacks, selectDesktopTemplatePacks, templatePackHash, validateDesktopTemplatePack } from '../desktop-client/template-packs'
const copy = () => JSON.parse(JSON.stringify(gujia))
const seal = (pack: ReturnType<typeof copy>) => { pack.contentHash = templatePackHash(pack); return pack }
describe('read-only native template package registry', () => {
test('ships one fixed, hash-verified combination with detail, main and SKU outputs', () => {
const value = desktopTemplatePacks(gujia.brandId, gujia.categoryId)
expect(value.items).toHaveLength(1)
const { pack } = value.items[0]
expect(value.items[0]).toMatchObject({ id: gujia.id, version: gujia.version, contentHash: gujia.contentHash })
expect(pack.outputs.some(output => output.component === 'detail')).toBe(true)
expect(pack.outputs.some(output => output.component === 'main_images')).toBe(true)
expect(pack.outputs.some(output => output.component === 'sku_images')).toBe(true)
const content = copy(); delete content.contentHash
const canonical = JSON.stringify(content, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item)
expect(pack.contentHash).toBe(createHash('sha256').update(canonical).digest('hex'))
expect(pack.source).toMatchObject({ templateId: 'GJ-DESK-DNZ0385' })
})
test('never substitutes a different brand or category and rejects missing or ambiguous query inputs', () => {
const other = '00000000-0000-4000-8000-000000000000'
expect(desktopTemplatePacks(other, gujia.categoryId).items).toEqual([])
expect(desktopTemplatePacks(gujia.brandId, other).items).toEqual([])
for (const [brand, category] of [[null, gujia.categoryId], [gujia.brandId, null], ['', gujia.categoryId], ['../private', gujia.categoryId]]) {
expect(() => desktopTemplatePacks(brand, category)).toThrow('品牌和品类编号')
}
expect(() => selectDesktopTemplatePacks(gujia.brandId, gujia.categoryId, [gujia, gujia])).toThrow('模板包暂时不可用')
})
test('the real Yijiada product receives its own brand package, preserving declared geometry provenance', () => {
expect(yijiada.brandId).not.toBe(gujia.brandId)
const selected = desktopTemplatePacks(yijiada.brandId, yijiada.categoryId)
expect(selected.items).toHaveLength(1)
expect(selected.items[0]).toMatchObject({ id: yijiada.id, contentHash: yijiada.contentHash })
expect(selected.items[0].pack.brandId).toBe(yijiada.brandId)
expect(selected.items[0].id).not.toBe(gujia.id)
expect(desktopTemplatePacks(gujia.brandId, gujia.categoryId).items[0].id).toBe(gujia.id)
})
test('Yijiada field labels describe visible appearances without inherited performance or material claims', () => {
const slots = yijiada.outputs.flatMap(output => output.slots)
const labels = yijiada.outputs.flatMap(output => [output.name, ...output.slots.flatMap(slot => [slot.name, (slot as { label?: string }).label || ''])])
expect(labels.join('\n')).not.toMatch(/承重|涂层|加厚|斜切|稳固|升级|木材|实木|松木|橡胶木|FAS|认证|环保|耐磨|防水|宽大|桌面厚度/i)
const byField = (id: string) => slots.find(slot => slot.fieldId === id)!
expect(byField('S02-I05').name).toBe('外观总览-日常使用场景图')
expect(byField('S02-I03').name).toBe('外观总览-桌面表面图')
expect(byField('S02-I06').name).toBe('外观总览-桌边外观图')
expect(byField('S10-I01').name).toBe('桌边细节图')
const geometry = (outputs: typeof yijiada.outputs) => outputs.map(output => ({ id: output.id, component: output.component, width: output.width, height: output.height,
slots: output.slots.map(slot => ({ fieldId: slot.fieldId, frame: slot.frame, crop: (slot as { crop?: string }).crop, reuseGroup: (slot as { reuseGroup?: string }).reuseGroup })) }))
expect(geometry(yijiada.outputs)).toEqual(geometry(gujia.outputs))
for (const slot of slots.filter(slot => slot.kind === 'text')) expect((slot as { defaultText?: string }).defaultText || '').toBe('')
})
test('returns a detached snapshot and cannot mutate the next caller or package revision', () => {
const first = desktopTemplatePacks(gujia.brandId, gujia.categoryId)
first.items[0].pack.name = 'changed caller copy'
first.items[0].pack.outputs[0].name = 'changed output'
const second = desktopTemplatePacks(gujia.brandId, gujia.categoryId)
expect(second.items[0].pack.name).toBe(gujia.name)
expect(second.items[0].pack.outputs[0].name).toBe(gujia.outputs[0].name)
})
test('changed geometry, text constraints and undeclared package bytes fail hash validation', () => {
const changed = copy(); changed.outputs[0].slots[0].frame.width++
expect(() => validateDesktopTemplatePack(changed)).toThrow('模板包暂时不可用')
const invalid = seal(changed)
expect(() => validateDesktopTemplatePack(invalid)).toThrow('模板包暂时不可用')
const duplicate = copy(); duplicate.outputs[1].slots[0].fieldId = duplicate.outputs[0].slots[0].fieldId
expect(() => validateDesktopTemplatePack(seal(duplicate))).toThrow('模板包暂时不可用')
})
test.each([
'C:\\Users\\54972\\private\\source.psd', 'S:/品牌源文件/模板.psd', '\\\\nas\\private\\source.psd',
'/mnt/private/template.psd', '/home/designer/template.psd', 'file:///D:/private/source.psd',
'记录位置 D:\\source\\secret.psd', '来源:D:\\source\\secret.psd', 'http://192.168.1.5/private', '.private/source.psd',
])('rejects private source metadata even when its content hash was recomputed: %s', location => {
const pack = copy(); pack.source.location = location
try { validateDesktopTemplatePack(seal(pack)); throw new Error('must reject private path') } catch (error) {
expect((error as Error).message).toBe('模板包暂时不可用,请联系管理员核对版本')
expect((error as Error).message).not.toContain(location)
}
})
test('refuses credential fields, external decoration sources and invalid layer placement', () => {
for (const input of [{ authorization: 'Bearer secret' }, { apiKey: 'sk-secret' }, { accessToken: 'secret' }, { sourcePath: 'relative/internal.psd' }]) {
const pack = copy(); Object.assign(pack.source, input)
expect(() => validateDesktopTemplatePack(seal(pack))).toThrow('模板包暂时不可用')
}
const pack = copy()
pack.outputs[0].decorations = [{ id: 'external', kind: 'rect', frame: { x: 0, y: 0, width: 100, height: 100 }, color: '#ffffff', opacity: 1, path: 'private.png' }]
expect(() => validateDesktopTemplatePack(seal(pack))).toThrow('模板包暂时不可用')
pack.outputs[0].decorations[0] = { ...pack.outputs[0].decorations[0], path: undefined, aboveImages: 'false' }
expect(() => validateDesktopTemplatePack(seal(pack))).toThrow('模板包暂时不可用')
})
})