feat(flags): add seed flags for 6 missing products + 6 new evaluator edge-case tests
- 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
This commit is contained in:
parent
dd113b96c9
commit
1e1ee969dc
@ -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' }] });
|
||||
|
||||
@ -235,6 +235,116 @@ const PRODUCT_FLAGS: Record<string, FlagSeedDef[]> = {
|
||||
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 ───────────────────────────────────────────────────────────
|
||||
|
||||
Loading…
Reference in New Issue
Block a user