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.
96 lines
7.1 KiB
TypeScript
96 lines
7.1 KiB
TypeScript
/** @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('模板包暂时不可用')
|
||
})
|
||
})
|