230 lines
10 KiB
Go
230 lines
10 KiB
Go
package store
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
"git.ipao.vip/rogee/go-sip/internal/tenant"
|
|
)
|
|
|
|
func currentResultPayload(t *testing.T) []byte {
|
|
t.Helper()
|
|
payload := map[string]any{
|
|
"task_id": "task-asr", "caller_profile_id": currentStoreSnapshot(t).Task.CallerProfileID,
|
|
"callee": "15003164745", "trunk_id": "trunk-mock",
|
|
"started_at": "2026-09-21T01:30:01Z", "ended_at": "2026-09-21T01:30:06Z", "duration_ms": 5000,
|
|
"outcome": "no_answer", "reason_code": 486, "reason_message": "busy",
|
|
"transcript": []any{}, "opt_out": false, "recording": map[string]any{},
|
|
}
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func currentResultCall(t *testing.T, s *CurrentStore, id string, finish bool) CurrentExecuteCommand {
|
|
t.Helper()
|
|
cmd := currentCall(id)
|
|
if _, _, err := s.RecordExecute(cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ReserveExecute(cmd.DispatcherID, cmd.EventID, currentReservation(), time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.MarkExecuteDispatched(cmd.DispatcherID, cmd.EventID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if finish {
|
|
if err := s.FinishExecute(cmd.DispatcherID, cmd.EventID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func TestCurrentFinalResultNeedsConfirmedEndAndRemainsExactlyOne(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
cmd := currentResultCall(t, s, "result-call-1", false)
|
|
payload := currentResultPayload(t)
|
|
if _, _, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, payload); err == nil {
|
|
t.Fatal("Agent report released a call before confirmed hangup")
|
|
}
|
|
if err := s.FinishExecute(cmd.DispatcherID, cmd.EventID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if occupancy, err := s.TrunkOccupancy(cmd.DispatcherID); err != nil || occupancy["trunk-mock"] != 0 {
|
|
t.Fatalf("confirmed end did not release the call before OSS/result delivery: %+v err=%v", occupancy, err)
|
|
}
|
|
event, created, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, payload)
|
|
if err != nil || !created || event.EventType != "call.execute.result" || event.EventID == cmd.EventID {
|
|
t.Fatalf("exact final result not persisted: event=%+v created=%t err=%v", event, created, err)
|
|
}
|
|
route, err := tenant.CurrentResultRoute(cmd.DispatcherID)
|
|
if err != nil || event.RoutingKey != route.BindingKey || contract.ValidateCurrent("mq", event.Body) != nil {
|
|
t.Fatalf("final result was not Schema-valid or routed to shared SaaS queue: route=%+v err=%v", route, err)
|
|
}
|
|
outbox, err := s.ListPendingOutbox(cmd.DispatcherID)
|
|
if err != nil || len(outbox) != 2 || !bytes.Equal(outbox[1].Body, event.Body) {
|
|
t.Fatalf("call ack or final result was lost: outbox=%+v err=%v", outbox, err)
|
|
}
|
|
repeated, created, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, payload)
|
|
if err != nil || created || !bytes.Equal(repeated.Body, event.Body) {
|
|
t.Fatalf("redelivered identical result inserted twice: created=%t err=%v", created, err)
|
|
}
|
|
for _, id := range []string{cmd.EventID, event.EventID} {
|
|
if err := s.MarkOutboxConfirmed(cmd.DispatcherID, id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, created, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, payload); err != nil || created {
|
|
t.Fatalf("confirmed outbox row was replaced instead of retained: created=%t err=%v", created, err)
|
|
}
|
|
if pending, err := s.ListPendingOutbox(cmd.DispatcherID); err != nil || len(pending) != 0 {
|
|
t.Fatalf("confirmed result was republished: pending=%+v err=%v", pending, err)
|
|
}
|
|
var total int
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM dispatcher_outbox WHERE dispatcher_id=?`, cmd.DispatcherID).Scan(&total); err != nil || total != 2 {
|
|
t.Fatalf("result history disappeared or multiplied: total=%d err=%v", total, err)
|
|
}
|
|
var claim map[string]any
|
|
if err := json.Unmarshal(payload, &claim); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
claim["reason_message"] = "different result after receipt loss"
|
|
changed, _ := json.Marshal(claim)
|
|
if _, _, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, changed); err == nil {
|
|
t.Fatal("same call identity accepted a second, conflicting final result")
|
|
}
|
|
}
|
|
|
|
func TestCurrentFinalResultAfterEndRacedOriginateAck(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
cmd := currentCall("result-ended-before-ack")
|
|
if _, _, err := s.RecordExecute(cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ReserveExecute(cmd.DispatcherID, cmd.EventID, currentReservation(), time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.FinishExecute(cmd.DispatcherID, cmd.EventID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, created, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, currentResultPayload(t))
|
|
if err != nil || !created {
|
|
t.Fatalf("confirmed early end could not produce its sole final result: created=%t err=%v", created, err)
|
|
}
|
|
if err := s.MarkExecuteDispatched(cmd.DispatcherID, cmd.EventID); err != nil {
|
|
t.Fatalf("late originate response conflicted with the confirmed result: %v", err)
|
|
}
|
|
outbox, err := s.ListPendingOutbox(cmd.DispatcherID)
|
|
if err != nil || len(outbox) != 2 || outbox[0].EventID != cmd.EventID || outbox[1].EventID != result.EventID {
|
|
t.Fatalf("early end lost its acknowledgment or produced duplicate results: outbox=%+v err=%v", outbox, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentFinalResultRejectsDifferentBoundTaskCallOrTrunk(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
cmd := currentResultCall(t, s, "result-bound-1", true)
|
|
for _, change := range []struct{ field, value string }{
|
|
{"call_id", "other-call"}, {"task_id", "other-task"}, {"callee", "15830461047"}, {"trunk_id", "other-trunk"}, {"caller_profile_id", "other-caller"},
|
|
} {
|
|
var claim map[string]any
|
|
if err := json.Unmarshal(currentResultPayload(t), &claim); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
claim[change.field] = change.value
|
|
raw, _ := json.Marshal(claim)
|
|
if _, _, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, raw); err == nil {
|
|
t.Fatalf("unbound %s was accepted in final result", change.field)
|
|
}
|
|
}
|
|
if outbox, err := s.ListPendingOutbox(cmd.DispatcherID); err != nil || len(outbox) != 1 {
|
|
t.Fatalf("invalid result escaped into SaaS outbox: outbox=%+v err=%v", outbox, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentFinalResultRejectsUnverifiedOSSAssetAndInventedState(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
cmd := currentResultCall(t, s, "result-unverified-1", true)
|
|
var claim map[string]any
|
|
if err := json.Unmarshal(currentResultPayload(t), &claim); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
claim["recording"] = map[string]any{"status": "uploaded", "bucket": "mock-bucket", "object_key": "tenant/rec.wav", "format": "wav", "channels": 1, "sample_rate_hz": 16000, "duration_ms": 3000, "size_bytes": 100, "checksum_sha256": strings.Repeat("a", 64)}
|
|
uploaded, _ := json.Marshal(claim)
|
|
if _, _, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, uploaded); !errors.Is(err, ErrCurrentUploadUnverified) {
|
|
t.Fatalf("Agent invented an ungranted uploaded asset: %v", err)
|
|
}
|
|
claim["recording"] = map[string]any{"status": "unavailable"}
|
|
invented, _ := json.Marshal(claim)
|
|
if _, _, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, invented); err == nil {
|
|
t.Fatal("unapproved recording status bypassed current MQ Schema")
|
|
}
|
|
if outbox, err := s.ListPendingOutbox(cmd.DispatcherID); err != nil || len(outbox) != 1 {
|
|
t.Fatalf("unverified asset produced a result: outbox=%+v err=%v", outbox, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentFinalResultOutboxFailureRollsBackAndCanResume(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
cmd := currentResultCall(t, s, "result-fault-1", true)
|
|
if _, err := s.db.Exec(`CREATE TRIGGER fail_final BEFORE INSERT ON dispatcher_outbox WHEN NEW.event_type='call.execute.result' BEGIN SELECT RAISE(ABORT,'injected final outbox failure'); END`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, currentResultPayload(t)); err == nil {
|
|
t.Fatal("failed SQLite outbox insert was treated as delivered")
|
|
}
|
|
if outbox, err := s.ListPendingOutbox(cmd.DispatcherID); err != nil || len(outbox) != 1 {
|
|
t.Fatalf("partial result leaked outside failed transaction: outbox=%+v err=%v", outbox, err)
|
|
}
|
|
if _, err := s.db.Exec(`DROP TRIGGER fail_final`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, created, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, currentResultPayload(t)); err != nil || !created {
|
|
t.Fatalf("original result was not recoverable after SQLite failure: created=%t err=%v", created, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentFinalResultRestartResumesSameOutboxBody(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
cmd := currentResultCall(t, s, "result-restart-1", true)
|
|
original, created, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, currentResultPayload(t))
|
|
if err != nil || !created {
|
|
t.Fatalf("persist original result: created=%t err=%v", created, err)
|
|
}
|
|
var seq int
|
|
var databaseName, databasePath string
|
|
if err := s.db.QueryRow(`PRAGMA database_list`).Scan(&seq, &databaseName, &databasePath); err != nil || databasePath == "" {
|
|
t.Fatalf("inspect private SQLite fixture: name=%q err=%v", databaseName, err)
|
|
}
|
|
if err := s.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restarted, err := OpenCurrent(databasePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer restarted.Close()
|
|
pending, err := restarted.ListPendingOutbox(cmd.DispatcherID)
|
|
if err != nil || len(pending) != 2 || !bytes.Equal(pending[1].Body, original.Body) {
|
|
t.Fatalf("restart lost the undelivered original MQ result: events=%+v err=%v", pending, err)
|
|
}
|
|
repeated, created, err := restarted.RecordCallResult(cmd.DispatcherID, cmd.EventID, currentResultPayload(t))
|
|
if err != nil || created || repeated.EventID != original.EventID || !bytes.Equal(repeated.Body, original.Body) {
|
|
t.Fatalf("retry created a second result after restart: created=%t err=%v", created, err)
|
|
}
|
|
if err := restarted.MarkOutboxConfirmed(cmd.DispatcherID, original.EventID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pending, err = restarted.ListPendingOutbox(cmd.DispatcherID)
|
|
if err != nil || len(pending) != 1 || pending[0].EventType != "call.execute" {
|
|
t.Fatalf("confirmed result was redelivered as a new event: events=%+v err=%v", pending, err)
|
|
}
|
|
}
|