Skip to content

Commit dfebd09

Browse files
committed
test(flags): cover experiment bucketing and add changeset
1 parent 718210f commit dfebd09

2 files changed

Lines changed: 106 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: minor
3+
---
4+
5+
Add build-time client config overrides via environment variables, with typed deterministic experiment bucketing helpers for progressive feature rollout and A/B testing.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { selectExperimentVariant, type ExperimentConfig } from './useClientConfig';
3+
4+
const baseExperiment: ExperimentConfig = {
5+
enabled: true,
6+
rolloutPercentage: 100,
7+
controlVariant: 'control',
8+
variants: ['alpha', 'beta'],
9+
};
10+
11+
describe('selectExperimentVariant', () => {
12+
it('returns control when experiment is disabled', () => {
13+
const result = selectExperimentVariant(
14+
'threadUI',
15+
{ ...baseExperiment, enabled: false },
16+
'@alice:example.org'
17+
);
18+
19+
expect(result.inExperiment).toBe(false);
20+
expect(result.variant).toBe('control');
21+
});
22+
23+
it('returns control when subject id is missing', () => {
24+
const result = selectExperimentVariant('threadUI', baseExperiment, undefined);
25+
26+
expect(result.inExperiment).toBe(false);
27+
expect(result.variant).toBe('control');
28+
});
29+
30+
it('returns control when rollout is 0', () => {
31+
const result = selectExperimentVariant(
32+
'threadUI',
33+
{ ...baseExperiment, rolloutPercentage: 0 },
34+
'@alice:example.org'
35+
);
36+
37+
expect(result.inExperiment).toBe(false);
38+
expect(result.variant).toBe('control');
39+
expect(result.rolloutPercentage).toBe(0);
40+
});
41+
42+
it('normalizes rollout less than 0 to 0', () => {
43+
const result = selectExperimentVariant(
44+
'threadUI',
45+
{ ...baseExperiment, rolloutPercentage: -10 },
46+
'@alice:example.org'
47+
);
48+
49+
expect(result.inExperiment).toBe(false);
50+
expect(result.variant).toBe('control');
51+
expect(result.rolloutPercentage).toBe(0);
52+
});
53+
54+
it('normalizes rollout greater than 100 to 100', () => {
55+
const result = selectExperimentVariant(
56+
'threadUI',
57+
{ ...baseExperiment, rolloutPercentage: 999 },
58+
'@alice:example.org'
59+
);
60+
61+
expect(result.inExperiment).toBe(true);
62+
expect(result.rolloutPercentage).toBe(100);
63+
expect(['alpha', 'beta']).toContain(result.variant);
64+
});
65+
66+
it('falls back to control when variants are missing after filtering', () => {
67+
const result = selectExperimentVariant(
68+
'threadUI',
69+
{
70+
...baseExperiment,
71+
variants: ['', 'control'],
72+
},
73+
'@alice:example.org'
74+
);
75+
76+
expect(result.inExperiment).toBe(false);
77+
expect(result.variant).toBe('control');
78+
});
79+
80+
it('is deterministic for the same key and subject', () => {
81+
const first = selectExperimentVariant('threadUI', baseExperiment, '@alice:example.org');
82+
const second = selectExperimentVariant('threadUI', baseExperiment, '@alice:example.org');
83+
84+
expect(second).toEqual(first);
85+
});
86+
87+
it('uses default control variant when none is provided', () => {
88+
const result = selectExperimentVariant(
89+
'threadUI',
90+
{
91+
enabled: true,
92+
rolloutPercentage: 100,
93+
variants: ['alpha'],
94+
},
95+
'@alice:example.org'
96+
);
97+
98+
expect(result.inExperiment).toBe(true);
99+
expect(result.variant).toBe('alpha');
100+
});
101+
});

0 commit comments

Comments
 (0)