mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 17:36:45 +08:00
* 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]>
122 lines
3.7 KiB
Go
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,
|
|
)
|
|
}
|
|
}
|
|
}
|