Skip to content

Commit daf56cf

Browse files
nicklaslclaude
andauthored
fix(resolver): bucket-match materialized assignments with duplicate variants (#345)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 781533c commit daf56cf

5 files changed

Lines changed: 130 additions & 6 deletions

File tree

confidence-resolver/src/lib.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,8 @@ impl MaterializationContext {
676676
unit: &str,
677677
rule: &str,
678678
assignments: &'b [rule::Assignment],
679+
segment_name: &str,
680+
bucket_count: i32,
679681
) -> Option<&'b rule::Assignment> {
680682
let records = match &self.records {
681683
MaterializationRecords::Complete(records) => records,
@@ -690,12 +692,29 @@ impl MaterializationContext {
690692
}
691693

692694
let variant = &record.variant;
693-
assignments.iter().find(|assignment| {
694-
matches!(
695-
&assignment.assignment,
696-
Some(Assignment::Variant(VariantAssignment { variant: v }))
697-
if variant == v
698-
)
695+
let matching: Vec<_> = assignments
696+
.iter()
697+
.filter(|assignment| {
698+
matches!(
699+
&assignment.assignment,
700+
Some(Assignment::Variant(VariantAssignment { variant: v }))
701+
if variant == v
702+
)
703+
})
704+
.collect();
705+
706+
if matching.len() <= 1 {
707+
return matching.into_iter().next();
708+
}
709+
710+
let variant_salt = segment_name.split('/').nth(1)?;
711+
let key = format!("{}|{}", variant_salt, unit);
712+
let b = bucket(hash(&key), bucket_count as u64).ok()? as i32;
713+
matching.into_iter().find(|assignment| {
714+
assignment
715+
.bucket_ranges
716+
.iter()
717+
.any(|range| range.lower <= b && b < range.upper)
699718
})
700719
}
701720

@@ -1153,6 +1172,8 @@ impl<'a, H: Host> AccountResolver<'a, H> {
11531172
unit_str,
11541173
&rule.name,
11551174
&spec.assignments,
1175+
segment_name,
1176+
spec.bucket_count,
11561177
) {
11571178
return resolved_value.try_with_variant_match(
11581179
rule, segment, assignment, unit_str,

confidence-resolver/src/resolver_spec_tests.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ struct SpecResolvedFlag {
131131
variant: String,
132132
value: Option<serde_json::Value>,
133133
should_apply: Option<bool>,
134+
assignment_id: Option<String>,
134135
}
135136

136137
// ---------------------------------------------------------------------------
@@ -459,6 +460,37 @@ fn run_spec_test(state: &ResolverState, test_case: &SpecTestCase) {
459460
test_case.name, expected_flag.flag, expected_should_apply, actual_should_apply,
460461
);
461462
}
463+
464+
// assignmentId assertion (from resolve token)
465+
if let Some(expected_assignment_id) = &expected_flag.assignment_id {
466+
let token = resolver
467+
.decrypt_resolve_token(&resolve_response.resolve_token)
468+
.unwrap_or_else(|e| {
469+
panic!(
470+
"[{}] Failed to decrypt resolve token: {}",
471+
test_case.name, e,
472+
)
473+
});
474+
match token.resolve_token {
475+
Some(flags_resolver::resolve_token::ResolveToken::TokenV1(token_v1)) => {
476+
let assigned = token_v1
477+
.assignments
478+
.get(&expected_flag.flag)
479+
.unwrap_or_else(|| {
480+
panic!(
481+
"[{}] Flag '{}' not found in resolve token assignments",
482+
test_case.name, expected_flag.flag,
483+
)
484+
});
485+
assert_eq!(
486+
&assigned.assignment_id, expected_assignment_id,
487+
"[{}] assignmentId mismatch for flag '{}'",
488+
test_case.name, expected_flag.flag,
489+
);
490+
}
491+
_ => panic!("[{}] Expected TokenV1 in resolve token", test_case.name,),
492+
}
493+
}
462494
}
463495
}
464496

@@ -685,3 +717,6 @@ spec_test!(combined_both_match);
685717
spec_test!(combined_only_attr);
686718
spec_test!(combined_only_mat);
687719
spec_test!(combined_neither);
720+
721+
// Materialization: duplicate variant assignments
722+
spec_test!(mat_same_variant_two_assignments_returns_correct_assignment_id);

confidence-resolver/test-payloads/resolver-spec/state.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,6 +2136,41 @@
21362136
}
21372137
}
21382138
]
2139+
},
2140+
"flags/mat-same-variant-flag": {
2141+
"name": "flags/mat-same-variant-flag",
2142+
"state": "ACTIVE",
2143+
"clients": ["clients/test-client"],
2144+
"variants": [
2145+
{ "name": "on", "value": { "enabled": true } }
2146+
],
2147+
"rules": [
2148+
{
2149+
"name": "flags/mat-same-variant-flag/rules/rule1",
2150+
"segment": "segments/country-se",
2151+
"enabled": true,
2152+
"targetingKeySelector": "targeting_key",
2153+
"materializationSpec": {
2154+
"readMaterialization": "materializations/mat1",
2155+
"writeMaterialization": "materializations/mat1"
2156+
},
2157+
"assignmentSpec": {
2158+
"bucketCount": 1000,
2159+
"assignments": [
2160+
{
2161+
"assignmentId": "a1",
2162+
"variant": { "variant": "on" },
2163+
"bucketRanges": [{ "lower": 0, "upper": 500 }]
2164+
},
2165+
{
2166+
"assignmentId": "a2",
2167+
"variant": { "variant": "on" },
2168+
"bucketRanges": [{ "lower": 500, "upper": 1000 }]
2169+
}
2170+
]
2171+
}
2172+
}
2173+
]
21392174
}
21402175
},
21412176
"segments": {

confidence-resolver/test-payloads/resolver-spec/tests.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4057,5 +4057,38 @@
40574057
"expectError": null
40584058
}
40594059
}
4060+
},
4061+
{
4062+
"name": "mat_same_variant_two_assignments_returns_correct_assignment_id",
4063+
"resolveRequest": {
4064+
"flags": ["flags/mat-same-variant-flag"],
4065+
"evaluationContext": { "targeting_key": "user1", "country": "SE" },
4066+
"clientSecret": "test-secret"
4067+
},
4068+
"materializations": {
4069+
"user1": {
4070+
"materializations/mat1": {
4071+
"isUnitInMaterialization": true,
4072+
"ruleToVariant": {
4073+
"flags/mat-same-variant-flag/rules/rule1": "on"
4074+
}
4075+
}
4076+
}
4077+
},
4078+
"expectedResult": {
4079+
"general": {
4080+
"resolvedFlags": [
4081+
{
4082+
"flag": "flags/mat-same-variant-flag",
4083+
"reason": "RESOLVE_REASON_MATCH",
4084+
"variant": "on",
4085+
"value": { "enabled": true },
4086+
"shouldApply": true,
4087+
"assignmentId": "a2"
4088+
}
4089+
],
4090+
"expectError": null
4091+
}
4092+
}
40604093
}
40614094
]
Binary file not shown.

0 commit comments

Comments
 (0)