package wing_test import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mathiasbq/hyperguild/ingestion/internal/wing" ) type fakeClassifier struct { s wing.Suggestion err error calls int } func (f *fakeClassifier) Classify(_ context.Context, _ string, _ []string) (wing.Suggestion, error) { f.calls++ return f.s, f.err } type fakeScorer map[string]float64 func (f fakeScorer) Score(string) map[string]float64 { return f } // eligibleExcept allows every name except the listed ones. Being permissive on // names it has never heard of means only the "known wing" check can stop an // invented wing, so a test of that check cannot pass by accident. func eligibleExcept(names ...string) func(string) bool { deny := map[string]bool{} for _, n := range names { deny[n] = true } return func(w string) bool { return !deny[w] } } func newResolver(c wing.Classifier, s wing.Scorer) wing.Resolver { return wing.Resolver{ Known: []string{"homelab", "hyperguild", "tapir", "rikatillsammans"}, Eligible: eligibleExcept("rikatillsammans"), // a client wing: known, never auto Classifier: c, Scorer: s, Threshold: 0.8, } } func TestResolve_ExplicitSignal(t *testing.T) { tests := []struct { name string note wing.Note want wing.Source wantWing string }{ {"subfolder of a known eligible wing", wing.Note{Dir: "homelab"}, wing.Explicit, "homelab"}, {"frontmatter of a known eligible wing", wing.Note{FrontmatterWing: "tapir"}, wing.Explicit, "tapir"}, {"subfolder and frontmatter agree", wing.Note{Dir: "tapir", FrontmatterWing: "tapir"}, wing.Explicit, "tapir"}, {"subfolder and frontmatter disagree", wing.Note{Dir: "tapir", FrontmatterWing: "homelab"}, wing.Exception, ""}, {"unknown wing is never created", wing.Note{Dir: "hoemlab"}, wing.Exception, "hoemlab"}, {"known but not eligible (client) wing is never auto-assigned", wing.Note{Dir: "rikatillsammans"}, wing.Exception, "rikatillsammans"}, {"subfolder is case-insensitive", wing.Note{Dir: "HomeLab"}, wing.Explicit, "homelab"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &fakeClassifier{} d := newResolver(c, fakeScorer{}).Resolve(context.Background(), tt.note) assert.Equal(t, tt.want, d.Source) assert.Equal(t, tt.wantWing, d.Wing) assert.Zero(t, c.calls, "an explicit signal must not spend an LLM call") assert.NotEmpty(t, d.Reason) }) } } func TestResolve_Classified(t *testing.T) { tests := []struct { name string sugg wing.Suggestion err error scores fakeScorer want wing.Source wantWing string }{ {"confident and scorer agrees", wing.Suggestion{Wing: "tapir", Confidence: 0.9}, nil, fakeScorer{"tapir": 3, "homelab": 1}, wing.Classified, "tapir"}, {"confidence exactly at threshold passes", wing.Suggestion{Wing: "tapir", Confidence: 0.8}, nil, fakeScorer{"tapir": 3}, wing.Classified, "tapir"}, {"below threshold is an exception, proposal kept", wing.Suggestion{Wing: "tapir", Confidence: 0.79}, nil, fakeScorer{"tapir": 3}, wing.Exception, "tapir"}, {"scorer disagrees", wing.Suggestion{Wing: "tapir", Confidence: 0.95}, nil, fakeScorer{"homelab": 5, "tapir": 1}, wing.Exception, "tapir"}, {"scorer has no opinion also needs review", wing.Suggestion{Wing: "tapir", Confidence: 0.95}, nil, fakeScorer{}, wing.Exception, "tapir"}, {"classifier proposes a client wing", wing.Suggestion{Wing: "rikatillsammans", Confidence: 0.99}, nil, fakeScorer{"rikatillsammans": 9}, wing.Exception, "rikatillsammans"}, {"classifier invents a wing", wing.Suggestion{Wing: "brand-new-wing", Confidence: 0.99}, nil, fakeScorer{"brand-new-wing": 9}, wing.Exception, "brand-new-wing"}, {"classifier abstains", wing.Suggestion{Wing: "", Confidence: 0}, nil, fakeScorer{"tapir": 3}, wing.Exception, ""}, {"classifier error", wing.Suggestion{}, errors.New("llm down"), fakeScorer{"tapir": 3}, wing.Exception, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &fakeClassifier{s: tt.sugg, err: tt.err} d := newResolver(c, tt.scores).Resolve(context.Background(), wing.Note{Text: "some note"}) assert.Equal(t, tt.want, d.Source) assert.Equal(t, tt.wantWing, d.Wing) assert.NotEmpty(t, d.Reason, "every decision says why, so a spot-check can judge it") assert.Equal(t, 1, c.calls) }) } } func TestResolve_NilDependenciesFailClosed(t *testing.T) { r := wing.Resolver{Known: []string{"tapir"}, Eligible: eligibleExcept(), Threshold: 0.8} d := r.Resolve(context.Background(), wing.Note{Text: "x"}) require.Equal(t, wing.Exception, d.Source, "no classifier configured must route to review, not panic or guess") }