167 lines
7.0 KiB
JavaScript
167 lines
7.0 KiB
JavaScript
// Test environment only. Uses real PostgreSQL in a new isolated schema; no Nest lifecycle or external delivery.
|
|
// CMPP_DIAGNOSTIC_ENV=test node --expose-gc <script> <candidate-api-root>
|
|
import { createRequire } from 'node:module';
|
|
import { resolve } from 'node:path';
|
|
import { readFileSync } from 'node:fs';
|
|
import assert from 'node:assert/strict';
|
|
|
|
if (process.env.CMPP_DIAGNOSTIC_ENV !== 'test') throw new Error('Explicit test environment is required');
|
|
const root = resolve(process.argv[2]);
|
|
const require = createRequire(resolve(root, 'package.json'));
|
|
const { Pool } = require('pg');
|
|
const { PrismaPg } = require('@prisma/adapter-pg');
|
|
const { PrismaClient } = require('@prisma/client');
|
|
const { SignatureRetirementService } = require(
|
|
resolve(root, 'dist/signature-retirement/signature-retirement.service.js'),
|
|
);
|
|
const schema = `cmpp_fix_20260908_${Date.now()}`;
|
|
const admin = new Pool({ connectionString: process.env.DATABASE_URL, max: 1 });
|
|
const tables = [
|
|
'Tenant',
|
|
'SmsApplication',
|
|
'SmsSignature',
|
|
'SmsChannel',
|
|
'SignatureRetirementDetection',
|
|
'SignatureRetirementMessage',
|
|
'SignatureRetirementSuppression',
|
|
'SignatureRetirementWebhook',
|
|
'SignatureRetirementWebhookDelivery',
|
|
'OperationLog',
|
|
'ChannelSignatureReportTask',
|
|
];
|
|
const clients = [];
|
|
const checks = [];
|
|
try {
|
|
const before = (await admin.query('SELECT count(*)::int AS count FROM public."SmsMessageRecord"')).rows[0].count;
|
|
await admin.query(`CREATE SCHEMA "${schema}"`);
|
|
for (const table of tables)
|
|
await admin.query(`CREATE TABLE "${schema}"."${table}" (LIKE public."${table}" INCLUDING ALL)`);
|
|
const columns = await admin.query(
|
|
'SELECT 1 FROM information_schema.columns WHERE table_schema=$1 AND table_name=$2 AND column_name=$3',
|
|
[schema, 'SignatureRetirementMessage', 'dailyGroupKey'],
|
|
);
|
|
if (!columns.rowCount) {
|
|
await admin.query(`SET search_path TO "${schema}"`);
|
|
await admin.query(
|
|
readFileSync(
|
|
resolve(root, 'prisma/migrations/20260908050000_retirement_application_daily_message/migration.sql'),
|
|
'utf8',
|
|
),
|
|
);
|
|
}
|
|
for (let i = 0; i < 2; i++) {
|
|
const url = new URL(process.env.DATABASE_URL);
|
|
url.searchParams.set('options', `-c search_path=${schema} -c statement_timeout=15000`);
|
|
clients.push(
|
|
new PrismaClient({
|
|
adapter: new PrismaPg(new Pool({ connectionString: url.toString(), max: 2 }), {
|
|
schema,
|
|
disposeExternalPool: true,
|
|
}),
|
|
}),
|
|
);
|
|
}
|
|
const [db, other] = clients;
|
|
assert.equal((await db.$queryRawUnsafe('SELECT current_schema() AS schema'))[0].schema, schema);
|
|
const service = new SignatureRetirementService(db);
|
|
const peer = new SignatureRetirementService(other);
|
|
const date = '2026-09-08';
|
|
for (const id of ['t1', 't2']) await db.tenant.create({ data: { id, code: `QA-${id}`, name: id } });
|
|
for (const id of ['app1', 'app2'])
|
|
await db.smsApplication.create({
|
|
data: {
|
|
id,
|
|
tenantId: 't1',
|
|
name: id,
|
|
cmppAccount: `QA-${id}`,
|
|
cmppEnterpriseCode: 'QA',
|
|
secretHash: 'isolated-not-a-credential',
|
|
status: 'disabled',
|
|
},
|
|
});
|
|
for (const [id, app, tenant] of [
|
|
['s1', 'app1', 't1'],
|
|
['s2', 'app1', 't1'],
|
|
['s3', 'app2', 't1'],
|
|
['s4', null, 't1'],
|
|
['s5', null, 't2'],
|
|
]) {
|
|
await db.smsSignature.create({
|
|
data: { id, tenantId: tenant, applicationId: app, name: id, auditStatus: 'approved' },
|
|
});
|
|
await db.signatureRetirementDetection.create({
|
|
data: {
|
|
id: `d-${id}`,
|
|
detectionDate: new Date(date),
|
|
dimensionType: 'enterprise',
|
|
tenantId: tenant,
|
|
applicationId: app,
|
|
signatureId: id,
|
|
carrier: 'mobile',
|
|
windowDays: 30,
|
|
threshold: 1,
|
|
approvedAt: new Date('2026-01-01'),
|
|
status: 'alert',
|
|
cycleId: `cycle-${id}`,
|
|
notificationTitle: '预警',
|
|
notificationContent: `冻结正文-${id}`,
|
|
},
|
|
});
|
|
}
|
|
const concurrent = await Promise.all([service.publishNotifications(date), peer.publishNotifications(date)]);
|
|
assert.equal(
|
|
concurrent.reduce((sum, item) => sum + item.created, 0),
|
|
4,
|
|
);
|
|
assert.equal(await db.signatureRetirementMessage.count(), 4);
|
|
assert.equal((await service.publishNotifications(date)).created, 0);
|
|
const grouped = await db.signatureRetirementMessage.findFirstOrThrow({ where: { applicationId: 'app1' } });
|
|
assert.deepEqual(grouped.detectionIds, ['d-s1', 'd-s2']);
|
|
assert.equal(grouped.content, '冻结正文-s1\n冻结正文-s2');
|
|
checks.push('two-instance daily uniqueness, app/tenant isolation, frozen contents, repeat idempotency');
|
|
const page = await service.listMessages({ dateFrom: date, dateTo: date, signatureKeyword: 's2' });
|
|
assert.equal(page.total, 1);
|
|
assert.equal(page.items[0].id, grouped.id);
|
|
assert.equal(page.items[0].detections.length, 2);
|
|
const unreadBefore = (await service.unreadCount()).count;
|
|
await service.markRead(grouped.id);
|
|
assert.equal((await service.unreadCount()).count, unreadBefore - 1);
|
|
checks.push('secondary member search, message pagination and deduplicated unread count');
|
|
await service.suppressMessage(grouped.id, { mode: 'temporary', days: 7, reason: 'isolated verification' });
|
|
assert.equal(await db.signatureRetirementSuppression.count(), 2);
|
|
assert.equal((await db.signatureRetirementMessage.findUniqueOrThrow({ where: { id: grouped.id } })).suppressed, true);
|
|
checks.push('atomic group suppression');
|
|
// Real database scale fixture, distinct from live business data and from the daily-message test above.
|
|
await admin.query(`INSERT INTO "${schema}"."SignatureRetirementDetection" (id,"detectionDate","dimensionType","tenantId","applicationId","signatureId","channelKey",carrier,"windowDays",threshold,"approvedAt",status)
|
|
SELECT 'scale-'||n, DATE '2026-09-08','channel','t1','app1','s1','scale-'||n,'mobile',30,1,DATE '2026-01-01','healthy' FROM generate_series(1,19783) n`);
|
|
const baseline = process.memoryUsage();
|
|
const started = Date.now();
|
|
const heatmap = await service.heatmap(date);
|
|
assert.equal(heatmap.items.length, 19788);
|
|
assert.ok(heatmap.items.every((item) => item.activityDate === '2026-09-07'));
|
|
const after = process.memoryUsage();
|
|
if (global.gc) global.gc();
|
|
const afterGc = process.memoryUsage();
|
|
assert.ok(after.rss - baseline.rss < 256 * 1024 ** 2, 'Heatmap RSS growth exceeds 256 MiB');
|
|
checks.push('19788 PostgreSQL rows through actual heatmap service, T-1 and bounded RSS');
|
|
const finalCount = (await admin.query('SELECT count(*)::int AS count FROM public."SmsMessageRecord"')).rows[0].count;
|
|
assert.equal(finalCount, before);
|
|
console.log(
|
|
JSON.stringify({
|
|
schema,
|
|
checks,
|
|
baseline,
|
|
after,
|
|
afterGc,
|
|
durationMs: Date.now() - started,
|
|
publicMessages: finalCount,
|
|
smsCalls: 0,
|
|
lifecycleStarted: false,
|
|
}),
|
|
);
|
|
} finally {
|
|
await Promise.all(clients.map((client) => client.$disconnect()));
|
|
// Keep the isolated schema for audit; no cleanup of existing schemas or business records.
|
|
await admin.end();
|
|
}
|