1package daemon23import (4 "encoding/json"5 "github.com/FelineStateMachine/tinyrelay/internal/event"6 "testing"7)89func TestChatActivitySharesApprovalStateAndScopesRooms(t *testing.T) {10 h := newRoomHarness(t)11 request := h.must("alice", event.KIND_JOB_REQUEST, [][]string{{"h", "main"}, {"p", h.keys["bob"]}, {"subject", "Build preview"}}, "Build preview")12 h.must("bob", event.KIND_JOB_PROGRESS, [][]string{{"h", "main"}, {"e", request.ID}, {"p", h.keys["alice"]}}, "2 of 3 checks complete")13 decision := h.must("alice", 9, [][]string{{"h", "main"}, {"request", "approve"}, {"p", h.keys["bob"]}, {"subject", "Publish preview"}}, "Publish it?")14 read := func(who, room string) map[string]any {15 t.Helper()16 raw, err := h.tenant.Execute(h.ctx, h.keys[who], "browsechatactivity", []json.RawMessage{rawJSON(map[string]string{"room": room})})17 if err != nil {18 t.Fatal(err)19 }20 return raw.(map[string]any)21 }22 data := read("bob", "main")23 jobs := data["jobs"].([]jobItem)24 if len(jobs) != 1 || jobs[0].Status != "running" || jobs[0].Progress == nil || jobs[0].Progress.Content != "2 of 3 checks complete" {25 t.Fatalf("jobs=%v", jobs)26 }27 approvals := data["approvals"].([]approvalItem)28 if len(approvals) != 1 || approvals[0].State != "open" {29 t.Fatalf("approvals=%v", approvals)30 }31 h.must("bob", 7, [][]string{{"h", "main"}, {"e", decision.ID}, {"p", h.keys["alice"]}}, "+")32 data = read("bob", "main")33 if data["approvals"].([]approvalItem)[0].State != "answered" {34 t.Fatal("inline approval not settled")35 }36 detail, err := h.tenant.Execute(h.ctx, h.keys["bob"], "browseapproval", []json.RawMessage{rawJSON(map[string]string{"id": decision.ID})})37 if err != nil {38 t.Fatal(err)39 }40 if detail.(map[string]any)["item"].(approvalItem).State != "answered" {41 t.Fatal("Approvals disagrees")42 }43 h.must("alice", event.KIND_CREATE_GROUP, [][]string{{"h", "private-chat"}, {"name", "Private"}, {"visibility", "members"}}, "")44 private := h.must("alice", event.KIND_JOB_REQUEST, [][]string{{"h", "private-chat"}, {"p", h.keys["alice"]}}, "Private work")45 if _, err := h.tenant.Execute(h.ctx, h.keys["bob"], "browsechatactivity", []json.RawMessage{rawJSON(map[string]string{"room": "private-chat"})}); err == nil {46 t.Fatal("outsider read private activity")47 }48 for _, job := range read("bob", "main")["jobs"].([]jobItem) {49 if job.ID == private.ID {50 t.Fatal("private job leaked")51 }52 }53 if _, err := h.publish("alice", event.KIND_JOB_PROGRESS, [][]string{{"e", private.ID}, {"p", h.keys["alice"]}}, "private progress"); err == nil {54 t.Fatal("unscoped progress accepted for a private room task")55 }56 if _, err := h.publish("alice", event.KIND_JOB_PROGRESS, [][]string{{"h", "main"}, {"e", private.ID}, {"p", h.keys["alice"]}}, "private progress"); err == nil {57 t.Fatal("progress in another room accepted for a private room task")58 }59}60func TestChatPresenceIsEphemeralAndRoomAuthorized(t *testing.T) {61 h := newRoomHarness(t)62 h.must("alice", event.KIND_CREATE_GROUP, [][]string{{"h", "private-chat"}, {"visibility", "members"}}, "")63 for _, kind := range []int{20001, 20002} {64 e := h.must("alice", kind, [][]string{{"h", "private-chat"}}, "typing")65 if rows := h.query("alice", event.Filter{IDs: []string{e.ID}}); len(rows) != 0 {66 t.Fatal("presence persisted")67 }68 if _, err := h.publish("bob", kind, [][]string{{"h", "private-chat"}}, "typing"); err == nil {69 t.Fatal("outsider presence accepted")70 }71 }72}7374func TestChatActivityIncludesParentOnlyRequestsInCanonicalThread(t *testing.T) {75 h := newRoomHarness(t)76 root := h.must("alice", 9, [][]string{{"h", "main"}}, "Root")77 parent := h.must("bob", 9, [][]string{{"h", "main"}, {"e", root.ID, "", "reply"}}, "Parent")78 approval := h.must("alice", 9, [][]string{{"h", "main"}, {"e", parent.ID, "", "reply"}, {"request", "approve"}, {"p", h.keys["bob"]}}, "Approve the nested action?")79 job := h.must("alice", event.KIND_JOB_REQUEST, [][]string{{"h", "main"}, {"e", parent.ID, "", "root"}, {"p", h.keys["bob"]}}, "Nested job")80 other := h.must("alice", 9, [][]string{{"h", "main"}}, "Another root")81 h.must("alice", 9, [][]string{{"h", "main"}, {"e", other.ID, "", "root"}, {"request", "approve"}, {"p", h.keys["bob"]}}, "Unrelated action")82 raw, err := h.tenant.Execute(h.ctx, h.keys["bob"], "browsechatactivity", []json.RawMessage{rawJSON(map[string]string{"room": "main", "root": root.ID})})83 if err != nil {84 t.Fatal(err)85 }86 data := raw.(map[string]any)87 approvals, jobs := data["approvals"].([]approvalItem), data["jobs"].([]jobItem)88 if len(approvals) != 1 || approvals[0].ID != approval.ID || len(jobs) != 1 || jobs[0].ID != job.ID {89 t.Fatalf("activity omitted nested request or included another thread: %v", data)90 }91}