Files
glpi-neural-brain/internal/graph/research_tasks_test.go
2026-08-07 17:05:03 +02:00

146 lines
4.2 KiB
Go

package graph
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/local/glpi-neural-brain/internal/model"
)
func TestResearchTaskLifecycleSQLite(t *testing.T) {
store, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer store.Close()
ctx := context.Background()
request := model.ResearchTask{
DedupeKey: "zfs-restore",
Topic: "ZFS Restore",
Reason: "missing_validation",
RequestedBy: "test",
Priority: .91,
Questions: []string{"Wie wird ein Restore validiert?"},
MaxAttempts: 3,
}
queued, created, err := store.EnqueueResearchTask(ctx, request, 24*time.Hour)
if err != nil || !created {
t.Fatalf("enqueue created=%t err=%v", created, err)
}
duplicate, created, err := store.EnqueueResearchTask(ctx, request, 24*time.Hour)
if err != nil || created || duplicate.ID != queued.ID {
t.Fatalf("dedupe created=%t original=%s duplicate=%s err=%v", created, queued.ID, duplicate.ID, err)
}
leased, ok, err := store.LeaseNextResearchTask(ctx, .65, 30*time.Minute)
if err != nil || !ok || leased.ID != queued.ID || leased.Status != "reserved" || leased.Attempts != 1 {
t.Fatalf("lease ok=%t task=%+v err=%v", ok, leased, err)
}
if err := store.MarkResearchTaskRunning(ctx, leased.ID); err != nil {
t.Fatal(err)
}
leased.EvidenceCount = 3
leased.ArticleCreated = true
leased.ArticleTitle = "ZFS Restore validieren"
leased.Outcome = "article_created"
if err := store.CompleteResearchTask(ctx, leased); err != nil {
t.Fatal(err)
}
completed, err := store.GetResearchTask(ctx, leased.ID)
if err != nil {
t.Fatal(err)
}
if completed.Status != "completed" || completed.EvidenceCount != 3 || !completed.ArticleCreated || completed.CompletedAt.IsZero() {
t.Fatalf("unexpected completed task: %+v", completed)
}
counts, err := store.ResearchTaskCounts(ctx)
if err != nil || counts["completed"] != 1 {
t.Fatalf("unexpected counts=%v err=%v", counts, err)
}
}
func TestResearchTaskFailureDefersUntilMaxAttempts(t *testing.T) {
store, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer store.Close()
ctx := context.Background()
queued, _, err := store.EnqueueResearchTask(ctx, model.ResearchTask{Topic: "Btrfs Timeline", Priority: .8, MaxAttempts: 2}, 0)
if err != nil {
t.Fatal(err)
}
leased, ok, err := store.LeaseNextResearchTask(ctx, 0, 30*time.Minute)
if err != nil || !ok {
t.Fatalf("lease ok=%t err=%v", ok, err)
}
leased.LastError = "temporary"
leased.Outcome = "failed"
if err := store.FailResearchTask(ctx, leased, time.Millisecond); err != nil {
t.Fatal(err)
}
first, err := store.GetResearchTask(ctx, queued.ID)
if err != nil || first.Status != "deferred" {
t.Fatalf("expected deferred task, got %+v err=%v", first, err)
}
time.Sleep(3 * time.Millisecond)
leased, ok, err = store.LeaseNextResearchTask(ctx, 0, 30*time.Minute)
if err != nil || !ok {
t.Fatalf("second lease ok=%t err=%v", ok, err)
}
leased.LastError = "permanent"
leased.Outcome = "failed"
if err := store.FailResearchTask(ctx, leased, 0); err != nil {
t.Fatal(err)
}
final, err := store.GetResearchTask(ctx, queued.ID)
if err != nil || final.Status != "failed" || final.Attempts != 2 {
t.Fatalf("expected failed task after max attempts, got %+v err=%v", final, err)
}
}
func TestResearchTaskConcurrentEnqueueDeduplicates(t *testing.T) {
store, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer store.Close()
ctx := context.Background()
request := model.ResearchTask{DedupeKey: "same-search", Topic: "Same SearXNG task", Questions: []string{"same question"}, Priority: .8}
var created atomic.Int64
var wg sync.WaitGroup
errs := make(chan error, 16)
for i := 0; i < 16; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, wasCreated, enqueueErr := store.EnqueueResearchTask(ctx, request, time.Hour)
if enqueueErr != nil {
errs <- enqueueErr
return
}
if wasCreated {
created.Add(1)
}
}()
}
wg.Wait()
close(errs)
for err := range errs {
t.Fatal(err)
}
if got := created.Load(); got != 1 {
t.Fatalf("expected exactly one created task, got %d", got)
}
tasks, err := store.ListResearchTasks(ctx, 100)
if err != nil {
t.Fatal(err)
}
if len(tasks) != 1 {
t.Fatalf("expected exactly one queued task, got %d: %+v", len(tasks), tasks)
}
}