Artifact 067965cd562f1aec887f262900a691f6de4ac6aa690ffd3f9785c01e8855d3b6:
- File
internal/chat/chat_test.go
— part of check-in
[5670d3643d]
at
2026-08-16 04:50:23
on branch trunk
— chat: per-repo live channel over NATS (0001 §5)
internal/chat: messages fan out over NATS with a 7-day JetStream window (replay on join), keyed by project code so a channel is shared across nodes. chat.history/chat.post ops (post is not lease-gated — it mutates no store); web page with on-load history, a plain post form (no-JS baseline), and an SSE append stream (Datastar enhancement). Verified live: form post + cross-connection SSE delivery in-browser, and cross-node fan-out on a shared external NATS. Durable forum-thread archival waits on zit forum verbs (zit#76); the window is interim durability. (user: dmestas, size: 3145) [annotate] [blame] [check-ins using]
package chat import ( "testing" "time" natsserver "github.com/nats-io/nats-server/v2/server" "craftdesign.group/quarry/internal/events" ) func room(t *testing.T, ev *events.Server) *Room { t.Helper() r, err := New(ev.Conn(), ev.JS()) if err != nil { t.Fatal(err) } return r } // Post then History replays the recent window, oldest first; a second // channel is isolated from the first. func TestPostAndHistory(t *testing.T) { ev, err := events.Start(t.TempDir()) if err != nil { t.Fatal(err) } defer ev.Close() r := room(t, ev) for _, txt := range []string{"first", "second", "third"} { if err := r.Post("repoA", Message{User: "dan", Text: txt}); err != nil { t.Fatal(err) } } if err := r.Post("repoB", Message{User: "eve", Text: "other channel"}); err != nil { t.Fatal(err) } hist, err := r.History("repoA", 50) if err != nil { t.Fatal(err) } if len(hist) != 3 || hist[0].Text != "first" || hist[2].Text != "third" { t.Fatalf("history = %+v", hist) } if hist[0].At == "" || hist[0].User != "dan" { t.Fatalf("message not stamped: %+v", hist[0]) } other, _ := r.History("repoB", 50) if len(other) != 1 || other[0].Text != "other channel" { t.Fatalf("channel isolation broken: %+v", other) } // Empty author defaults to anonymous. r.Post("repoA", Message{Text: "no name"}) hist, _ = r.History("repoA", 50) if hist[len(hist)-1].User != "anonymous" { t.Fatalf("anon default missing: %+v", hist[len(hist)-1]) } } // A live subscriber receives posts as they arrive. func TestLiveSubscribe(t *testing.T) { ev, err := events.Start(t.TempDir()) if err != nil { t.Fatal(err) } defer ev.Close() r := room(t, ev) ch, cancel := r.Subscribe("repo") defer cancel() if err := r.Post("repo", Message{User: "dan", Text: "live message"}); err != nil { t.Fatal(err) } select { case m := <-ch: if m.Text != "live message" || m.User != "dan" { t.Fatalf("got %+v", m) } case <-time.After(5 * time.Second): t.Fatal("live message not delivered") } } // Chat fans out across nodes on a shared external NATS — a post on one // node reaches a subscriber on another (the multi-node property). func TestCrossNodeChat(t *testing.T) { url := externalURL(t) evA, err := events.Connect(url) if err != nil { t.Fatal(err) } defer evA.Close() evB, err := events.Connect(url) if err != nil { t.Fatal(err) } defer evB.Close() a, b := room(t, evA), room(t, evB) ch, cancel := b.Subscribe("shared") defer cancel() if err := a.Post("shared", Message{User: "dan", Text: "cross-node hello"}); err != nil { t.Fatal(err) } select { case m := <-ch: if m.Text != "cross-node hello" { t.Fatalf("got %+v", m) } case <-time.After(5 * time.Second): t.Fatal("cross-node chat not delivered") } } func externalURL(t *testing.T) string { t.Helper() srv, err := natsserver.NewServer(&natsserver.Options{ Host: "127.0.0.1", Port: -1, JetStream: true, StoreDir: t.TempDir(), }) if err != nil { t.Fatal(err) } go srv.Start() if !srv.ReadyForConnections(10 * time.Second) { t.Fatal("external NATS not ready") } t.Cleanup(srv.Shutdown) return srv.ClientURL() }