248 lines
8.4 KiB
Go
248 lines
8.4 KiB
Go
package validators
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/modulecatalog"
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
|
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
|
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
|
"gitea.maximumdirect.net/eric/audita/internal/validators/confidence_threshold"
|
|
"gitea.maximumdirect.net/eric/audita/internal/validators/editorial_review"
|
|
"gitea.maximumdirect.net/eric/audita/internal/validators/meaning_reversal_review"
|
|
validatormetadata "gitea.maximumdirect.net/eric/audita/internal/validators/metadata"
|
|
"gitea.maximumdirect.net/eric/audita/internal/validators/no_effect"
|
|
"gitea.maximumdirect.net/eric/audita/internal/validators/non_empty_corrected_text"
|
|
"gitea.maximumdirect.net/eric/audita/internal/validators/original_text_presence"
|
|
"gitea.maximumdirect.net/eric/audita/internal/validators/proposal_shape"
|
|
"gitea.maximumdirect.net/eric/audita/internal/validators/protected_terms"
|
|
"gitea.maximumdirect.net/eric/audita/internal/validators/spoken_form_plausibility"
|
|
)
|
|
|
|
func TestBuiltInRegistryRegistersAllKeys(t *testing.T) {
|
|
r := NewBuiltInRegistry()
|
|
for _, key := range []string{
|
|
KeyProposalShape,
|
|
KeyConfidenceThreshold,
|
|
KeyOriginalTextPresence,
|
|
KeyNonEmptyCorrectedText,
|
|
KeyNoEffect,
|
|
KeyProtectedTerms,
|
|
KeySpokenFormPlausibility,
|
|
KeyMeaningReversalReview,
|
|
KeyEditorialReview,
|
|
} {
|
|
def, ok := r.Lookup(key)
|
|
if !ok {
|
|
t.Fatalf("expected validator key %q to be registered", key)
|
|
}
|
|
v, err := def.Build()
|
|
if err != nil {
|
|
t.Fatalf("build validator %q: %v", key, err)
|
|
}
|
|
if v.Name() != key {
|
|
t.Fatalf("expected validator name %q, got %q", key, v.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuiltInValidatorPackagesConstruct(t *testing.T) {
|
|
type validatorCtor struct {
|
|
name string
|
|
key string
|
|
build func() (contracts.Validator, error)
|
|
wantClass validatormetadata.ExecutionClass
|
|
}
|
|
cases := []validatorCtor{
|
|
{name: "proposal_shape", key: KeyProposalShape, build: proposal_shape.New, wantClass: validatormetadata.ExecutionClassDeterministic},
|
|
{name: "confidence_threshold", key: KeyConfidenceThreshold, build: confidence_threshold.New, wantClass: validatormetadata.ExecutionClassDeterministic},
|
|
{name: "original_text_presence", key: KeyOriginalTextPresence, build: original_text_presence.New, wantClass: validatormetadata.ExecutionClassDeterministic},
|
|
{name: "non_empty_corrected_text", key: KeyNonEmptyCorrectedText, build: non_empty_corrected_text.New, wantClass: validatormetadata.ExecutionClassDeterministic},
|
|
{name: "no_effect", key: KeyNoEffect, build: no_effect.New, wantClass: validatormetadata.ExecutionClassDeterministic},
|
|
{name: "protected_terms", key: KeyProtectedTerms, build: protected_terms.New, wantClass: validatormetadata.ExecutionClassDeterministic},
|
|
{name: "spoken_form_plausibility", key: KeySpokenFormPlausibility, build: spoken_form_plausibility.New, wantClass: validatormetadata.ExecutionClassLLMBacked},
|
|
{name: "meaning_reversal_review", key: KeyMeaningReversalReview, build: meaning_reversal_review.New, wantClass: validatormetadata.ExecutionClassLLMBacked},
|
|
{name: "editorial_review", key: KeyEditorialReview, build: editorial_review.New, wantClass: validatormetadata.ExecutionClassLLMBacked},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
v, err := tc.build()
|
|
if err != nil {
|
|
t.Fatalf("%s: build: %v", tc.name, err)
|
|
}
|
|
if v.Name() != tc.key {
|
|
t.Fatalf("%s: expected key %q, got %q", tc.name, tc.key, v.Name())
|
|
}
|
|
if got := validatormetadata.ClassOf(v); got != tc.wantClass {
|
|
t.Fatalf("%s: expected class %q, got %q", tc.name, tc.wantClass, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegistryBuildsClassifiedValidators(t *testing.T) {
|
|
r := NewBuiltInRegistry()
|
|
for _, key := range r.RegisteredKeys() {
|
|
v, err := r.MustBuild(key)
|
|
if err != nil {
|
|
t.Fatalf("must build %q: %v", key, err)
|
|
}
|
|
if _, ok := v.(validatormetadata.ClassifiedValidator); !ok {
|
|
t.Fatalf("expected built validator %q to expose execution classification metadata", key)
|
|
}
|
|
want := validatormetadata.ClassForKey(key)
|
|
if got := validatormetadata.ClassOf(v); got != want {
|
|
t.Fatalf("expected class %q for %q, got %q", want, key, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExecutionClassResolvableByStableKeyAndByValidatorInstance(t *testing.T) {
|
|
r := NewBuiltInRegistry()
|
|
for _, key := range r.RegisteredKeys() {
|
|
v, err := r.MustBuild(key)
|
|
if err != nil {
|
|
t.Fatalf("must build %q: %v", key, err)
|
|
}
|
|
fromKey := validatormetadata.ClassForKey(key)
|
|
fromInstance := validatormetadata.ClassOf(v)
|
|
if fromInstance != fromKey {
|
|
t.Fatalf("class mismatch for %q: key=%q instance=%q", key, fromKey, fromInstance)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegistryProtectedTermsUsesNonGlossaryStageBehavior(t *testing.T) {
|
|
r := NewBuiltInRegistry()
|
|
v, err := r.MustBuild(KeyProtectedTerms)
|
|
if err != nil {
|
|
t.Fatalf("must build protected_terms: %v", err)
|
|
}
|
|
|
|
req := contracts.ValidationRequest{
|
|
ModuleKey: "glossary",
|
|
Glossary: &schema.Glossary{
|
|
Entries: []schema.GlossaryEntry{{Name: "Jesters", Category: "faction", Summary: "Protected"}},
|
|
},
|
|
CandidateProposal: []proposals.EnrichedCorrectionProposal{
|
|
{
|
|
CorrectionProposal: proposals.CorrectionProposal{
|
|
TargetSegmentID: 1,
|
|
OriginalText: "Jesters",
|
|
CorrectedText: "Gestures",
|
|
Confidence: 1.0,
|
|
},
|
|
ProposalMetadata: proposals.ProposalMetadata{
|
|
ProposalIndex: 0,
|
|
ModuleKey: "glossary",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result, err := v.Validate(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("validate protected_terms: %v", err)
|
|
}
|
|
if len(result.Decisions) != 1 {
|
|
t.Fatalf("expected one decision, got %+v", result.Decisions)
|
|
}
|
|
if !result.Decisions[0].Approved {
|
|
t.Fatalf("expected glossary module proposal to be approved by non-glossary-stage protected_terms validator, got %+v", result.Decisions[0])
|
|
}
|
|
}
|
|
|
|
func TestResolveBuiltInChainUsesGlossaryStageProtectedTermsBehavior(t *testing.T) {
|
|
chain, err := ResolveBuiltInChain("glossary", NewBuiltInRegistry())
|
|
if err != nil {
|
|
t.Fatalf("resolve glossary chain: %v", err)
|
|
}
|
|
|
|
var v contracts.Validator
|
|
for _, candidate := range chain {
|
|
if candidate.Name() == KeyProtectedTerms {
|
|
v = candidate
|
|
break
|
|
}
|
|
}
|
|
if v == nil {
|
|
t.Fatalf("expected protected_terms validator in glossary chain")
|
|
}
|
|
|
|
req := contracts.ValidationRequest{
|
|
ModuleKey: "glossary",
|
|
Glossary: &schema.Glossary{
|
|
Entries: []schema.GlossaryEntry{{Name: "Jesters", Category: "faction", Summary: "Protected"}},
|
|
},
|
|
CandidateProposal: []proposals.EnrichedCorrectionProposal{
|
|
{
|
|
CorrectionProposal: proposals.CorrectionProposal{
|
|
TargetSegmentID: 1,
|
|
OriginalText: "Jesters",
|
|
CorrectedText: "Gestures",
|
|
Confidence: 1.0,
|
|
},
|
|
ProposalMetadata: proposals.ProposalMetadata{
|
|
ProposalIndex: 0,
|
|
ModuleKey: "glossary",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result, err := v.Validate(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("validate protected_terms: %v", err)
|
|
}
|
|
if len(result.Decisions) != 1 {
|
|
t.Fatalf("expected one decision, got %+v", result.Decisions)
|
|
}
|
|
if result.Decisions[0].Approved {
|
|
t.Fatalf("expected glossary-stage protected_terms rejection, got %+v", result.Decisions[0])
|
|
}
|
|
}
|
|
|
|
func TestBuiltInRegistryUnknownKeyFails(t *testing.T) {
|
|
r := NewBuiltInRegistry()
|
|
if _, err := r.MustBuild("missing"); err == nil {
|
|
t.Fatalf("expected unknown validator key error")
|
|
}
|
|
}
|
|
|
|
func TestBuiltInChainKeysResolveForProductionModules(t *testing.T) {
|
|
for _, moduleKey := range modulecatalog.SupportedKeys() {
|
|
keys, err := BuiltInChainKeys(moduleKey)
|
|
if err != nil {
|
|
t.Fatalf("resolve keys for %q: %v", moduleKey, err)
|
|
}
|
|
if len(keys) == 0 {
|
|
t.Fatalf("expected non-empty chain for %q", moduleKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveBuiltInChainUsesRegisteredKeys(t *testing.T) {
|
|
r := NewBuiltInRegistry()
|
|
for _, moduleKey := range modulecatalog.SupportedKeys() {
|
|
chain, err := ResolveBuiltInChain(moduleKey, r)
|
|
if err != nil {
|
|
t.Fatalf("resolve chain for %q: %v", moduleKey, err)
|
|
}
|
|
if len(chain) == 0 {
|
|
t.Fatalf("expected non-empty chain for %q", moduleKey)
|
|
}
|
|
for _, v := range chain {
|
|
if v == nil {
|
|
t.Fatalf("nil validator in %q chain", moduleKey)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuiltInChainUnknownModuleFails(t *testing.T) {
|
|
if _, err := BuiltInChainKeys("unknown"); err == nil {
|
|
t.Fatalf("expected unknown module chain failure")
|
|
}
|
|
if _, err := ResolveBuiltInChain("unknown", NewBuiltInRegistry()); err == nil {
|
|
t.Fatalf("expected unknown module chain failure")
|
|
}
|
|
}
|