| The data model, in Nim d333b6f nandi 13h ago | 1 | ## The list, the overview and the read marker. |
| 2 | import std/algorithm |
| 3 | import std/strutils |
| 4 | ## |
| 5 | ## The marker rules are the ones worth the most here: unread is derived rather |
| 6 | ## than counted precisely so a replayed backlog cannot inflate it, and these |
| 7 | ## are the cases that prove it. |
| 8 | |
| 9 | import std/[sequtils, tables, unittest] |
| 10 | import frq/[model, rooms] |
| 11 | |
| 12 | proc msg(frm, text: string, at: int64 = 0, id = "", system = false): Message = |
| 13 | Message(frm: frm, text: text, at: at, id: id, system: system) |
| 14 | |
| 15 | suite "channelList": |
| 16 | setup: |
| 17 | var chans = initOrderedTable[string, Room]() |
| 18 | for (n, acc) in [("#alpha", 0'i64), ("#beta", 5'i64), ("#gamma", 9'i64)]: |
| 19 | var c = initRoom(n); c.accessed = acc; chans[n] = c |
| 20 | |
| 21 | test "most recently opened first": |
| 22 | check channelList(chans, "").mapIt(it.name) == @["#gamma", "#beta", "#alpha"] |
| 23 | |
| 24 | test "never-opened buffers sort under those, by name": |
| 25 | chans["#aaa"] = initRoom("#aaa") |
| 26 | chans["#zzz"] = initRoom("#zzz") |
| 27 | let names = channelList(chans, "").mapIt(it.name) |
| 28 | check names == @["#gamma", "#beta", "#aaa", "#alpha", "#zzz"] |
| 29 | |
| 30 | test "the search box filters, case-insensitively": |
| 31 | check channelList(chans, "BET").mapIt(it.name) == @["#beta"] |
| 32 | test "a blank search filters nothing": |
| 33 | check channelList(chans, " ").len == 3 |
| 34 | |
| 35 | suite "seenMessage": |
| 36 | test "a msgid we already hold is a replay": |
| 37 | let msgs = @[msg("a", "hi", id = "1")] |
| 38 | check seenMessage(msgs, "1", "a", "hi", "me") |
| 39 | check not seenMessage(msgs, "2", "a", "hi", "me") |
| 40 | |
| 41 | test "an untagged line is known by its sender and words": |
| 42 | # A replayed line with no tags has no identity, so left alone it arrives |
| 43 | # new on every rejoin and the room can never be finished reading. |
| 44 | let msgs = @[msg("alice", "hello")] |
| 45 | check seenMessage(msgs, "", "alice", "hello", "me") |
| 46 | check not seenMessage(msgs, "", "alice", "different", "me") |
| 47 | |
| 48 | test "our own untagged line is never a replay": |
| 49 | # A second "ok" from this client is a real event. |
| 50 | let msgs = @[msg("me", "ok")] |
| 51 | check not seenMessage(msgs, "", "me", "ok", "me") |
| 52 | |
| 53 | test "nor is the system's": |
| 54 | let msgs = @[msg("*", "alice joined")] |
| 55 | check not seenMessage(msgs, "", "*", "alice joined", "me") |
| 56 | |
| 57 | suite "the read marker": |
| 58 | setup: |
| 59 | var ch = initRoom("#test") |
| 60 | ch.messages = @[msg("a", "one", at = 100, id = "1"), |
| 61 | msg("b", "two", at = 200, id = "2"), |
| 62 | msg("c", "three", at = 300, id = "3")] |
| 63 | |
| 64 | test "everything after the marked id is unread": |
| 65 | ch.lastReadId = "1" |
| 66 | check ch.afterMarker.mapIt(it.text) == @["two", "three"] |
| 67 | |
| 68 | test "by time when the marked line is no longer held": |
| 69 | ch.lastReadId = "gone" |
| 70 | ch.lastReadAt = 150 |
| 71 | check ch.afterMarker.mapIt(it.text) == @["two", "three"] |
| 72 | |
| 73 | test "a replayed backlog cannot inflate the count": |
| 74 | # The whole reason unread is derived rather than counted. Two guards act |
| 75 | # together and this checks the pair, because either alone is not enough: |
| 76 | # |
| 77 | # seenMessage keeps the replayed line out of the buffer, and |
| 78 | # afterMarker means a line that IS older than the marker counts nothing. |
| 79 | # |
| 80 | # The first draft of this test appended the backlog twice and expected |
| 81 | # zero, which is a state seenMessage exists to make impossible — and the |
| 82 | # Clojure answers three to it as well. A test for an unreachable state |
| 83 | # tells you nothing about the reachable ones. |
| 84 | let marked = ch.markRead |
| 85 | check marked.recount("me").unread == 0 |
| 86 | for m in ch.messages: |
| 87 | check seenMessage(marked.messages, m.id, m.frm, m.text, "me") |
| 88 | |
| 89 | test "a line older than the marker counts for nothing": |
| 90 | ch.lastReadId = "" |
| 91 | ch.lastReadAt = 250 |
| 92 | check ch.afterMarker.mapIt(it.text) == @["three"] |
| 93 | |
| 94 | test "markRead never walks the marker backwards": |
| 95 | ch.lastReadAt = 500 # read past a backlog that then arrived |
| 96 | let marked = ch.markRead |
| 97 | check marked.lastReadAt == 500 |
| 98 | |
| 99 | test "system lines are read but never make a room worth looking at": |
| 100 | ch.messages.add msg("*", "you joined", at = 400, system = true) |
| 101 | check ch.recount("me").unread == 4 - 1 # the three said lines only... |
| 102 | ch.lastReadId = "3" |
| 103 | check ch.recount("me").unread == 0 |
| 104 | |
| 105 | test "a mention is noticed, and only somebody else's": |
| 106 | ch.messages = @[msg("alice", "hey me, look", at = 100), |
| 107 | msg("me", "me me me", at = 200)] |
| 108 | let r = ch.recount("me") |
| 109 | check r.unread == 2 |
| 110 | check r.mention |
| 111 | |
| 112 | test "no mention where the reader is not named": |
| 113 | ch.messages = @[msg("alice", "nothing here", at = 100)] |
| 114 | check not ch.recount("me").mention |
| 115 | |
| 116 | suite "recentEverywhere": |
| 117 | setup: |
| 118 | var chans = initOrderedTable[string, Room]() |
| 119 | for n in ["#a", "#b"]: |
| 120 | var c = initRoom(n) |
| 121 | for i in 1 .. 3: |
| 122 | c.messages.add msg("u", n & $i, at = i.int64 * 10) |
| 123 | chans[n] = c |
| 124 | |
| 125 | test "the room being read is left out": |
| 126 | let names = recentEverywhere(chans, "#a").mapIt(it.text) |
| 127 | check names.allIt(it.startsWith("#b")) |
| 128 | |
| 129 | test "system lines are left out": |
| 130 | var c = initRoom("#c") |
| 131 | c.messages = @[msg("*", "joined", at = 99, system = true)] |
| 132 | chans["#c"] = c |
| 133 | check recentEverywhere(chans, "").allIt(it.text != "joined") |
| 134 | |
| 135 | test "newest first": |
| 136 | let ats = recentEverywhere(chans, "").mapIt(it.at) |
| 137 | check ats == ats.sorted(SortOrder.Descending) |
| 138 | |
| 139 | test "a turn each, so a busy room cannot crowd a quiet one out": |
| 140 | # The reason for round-robin: taking the newest N outright would answer |
| 141 | # about whichever room is busiest, which is the one you can already see. |
| 142 | var busy = initRoom("#busy") |
| 143 | for i in 1 .. 200: |
| 144 | busy.messages.add msg("u", "spam" & $i, at = i.int64) |
| 145 | chans["#busy"] = busy |
| 146 | let got = recentEverywhere(chans, "") |
| 147 | check got.anyIt(it.text.startsWith("#a")) |
| 148 | check got.len <= overviewLimit |
| A sent message showed twice 9c1dfe9 nandi 1h ago | 149 | |
| 150 | suite "adoptEcho": |
| 151 | setup: |
| 152 | var r = initRoom("#test") |
| 153 | r.messages = @[msg("alice", "hello", at = 100, id = "1")] |
| 154 | # What `sendDraft` leaves behind: shown at once, no msgid yet. |
| 155 | r.messages.add Message(frm: "me", text: "hi there", at: 200, |
| 156 | localId: "local-1", pending: true) |
| 157 | |
| 158 | test "our own line coming back folds onto the copy we showed": |
| 159 | # This is the bug: without it every sent message appeared twice, because |
| 160 | # echo-message is negotiated and the server sends it back with an id. |
| 161 | check r.adoptEcho("me", "hi there", "srv-9", 250, "did:plc:me") |
| 162 | check r.messages.len == 2 |
| 163 | check r.messages[1].id == "srv-9" |
| 164 | check not r.messages[1].pending |
| 165 | |
| 166 | test "and that is how the client learns the msgid": |
| 167 | # Which is the whole point of asking for the cap: a reaction or a reply |
| 168 | # aimed at our own line has nothing to name until this happens. |
| 169 | discard r.adoptEcho("me", "hi there", "srv-9", 250, "") |
| 170 | check r.messages[1].answersTo("srv-9") |
| 171 | check rowId(r.messages[1]) == "srv-9" |
| 172 | |
| 173 | test "the server's timestamp wins over ours": |
| 174 | discard r.adoptEcho("me", "hi there", "srv-9", 250, "") |
| 175 | check r.messages[1].at == 250 |
| 176 | |
| 177 | test "a line that is not ours is not adopted": |
| 178 | check not r.adoptEcho("alice", "hello", "srv-9", 250, "") |
| 179 | |
| 180 | test "nor is one we have already seen back": |
| 181 | check r.adoptEcho("me", "hi there", "srv-9", 250, "") |
| 182 | # A second "hi there" from this client is a real event, not an echo. |
| 183 | check not r.adoptEcho("me", "hi there", "srv-10", 260, "") |
| 184 | |
| 185 | test "the same text sent twice adopts oldest first": |
| 186 | # The server echoes in the order it received. |
| 187 | r.messages.add Message(frm: "me", text: "hi there", at: 300, |
| 188 | localId: "local-2", pending: true) |
| 189 | check r.adoptEcho("me", "hi there", "srv-A", 310, "") |
| 190 | check r.messages[1].id == "srv-A" |
| 191 | check r.messages[2].id == "" |
| 192 | check r.adoptEcho("me", "hi there", "srv-B", 320, "") |
| 193 | check r.messages[2].id == "srv-B" |
| 194 | |
| 195 | test "different text is not adopted": |
| 196 | check not r.adoptEcho("me", "something else", "srv-9", 250, "") |