Details (original message): Add multi-instance groundwork: blobstore seam for artifacts, PG LISTEN/NOTIFY event fanout, and per-tenant rate limiting with native Redis backend
83 lines
2.2 KiB
Go
83 lines
2.2 KiB
Go
package pgbus
|
||
|
||
// PG 档事件扇出集成测试(环境门控:AGENT_PG_TEST_DSN,原生 PostgreSQL 实例)。
|
||
// 验证:实例 A 落盘+NOTIFY → 实例 B 的 LISTEN 取回并转发到其本地 bus。
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"testing"
|
||
"time"
|
||
|
||
core "agent-system-core"
|
||
"agent-system-backend/internal/bus"
|
||
"agent-system-backend/internal/store"
|
||
)
|
||
|
||
func pgTestStore(t *testing.T) (*store.Store, string) {
|
||
t.Helper()
|
||
dsn := os.Getenv("AGENT_PG_TEST_DSN")
|
||
if dsn == "" {
|
||
t.Skip("AGENT_PG_TEST_DSN not set")
|
||
}
|
||
schema := "pgbus_" + strconv.Itoa(os.Getpid()) + "_" + fmt.Sprint(time.Now().UnixNano()%1e6)
|
||
admin, err := sql.Open("pgx", dsn)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := admin.Exec("CREATE SCHEMA " + schema); err != nil {
|
||
admin.Close()
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() {
|
||
admin.Exec("DROP SCHEMA IF EXISTS " + schema + " CASCADE")
|
||
admin.Close()
|
||
})
|
||
full := dsn + "&search_path=" + schema
|
||
st, err := store.OpenPostgres(full, filepath.Join(t.TempDir(), "artifacts"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { st.Close() })
|
||
return st, full
|
||
}
|
||
|
||
func TestNotifyListenerFanout(t *testing.T) {
|
||
st, dsn := pgTestStore(t)
|
||
|
||
// “实例 B”:独立 bus + 监听连接。
|
||
b2 := bus.New()
|
||
listener, err := Start(context.Background(), dsn, st, b2)
|
||
if err != nil {
|
||
t.Fatalf("listen: %v", err)
|
||
}
|
||
defer listener.Close(context.Background())
|
||
|
||
ch, stop := b2.Subscribe("conv_x")
|
||
defer stop()
|
||
|
||
// “实例 A”:批落盘 + NOTIFY(Notifier 与 listener 共库不同连接)。
|
||
notifier := NewNotifier(st)
|
||
evs := []core.StreamEvent{
|
||
{ConversationID: "conv_x", RunID: "r1", TS: time.Now().UTC().Format(time.RFC3339Nano), V: 1, Type: core.EvRunPhase, Payload: []byte(`{"phase":"thinking"}`)},
|
||
{ConversationID: "conv_x", RunID: "r1", TS: time.Now().UTC().Format(time.RFC3339Nano), V: 1, Type: core.EvRunCompleted, Payload: []byte(`{}`)},
|
||
}
|
||
if _, err := st.AppendEvents("conv_x", evs); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
notifier.Notify("conv_x", evs)
|
||
|
||
select {
|
||
case e := <-ch:
|
||
if e.ConversationID != "conv_x" || e.Seq == 0 {
|
||
t.Fatalf("fanout event malformed: %+v", e)
|
||
}
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatal("no fanout within 3s")
|
||
}
|
||
}
|