diff --git a/core/pkg/evaluator/fractional.go b/core/pkg/evaluator/fractional.go index 5d3973789..1d4cc552f 100644 --- a/core/pkg/evaluator/fractional.go +++ b/core/pkg/evaluator/fractional.go @@ -1,213 +1,293 @@ package evaluator import ( - "errors" - "fmt" - "math" + "errors" + "fmt" + "math" - "github.com/open-feature/flagd/core/pkg/logger" - "github.com/twmb/murmur3" + "github.com/fxamacker/cbor/v2" + "github.com/open-feature/flagd/core/pkg/logger" + "github.com/twmb/murmur3" ) +var cborEncMode, _ = cbor.CoreDetEncOptions().EncMode() + const maxWeightSum = math.MaxInt32 // 2,147,483,647 const FractionEvaluationName = "fractional" type Fractional struct { - Logger *logger.Logger + Logger *logger.Logger } type fractionalEvaluationDistribution struct { - totalWeight int32 - weightedVariants []fractionalEvaluationVariant - data any - logger *logger.Logger + totalWeight int32 + weightedVariants []fractionalEvaluationVariant + data any + logger *logger.Logger } type fractionalEvaluationVariant struct { - variant any // string, bool, number or nil - weight int32 + variant any // string, bool, number or nil + weight int32 } func (v fractionalEvaluationVariant) getPercentage(totalWeight int32) float64 { - if totalWeight == 0 { - return 0 - } + if totalWeight == 0 { + return 0 + } - return 100 * float64(v.weight) / float64(totalWeight) + return 100 * float64(v.weight) / float64(totalWeight) } func NewFractional(logger *logger.Logger) *Fractional { - return &Fractional{Logger: logger} + return &Fractional{Logger: logger} } func (fe *Fractional) Evaluate(values, data any) any { - valueToDistribute, feDistributions, err := parseFractionalEvaluationData(values, data, fe.Logger) - if err != nil { - fe.Logger.Warn(fmt.Sprintf("parse fractional evaluation data: %v", err)) - return nil - } - - if feDistributions == nil { - return nil - } - - hashValue := uint32(murmur3.StringSum32(valueToDistribute)) - return distributeValue(hashValue, feDistributions) + bytesToDistribute, feDistributions, err := parseFractionalEvaluationData(values, data, fe.Logger) + if err != nil { + fe.Logger.Warn(fmt.Sprintf("parse fractional evaluation data: %v", err)) + return nil + } + + if feDistributions == nil { + return nil + } + + hashValue := murmur3.Sum32(bytesToDistribute) + return distributeValue(hashValue, feDistributions) +} + +func normalizeValue(val any) any { + switch v := val.(type) { + case float64: + if math.IsNaN(v) || math.IsInf(v, 0) { + return v + } + if v == math.Trunc(v) { + if v >= 0 && v <= float64(math.MaxUint64) { + return uint64(v) + } + if v < 0 && v >= float64(math.MinInt64) { + return int64(v) + } + } + return v + case float32: + return normalizeValue(float64(v)) + case int: + if v >= 0 { + return uint64(v) + } + return int64(v) + case int64: + if v >= 0 { + return uint64(v) + } + return v + case uint: + return uint64(v) + case uint32: + return uint64(v) + case uint64: + return v + case map[string]any: + res := make(map[string]any, len(v)) + for k, item := range v { + res[k] = normalizeValue(item) + } + return res + case []any: + res := make([]any, len(v)) + for i, item := range v { + res[i] = normalizeValue(item) + } + return res + default: + return v + } +} + +func encodeDeterministicCBOR(val any) ([]byte, error) { + normalized := normalizeValue(val) + return cborEncMode.Marshal(normalized) } -func parseFractionalEvaluationData(values, data any, logger *logger.Logger) (string, *fractionalEvaluationDistribution, error) { - valuesArray, ok := values.([]any) - if !ok { - return "", nil, errors.New("fractional evaluation data is not an array") - } - if len(valuesArray) < 1 { - return "", nil, errors.New("fractional evaluation data must contain at least one distribution") - } - - dataMap, ok := data.(map[string]any) - if !ok { - return "", nil, errors.New("data isn't of type map[string]any") - } - - properties, _ := getFlagdProperties(dataMap) - flagKey := properties.FlagKey - - bucketBy, ok := valuesArray[0].(string) - if ok { - valuesArray = valuesArray[1:] - } else { - // check for nil here as custom property could be nil/missing - if valuesArray[0] == nil { - valuesArray = valuesArray[1:] - } - - if dataMap[targetingKeyKey] == nil { - return "", nil, nil - } - targetingKey, ok := dataMap[targetingKeyKey].(string) - if !ok { - return "", nil, fmt.Errorf("flag %q: bucketing value not supplied and no targetingKey in context", flagKey) - } - - if targetingKey == "" { - return "", nil, nil - } - - bucketBy = fmt.Sprintf("%s%s", properties.FlagKey, targetingKey) - } - - feDistributions, err := parseFractionalEvaluationDistributions(valuesArray, data, logger, flagKey) - if err != nil { - return "", nil, err - } - - return bucketBy, feDistributions, nil +func parseFractionalEvaluationData(values, data any, logger *logger.Logger) ([]byte, *fractionalEvaluationDistribution, error) { + valuesArray, ok := values.([]any) + if !ok { + return nil, nil, errors.New("fractional evaluation data is not an array") + } + if len(valuesArray) < 1 { + return nil, nil, errors.New("fractional evaluation data must contain at least one distribution") + } + + dataMap, ok := data.(map[string]any) + if !ok { + return nil, nil, errors.New("data isn't of type map[string]any") + } + + properties, _ := getFlagdProperties(dataMap) + flagKey := properties.FlagKey + + // If first element evaluates to null/nil, report an error and return nil. + if valuesArray[0] == nil { + return nil, nil, fmt.Errorf("flag %q: first element of fractional evaluation data is null", flagKey) + } + + // If first element is a non-array type, use it as explicit hashing input. + if _, isArray := valuesArray[0].([]any); !isArray { + hashingInput := valuesArray[0] + valuesArray = valuesArray[1:] + + bytesToHash, err := encodeDeterministicCBOR(hashingInput) + if err != nil { + return nil, nil, fmt.Errorf("flag %q: failed to encode hashing input: %w", flagKey, err) + } + + feDistributions, err := parseFractionalEvaluationDistributions(valuesArray, data, logger, flagKey) + if err != nil { + return nil, nil, err + } + + return bytesToHash, feDistributions, nil + } + + // First element is an array ([]any), meaning no explicit hashing input was provided. + // We fall back to implicit targetingKey rules. + rawTargetingKey, exists := dataMap[targetingKeyKey] + if !exists || rawTargetingKey == nil { + return nil, nil, fmt.Errorf("flag %q: bucketing value not supplied and no targetingKey in context", flagKey) + } + + targetingKey, isString := rawTargetingKey.(string) + if !isString { + return nil, nil, fmt.Errorf("flag %q: targetingKey is not a string", flagKey) + } + + if targetingKey == "" { + return nil, nil, fmt.Errorf("flag %q: targetingKey is empty", flagKey) + } + + // Build 2-element array [flagKey, targetingKey] and encode to CBOR. + implicitInput := []any{flagKey, targetingKey} + bytesToHash, err := encodeDeterministicCBOR(implicitInput) + if err != nil { + return nil, nil, fmt.Errorf("flag %q: failed to encode implicit targetingKey: %w", flagKey, err) + } + + feDistributions, err := parseFractionalEvaluationDistributions(valuesArray, data, logger, flagKey) + if err != nil { + return nil, nil, err + } + + return bytesToHash, feDistributions, nil } func parseFractionalEvaluationDistributions(values []any, data any, logger *logger.Logger, flagKey string) (*fractionalEvaluationDistribution, error) { - feDistributions := &fractionalEvaluationDistribution{ - totalWeight: 0, - weightedVariants: make([]fractionalEvaluationVariant, len(values)), - data: data, - logger: logger, - } - - // parse all weights first to validate the sum - var totalWeightInt64 int64 = 0 - - for i := 0; i < len(values); i++ { - distributionArray, ok := values[i].([]any) - if !ok { - return nil, fmt.Errorf("flag %q: distribution elements aren't of type []any. "+ - "please check your rule in flag definition", flagKey) - } - - if len(distributionArray) == 0 { - return nil, fmt.Errorf("flag %q: distribution element needs at least one element", flagKey) - } - - // JSONLogic pre-evaluates all arguments before they reach fractional. - // Pre-evaluated operators become primitive values (strings, numbers, etc.), never map[string]any nodes. - var variant any - switch v := distributionArray[0].(type) { - case string: - variant = v - case bool: - variant = v - case float64: - variant = v - case nil: - variant = nil - default: - return nil, fmt.Errorf("flag %q: first element of distribution element must be a string, bool, number, or nil", flagKey) - } - - weight := int64(1) - if len(distributionArray) >= 2 { - // parse as float64 first since that's what JSON gives us - distributionWeight, ok := distributionArray[1].(float64) - if !ok && distributionArray[1] != nil { - return nil, fmt.Errorf("flag %q: weight must be a number", flagKey) - } - if ok { - weight = int64(distributionWeight) - } - } - - // validate weight is a whole number - if len(distributionArray) >= 2 { - distributionWeight, ok := distributionArray[1].(float64) - if ok && distributionWeight != float64(int64(distributionWeight)) { - return nil, fmt.Errorf("flag %q: weights must be integers", flagKey) - } - } - - // validate individual weight doesn't exceed int32 - if weight > math.MaxInt32 { - return nil, fmt.Errorf("flag %q: weight %d exceeds maximum allowed value %d", flagKey, weight, math.MaxInt32) - } - - // clamp negative weights to 0 - if weight < 0 { - // negative weights can be the result of rollout calculations, so we log and clamp to 0 rather than returning an error - logger.Debug(fmt.Sprintf("flag %q: negative weight %d clamped to 0", flagKey, weight)) - weight = 0 - } - - totalWeightInt64 += weight - feDistributions.weightedVariants[i] = fractionalEvaluationVariant{ - variant: variant, - weight: int32(weight), - } - } - - // validate total weight doesn't exceed MaxInt32 - if totalWeightInt64 > int64(maxWeightSum) { - return nil, fmt.Errorf("flag %q: sum of all weights (%d) exceeds maximum allowed value (%d)", flagKey, totalWeightInt64, maxWeightSum) - } - - feDistributions.totalWeight = int32(totalWeightInt64) - return feDistributions, nil + feDistributions := &fractionalEvaluationDistribution{ + totalWeight: 0, + weightedVariants: make([]fractionalEvaluationVariant, len(values)), + data: data, + logger: logger, + } + + // parse all weights first to validate the sum + var totalWeightInt64 int64 = 0 + + for i := 0; i < len(values); i++ { + distributionArray, ok := values[i].([]any) + if !ok { + return nil, fmt.Errorf("flag %q: distribution elements aren't of type []any. "+ + "please check your rule in flag definition", flagKey) + } + + if len(distributionArray) == 0 { + return nil, fmt.Errorf("flag %q: distribution element needs at least one element", flagKey) + } + + // JSONLogic pre-evaluates all arguments before they reach fractional. + // Pre-evaluated operators become primitive values (strings, numbers, etc.), never map[string]any nodes. + var variant any + switch v := distributionArray[0].(type) { + case string: + variant = v + case bool: + variant = v + case float64: + variant = v + case nil: + variant = nil + default: + return nil, fmt.Errorf("flag %q: first element of distribution element must be a string, bool, number, or nil", flagKey) + } + + weight := int64(1) + if len(distributionArray) >= 2 { + // parse as float64 first since that's what JSON gives us + distributionWeight, ok := distributionArray[1].(float64) + if !ok && distributionArray[1] != nil { + return nil, fmt.Errorf("flag %q: weight must be a number", flagKey) + } + if ok { + weight = int64(distributionWeight) + } + } + + // validate weight is a whole number + if len(distributionArray) >= 2 { + distributionWeight, ok := distributionArray[1].(float64) + if ok && distributionWeight != float64(int64(distributionWeight)) { + return nil, fmt.Errorf("flag %q: weights must be integers", flagKey) + } + } + + // validate individual weight doesn't exceed int32 + if weight > math.MaxInt32 { + return nil, fmt.Errorf("flag %q: weight %d exceeds maximum allowed value %d", flagKey, weight, math.MaxInt32) + } + + // clamp negative weights to 0 + if weight < 0 { + // negative weights can be the result of rollout calculations, so we log and clamp to 0 rather than returning an error + logger.Debug(fmt.Sprintf("flag %q: negative weight %d clamped to 0", flagKey, weight)) + weight = 0 + } + + totalWeightInt64 += weight + feDistributions.weightedVariants[i] = fractionalEvaluationVariant{ + variant: variant, + weight: int32(weight), + } + } + + // validate total weight doesn't exceed MaxInt32 + if totalWeightInt64 > int64(maxWeightSum) { + return nil, fmt.Errorf("flag %q: sum of all weights (%d) exceeds maximum allowed value (%d)", flagKey, totalWeightInt64, maxWeightSum) + } + + feDistributions.totalWeight = int32(totalWeightInt64) + return feDistributions, nil } // distributeValue accepts a pre-computed 32-bit hash value and distributes it to a variant using high-precision integer arithmetic. // It maps a 32-bit hash to the range [0, totalWeight) and finds the variant bucket that contains that value. func distributeValue(hashValue uint32, feDistribution *fractionalEvaluationDistribution) any { - if feDistribution.totalWeight == 0 { - return nil - } - - bucket := (uint64(hashValue) * uint64(feDistribution.totalWeight)) >> 32 - - var rangeEnd uint64 = 0 - for _, variant := range feDistribution.weightedVariants { - rangeEnd += uint64(variant.weight) - if bucket < rangeEnd { - return variant.variant - } - } - - // unreachable given validation - return nil + if feDistribution.totalWeight == 0 { + return nil + } + + bucket := (uint64(hashValue) * uint64(feDistribution.totalWeight)) >> 32 + + var rangeEnd uint64 = 0 + for _, variant := range feDistribution.weightedVariants { + rangeEnd += uint64(variant.weight) + if bucket < rangeEnd { + return variant.variant + } + } + + // unreachable given validation + return nil } diff --git a/core/pkg/evaluator/fractional_test.go b/core/pkg/evaluator/fractional_test.go index 789a626d4..edcf4a5b0 100644 --- a/core/pkg/evaluator/fractional_test.go +++ b/core/pkg/evaluator/fractional_test.go @@ -137,8 +137,8 @@ func TestFractionalEvaluation(t *testing.T) { context: map[string]any{ emailField: rachelEmail, }, - expectedVariant: blueVariant, - expectedValue: blueHex, + expectedVariant: redVariant, + expectedValue: redHex, expectedReason: model.TargetingMatchReason, }, monicaEmail: { @@ -147,8 +147,8 @@ func TestFractionalEvaluation(t *testing.T) { context: map[string]any{ emailField: monicaEmail, }, - expectedVariant: yellowVariant, - expectedValue: yellowHex, + expectedVariant: greenVariant, + expectedValue: greenHex, expectedReason: model.TargetingMatchReason, }, joeyEmail: { @@ -167,8 +167,8 @@ func TestFractionalEvaluation(t *testing.T) { context: map[string]any{ emailField: rossEmail, }, - expectedVariant: blueVariant, - expectedValue: blueHex, + expectedVariant: greenVariant, + expectedValue: greenHex, expectedReason: model.TargetingMatchReason, }, "rachel@faas.com with custom seed": { @@ -187,8 +187,8 @@ func TestFractionalEvaluation(t *testing.T) { context: map[string]any{ "email": "monica@faas.com", }, - expectedVariant: redVariant, - expectedValue: redHex, + expectedVariant: yellowVariant, + expectedValue: yellowHex, expectedReason: model.TargetingMatchReason, }, "joey@faas.com with custom seed": { @@ -197,8 +197,8 @@ func TestFractionalEvaluation(t *testing.T) { context: map[string]any{ "email": "joey@faas.com", }, - expectedVariant: blueVariant, - expectedValue: blueHex, + expectedVariant: greenVariant, + expectedValue: greenHex, expectedReason: model.TargetingMatchReason, }, "ross@faas.com with custom seed": { @@ -207,8 +207,8 @@ func TestFractionalEvaluation(t *testing.T) { context: map[string]any{ "email": "ross@faas.com", }, - expectedVariant: greenVariant, - expectedValue: greenHex, + expectedVariant: redVariant, + expectedValue: redHex, expectedReason: model.TargetingMatchReason, }, "ross@faas.com with different flag key": { @@ -295,8 +295,8 @@ func TestFractionalEvaluation(t *testing.T) { context: map[string]any{ "email": "test4@faas.com", }, - expectedVariant: greenVariant, - expectedValue: greenHex, + expectedVariant: blueVariant, + expectedValue: blueHex, expectedReason: model.TargetingMatchReason, }, "fallback to default variant if no email provided": { @@ -414,8 +414,8 @@ func TestFractionalEvaluation(t *testing.T) { context: map[string]any{ "targetingKey": "foo@foo.com", }, - expectedVariant: greenVariant, - expectedValue: greenHex, + expectedVariant: blueVariant, + expectedValue: blueHex, expectedReason: model.TargetingMatchReason, }, "missing email - parser should ignore nil/missing custom variables and continue": { @@ -438,9 +438,9 @@ func TestFractionalEvaluation(t *testing.T) { context: map[string]any{ "targetingKey": "foo@foo.com", }, - expectedVariant: blueVariant, - expectedValue: blueHex, - expectedReason: model.TargetingMatchReason, + expectedVariant: redVariant, + expectedValue: redHex, + expectedReason: model.DefaultReason, }, "null targetingKey returns default variant": { flags: []model.Flag{{ @@ -654,8 +654,8 @@ func BenchmarkFractionalEvaluation(b *testing.B) { context: map[string]any{ emailField: testAEmail, }, - expectedVariant: blueVariant, - expectedValue: blueHex, + expectedVariant: redVariant, + expectedValue: redHex, expectedReason: model.TargetingMatchReason, }, testBEmail: { @@ -684,8 +684,8 @@ func BenchmarkFractionalEvaluation(b *testing.B) { context: map[string]any{ emailField: testDEmail, }, - expectedVariant: blueVariant, - expectedValue: blueHex, + expectedVariant: greenVariant, + expectedValue: greenHex, expectedReason: model.TargetingMatchReason, }, } diff --git a/test-harness b/test-harness index df6a84377..82ba89ec8 160000 --- a/test-harness +++ b/test-harness @@ -1 +1 @@ -Subproject commit df6a84377aa29d022659020807825cdcb191f87f +Subproject commit 82ba89ec8db498fa51368e558e4d87642d9e93c4 diff --git a/test/integration/go.mod b/test/integration/go.mod index adfa25f63..38c30b4b7 100644 --- a/test/integration/go.mod +++ b/test/integration/go.mod @@ -60,6 +60,7 @@ require ( github.com/fsnotify/fsevents v0.2.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -150,6 +151,7 @@ require ( github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/twmb/murmur3 v1.1.8 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect @@ -193,3 +195,5 @@ require ( gotest.tools/v3 v3.5.2 // indirect tags.cncf.io/container-device-interface v1.1.0 // indirect ) + +replace github.com/open-feature/flagd/core => ../../core diff --git a/test/integration/go.sum b/test/integration/go.sum index 160e21aff..af46aab88 100644 --- a/test/integration/go.sum +++ b/test/integration/go.sum @@ -149,6 +149,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -348,8 +350,6 @@ github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/open-feature/flagd-schemas v0.2.13 h1:LzoyQfirfpR8cxI4PKnoFRtpwPjpC/cOO8N0n8dpbRc= github.com/open-feature/flagd-schemas v0.2.13/go.mod h1:C0jnJ4C3j2LzGuqKgLDdTsdfKEWQp6sOHZyxu3QohFU= -github.com/open-feature/flagd/core v0.16.0 h1:HF3w8g2Jb+KDLZ0ZP+2xh1n+J7eoWudA6izyWOs+Q2Q= -github.com/open-feature/flagd/core v0.16.0/go.mod h1:1SsHbYWrpcEgmFmCpJOMQyD99r5+nhQ4122bV6EfWXw= github.com/open-feature/go-sdk v1.17.0 h1:/OUBBw5d9D61JaNZZxb2Nnr5/EJrEpjtKCTY3rspJQk= github.com/open-feature/go-sdk v1.17.0/go.mod h1:lPxPSu1UnZ4E3dCxZi5gV3et2ACi8O8P+zsTGVsDZUw= github.com/open-feature/go-sdk-contrib/providers/flagd v0.6.0 h1:IghLu7dV37cA12aPqn3zRM94RzYennrdc65jjWqFows= @@ -477,6 +477,8 @@ github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index d286293e7..15b55f8b2 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -170,7 +170,7 @@ func TestRPC(t *testing.T) { } // Run tests with RPC-specific tags - exclude connection/event issues we won't tackle - tags := "@rpc && ~@unixsocket && ~@targetURI && ~@sync && ~@metadata && ~@grace && ~@events && ~@customCert && ~@reconnect && ~@caching && ~@forbidden && ~@fractional-v1 && ~@deprecated" + tags := "@rpc && ~@unixsocket && ~@targetURI && ~@sync && ~@metadata && ~@grace && ~@events && ~@customCert && ~@reconnect && ~@caching && ~@forbidden && ~@fractional-v1 && ~@fractional-v2 && ~@deprecated" if err := runner.RunGherkinTestsWithSubtests(t, featurePaths, tags); err != nil { t.Fatalf("Gherkin tests failed: %v", err) @@ -205,7 +205,7 @@ func TestInProcess(t *testing.T) { // not to fully test the go in-process provider (that happens in go-sdk-contrib). // Many tags are excluded because they require a more complex testbed than what's built here. // TODO: remove ~@operator-errors and ~@semver-v-prefix once go-sdk-contrib picks up the fixes - tags := "@in-process && ~@unixsocket && ~@metadata && ~@contextEnrichment && ~@customCert && ~@forbidden && ~@sync-port && ~@sync-payload && ~@fractional-v1 && ~@fractional-single-entry && ~@deprecated && ~@operator-errors && ~@semver-v-prefix" + tags := "@in-process && ~@unixsocket && ~@metadata && ~@contextEnrichment && ~@customCert && ~@forbidden && ~@sync-port && ~@sync-payload && ~@fractional-v1 && ~@fractional-v2 && ~@fractional-single-entry && ~@deprecated && ~@operator-errors && ~@semver-v-prefix" if err := runner.RunGherkinTestsWithSubtests(t, featurePaths, tags); err != nil { t.Fatalf("Gherkin tests failed: %v", err)