- Orchestrator.Analyze: tier1 → confidence check → tier2 escalation if < threshold - CatalogStatus mapped from confidence: >= threshold → StatusIndexed, else StatusNeedsResearch - Both tiers fail gracefully: returns zero-value IntakeResult + StatusNeedsResearch, err nil - ResearchClient interface + NoOpResearchClient stub for Phase 7 SearXNG - 5 TestOrchestrator* tests all passing (TDD green)
126 lines
4.1 KiB
Go
126 lines
4.1 KiB
Go
package ai
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"git.georgsen.dk/hwlab/internal/inventory"
|
|
)
|
|
|
|
func testReq() IntakeRequest {
|
|
return IntakeRequest{PhotosBase64: []string{"data:image/jpeg;base64,/9j/"}, JobID: "test-job"}
|
|
}
|
|
|
|
// TestOrchestratorHighConfidence: tier1 returns confidence 0.95; tier2 never called; status == StatusIndexed
|
|
func TestOrchestratorHighConfidence(t *testing.T) {
|
|
tier1 := &MockAIClient{FixedResult: HighConfidenceResult()}
|
|
tier2 := &MockAIClient{}
|
|
|
|
o := NewOrchestrator(tier1, tier2, 0.75)
|
|
result, status, err := o.Analyze(context.Background(), testReq())
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
if status != inventory.StatusIndexed {
|
|
t.Errorf("expected StatusIndexed, got %q", status)
|
|
}
|
|
if len(tier2.Calls) != 0 {
|
|
t.Errorf("expected tier2 never called, got %d calls", len(tier2.Calls))
|
|
}
|
|
if len(tier1.Calls) != 1 {
|
|
t.Errorf("expected tier1 called once, got %d calls", len(tier1.Calls))
|
|
}
|
|
}
|
|
|
|
// TestOrchestratorLowConfidenceEscalates: tier1 returns confidence 0.40; tier2 called once; tier2 returns 0.85; status == StatusIndexed
|
|
func TestOrchestratorLowConfidenceEscalates(t *testing.T) {
|
|
tier1 := &MockAIClient{FixedResult: LowConfidenceResult()}
|
|
tier2 := &MockAIClient{FixedResult: HighConfidenceResult()}
|
|
|
|
o := NewOrchestrator(tier1, tier2, 0.75)
|
|
result, status, err := o.Analyze(context.Background(), testReq())
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
if status != inventory.StatusIndexed {
|
|
t.Errorf("expected StatusIndexed after tier2 escalation, got %q", status)
|
|
}
|
|
if len(tier2.Calls) != 1 {
|
|
t.Errorf("expected tier2 called once, got %d calls", len(tier2.Calls))
|
|
}
|
|
if result.Confidence < 0.75 {
|
|
t.Errorf("expected tier2 result confidence >= 0.75, got %.2f", result.Confidence)
|
|
}
|
|
}
|
|
|
|
// TestOrchestratorBothTiersFail: both tiers error; result is non-nil, status == StatusNeedsResearch, err == nil
|
|
func TestOrchestratorBothTiersFail(t *testing.T) {
|
|
tier1 := &MockAIClient{FixedError: errors.New("tier1 connection refused")}
|
|
tier2 := &MockAIClient{FixedError: errors.New("tier2 connection refused")}
|
|
|
|
o := NewOrchestrator(tier1, tier2, 0.75)
|
|
result, status, err := o.Analyze(context.Background(), testReq())
|
|
|
|
if err != nil {
|
|
t.Fatalf("orchestrator must not propagate tier errors, got: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("orchestrator must return non-nil result even on total failure")
|
|
}
|
|
if status != inventory.StatusNeedsResearch {
|
|
t.Errorf("expected StatusNeedsResearch on total failure, got %q", status)
|
|
}
|
|
}
|
|
|
|
// TestOrchestratorTier1NilResult: tier1 returns nil result with nil error; orchestrator escalates to tier2
|
|
func TestOrchestratorTier1NilResult(t *testing.T) {
|
|
tier1 := &MockAIClient{FixedResult: nil, FixedError: nil}
|
|
tier2 := &MockAIClient{FixedResult: HighConfidenceResult()}
|
|
|
|
o := NewOrchestrator(tier1, tier2, 0.75)
|
|
result, status, err := o.Analyze(context.Background(), testReq())
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result from tier2 escalation")
|
|
}
|
|
if len(tier2.Calls) != 1 {
|
|
t.Errorf("expected tier2 called once after tier1 nil result, got %d calls", len(tier2.Calls))
|
|
}
|
|
if status != inventory.StatusIndexed {
|
|
t.Errorf("expected StatusIndexed from tier2, got %q", status)
|
|
}
|
|
}
|
|
|
|
// TestOrchestratorNeedsResearch: tier1 returns 0.40; tier2 also returns 0.40; final status == StatusNeedsResearch
|
|
func TestOrchestratorNeedsResearch(t *testing.T) {
|
|
tier1 := &MockAIClient{FixedResult: LowConfidenceResult()}
|
|
tier2 := &MockAIClient{FixedResult: LowConfidenceResult()}
|
|
|
|
o := NewOrchestrator(tier1, tier2, 0.75)
|
|
result, status, err := o.Analyze(context.Background(), testReq())
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
if status != inventory.StatusNeedsResearch {
|
|
t.Errorf("expected StatusNeedsResearch when both tiers return low confidence, got %q", status)
|
|
}
|
|
if len(tier2.Calls) != 1 {
|
|
t.Errorf("expected tier2 called once, got %d calls", len(tier2.Calls))
|
|
}
|
|
}
|