276 lines
19 KiB
JavaScript
276 lines
19 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { createHash } from 'node:crypto'
|
|
import { createRequire } from 'node:module'
|
|
import test from 'node:test'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { bundleReleaseGate, buildPlan, canonicalDeliverySource, compareChecks, compareDeliveryRewrites, rewriteModule, CRITICAL_MATERIAL_IDS, DELIVERY_FILES, KNOWN_LEGACY_MEMBERS_SHA256, normalizeDeliveryConfig, POLICY, runProbes, runReleaseStage, validateAccepted } from './material-preview-release-gate.mjs'
|
|
import { LEGACY_DOMAINS } from './check-legacy-storage-boundaries.mjs'
|
|
|
|
const sharp = createRequire(import.meta.url)('sharp')
|
|
const PNG = await sharp({ create: { width: 3, height: 2, channels: 3, background: '#4d5e6f' } }).png().toBuffer()
|
|
const hash = value => createHash('sha256').update(value).digest('hex')
|
|
const digest = value => hash(JSON.stringify(value))
|
|
const current = id => `https://s3.jiansun.vip/gongxiangpan/${id}.png`
|
|
const independent = id => `https://${LEGACY_DOMAINS[0]}/jsxiangche/${id}.png`
|
|
const compatible = id => `https://${LEGACY_DOMAINS[1]}/gongxiangpan/${id}.png`
|
|
const FRP = '<html><p>The server is powered by <a href="https://github.com/fatedier/frp">frp</a>.</p><p>Faithfully yours, frp.</p></html>'
|
|
const row = (materialId, registeredSource = current(materialId)) => ({ materialId, registeredSource, sourceSha256: hash(registeredSource) })
|
|
const code = Object.fromEntries(DELIVERY_FILES.map(file => [file, 'c'.repeat(64)]))
|
|
const config = { S3_ENDPOINT: 'http://192.168.58.20:9000', S3_BUCKET: 'gongxiangpan', S3_FILE_PATH: '由壹点AI/',
|
|
S3_PUBLIC_DOMAIN: 'https://s3.jiansun.vip/gongxiangpan', S3_REGION: 'us-east-1', SERVER_IP: '192.168.58.20', MINIO_API_PORT: '9000', APP_PUBLIC_URL: 'https://sc.jiansun.vip' }
|
|
const initial = () => [...CRITICAL_MATERIAL_IDS.map((id, i) => row(id, i ? current(id) : compatible(id))), row('legacy', independent('legacy')), row('untested-current'),
|
|
{ materialId: 'private-source', sourceSha256: hash('uploads/materials/private'), uncheckable: { classification: 'source_rejected', code: 'invalid_url' } }]
|
|
|
|
function database(rows) {
|
|
const events = []
|
|
const normalized = rows.map(r => ({ id: r.materialId, previewUrl: r.registeredSource || 'uploads/materials/private', materialType: '材质' })).sort((a, b) => a.id.localeCompare(b.id, 'en'))
|
|
return { events, prisma: { async $transaction(fn, options) {
|
|
events.push(options)
|
|
return fn({ async $executeRawUnsafe(sql) { events.push(sql) }, material: {
|
|
async count(query) { events.push(query); return normalized.length }, async findMany(query) {
|
|
events.push(query)
|
|
const start = query.cursor ? normalized.findIndex(r => r.id === query.cursor.id) + 1 : 0
|
|
return normalized.slice(start, start + query.take)
|
|
},
|
|
} })
|
|
} } }
|
|
}
|
|
|
|
const fixtureFetch = async url => {
|
|
const parsed = new URL(url)
|
|
if (parsed.hostname === LEGACY_DOMAINS[0]) return new Response(FRP, { status: 404, headers: { 'Content-Type': 'text/html' } })
|
|
if (parsed.hostname === LEGACY_DOMAINS[1]) throw new Error('old same-bucket route unavailable')
|
|
return new Response(PNG, { status: 200, headers: { 'Content-Type': 'image/png' } })
|
|
}
|
|
|
|
async function stage(stageName, rows, { accepted, capture, candidate, probeFetch = fixtureFetch, actualCode = code, targetCode = code, rewriteComparison = null, ...input } = {}) {
|
|
const db = database(rows)
|
|
const report = await runReleaseStage({ version: 1, stage: stageName, commit: 'b'.repeat(40), accepted, capture, candidate,
|
|
targetDeliveryConfig: config, ...input }, {
|
|
prismaClient: db.prisma, readDeliveryCode: async () => actualCode, targetDeliveryCode: targetCode,
|
|
environment: config, probe: plan => runProbes(plan, { fetchImpl: probeFetch }),
|
|
compareRewrites: async () => rewriteComparison,
|
|
// Synthetic offline fixture proofs; the production stdin runner cannot supply
|
|
// these dependency injections and uses the fixed incident/original SHA values.
|
|
legacyFailureProof: { bytes: Buffer.byteLength(FRP), bodySha256: hash(FRP) },
|
|
compatibilityProof: { materialId: CRITICAL_MATERIAL_IDS[0], bodySha256: hash(PNG), width: 3, height: 2 },
|
|
legacyMembers: [row('legacy', independent('legacy'))],
|
|
})
|
|
assert.equal(db.events[1], 'SET TRANSACTION READ ONLY')
|
|
assert.deepEqual(db.events[0], { isolationLevel: 'RepeatableRead', timeout: 60_000, maxWait: 5000 })
|
|
return report
|
|
}
|
|
|
|
test('bootstrap records all metadata, exact 12 materials and all independent legacy rows without inventing health for omitted sources', async () => {
|
|
const report = await stage('bootstrap', initial())
|
|
assert.equal(report.passed, true)
|
|
assert.equal(report.allSelectedHealthy, false)
|
|
assert.equal(report.bootstrapRequiresExplicitShaReview, true)
|
|
assert.equal(report.inventory.length, 15)
|
|
assert.equal(report.plan.selected.length, 13)
|
|
assert.equal(report.checks.length, 14)
|
|
assert.equal(report.knownFailures.length, 2)
|
|
assert.equal(report.plan.unverified.length, 2)
|
|
assert.equal(report.plan.unverified.find(r => r.materialId === 'private-source').reason, 'unsupported_or_missing_registered_source')
|
|
assert.equal(report.budget.retries, 0)
|
|
validateAccepted(report)
|
|
assert.equal(report.checks.find(r => r.materialId === CRITICAL_MATERIAL_IDS[0] && r.role === 'registered').result.ok, false)
|
|
assert.equal(report.checks.find(r => r.materialId === CRITICAL_MATERIAL_IDS[0] && r.role === 'canonical').result.bodySha256, hash(PNG))
|
|
})
|
|
|
|
test('fixed same plan is checked before and after candidate with no metadata or content writes', async () => {
|
|
const accepted = await stage('bootstrap', initial())
|
|
const rows = [...initial(), row('new-current')]
|
|
const capture = await stage('capture', rows, { accepted })
|
|
const candidate = await stage('candidate', rows, { accepted, capture })
|
|
const post = await stage('post', rows, { accepted, capture, candidate })
|
|
assert.equal(capture.plan.selected.some(r => r.materialId === 'new-current' && r.reasons.includes('new_or_changed_source')), true)
|
|
assert.equal(post.passed, true)
|
|
assert.equal(post.planSha256, capture.planSha256)
|
|
assert.equal(candidate.planSha256, capture.planSha256)
|
|
assert.equal(post.inventorySha256, capture.inventorySha256)
|
|
validateAccepted(post)
|
|
})
|
|
|
|
test('missing or forged first baseline stops capture before image GET', async () => {
|
|
let calls = 0
|
|
await assert.rejects(stage('capture', initial(), { probeFetch: async () => { calls++; return fixtureFetch(current('x')) } }), /reviewed_baseline_required/)
|
|
const accepted = await stage('bootstrap', initial())
|
|
accepted.inventory.pop()
|
|
await assert.rejects(stage('capture', initial(), { accepted }), /baseline_digest_mismatch/)
|
|
assert.equal(calls, 0)
|
|
})
|
|
|
|
test('new failure or changed failure type blocks candidate; existing FRP incident remains explicit', async () => {
|
|
const accepted = await stage('bootstrap', initial()), capture = await stage('capture', initial(), { accepted })
|
|
const bad = await stage('candidate', initial(), { accepted, capture, probeFetch: async url => url === current(CRITICAL_MATERIAL_IDS[1])
|
|
? new Response('<html>failed</html>', { status: 200, headers: { 'Content-Type': 'text/html' } }) : fixtureFetch(url) })
|
|
assert.equal(bad.passed, false)
|
|
assert.ok(bad.problems.some(r => r.code === 'new_or_changed_failure'))
|
|
assert.ok(bad.knownFailures.some(r => r.materialId === 'legacy' && r.code === 'frp_default_404'))
|
|
const legacyChange = await stage('candidate', initial(), { accepted, capture, probeFetch: async url => url === independent('legacy')
|
|
? new Response('<Error><Code>NoSuchKey</Code></Error>', { status: 404, headers: { 'Content-Type': 'application/xml' } }) : fixtureFetch(url) })
|
|
assert.equal(legacyChange.passed, false)
|
|
})
|
|
|
|
test('a newly registered legacy failure cannot join the known baseline automatically', async () => {
|
|
const accepted = await stage('bootstrap', initial())
|
|
const capture = await stage('capture', [...initial(), row('new-legacy', independent('new-legacy'))], { accepted })
|
|
assert.equal(capture.passed, false)
|
|
assert.ok(capture.problems.some(r => r.materialId === 'new-legacy' && r.code === 'new_or_changed_failure'))
|
|
})
|
|
|
|
test('bootstrap only permits the exact documented FRP body and original canonical compatibility proof', async () => {
|
|
const wrongLegacy = await stage('bootstrap', initial(), { probeFetch: async url => url === independent('legacy')
|
|
? new Response(FRP + 'changed', { status: 404, headers: { 'Content-Type': 'text/html' } }) : fixtureFetch(url) })
|
|
assert.equal(wrongLegacy.passed, false)
|
|
const wrongCanonical = await stage('bootstrap', initial(), { probeFetch: async url => {
|
|
if (url !== current(CRITICAL_MATERIAL_IDS[0])) return fixtureFetch(url)
|
|
const different = await sharp({ create: { width: 3, height: 2, channels: 3, background: '#112233' } }).png().toBuffer()
|
|
return new Response(different, { headers: { 'Content-Type': 'image/png' } })
|
|
} })
|
|
assert.equal(wrongCanonical.passed, false)
|
|
})
|
|
|
|
test('bootstrap rejects an extra historical row, a replacement ID at equal count, and a changed historical source', async () => {
|
|
for (const rows of [
|
|
[...initial(), row('unreviewed-old', independent('unreviewed-old'))],
|
|
initial().map(r => r.materialId === 'legacy' ? row('replacement-old', independent('legacy')) : r),
|
|
initial().map(r => r.materialId === 'legacy' ? row('legacy', independent('different-source')) : r),
|
|
]) {
|
|
const bootstrap = await stage('bootstrap', rows)
|
|
assert.equal(bootstrap.passed, false)
|
|
assert.ok(bootstrap.problems.some(r => r.code === 'new_or_changed_failure'))
|
|
}
|
|
})
|
|
|
|
test('healthy same URL with different bytes requires review rather than silently rebaselining', async () => {
|
|
const accepted = await stage('bootstrap', initial()), capture = await stage('capture', initial(), { accepted })
|
|
const variant = await sharp({ create: { width: 3, height: 2, channels: 3, background: '#ffffff' } }).png().toBuffer()
|
|
const candidate = await stage('candidate', initial(), { accepted, capture, probeFetch: async url => url === current(CRITICAL_MATERIAL_IDS[1])
|
|
? new Response(variant, { headers: { 'Content-Type': 'image/png' } }) : fixtureFetch(url) })
|
|
assert.equal(candidate.passed, false)
|
|
assert.ok(candidate.problems.some(r => r.code === 'unchanged_url_content_changed'))
|
|
})
|
|
|
|
test('inventory drift, missing coverage, missing candidate, and detector failure all fail closed', async () => {
|
|
const accepted = await stage('bootstrap', initial()), capture = await stage('capture', initial(), { accepted })
|
|
await assert.rejects(stage('candidate', [...initial(), row('concurrent-upload')], { accepted, capture }), /inventory_or_capture_drift/)
|
|
await assert.rejects(stage('post', initial(), { accepted, capture }), /candidate_receipt_required/)
|
|
const broken = structuredClone(capture); broken.checks.pop(); broken.checksSha256 = digest(broken.checks)
|
|
await assert.rejects(stage('candidate', initial(), { accepted, capture: broken }), /probe_coverage_incomplete/)
|
|
const faulty = structuredClone(capture.checks)
|
|
faulty[0].result.ok = false; faulty[0].result.classification = 'decoder_unavailable'; faulty[0].result.code = 'image_decoder_unavailable'
|
|
assert.equal(compareChecks(capture.plan, faulty, capture).passed, false)
|
|
})
|
|
|
|
test('new unsupported source, absent key material and changes beyond budget cannot be dropped', async () => {
|
|
const accepted = await stage('bootstrap', initial())
|
|
await assert.rejects(stage('capture', [...initial(), { materialId: 'unsupported-new', sourceSha256: hash('x'), uncheckable: { code: 'invalid_url' } }], { accepted }), /new_or_changed_uncheckable_preview/)
|
|
assert.throws(() => buildPlan(initial().slice(1), accepted, { publicDomain: 'https://s3.jiansun.vip/gongxiangpan' }), /critical_material_coverage_missing/)
|
|
assert.throws(() => buildPlan([...initial(), ...Array.from({ length: POLICY.probes }, (_, i) => row(`new-${i}`))], accepted,
|
|
{ publicDomain: 'https://s3.jiansun.vip/gongxiangpan' }), /probe_budget_requires_independent_full_acceptance/)
|
|
})
|
|
|
|
test('delivery source or storage code changes expand scope and cannot skip large inventory', async () => {
|
|
const accepted = await stage('bootstrap', initial())
|
|
const changed = { ...code, [DELIVERY_FILES[0]]: 'd'.repeat(64) }
|
|
const capture = await stage('capture', initial(), { accepted, targetCode: changed })
|
|
assert.equal(capture.plan.expandedScope, true)
|
|
assert.ok(capture.plan.selected.some(r => r.materialId === 'untested-current'))
|
|
await assert.rejects(stage('candidate', initial(), { accepted, capture, targetCode: changed }), /candidate_delivery_identity_mismatch/)
|
|
const many = [...initial(), ...Array.from({ length: POLICY.probes }, (_, i) => row(`unchanged-${i}`))]
|
|
const largeBase = await stage('bootstrap', many)
|
|
await assert.rejects(stage('capture', many, { accepted: largeBase, targetCode: changed }), /probe_budget_requires_independent_full_acceptance/)
|
|
const outOfBand = await stage('capture', initial(), { accepted, actualCode: changed, targetCode: changed })
|
|
assert.equal(outOfBand.plan.expandedScope, true, 'a change since the accepted code baseline cannot disappear when target already equals current')
|
|
})
|
|
|
|
test('upload-only changes preserve the existing preview scope and changed URL outputs select their materials', async () => {
|
|
const many = [...initial(), ...Array.from({ length: POLICY.probes }, (_, i) => row(`unchanged-${i}`))]
|
|
const accepted = await stage('bootstrap', many)
|
|
const uploadOnly = { ...code, 'lib/s3.ts': 'd'.repeat(64) }
|
|
const uploadCapture = await stage('capture', many, { accepted, targetCode: uploadOnly })
|
|
assert.equal(uploadCapture.plan.expandedScope, false)
|
|
assert.equal(uploadCapture.plan.selected.some(value => value.materialId === 'unchanged-0'), false)
|
|
const rewrite = { ...uploadOnly, 'lib/url-rewrite.ts': 'e'.repeat(64) }
|
|
const capture = await stage('capture', many, { accepted, targetCode: rewrite,
|
|
rewriteComparison: { checkedSources: many.length - 1, contexts: 3, changedMaterialIds: ['unchanged-0'] } })
|
|
assert.equal(capture.plan.expandedScope, false)
|
|
assert.ok(capture.plan.selected.some(value => value.materialId === 'unchanged-0' && value.reasons.includes('delivery_dependency_changed')))
|
|
assert.equal(capture.plan.selected.some(value => value.materialId === 'unchanged-1'), false)
|
|
const proxyChange = { ...code, 'lib/proxy-media-security.ts': 'f'.repeat(64) }
|
|
await assert.rejects(stage('capture', many, { accepted, targetCode: proxyChange }), /probe_budget_requires_independent_full_acceptance/)
|
|
})
|
|
|
|
test('authenticated material previews remain explicitly unverified without anonymous fetches', async () => {
|
|
const accepted = await stage('bootstrap', initial())
|
|
const privateRow = row('restricted-new', 'uploads/materials/restricted-new/protected-display/cover.webp')
|
|
const capture = await stage('capture', [...initial(), privateRow], { accepted })
|
|
assert.ok(capture.plan.unverified.some(value => value.materialId === privateRow.materialId && value.reason === 'authenticated_preview_outside_public_probe'))
|
|
assert.equal(capture.checks.some(value => value.materialId === privateRow.materialId), false)
|
|
})
|
|
|
|
test('real rewriting comparison preserves network contexts and identifies changed output URLs', async () => {
|
|
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
const base = await rewriteModule(root)
|
|
const sources = [row('first'), row('second')]
|
|
const unchanged = await compareDeliveryRewrites(sources, root, base)
|
|
assert.deepEqual(unchanged, { checkedSources: 2, contexts: 3, changedMaterialIds: [] })
|
|
const target = 'data:text/javascript;base64,' + Buffer.from(`import * as base from ${JSON.stringify(base)};
|
|
export const rewriteStorageUrlForClient=(value,request)=>base.rewriteStorageUrlForClient(value,request);
|
|
export const rewriteMediaUrlForClient=(value,request)=>value.includes('/second.png')?'https://changed.invalid/second.png':base.rewriteMediaUrlForClient(value,request);`).toString('base64')
|
|
assert.deepEqual((await compareDeliveryRewrites(sources, root, target)).changedMaterialIds, ['second'])
|
|
})
|
|
|
|
test('nonsecret delivery configuration changes expand scope and inconsistent candidate settings stop before GET', async () => {
|
|
const accepted = await stage('bootstrap', initial())
|
|
for (const [key, value] of [['S3_ENDPOINT', 'http://192.168.58.21:9000'], ['S3_BUCKET', 'other-bucket'], ['S3_FILE_PATH', 'other/'],
|
|
['SERVER_IP', '192.168.58.21'], ['MINIO_API_PORT', '9001'], ['S3_REGION', 'other-region']]) {
|
|
const targetDeliveryConfig = { ...config, [key]: value }
|
|
const capture = await stage('capture', initial(), { accepted, targetDeliveryConfig })
|
|
assert.equal(capture.plan.expandedScope, true, key)
|
|
await assert.rejects(stage('candidate', initial(), { accepted, capture, targetDeliveryConfig }), /candidate_delivery_identity_mismatch/)
|
|
}
|
|
await assert.rejects(stage('capture', initial(), { accepted, targetDeliveryConfig: undefined }), /complete_delivery_configuration_required/)
|
|
assert.throws(() => normalizeDeliveryConfig({ ...config, S3_ENDPOINT: 'http://user:secret@192.168.58.20:9000' }), /credential_or_query_in_delivery_configuration/)
|
|
assert.throws(() => normalizeDeliveryConfig({ ...config, S3_ACCESS_KEY_ID: 'must-not-be-logged' }), /complete_delivery_configuration_required/)
|
|
})
|
|
|
|
test('versioned historical membership proof exactly matches the independently reviewed 472 IDs and normalized source digests', async () => {
|
|
const { readFile } = await import('node:fs/promises')
|
|
const policy = JSON.parse(await readFile(new URL('./fixtures/material-preview-known-legacy-20261023.json', import.meta.url), 'utf8'))
|
|
assert.equal(policy.members.length, 472)
|
|
assert.equal(digest(policy.members), KNOWN_LEGACY_MEMBERS_SHA256)
|
|
assert.deepEqual(Object.keys(policy.members[0]).sort(), ['materialId', 'sourceSha256'])
|
|
assert.equal(JSON.stringify(policy).includes('https://'), false)
|
|
})
|
|
|
|
test('canonical mapping preserves bucket and key and never rewrites independent old storage', () => {
|
|
assert.equal(canonicalDeliverySource(independent('original'), 'https://s3.jiansun.vip/gongxiangpan'), independent('original'))
|
|
assert.equal(canonicalDeliverySource(compatible('%E4%B8%AD%E6%96%87'), 'https://s3.jiansun.vip/gongxiangpan'), current('%E4%B8%AD%E6%96%87'))
|
|
assert.throws(() => canonicalDeliverySource(compatible('x'), 'https://elsewhere.invalid/gongxiangpan'), /untrusted_public_bucket_configuration/)
|
|
})
|
|
|
|
test('read-only probe queue is bounded to four requests and one attempt with a fixed wall budget', async () => {
|
|
const accepted = await stage('bootstrap', initial()), plan = buildPlan(initial(), accepted, { publicDomain: 'https://s3.jiansun.vip/gongxiangpan' })
|
|
let active = 0, peak = 0, calls = 0
|
|
await runProbes(plan, { fetchImpl: async (url, options) => {
|
|
calls++; peak = Math.max(peak, ++active)
|
|
assert.equal(options.method, 'GET'); assert.equal(options.redirect, 'manual'); assert.equal(options.cache, 'no-store')
|
|
await new Promise(resolve => setTimeout(resolve, 2)); active--
|
|
return fixtureFetch(url)
|
|
} })
|
|
assert.equal(calls, plan.probes.length); assert.equal(peak, POLICY.concurrency)
|
|
let time = 0
|
|
await assert.rejects(runProbes(plan, { now: () => (time += POLICY.wallMs), fetchImpl: () => { throw Error('must not fetch') } }), /release_probe_wall_budget/)
|
|
})
|
|
|
|
test('exact-checkout bundle loads without an old image containing new scripts', async () => {
|
|
const bundle = await bundleReleaseGate(undefined, { runtimeBase: new URL('../package.json', import.meta.url).href })
|
|
assert.ok(bundle.length < 524288)
|
|
const gate = await import(bundle)
|
|
assert.equal(typeof gate.runReleaseStage, 'function')
|
|
assert.deepEqual(gate.POLICY, POLICY)
|
|
})
|