package runtime import ( "errors" "testing" "github.com/tegwick/fluid-core/internal/contract" ) const testInterface contract.InterfaceID = "hall-publishing" // descriptor builds a minimal routable revision descriptor. func descriptor(id contract.RevisionID, state contract.RevisionState, cohorts ...contract.CohortID) contract.Revision { d := contract.Revision{ SchemaVersion: "0.1", ID: id, Interface: testInterface, State: state, Contract: contract.RevisionContract{ Type: contract.RevisionContractTypeOpenapi, Digest: contract.Digest("sha256:" + zeros(64)), }, Runtime: contract.RevisionRuntime{Upstream: "http://adapter:8080"}, Intent: contract.RevisionIntent{Version: "IEI-1"}, Policy: contract.RevisionPolicy{ Compatibility: contract.RevisionPolicyCompatibilityAdditive, SecurityCheck: contract.RevisionPolicySecurityCheckPassed, }, } if len(cohorts) > 0 { d.Routing = &contract.RevisionRouting{EligibleCohorts: cohorts} } return d } func zeros(n int) string { b := make([]byte, n) for i := range b { b[i] = '0' } return string(b) } func policyWith(defaultRev contract.RevisionID, gen int64, rules ...contract.RoutingPolicyRulesItem) contract.RoutingPolicy { return contract.RoutingPolicy{ SchemaVersion: "0.1", Interface: testInterface, Generation: gen, DefaultRevision: defaultRev, Rules: rules, } } func newFixture(t *testing.T, revs ...contract.Revision) *Registry { t.Helper() reg := NewRegistry(testInterface) for _, r := range revs { if err := reg.PutRevision(r); err != nil { t.Fatalf("PutRevision(%s): %v", r.ID, err) } } return reg } func TestResolvePrecedence(t *testing.T) { reg := newFixture(t, descriptor("R-1", contract.RevisionStateStable), descriptor("R-2", contract.RevisionStateExperiment), descriptor("R-3", contract.RevisionStateCandidate), ) if err := reg.PutPolicy(policyWith("R-1", 1)); err != nil { t.Fatal(err) } res := NewResolver(reg, true) t.Run("explicit wins", func(t *testing.T) { got, err := res.Resolve(Request{ExplicitRevision: "R-3", BoundRevision: "R-2"}) if err != nil { t.Fatal(err) } if got.Revision != "R-3" { t.Errorf("got %s, want R-3", got.Revision) } if got.Reason != contract.FluidTelemetryResolutionReasonExplicitRevision { t.Errorf("reason = %s", got.Reason) } }) t.Run("bound contract beats default", func(t *testing.T) { got, err := res.Resolve(Request{BoundRevision: "R-2"}) if err != nil { t.Fatal(err) } if got.Revision != "R-2" || got.Reason != contract.FluidTelemetryResolutionReasonBoundContract { t.Errorf("got %s via %s, want R-2 via bound_contract", got.Revision, got.Reason) } }) t.Run("falls through to stable default", func(t *testing.T) { got, err := res.Resolve(Request{}) if err != nil { t.Fatal(err) } if got.Revision != "R-1" || got.Reason != contract.FluidTelemetryResolutionReasonStableDefault { t.Errorf("got %s via %s, want R-1 via stable_default", got.Revision, got.Reason) } }) t.Run("explicit disabled is ignored", func(t *testing.T) { strict := NewResolver(reg, false) got, err := strict.Resolve(Request{ExplicitRevision: "R-3"}) if err != nil { t.Fatal(err) } if got.Revision != "R-1" { t.Errorf("got %s, want the default R-1 when pinning is disabled", got.Revision) } }) } func TestUnroutableRevisionsAreRefused(t *testing.T) { reg := newFixture(t, descriptor("R-1", contract.RevisionStateStable), descriptor("R-created", contract.RevisionStateCreated), descriptor("R-retired", contract.RevisionStateRetired), ) failed := descriptor("R-insecure", contract.RevisionStateStable) failed.Policy.SecurityCheck = contract.RevisionPolicySecurityCheckFailed if err := reg.PutRevision(failed); err != nil { t.Fatal(err) } if err := reg.PutPolicy(policyWith("R-1", 1)); err != nil { t.Fatal(err) } res := NewResolver(reg, true) for _, id := range []contract.RevisionID{"R-created", "R-retired", "R-insecure"} { if _, err := res.Resolve(Request{ExplicitRevision: id}); err == nil { t.Errorf("resolving %s should have been refused", id) } else if !errors.Is(err, ErrRevisionNotRoutable) { t.Errorf("resolving %s: got %v, want ErrRevisionNotRoutable", id, err) } } } func TestBoundRevisionDegradesToDefault(t *testing.T) { // A consumer pinned to a revision that has since retired should keep being // served rather than start failing. reg := newFixture(t, descriptor("R-1", contract.RevisionStateStable), descriptor("R-old", contract.RevisionStateRetired), ) if err := reg.PutPolicy(policyWith("R-1", 1)); err != nil { t.Fatal(err) } got, err := NewResolver(reg, false).Resolve(Request{BoundRevision: "R-old"}) if err != nil { t.Fatalf("a retired binding should degrade, not fail: %v", err) } if got.Revision != "R-1" { t.Errorf("got %s, want R-1", got.Revision) } } func TestCohortEligibility(t *testing.T) { reg := newFixture(t, descriptor("R-1", contract.RevisionStateStable), descriptor("R-2", contract.RevisionStateExperiment, "coding-agents"), ) if err := reg.PutPolicy(policyWith("R-1", 1)); err != nil { t.Fatal(err) } if err := reg.CheckRoutable("R-2", "coding-agents"); err != nil { t.Errorf("eligible cohort refused: %v", err) } if err := reg.CheckRoutable("R-2", "partner-integrations"); err == nil { t.Error("ineligible cohort accepted") } } func TestAllocationIsStickyAndProportional(t *testing.T) { reg := newFixture(t, descriptor("R-1", contract.RevisionStateStable), descriptor("R-2", contract.RevisionStateExperiment), ) exp := contract.ExperimentID("E-1") rule := contract.RoutingPolicyRulesItem{ Experiment: &exp, Allocation: map[string]contract.UnitInterval{"R-1": 0.9, "R-2": 0.1}, } if err := reg.PutPolicy(policyWith("R-1", 1, rule)); err != nil { t.Fatal(err) } res := NewResolver(reg, false) // Stickiness: the same consumer must resolve identically every time. first, err := res.Resolve(Request{ConsumerRef: "consumer-42"}) if err != nil { t.Fatal(err) } for i := 0; i < 50; i++ { again, err := res.Resolve(Request{ConsumerRef: "consumer-42"}) if err != nil { t.Fatal(err) } if again.Revision != first.Revision { t.Fatalf("assignment drifted: %s then %s", first.Revision, again.Revision) } } // Proportionality: roughly a tenth of consumers should see the candidate. const n = 4000 candidate := 0 for i := 0; i < n; i++ { got, err := res.Resolve(Request{ConsumerRef: consumerName(i)}) if err != nil { t.Fatal(err) } if got.Revision == "R-2" { candidate++ } if got.Experiment == nil || *got.Experiment != exp { t.Fatalf("experiment not recorded on resolution for consumer %d", i) } } share := float64(candidate) / n if share < 0.07 || share > 0.13 { t.Errorf("candidate share %.3f, want approximately 0.10", share) } } func consumerName(i int) string { digits := "0123456789" out := []byte("consumer-") if i == 0 { return string(append(out, '0')) } var rev []byte for i > 0 { rev = append(rev, digits[i%10]) i /= 10 } for j := len(rev) - 1; j >= 0; j-- { out = append(out, rev[j]) } return string(out) } func TestStalePolicyRefused(t *testing.T) { reg := newFixture(t, descriptor("R-1", contract.RevisionStateStable)) if err := reg.PutPolicy(policyWith("R-1", 5)); err != nil { t.Fatal(err) } if err := reg.PutPolicy(policyWith("R-1", 4)); !errors.Is(err, ErrStalePolicy) { t.Errorf("older generation accepted: %v", err) } if err := reg.PutPolicy(policyWith("R-1", 5)); !errors.Is(err, ErrStalePolicy) { t.Errorf("equal generation accepted: %v", err) } if err := reg.PutPolicy(policyWith("R-1", 6)); err != nil { t.Errorf("newer generation refused: %v", err) } } func TestWrongInterfaceRefused(t *testing.T) { reg := NewRegistry(testInterface) other := descriptor("R-1", contract.RevisionStateStable) other.Interface = "some-other-api" if err := reg.PutRevision(other); !errors.Is(err, ErrWrongInterface) { t.Errorf("foreign descriptor accepted: %v", err) } }