From 1e1ee969dc73fe5570b41d20c8a8285874c136d5 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 21 Mar 2026 11:52:08 -0700 Subject: [PATCH] feat(flags): add seed flags for 6 missing products + 6 new evaluator edge-case tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - seed.ts: add default flags for jarvisjr (3), peakpulse (3), flowmonk (2), notelett (2), actiontrail (2), localmemgpt (2) Previously only chronomind, nomgap, mindlyst, smartauth, lysnrai had seed flags — common flags (maintenance_mode, telemetry_enabled) were not seeded for newer products - flags.test.ts: 63 → 69 tests (+6): - anonymous user with partial percentage returns off - partial rule rollout (0%) skips even matching users - neq operator (not equal) - contains operator - gte + lt numeric range on custom attributes - missing context attribute returns off --- .../src/modules/flags/flags.test.ts | 137 ++++++++++++++++++ .../src/modules/flags/seed.ts | 110 ++++++++++++++ 2 files changed, 247 insertions(+) diff --git a/services/platform-service/src/modules/flags/flags.test.ts b/services/platform-service/src/modules/flags/flags.test.ts index ce107b1a..9c418a49 100644 --- a/services/platform-service/src/modules/flags/flags.test.ts +++ b/services/platform-service/src/modules/flags/flags.test.ts @@ -680,6 +680,143 @@ describe('evaluateFlag', () => { expect(result.reason).not.toBe('prerequisite_failed'); }); + it('returns off for anonymous user with partial percentage', () => { + const flag = makeFlag({ percentage: 50 }); + const result = evaluateFlag({ flag, ctx: {}, allFlags: [flag], segments: [] }); + expect(result.reason).toBe('off'); + expect(result.value).toBe(false); + }); + + it('applies partial rollout on targeting rule', () => { + // Rule has 0% rollout — even matching users don't get it + const flag = makeFlag({ + percentage: 0, + targetingRules: [ + { + id: 'r1', + clauses: [{ attribute: 'platform', operator: 'eq', values: ['ios'] }], + variationKey: 'on', + rolloutPercentage: 0, + }, + ], + }); + const result = evaluateFlag({ + flag, + ctx: { userId: 'u1', platform: 'ios' }, + allFlags: [flag], + segments: [], + }); + expect(result.reason).toBe('off'); + }); + + it('evaluates neq operator', () => { + const flag = makeFlag({ + percentage: 0, + targetingRules: [ + { + id: 'r1', + clauses: [{ attribute: 'platform', operator: 'neq', values: ['web'] }], + variationKey: 'on', + rolloutPercentage: 100, + }, + ], + }); + const ios = evaluateFlag({ + flag, + ctx: { userId: 'u1', platform: 'ios' }, + allFlags: [flag], + segments: [], + }); + expect(ios.reason).toBe('rule_match'); + const web = evaluateFlag({ + flag, + ctx: { userId: 'u1', platform: 'web' }, + allFlags: [flag], + segments: [], + }); + expect(web.reason).toBe('off'); + }); + + it('evaluates contains operator', () => { + const flag = makeFlag({ + percentage: 0, + targetingRules: [ + { + id: 'r1', + clauses: [{ attribute: 'email', operator: 'contains', values: ['bytelyst'] }], + variationKey: 'on', + rolloutPercentage: 100, + }, + ], + }); + const match = evaluateFlag({ + flag, + ctx: { userId: 'u1', email: 'dev@bytelyst.com' }, + allFlags: [flag], + segments: [], + }); + expect(match.reason).toBe('rule_match'); + const noMatch = evaluateFlag({ + flag, + ctx: { userId: 'u1', email: 'dev@gmail.com' }, + allFlags: [flag], + segments: [], + }); + expect(noMatch.reason).toBe('off'); + }); + + it('evaluates gt and lt operators on numeric custom attribute', () => { + const flag = makeFlag({ + percentage: 0, + targetingRules: [ + { + id: 'r1', + clauses: [ + { attribute: 'age', operator: 'gte', values: [18] }, + { attribute: 'age', operator: 'lt', values: [65] }, + ], + variationKey: 'on', + rolloutPercentage: 100, + }, + ], + }); + const adult = evaluateFlag({ + flag, + ctx: { userId: 'u1', custom: { age: 30 } }, + allFlags: [flag], + segments: [], + }); + expect(adult.reason).toBe('rule_match'); + const minor = evaluateFlag({ + flag, + ctx: { userId: 'u1', custom: { age: 15 } }, + allFlags: [flag], + segments: [], + }); + expect(minor.reason).toBe('off'); + }); + + it('returns off when clause attribute is missing from context', () => { + const flag = makeFlag({ + percentage: 0, + targetingRules: [ + { + id: 'r1', + clauses: [{ attribute: 'email', operator: 'eq', values: ['test@test.com'] }], + variationKey: 'on', + rolloutPercentage: 100, + }, + ], + }); + const result = evaluateFlag({ + flag, + ctx: { userId: 'u1' }, + allFlags: [flag], + segments: [], + }); + expect(result.reason).toBe('off'); + }); + it('handles circular prerequisites gracefully', () => { const a = makeFlag({ key: 'a', prerequisites: [{ flagKey: 'b', variationKey: 'on' }] }); const b = makeFlag({ key: 'b', prerequisites: [{ flagKey: 'a', variationKey: 'on' }] }); diff --git a/services/platform-service/src/modules/flags/seed.ts b/services/platform-service/src/modules/flags/seed.ts index a9aeb55a..34a389ed 100644 --- a/services/platform-service/src/modules/flags/seed.ts +++ b/services/platform-service/src/modules/flags/seed.ts @@ -235,6 +235,116 @@ const PRODUCT_FLAGS: Record = { percentage: 0, }, ], + jarvisjr: [ + { + key: 'voice_sessions_enabled', + enabled: true, + description: 'Voice-first coaching sessions', + platforms: ['ios', 'web'], + percentage: 100, + }, + { + key: 'agent_marketplace_enabled', + enabled: false, + description: 'Agent marketplace browse and purchase', + platforms: ['ios', 'web'], + percentage: 0, + }, + { + key: 'agent_memory_enabled', + enabled: true, + description: 'Per-agent persistent memory', + platforms: ['ios', 'web'], + percentage: 100, + }, + ], + peakpulse: [ + { + key: 'live_activity_enabled', + enabled: true, + description: 'Dynamic Island + Lock Screen live activity', + platforms: ['ios'], + percentage: 100, + }, + { + key: 'ski_intelligence_enabled', + enabled: true, + description: 'Ski run detection and speed zones', + platforms: ['ios'], + percentage: 100, + }, + { + key: 'cloud_sync_enabled', + enabled: false, + description: 'Cloud sync for sessions', + platforms: [], + percentage: 0, + }, + ], + flowmonk: [ + { + key: 'agent_recommendations_enabled', + enabled: true, + description: 'AI agent scheduling recommendations', + platforms: ['web', 'mobile'], + percentage: 100, + }, + { + key: 'schedule_sse_enabled', + enabled: true, + description: 'Real-time schedule updates via SSE', + platforms: ['web'], + percentage: 100, + }, + ], + notelett: [ + { + key: 'mcp_tools_enabled', + enabled: true, + description: 'MCP tool integration for AI agents', + platforms: [], + percentage: 100, + }, + { + key: 'note_artifacts_enabled', + enabled: true, + description: 'File/artifact attachments on notes', + platforms: ['web', 'mobile'], + percentage: 100, + }, + ], + actiontrail: [ + { + key: 'trace_explorer_enabled', + enabled: true, + description: 'Trace grouping and explorer UI', + platforms: ['web'], + percentage: 100, + }, + { + key: 'webhook_dispatch_enabled', + enabled: true, + description: 'Webhook event dispatch to subscribers', + platforms: [], + percentage: 100, + }, + ], + localmemgpt: [ + { + key: 'multi_model_compare_enabled', + enabled: true, + description: 'Multi-model compare (SSE)', + platforms: ['web'], + percentage: 100, + }, + { + key: 'rag_documents_enabled', + enabled: true, + description: 'RAG document upload and retrieval', + platforms: ['web'], + percentage: 100, + }, + ], }; // ─── Seed function ───────────────────────────────────────────────────────────