Files
glpi-ai-agent/internal/agent/agent_test.go
jbergner 9b3227348d
All checks were successful
release-tag / release-image (push) Successful in 1m33s
init
2026-07-27 17:32:35 +02:00

131 lines
5.1 KiB
Go

package agent
import (
"context"
"os"
"testing"
"time"
"github.com/example/glpi-ai-agent/internal/config"
"github.com/example/glpi-ai-agent/internal/knowledge"
"github.com/example/glpi-ai-agent/internal/metrics"
"github.com/example/glpi-ai-agent/internal/model"
"github.com/example/glpi-ai-agent/internal/queue"
"github.com/example/glpi-ai-agent/internal/state"
)
type fakeGLPI struct {
ticket model.Ticket
followups []model.Followup
cats []model.Category
setCategory int
addReply int
ticketReads int
followupReads int
injectFollowupOnSecondCheck bool
}
func (f *fakeGLPI) Ping(context.Context) error { return nil }
func (f *fakeGLPI) ValidateContract(context.Context) error { return nil }
func (f *fakeGLPI) ListRecentTickets(context.Context, int, string) ([]model.Ticket, error) {
return nil, nil
}
func (f *fakeGLPI) GetTicket(context.Context, int64) (model.Ticket, error) {
f.ticketReads++
return f.ticket, nil
}
func (f *fakeGLPI) GetFollowups(context.Context, int64) ([]model.Followup, error) {
f.followupReads++
if f.injectFollowupOnSecondCheck && f.followupReads >= 2 {
return []model.Followup{{ID: 99}}, nil
}
return f.followups, nil
}
func (f *fakeGLPI) SetCategory(_ context.Context, _ int64, id int64) error {
f.setCategory++
f.ticket.CategoryID = id
f.ticket.DateMod = "v2"
return nil
}
func (f *fakeGLPI) AddFollowup(context.Context, int64, string) error {
f.addReply++
f.ticket.DateMod = "v3"
return nil
}
func (f *fakeGLPI) GetCategories(context.Context) ([]model.Category, error) { return f.cats, nil }
type fakeAI struct{ d model.Decision }
func (f fakeAI) Ping(context.Context) error { return nil }
func (f fakeAI) Analyse(context.Context, model.Ticket, []model.Category, []model.KnowledgeHit, model.ContextSnapshot) (model.Decision, error) {
return f.d, nil
}
func newTestService(t *testing.T, g *fakeGLPI, d model.Decision, autoReply bool) *Service {
t.Helper()
dir := t.TempDir()
kDir := dir + "/k"
if err := os.MkdirAll(kDir, 0o755); err != nil {
t.Fatal(err)
}
doc := `{"id":"KB1","title":"Known","text":"vpn gateway","answer":"Bitte starten Sie den VPN-Client neu.","auto_reply":true,"min_score":0,"categories":[2],"keywords":["vpn","gateway"],"source":"internal-kb","language":"de-DE","communication_style":"formal"}`
if err := os.WriteFile(kDir+"/kb.json", []byte(doc), 0o644); err != nil {
t.Fatal(err)
}
k, err := knowledge.Load(context.Background(), kDir, dir, nil, false, []string{"internal-kb"})
if err != nil {
t.Fatal(err)
}
st, err := state.Open(dir, 100)
if err != nil {
t.Fatal(err)
}
cfg := config.Config{DryRun: false, AutoCategory: true, AutoReply: autoReply, CategoryConfidence: .9, ReplyConfidence: .9, KnowledgeMinScore: 0, KnowledgeTopK: 1, CategoryPromptLimit: 20, Workers: 1, GLPIAllowedStatusIDs: []int64{1}, KnowledgeAllowedSources: []string{"internal-kb"}, KnowledgeAutoReplySources: []string{"internal-kb"}, CommunicationLanguage: "de-DE", CommunicationStyle: "formal", CommunicationSalutation: "Guten Tag,", CommunicationClosing: "Mit freundlichen Grüßen", CommunicationSignature: "IT-Service"}
return New(cfg, g, fakeAI{d: d}, k, st, queue.New(8), metrics.New(), nil)
}
func TestExistingFollowupBlocksReplyButNotCategory(t *testing.T) {
g := &fakeGLPI{ticket: model.Ticket{ID: 1, Name: "vpn", Content: "gateway", DateMod: "v1", StatusID: 1, CategoryID: 1}, followups: []model.Followup{{ID: 5}}, cats: []model.Category{{ID: 1}, {ID: 2}}}
var d model.Decision
d.Category.ID, d.Category.Change, d.Category.Confidence = 2, true, 1
d.Reply.Allowed, d.Reply.Confidence, d.Reply.KnowledgeID = true, 1, "KB1"
svc := newTestService(t, g, d, true)
if err := svc.Process(context.Background(), 1); err != nil {
t.Fatal(err)
}
if g.setCategory != 1 {
t.Fatalf("category writes=%d", g.setCategory)
}
if g.addReply != 0 {
t.Fatalf("reply writes=%d", g.addReply)
}
}
func TestRaceFollowupBlocksReply(t *testing.T) {
g := &fakeGLPI{ticket: model.Ticket{ID: 1, Name: "vpn", Content: "gateway", DateMod: "v1", StatusID: 1, CategoryID: 2}, cats: []model.Category{{ID: 2}}, injectFollowupOnSecondCheck: true}
var d model.Decision
d.Reply.Allowed, d.Reply.Confidence, d.Reply.KnowledgeID = true, 1, "KB1"
svc := newTestService(t, g, d, true)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := svc.Process(ctx, 1); err != nil {
t.Fatal(err)
}
if g.addReply != 0 {
t.Fatalf("reply writes=%d", g.addReply)
}
}
func TestDisallowedStatusSkipsWithoutWrites(t *testing.T) {
g := &fakeGLPI{ticket: model.Ticket{ID: 1, Name: "vpn", Content: "gateway", DateMod: "v1", StatusID: 6, CategoryID: 1}, cats: []model.Category{{ID: 1}, {ID: 2}}}
var d model.Decision
d.Category.ID, d.Category.Change, d.Category.Confidence = 2, true, 1
svc := newTestService(t, g, d, true)
if err := svc.Process(context.Background(), 1); err != nil {
t.Fatal(err)
}
if g.setCategory != 0 || g.addReply != 0 {
t.Fatalf("unexpected writes: category=%d reply=%d", g.setCategory, g.addReply)
}
}