150 lines
4.3 KiB
Go
150 lines
4.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"dana/internal/onec"
|
|
)
|
|
|
|
// Run only against the disposable DB created by the autonomy acceptance command.
|
|
func TestAutonomousBookingLifecycle(t *testing.T) {
|
|
dsn := os.Getenv("DANA_AUTONOMY_TEST_DSN")
|
|
if dsn == "" {
|
|
t.Skip("requires disposable autonomy database")
|
|
}
|
|
ctx := context.Background()
|
|
s, err := New(ctx, dsn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
if err = s.Migrate(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
must := func(err error) {
|
|
t.Helper()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
doctor := fmt.Sprintf("autonomy-%d", time.Now().UnixNano())
|
|
start := time.Now().UTC().Truncate(time.Hour).Add(48 * time.Hour)
|
|
end := start.Add(30 * time.Minute)
|
|
slot := onec.Slot{DoctorID: doctor, BranchID: "branch", Start: start, End: end, Status: "free"}
|
|
from, to := start.Add(-time.Hour), end.Add(time.Hour)
|
|
must(s.AcceptOnecSlots(ctx, from, to, []onec.Slot{slot}))
|
|
if !s.OnecFresh(ctx, time.Hour) {
|
|
t.Fatal("push did not mark freshness")
|
|
}
|
|
var wins atomic.Int32
|
|
var winner OnecAppointment
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 12; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
a, created, err := s.BeginOnecAppointment(ctx, OnecAppointment{RequestID: fmt.Sprintf("%s-%d", doctor, i), DoctorOnec: doctor, BranchID: "branch", Start: start, Patient: "test", Phone: "test", Comment: "test no calls"})
|
|
if errors.Is(err, ErrOnecSlotBusy) {
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
if !created {
|
|
t.Error("unexpected duplicate")
|
|
return
|
|
}
|
|
wins.Add(1)
|
|
mu.Lock()
|
|
winner = a
|
|
mu.Unlock()
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
if wins.Load() != 1 {
|
|
t.Fatalf("concurrent reservation winners=%d", wins.Load())
|
|
}
|
|
assertFree := func(n int) {
|
|
t.Helper()
|
|
v, err := s.FreeOnecSlots(ctx, doctor, from, to)
|
|
must(err)
|
|
if len(v) != n {
|
|
t.Fatalf("free slots=%d want %d", len(v), n)
|
|
}
|
|
}
|
|
assertFree(0)
|
|
same, created, err := s.BeginOnecAppointment(ctx, OnecAppointment{RequestID: winner.RequestID, DoctorOnec: doctor, Start: start})
|
|
must(err)
|
|
if created || same.ID != winner.ID {
|
|
t.Fatal("request retry duplicated booking")
|
|
}
|
|
must(s.AcceptOnecSlots(ctx, from, to, []onec.Slot{slot}))
|
|
assertFree(0) // stale free snapshot cannot reopen pending
|
|
pending, err := s.PendingOnecAppointments(ctx, 50)
|
|
must(err)
|
|
found := false
|
|
for _, p := range pending {
|
|
if p.ID == winner.ID {
|
|
found = true
|
|
if p.Start.Format("-07:00") != "+03:00" || p.Comment != "test no calls" {
|
|
t.Fatalf("bad pull fields: %+v", p)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("pending booking missing")
|
|
}
|
|
uuid := "00000000-0000-0000-0000-000000000001"
|
|
must(s.ConfirmPendingAppointment(ctx, winner.ID, uuid, &end))
|
|
must(s.ConfirmPendingAppointment(ctx, winner.ID, uuid, &end))
|
|
if !errors.Is(s.ConfirmPendingAppointment(ctx, winner.ID, "different", &end), ErrOnecTransition) {
|
|
t.Fatal("mismatched confirm accepted")
|
|
}
|
|
if !errors.Is(s.RejectPendingAppointment(ctx, winner.ID, "late reject"), ErrOnecTransition) {
|
|
t.Fatal("late reject accepted")
|
|
}
|
|
must(s.AcceptOnecSlots(ctx, from, to, []onec.Slot{slot}))
|
|
assertFree(0) // booked also survives full Push
|
|
must(s.QueueOnecCommand(ctx, winner.ID, "link_bitrix", "123", ""))
|
|
must(s.QueueOnecCommand(ctx, winner.ID, "link_bitrix", "123", ""))
|
|
commands, err := s.PendingOnecCommands(ctx)
|
|
must(err)
|
|
if len(commands) != 1 {
|
|
t.Fatal("duplicate link commands")
|
|
}
|
|
must(s.AckOnecCommand(ctx, commands[0].ID, false, "retry"))
|
|
commands, err = s.PendingOnecCommands(ctx)
|
|
must(err)
|
|
if len(commands) != 1 {
|
|
t.Fatal("failed ACK lost command")
|
|
}
|
|
must(s.AckOnecCommand(ctx, commands[0].ID, true, ""))
|
|
must(s.AckOnecCommand(ctx, commands[0].ID, true, ""))
|
|
must(s.QueueOnecCommand(ctx, winner.ID, "cancel", "", "cancelled"))
|
|
assertFree(0)
|
|
commands, err = s.PendingOnecCommands(ctx)
|
|
must(err)
|
|
if len(commands) != 1 || commands[0].Kind != "cancel" {
|
|
t.Fatal("cancel not queued")
|
|
}
|
|
must(s.AckOnecCommand(ctx, commands[0].ID, true, ""))
|
|
assertFree(1)
|
|
_, _, known, err := s.SetOnecAppointmentStatus(ctx, uuid, "booked")
|
|
must(err)
|
|
if known {
|
|
t.Fatal("delayed booked hook resurrected cancellation")
|
|
}
|
|
assertFree(1)
|
|
must(s.AcceptOnecSlots(ctx, from, to, []onec.Slot{}))
|
|
assertFree(0) // explicit empty horizon clears old shifts
|
|
}
|