154 lines
6.5 KiB
Go
154 lines
6.5 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func localTerminalFixture() LocalCallTerminal {
|
|
return LocalCallTerminal{
|
|
ExecutionID: "execution-a", CallID: "call-a", Source: "mock_agent",
|
|
StartedAt: time.Date(2026, 9, 21, 2, 0, 0, 0, time.UTC),
|
|
EndedAt: time.Date(2026, 9, 21, 2, 1, 0, 0, time.UTC),
|
|
Outcome: "no_answer", ReasonCode: "no_answer",
|
|
}
|
|
}
|
|
|
|
func localFinalEventFixture() LocalEventRecord {
|
|
return LocalEventRecord{
|
|
EventID: "result-a", TenantKey: "tenant-key-a",
|
|
Exchange: "sip.results.v3", RoutingKey: "d.d-1.out", Body: []byte(`{"event_type":"call.result"}`),
|
|
}
|
|
}
|
|
|
|
func assertLocalTerminalQuota(t *testing.T, st *Store, wantReserved, wantUnknown int, wantState string) {
|
|
t.Helper()
|
|
var reserved, unknown int
|
|
var state string
|
|
if err := st.db.QueryRow(`SELECT q.reserved_value,q.unknown_value,r.state FROM reservations r
|
|
JOIN quotas q ON q.scope=? WHERE r.execution_id='execution-a'`,
|
|
LocalTenantQuotaScope("d-1", "tenant-a")).Scan(&reserved, &unknown, &state); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reserved != wantReserved || unknown != wantUnknown || state != wantState {
|
|
t.Fatalf("quota reserved=%d unknown=%d reservation=%s; want %d/%d/%s", reserved, unknown, state, wantReserved, wantUnknown, wantState)
|
|
}
|
|
}
|
|
|
|
func TestLocalTerminalReleaseAndFinalEventAreAtomicAndUnique(t *testing.T) {
|
|
st, _ := readyLocalOrigination(t)
|
|
if err := st.ClaimLocalOrigination("d-1", "execution-a", "agent-1", localTestOriginationBinding(t, st, "execution-a"), localTerminalFixture().StartedAt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fact, event := localTerminalFixture(), localFinalEventFixture()
|
|
if err := st.RecordLocalCallTerminal(fact, &event); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertLocalTerminalQuota(t, st, 0, 0, "released")
|
|
var status, eventID string
|
|
if err := st.db.QueryRow(`SELECT t.status,f.result_event_id FROM tasks t JOIN local_v01_call_terminals f ON f.execution_id=t.execution_id WHERE t.execution_id='execution-a'`).Scan(&status, &eventID); err != nil || status != "finished" || eventID != event.EventID {
|
|
t.Fatalf("finished terminal status=%q event=%q err=%v", status, eventID, err)
|
|
}
|
|
if err := st.RecordLocalCallTerminal(fact, &event); err != nil {
|
|
t.Fatalf("identical confirmed terminal replay: %v", err)
|
|
}
|
|
if err := st.EnqueueLocalFinalEvent(fact.ExecutionID, fact.CallID, event); err != nil {
|
|
t.Fatalf("identical final event replay: %v", err)
|
|
}
|
|
changed := event
|
|
changed.EventID = "result-b"
|
|
if err := st.EnqueueLocalFinalEvent(fact.ExecutionID, fact.CallID, changed); !errors.Is(err, ErrCommandConflict) {
|
|
t.Fatalf("second final event=%v", err)
|
|
}
|
|
changed = event
|
|
changed.Body = []byte(`{"event_type":"different"}`)
|
|
if err := st.EnqueueLocalFinalEvent(fact.ExecutionID, fact.CallID, changed); !errors.Is(err, ErrCommandConflict) {
|
|
t.Fatalf("conflicting final body=%v", err)
|
|
}
|
|
changedFact := fact
|
|
changedFact.EndedAt = changedFact.EndedAt.Add(time.Second)
|
|
if err := st.RecordLocalCallTerminal(changedFact, &event); !errors.Is(err, ErrCommandConflict) {
|
|
t.Fatalf("conflicting terminal fact=%v", err)
|
|
}
|
|
var count int
|
|
if err := st.db.QueryRow(`SELECT COUNT(*) FROM outbox WHERE event_id='result-a'`).Scan(&count); err != nil || count != 1 {
|
|
t.Fatalf("unique outbox row count=%d err=%v", count, err)
|
|
}
|
|
}
|
|
|
|
func TestLocalTerminalOutboxFailureRollsBackQuotaAndTerminal(t *testing.T) {
|
|
st, _ := readyLocalOrigination(t)
|
|
if err := st.ClaimLocalOrigination("d-1", "execution-a", "agent-1", localTestOriginationBinding(t, st, "execution-a"), localTerminalFixture().StartedAt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
event := localFinalEventFixture()
|
|
prior := event
|
|
prior.Body = []byte(`{"event_type":"unrelated"}`)
|
|
if _, err := st.EnqueueLocalEvent(prior); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.RecordLocalCallTerminal(localTerminalFixture(), &event); !errors.Is(err, ErrCommandConflict) {
|
|
t.Fatalf("conflicting result did not roll back terminal: %v", err)
|
|
}
|
|
assertLocalTerminalQuota(t, st, 1, 0, "held")
|
|
var count int
|
|
if err := st.db.QueryRow(`SELECT COUNT(*) FROM local_v01_call_terminals WHERE execution_id='execution-a'`).Scan(&count); err != nil || count != 0 {
|
|
t.Fatalf("terminal persisted despite outbox conflict: count=%d err=%v", count, err)
|
|
}
|
|
event.EventID = "result-b"
|
|
if err := st.RecordLocalCallTerminal(localTerminalFixture(), &event); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertLocalTerminalQuota(t, st, 0, 0, "released")
|
|
}
|
|
|
|
func TestLocalTerminalReleasesUnknownQuotaBeforeRecordingAndFinalResult(t *testing.T) {
|
|
st, record := readyLocalOrigination(t)
|
|
if err := st.ClaimLocalOrigination("d-1", "execution-a", "agent-1", localTestOriginationBinding(t, st, "execution-a"), localTerminalFixture().StartedAt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.ReleaseReservation(record.Admission.ReservationID, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertLocalTerminalQuota(t, st, 0, 1, "unknown")
|
|
fact := localTerminalFixture()
|
|
fact.RecordingExpected = true
|
|
fact.Recording = &LocalRecordingManifest{
|
|
RecordingID: "rec-a", UploadID: "upload-a", Format: "wav", Channels: 1, SampleRateHz: 8000, DurationMs: 60000,
|
|
}
|
|
if err := st.RecordLocalCallTerminal(fact, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertLocalTerminalQuota(t, st, 0, 0, "released")
|
|
var resultID sql.NullString
|
|
if err := st.db.QueryRow(`SELECT result_event_id FROM local_v01_call_terminals WHERE execution_id='execution-a'`).Scan(&resultID); err != nil || resultID.Valid {
|
|
t.Fatalf("recording-pending call already had a result: event=%v err=%v", resultID, err)
|
|
}
|
|
event := localFinalEventFixture()
|
|
if err := st.EnqueueLocalFinalEvent(fact.ExecutionID, fact.CallID, event); !errors.Is(err, ErrCommandConflict) {
|
|
t.Fatalf("pending recording allowed final result without an outcome: %v", err)
|
|
}
|
|
if err := st.RecordLocalRecordingOutcome(fact.ExecutionID, LocalRecordingOutcome{
|
|
Status: "unavailable", ErrorCode: "upload_timeout", ObservedAt: fact.EndedAt.Add(LocalRecordingDeadline),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.EnqueueLocalFinalEvent(fact.ExecutionID, fact.CallID, event); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertLocalTerminalQuota(t, st, 0, 0, "released")
|
|
}
|
|
|
|
func TestLocalTerminalRequiresIssuedOrigination(t *testing.T) {
|
|
st, _ := readyLocalOrigination(t)
|
|
if err := st.RefuseLocalOrigination("d-1", "execution-a", "policy_denied", localTerminalFixture().StartedAt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.RecordLocalCallTerminal(localTerminalFixture(), nil); !errors.Is(err, ErrCommandConflict) {
|
|
t.Fatalf("refused origination recorded a call: %v", err)
|
|
}
|
|
assertLocalTerminalQuota(t, st, 1, 0, "held")
|
|
}
|