package main import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" "time" "git.ipao.vip/rogee/go-sip/internal/contract" "github.com/google/uuid" amqp "github.com/rabbitmq/amqp091-go" ) func TestOneShotDispatchReceiptDoesNotEndCapture(t *testing.T) { data, err := loadDataset(testDataDir(t), testDispatcher) if err != nil { t.Fatal(err) } receipt := map[string]any{ "event_id": "event-once-1", "event_type": "call.execute", "dispatcher_id": data.dispatcherID, "tenant_id": data.tenantID, "issued_at": "2026-10-04T02:00:00Z", "payload": map[string]any{"status": "dispatched"}, } body, err := json.Marshal(receipt) if err != nil { t.Fatal(err) } if err := contract.ValidateCurrent("mq", body); err != nil { t.Fatal(err) } if status, err := verifyOneShotReceipt(body, data.dispatcherID, data.tenantID, "event-once-1"); err != nil || status != "dispatched" { t.Fatalf("expected dispatch receipt: %s %v", status, err) } if _, err := verifyOneShotReceipt(body, data.dispatcherID, data.tenantID, "different-event"); err == nil { t.Fatal("unrelated call receipt must remain unacknowledged") } receipt["payload"].(map[string]any)["status"] = "rejected" receipt["payload"].(map[string]any)["reason_code"] = nil receipt["payload"].(map[string]any)["reason_message"] = "Agent refused before outbound call" body, _ = json.Marshal(receipt) if err := contract.ValidateCurrent("mq", body); err != nil { t.Fatal(err) } if status, err := verifyOneShotReceipt(body, data.dispatcherID, data.tenantID, "event-once-1"); err != nil || status != "rejected" { t.Fatalf("definite no-dial rejection should be acknowledged without a final result: %s %v", status, err) } receipt["payload"].(map[string]any)["status"] = "unknown" body, _ = json.Marshal(receipt) if _, err := verifyOneShotReceipt(body, data.dispatcherID, data.tenantID, "event-once-1"); err == nil { t.Fatal("unknown receipt status cannot authorize completion") } } type testMQAck struct{ accepted, requeued int } func (a *testMQAck) Ack(uint64, bool) error { a.accepted++; return nil } func (a *testMQAck) Nack(uint64, bool, bool) error { a.requeued++; return nil } func (a *testMQAck) Reject(uint64, bool) error { a.requeued++; return nil } func TestRejectedOneShotReceiptIsSavedBeforeAckAndStopsWaiting(t *testing.T) { data, err := loadDataset(testDataDir(t), testDispatcher) if err != nil { t.Fatal(err) } body, err := json.Marshal(map[string]any{"event_id": "call-rejected", "event_type": "call.execute", "dispatcher_id": data.dispatcherID, "tenant_id": data.tenantID, "issued_at": "2026-10-04T02:00:00Z", "payload": map[string]any{"status": "rejected", "reason_code": nil, "reason_message": "Agent refused before outbound call"}}) if err != nil { t.Fatal(err) } ack := &testMQAck{} incoming := make(chan amqp.Delivery, 1) incoming <- amqp.Delivery{Body: body, Acknowledger: ack} consumer := &resultConsumer{deliveries: incoming} dir := t.TempDir() if err := os.Chmod(dir, 0700); err != nil { t.Fatal(err) } file := filepath.Join(dir, "result.json") _, err = consumer.wait(context.Background(), file, data.dispatcherID, data.tenantID, "call-rejected", "task-full", "15003164745", "trunk-mock") if err == nil || !strings.Contains(err.Error(), "rejected") || ack.accepted != 1 || ack.requeued != 0 { t.Fatalf("rejected receipt was not acknowledged as terminal no-dial: accepted=%d requeued=%d err=%v", ack.accepted, ack.requeued, err) } saved, err := os.ReadFile(file + ".rejected.json") if err != nil || !bytes.Equal(saved, body) { t.Fatalf("receipt not durably retained: %v", err) } if _, err := os.Stat(file); !os.IsNotExist(err) { t.Fatalf("false final result created: %v", err) } } func TestOneShotResultMustMatchSourceIdentityAndPersistBeforeReceipt(t *testing.T) { data, err := loadDataset(testDataDir(t), testDispatcher) if err != nil { t.Fatal(err) } sourceID := "event-once-1" resultID := expectedResultEventID(data.dispatcherID, sourceID) result := map[string]any{} example, err := os.ReadFile("../../../contracts/schema/examples/mq-result-uploaded.json") if err != nil { t.Fatal(err) } if err := json.Unmarshal(example, &result); err != nil { t.Fatal(err) } result["event_id"] = resultID result["dispatcher_id"] = data.dispatcherID payload := result["payload"].(map[string]any) payload["task_id"], payload["callee"], payload["trunk_id"] = "task-full", "15003164745", "trunk-mock" body, err := json.Marshal(result) if err != nil { t.Fatal(err) } if err := contract.ValidateCurrent("mq", body); err != nil { t.Fatal(err) } if _, err := summarizeOneShotResult(body, data.dispatcherID, sourceID, "task-full", "15003164745", "trunk-mock"); err != nil { t.Fatal(err) } if _, err := summarizeOneShotResult(body, data.dispatcherID, "different-event", "task-full", "15003164745", "trunk-mock"); err == nil { t.Fatal("unrelated final result must not be acknowledged") } if _, err := summarizeOneShotResult(body, data.dispatcherID, sourceID, "task-full", "15003164745", "different-trunk"); err == nil { t.Fatal("result from the wrong trunk must not be acknowledged") } dir := t.TempDir() if err := os.Chmod(dir, 0700); err != nil { t.Fatal(err) } file := filepath.Join(dir, "one-result.json") if err := persistOneShotResult(file, body); err != nil { t.Fatal(err) } stored, err := os.ReadFile(file) if err != nil || !bytes.Equal(stored, body) { t.Fatal("final result not durably saved") } if info, err := os.Stat(file); err != nil || info.Mode().Perm() != 0600 { t.Fatal("result must remain private") } if err := persistOneShotResult(file, body); err == nil { t.Fatal("existing final result must not be overwritten") } } func TestSaaSMockAwaitsExactResultBeforeStoppingCapture(t *testing.T) { url := os.Getenv("SAAS_MOCK_TEST_BROKER_URL") if url == "" { t.Skip("requires isolated ephemeral dedicated-vhost RabbitMQ") } data, err := loadDataset(testDataDir(t), testDispatcher) if err != nil { t.Fatal(err) } if err := provisionMQ(url, data); err != nil { t.Fatal(err) } eventID := "test-" + uuid.NewString() result, err := os.ReadFile("../../../contracts/schema/examples/mq-result-uploaded.json") if err != nil { t.Fatal(err) } var message map[string]any if err := json.Unmarshal(result, &message); err != nil { t.Fatal(err) } message["event_id"] = expectedResultEventID(data.dispatcherID, eventID) message["dispatcher_id"] = data.dispatcherID payload := message["payload"].(map[string]any) payload["task_id"], payload["callee"], payload["trunk_id"] = "task-full", "15003164745", "trunk-mock" result, err = json.Marshal(message) if err != nil { t.Fatal(err) } receipt, err := json.Marshal(map[string]any{"event_id": eventID, "event_type": "call.execute", "dispatcher_id": data.dispatcherID, "tenant_id": data.tenantID, "issued_at": "2026-10-04T02:00:00Z", "payload": map[string]any{"status": "dispatched"}}) if err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() producer := make(chan error, 1) go func() { conn, err := amqp.Dial(url) if err != nil { producer <- err return } defer conn.Close() ch, err := conn.Channel() if err != nil { producer <- err return } defer ch.Close() queue := "agent-call.d." + testDispatcher + ".task.task-full.v1" for { command, found, err := ch.Get(queue, false) if err != nil { producer <- err return } if found { if !bytes.Contains(command.Body, []byte(eventID)) { producer <- fmt.Errorf("wrong command received") return } if err := verifyExecutePayload(command.Body, "task-full", "15003164745"); err != nil { producer <- err return } if err := command.Ack(false); err != nil { producer <- err return } if err := ch.PublishWithContext(ctx, "agent-call.saas.v1", "d."+testDispatcher+".out", true, false, amqp.Publishing{DeliveryMode: amqp.Persistent, ContentType: "application/json", Body: receipt}); err != nil { producer <- err return } err = ch.PublishWithContext(ctx, "agent-call.saas.v1", "d."+testDispatcher+".out", true, false, amqp.Publishing{DeliveryMode: amqp.Persistent, ContentType: "application/json", Body: result}) producer <- err return } select { case <-ctx.Done(): producer <- ctx.Err() return case <-time.After(20 * time.Millisecond): } } }() dir := t.TempDir() if err := os.Chmod(dir, 0700); err != nil { t.Fatal(err) } file := filepath.Join(dir, "final.json") observed, err := publishAndAwait(ctx, url, data, eventID, "task-full", "15003164745", file, time.Date(2026, 10, 4, 2, 0, 0, 0, time.UTC)) if err != nil { t.Fatal(err) } if err := <-producer; err != nil { t.Fatal(err) } if observed.Outcome != "answered" || observed.RecordingStatus != "uploaded" { t.Fatalf("result not acknowledged with real-looking recording: %+v", observed) } if saved, err := os.ReadFile(file); err != nil || !bytes.Equal(saved, result) { t.Fatalf("result was not saved before broker receipt: err=%v", err) } if saved, err := os.ReadFile(file + ".receipt.json"); err != nil || !bytes.Equal(saved, receipt) { t.Fatalf("issuance receipt was not persisted before acknowledgment: err=%v", err) } } func TestSaaSMockPublishesOneCommandOnProvisionedRabbitMQ(t *testing.T) { url := os.Getenv("SAAS_MOCK_TEST_BROKER_URL") if url == "" { t.Skip("requires an isolated ephemeral dedicated-vhost RabbitMQ") } data, err := loadDataset(testDataDir(t), testDispatcher) if err != nil { t.Fatal(err) } if err := provisionMQ(url, data); err != nil { t.Fatal(err) } found := false for _, discovered := range data.discovery { found = found || discovered.TaskID == "task-full" } if !found { t.Fatal("fixture task-full was not preprovisioned") } conn, err := amqp.Dial(url) if err != nil { t.Fatal(err) } defer conn.Close() ch, err := conn.Channel() if err != nil { t.Fatal(err) } defer ch.Close() eventID := "test-" + uuid.NewString() at := time.Date(2026, 10, 4, 2, 0, 0, 0, time.UTC) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := publishExecuteAt(ctx, url, data, eventID, "task-full", "15003164745", at); err != nil { t.Fatal(err) } queue := "agent-call.d." + testDispatcher + ".task.task-full.v1" message, ok, err := ch.Get(queue, false) if err != nil || !ok { t.Fatalf("one command must reach exactly its preprovisioned task queue: present=%v err=%v", ok, err) } if err := contract.ValidateCurrent("mq", message.Body); err != nil || !strings.Contains(string(message.Body), eventID) || message.DeliveryMode != amqp.Persistent { t.Fatalf("broker delivered wrong command identity or persistence: err=%v", err) } if err := verifyExecutePayload(message.Body, "task-full", "15003164745"); err != nil { t.Fatal(err) } if err := message.Ack(false); err != nil { t.Fatal(err) } if extra, ok, err := ch.Get(queue, false); err != nil || ok { if ok { _ = extra.Nack(false, true) } t.Fatalf("one publish created a second command: present=%v err=%v", ok, err) } } func TestSaaSMockBuildsOneApprovedCommandForTheBoundTask(t *testing.T) { data, err := loadDataset(testDataDir(t), testDispatcher) if err != nil { t.Fatal(err) } inside := time.Date(2026, 10, 4, 2, 0, 0, 0, time.UTC) // Shanghai 10:00 key, body, err := buildExecute(data, "event-once-1", "task-full", "15003164745", inside) if err != nil || key != "d."+testDispatcher+".task.task-full.in" { t.Fatalf("SaaS command must target only the assigned Dispatcher/task: key=%q err=%v", key, err) } if err := contract.ValidateCurrent("mq", body); err != nil { t.Fatalf("call command violates the formal contract: %v", err) } if err := verifyExecutePayload(body, "task-full", "15003164745"); err != nil { t.Fatal(err) } var event struct { EventID string `json:"event_id"` Type string `json:"event_type"` DispatcherID string `json:"dispatcher_id"` TenantID int64 `json:"tenant_id"` Payload struct { TaskID string `json:"task_id"` Callee string `json:"callee"` } `json:"payload"` } if err := json.Unmarshal(body, &event); err != nil || event.EventID != "event-once-1" || event.Type != "call.execute" || event.DispatcherID != testDispatcher || event.TenantID != 1001 || event.Payload.TaskID != "task-full" || event.Payload.Callee != "15003164745" || strings.Contains(string(body), "trunk-mock") { t.Fatalf("SaaS must not leak a trunk/caller/AI override into the execute command: %+v err=%v", event, err) } for _, at := range []time.Time{inside.Add(-2 * time.Hour), inside, inside.Add(10 * time.Hour)} { for _, number := range []string{"15803300952", "13900000000"} { _, eventBody, err := buildExecute(data, "saas-event-"+number, "task-full", number, at) if err != nil || contract.ValidateCurrent("mq", eventBody) != nil || !strings.Contains(string(eventBody), `"callee":"`+number+`"`) { t.Fatalf("valid SaaS task command was changed or rejected: %q at=%v err=%v", number, at, err) } } } for _, test := range []struct { id, task, callee string at time.Time }{ {"../event", "task-full", "15003164745", inside}, {"event-2", "missing", "15003164745", inside}, {"event-3", "task-full", "abc", inside}, {"event-4", "task-full", strings.Repeat("1", 33), inside}, } { if _, _, err := buildExecute(data, test.id, test.task, test.callee, test.at); err == nil { t.Fatalf("invalid command was allowed: event=%q task=%q", test.id, test.task) } } }