package ai import ( "context" "encoding/json" "errors" "net/http/httptest" "strings" "testing" "github.com/missionworkforce/missionmatch/internal/platform/inference" ) // unit(384) builds a deterministic unit basis vector for testing the pure comparison without a plane. func unit(dim, i int) []float32 { v := make([]float32, dim) v[i%dim] = 1 return v } func TestVectorsMatchModelAcceptsSameModel(t *testing.T) { // Stored equals fresh: the same model produced both, cosine 1.0, so the store is verified. stored := [][]float32{unit(384, 0), unit(384, 1)} fresh := [][]float32{unit(384, 0), unit(384, 1)} if err := vectorsMatchModel([]string{"a", "b"}, stored, fresh, 384); err != nil { t.Fatalf("matching vectors must verify, got %v", err) } } func TestVectorsMatchModelRejectsModelSwap(t *testing.T) { // Fresh is orthogonal to stored (cosine 0): a different model produced the store. Must be a mismatch. stored := [][]float32{unit(384, 0)} fresh := [][]float32{unit(384, 1)} err := vectorsMatchModel([]string{"a"}, stored, fresh, 384) if !errors.Is(err, ErrVectorModelMismatch) { t.Fatalf("a model swap must be ErrVectorModelMismatch, got %v", err) } } func TestVectorsMatchModelRejectsWrongWidth(t *testing.T) { stored := [][]float32{make([]float32, 256)} // wrong width fresh := [][]float32{make([]float32, 384)} err := vectorsMatchModel([]string{"a"}, stored, fresh, 384) if !errors.Is(err, ErrVectorModelMismatch) { t.Fatalf("a wrong width stored vector must be ErrVectorModelMismatch, got %v", err) } } func TestVectorGateMismatchIsPermanent(t *testing.T) { calls := 0 g := NewVectorGate(func(context.Context) error { calls++ return ErrVectorModelMismatch }) if err := g.Check(context.Background()); !errors.Is(err, ErrVectorModelMismatch) { t.Fatalf("first check must report the mismatch, got %v", err) } if err := g.Check(context.Background()); !errors.Is(err, ErrVectorModelMismatch) { t.Fatalf("second check must still report the mismatch, got %v", err) } if calls != 1 { t.Fatalf("a proven mismatch must not re run the check, got %d calls", calls) } } func TestVectorGateTransientThenVerified(t *testing.T) { calls := 0 g := NewVectorGate(func(context.Context) error { calls++ if calls == 1 { return errors.New("dial tcp: connection refused") // plane down at boot } return nil // plane recovered }) // A transient failure surfaces as a plane outage, not a mismatch, and the gate stays unverified. err := g.Check(context.Background()) if !errors.Is(err, inference.ErrPlaneUnavailable) { t.Fatalf("a transient failure must surface as ErrPlaneUnavailable, got %v", err) } if errors.Is(err, ErrVectorModelMismatch) { t.Fatalf("a transient failure must NOT be treated as a mismatch") } // The next query re verifies and the gate self heals. if err := g.Check(context.Background()); err != nil { t.Fatalf("the gate must self heal once the plane recovers, got %v", err) } if err := g.Check(context.Background()); err != nil { t.Fatalf("once verified the gate stays open, got %v", err) } if calls != 2 { t.Fatalf("expected exactly two verify calls (retry then success), got %d", calls) } } func TestVectorGateNilVerifierIsOpen(t *testing.T) { if err := NewVectorGate(nil).Check(context.Background()); err != nil { t.Fatalf("a gate with no verifier must be open, got %v", err) } } // TestHandlerVectorMismatchReturns503 proves the serving boundary: a brain whose gate is in a model // mismatch returns a typed 503 vector_model_mismatch, never a hang and never a wrong ranking. func TestHandlerVectorMismatchReturns503(t *testing.T) { h := NewHandler(&Orchestrator{}).WithVectorGate(NewVectorGate(func(context.Context) error { return ErrVectorModelMismatch })) req := httptest.NewRequest("POST", "/ai/query", strings.NewReader(`{"question":"who can support this"}`)) rec := httptest.NewRecorder() h.query(rec, req) if rec.Code != 503 { t.Fatalf("a model mismatch must be a 503, got %d", rec.Code) } var body errorBody if err := json.NewDecoder(rec.Body).Decode(&body); err != nil { t.Fatalf("decode error body: %v", err) } if body.Error.Code != "vector_model_mismatch" { t.Fatalf("error code must be vector_model_mismatch, got %q", body.Error.Code) } }