package policy import ( "strings" "testing" "github.com/tegwick/fluid-core/internal/contract" "github.com/tegwick/fluid-core/internal/intent" ) func passing() contract.Revision { pc := contract.RevisionPolicyPolicyCheckPassed return contract.Revision{ ID: "R-2", Interface: "hall-publishing", State: contract.RevisionStateCandidate, Policy: contract.RevisionPolicy{ Compatibility: contract.RevisionPolicyCompatibilityAdditive, SecurityCheck: contract.RevisionPolicySecurityCheckPassed, PolicyCheck: &pc, }, } } func human() *contract.Actor { return &contract.Actor{Type: contract.ActorTypeHuman, ID: "worsch"} } func baseInput() Input { return Input{ Descriptor: passing(), GoverningMode: intent.ModeAdvisory, AdaptationClasses: []contract.AdaptationClass{contract.AdaptationClassPresentation}, ComplexityDelta: 0.2, RequestedTrafficShare: 0.10, Approved: true, ApprovedBy: human(), } } func TestGateAllowsAConformingCandidate(t *testing.T) { d := NewGate(DefaultLimits()).Evaluate(baseInput()) if !d.Allowed { t.Fatalf("conforming candidate rejected: %v", d.Reasons) } } func TestGateRefusesUnverifiedSecurity(t *testing.T) { in := baseInput() in.Descriptor.Policy.SecurityCheck = contract.RevisionPolicySecurityCheckPending d := NewGate(DefaultLimits()).Evaluate(in) if d.Allowed { t.Fatal("a candidate with a pending security check was allowed") } if !mentions(d.Reasons, "security check") { t.Errorf("reasons do not name the security check: %v", d.Reasons) } } func TestGateRefusesBreakingChange(t *testing.T) { in := baseInput() in.Descriptor.Policy.Compatibility = contract.RevisionPolicyCompatibilityBreaking if d := NewGate(DefaultLimits()).Evaluate(in); d.Allowed { t.Fatal("a breaking change passed the default gate") } } func TestGateEnforcesAuthorityMode(t *testing.T) { limits := DefaultLimits() limits.RequiredMode = intent.ModeExperimental in := baseInput() in.GoverningMode = intent.ModeAdvisory d := NewGate(limits).Evaluate(in) if d.Allowed { t.Fatal("promotion requiring FLUID-4 was allowed under a FLUID-2 intent") } if !mentions(d.Reasons, "FLUID-2") { t.Errorf("reasons do not name the governing mode: %v", d.Reasons) } in.GoverningMode = intent.ModeEvolutionary if d := NewGate(limits).Evaluate(in); !d.Allowed { t.Errorf("FLUID-6 should satisfy a FLUID-4 requirement: %v", d.Reasons) } } func TestGateEnforcesComplexityBudget(t *testing.T) { // FluidAPIStandards.md section 23: a candidate may be rejected even when it // increases local utility. in := baseInput() in.ComplexityDelta = 9.0 d := NewGate(DefaultLimits()).Evaluate(in) if d.Allowed { t.Fatal("a candidate far over the complexity budget was allowed") } if !mentions(d.Reasons, "complexity") { t.Errorf("reasons do not name complexity: %v", d.Reasons) } } func TestGateTakesTheTighterTrafficCeiling(t *testing.T) { limits := DefaultLimits() limits.MaxTrafficShare = 0.50 in := baseInput() share := contract.UnitInterval(0.10) in.Descriptor.Routing = &contract.RevisionRouting{MaxTrafficShare: &share} in.RequestedTrafficShare = 0.30 // The descriptor restricts itself below the gate's ceiling; the tighter of // the two must win, or a descriptor's self-restriction would be advisory. if d := NewGate(limits).Evaluate(in); d.Allowed { t.Fatal("requested share exceeded the descriptor's own ceiling but was allowed") } // A descriptor must not be able to widen past the gate. wide := contract.UnitInterval(0.99) in.Descriptor.Routing = &contract.RevisionRouting{MaxTrafficShare: &wide} in.RequestedTrafficShare = 0.80 if d := NewGate(limits).Evaluate(in); d.Allowed { t.Fatal("a descriptor widened its own exposure past the gate ceiling") } } func TestGateRequiresApproval(t *testing.T) { in := baseInput() in.Approved = false in.ApprovedBy = nil if d := NewGate(DefaultLimits()).Evaluate(in); d.Allowed { t.Fatal("an unapproved candidate passed a gate that requires approval") } } // TestDaimonCannotSelfApproveBelowBoundedAutonomy defends Blueprint 28.1: // generation authority is not promotion authority. A candidate a Daimon both // produced and approved has had no independent check. func TestDaimonCannotSelfApproveBelowBoundedAutonomy(t *testing.T) { in := baseInput() in.ApprovedBy = &contract.Actor{Type: contract.ActorTypeDaimon, ID: "fluid-daimon/hall"} d := NewGate(DefaultLimits()).Evaluate(in) if d.Allowed { t.Fatal("a daimon authorized its own promotion at FLUID-2") } if !mentions(d.Reasons, "daimon") { t.Errorf("reasons do not name the daimon authorization: %v", d.Reasons) } // At bounded autonomy, with the policy demanding it, this is legitimate. limits := DefaultLimits() limits.RequiredMode = intent.ModeBoundedAutonomous in.GoverningMode = intent.ModeBoundedAutonomous if d := NewGate(limits).Evaluate(in); !d.Allowed { t.Errorf("daimon authorization refused at FLUID-5: %v", d.Reasons) } } func TestGateReportsEveryFailure(t *testing.T) { // A caller fixing one rejection only to hit the next learns less than one // told everything at once, and the audit trail wants the whole list. in := baseInput() in.Descriptor.Policy.SecurityCheck = contract.RevisionPolicySecurityCheckFailed in.Descriptor.Policy.Compatibility = contract.RevisionPolicyCompatibilityBreaking in.ComplexityDelta = 50 in.Approved = false in.ApprovedBy = nil in.AdaptationClasses = []contract.AdaptationClass{contract.AdaptationClassContract} d := NewGate(DefaultLimits()).Evaluate(in) if d.Allowed { t.Fatal("a candidate failing five gates was allowed") } if len(d.Reasons) < 5 { t.Errorf("expected at least five reasons, got %d: %v", len(d.Reasons), d.Reasons) } } func TestGateIsDeterministic(t *testing.T) { // The gate must be a pure function: same input, same verdict, same reasons // in the same order. A gate whose output varies cannot be audited. in := baseInput() in.Descriptor.Policy.SecurityCheck = contract.RevisionPolicySecurityCheckFailed in.AdaptationClasses = []contract.AdaptationClass{contract.AdaptationClassContract} g := NewGate(DefaultLimits()) first := g.Evaluate(in) for i := 0; i < 100; i++ { again := g.Evaluate(in) if again.Allowed != first.Allowed || len(again.Reasons) != len(first.Reasons) { t.Fatalf("verdict varied between runs") } for j := range first.Reasons { if again.Reasons[j] != first.Reasons[j] { t.Fatalf("reason order varied: %q vs %q", first.Reasons[j], again.Reasons[j]) } } } } func mentions(reasons []string, substr string) bool { for _, r := range reasons { if strings.Contains(strings.ToLower(r), strings.ToLower(substr)) { return true } } return false }