package support import ( "context" "encoding/json" "errors" "strings" "testing" "github.com/descrybe/descrybe-v2/apps/api/internal/processing" "github.com/google/uuid" ) type mockCompleter struct { text string err error calls int lastSystem string lastUser string } func (m *mockCompleter) Complete(ctx context.Context, system, user string) (processing.Completion, error) { m.calls++ m.lastSystem = system m.lastUser = user if m.err != nil { return processing.Completion{}, m.err } return processing.Completion{Text: m.text, Model: "mock"}, nil } func TestParseAIAssistResult(t *testing.T) { t.Parallel() got, err := parseAIAssistResult(map[string]any{ "body": "Hello", "confidence": 0.9, "handoff": false, "citations": []any{"kb:pay"}, }) if err != nil { t.Fatal(err) } if got.Body != "Hello" || got.Confidence != 0.9 || got.Handoff || len(got.Citations) != 1 { t.Fatalf("%+v", got) } } func TestCompleterSupportAI_draftOnly(t *testing.T) { t.Parallel() mc := &mockCompleter{text: `{"body":"Reset via Settings → Security.","confidence":0.91,"handoff":false,"citations":["kb:password"]}`} sys, user := BuildAutoReplyMessages(AutoPromptInput{ Subject: "password reset", Body: "I forgot my password", Category: "account", CompanyID: uuid.New(), KBSnippets: []KBSnippet{{Slug: "password", Title: "Reset", BodyMD: "Use Settings."}}, }) comp, obj, cerr := processing.CompleteJSON(context.Background(), mc, sys, user, processing.CompleteOptions{MaxTokens: 100}) if cerr != nil { t.Fatal(cerr) } if comp.Text == "" || obj["body"] == nil { t.Fatalf("comp=%+v obj=%v", comp, obj) } res, err := parseAIAssistResult(obj) if err != nil || res.Confidence < 0.9 { t.Fatalf("res=%+v err=%v", res, err) } if mc.calls < 1 { t.Fatal("expected completer call") } labeled := labelAIBody(res.Body) if !strings.Contains(labeled, "AI-assisted") { t.Fatalf("%q", labeled) } } func TestCompleterSupportAI_handoffOnLowConfidence(t *testing.T) { t.Parallel() mc := &mockCompleter{text: `{"body":"Not sure","confidence":0.2,"handoff":false}`} res, err := parseAIAssistResult(mustJSON(mc.text)) if err != nil { t.Fatal(err) } cfg := AutoConfig{AIConfidenceThreshold: 0.65} if !(res.Handoff || res.Confidence < cfg.AIConfidenceThreshold) { t.Fatal("expected handoff branch") } } func TestCompleterSupportAI_handoffFlag(t *testing.T) { t.Parallel() res, err := parseAIAssistResult(map[string]any{"body": "x", "confidence": 0.99, "handoff": true}) if err != nil || !res.Handoff { t.Fatalf("%+v %v", res, err) } } func TestLabelAIBody(t *testing.T) { t.Parallel() got := labelAIBody("Thanks for writing.") if !strings.Contains(got, "AI-assisted") { t.Fatalf("%q", got) } again := labelAIBody(got) if strings.Count(again, "AI-assisted reply") != 1 { t.Fatalf("double footer: %q", again) } } func TestTryAutoReplyLLM_usesRunner(t *testing.T) { t.Parallel() stub := &stubAIRunner{} s := &Service{SupportAI: stub} // nil pool → disabled before runner err := s.TryAutoReplyLLM(context.Background(), uuid.New()) if !errors.Is(err, ErrAIAutoReplyDisabled) { t.Fatalf("got %v", err) } if stub.calls != 0 { t.Fatal("runner should not run without pool") } } func mustJSON(s string) map[string]any { obj, err := processing.ParseJSONObject(s) if err != nil { panic(err) } return obj } func TestAIDeliveryConstants(t *testing.T) { t.Parallel() if AIDeliveryDraft != "draft" || AIDeliveryAutoSend != "auto_send" { t.Fatal("delivery constants") } if AutoSourceAI != "ai" { t.Fatal("auto source") } } func TestEnqueueAIFallback_nilSafe(t *testing.T) { t.Parallel() s := &Service{} if err := s.EnqueueAIFallback(context.Background(), Ticket{ID: uuid.New()}); err != nil { t.Fatal(err) } } func TestMaybeAutoReplyOnCreate_aiEnqueuePathDocumented(t *testing.T) { t.Parallel() // Without pool, GetAutoConfig would panic — document that orchestrator requires Pool. cfg := AutoConfig{Enabled: true, FAQEnabled: true, AIEnabled: true, MatchConfidenceThreshold: 0.78} match := MatchAutoReplyResult{Matched: false, Confidence: 0.1, Kind: MatchKindNone} if match.Matched && match.Confidence >= cfg.MatchConfidenceThreshold { t.Fatal("should miss") } if !cfg.AIEnabled { t.Fatal("AI should enqueue") } } func TestRedactedJSONMetaNoPII(t *testing.T) { t.Parallel() meta, _ := json.Marshal(map[string]any{ "source": AutoSourceAI, "reason": "llm_error", "err": RedactForAutoLog("fail sk-abcdefghijklmnopqrstuvwxyz user@example.com"), }) s := string(meta) if strings.Contains(s, "sk-abcdefghijklmnopqrstuvwxyz") || strings.Contains(s, "user@example.com") { t.Fatalf("PII in meta: %s", s) } }