eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2@testable import LChatCore
3
4// 🤖 Cross-platform event contract tests for the To-do app.
5// These tests document what a native To-do implementation must do.
6// They will initially fail (the implementation doesn't exist yet);
7// as you build the native side, make them pass.
8//
9// Source of truth: web/static/apps/todo.js
10// Event types: todo.add, todo.done, todo.reopen, todo.remove
11// State shape: { items: [{ id, text, done, doneAt, by }] }
12
13final class TodoAppVectorTests: XCTestCase {
14 // A to-do item as stored locally
15 struct TodoItem: Equatable {
16 let id: String
17 let text: String
18 let done: Bool
19 let doneAt: Int64 // sender's send time (drives grace-period animation)
20 let by: String // who added it
21 }
22
23 // Fold state for tests
24 struct TodoState: Equatable {
25 var items: [TodoItem] = []
26 }
27
28 // Helper: apply a fold event to state (mimics the web app's reducers)
29 func foldTodo(_ state: TodoState, event: [String: Any]) -> TodoState {
30 var items = state.items
31
32 if let type = event["type"] as? String {
33 switch type {
34 case "todo.add":
35 if let text = event["text"] as? String,
36 let id = event["id"] as? String,
37 !text.isEmpty,
38 !id.isEmpty {
39 // addItem dedupes by id
40 if !items.contains(where: { $0.id == id }) {
41 let by = (event["name"] as? String) ?? ""
42 items.append(TodoItem(id: id, text: text, done: false, doneAt: 0, by: by))
43 }
44 }
45
46 case "todo.done":
47 if let id = event["id"] as? String {
48 let ts = (event["ts"] as? Int64) ?? 0
49 items = items.map { item in
50 item.id == id ? TodoItem(id: item.id, text: item.text, done: true, doneAt: ts, by: item.by) : item
51 }
52 }
53
54 case "todo.reopen":
55 if let id = event["id"] as? String {
56 items = items.map { item in
57 item.id == id ? TodoItem(id: item.id, text: item.text, done: false, doneAt: 0, by: item.by) : item
58 }
59 }
60
61 case "todo.remove":
62 if let id = event["id"] as? String {
63 items.removeAll { $0.id == id }
64 }
65
66 default:
67 break
68 }
69 }
70
71 return TodoState(items: items)
72 }
73
74 func testTodoAdd() {
75 var state = TodoState()
76 let event: [String: Any] = [
77 "type": "todo.add",
78 "id": "todo-1",
79 "text": "buy milk",
80 "name": "Alice",
81 ]
82
83 state = foldTodo(state, event: event)
84
85 XCTAssertEqual(state.items.count, 1, "should have one item")
86 let item = state.items[0]
87 XCTAssertEqual(item.id, "todo-1")
88 XCTAssertEqual(item.text, "buy milk")
89 XCTAssertEqual(item.done, false, "newly added items are not done")
90 XCTAssertEqual(item.doneAt, 0, "newly added items have no doneAt")
91 XCTAssertEqual(item.by, "Alice")
92 }
93
94 func testTodoDone() {
95 var state = TodoState(items: [
96 TodoItem(id: "todo-1", text: "buy milk", done: false, doneAt: 0, by: "Alice"),
97 ])
98
99 let event: [String: Any] = [
100 "type": "todo.done",
101 "id": "todo-1",
102 "ts": Int64(1700000000000),
103 ]
104
105 state = foldTodo(state, event: event)
106
107 XCTAssertEqual(state.items.count, 1)
108 let item = state.items[0]
109 XCTAssertEqual(item.done, true, "item should be marked done")
110 XCTAssertEqual(item.doneAt, 1700000000000, "doneAt should record the event timestamp")
111 }
112
113 func testTodoReopen() {
114 var state = TodoState(items: [
115 TodoItem(id: "todo-1", text: "buy milk", done: true, doneAt: 1700000000000, by: "Alice"),
116 ])
117
118 let event: [String: Any] = [
119 "type": "todo.reopen",
120 "id": "todo-1",
121 ]
122
123 state = foldTodo(state, event: event)
124
125 XCTAssertEqual(state.items.count, 1)
126 let item = state.items[0]
127 XCTAssertEqual(item.done, false, "item should be marked not done")
128 XCTAssertEqual(item.doneAt, 0, "doneAt should be cleared")
129 }
130
131 func testTodoRemove() {
132 var state = TodoState(items: [
133 TodoItem(id: "todo-1", text: "buy milk", done: false, doneAt: 0, by: "Alice"),
134 TodoItem(id: "todo-2", text: "call mum", done: false, doneAt: 0, by: "Alice"),
135 ])
136
137 let event: [String: Any] = [
138 "type": "todo.remove",
139 "id": "todo-1",
140 ]
141
142 state = foldTodo(state, event: event)
143
144 XCTAssertEqual(state.items.count, 1, "one item removed, one remains")
145 XCTAssertEqual(state.items[0].id, "todo-2", "remaining item should be todo-2")
146 }
147
148 func testTodoIdempotence() {
149 var state = TodoState()
150
151 let event: [String: Any] = [
152 "type": "todo.add",
153 "id": "todo-1",
154 "text": "buy milk",
155 "name": "Alice",
156 ]
157
158 // Fold the same event twice (e.g., duplicate delivery on reconnect)
159 state = foldTodo(state, event: event)
160 state = foldTodo(state, event: event)
161
162 XCTAssertEqual(state.items.count, 1, "should have exactly one item (duplicate add deduped)")
163 }
164
165 func testTodoGracePeriodPartition() {
166 // Grace period: items are kept visible for ~750ms after being checked off,
167 // then tucked into a "Completed" section. This is derived in render from
168 // Date.now() and doneAt, never by a timer mutating state.
169 //
170 // Implementation note: keep the doneAt timestamp with each item; in the UI,
171 // partition items where doneAt > (now - 750) into "active", others into "completed".
172
173 let now: Int64 = 1700000000000
174 let gracePeriod: Int64 = 750
175
176 var state = TodoState(items: [
177 TodoItem(id: "todo-1", text: "just checked", done: true, doneAt: now - 100, by: "Alice"), // within grace
178 TodoItem(id: "todo-2", text: "checked long ago", done: true, doneAt: now - 1000, by: "Alice"), // past grace
179 TodoItem(id: "todo-3", text: "not done", done: false, doneAt: 0, by: "Alice"),
180 ])
181
182 // Partition logic
183 let active = state.items.filter { item in
184 !item.done || (item.done && item.doneAt > now - gracePeriod)
185 }
186 let archived = state.items.filter { item in
187 item.done && item.doneAt <= now - gracePeriod
188 }
189
190 XCTAssertEqual(active.count, 2, "should have 2 active items (not done + recently done)")
191 XCTAssertEqual(archived.count, 1, "should have 1 archived item")
192 XCTAssertTrue(active.contains { $0.id == "todo-1" }, "recently done item stays active")
193 XCTAssertTrue(active.contains { $0.id == "todo-3" }, "not-done item stays active")
194 XCTAssertTrue(archived.contains { $0.id == "todo-2" }, "old done item is archived")
195 }
196
197 func testTodoMultipleItems() {
198 var state = TodoState()
199
200 let events: [[String: Any]] = [
201 ["type": "todo.add", "id": "todo-1", "text": "milk", "name": "Alice"],
202 ["type": "todo.add", "id": "todo-2", "text": "bread", "name": "Alice"],
203 ["type": "todo.add", "id": "todo-3", "text": "eggs", "name": "Bob"],
204 ]
205
206 for event in events {
207 state = foldTodo(state, event: event)
208 }
209
210 XCTAssertEqual(state.items.count, 3, "should have three items")
211 XCTAssertEqual(state.items[0].text, "milk")
212 XCTAssertEqual(state.items[1].text, "bread")
213 XCTAssertEqual(state.items[2].text, "eggs")
214 }
215
216 func testTodoTextPreservation() {
217 let specialText = "Buy 2x milk™ — €5 each\n(or substitute)"
218 var state = TodoState()
219
220 let event: [String: Any] = [
221 "type": "todo.add",
222 "id": "todo-1",
223 "text": specialText,
224 "name": "Alice",
225 ]
226
227 state = foldTodo(state, event: event)
228
229 XCTAssertEqual(state.items[0].text, specialText, "should preserve text exactly, including unicode and newlines")
230 }
231}
232