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.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user