Files
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

176 lines
4.6 KiB
Go

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)
}
}