package main import ( "sync" "sync/atomic" "testing" ) // These tests exercise the Postgres-backed store directly. They run only when // AI_BOT_TEST_DATABASE_URL points at a throwaway database (openTestStore skips // otherwise) and start from a clean slate (openTestStore truncates). func TestStoreTxnDedup(t *testing.T) { st := openTestStore(t) defer st.Close() if got, err := st.HasTxn("txn-1"); err != nil || got { t.Fatalf("fresh txn: got (%v,%v), want (false,nil)", got, err) } if err := st.MarkTxn("txn-1"); err != nil { t.Fatalf("mark: %v", err) } if got, err := st.HasTxn("txn-1"); err != nil || !got { t.Fatalf("marked txn: got (%v,%v), want (true,nil)", got, err) } // Re-marking is idempotent (a retried transaction). if err := st.MarkTxn("txn-1"); err != nil { t.Fatalf("re-mark: %v", err) } if got, _ := st.HasTxn("txn-2"); got { t.Fatalf("unrelated txn must be unseen") } } func TestStoreSeenEvent(t *testing.T) { st := openTestStore(t) defer st.Close() first, err := st.SeenEvent("$ev1") if err != nil || !first { t.Fatalf("first SeenEvent: got (%v,%v), want (true,nil)", first, err) } again, err := st.SeenEvent("$ev1") if err != nil || again { t.Fatalf("repeat SeenEvent: got (%v,%v), want (false,nil)", again, err) } other, err := st.SeenEvent("$ev2") if err != nil || !other { t.Fatalf("new SeenEvent: got (%v,%v), want (true,nil)", other, err) } } // Dedup state must survive a process restart — the whole point of the durable store. func TestStoreDedupSurvivesRestart(t *testing.T) { st := openTestStore(t) if _, err := st.SeenEvent("$ev-restart"); err != nil { t.Fatalf("seen: %v", err) } if err := st.MarkTxn("txn-restart"); err != nil { t.Fatalf("mark: %v", err) } st.Close() // Reopen the same database WITHOUT truncating: simulates a container restart. st2, err := OpenStore(testDSN()) if err != nil { t.Fatalf("reopen: %v", err) } defer st2.Close() if isNew, err := st2.SeenEvent("$ev-restart"); err != nil || isNew { t.Fatalf("event after restart must be already-seen: got (%v,%v)", isNew, err) } if seen, err := st2.HasTxn("txn-restart"); err != nil || !seen { t.Fatalf("txn after restart must be seen: got (%v,%v)", seen, err) } } func TestStoreLimiterPerUserCap(t *testing.T) { st := openTestStore(t) defer st.Close() const user = "@u:vojo.chat" const cap, ceiling = 2, 100.0 for i := 0; i < cap; i++ { if res, err := st.Reserve(user, cap, ceiling); err != nil || res != reserveOK { t.Fatalf("reserve %d: got (%v,%v), want reserveOK", i, res, err) } } // The (cap+1)th request is denied per-user. if res, err := st.Reserve(user, cap, ceiling); err != nil || res != reserveDeniedUser { t.Fatalf("over-cap reserve: got (%v,%v), want reserveDeniedUser", res, err) } // A different user is unaffected. if res, err := st.Reserve("@v:vojo.chat", cap, ceiling); err != nil || res != reserveOK { t.Fatalf("other user reserve: got (%v,%v), want reserveOK", res, err) } // Refund returns a slot, so the first user can reserve once more. if err := st.RefundRequest(user); err != nil { t.Fatalf("refund: %v", err) } if res, err := st.Reserve(user, cap, ceiling); err != nil || res != reserveOK { t.Fatalf("post-refund reserve: got (%v,%v), want reserveOK", res, err) } } // A zero per-user cap denies even the first request — the SQLite store's // requests(0) >= cap(0) behaviour, preserved. func TestStoreLimiterZeroCap(t *testing.T) { st := openTestStore(t) defer st.Close() if res, err := st.Reserve("@u:vojo.chat", 0, 100.0); err != nil || res != reserveDeniedUser { t.Fatalf("zero-cap reserve: got (%v,%v), want reserveDeniedUser", res, err) } } // A zero ceiling denies the very first request of the day even before any spend row // exists — the SQLite store treated SUM(NULL) as 0.0 (0 >= 0), and the PG store must // match (SUM over zero rows is NULL). func TestStoreLimiterZeroCeiling(t *testing.T) { st := openTestStore(t) defer st.Close() if res, err := st.Reserve("@u:vojo.chat", 1_000_000, 0); err != nil || res != reserveDeniedGlobal { t.Fatalf("zero-ceiling reserve on empty store: got (%v,%v), want reserveDeniedGlobal", res, err) } } func TestStoreLimiterGlobalCeiling(t *testing.T) { st := openTestStore(t) defer st.Close() const ceiling = 1.0 // Book spend up to the ceiling (Reconcile is what feeds the global gate). if err := st.Reconcile("@a:vojo.chat", 0.6); err != nil { t.Fatalf("reconcile a: %v", err) } if err := st.Reconcile("@b:vojo.chat", 0.5); err != nil { t.Fatalf("reconcile b: %v", err) } if spent, err := st.SpentTodayUSD(); err != nil || spent < 1.1 { t.Fatalf("spent today: got (%v,%v), want >= 1.1", spent, err) } // Now any reservation is denied globally, regardless of the per-user cap. if res, err := st.Reserve("@c:vojo.chat", 1_000_000, ceiling); err != nil || res != reserveDeniedGlobal { t.Fatalf("over-ceiling reserve: got (%v,%v), want reserveDeniedGlobal", res, err) } } // The pgx pool is concurrent (the SQLite store serialized on one connection). The // advisory lock in Reserve must still admit EXACTLY perUserCap requests when many // arrive at once for the same user — the same user messaging from several rooms // simultaneously must not slip past the cap. func TestStoreReserveConcurrentRespectsCap(t *testing.T) { st := openTestStore(t) defer st.Close() const user = "@race:vojo.chat" const cap = 10 const goroutines = 50 var ok int64 var wg sync.WaitGroup for i := 0; i < goroutines; i++ { wg.Add(1) go func() { defer wg.Done() res, err := st.Reserve(user, cap, 1e9) if err != nil { t.Errorf("reserve: %v", err) return } if res == reserveOK { atomic.AddInt64(&ok, 1) } }() } wg.Wait() if ok != cap { t.Fatalf("concurrent reserves admitted %d, want exactly %d (the per-user cap)", ok, cap) } } func TestStoreWarnedEncrypted(t *testing.T) { st := openTestStore(t) const room = "!enc:vojo.chat" if warned, err := st.HasWarnedEncrypted(room); err != nil || warned { t.Fatalf("fresh room: got (%v,%v), want (false,nil)", warned, err) } if err := st.SetWarnedEncrypted(room); err != nil { t.Fatalf("set: %v", err) } // Setting twice is idempotent. if err := st.SetWarnedEncrypted(room); err != nil { t.Fatalf("re-set: %v", err) } if warned, err := st.HasWarnedEncrypted(room); err != nil || !warned { t.Fatalf("warned room: got (%v,%v), want (true,nil)", warned, err) } st.Close() // The one-shot flag must outlive a restart (F5: no re-react after restart). st2, err := OpenStore(testDSN()) if err != nil { t.Fatalf("reopen: %v", err) } defer st2.Close() if warned, err := st2.HasWarnedEncrypted(room); err != nil || !warned { t.Fatalf("warned after restart: got (%v,%v), want (true,nil)", warned, err) } }