Skip to content

Commit 648ad3e

Browse files
committed
Add config-driven cohort creation, direct transfers.
1 parent be03ce6 commit 648ad3e

17 files changed

+1623
-136
lines changed

functions/src/cohort.utils.test.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/**
2+
* Tests for cohort utility functions.
3+
*
4+
* Unit tests for transformCohortValuesKeys (no Firestore needed).
5+
* Integration tests require Firestore emulator - run via: npm run test:firestore
6+
*/
7+
8+
import {
9+
createStaticVariableConfig,
10+
createRandomPermutationVariableConfig,
11+
VariableScope,
12+
VariableType,
13+
VariableConfigType,
14+
SeedStrategy,
15+
} from '@deliberation-lab/utils';
16+
import {transformCohortValuesKeys} from './cohort.utils';
17+
18+
describe('transformCohortValuesKeys', () => {
19+
it('should transform alias key to cohortId for matching static variable', () => {
20+
const configs = [
21+
createStaticVariableConfig({
22+
scope: VariableScope.COHORT,
23+
definition: {
24+
name: 'treatment',
25+
description: 'Treatment condition',
26+
schema: VariableType.STRING,
27+
},
28+
value: JSON.stringify('control'),
29+
cohortValues: {
30+
'arm-pro-ai': JSON.stringify('pro_ai'),
31+
'arm-skeptic': JSON.stringify('skeptic'),
32+
},
33+
}),
34+
];
35+
36+
const result = transformCohortValuesKeys(
37+
configs,
38+
'arm-pro-ai',
39+
'cohort-123',
40+
);
41+
42+
expect(result).toHaveLength(1);
43+
expect(result[0].type).toBe(VariableConfigType.STATIC);
44+
if (result[0].type === VariableConfigType.STATIC) {
45+
expect(result[0].cohortValues).toEqual({
46+
'cohort-123': JSON.stringify('pro_ai'),
47+
});
48+
}
49+
});
50+
51+
it('should not modify config when alias not in cohortValues', () => {
52+
const configs = [
53+
createStaticVariableConfig({
54+
scope: VariableScope.COHORT,
55+
definition: {
56+
name: 'treatment',
57+
description: 'Treatment condition',
58+
schema: VariableType.STRING,
59+
},
60+
value: JSON.stringify('control'),
61+
cohortValues: {
62+
'arm-pro-ai': JSON.stringify('pro_ai'),
63+
},
64+
}),
65+
];
66+
67+
const result = transformCohortValuesKeys(
68+
configs,
69+
'arm-other',
70+
'cohort-123',
71+
);
72+
73+
expect(result).toHaveLength(1);
74+
expect(result[0]).toBe(configs[0]); // Same reference, not modified
75+
});
76+
77+
it('should not modify config when cohortValues not present', () => {
78+
const configs = [
79+
createStaticVariableConfig({
80+
scope: VariableScope.COHORT,
81+
definition: {
82+
name: 'treatment',
83+
description: 'Treatment condition',
84+
schema: VariableType.STRING,
85+
},
86+
value: JSON.stringify('control'),
87+
// No cohortValues
88+
}),
89+
];
90+
91+
const result = transformCohortValuesKeys(
92+
configs,
93+
'arm-pro-ai',
94+
'cohort-123',
95+
);
96+
97+
expect(result).toHaveLength(1);
98+
expect(result[0]).toBe(configs[0]); // Same reference, not modified
99+
});
100+
101+
it('should not modify non-static variable configs', () => {
102+
const configs = [
103+
createRandomPermutationVariableConfig({
104+
scope: VariableScope.COHORT,
105+
definition: {
106+
name: 'items',
107+
description: 'Random items',
108+
schema: VariableType.array(VariableType.STRING),
109+
},
110+
values: [JSON.stringify('a'), JSON.stringify('b')],
111+
shuffleConfig: {
112+
shuffle: true,
113+
seed: SeedStrategy.COHORT,
114+
customSeed: '',
115+
},
116+
}),
117+
];
118+
119+
const result = transformCohortValuesKeys(
120+
configs,
121+
'arm-pro-ai',
122+
'cohort-123',
123+
);
124+
125+
expect(result).toHaveLength(1);
126+
expect(result[0]).toBe(configs[0]); // Same reference, not modified
127+
});
128+
129+
it('should transform multiple configs with matching aliases', () => {
130+
const configs = [
131+
createStaticVariableConfig({
132+
scope: VariableScope.COHORT,
133+
definition: {
134+
name: 'treatment',
135+
description: 'Treatment condition',
136+
schema: VariableType.STRING,
137+
},
138+
value: JSON.stringify('control'),
139+
cohortValues: {
140+
'arm-pro-ai': JSON.stringify('pro_ai'),
141+
'arm-skeptic': JSON.stringify('skeptic'),
142+
},
143+
}),
144+
createStaticVariableConfig({
145+
scope: VariableScope.COHORT,
146+
definition: {
147+
name: 'agent_name',
148+
description: 'Agent name',
149+
schema: VariableType.STRING,
150+
},
151+
value: JSON.stringify('Default Agent'),
152+
cohortValues: {
153+
'arm-pro-ai': JSON.stringify('Pro-AI Agent'),
154+
'arm-skeptic': JSON.stringify('Skeptic Agent'),
155+
},
156+
}),
157+
createStaticVariableConfig({
158+
scope: VariableScope.EXPERIMENT,
159+
definition: {
160+
name: 'experiment_name',
161+
description: 'Experiment name',
162+
schema: VariableType.STRING,
163+
},
164+
value: JSON.stringify('My Experiment'),
165+
// No cohortValues - experiment-level variable
166+
}),
167+
];
168+
169+
const result = transformCohortValuesKeys(
170+
configs,
171+
'arm-pro-ai',
172+
'cohort-xyz',
173+
);
174+
175+
expect(result).toHaveLength(3);
176+
177+
// First config transformed
178+
if (result[0].type === VariableConfigType.STATIC) {
179+
expect(result[0].cohortValues).toEqual({
180+
'cohort-xyz': JSON.stringify('pro_ai'),
181+
});
182+
}
183+
184+
// Second config transformed
185+
if (result[1].type === VariableConfigType.STATIC) {
186+
expect(result[1].cohortValues).toEqual({
187+
'cohort-xyz': JSON.stringify('Pro-AI Agent'),
188+
});
189+
}
190+
191+
// Third config unchanged (no cohortValues)
192+
expect(result[2]).toBe(configs[2]);
193+
});
194+
195+
it('should handle empty variable configs array', () => {
196+
const result = transformCohortValuesKeys([], 'arm-pro-ai', 'cohort-123');
197+
expect(result).toEqual([]);
198+
});
199+
200+
it('should preserve other properties of static config when transforming', () => {
201+
const configs = [
202+
createStaticVariableConfig({
203+
id: 'var-1',
204+
scope: VariableScope.COHORT,
205+
definition: {
206+
name: 'treatment',
207+
description: 'Treatment condition',
208+
schema: VariableType.STRING,
209+
},
210+
value: JSON.stringify('control'),
211+
cohortValues: {
212+
'arm-pro-ai': JSON.stringify('pro_ai'),
213+
},
214+
}),
215+
];
216+
217+
const result = transformCohortValuesKeys(
218+
configs,
219+
'arm-pro-ai',
220+
'cohort-123',
221+
);
222+
223+
expect(result).toHaveLength(1);
224+
if (result[0].type === VariableConfigType.STATIC) {
225+
expect(result[0].id).toBe('var-1');
226+
expect(result[0].scope).toBe(VariableScope.COHORT);
227+
expect(result[0].value).toBe(JSON.stringify('control'));
228+
expect(result[0].definition.name).toBe('treatment');
229+
}
230+
});
231+
232+
it('should work with complex cohortValues containing objects', () => {
233+
const proAiPersona = {
234+
name: 'Pro-AI Agent',
235+
traits: ['enthusiastic', 'technical'],
236+
confidence: 0.9,
237+
};
238+
239+
const configs = [
240+
createStaticVariableConfig({
241+
scope: VariableScope.COHORT,
242+
definition: {
243+
name: 'persona',
244+
description: 'Agent persona',
245+
schema: VariableType.object({
246+
name: VariableType.STRING,
247+
traits: VariableType.array(VariableType.STRING),
248+
confidence: VariableType.NUMBER,
249+
}),
250+
},
251+
value: JSON.stringify({name: 'Default', traits: [], confidence: 0.5}),
252+
cohortValues: {
253+
'arm-pro-ai': JSON.stringify(proAiPersona),
254+
},
255+
}),
256+
];
257+
258+
const result = transformCohortValuesKeys(
259+
configs,
260+
'arm-pro-ai',
261+
'cohort-123',
262+
);
263+
264+
if (result[0].type === VariableConfigType.STATIC) {
265+
expect(result[0].cohortValues).toEqual({
266+
'cohort-123': JSON.stringify(proAiPersona),
267+
});
268+
}
269+
});
270+
});

0 commit comments

Comments
 (0)