import assert from 'node:assert/strict'; import pg from 'pg'; import { spawn } from 'node:child_process'; const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:postgres@127.0.0.1:54322/postgres'; const tenantId = '00000000-0000-0000-0000-000000000001'; const ids = { okAsset: '20000000-0000-0000-0000-000000000801', badAsset: '20000000-0000-0000-0000-000000000802', }; const checksumA = 'a'.repeat(64); const checksumB = 'b'.repeat(64); async function runWorkerOnce() { const child = spawn(process.execPath, ['apps/worker/dist/apps/worker/src/index.js', '--once', '--job', 'assets'], { cwd: process.cwd(), env: { ...process.env, DATABASE_URL: databaseUrl, WORKER_ASSET_BATCH_SIZE: '20', WORKER_ASSET_MIN_AGE_SECONDS: '0', WORKER_ASSET_RECHECK_INTERVAL_SECONDS: '0', WORKER_ASSET_REQUEST_TIMEOUT_MS: '5000', STORAGE_REQUIRE_TENANT_PREFIX: 'true', }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, }); let output = ''; child.stdout.on('data', chunk => { output += chunk.toString(); }); child.stderr.on('data', chunk => { output += chunk.toString(); }); const code = await new Promise(resolve => child.on('exit', resolve)); assert.equal(code, 0, `worker should exit 0\n${output}`); assert.match(output, /assets batch processed=\d+/, 'worker output should include assets summary'); return output; } async function cleanup(pool) { await pool.query( ` delete from public.audit_logs where tenant_id = $1 and target_type = 'content_asset' and target_id in ($2, $3) `, [tenantId, ids.okAsset, ids.badAsset], ); await pool.query( ` delete from public.content_assets where tenant_id = $1 and id in ($2::uuid, $3::uuid) `, [tenantId, ids.okAsset, ids.badAsset], ); } async function seed(pool) { await pool.query( ` insert into public.content_assets ( id, tenant_id, asset_key, title, asset_type, storage_provider, bucket, object_key, file_name, mime_type, file_size_bytes, checksum_sha256, visibility, status, upload_status, verified_at, verified_size_bytes, verified_checksum_sha256, verification_details, security_flags, source, created_at, updated_at ) values ( $1, $2, 'asset-worker-ok', '资源复检正常 PDF', 'pdf', 'local_dev', 'tenant-assets', $3, 'ok.pdf', 'application/pdf', 4096, $4, 'tenant', 'active', 'verified', now() - interval '2 days', 4096, $4, '{"source":"asset-worker-test"}'::jsonb, '{}'::jsonb, 'integration-test', now() - interval '2 days', now() - interval '2 days' ), ( $5, $2, 'asset-worker-bad', '资源复检异常 PDF', 'pdf', 'local_dev', 'tenant-assets', $6, 'bad.pdf', 'application/pdf', 1024, $7, 'tenant', 'active', 'verified', now() - interval '2 days', 2048, $7, '{"source":"asset-worker-test"}'::jsonb, '{}'::jsonb, 'integration-test', now() - interval '2 days', now() - interval '2 days' ) `, [ ids.okAsset, tenantId, `${tenantId}/assets/worker-ok.pdf`, checksumA, ids.badAsset, `${tenantId}/assets/worker-bad.pdf`, checksumB, ], ); } async function main() { const pool = new pg.Pool({ connectionString: databaseUrl }); let seeded = false; try { await pool.query('begin'); await cleanup(pool); await seed(pool); await pool.query('commit'); seeded = true; const output = await runWorkerOnce(); assert.match(output, /verified=1/, 'worker should verify one asset'); assert.match(output, /failed=1/, 'worker should fail one mismatched asset'); const assets = await pool.query( ` select id, status, upload_status, verification_details, security_flags from public.content_assets where tenant_id = $1 and id in ($2::uuid, $3::uuid) order by id `, [tenantId, ids.okAsset, ids.badAsset], ); const okAsset = assets.rows.find(row => row.id === ids.okAsset); const badAsset = assets.rows.find(row => row.id === ids.badAsset); assert.equal(okAsset?.status, 'active', 'verified asset should remain active'); assert.equal(okAsset?.upload_status, 'verified', 'verified asset should remain verified'); assert.equal(okAsset?.verification_details?.assetWorker?.lastResult, 'verified', 'verified asset should record worker result'); assert.equal(okAsset?.security_flags?.assetRecheckFailed, undefined, 'verified asset should not keep recheck failure flag'); assert.equal(badAsset?.status, 'draft', 'mismatched asset should be unpublished'); assert.equal(badAsset?.upload_status, 'failed', 'mismatched asset should be marked failed'); assert.equal(badAsset?.security_flags?.assetRecheckFailed, true, 'mismatched asset should record security flag'); assert.deepEqual( badAsset?.verification_details?.assetWorker?.issues, ['file_size_mismatch'], 'mismatched asset should record exact issue', ); const audits = await pool.query( ` select action, details from public.audit_logs where tenant_id = $1 and target_type = 'content_asset' and target_id in ($2, $3) order by created_at asc `, [tenantId, ids.okAsset, ids.badAsset], ); assert.ok( audits.rows.some(row => row.action === 'content.asset.rechecked' && row.details?.result === 'verified'), 'worker should write verified audit log', ); assert.ok( audits.rows.some(row => row.action === 'content.asset.recheck_failed' && row.details?.issues?.includes('file_size_mismatch')), 'worker should write failed audit log', ); console.log('Asset worker integration test complete.'); } catch (error) { await pool.query('rollback').catch(() => {}); throw error; } finally { if (seeded) { await cleanup(pool).catch(() => {}); } await pool.end(); } } main().catch(error => { console.error(error); process.exit(1); });