146 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|