import assert from 'node:assert/strict'; import pg from 'pg'; import { autoGrantBadges } from '../apps/api/src/features/profile/badges.ts'; import { assertDestructiveTestDatabase, resolveDestructiveTestConfirmation, } from './lib/destructive-test-database-guard.js'; const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://postgres:postgres@127.0.0.1:54322/postgres'; const TENANT_ID = process.env.TENANT_ID || '00000000-0000-0000-0000-000000000001'; const USER_ID = process.env.USER_ID || '00000000-0000-0000-0000-000000000101'; const BADGE_ID = '00000000-0000-0000-0000-00000000b881'; const destructiveTestConfirmation = resolveDestructiveTestConfirmation(); const pool = new pg.Pool({ connectionString: DATABASE_URL, max: 8 }); async function resetFixture() { await pool.query( ` delete from public.user_notifications where tenant_id = $1 and dedupe_key = $2 `, [TENANT_ID, `badge:${BADGE_ID}:user:${USER_ID}`], ); await pool.query( ` delete from public.user_badges where tenant_id = $1 and (badge_id = $2::uuid or legacy_id = $3) `, [TENANT_ID, BADGE_ID, `auto:${BADGE_ID}:user:${USER_ID}`], ); await pool.query( ` delete from public.badges where tenant_id = $1 and id = $2::uuid `, [TENANT_ID, BADGE_ID], ); await pool.query( ` insert into public.badges ( id, tenant_id, legacy_id, name, description, category, level, unlock_type, condition_field, condition_operator, condition_value, condition_extra, metadata, sort_order, is_active ) values ( $1::uuid, $2, 'integration-badge-auto-concurrency', '并发自动勋章烟测', '并发触发时只能发放一次', 'practice', 1, 'practice_count', 'concurrencyGrantCount', 'gte', 1, '{}'::jsonb, '{"source":"auto-badge-concurrency-test"}'::jsonb, 99, true ) `, [BADGE_ID, TENANT_ID], ); } async function grantOnce(index) { const client = await pool.connect(); try { await client.query('begin'); const grants = await autoGrantBadges(client, { tenantId: TENANT_ID, userId: USER_ID, trigger: 'practice_count', evidence: { concurrencyGrantCount: 1, workerIndex: index, }, }); await client.query('commit'); return grants; } catch (error) { await client.query('rollback').catch(() => undefined); throw error; } finally { client.release(); } } async function main() { try { await assertDestructiveTestDatabase({ client: pool, databaseUrl: DATABASE_URL, confirmation: destructiveTestConfirmation, operation: 'auto badge concurrency test', }); await resetFixture(); const results = await Promise.all(Array.from({ length: 20 }, (_, index) => grantOnce(index))); const grantedRows = results.flat().filter(item => item.badgeId === BADGE_ID); const count = await pool.query( ` select count(*)::int as count from public.user_badges where tenant_id = $1 and user_id = $2 and badge_id = $3::uuid `, [TENANT_ID, USER_ID, BADGE_ID], ); const legacyCount = await pool.query( ` select count(*)::int as count from public.user_badges where tenant_id = $1 and legacy_id = $2 `, [TENANT_ID, `auto:${BADGE_ID}:user:${USER_ID}`], ); assert.equal(Number(count.rows[0]?.count || 0), 1, 'concurrent auto badge grants should create one user_badges row'); assert.equal(Number(legacyCount.rows[0]?.count || 0), 0, 'auto badge grants should not write legacy_id idempotency keys'); assert.equal(grantedRows.length, 1, 'only one concurrent caller should receive a new grant payload'); console.log('Auto badge concurrency test complete.'); } finally { await pool.end(); } } main().catch(error => { console.error(error); process.exitCode = 1; });