Skip to content
quarry Artifact [66337ffde7]
Login

Artifact [66337ffde7]

Artifact 66337ffde79ced699cf602ec20c9f8ea76093c1d361943a82f9477a0195b580d:


package web

import (
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

// teamFakeLive serves one merged room: whatever it is given, in order.
type teamFakeLive struct{ msgs []ChatMessage }

func (f teamFakeLive) Tail(string, uint64) (<-chan ChatMessage, func()) {
	ch := make(chan ChatMessage)
	close(ch)
	return ch, func() {}
}
func (f teamFakeLive) Join(string, string) func() { return func() {} }
func (f teamFakeLive) Present(string) []string    { return nil }
func (f teamFakeLive) PresenceChanged(string) (<-chan struct{}, func()) {
	return make(chan struct{}), func() {}
}
func (f teamFakeLive) Activity() (<-chan struct{}, func()) {
	return make(chan struct{}), func() {}
}
func (f teamFakeLive) TailTeam(string) (<-chan ChatMessage, func()) {
	ch := make(chan ChatMessage, len(f.msgs))
	for _, m := range f.msgs {
		ch <- m
	}
	close(ch)
	return ch, func() {}
}

// A line arriving live is the same markup as the same line after a
// reload — including the origin chip, which comes off the MESSAGE and
// not off the page. The room used to look like a live channel and not
// be one; this is what made it one.
func TestTheTeamRoomAppendsArrivingLinesThroughThePageRenderer(t *testing.T) {
	live := teamFakeLive{msgs: []ChatMessage{
		// The SAME author, the SAME minute, two different channels —
		// the only shape that can tell "runs group by author" apart
		// from "runs never span two channels".
		{Seq: 4, Kind: "msg", User: "lou", Repo: "zit", Text: "in zit", At: "2026-09-14T17:00:00Z"},
		{Seq: 9, Kind: "msg", User: "lou", ToTeam: true, Text: "to everyone", At: "2026-09-14T17:00:00Z"},
	}}
	req := httptest.NewRequest("GET", "/t/core/chat.sse", nil)
	w := httptest.NewRecorder()
	done := make(chan struct{})
	go func() { defer close(done); liveTeamRoom(live, "core", w, req) }()
	select {
	case <-done:
	case <-time.After(3 * time.Second):
		t.Fatal("the stream did not finish when the channel closed")
	}
	body := w.Body.String()
	if ct := w.Header().Get("Content-Type"); ct != "text/event-stream" {
		t.Errorf("FAIL: content type = %q", ct)
	}
	for _, want := range []string{
		"event: datastar-patch-elements", "data: mode append",
		"data: selector #chatlog",
		// the shared row, and each line's own origin
		`class="chatmsg"`,
		`data-hook="chat-origin" href="/r/zit"`,
		`data-hook="chat-origin" data-to-team`,
	} {
		if !strings.Contains(body, want) {
			t.Errorf("FAIL: the stream is missing %q", want)
		}
	}
	// A run never spans two channels, so the second line is NOT drawn
	// as a continuation of the first even though it is a minute apart.
	if strings.Contains(body, "data-grouped") {
		t.Errorf("FAIL: two channels were grouped into one run")
	}
}

// Tombstones are skipped: each channel numbers its own sequences, so a
// merged view cannot address the message an edit or delete refers to.
// Reloading shows the folded truth; appending the tombstone would not.
func TestTheTeamRoomDoesNotAppendTombstones(t *testing.T) {
	live := teamFakeLive{msgs: []ChatMessage{
		{Seq: 5, Kind: "del", Ref: 4, User: "lou", Repo: "zit"},
		{Seq: 6, Kind: "edit", Ref: 4, User: "lou", Repo: "zit", Text: "fixed"},
	}}
	req := httptest.NewRequest("GET", "/t/core/chat.sse", nil)
	w := httptest.NewRecorder()
	done := make(chan struct{})
	go func() { defer close(done); liveTeamRoom(live, "core", w, req) }()
	select {
	case <-done:
	case <-time.After(3 * time.Second):
		t.Fatal("the stream did not finish")
	}
	if strings.Contains(w.Body.String(), "datastar-patch-elements") {
		t.Errorf("FAIL: a tombstone was appended as a row")
	}
}