import assert from 'node:assert/strict' import { createHash } from 'node:crypto' import test from 'node:test' import { AUDIT_LIMITS, auditPreviewBatch, auditUniquePreviews, main, metadataCandidates, validateAuditSnapshot } from './audit-legacy-material-previews.mjs' import { LEGACY_DOMAINS } from './check-legacy-storage-boundaries.mjs' const host = LEGACY_DOMAINS[0] const source = n => `https://${host}/jsxiangche/preview-${n}.webp` const row = (id, index = id) => ({ id, materialType: '详情模板', previewUrl: source(index) }) const html = 'Not Found. The server is powered by frp. Faithfully yours, frp.' const response = () => new Response(html, { status: 404, headers: { 'content-type': 'text/html' } }) test('snapshot must match exact count, identity, and registered legacy host before any GET', () => { const value = { readOnly: true, count: 2, materials: [row('a'), row('b')] } assert.equal(validateAuditSnapshot(value, { host, expectedCount: 2 }).length, 2) assert.throws(() => validateAuditSnapshot(value, { host }), /exact_inventory_count_required/) assert.throws(() => validateAuditSnapshot({ ...value, materials: [row('a'), row('a')] }, { host, expectedCount: 2 }), /unique_material_identity_required/) assert.throws(() => validateAuditSnapshot({ ...value, materials: [row('a'), { ...row('b'), previewUrl: 'https://evil.example/a.webp' }] }, { host, expectedCount: 2 }), /inventory_host_changed/) }) test('candidate mapping keeps same-material display/preview evidence and excludes ZIP-like main role', () => { const material = { ...row('a'), displayImages: [source('display'), 'https://s3.jiansun.vip/gongxiangpan/p.png'], assets: [{ id: 'main', assetType: 'main', fileUrl: source('looks-like-image'), fileSize: '10' }, { id: 'good', assetType: 'main-preview', fileUrl: source('real-preview'), fileSize: '0' }, { id: 'bad', assetType: 'preview', fileUrl: 'https://s3.jiansun.vip/gongxiangpan/download.zip', fileSize: '1000000000' }], skus: [{ id: 'sku', previewUrl: source('sku'), filePath: 'https://s3.jiansun.vip/gongxiangpan/big.zip' }] } const result = metadataCandidates(material) assert.equal(result.materialId, 'a') assert.equal(result.candidates.length, 4) assert.equal(result.candidates.find(item => item.identity === 'good').declaredBytes, '0') assert.ok(result.candidates.every(item => item.metadataOnly && item.bytesRead === 0 && item.originalIdentityVerified === false)) assert.deepEqual(result.excludedAssets.map(item => item.assetId), ['main', 'bad']) assert.ok(!JSON.stringify(result.candidates).includes('big.zip')) }) test('FRP failure still retains hash of the complete actual response body', async () => { let calls = 0 const report = await auditPreviewBatch({ version: 1, reviewed: true, materials: [{ materialId: 'a', registeredSource: source('a') }] }, { fetchImpl: async () => { calls++; return response() } }) assert.equal(calls, 1) assert.equal(report.results[0].code, 'frp_default_404') assert.equal(report.results[0].responseBodyComplete, true) assert.equal(report.results[0].responseBodySha256, createHash('sha256').update(html).digest('hex')) assert.equal(report.results[0].responseBytes, Buffer.byteLength(html)) }) test('batches stay at fifty, duplicate URLs map every ID, and at most two GETs run concurrently', async () => { const materials = Array.from({ length: 103 }, (_, n) => row(`m-${n}`, n % 53)) let active = 0, maxActive = 0 const gets = new Map(), events = [] const result = await auditUniquePreviews(materials, { fetchImpl: async url => { gets.set(url, (gets.get(url) || 0) + 1) active++; maxActive = Math.max(maxActive, active) await new Promise(resolve => setTimeout(resolve, 1)) active-- return response() }, onBatch: async event => events.push(event) }) assert.equal(maxActive, 2) assert.equal(result.actualGetAttempts, 53) assert.equal(gets.size, 53) assert.ok([...gets.values()].every(value => value === 1)) assert.deepEqual(result.batches.map(item => item.materials.length), [50, 3]) assert.equal(events.length, 2) assert.equal(result.results.length, 103) assert.equal(new Set(result.results.map(item => item.materialId)).size, 103) assert.equal(result.allRowsAccounted, true) assert.ok(result.results.every(item => item.code === 'frp_default_404' && item.responseBodyComplete)) assert.equal(result.results.find(item => item.materialId === 'm-53').checkedRepresentativeId, 'm-0') assert.equal(AUDIT_LIMITS.concurrency, 2) }) test('a non-preview source is recorded without GET and a timeout is never automatically retried', async () => { const materials = [row('a'), { ...row('b'), previewUrl: 'http://127.0.0.1/private' }] let calls = 0 const result = await auditUniquePreviews(materials, { timeoutMs: 5, fetchImpl: () => { calls++; return new Promise(() => {}) } }) assert.equal(calls, 1) assert.equal(result.results[0].classification, 'timeout') assert.equal(result.results[0].responseBodySha256, null) assert.equal(result.results[1].attempts, 0) assert.equal(result.results[1].classification, 'source_rejected') assert.equal(result.allRowsAccounted, true) assert.equal(result.retries, 0) }) test('default invocation is plan-only without SSH, database, images or file writes', async () => { const result = await main([]) assert.equal(result.planOnly, true) assert.equal(result.expectedCount, 472) assert.equal(result.productionWrites, 0) await assert.rejects(main(['--run']), /run_requires_new_absolute_output_directory/) })