FRE-4510: Implement feature flag checks for spam classification

- Add runtime flag evaluation from FLAG_<KEY> environment variables
- Add enableCallAnalysis flag check to analyzeCall() and interceptCall()
- Add enableFeedbackLoop flag check to recordFeedback()
- Add 19 tests for feature flag behavior (checkFeatureFlag, getters, service integration)
- Add vitest config and test script to spamshield package
This commit is contained in:
2026-05-02 01:53:59 -04:00
parent 90fbbc4465
commit e580a693c7
5 changed files with 225 additions and 4 deletions

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SpamShieldService } from '../src/services/spamshield.service';
import { spamConfig } from '../src/config/spamshield.config';
import { spamConfig, checkFeatureFlag, spamFeatureFlags, spamFeatureFlagDefaults } from '../src/config/spamshield.config';
import { validatePhoneNumber } from '../src/utils/phone-validation';
const mockFetch = vi.fn();
@@ -306,4 +306,172 @@ describe('SpamShieldService', () => {
});
});
});
describe('feature flags', () => {
afterEach(() => {
for (const key of ['FLAG_ENABLEHIYAINTEGRATION', 'FLAG_ENABLETRUECALLERINTEGRATION', 'FLAG_ENABLESMSCLASSIFICATION', 'FLAG_ENABLECALLANALYSIS', 'FLAG_ENABLEFEEDBACKLOOP']) {
delete process.env[key];
}
});
describe('checkFeatureFlag', () => {
it('returns default true when env var is unset', () => {
expect(checkFeatureFlag('enableHiyaIntegration')).toBe(true);
expect(checkFeatureFlag('enableCallAnalysis')).toBe(true);
expect(checkFeatureFlag('enableFeedbackLoop')).toBe(true);
});
it('returns true when env var is "true"', () => {
process.env.FLAG_ENABLECALLANALYSIS = 'true';
expect(checkFeatureFlag('enableCallAnalysis')).toBe(true);
});
it('returns true when env var is "1"', () => {
process.env.FLAG_ENABLECALLANALYSIS = '1';
expect(checkFeatureFlag('enableCallAnalysis')).toBe(true);
});
it('returns false when env var is "false"', () => {
process.env.FLAG_ENABLECALLANALYSIS = 'false';
expect(checkFeatureFlag('enableCallAnalysis')).toBe(false);
});
it('returns false when env var is "0"', () => {
process.env.FLAG_ENABLECALLANALYSIS = '0';
expect(checkFeatureFlag('enableCallAnalysis')).toBe(false);
});
it('returns false for any non-true/non-1 value', () => {
process.env.FLAG_ENABLECALLANALYSIS = 'yes';
expect(checkFeatureFlag('enableCallAnalysis')).toBe(false);
});
});
describe('spamFeatureFlags getters', () => {
it('reflects env var overrides for enableCallAnalysis', () => {
expect(spamFeatureFlags.enableCallAnalysis).toBe(true);
process.env.FLAG_ENABLECALLANALYSIS = 'false';
expect(spamFeatureFlags.enableCallAnalysis).toBe(false);
});
it('reflects env var overrides for enableFeedbackLoop', () => {
expect(spamFeatureFlags.enableFeedbackLoop).toBe(true);
process.env.FLAG_ENABLEFEEDBACKLOOP = 'false';
expect(spamFeatureFlags.enableFeedbackLoop).toBe(false);
});
it('reflects env var overrides for enableHiyaIntegration', () => {
expect(spamFeatureFlags.enableHiyaIntegration).toBe(true);
process.env.FLAG_ENABLEHIYAINTEGRATION = 'false';
expect(spamFeatureFlags.enableHiyaIntegration).toBe(false);
});
it('reflects env var overrides for enableTruecallerIntegration', () => {
expect(spamFeatureFlags.enableTruecallerIntegration).toBe(true);
process.env.FLAG_ENABLETRUECALLERINTEGRATION = 'false';
expect(spamFeatureFlags.enableTruecallerIntegration).toBe(false);
});
it('reflects env var overrides for enableSMSClassification', () => {
expect(spamFeatureFlags.enableSMSClassification).toBe(true);
process.env.FLAG_ENABLESMSCLASSIFICATION = 'false';
expect(spamFeatureFlags.enableSMSClassification).toBe(false);
});
});
describe('enableCallAnalysis flag', () => {
it('throws when call analysis is disabled in analyzeCall', async () => {
process.env.FLAG_ENABLECALLANALYSIS = 'false';
const result = service.analyzeCall('+14155552671', new Date());
await expect(result).rejects.toThrow('Call analysis disabled via feature flag');
});
it('throws when call analysis is disabled in interceptCall', async () => {
process.env.FLAG_ENABLECALLANALYSIS = 'false';
const call = {
callId: 'call-1',
phoneNumber: '+14155552671',
from: '+14155552671',
to: '+14155551234',
startTime: new Date(),
direction: 'inbound' as const,
carrierType: 'twilio' as any,
carrierSid: 'CA123',
};
const result = service.interceptCall(call);
await expect(result).rejects.toThrow('Call analysis disabled via feature flag');
});
it('passes flag check when call analysis is enabled', async () => {
process.env.FLAG_ENABLECALLANALYSIS = 'true';
const result = service.analyzeCall('+14155552671', new Date());
try {
const res = await result;
expect(res).toMatchObject({
decision: 'ALLOW',
confidence: 0,
ruleMatches: [],
});
} catch (e) {
expect((e as Error).message).not.toContain('Call analysis disabled');
}
});
});
describe('enableFeedbackLoop flag', () => {
it('throws when feedback loop is disabled in recordFeedback', async () => {
process.env.FLAG_ENABLEFEEDBACKLOOP = 'false';
const result = service.recordFeedback('user123', '+14155552671', true);
await expect(result).rejects.toThrow('Feedback loop disabled via feature flag');
});
it('passes flag check when feedback loop is enabled', async () => {
process.env.FLAG_ENABLEFEEDBACKLOOP = 'true';
const result = service.recordFeedback('user123', '+14155552671', true);
try {
await result;
} catch (e) {
expect((e as Error).message).not.toContain('Feedback loop disabled');
}
});
});
describe('enableHiyaIntegration flag', () => {
it('excludes Hiya score when integration is disabled', async () => {
service.resetCircuits();
process.env.FLAG_ENABLEHIYAINTEGRATION = 'false';
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({ spamProbability: 0.3 }),
} as Response);
const result = await service.checkReputation('+14155552671');
expect(result.hiyaScore).toBe(undefined);
expect(result.source).toBe('truecaller');
});
});
describe('enableTruecallerIntegration flag', () => {
it('excludes Truecaller score when integration is disabled', async () => {
service.resetCircuits();
process.env.FLAG_ENABLETRUECALLERINTEGRATION = 'false';
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({ spamScore: 0.8 }),
} as Response);
const result = await service.checkReputation('+14155552671');
expect(result.source).toBe('hiya');
expect(result.truecallerScore).toBe(undefined);
});
});
describe('enableSMSClassification flag', () => {
it('throws when SMS classification is disabled in classifySms', async () => {
process.env.FLAG_ENABLESMSCLASSIFICATION = 'false';
const result = service.classifySms('test message');
await expect(result).rejects.toThrow('SMS Classification disabled via feature flag');
});
});
});
});