| The data model, in Nim d333b6f nandi 16h 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 |