Files
Kordant/packages/api/src/__tests__/sms-classifier-race-condition.test.ts
Michael Freno e704a9074a FRE-4533: Merge apps/{api,web,mobile} and shared-db into ShieldAI repo
Merge FrenoCorp apps into ShieldAI packages/:
- packages/api: merged routes (notifications), middleware (auth, rate-limit, error, logging), config, services (darkwatch, spamshield, voiceprint), tests
- packages/web: new SolidJS web app stub
- packages/mobile: new SolidJS mobile app stub
- packages/shared-db: new Prisma DB package (separate from existing packages/db)
- pnpm-workspace.yaml: restored (apps/* removed, already covered by packages/*)

Next: reconcile packages/shared-db with packages/db, and fix server.ts correlationRoutes import
2026-05-02 10:19:11 -04:00

145 lines
4.4 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { SMSClassifierService } from '../services/spamshield/spamshield.service';
// Mock shared-db before anything else (Prisma client is not generated in test env)
vi.mock('@shieldsai/shared-db', () => ({
prisma: {},
SpamFeedback: {},
}));
// Mock the feature flags module to control enableMLClassifier
vi.mock('../services/spamshield/spamshield.config', () => ({
spamShieldEnv: {
SPAM_THRESHOLD_AUTO_BLOCK: 0.85,
SPAM_THRESHOLD_FLAG: 0.6,
},
spamFeatureFlags: {
enableMLClassifier: true,
},
SpamDecision: {
ALLOW: 'allow',
FLAG: 'flag',
BLOCK: 'block',
CHALLENGE: 'challenge',
},
SpamLayer: {
NUMBER_REPUTATION: 'number_reputation',
CONTENT_CLASSIFICATION: 'content_classification',
BEHAVIORAL_ANALYSIS: 'behavioral_analysis',
COMMUNITY_INTELLIGENCE: 'community_intelligence',
},
ConfidenceLevel: {
LOW: 'low',
MEDIUM: 'medium',
HIGH: 'high',
VERY_HIGH: 'very_high',
},
spamRateLimits: {},
}));
describe('SMSClassifierService', () => {
let classifier: SMSClassifierService;
let initializeCalls: number;
let initializeDelay: Promise<void>;
beforeEach(() => {
// Re-import after mock to get fresh module state
initializeCalls = 0;
initializeDelay = new Promise(resolve => setTimeout(resolve, 50));
classifier = new SMSClassifierService();
// Override initialize to track calls and add delay
classifier.initialize = async () => {
initializeCalls++;
await initializeDelay;
};
});
describe('initialization race condition', () => {
it('should call initialize only once under concurrent classify calls', async () => {
const promises = Array.from({ length: 10 }, () =>
classifier.classify('ACT NOW - Limited offer!'),
);
const results = await Promise.all(promises);
expect(initializeCalls).toBe(1);
expect(results).toHaveLength(10);
results.forEach(r => {
expect(r).toHaveProperty('isSpam');
expect(r).toHaveProperty('confidence');
expect(r).toHaveProperty('spamFeatures');
});
});
it('should handle interleaved calls after partial initialization', async () => {
const batch1 = Array.from({ length: 5 }, () =>
classifier.classify('First batch message'),
);
await Promise.all(batch1);
expect(initializeCalls).toBe(1);
const batch2 = Array.from({ length: 5 }, () =>
classifier.classify('Second batch message'),
);
await Promise.all(batch2);
// initialize should still only have been called once
expect(initializeCalls).toBe(1);
});
it('should return consistent results for same input under concurrency', async () => {
const text = 'URGENT: Click http://example.com now!';
const promises = Array.from({ length: 20 }, () =>
classifier.classify(text),
);
const results = await Promise.all(promises);
const firstResult = results[0];
results.forEach((r, i) => {
expect(r.isSpam).toBe(firstResult.isSpam);
expect(r.confidence).toBe(firstResult.confidence);
expect(r.spamFeatures).toEqual(firstResult.spamFeatures);
});
});
it('should handle rapid sequential calls without re-initializing', async () => {
for (let i = 0; i < 50; i++) {
await classifier.classify(`Message ${i}`);
}
expect(initializeCalls).toBe(1);
});
});
describe('feature extraction', () => {
it('should detect URL presence', async () => {
const result = await classifier.classify('Visit www.example.com');
expect(result.spamFeatures).toContain('url_present');
});
it('should detect urgency keywords', async () => {
const result = await classifier.classify('Act now! This offer is urgent.');
expect(result.spamFeatures).toContain('urgency_keyword');
});
it('should detect excessive capitalization', async () => {
const result = await classifier.classify('BUY THIS NOW!!!');
expect(result.spamFeatures).toContain('excessive_caps');
});
it('should detect multiple features', async () => {
const result = await classifier.classify(
'URGENT: Visit www.example.com NOW!!!',
);
expect(result.spamFeatures).toContain('url_present');
expect(result.spamFeatures).toContain('urgency_keyword');
expect(result.spamFeatures).toContain('excessive_caps');
});
});
});