@traffical/core 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dedup/decision-dedup.d.ts +74 -0
- package/dist/dedup/decision-dedup.d.ts.map +1 -0
- package/dist/dedup/decision-dedup.js +132 -0
- package/dist/dedup/decision-dedup.js.map +1 -0
- package/dist/dedup/index.d.ts +5 -0
- package/dist/dedup/index.d.ts.map +1 -0
- package/dist/dedup/index.js +5 -0
- package/dist/dedup/index.js.map +1 -0
- package/dist/edge/client.d.ts +109 -0
- package/dist/edge/client.d.ts.map +1 -0
- package/dist/edge/client.js +154 -0
- package/dist/edge/client.js.map +1 -0
- package/dist/edge/index.d.ts +7 -0
- package/dist/edge/index.d.ts.map +1 -0
- package/dist/edge/index.js +7 -0
- package/dist/edge/index.js.map +1 -0
- package/dist/hashing/bucket.d.ts +56 -0
- package/dist/hashing/bucket.d.ts.map +1 -0
- package/dist/hashing/bucket.js +89 -0
- package/dist/hashing/bucket.js.map +1 -0
- package/dist/hashing/fnv1a.d.ts +17 -0
- package/dist/hashing/fnv1a.d.ts.map +1 -0
- package/dist/hashing/fnv1a.js +27 -0
- package/dist/hashing/fnv1a.js.map +1 -0
- package/dist/hashing/index.d.ts +8 -0
- package/dist/hashing/index.d.ts.map +1 -0
- package/dist/hashing/index.js +8 -0
- package/dist/hashing/index.js.map +1 -0
- package/dist/ids/index.d.ts +83 -0
- package/dist/ids/index.d.ts.map +1 -0
- package/dist/ids/index.js +165 -0
- package/dist/ids/index.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/resolution/conditions.d.ts +81 -0
- package/dist/resolution/conditions.d.ts.map +1 -0
- package/dist/resolution/conditions.js +197 -0
- package/dist/resolution/conditions.js.map +1 -0
- package/dist/resolution/engine.d.ts +54 -0
- package/dist/resolution/engine.d.ts.map +1 -0
- package/dist/resolution/engine.js +382 -0
- package/dist/resolution/engine.js.map +1 -0
- package/dist/resolution/index.d.ts +8 -0
- package/dist/resolution/index.d.ts.map +1 -0
- package/dist/resolution/index.js +10 -0
- package/dist/resolution/index.js.map +1 -0
- package/dist/types/index.d.ts +440 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +51 -0
- package/src/dedup/decision-dedup.ts +175 -0
- package/src/dedup/index.ts +6 -0
- package/src/edge/client.ts +256 -0
- package/src/edge/index.ts +16 -0
- package/src/hashing/bucket.ts +115 -0
- package/src/hashing/fnv1a.test.ts +87 -0
- package/src/hashing/fnv1a.ts +31 -0
- package/src/hashing/index.ts +15 -0
- package/src/ids/index.ts +221 -0
- package/src/index.ts +136 -0
- package/src/resolution/conditions.ts +253 -0
- package/src/resolution/engine.test.ts +242 -0
- package/src/resolution/engine.ts +480 -0
- package/src/resolution/index.ts +32 -0
- package/src/types/index.ts +508 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution Engine Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates parameter resolution using test vectors.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect } from "bun:test";
|
|
8
|
+
import { resolveParameters, decide } from "./engine.js";
|
|
9
|
+
import type { ConfigBundle } from "../types/index.js";
|
|
10
|
+
|
|
11
|
+
// Load test fixtures
|
|
12
|
+
import bundleBasic from "../../../test-vectors/fixtures/bundle_basic.json";
|
|
13
|
+
import bundleConditions from "../../../test-vectors/fixtures/bundle_conditions.json";
|
|
14
|
+
|
|
15
|
+
// Default values for basic bundle parameters
|
|
16
|
+
const basicDefaults = {
|
|
17
|
+
"ui.primaryColor": "#0000FF",
|
|
18
|
+
"ui.buttonText": "Click Me",
|
|
19
|
+
"pricing.discount": 0,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Default values for conditions bundle parameters
|
|
23
|
+
const conditionsDefaults = {
|
|
24
|
+
"checkout.ctaText": "Complete Purchase",
|
|
25
|
+
"checkout.showUrgency": false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe("resolveParameters", () => {
|
|
29
|
+
const bundle = bundleBasic as unknown as ConfigBundle;
|
|
30
|
+
|
|
31
|
+
test("resolves user-abc correctly", () => {
|
|
32
|
+
// Bucket 551 for layer_ui (treatment: 500-999)
|
|
33
|
+
// Bucket 913 for layer_pricing (no allocation: outside 0-599)
|
|
34
|
+
const assignments = resolveParameters(bundle, { userId: "user-abc" }, basicDefaults);
|
|
35
|
+
|
|
36
|
+
expect(assignments["ui.primaryColor"]).toBe("#FF0000"); // treatment (bucket 551 >= 500)
|
|
37
|
+
expect(assignments["ui.buttonText"]).toBe("Click Me"); // default
|
|
38
|
+
expect(assignments["pricing.discount"]).toBe(0); // no allocation (bucket 913 >= 600)
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("resolves user-xyz correctly", () => {
|
|
42
|
+
// Bucket 214 for layer_ui (control: 0-499)
|
|
43
|
+
// Bucket 42 for layer_pricing (discount_10: 0-299)
|
|
44
|
+
const assignments = resolveParameters(bundle, { userId: "user-xyz" }, basicDefaults);
|
|
45
|
+
|
|
46
|
+
expect(assignments["ui.primaryColor"]).toBe("#0000FF"); // control (bucket 214 < 500)
|
|
47
|
+
expect(assignments["ui.buttonText"]).toBe("Click Me"); // default
|
|
48
|
+
expect(assignments["pricing.discount"]).toBe(10); // discount_10 (bucket 42 in 0-299)
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("resolves user-123 correctly", () => {
|
|
52
|
+
// Bucket 871 for layer_ui (treatment: 500-999)
|
|
53
|
+
// Bucket 177 for layer_pricing (discount_10: 0-299)
|
|
54
|
+
const assignments = resolveParameters(bundle, { userId: "user-123" }, basicDefaults);
|
|
55
|
+
|
|
56
|
+
expect(assignments["ui.primaryColor"]).toBe("#FF0000"); // treatment (bucket 871 >= 500)
|
|
57
|
+
expect(assignments["ui.buttonText"]).toBe("Click Me"); // default
|
|
58
|
+
expect(assignments["pricing.discount"]).toBe(10); // discount_10 (bucket 177 in 0-299)
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("returns defaults when unit key is missing", () => {
|
|
62
|
+
const assignments = resolveParameters(bundle, {}, basicDefaults);
|
|
63
|
+
|
|
64
|
+
// Should return caller defaults when unit key is missing
|
|
65
|
+
expect(assignments["ui.primaryColor"]).toBe("#0000FF");
|
|
66
|
+
expect(assignments["ui.buttonText"]).toBe("Click Me");
|
|
67
|
+
expect(assignments["pricing.discount"]).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("returns defaults when bundle is null", () => {
|
|
71
|
+
const assignments = resolveParameters(null, { userId: "user-abc" }, basicDefaults);
|
|
72
|
+
|
|
73
|
+
expect(assignments["ui.primaryColor"]).toBe("#0000FF");
|
|
74
|
+
expect(assignments["ui.buttonText"]).toBe("Click Me");
|
|
75
|
+
expect(assignments["pricing.discount"]).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("only resolves requested parameters from defaults", () => {
|
|
79
|
+
const partialDefaults = {
|
|
80
|
+
"ui.primaryColor": "#FFFFFF",
|
|
81
|
+
};
|
|
82
|
+
const assignments = resolveParameters(bundle, { userId: "user-abc" }, partialDefaults);
|
|
83
|
+
|
|
84
|
+
expect(Object.keys(assignments)).toEqual(["ui.primaryColor"]);
|
|
85
|
+
expect(assignments["ui.primaryColor"]).toBe("#FF0000");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("resolveParameters with conditions", () => {
|
|
90
|
+
const bundle = bundleConditions as unknown as ConfigBundle;
|
|
91
|
+
|
|
92
|
+
test("high value cart triggers urgency", () => {
|
|
93
|
+
const assignments = resolveParameters(
|
|
94
|
+
bundle,
|
|
95
|
+
{
|
|
96
|
+
userId: "user-high-value",
|
|
97
|
+
cartValue: 150,
|
|
98
|
+
deviceType: "desktop",
|
|
99
|
+
},
|
|
100
|
+
conditionsDefaults
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(assignments["checkout.ctaText"]).toBe("Buy Now - Limited Stock!");
|
|
104
|
+
expect(assignments["checkout.showUrgency"]).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("mobile user with low cart gets mobile CTA", () => {
|
|
108
|
+
const assignments = resolveParameters(
|
|
109
|
+
bundle,
|
|
110
|
+
{
|
|
111
|
+
userId: "user-mobile",
|
|
112
|
+
cartValue: 50,
|
|
113
|
+
deviceType: "mobile",
|
|
114
|
+
},
|
|
115
|
+
conditionsDefaults
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(assignments["checkout.ctaText"]).toBe("Buy Now");
|
|
119
|
+
expect(assignments["checkout.showUrgency"]).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("desktop user with low cart gets defaults", () => {
|
|
123
|
+
const assignments = resolveParameters(
|
|
124
|
+
bundle,
|
|
125
|
+
{
|
|
126
|
+
userId: "user-desktop",
|
|
127
|
+
cartValue: 50,
|
|
128
|
+
deviceType: "desktop",
|
|
129
|
+
},
|
|
130
|
+
conditionsDefaults
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(assignments["checkout.ctaText"]).toBe("Complete Purchase");
|
|
134
|
+
expect(assignments["checkout.showUrgency"]).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("high value cart takes precedence over mobile", () => {
|
|
138
|
+
const assignments = resolveParameters(
|
|
139
|
+
bundle,
|
|
140
|
+
{
|
|
141
|
+
userId: "user-mobile-high",
|
|
142
|
+
cartValue: 200,
|
|
143
|
+
deviceType: "mobile",
|
|
144
|
+
},
|
|
145
|
+
conditionsDefaults
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(assignments["checkout.ctaText"]).toBe("Buy Now - Limited Stock!");
|
|
149
|
+
expect(assignments["checkout.showUrgency"]).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("resolveParameters graceful degradation", () => {
|
|
154
|
+
const bundle = bundleBasic as unknown as ConfigBundle;
|
|
155
|
+
|
|
156
|
+
test("returns defaults when bundle is null", () => {
|
|
157
|
+
const defaults = {
|
|
158
|
+
"ui.primaryColor": "#FFFFFF",
|
|
159
|
+
"ui.fontSize": 16,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const result = resolveParameters(null, { userId: "user-abc" }, defaults);
|
|
163
|
+
|
|
164
|
+
expect(result["ui.primaryColor"]).toBe("#FFFFFF");
|
|
165
|
+
expect(result["ui.fontSize"]).toBe(16);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("merges bundle values with caller defaults", () => {
|
|
169
|
+
const defaults = {
|
|
170
|
+
"ui.primaryColor": "#FFFFFF",
|
|
171
|
+
"ui.fontSize": 16,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = resolveParameters(bundle, { userId: "user-abc" }, defaults);
|
|
175
|
+
|
|
176
|
+
// From bundle (overridden by policy) - bucket 551 = treatment
|
|
177
|
+
expect(result["ui.primaryColor"]).toBe("#FF0000");
|
|
178
|
+
// From caller defaults (not in bundle)
|
|
179
|
+
expect(result["ui.fontSize"]).toBe(16);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("returns caller defaults when unit key is missing", () => {
|
|
183
|
+
const defaults = {
|
|
184
|
+
"ui.primaryColor": "#FFFFFF",
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const result = resolveParameters(bundle, {}, defaults);
|
|
188
|
+
|
|
189
|
+
expect(result["ui.primaryColor"]).toBe("#FFFFFF");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("decide", () => {
|
|
194
|
+
const bundle = bundleBasic as unknown as ConfigBundle;
|
|
195
|
+
|
|
196
|
+
test("returns decision with metadata", () => {
|
|
197
|
+
const decision = decide(bundle, { userId: "user-abc" }, basicDefaults);
|
|
198
|
+
|
|
199
|
+
expect(decision.decisionId).toMatch(/^dec_/);
|
|
200
|
+
expect(decision.assignments["ui.primaryColor"]).toBe("#FF0000");
|
|
201
|
+
expect(decision.metadata.unitKeyValue).toBe("user-abc");
|
|
202
|
+
expect(decision.metadata.layers).toHaveLength(2);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("includes correct layer resolution info", () => {
|
|
206
|
+
const decision = decide(bundle, { userId: "user-abc" }, basicDefaults);
|
|
207
|
+
|
|
208
|
+
const uiLayer = decision.metadata.layers.find(
|
|
209
|
+
(l) => l.layerId === "layer_ui"
|
|
210
|
+
);
|
|
211
|
+
expect(uiLayer).toBeDefined();
|
|
212
|
+
expect(uiLayer!.bucket).toBe(551);
|
|
213
|
+
expect(uiLayer!.policyId).toBe("policy_color_test");
|
|
214
|
+
expect(uiLayer!.allocationName).toBe("treatment");
|
|
215
|
+
|
|
216
|
+
const pricingLayer = decision.metadata.layers.find(
|
|
217
|
+
(l) => l.layerId === "layer_pricing"
|
|
218
|
+
);
|
|
219
|
+
expect(pricingLayer).toBeDefined();
|
|
220
|
+
expect(pricingLayer!.bucket).toBe(913);
|
|
221
|
+
// No allocation matched (bucket 913 is outside all ranges 0-599)
|
|
222
|
+
expect(pricingLayer!.policyId).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("returns defaults with empty metadata when bundle is null", () => {
|
|
226
|
+
const decision = decide(null, { userId: "user-abc" }, basicDefaults);
|
|
227
|
+
|
|
228
|
+
expect(decision.decisionId).toMatch(/^dec_/);
|
|
229
|
+
expect(decision.assignments["ui.primaryColor"]).toBe("#0000FF");
|
|
230
|
+
expect(decision.metadata.unitKeyValue).toBe("");
|
|
231
|
+
expect(decision.metadata.layers).toHaveLength(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("returns defaults with empty metadata when unit key is missing", () => {
|
|
235
|
+
const decision = decide(bundle, {}, basicDefaults);
|
|
236
|
+
|
|
237
|
+
expect(decision.decisionId).toMatch(/^dec_/);
|
|
238
|
+
expect(decision.assignments["ui.primaryColor"]).toBe("#0000FF");
|
|
239
|
+
expect(decision.metadata.unitKeyValue).toBe("");
|
|
240
|
+
expect(decision.metadata.layers).toHaveLength(0);
|
|
241
|
+
});
|
|
242
|
+
});
|