iop/packages/go/streamgate/evaluation_contract_test.go
toki 69d2759155 archive completed tasks and apply Go streamgate improvements
- Archive evidence-gate-core tasks (09, 10+09, 11+09,10) to archive/2026/07
- Improve streamgate evidence_tail with contract validation and filter registry enhancements
- Add consumer_contract_test.go for Go streamgate
- Update dispatch.py and test_dispatch.py
2026-07-26 14:22:15 +09:00

1329 lines
46 KiB
Go

package streamgate
import (
"context"
"sort"
"strings"
"testing"
"time"
)
// stubEvaluator implements FilterEvaluator for tests. It exposes the id and
// a return decision/error so test cases can drive readiness normalisation and
// outcome composition without touching the guardrail.
type stubEvaluator struct {
id string
decision FilterDecision
err error
evaluateInvoked bool
}
func (s *stubEvaluator) ID() string { return s.id }
func (s *stubEvaluator) Evaluate(_ context.Context, _ EvidenceBatch) (FilterDecision, error) {
s.evaluateInvoked = true
return s.decision, s.err
}
// newPassDecision returns a passing FilterDecision for use in readiness and
// outcome composition tests.
func newPassDecision(t *testing.T, filterID string) FilterDecision {
t.Helper()
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
evidence := buildValidEvidence(t, ts)
d, err := NewFilterDecision(FilterDecisionKindPass, "consumer.id", filterID, "rule.id", evidence, nil)
if err != nil {
t.Fatalf("newPassDecision: %v", err)
}
return d
}
// newViolationDecision returns a violating FilterDecision for enforcement
// composition tests.
func newViolationDecision(t *testing.T, filterID string) FilterDecision {
t.Helper()
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
evidence := buildValidEvidence(t, ts)
d, err := NewFilterDecision(FilterDecisionKindViolation, "consumer.id", filterID, "rule.id", evidence, nil)
if err != nil {
t.Fatalf("newViolationDecision: %v", err)
}
return d
}
// newFatalDecision returns a fatal FilterDecision for enforcement composition tests.
func newFatalDecision(t *testing.T, filterID string) FilterDecision {
t.Helper()
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
evidence := buildValidEvidence(t, ts)
d, err := NewFilterDecision(FilterDecisionKindFatal, "consumer.id", filterID, "rule.id", evidence, nil)
if err != nil {
t.Fatalf("newFatalDecision: %v", err)
}
return d
}
// mustEvalError creates a FilterOutcome of kind evaluation_error with a
// stable error code, failing the test on any error.
func mustEvalError(t *testing.T, code string) FilterOutcome {
t.Helper()
o, err := NewFilterOutcomeEvaluationError(code)
if err != nil {
t.Fatalf("NewFilterOutcomeEvaluationError: %v", err)
}
return o
}
// --- [API-1] Resolved epoch filter binding ---
// TestEpochFilterNormalizesApplicabilityAndRequirement verifies the readiness
// normalisation table across all four states. Each matrix entry is driven by
// a distinct stub evaluator so accidental coupling between tests is avoided.
func TestEpochFilterNormalizesApplicabilityAndRequirement(t *testing.T) {
passDecision := newPassDecision(t, "filter.id")
cases := []struct {
name string
subscribed bool
triggerReady bool
blocksRelease bool
enforcement FilterEnforcement
wantForEpoch bool
wantNotApplicable bool
wantDeferred bool
}{
{
name: "subscribed_trigger_ready",
subscribed: true,
triggerReady: true,
blocksRelease: false,
enforcement: FilterEnforcementBlocking,
wantForEpoch: true,
wantNotApplicable: false,
wantDeferred: false,
},
{
name: "subscribed_trigger_not_ready_blocks",
subscribed: true,
triggerReady: false,
blocksRelease: true,
enforcement: FilterEnforcementBlocking,
wantForEpoch: false,
wantNotApplicable: false,
wantDeferred: true,
},
{
name: "subscribed_trigger_not_ready_nonblock",
subscribed: true,
triggerReady: false,
blocksRelease: false,
enforcement: FilterEnforcementObserveOnly,
wantForEpoch: false,
wantNotApplicable: true,
wantDeferred: false,
},
{
name: "unsubscribed",
subscribed: false,
triggerReady: false,
blocksRelease: false,
enforcement: FilterEnforcementBlocking,
wantForEpoch: false,
wantNotApplicable: true,
wantDeferred: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
e := &stubEvaluator{id: "filter.id", decision: passDecision}
f, err := NewEpochFilter(
e, 100,
tc.subscribed, tc.triggerReady, tc.blocksRelease,
tc.enforcement,
100*time.Millisecond,
)
if err != nil {
t.Fatalf("NewEpochFilter: %v", err)
}
if f.EvaluatedForEpoch() != tc.wantForEpoch {
t.Errorf("EvaluatedForEpoch() = %v, want %v", f.EvaluatedForEpoch(), tc.wantForEpoch)
}
oc := f.NormalizeOutcome()
if tc.wantNotApplicable && oc.Kind() != FilterOutcomeKindNotApplicableForEpoch {
t.Errorf("NormalizeOutcome() Kind() = %v, want not_applicable_for_epoch", oc.Kind())
}
if tc.wantDeferred && oc.Kind() != FilterOutcomeKindDeferredByRequirement {
t.Errorf("NormalizeOutcome() Kind() = %v, want deferred_by_requirement", oc.Kind())
}
// Accessors must reflect construction inputs exactly.
if f.SubscribedEventPresent() != tc.subscribed {
t.Errorf("SubscribedEventPresent() = %v, want %v", f.SubscribedEventPresent(), tc.subscribed)
}
if f.TriggerReady() != tc.triggerReady {
t.Errorf("TriggerReady() = %v, want %v", f.TriggerReady(), tc.triggerReady)
}
if f.BlocksRelease() != tc.blocksRelease {
t.Errorf("BlocksRelease() = %v, want %v", f.BlocksRelease(), tc.blocksRelease)
}
if f.EnforcedAs() != tc.enforcement {
t.Errorf("EnforcedAs() = %v, want %v", f.EnforcedAs(), tc.enforcement)
}
})
}
}
// TestEpochFilterRejectsInvalidExecutionContract verifies that NewEpochFilter
// rejects every invalid constructor combination: nil evaluator, empty id,
// invalid id grammar, unknown enforcement, and non-positive timeout.
func TestEpochFilterRejectsInvalidExecutionContract(t *testing.T) {
passDecision := newPassDecision(t, "filter.id")
// --- nil evaluator ---
t.Run("nil_evaluator", func(t *testing.T) {
_, err := NewEpochFilter(
nil, 100,
true, true, false,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err == nil {
t.Error("NewEpochFilter(nil, ...) should return error")
}
})
// --- negative priority ---
t.Run("negative_priority", func(t *testing.T) {
_, err := NewEpochFilter(
&stubEvaluator{id: "filter.id", decision: passDecision}, -1,
true, true, false,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err == nil {
t.Error("NewEpochFilter(negative priority, ...) should return error")
}
})
// --- empty evaluator id ---
t.Run("empty_evaluator_id", func(t *testing.T) {
_, err := NewEpochFilter(
&stubEvaluator{id: "", decision: passDecision}, 100,
true, true, false,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err == nil {
t.Error("NewEpochFilter(empty id, ...) should return error")
}
})
// --- invalid evaluator id grammar ---
t.Run("invalid_evaluator_id_grammar", func(t *testing.T) {
_, err := NewEpochFilter(
&stubEvaluator{id: "INVALID-ID!", decision: passDecision}, 100,
true, true, false,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err == nil {
t.Error("NewEpochFilter(invalid id grammar, ...) should return error")
}
})
// --- unknown enforcement ---
t.Run("unknown_enforcement", func(t *testing.T) {
_, err := NewEpochFilter(
&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
true, true, false,
"unknown",
100*time.Millisecond,
)
if err == nil {
t.Error("NewEpochFilter(unknown enforcement, ...) should return error")
}
})
// --- zero timeout ---
t.Run("zero_timeout", func(t *testing.T) {
_, err := NewEpochFilter(
&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
true, true, false,
FilterEnforcementBlocking,
0,
)
if err == nil {
t.Error("NewEpochFilter(zero timeout, ...) should return error")
}
})
// --- negative timeout ---
t.Run("negative_timeout", func(t *testing.T) {
_, err := NewEpochFilter(
&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
true, true, false,
FilterEnforcementBlocking,
-1*time.Millisecond,
)
if err == nil {
t.Error("NewEpochFilter(negative timeout, ...) should return error")
}
})
}
// TestEpochFilterTimeoutAndAccessorImmutability verifies that the timeout is
// stored exactly and that accessor reads do not leak mutable state.
func TestEpochFilterTimeoutAndAccessorImmutability(t *testing.T) {
passDecision := newPassDecision(t, "filter.id")
timeout := 42 * time.Millisecond
f, err := NewEpochFilter(
&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
true, true, false,
FilterEnforcementObserveOnly,
timeout,
)
if err != nil {
t.Fatalf("NewEpochFilter: %v", err)
}
if f.EvaluationTimeout() != timeout {
t.Errorf("EvaluationTimeout() = %v, want %v", f.EvaluationTimeout(), timeout)
}
// The evaluator is returned by reference; verify that it is the same
// instance but that the accessors do not expose internal mutation paths.
e := f.Evaluator()
if e.ID() != "filter.id" {
t.Errorf("Evaluator().ID() = %q, want %q", e.ID(), "filter.id")
}
}
// --- [API-2] Immutable evaluation outcome set ---
// TestEvaluationSetIsStableAndImmutable verifies that the set sorts by filter
// id regardless of input order, rejects duplicates, and returns defensive
// copies on every accessor path.
func TestEvaluationSetIsStableAndImmutable(t *testing.T) {
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
evidence := buildValidEvidence(t, ts)
// Build outcomes in reverse id order to exercise sort stability.
d1, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "beta.id", "rule.id", evidence, nil)
d2, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "alpha.id", "rule.id", evidence, nil)
d3, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "gamma.id", "rule.id", evidence, nil)
ocBeta, _ := NewFilterOutcomeEvaluated(d1)
ocAlpha, _ := NewFilterOutcomeEvaluated(d2)
ocGamma, _ := NewFilterOutcomeEvaluated(d3)
betaO, _ := NewEpochFilterOutcome("beta.id", 100, ocBeta, FilterEnforcementBlocking)
alphaO, _ := NewEpochFilterOutcome("alpha.id", 100, ocAlpha, FilterEnforcementBlocking)
gammaO, _ := NewEpochFilterOutcome("gamma.id", 100, ocGamma, FilterEnforcementBlocking)
// Input in reverse id order: gamma, beta, alpha.
inputs := []EpochFilterOutcome{
gammaO,
betaO,
alphaO,
}
set, err := NewEvaluationSet(inputs)
if err != nil {
t.Fatalf("NewEvaluationSet: %v", err)
}
// Stable order: alpha < beta < gamma.
wantOrder := []string{"alpha.id", "beta.id", "gamma.id"}
for i, w := range wantOrder {
o, err := set.At(i)
if err != nil {
t.Fatalf("At(%d): %v", i, err)
}
if o.FilterID() != w {
t.Errorf("At(%d).FilterID() = %q, want %q", i, o.FilterID(), w)
}
}
// --- ByID lookup succeeds for sorted ids ---
if _, err := set.ByID("alpha.id"); err != nil {
t.Errorf("ByID('alpha.id'): %v", err)
}
if _, err := set.ByID("nonexistent"); err == nil {
t.Error("ByID('nonexistent') should return error")
}
// --- All() returns defensive copy in stable order ---
all := set.All()
if len(all) != 3 {
t.Fatalf("All() len = %d, want 3", len(all))
}
if all[0].FilterID() != "alpha.id" {
t.Errorf("All()[0].FilterID() = %q, want %q", all[0].FilterID(), "alpha.id")
}
// --- Mutating All() does not affect stored ---
all[0] = EpochFilterOutcome{}
if set.Len() != 3 {
t.Errorf("Len() after All() mutation = %d, want 3", set.Len())
}
// --- Duplicate filter id is rejected ---
dupInputs := []EpochFilterOutcome{
alphaO,
alphaO,
}
if _, err := NewEvaluationSet(dupInputs); err == nil {
t.Error("NewEvaluationSet(duplicate id) should return error")
}
}
// TestEpochFilterOutcomeValidatesFailurePolicy verifies that the failure
// disposition is derived correctly from the outcome kind, enforcement mode,
// and decision kind. It also verifies that invalid combinations are rejected.
func TestEpochFilterOutcomeValidatesFailurePolicy(t *testing.T) {
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
_ = ts // reserved for future expansion
cases := []struct {
name string
filterID string
enforcement FilterEnforcement
wantKind FilterOutcomeKind
desiredDisp EvaluationFailureDisposition
}{
{
name: "pass_blocking",
filterID: "f1",
enforcement: FilterEnforcementBlocking,
wantKind: FilterOutcomeKindEvaluated,
desiredDisp: EvaluationFailureDispositionNone,
},
{
name: "pass_observe",
filterID: "f2",
enforcement: FilterEnforcementObserveOnly,
wantKind: FilterOutcomeKindEvaluated,
desiredDisp: EvaluationFailureDispositionNone,
},
{
name: "violation_blocking",
filterID: "f3",
enforcement: FilterEnforcementBlocking,
wantKind: FilterOutcomeKindEvaluated,
desiredDisp: EvaluationFailureDispositionBlockingFatal,
},
{
name: "violation_observe",
filterID: "f4",
enforcement: FilterEnforcementObserveOnly,
wantKind: FilterOutcomeKindEvaluated,
desiredDisp: EvaluationFailureDispositionObserveError,
},
{
name: "fatal_blocking",
filterID: "f5",
enforcement: FilterEnforcementBlocking,
wantKind: FilterOutcomeKindEvaluated,
desiredDisp: EvaluationFailureDispositionBlockingFatal,
},
{
name: "fatal_observe",
filterID: "f6",
enforcement: FilterEnforcementObserveOnly,
wantKind: FilterOutcomeKindEvaluated,
desiredDisp: EvaluationFailureDispositionObserveError,
},
{
name: "not_applicable_blocking",
filterID: "f9",
enforcement: FilterEnforcementBlocking,
wantKind: FilterOutcomeKindNotApplicableForEpoch,
desiredDisp: EvaluationFailureDispositionNone,
},
{
name: "deferred",
filterID: "f10",
enforcement: FilterEnforcementObserveOnly,
wantKind: FilterOutcomeKindDeferredByRequirement,
desiredDisp: EvaluationFailureDispositionNone,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var oc FilterOutcome
switch tc.name {
case "violation_blocking", "violation_observe":
oc = mustEvalOutcome(t, newViolationDecision(t, tc.filterID))
case "fatal_blocking", "fatal_observe":
oc = mustEvalOutcome(t, newFatalDecision(t, tc.filterID))
case "not_applicable_blocking":
oc = NewFilterOutcomeNotApplicableForEpoch()
case "deferred":
oc = NewFilterOutcomeDeferredByRequirement()
default:
oc = mustEvalOutcome(t, newPassDecision(t, tc.filterID))
}
o, err := NewEpochFilterOutcome(tc.filterID, 100, oc, tc.enforcement)
if err != nil {
t.Fatalf("NewEpochFilterOutcome: %v", err)
}
if o.FailureDisposition() != tc.desiredDisp {
t.Errorf("FailureDisposition() = %v, want %v", o.FailureDisposition(), tc.desiredDisp)
}
if o.Outcome().Kind() != tc.wantKind {
t.Errorf("Outcome().Kind() = %v, want %v", o.Outcome().Kind(), tc.wantKind)
}
if o.FilterID() != tc.filterID {
t.Errorf("FilterID() = %q, want %q", o.FilterID(), tc.filterID)
}
if o.Enforcement() != tc.enforcement {
t.Errorf("Enforcement() = %v, want %v", o.Enforcement(), tc.enforcement)
}
})
}
// --- evaluation_error with blocking/observe_only ---
t.Run("eval_error_blocking", func(t *testing.T) {
eoc := mustEvalError(t, "err.code")
o, err := NewEpochFilterOutcome("f7", 100, eoc, FilterEnforcementBlocking)
if err != nil {
t.Fatalf("NewEpochFilterOutcome: %v", err)
}
if o.Outcome().Kind() != FilterOutcomeKindEvaluationError {
t.Errorf("Outcome().Kind() = %v, want evaluation_error", o.Outcome().Kind())
}
if o.FailureDisposition() != EvaluationFailureDispositionBlockingFatal {
t.Errorf("FailureDisposition() = %v, want %v", o.FailureDisposition(), EvaluationFailureDispositionBlockingFatal)
}
})
t.Run("eval_error_observe", func(t *testing.T) {
eoc := mustEvalError(t, "err.code")
o, err := NewEpochFilterOutcome("f8", 100, eoc, FilterEnforcementObserveOnly)
if err != nil {
t.Fatalf("NewEpochFilterOutcome: %v", err)
}
if o.Outcome().Kind() != FilterOutcomeKindEvaluationError {
t.Errorf("Outcome().Kind() = %v, want evaluation_error", o.Outcome().Kind())
}
if o.FailureDisposition() != EvaluationFailureDispositionObserveError {
t.Errorf("FailureDisposition() = %v, want %v", o.FailureDisposition(), EvaluationFailureDispositionObserveError)
}
})
// --- Invalid combinations are rejected ---
t.Run("invalid_empty_filter_id", func(t *testing.T) {
_, err := NewEpochFilterOutcome("", 100, NewFilterOutcomeNotApplicableForEpoch(), FilterEnforcementBlocking)
if err == nil {
t.Error("NewEpochFilterOutcome(empty id) should return error")
}
})
t.Run("invalid_bad_id_grammar", func(t *testing.T) {
_, err := NewEpochFilterOutcome("BAD!ID", 100, NewFilterOutcomeNotApplicableForEpoch(), FilterEnforcementBlocking)
if err == nil {
t.Error("NewEpochFilterOutcome(bad id grammar) should return error")
}
})
t.Run("invalid_unknown_enforcement", func(t *testing.T) {
_, err := NewEpochFilterOutcome("f.id", 100, NewFilterOutcomeNotApplicableForEpoch(), "unknown")
if err == nil {
t.Error("NewEpochFilterOutcome(unknown enforcement) should return error")
}
})
t.Run("invalid_negative_priority", func(t *testing.T) {
_, err := NewEpochFilterOutcome("f.id", -1, NewFilterOutcomeNotApplicableForEpoch(), FilterEnforcementBlocking)
if err == nil {
t.Error("NewEpochFilterOutcome(negative priority) should return error")
}
})
}
// TestEvaluationSetRejectsInvalidEntries verifies that invalid EpochFilterOutcome
// entries (empty filter id, bad grammar, invalid outcome) are rejected at
// set construction.
func TestEvaluationSetRejectsInvalidEntries(t *testing.T) {
// Empty filter id is rejected at NewEpochFilterOutcome level.
goodOutcome := NewFilterOutcomeNotApplicableForEpoch()
badO, err := NewEpochFilterOutcome("", 100, goodOutcome, FilterEnforcementBlocking)
if err == nil {
t.Error("NewEpochFilterOutcome(empty id) should return error")
}
if badO.FilterID() != "" {
t.Errorf("badO.FilterID() = %q, want empty", badO.FilterID())
}
}
// TestEvaluationSetAccessorReturnsDefensiveCopy verifies that mutating the
// return value of All() does not affect the set's stored outcomes.
func TestEvaluationSetAccessorReturnsDefensiveCopy(t *testing.T) {
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
evidence := buildValidEvidence(t, ts)
d, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "filter.id", "rule.id", evidence, nil)
oc, _ := NewFilterOutcomeEvaluated(d)
o, _ := NewEpochFilterOutcome("filter.id", 100, oc, FilterEnforcementBlocking)
set, err := NewEvaluationSet([]EpochFilterOutcome{o})
if err != nil {
t.Fatalf("NewEvaluationSet: %v", err)
}
// --- All() mutation ---
all := set.All()
all[0] = EpochFilterOutcome{}
if set.Len() != 1 {
t.Error("All() replacement leaked to set length")
}
// --- At() element ---
atO, err := set.At(0)
if err != nil {
t.Fatalf("At(0): %v", err)
}
if atO.FilterID() != "filter.id" {
t.Errorf("At(0).FilterID() = %q, want %q", atO.FilterID(), "filter.id")
}
// --- ByID() lookup ---
byID, err := set.ByID("filter.id")
if err != nil {
t.Fatalf("ByID: %v", err)
}
if byID.FilterID() != "filter.id" {
t.Errorf("ByID.FilterID() = %q, want %q", byID.FilterID(), "filter.id")
}
}
// TestEvaluationSetStableOrderAcrossInputOrders verifies that regardless of
// the order the inputs are provided, the resulting EvaluationSet always
// orders outcomes by ascending stable-token id. This ensures Arbiter inputs
// do not depend on completion order.
func TestEvaluationSetStableOrderAcrossInputOrders(t *testing.T) {
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
evidence := buildValidEvidence(t, ts)
makeOutcome := func(id string) EpochFilterOutcome {
d, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", id, "rule.id", evidence, nil)
oc, _ := NewFilterOutcomeEvaluated(d)
o, _ := NewEpochFilterOutcome(id, 100, oc, FilterEnforcementBlocking)
return o
}
// Generate a non-sorted order of ids.
ids := []string{"z.id", "a.id", "m.id", "c.id", "y.id", "b.id"}
// Sort them to know the expected order.
sort.Strings(ids)
// Build inputs in random order.
inputs := make([]EpochFilterOutcome, len(ids))
for i, id := range ids {
inputs[i] = makeOutcome(id)
}
set, err := NewEvaluationSet(inputs)
if err != nil {
t.Fatalf("NewEvaluationSet: %v", err)
}
for i, wantID := range ids {
o, err := set.At(i)
if err != nil {
t.Fatalf("At(%d): %v", i, err)
}
if o.FilterID() != wantID {
t.Errorf("At(%d).FilterID() = %q, want %q", i, o.FilterID(), wantID)
}
}
}
// TestEpochFilterOutcomeNoRawErrorPreserved verifies that the outcome stored
// in EpochFilterOutcome does not contain raw error messages; only the
// sanitized stable error code is retained.
func TestEpochFilterOutcomeNoRawErrorPreserved(t *testing.T) {
// Build an evaluation_error outcome with a stable error code.
oc, _ := NewFilterOutcomeEvaluationError("runtime.guardrail.timeout")
o, err := NewEpochFilterOutcome("f.id", 100, oc, FilterEnforcementBlocking)
if err != nil {
t.Fatalf("NewEpochFilterOutcome: %v", err)
}
if o.Outcome().ErrorCode() != "runtime.guardrail.timeout" {
t.Errorf("ErrorCode() = %q, want %q", o.Outcome().ErrorCode(), "runtime.guardrail.timeout")
}
if o.Outcome().Decision() != nil {
t.Error("Evaluation error outcome must not carry a decision")
}
}
// TestEpochFilterNormalizeOutcomeForReadyFilter verifies that a ready filter
// (subscribed + triggerReady) produces an empty FilterOutcome from
// NormalizeOutcome. The coordinator is responsible for wrapping the actual
// evaluation result.
func TestEpochFilterNormalizeOutcomeForReadyFilter(t *testing.T) {
passDecision := newPassDecision(t, "filter.id")
f, err := NewEpochFilter(
&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
true, true, false,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err != nil {
t.Fatalf("NewEpochFilter: %v", err)
}
oc := f.NormalizeOutcome()
// Ready filter has no pre-evaluation outcome; the kind is the zero value.
if oc.Kind() != "" {
t.Errorf("NormalizeOutcome().Kind() = %q, want empty (ready filter has no pre-evaluation outcome)", oc.Kind())
}
}
// TestEvaluationSetEmptyIsValid verifies that an empty EvaluationSet is valid
// and that its accessors behave safely.
func TestEvaluationSetEmptyIsValid(t *testing.T) {
set, err := NewEvaluationSet(nil)
if err != nil {
t.Fatalf("NewEvaluationSet(nil): %v", err)
}
if set.Len() != 0 {
t.Errorf("Len() = %d, want 0", set.Len())
}
if set.All() != nil {
t.Error("All() of empty set should return nil")
}
if _, err := set.At(0); err == nil {
t.Error("At(0) on empty set should return error")
}
if _, err := set.ByID("any"); err == nil {
t.Error("ByID on empty set should return error")
}
}
// TestEpochFilterNoPartialConstructionLeak verifies that the filter
// contract is also invariant under concurrent access — the constructor does
// not produce a filter that can be observed in a partially constructed state.
// This is enforced by the immutable design of the returned value.
func TestEpochFilterNoPartialConstructionLeak(t *testing.T) {
// The constructor validates all fields before returning. Since all
// fields are stored in a struct value (not pointers to shared state),
// a partially constructed EpochFilter can never be observed. This test
// simply verifies that a fully valid filter has stable accessors.
passDecision := newPassDecision(t, "filter.id")
f, err := NewEpochFilter(
&stubEvaluator{id: "filter.id", decision: passDecision}, 100,
true, true, false,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err != nil {
t.Fatalf("NewEpochFilter: %v", err)
}
// All accessors should return consistent values.
if f.ID() != "filter.id" {
t.Errorf("ID() = %q, want %q", f.ID(), "filter.id")
}
if f.EnforcedAs() != FilterEnforcementBlocking {
t.Errorf("EnforcedAs() = %v, want %v", f.EnforcedAs(), FilterEnforcementBlocking)
}
if !f.SubscribedEventPresent() {
t.Error("SubscribedEventPresent() should be true")
}
if !f.TriggerReady() {
t.Error("TriggerReady() should be true")
}
if !f.EvaluatedForEpoch() {
t.Error("EvaluatedForEpoch() should be true for subscribed+triggered filter")
}
}
// TestEpochFilterOutcomeValidateRejection verifies that EpochFilterOutcome.Validate()
// rejects entries with invalid filter ids, invalid outcomes, invalid
// enforcement, and invalid dispositions.
func TestEpochFilterOutcomeValidateRejection(t *testing.T) {
// --- Empty filter id ---
o := EpochFilterOutcome{
filterID: StableToken{},
outcome: NewFilterOutcomeNotApplicableForEpoch(),
enforcement: FilterEnforcementBlocking,
failureDisposition: EvaluationFailureDispositionNone,
}
if err := o.Validate(); err == nil {
t.Error("Validate() with empty filter id should return error")
}
// --- Invalid filter id grammar ---
badToken, _ := NewStableTokenRequired("filterID", "BAD-ID!")
o2 := EpochFilterOutcome{
filterID: badToken,
outcome: NewFilterOutcomeNotApplicableForEpoch(),
enforcement: FilterEnforcementBlocking,
failureDisposition: EvaluationFailureDispositionNone,
}
if err := o2.Validate(); err == nil {
t.Error("Validate() with bad id grammar should return error")
}
// --- Invalid outcome ---
o3 := EpochFilterOutcome{
filterID: mustToken("filter.id"),
outcome: FilterOutcome{},
enforcement: FilterEnforcementBlocking,
failureDisposition: EvaluationFailureDispositionNone,
}
if err := o3.Validate(); err == nil {
t.Error("Validate() with invalid outcome should return error")
}
// --- Invalid enforcement ---
o4 := EpochFilterOutcome{
filterID: mustToken("filter.id"),
outcome: NewFilterOutcomeNotApplicableForEpoch(),
enforcement: "unknown",
failureDisposition: EvaluationFailureDispositionNone,
}
if err := o4.Validate(); err == nil {
t.Error("Validate() with invalid enforcement should return error")
}
}
// TestEpochFilterOutcomeStableTokenGrammar verifies that EpochFilterOutcome
// constructor enforces the stable-token grammar on the filter id.
func TestEpochFilterOutcomeStableTokenGrammar(t *testing.T) {
goodOutcome := NewFilterOutcomeNotApplicableForEpoch()
// --- Uppercase rejected ---
_, err := NewEpochFilterOutcome("Filter.ID", 100, goodOutcome, FilterEnforcementBlocking)
if err == nil {
t.Error("NewEpochFilterOutcome(uppercase id) should return error")
}
// --- Special chars rejected ---
_, err = NewEpochFilterOutcome("filter@id", 100, goodOutcome, FilterEnforcementBlocking)
if err == nil {
t.Error("NewEpochFilterOutcome(special chars id) should return error")
}
// --- Empty rejected ---
_, err = NewEpochFilterOutcome("", 100, goodOutcome, FilterEnforcementBlocking)
if err == nil {
t.Error("NewEpochFilterOutcome(empty id) should return error")
}
// --- Valid grammar accepted ---
_, err = NewEpochFilterOutcome("filter.id-01", 100, goodOutcome, FilterEnforcementBlocking)
if err != nil {
t.Errorf("NewEpochFilterOutcome(valid id) = %v, want nil", err)
}
}
// TestEpochFilterPrioritySnapshotReadyAndNonReady verifies that EpochFilter
// preserves the priority value for both ready and non-ready paths, and that
// the priority is accessible via the Priority() accessor regardless of
// EvaluatedForEpoch() state.
func TestEpochFilterPrioritySnapshotReadyAndNonReady(t *testing.T) {
passDecision := newPassDecision(t, "filter.id")
// Ready path: subscribed + triggerReady.
fReady, err := NewEpochFilter(
&stubEvaluator{id: "ready.filter", decision: passDecision}, 42,
true, true, false,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err != nil {
t.Fatalf("NewEpochFilter ready: %v", err)
}
if fReady.Priority() != 42 {
t.Errorf("Ready filter Priority() = %d, want 42", fReady.Priority())
}
if !fReady.EvaluatedForEpoch() {
t.Error("Ready filter should be evaluated for epoch")
}
// Non-ready path (deferred): subscribed + trigger not ready + blocks release.
fDeferred, err := NewEpochFilter(
&stubEvaluator{id: "defer.filter", decision: passDecision}, 77,
true, false, true,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err != nil {
t.Fatalf("NewEpochFilter deferred: %v", err)
}
if fDeferred.Priority() != 77 {
t.Errorf("Deferred filter Priority() = %d, want 77", fDeferred.Priority())
}
if fDeferred.EvaluatedForEpoch() {
t.Error("Deferred filter should NOT be evaluated for epoch")
}
// Non-ready path (not_applicable): subscribed + trigger not ready + !blocksRelease.
fNotApplicable, err := NewEpochFilter(
&stubEvaluator{id: "na.filter", decision: passDecision}, 99,
true, false, false,
FilterEnforcementObserveOnly,
100*time.Millisecond,
)
if err != nil {
t.Fatalf("NewEpochFilter not_applicable: %v", err)
}
if fNotApplicable.Priority() != 99 {
t.Errorf("Not-applicable filter Priority() = %d, want 99", fNotApplicable.Priority())
}
if fNotApplicable.EvaluatedForEpoch() {
t.Error("Not-applicable filter should NOT be evaluated for epoch")
}
// Unsubscribed path.
fUnsub, err := NewEpochFilter(
&stubEvaluator{id: "unsub.filter", decision: passDecision}, 5,
false, false, false,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err != nil {
t.Fatalf("NewEpochFilter unsubscribed: %v", err)
}
if fUnsub.Priority() != 5 {
t.Errorf("Unsubscribed filter Priority() = %d, want 5", fUnsub.Priority())
}
if fUnsub.EvaluatedForEpoch() {
t.Error("Unsubscribed filter should NOT be evaluated for epoch")
}
}
// TestEpochFilterOutcomePrioritySnapshotReadyAndNonReady verifies that
// EpochFilterOutcome preserves the priority value for both evaluated and
// non-evaluated outcome kinds.
func TestEpochFilterOutcomePrioritySnapshotReadyAndNonReady(t *testing.T) {
passDecision := newPassDecision(t, "f.ready")
// Evaluated outcome with priority.
ocEval, _ := NewFilterOutcomeEvaluated(passDecision)
oEval, err := NewEpochFilterOutcome("f.ready", 150, ocEval, FilterEnforcementBlocking)
if err != nil {
t.Fatalf("NewEpochFilterOutcome evaluated: %v", err)
}
if oEval.Priority() != 150 {
t.Errorf("Evaluated outcome Priority() = %d, want 150", oEval.Priority())
}
// Not-applicable outcome with priority.
ocNA := NewFilterOutcomeNotApplicableForEpoch()
oNA, err := NewEpochFilterOutcome("f.na", 200, ocNA, FilterEnforcementBlocking)
if err != nil {
t.Fatalf("NewEpochFilterOutcome not_applicable: %v", err)
}
if oNA.Priority() != 200 {
t.Errorf("Not-applicable outcome Priority() = %d, want 200", oNA.Priority())
}
// Deferred outcome with priority.
ocDeferred := NewFilterOutcomeDeferredByRequirement()
oDeferred, err := NewEpochFilterOutcome("f.defer", 250, ocDeferred, FilterEnforcementBlocking)
if err != nil {
t.Fatalf("NewEpochFilterOutcome deferred: %v", err)
}
if oDeferred.Priority() != 250 {
t.Errorf("Deferred outcome Priority() = %d, want 250", oDeferred.Priority())
}
// Evaluation error outcome with priority.
evalError, _ := NewFilterOutcomeEvaluationError("err.code")
oErr, err := NewEpochFilterOutcome("f.err", 300, evalError, FilterEnforcementBlocking)
if err != nil {
t.Fatalf("NewEpochFilterOutcome eval_error: %v", err)
}
if oErr.Priority() != 300 {
t.Errorf("Eval-error outcome Priority() = %d, want 300", oErr.Priority())
}
}
// TestResolvedFilterPriorityPreservedFromRegistry verifies that ResolvedFilter
// preserves the priority from the registration's effective policy for both
// ready and non-ready paths.
func TestResolvedFilterPriorityPreservedFromRegistry(t *testing.T) {
ts := time.Second
f := newMockFilter("f-priority")
reg, _ := NewFilterRegistration(f, "cap-test", true, FilterEnforcementBlocking, ts, 55)
_ = reg
snap, _ := NewFilterRegistrySnapshot("gen-1", []FilterRegistration{reg}, nil)
rc := reqCtx("gen-1", "attempt-1", "staging")
rsnap, _ := snap.BeginRequest(rc)
target, _ := NewAttemptTarget("mg", "model-a", "prov-x", "primary", []string{"cap-test"})
resolved, err := rsnap.ResolveAttempt(target)
if err != nil {
t.Fatalf("ResolveAttempt: %v", err)
}
if len(resolved) != 1 {
t.Fatalf("expected 1 resolved, got %d", len(resolved))
}
rf := resolved[0]
if rf.Priority() != 55 {
t.Errorf("ResolvedFilter.Priority() = %d, want 55", rf.Priority())
}
// BindEpoch with ready=true should preserve the registry priority.
appReady, _ := NewFilterApplicability(rf.FilterID(), true, true)
efReady, err := rf.BindEpoch(1, appReady)
if err != nil {
t.Fatalf("BindEpoch ready: %v", err)
}
if efReady.Priority() != 55 {
t.Errorf("BindEpoch ready Priority() = %d, want 55", efReady.Priority())
}
// BindEpoch with trigger not ready should also preserve.
appDeferred, _ := NewFilterApplicability(rf.FilterID(), true, false)
efDeferred, err := rf.BindEpoch(1, appDeferred)
if err != nil {
t.Fatalf("BindEpoch deferred: %v", err)
}
if efDeferred.Priority() != 55 {
t.Errorf("BindEpoch deferred Priority() = %d, want 55", efDeferred.Priority())
}
// BindEpoch with not-applicable.
appNotApp, _ := NewFilterApplicability(rf.FilterID(), true, false)
efNotApp, err := rf.BindEpoch(1, appNotApp)
if err != nil {
t.Fatalf("BindEpoch not_applicable: %v", err)
}
if efNotApp.Priority() != 55 {
t.Errorf("BindEpoch not_applicable Priority() = %d, want 55", efNotApp.Priority())
}
// BindEpoch with unsubscribed.
appUnsub, _ := NewFilterApplicability(rf.FilterID(), false, false)
efUnsub, err := rf.BindEpoch(1, appUnsub)
if err != nil {
t.Fatalf("BindEpoch unsubscribed: %v", err)
}
if efUnsub.Priority() != 55 {
t.Errorf("BindEpoch unsubscribed Priority() = %d, want 55", efUnsub.Priority())
}
}
// TestGateCoordinatorEvaluateReturnsArbiterResult verifies that GateCoordinator
// Evaluate returns the exact ArbitrationResult produced by the arbiter, and
// that Submit also returns the same result.
func TestGateCoordinatorEvaluateReturnsArbiterResult(t *testing.T) {
batch := makeTestBatch(t)
passDecision := makeTestPassDecision(t, "pass.filter")
passEval := &stubEvaluator{id: "pass.filter", decision: passDecision}
fPass := mustMakeEpochFilter(t, passEval, true, true, false, FilterEnforcementBlocking)
// Custom arbiter that captures the result.
var capturedResult ArbitrationResult
capturingArb := &capturingArbiterForProp{
capture: &capturedResult,
}
coord, cleanup := mustNewCoordinator(t, capturingArb, 0)
defer cleanup()
set, evalResult, err := coord.Evaluate(context.Background(), batch, []EpochFilter{fPass})
if err != nil {
t.Fatalf("Evaluate error: %v", err)
}
// Evaluate must return the same result the arbiter produced.
if evalResult.Action() != capturedResult.Action() {
t.Errorf("Evaluate result Action = %v, arbiter result Action = %v", evalResult.Action(), capturedResult.Action())
}
if evalResult.FilterID() != capturedResult.FilterID() {
t.Errorf("Evaluate result FilterID = %s, arbiter result FilterID = %s", evalResult.FilterID(), capturedResult.FilterID())
}
if evalResult.RuleID() != capturedResult.RuleID() {
t.Errorf("Evaluate result RuleID = %s, arbiter result RuleID = %s", evalResult.RuleID(), capturedResult.RuleID())
}
if evalResult.BaseDisposition() != capturedResult.BaseDisposition() {
t.Errorf("Evaluate result BaseDisposition = %v, arbiter result BaseDisposition = %v", evalResult.BaseDisposition(), capturedResult.BaseDisposition())
}
// The set must be non-empty and consistent.
if set.Len() == 0 {
t.Error("Evaluate returned empty set")
}
// Submit must also return the same result.
submitResult, err := coord.Submit(context.Background(), batch, []EpochFilter{fPass})
if err != nil {
t.Fatalf("Submit error: %v", err)
}
if submitResult.Action() != capturedResult.Action() {
t.Errorf("Submit result Action = %v, expected %v", submitResult.Action(), capturedResult.Action())
}
if submitResult.FilterID() != capturedResult.FilterID() {
t.Errorf("Submit result FilterID = %s, expected %s", submitResult.FilterID(), capturedResult.FilterID())
}
if submitResult.RuleID() != capturedResult.RuleID() {
t.Errorf("Submit result RuleID = %s, expected %s", submitResult.RuleID(), capturedResult.RuleID())
}
if submitResult.BaseDisposition() != capturedResult.BaseDisposition() {
t.Errorf("Submit result BaseDisposition = %v, expected %v", submitResult.BaseDisposition(), capturedResult.BaseDisposition())
}
}
// violationReturningEvaluator is a FilterEvaluator that always returns a
// specific FilterDecision.
type violationReturningEvaluator struct {
id string
decision FilterDecision
}
func (v *violationReturningEvaluator) ID() string { return v.id }
func (v *violationReturningEvaluator) Evaluate(_ context.Context, _ EvidenceBatch) (FilterDecision, error) {
return v.decision, nil
}
// capturingArbiterForProp is an EpochArbiter that captures the result for
// propagation verification tests.
type capturingArbiterForProp struct {
capture *ArbitrationResult
}
func (c *capturingArbiterForProp) Arbitrate(_ context.Context, batch EvidenceBatch, set EvaluationSet) (ArbitrationResult, error) {
baseDisp, _ := batch.BaseDisposition()
res, err := NewArbitrationResult(ArbitrationActionRelease, baseDisp, "", "", nil, nil)
if err != nil {
return ArbitrationResult{}, err
}
if c.capture != nil {
*c.capture = res
}
return res, nil
}
// ensure unused imports are consumed.
var _ = context.Background
var _ = sort.Strings
// TestEpochFilterSnapshotsEvaluatorID verifies that EpochFilter.ID() returns
// the evaluator id snapshot taken at construction time, not the evaluator's
// current id after mutation.
func TestEpochFilterSnapshotsEvaluatorID(t *testing.T) {
passDecision := newPassDecision(t, "filter.id")
mutable := &mutableIDEvaluator{id: "filter.id", decision: passDecision}
f, err := NewEpochFilter(
mutable, 100,
true, true, false,
FilterEnforcementBlocking,
100*time.Millisecond,
)
if err != nil {
t.Fatalf("NewEpochFilter: %v", err)
}
// Snapshot must match the id at construction time.
if got := f.ID(); got != "filter.id" {
t.Errorf("ID() = %q, want %q", got, "filter.id")
}
// Mutate the evaluator id.
mutable.id = "mutated.id"
// ID() must still return the original snapshot.
if got := f.ID(); got != "filter.id" {
t.Errorf("ID() after mutation = %q, want %q", got, "filter.id")
}
// Evaluator().ID() must reflect the mutation (seam is preserved).
if got := f.Evaluator().ID(); got != "mutated.id" {
t.Errorf("Evaluator().ID() = %q, want %q", got, "mutated.id")
}
}
// mutableIDEvaluator is a FilterEvaluator whose id can be changed at runtime
// to test that EpochFilter snapshot is not affected by evaluator mutation.
type mutableIDEvaluator struct {
id string
decision FilterDecision
}
func (m *mutableIDEvaluator) ID() string { return m.id }
func (m *mutableIDEvaluator) Evaluate(_ context.Context, _ EvidenceBatch) (FilterDecision, error) {
return m.decision, nil
}
// TestEpochFilterOutcomeRejectsDecisionIDMismatch verifies that NewEpochFilterOutcome
// rejects an evaluated outcome whose decision carries a different filter id
// than the constructor's wrapper filter id.
func TestEpochFilterOutcomeRejectsDecisionIDMismatch(t *testing.T) {
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
evidence := buildValidEvidence(t, ts)
// Build a decision with a different filter id from the wrapper.
d, err := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "other.id", "rule.id", evidence, nil)
if err != nil {
t.Fatalf("NewFilterDecision: %v", err)
}
_, err = NewEpochFilterOutcome("wrapper.id", 100, mustEvalOutcome(t, d), FilterEnforcementBlocking)
if err == nil {
t.Error("NewEpochFilterOutcome(mismatched decision) should return error")
} else if !strings.Contains(err.Error(), "filter id mismatch") {
t.Errorf("error = %v, want to contain 'filter id mismatch'", err)
}
}
// TestEvaluationSetRejectsForgedFailureDispositionMismatch verifies that a package-local
// forged EpochFilterOutcome (constructed directly without NewEpochFilterOutcome)
// whose failure disposition does not match the outcome kind and enforcement is
// rejected by both Validate() and NewEvaluationSet. Covers representative
// cases from the evaluated, evaluation-error, not-applicable, and deferred families.
func TestEvaluationSetRejectsForgedFailureDispositionMismatch(t *testing.T) {
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
evidence := buildValidEvidence(t, ts)
forgedToken, err := NewStableTokenRequired("filterID", "forged.id")
if err != nil {
t.Fatalf("NewStableTokenRequired: %v", err)
}
// Case 1: evaluated pass + blocking → should be none, but forged as blocking_fatal.
t.Run("evaluated_pass_with_blocking_fatal", func(t *testing.T) {
d, _ := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "forged.id", "rule.id", evidence, nil)
oc, _ := NewFilterOutcomeEvaluated(d)
forged := EpochFilterOutcome{
filterID: forgedToken,
outcome: oc,
enforcement: FilterEnforcementBlocking,
failureDisposition: EvaluationFailureDispositionBlockingFatal,
}
if err := forged.Validate(); err == nil {
t.Error("Validate() should reject forged evaluated+pass with blocking_fatal disposition")
} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
}
if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
t.Error("NewEvaluationSet should reject forged evaluated+pass with blocking_fatal")
}
})
// Case 2: evaluated violation + observe_only → should be observe_error, but forged as blocking_fatal.
t.Run("evaluated_violation_with_observe_blocking_fatal", func(t *testing.T) {
d, _ := NewFilterDecision(FilterDecisionKindViolation, "consumer.id", "forged.id", "rule.id", evidence, nil)
oc, _ := NewFilterOutcomeEvaluated(d)
forged := EpochFilterOutcome{
filterID: forgedToken,
outcome: oc,
enforcement: FilterEnforcementObserveOnly,
failureDisposition: EvaluationFailureDispositionBlockingFatal,
}
if err := forged.Validate(); err == nil {
t.Error("Validate() should reject forged evaluated+violation with blocking_fatal disposition under observe_only")
} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
}
if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
t.Error("NewEvaluationSet should reject forged evaluated+violation with blocking_fatal under observe_only")
}
})
// Case 3: evaluation_error + blocking → should be blocking_fatal, but forged as observe_error.
t.Run("eval_error_with_observe_error", func(t *testing.T) {
eoc := mustEvalError(t, "err.code")
forged := EpochFilterOutcome{
filterID: forgedToken,
outcome: eoc,
enforcement: FilterEnforcementBlocking,
failureDisposition: EvaluationFailureDispositionObserveError,
}
if err := forged.Validate(); err == nil {
t.Error("Validate() should reject forged eval_error with observe_error under blocking")
} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
}
if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
t.Error("NewEvaluationSet should reject forged eval_error with observe_error under blocking")
}
})
// Case 4: not_applicable → should be none, but forged as blocking_fatal.
t.Run("not_applicable_with_blocking_fatal", func(t *testing.T) {
oc := NewFilterOutcomeNotApplicableForEpoch()
forged := EpochFilterOutcome{
filterID: forgedToken,
outcome: oc,
enforcement: FilterEnforcementBlocking,
failureDisposition: EvaluationFailureDispositionBlockingFatal,
}
if err := forged.Validate(); err == nil {
t.Error("Validate() should reject forged not_applicable with blocking_fatal disposition")
} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
}
if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
t.Error("NewEvaluationSet should reject forged not_applicable with blocking_fatal")
}
})
// Case 5: deferred → should be none, but forged as observe_error.
t.Run("deferred_with_observe_error", func(t *testing.T) {
oc := NewFilterOutcomeDeferredByRequirement()
forged := EpochFilterOutcome{
filterID: forgedToken,
outcome: oc,
enforcement: FilterEnforcementObserveOnly,
failureDisposition: EvaluationFailureDispositionObserveError,
}
if err := forged.Validate(); err == nil {
t.Error("Validate() should reject forged deferred with observe_error disposition")
} else if !strings.Contains(err.Error(), "failure disposition mismatch") {
t.Errorf("Validate error = %v, want to contain 'failure disposition mismatch'", err)
}
if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
t.Error("NewEvaluationSet should reject forged deferred with observe_error")
}
})
}
// TestEvaluationSetRejectsForgedDecisionIDMismatch verifies that a package-local
// forged EpochFilterOutcome (constructed directly without NewEpochFilterOutcome)
// whose wrapper filter id does not match the evaluated decision's filter id is
// rejected by both Validate() and NewEvaluationSet(). This is the set-boundary
// regression complement to TestEpochFilterOutcomeRejectsDecisionIDMismatch,
// which validates the constructor boundary.
func TestEvaluationSetRejectsForgedDecisionIDMismatch(t *testing.T) {
forged := EpochFilterOutcome{
filterID: mustToken("wrapper.id"),
outcome: mustEvalOutcome(t, newPassDecision(t, "other.id")),
enforcement: FilterEnforcementBlocking,
failureDisposition: EvaluationFailureDispositionNone,
}
if err := forged.Validate(); err == nil {
t.Fatal("Validate() should reject forged decision id mismatch")
} else if !strings.Contains(err.Error(), "filter id mismatch") {
t.Errorf("Validate error = %v, want to contain 'filter id mismatch'", err)
}
if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
t.Fatal("NewEvaluationSet should reject forged decision id mismatch")
}
}
// mustEvalOutcome wraps a FilterDecision into a FilterOutcome of kind
// evaluated, failing the test on any error.
func mustEvalOutcome(t *testing.T, d FilterDecision) FilterOutcome {
t.Helper()
o, err := NewFilterOutcomeEvaluated(d)
if err != nil {
t.Fatalf("NewFilterOutcomeEvaluated: %v", err)
}
return o
}
// ensure unused imports are consumed.
var _ = context.Background
var _ = sort.Strings