Files
multica/server/pkg/featureflag/eval_context_test.go
603519d158 MUL-6977: test: remove implementation-only coverage (#7932)
* test: remove implementation-only coverage

Co-authored-by: multica-agent <[email protected]>

* test: remove remaining implementation-only coverage

Co-authored-by: multica-agent <[email protected]>

* test: restore compatibility and fragment navigation coverage

Co-authored-by: multica-agent <[email protected]>

* test: preserve behavioral cleanup contracts

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-03 13:23:25 +08:00

122 lines
3.7 KiB
Go

package featureflag
import (
"context"
"testing"
)
func TestEvalContextLookup(t *testing.T) {
t.Parallel()
ec := EvalContext{
UserID: "u-1",
WorkspaceID: "w-2",
Attributes: map[string]string{"plan": "pro", "country": ""},
}
tests := []struct {
name string
key string
value string
found bool
}{
{"user_id", "user_id", "u-1", true},
{"workspace_id", "workspace_id", "w-2", true},
{"plan", "plan", "pro", true},
{"empty attribute treated as missing", "country", "", false},
{"unknown attribute", "unknown", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v, ok := ec.Lookup(tt.key)
if v != tt.value || ok != tt.found {
t.Fatalf("Lookup(%q) = (%q, %v), want (%q, %v)", tt.key, v, ok, tt.value, tt.found)
}
})
}
}
func TestEvalContextRoundTripThroughContext(t *testing.T) {
t.Parallel()
ec := EvalContext{UserID: "u-1"}
ctx := WithEvalContext(context.Background(), ec)
got := EvalContextFrom(ctx)
if got.UserID != "u-1" {
t.Fatalf("EvalContext did not round-trip, got %+v", got)
}
}
func TestEvalContextFromUnattachedContext(t *testing.T) {
t.Parallel()
// An unattached context must return the zero value, not panic.
got := EvalContextFrom(context.Background())
if got.UserID != "" || got.WorkspaceID != "" || got.Attributes != nil {
t.Fatalf("unattached context should yield zero EvalContext, got %+v", got)
}
}
func TestEvalContextFromNilContext(t *testing.T) {
t.Parallel()
//nolint:staticcheck // deliberately exercise the nil-ctx defensive path.
got := EvalContextFrom(nil)
if got.UserID != "" {
t.Fatalf("nil context must yield zero EvalContext, got %+v", got)
}
}
func TestPercentBucketSeparator(t *testing.T) {
t.Parallel()
// Without a separator, ("ab", "c") and ("a", "bc") would collide.
// The separator must keep them distinct, otherwise two unrelated
// flags could share buckets and skew an experiment.
left := bucketFor("ab", "c")
right := bucketFor("a", "bc")
if left == right {
// Not guaranteed unequal in general, but for these inputs the
// FNV-1a + zero separator should produce different buckets.
// If this ever does collide we should switch separators, not
// hide the regression.
t.Fatalf("hash separator failed: bucketFor('ab','c') == bucketFor('a','bc') == %d", left)
}
}
// TestPercentBucketCrossLanguageGolden pins concrete (key, identifier) ->
// bucket values that the Go side MUST agree on with the TS side. The same
// values are duplicated in packages/core/feature-flags/hash.test.ts; if
// either side drifts, both tests fail and one must be brought back in
// sync. This is the single source of truth for "same user, same bucket"
// across the backend and the frontend.
//
// The non-ASCII cases (CJK, accented, emoji) exist on purpose: Go hashes
// the UTF-8 byte representation of a string, and the TS side must do the
// same. A regression that swaps charCodeAt for UTF-8 decoding on either
// side would only be caught by these inputs.
func TestPercentBucketCrossLanguageGolden(t *testing.T) {
t.Parallel()
cases := []struct {
key, id string
want int
}{
// ASCII baseline.
{"billing_new_invoice", "user-42", 97},
{"feature_a", "user-1", 50},
{"checkout_algo", "u-7f8a", 11},
{"ws_rollout", "workspace-1", 62},
{"empty_id_flag", "", 83},
// Non-ASCII: enforces UTF-8 parity with TextEncoder on the TS side.
{"flag", "é", 53},
{"flag", "🦄", 82},
{"实验", "user-1", 90},
{"flag", "用户-1", 95},
{"checkout_算法", "user-100", 79},
}
for _, tc := range cases {
got := bucketFor(tc.key, tc.id)
if got != tc.want {
t.Fatalf(
"cross-language golden mismatch: bucketFor(%q, %q) = %d, want %d. "+
"If you changed the hash you MUST also update hash.test.ts.",
tc.key, tc.id, got, tc.want,
)
}
}
}