Skip to content
quarry Artifact [64d9fd61b7]
Login

Artifact [64d9fd61b7]

Artifact 64d9fd61b7340d95d8e87a67df40b160a8619f204a41a63840dae47063e3f186:


package chat

import (
	"context"
	"encoding/json"
	"strings"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/events"
)

// robotFixture stands up the vocabulary stream beside the chat stream,
// wires a robot over both, and hands back a publisher for raised events.
func robotFixture(t *testing.T, patterns map[string]string) (*Room, func(kind, repo string, payload map[string]any)) {
	t.Helper()
	ev, err := events.Start(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(ev.Close)
	room := room(t, ev)
	js := ev.JS()
	robot := NewRobot(room, js,
		func(repo string) string { return patterns[repo] },
		func(repo string) (string, bool) { return repo, true })
	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	if err := robot.Start(ctx); err != nil {
		t.Fatal(err)
	}
	// The consumer must exist before anything is published: DeliverNew
	// means an event raised ahead of it is not a backlog to announce.
	time.Sleep(200 * time.Millisecond)
	return room, func(kind, repo string, payload map[string]any) {
		body, err := json.Marshal(map[string]any{
			"kind": kind, "repo": repo, "payload": payload,
		})
		if err != nil {
			t.Fatal(err)
		}
		if _, err := js.Publish("quarry.evt."+repo+"."+kind, body); err != nil {
			t.Fatal(err)
		}
	}
}

func waitForRobot(t *testing.T, room *Room, code string) []Message {
	t.Helper()
	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		msgs, _, err := room.History(code, 0, 50)
		if err != nil {
			t.Fatal(err)
		}
		if len(msgs) > 0 {
			return msgs
		}
		time.Sleep(100 * time.Millisecond)
	}
	return nil
}

func TestRobotAnnouncesAMatchingEvent(t *testing.T) {
	room, raise := robotFixture(t, map[string]string{"alpha": "checkin"})
	raise("checkin.created", "alpha", map[string]any{
		"hash": "a91f3c2edeadbeefcafe", "actor": "drh",
		"branch": "trunk", "summary": "fix the parser",
	})
	msgs := waitForRobot(t, room, "alpha")
	if len(msgs) != 1 {
		t.Fatalf("channel holds %d messages, want 1", len(msgs))
	}
	m := msgs[0]
	if m.Kind != KindRobot || m.Origin != "robot" {
		t.Fatalf("line is kind=%q origin=%q, want robot/robot", m.Kind, m.Origin)
	}
	for _, want := range []string{"a91f3c2edead", "trunk", "drh", "fix the parser"} {
		if !strings.Contains(m.Text, want) {
			t.Errorf("line %q does not name %q", m.Text, want)
		}
	}
}

func TestRobotStaysQuietForUnmatchedEvents(t *testing.T) {
	room, raise := robotFixture(t, map[string]string{"alpha": "landing"})
	raise("checkin.created", "alpha", map[string]any{"hash": "abc", "actor": "drh"})
	// A repository that never opted in hears nothing either.
	raise("checkin.created", "beta", map[string]any{"hash": "def", "actor": "drh"})
	time.Sleep(time.Second)
	for _, code := range []string{"alpha", "beta"} {
		msgs, _, err := room.History(code, 0, 50)
		if err != nil {
			t.Fatal(err)
		}
		if len(msgs) != 0 {
			t.Fatalf("%s heard %d robot lines for an unmatched event", code, len(msgs))
		}
	}
}

// Robot lines are the channel talking about the repository, so a badge
// must not light up for them.
func TestRobotLinesAreNeverUnread(t *testing.T) {
	ev, err := events.Start(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	defer ev.Close()
	room := room(t, ev)
	if _, err := room.Post("alpha", Message{
		Kind: KindRobot, Origin: "robot", User: "robot", Text: "check-in abc on trunk",
	}); err != nil {
		t.Fatal(err)
	}
	n, err := room.Unread("alpha", 0)
	if err != nil {
		t.Fatal(err)
	}
	if n != 0 {
		t.Fatalf("a robot line counted %d unread, want 0", n)
	}
	if _, err := room.Post("alpha", Message{User: "alice", Text: "hello"}); err != nil {
		t.Fatal(err)
	}
	n, err = room.Unread("alpha", 0)
	if err != nil {
		t.Fatal(err)
	}
	if n != 1 {
		t.Fatalf("unread is %d, want 1 — the person's line and not the robot's", n)
	}
}

func TestRobotLineShapes(t *testing.T) {
	cases := []struct {
		kind    string
		payload map[string]any
		want    []string
	}{
		{"landing.accepted", map[string]any{"landing": "1234567890abcdef", "actor": "alice"},
			[]string{"landing", "1234567890ab"[:12], "accepted", "alice"}},
		{"ci.completed", map[string]any{"hash": "abcdef012345678", "state": "passed"},
			[]string{"CI", "passed", "abcdef012345"}},
		{"account.added", map[string]any{"actor": "root"}, nil}, // nothing useful to say
	}
	for _, c := range cases {
		got := RobotLine(c.kind, c.payload)
		if len(c.want) == 0 {
			if got != "" {
				t.Errorf("RobotLine(%s) = %q, want silence", c.kind, got)
			}
			continue
		}
		for _, w := range c.want {
			if !strings.Contains(got, w) {
				t.Errorf("RobotLine(%s) = %q, missing %q", c.kind, got, w)
			}
		}
	}
}