149 lines
5.6 KiB
Go
149 lines
5.6 KiB
Go
package store
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPersistLocalCommandIsAtomicAndIdempotent(t *testing.T) {
|
|
st, err := Open(filepath.Join(t.TempDir(), "dispatcher.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
|
|
bundle := localConfigBundle("dispatcher-1", "tenant-id-1", "tenant-key-1", "task-1", 1, 1, 1, 2, 2, time.Now().UTC())
|
|
if err := st.SaveLocalConfigBundle(bundle); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
record := localAdmissionCommand("command-1", "execution-1", "task-1", bundle, "dispatcher-1", "tenant-id-1", "tenant-key-1")
|
|
seedLocalCommandAssignments(t, st, record)
|
|
record.Exchange = "agent-call.saas.v3"
|
|
record.RoutingKey = "d.dispatcher-1.out"
|
|
duplicate, err := st.PersistLocalCommand(record)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if duplicate {
|
|
t.Fatal("first command was reported as a duplicate")
|
|
}
|
|
|
|
var inboxStatus string
|
|
var receiptCount, outboxCount int
|
|
if err := st.db.QueryRow(`SELECT status FROM inbox WHERE tenant_id=? AND command_id=?`, record.TenantID, record.CommandID).Scan(&inboxStatus); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inboxStatus != "persisted" {
|
|
t.Fatalf("inbox status = %q, want persisted", inboxStatus)
|
|
}
|
|
if err := st.db.QueryRow(`SELECT COUNT(*) FROM mq_command_receipts WHERE tenant_id=? AND command_id=? AND response_id=?`, record.TenantID, record.CommandID, record.ReceiptID).Scan(&receiptCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.db.QueryRow(`SELECT COUNT(*) FROM outbox WHERE event_id=? AND status='pending'`, record.ReceiptID).Scan(&outboxCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if receiptCount != 1 || outboxCount != 1 {
|
|
t.Fatalf("receipt links=%d outbox rows=%d, want 1 each", receiptCount, outboxCount)
|
|
}
|
|
|
|
first, err := st.ClaimOutbox(1)
|
|
if err != nil || len(first) != 1 {
|
|
t.Fatalf("claim first receipt: records=%d err=%v", len(first), err)
|
|
}
|
|
if err := st.MarkOutboxPublished(first[0].ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
duplicate, err = st.PersistLocalCommand(record)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !duplicate {
|
|
t.Fatal("redelivered command was not identified as a duplicate")
|
|
}
|
|
second, err := st.ClaimOutbox(1)
|
|
if err != nil || len(second) != 1 {
|
|
t.Fatalf("claim recovered receipt: records=%d err=%v", len(second), err)
|
|
}
|
|
if second[0].EventID != first[0].EventID || string(second[0].Body) != string(first[0].Body) {
|
|
t.Fatalf("duplicate changed receipt: first=%+v second=%+v", first[0], second[0])
|
|
}
|
|
|
|
record.Body = []byte(`{"command_id":"command-1","callee":"different"}`)
|
|
if _, err := st.PersistLocalCommand(record); !errors.Is(err, ErrCommandConflict) {
|
|
t.Fatalf("same command ID with different body error = %v, want ErrCommandConflict", err)
|
|
}
|
|
}
|
|
|
|
func TestEnqueueLocalEventIsIdempotentAndRejectsContentConflict(t *testing.T) {
|
|
st, err := Open(filepath.Join(t.TempDir(), "dispatcher.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
|
|
event := LocalEventRecord{
|
|
EventID: "call-result-1", TenantKey: "tenant-key-1", Exchange: "agent-call.saas.v3",
|
|
RoutingKey: "d.dispatcher-1.out", Body: []byte(`{"event_id":"call-result-1"}`),
|
|
}
|
|
duplicate, err := st.EnqueueLocalEvent(event)
|
|
if err != nil || duplicate {
|
|
t.Fatalf("first enqueue duplicate=%v err=%v", duplicate, err)
|
|
}
|
|
duplicate, err = st.EnqueueLocalEvent(event)
|
|
if err != nil || !duplicate {
|
|
t.Fatalf("same event enqueue duplicate=%v err=%v", duplicate, err)
|
|
}
|
|
var count int
|
|
if err := st.db.QueryRow(`SELECT COUNT(*) FROM outbox WHERE event_id=?`, event.EventID).Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("outbox rows = %d, want 1", count)
|
|
}
|
|
event.Body = []byte(`{"event_id":"call-result-1","different":true}`)
|
|
if _, err := st.EnqueueLocalEvent(event); !errors.Is(err, ErrCommandConflict) {
|
|
t.Fatalf("same event ID with different body error = %v, want ErrCommandConflict", err)
|
|
}
|
|
}
|
|
|
|
func TestVerifyLocalAcceptedCommandBindsCallResultToReceipt(t *testing.T) {
|
|
st, err := Open(filepath.Join(t.TempDir(), "dispatcher.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
|
|
bundle := localConfigBundle("dispatcher-a", "tenant-a", "tenant-a", "task-a", 1, 1, 1, 2, 2, time.Now().UTC())
|
|
if err := st.SaveLocalConfigBundle(bundle); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
record := localAdmissionCommand("command-a", "execution-a", "task-a", bundle, "dispatcher-a", "tenant-a", "tenant-a")
|
|
seedLocalCommandAssignments(t, st, record)
|
|
record.Body = []byte(`{"command_id":"command-a","command_type":"call.execute","tenant_id":"tenant-a","tenant_key":"tenant-a","payload":{"task_id":"task-a"}}`)
|
|
record.ReceiptID = "receipt-a"
|
|
record.Exchange = "agent-call.saas.v3"
|
|
record.RoutingKey = "d.dispatcher-a.out"
|
|
record.ReceiptBody = []byte(`{"event_type":"command.result","tenant_id":"tenant-a","tenant_key":"tenant-a","payload":{"command_id":"command-a","command_type":"call.execute","status":"accepted","execution_id":"execution-a"}}`)
|
|
_, err = st.PersistLocalCommand(record)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.VerifyLocalAcceptedCommand("tenant-a", "tenant-a", "command-a", "execution-a", "task-a"); err != nil {
|
|
t.Fatalf("valid result binding: %v", err)
|
|
}
|
|
for _, tc := range []struct {
|
|
tenantID, tenantKey, commandID, executionID, taskID string
|
|
}{
|
|
{"tenant-b", "tenant-a", "command-a", "execution-a", "task-a"},
|
|
{"tenant-a", "tenant-b", "command-a", "execution-a", "task-a"},
|
|
{"tenant-a", "tenant-a", "command-a", "execution-b", "task-a"},
|
|
{"tenant-a", "tenant-a", "command-a", "execution-a", "task-b"},
|
|
} {
|
|
if err := st.VerifyLocalAcceptedCommand(tc.tenantID, tc.tenantKey, tc.commandID, tc.executionID, tc.taskID); !errors.Is(err, ErrCommandConflict) {
|
|
t.Fatalf("invalid result binding %+v error = %v, want ErrCommandConflict", tc, err)
|
|
}
|
|
}
|
|
}
|