import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import { createRequire } from 'node:module'; import { resolve } from 'node:path'; import { existsSync } from 'node:fs'; // Only isolated local QA data. Never starts workers, publishes SMS, changes // live configuration, or fabricates supplier/customer delivery evidence. const require = createRequire(resolve('api/package.json')); for (const file of ['api/.env', '.env']) if (existsSync(file)) process.loadEnvFile(file); const name = process.env.CMPP_CHANNEL_WORD_QA_DATABASE; assert.match(name ?? '', /^cmpp_qa_channel_words_\d+$/); const url = new URL(process.env.DATABASE_URL); assert(['127.0.0.1', 'localhost'].includes(url.hostname)); url.pathname = `/${name}`; process.env.DATABASE_URL = url.toString(); process.env.CMPP_PROCESS_ROLE = 'api'; const { PrismaService } = require('./dist/prisma/prisma.service.js'); const { ChannelSensitiveWordsService } = require('./dist/dictionaries/channel-sensitive-words.service.js'); const { SendChainService } = require('./dist/send-chain/send-chain.service.js'); const { loadChannelWords } = require('./dist/send-chain/channel-sensitive-routing.js'); const { OperationsMessageQueries } = require('./dist/operations/queries/messages.queries.js'); const { clientMessageView } = require('./dist/operations/operations.helpers.js'); const db = new PrismaService(), prefix = `qa-channel-word-${randomUUID()}`, checks = []; try { const role = await db.role.findUniqueOrThrow({ where: { code: 'platform_admin' } }); const admin = await db.user.create({ data: { username: prefix, displayName: '隔离验收管理员', passwordHash: 'isolated-no-login', roles: { create: { roleId: role.id } }, }, }); const unauthorized = await db.user.create({ data: { username: `${prefix}-unauthorized`, displayName: '隔离无权限用户', passwordHash: 'isolated-no-login' }, }); const tenant = await db.tenant.create({ data: { code: prefix, name: prefix } }); const application = await db.smsApplication.create({ data: { tenantId: tenant.id, name: prefix, cmppAccount: prefix, cmppEnterpriseCode: 'QA', secretHash: 'isolated-no-login', }, }); const signature = await db.smsSignature.create({ data: { tenantId: tenant.id, applicationId: application.id, name: prefix, auditStatus: 'approved' }, }); const group = await db.smsChannelGroup.create({ data: { code: prefix, name: prefix, carrier: 'mobile' } }); const channels = []; for (const index of [0, 1]) { const channel = await db.smsChannel.create({ data: { code: `${prefix}-${index}`, name: `验收通道${index ? 'B' : 'A'}`, carrier: 'all', carriers: ['mobile', 'unicom', 'telecom'], gatewayHost: '127.0.0.1', gatewayPort: 1, account: prefix, passwordCipher: 'isolated-no-transport', srcId: '1069', }, }); channels.push(channel); await db.cmppConnectionState.create({ data: { channelId: channel.id, connectionId: prefix, status: 'connected', currentConnections: 1, desiredConnections: 1, }, }); await db.smsChannelGroupItem.create({ data: { groupId: group.id, channelId: channel.id, carrier: 'mobile', priority: index + 1 }, }); await db.channelSignatureReportTask.create({ data: { channelId: channel.id, signatureId: signature.id, tenantId: tenant.id, carrier: 'mobile', approvalScope: 'carrier_specific', reportType: 'signature', status: 'approved', }, }); } await db.channelRouteRule.create({ data: { tenantId: tenant.id, applicationId: application.id, groupId: group.id, carrier: 'mobile' }, }); const service = new ChannelSensitiveWordsService(db); await assert.rejects(service.list(unauthorized.id, {}), /权限/); await assert.rejects(service.save(admin.id, { channelId: channels[0].id, word: ' ', status: 'active' }), /字符/); const data = { channelId: channels[0].id, word: ' 贷款 ', status: 'active', remark: 'QA' }; let word = await service.save(admin.id, data); assert.equal(word.word, '贷款'); await assert.rejects(service.save(admin.id, data), /相同/); const concurrent = await Promise.allSettled([ service.save(admin.id, { ...data, version: word.version, remark: 'first' }, word.id), service.save(admin.id, { ...data, version: word.version, remark: 'second' }, word.id), ]); assert.equal(concurrent.filter((result) => result.status === 'fulfilled').length, 1); assert.equal(concurrent.filter((result) => result.status === 'rejected').length, 1); word = await db.channelSensitiveWord.findUniqueOrThrow({ where: { id: word.id } }); assert.equal(word.version, 2); checks.push('real PostgreSQL validation, permission, unique word, concurrent optimistic version conflict'); const list = await service.list(admin.id, { channelId: channels[0].id, keyword: '贷', page: '1', pageSize: '25', status: 'active', }); assert.equal(list.total, 1); assert.equal(list.items[0].id, word.id); const before = await loadChannelWords( db, channels.map((channel) => channel.id), ); word = await service.save(admin.id, { ...data, version: word.version, status: 'inactive' }, word.id); assert.equal(before.hits('贷款').length, 1); assert.equal( ( await loadChannelWords( db, channels.map((channel) => channel.id), ) ).hits('贷款').length, 0, ); await service.remove(admin.id, word.id, word.version); assert.equal((await service.list(admin.id, { channelId: channels[0].id })).total, 0); const restored = await service.save(admin.id, data); assert.equal(restored.id, word.id); assert(restored.version > word.version); assert.equal(await db.operationLog.count({ where: { resourceId: word.id, resource: 'channel_sensitive_word' } }), 5); checks.push('filter, disable, soft delete, audited restore retain ID; old snapshot unaffected by config edits'); const messages = []; for (let index = 0; index < 100; index++) messages.push( await db.smsMessageRecord.create({ data: { tenantId: tenant.id, applicationId: application.id, signatureId: signature.id, messageId: `${prefix}-${index}`, content: `【验收】贷款咨询${'字'.repeat(80)}`, phoneNumber: '13800000000', carrier: 'mobile', }, }), ); const sendChain = new SendChainService(db, {}, {}, {}); const ordinary = await sendChain.selectChannelForMessage(messages[0]); assert.equal(ordinary.channel.id, channels[1].id); const started = performance.now(); let reads = 0, writes = 0; const originalRead = db.channelSensitiveWord.findMany.bind(db.channelSensitiveWord); const originalWrite = db.smsChannelSensitiveDecision.createMany.bind(db.smsChannelSensitiveDecision); db.channelSensitiveWord.findMany = (...args) => { reads++; return originalRead(...args); }; db.smsChannelSensitiveDecision.createMany = (...args) => { writes++; return originalWrite(...args); }; const batch = await sendChain.submission.gatewaySubmit.planRoutesBatch(messages); assert.equal(batch.planned.length, 100); assert.equal(batch.failed.length, 0); assert(batch.planned.every((item) => item.routed.channel.id === channels[1].id)); assert.equal(reads, 1); assert.equal(writes, 1); const durationMs = Math.round(performance.now() - started); checks.push( `ordinary and 100-message batch choose B, one word SQL and one decision SQL; ${durationMs}ms includes existing drainage work`, ); await service.save(admin.id, { ...data, channelId: channels[1].id }); const failed = await sendChain.submission.gatewaySubmit.planRoutesBatch([messages[1]]); assert.equal(failed.failed[0].code, 'CHANNEL_SENSITIVE_WORD_NO_ROUTE'); await assert.rejects(sendChain.selectChannelForMessage(messages[2]), /均命中/); const detail = await new OperationsMessageQueries(db).getMessage(messages[0].id); assert( detail.channelWordDecisions.some((decision) => decision.snapshot.hits.some((hit) => hit.channelName === '验收通道A'), ), ); assert.equal(clientMessageView(detail).channelWordDecisions, undefined); assert.equal( (await db.smsMessageRecord.findUniqueOrThrow({ where: { id: messages[0].id } })).content, messages[0].content, ); await before.persist(db); assert.equal( await db.gatewaySubmitOutbox.count({ where: { messageRecordId: { in: messages.map((message) => message.id) } } }), 0, ); assert.equal( await db.smsSubmitRecord.count({ where: { messageRecordId: { in: messages.map((message) => message.id) } } }), 0, ); checks.push( 'all candidates rejected in ordinary/batch, historical admin explanation, client redaction, original text unchanged, no submit intents', ); // Exercise the actual failure SQL before an isolated release failure. const task = await db.smsBatchTask.create({ data: { tenantId: tenant.id, applicationId: application.id, taskNo: prefix, sourceType: 'client', content: '贷款', phoneTotal: 1, }, }); const pending = await db.smsMessageRecord.update({ where: { id: messages[3].id }, data: { batchTaskId: task.id }, include: { batchTask: true }, }); sendChain.releaseMessageReservation = async () => { throw Error('isolated release failure'); }; await assert.rejects( sendChain.submission.gatewaySubmit.failRouteBatch( [{ message: pending, reason: '可用通道均命中通道敏感词', code: 'CHANNEL_SENSITIVE_WORD_NO_ROUTE' }], new Map(), ), /isolated release failure/, ); const persisted = await db.smsMessageRecord.findUniqueOrThrow({ where: { id: pending.id } }); assert.equal(persisted.status, 'failed'); assert.equal(persisted.channelWordFinalizationPending, true); assert.equal(persisted.drainageReceiptPending, false); checks.push( 'real batch failure SQL marks non-CMPP finalization pending before injected release failure; no callback or SMS', ); console.log( JSON.stringify({ success: true, database: name, fixturePrefix: prefix, messageId: messages[0].id, checks }), ); } finally { await db.$disconnect(); }