Improved harness - with card selection
This commit is contained in:
@@ -1,96 +1,78 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { resolve } from 'node:path'
|
||||
import { PNG } from 'pngjs'
|
||||
import { scanCardCatalog } from './card-catalog.mjs'
|
||||
import { printings, resolutions, resolveCardAssetRequest, scanCardCatalog } from './card-catalog.mjs'
|
||||
|
||||
function png(width, height, pixel) {
|
||||
const image = new PNG({ width, height })
|
||||
for (let offset = 0; offset < image.data.length; offset += 4) {
|
||||
image.data.set(pixel, offset)
|
||||
async function createCard(root, options = {}) {
|
||||
const folderName = options.folderName ?? 'BP-002-david'
|
||||
const directory = resolve(root, folderName)
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(resolve(directory, 'card.json'), JSON.stringify({ cardId: 'BP-002', folderName, title: 'David', rarity: 'Blessed' }))
|
||||
await writeFile(resolve(directory, 'manifest.json'), JSON.stringify({
|
||||
stage: options.stage ?? 'approved', cardId: 'BP-002', folderName, revision: 'accepted-revision',
|
||||
}))
|
||||
for (const [resolutionName, dimensions] of Object.entries(resolutions)) {
|
||||
const files = {}
|
||||
for (const printing of printings) {
|
||||
const target = resolve(directory, resolutionName, printing)
|
||||
await mkdir(target, { recursive: true })
|
||||
for (const filename of ['card.png', 'finish-mask.png', ...(['normal', 'borderless'].includes(printing) ? ['text-mask.png'] : [])]) {
|
||||
const key = `${printing}/${filename}`
|
||||
files[key] = { sha256: `${resolutionName}-${printing}-${filename}`, dimensions }
|
||||
await writeFile(resolve(target, filename), key)
|
||||
}
|
||||
}
|
||||
await writeFile(resolve(directory, resolutionName, 'manifest.json'), JSON.stringify({ dimensions, files }))
|
||||
}
|
||||
return PNG.sync.write(image)
|
||||
return directory
|
||||
}
|
||||
|
||||
test('catalog pairs filename-based card art with validated masks', async (t) => {
|
||||
const root = await mkdtemp(resolve(tmpdir(), 'card-catalog-'))
|
||||
test('catalog exposes every accepted resolution and printing from artifact manifests', async (t) => {
|
||||
const root = await mkdtemp(resolve(tmpdir(), 'accepted-card-catalog-'))
|
||||
t.after(() => rm(root, { recursive: true }))
|
||||
await Promise.all([
|
||||
writeFile(resolve(root, 'david.png'), png(10, 14, [20, 40, 60, 255])),
|
||||
writeFile(resolve(root, 'david-mask.png'), png(10, 14, [128, 128, 128, 255])),
|
||||
])
|
||||
await createCard(root)
|
||||
const catalog = await scanCardCatalog(root)
|
||||
assert.deepEqual(catalog.errors, [])
|
||||
assert.equal(catalog.schemaVersion, 2)
|
||||
assert.equal(catalog.cards.length, 1)
|
||||
assert.equal(catalog.cards[0].name, 'david')
|
||||
assert.match(catalog.cards[0].artwork, /^\/card-art\/david\.png\?v=[0-9a-f]{16}$/)
|
||||
assert.equal(catalog.cards[0].width, 10)
|
||||
assert.equal(catalog.cards[0].height, 14)
|
||||
const card = catalog.cards[0]
|
||||
assert.equal(card.cardId, 'BP-002')
|
||||
assert.equal(card.title, 'David')
|
||||
assert.equal(card.acceptedRevision, 'accepted-revision')
|
||||
assert.equal(card.variants.high.normal.width, 2000)
|
||||
assert.match(card.variants.high.normal.artwork, /^\/__card_asset\/BP-002-david\/high\/normal\/card\.png\?v=/)
|
||||
assert.match(card.variants.low.normal.textMask, /text-mask\.png\?v=/)
|
||||
assert.equal(card.variants.low.textless.textMask, undefined)
|
||||
})
|
||||
|
||||
test('catalog reports missing pairs and invalid runtime images explicitly', async (t) => {
|
||||
const root = await mkdtemp(resolve(tmpdir(), 'card-catalog-invalid-'))
|
||||
test('catalog rejects unapproved and identity-mismatched packages without hiding errors', async (t) => {
|
||||
const root = await mkdtemp(resolve(tmpdir(), 'invalid-accepted-card-catalog-'))
|
||||
t.after(() => rm(root, { recursive: true }))
|
||||
await Promise.all([
|
||||
writeFile(resolve(root, 'orphan.png'), png(10, 14, [20, 40, 60, 255])),
|
||||
writeFile(resolve(root, 'mask-only-mask.png'), png(10, 14, [0, 0, 0, 255])),
|
||||
writeFile(resolve(root, 'bad.png'), png(10, 14, [20, 40, 60, 254])),
|
||||
writeFile(resolve(root, 'bad-mask.png'), png(10, 14, [20, 21, 20, 255])),
|
||||
])
|
||||
await createCard(root, { folderName: 'BP-002-draft', stage: 'candidate' })
|
||||
const bad = await createCard(root, { folderName: 'BP-002-wrong-folder' })
|
||||
await writeFile(resolve(bad, 'manifest.json'), JSON.stringify({
|
||||
stage: 'approved', cardId: 'BP-999', folderName: 'BP-002-wrong-folder', revision: 'x',
|
||||
}))
|
||||
const catalog = await scanCardCatalog(root)
|
||||
assert.deepEqual(catalog.cards, [])
|
||||
assert.ok(catalog.errors.some(error => error.includes('Missing orphan-mask.png')))
|
||||
assert.ok(catalog.errors.some(error => error.includes('Missing mask-only.png')))
|
||||
assert.ok(catalog.errors.some(error => error.includes('bad.png must be fully opaque')))
|
||||
assert.ok(catalog.errors.some(error => error.includes('not approved')))
|
||||
assert.ok(catalog.errors.some(error => error.includes('identity differs')))
|
||||
})
|
||||
|
||||
test('optional text masks are discovered without becoming artwork or orphan finish masks', async (t) => {
|
||||
const root = await mkdtemp(resolve(tmpdir(), 'card-text-mask-'))
|
||||
test('artifact asset routes allow only real runtime files inside the accepted root', async (t) => {
|
||||
const root = await mkdtemp(resolve(tmpdir(), 'accepted-card-route-'))
|
||||
t.after(() => rm(root, { recursive: true }))
|
||||
const artwork = png(10, 14, [20, 40, 60, 255])
|
||||
const finish = png(10, 14, [128, 128, 128, 255])
|
||||
await Promise.all([
|
||||
writeFile(resolve(root, 'Card name.png'), artwork),
|
||||
writeFile(resolve(root, 'Card name-mask.png'), finish),
|
||||
])
|
||||
const legacy = await scanCardCatalog(root)
|
||||
assert.equal(legacy.cards[0].textMask, undefined)
|
||||
const { createHash } = await import('node:crypto')
|
||||
assert.equal(legacy.cards[0].revision,
|
||||
createHash('sha256').update(artwork).update(finish).digest('hex').slice(0, 16))
|
||||
await writeFile(resolve(root, 'Card name-text-mask.png'), png(10, 14, [255, 255, 255, 255]))
|
||||
const masked = await scanCardCatalog(root)
|
||||
assert.deepEqual(masked.errors, [])
|
||||
assert.equal(masked.cards.length, 1)
|
||||
assert.match(masked.cards[0].textMask, /^\/card-art\/Card%20name-text-mask\.png\?v=[0-9a-f]{16}$/)
|
||||
assert.notEqual(masked.cards[0].revision, legacy.cards[0].revision)
|
||||
await writeFile(resolve(root, 'Card name-text-mask.png'), png(10, 14, [0, 0, 0, 255]))
|
||||
assert.notEqual((await scanCardCatalog(root)).cards[0].revision, masked.cards[0].revision)
|
||||
await rm(resolve(root, 'Card name-text-mask.png'))
|
||||
assert.deepEqual(await scanCardCatalog(root), legacy)
|
||||
})
|
||||
|
||||
test('invalid optional text masks reject the card and orphan masks name the correct missing artwork', async (t) => {
|
||||
const root = await mkdtemp(resolve(tmpdir(), 'invalid-card-text-mask-'))
|
||||
t.after(() => rm(root, { recursive: true }))
|
||||
await Promise.all([
|
||||
writeFile(resolve(root, 'card.png'), png(10, 14, [20, 40, 60, 255])),
|
||||
writeFile(resolve(root, 'card-mask.png'), png(10, 14, [128, 128, 128, 255])),
|
||||
writeFile(resolve(root, 'orphan-text-mask.png'), png(10, 14, [0, 0, 0, 255])),
|
||||
])
|
||||
for (const [bytes, error] of [
|
||||
[png(5, 7, [0, 0, 0, 255]), 'must match artwork dimensions'],
|
||||
[png(10, 14, [0, 1, 0, 255]), 'must be an opaque grayscale mask'],
|
||||
[png(10, 14, [0, 0, 0, 254]), 'must be an opaque grayscale mask'],
|
||||
[Buffer.from('broken PNG'), 'is not a readable PNG'],
|
||||
]) {
|
||||
await writeFile(resolve(root, 'card-text-mask.png'), bytes)
|
||||
const catalog = await scanCardCatalog(root)
|
||||
assert.deepEqual(catalog.cards, [])
|
||||
assert.ok(catalog.errors.some(message => message.includes('card-text-mask.png') && message.includes(error)))
|
||||
assert.ok(catalog.errors.includes('Missing orphan.png for orphan-text-mask.png'))
|
||||
assert.ok(!catalog.errors.some(message => message.includes('orphan-text.png')))
|
||||
}
|
||||
await createCard(root)
|
||||
const path = await resolveCardAssetRequest('/__card_asset/BP-002-david/high/normal/card.png', root)
|
||||
assert.equal(path, resolve(root, 'BP-002-david/high/normal/card.png'))
|
||||
assert.equal(await resolveCardAssetRequest('/__card_asset/BP-002-david/high/normal/manifest.json', root), undefined)
|
||||
assert.equal(await resolveCardAssetRequest('/__card_asset/..%2Foutside/high/normal/card.png', root), undefined)
|
||||
const outside = resolve(root, '..', `outside-${Date.now()}.png`)
|
||||
await writeFile(outside, 'outside')
|
||||
t.after(() => rm(outside))
|
||||
await rm(resolve(root, 'BP-002-david/high/normal/card.png'))
|
||||
await symlink(outside, resolve(root, 'BP-002-david/high/normal/card.png'))
|
||||
assert.equal(await resolveCardAssetRequest('/__card_asset/BP-002-david/high/normal/card.png', root), undefined)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user