eleven / ocho Public

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

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 Reminders app.
5// These tests document what a native Reminders 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/remind.js
10// Event types: remind.add, remind.fire, remind.done
11// State shape: { items: [{ id, text, when, ts, sid, fired, firedAt }] }
12// Note: `when` is absolute epoch ms (due time); `ts` is add time; `sid` is server schedule id
13
14final class RemindAppVectorTests: XCTestCase {
15 // A reminder item as stored locally
16 struct ReminderItem: Equatable {
17 let id: String
18 let text: String
19 let when: Int64 // absolute epoch ms when the reminder fires
20 let ts: Int64 // epoch ms when added (send time)
21 let sid: String // server schedule id ("" if scheduling failed)
22 let fired: Bool // has the reminder fired?
23 let firedAt: Int64 // epoch ms when it fired (drives grace-period animation)
24 }
25
26 // Fold state for tests
27 struct RemindState: Equatable {
28 var items: [ReminderItem] = []
29 }
30
31 // Helper: apply a fold event to state (mimics the web app's reducers)
32 func foldRemind(_ state: RemindState, event: [String: Any]) -> RemindState {
33 var items = state.items
34
35 if let type = event["type"] as? String {
36 switch type {
37 case "remind.add":
38 if let id = event["id"] as? String,
39 let text = event["text"] as? String,
40 let when = event["when"] as? Int64,
41 !id.isEmpty,
42 !text.isEmpty {
43 // addItem dedupes by id
44 if !items.contains(where: { $0.id == id }) {
45 let ts = (event["ts"] as? Int64) ?? 0
46 let sid = (event["sid"] as? String) ?? ""
47 items.append(ReminderItem(id: id, text: text, when: when, ts: ts, sid: sid, fired: false, firedAt: 0))
48 }
49 // Sort by `when` (soonest first)
50 items.sort { $0.when < $1.when }
51 }
52
53 case "remind.fire":
54 if let id = event["id"] as? String {
55 let firedAt = (event["ts"] as? Int64) ?? 0
56 items = items.map { item in
57 item.id == id ? ReminderItem(id: item.id, text: item.text, when: item.when, ts: item.ts, sid: item.sid, fired: true, firedAt: firedAt) : item
58 }
59 }
60
61 case "remind.done":
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 RemindState(items: items)
72 }
73
74 func testRemindAdd() {
75 var state = RemindState()
76 let event: [String: Any] = [
77 "type": "remind.add",
78 "id": "remind-1",
79 "text": "call plumber",
80 "when": Int64(1700003600000),
81 "ts": Int64(1700000000000),
82 "sid": "sch-abc123",
83 ]
84
85 state = foldRemind(state, event: event)
86
87 XCTAssertEqual(state.items.count, 1, "should have one reminder")
88 let item = state.items[0]
89 XCTAssertEqual(item.id, "remind-1")
90 XCTAssertEqual(item.text, "call plumber")
91 XCTAssertEqual(item.when, 1700003600000, "when should be the due time")
92 XCTAssertEqual(item.ts, 1700000000000, "ts should be the add time")
93 XCTAssertEqual(item.sid, "sch-abc123", "sid should be the server schedule id")
94 XCTAssertEqual(item.fired, false, "newly added reminders are not fired")
95 XCTAssertEqual(item.firedAt, 0, "newly added reminders have no firedAt")
96 }
97
98 func testRemindFire() {
99 var state = RemindState(items: [
100 ReminderItem(id: "remind-1", text: "call plumber", when: 1700003600000, ts: 1700000000000, sid: "sch-abc123", fired: false, firedAt: 0),
101 ])
102
103 let event: [String: Any] = [
104 "type": "remind.fire",
105 "id": "remind-1",
106 "notice": [
107 "title": "⏰ Reminder",
108 "body": "call plumber",
109 ],
110 "ts": Int64(1700003600000),
111 ]
112
113 state = foldRemind(state, event: event)
114
115 XCTAssertEqual(state.items.count, 1)
116 let item = state.items[0]
117 XCTAssertEqual(item.fired, true, "item should be marked fired")
118 XCTAssertEqual(item.firedAt, 1700003600000, "firedAt should record the fire timestamp")
119 }
120
121 func testRemindDone() {
122 var state = RemindState(items: [
123 ReminderItem(id: "remind-1", text: "call plumber", when: 1700003600000, ts: 1700000000000, sid: "sch-abc123", fired: false, firedAt: 0),
124 ])
125
126 let event: [String: Any] = [
127 "type": "remind.done",
128 "id": "remind-1",
129 ]
130
131 state = foldRemind(state, event: event)
132
133 XCTAssertEqual(state.items.count, 0, "reminder should be removed")
134 }
135
136 func testRemindSorted() {
137 var state = RemindState()
138
139 let events: [[String: Any]] = [
140 [
141 "type": "remind.add",
142 "id": "remind-1",
143 "text": "late reminder",
144 "when": Int64(1700010000000),
145 "ts": Int64(1700000000000),
146 "sid": "sch-abc123",
147 ],
148 [
149 "type": "remind.add",
150 "id": "remind-2",
151 "text": "early reminder",
152 "when": Int64(1700003600000),
153 "ts": Int64(1700000100000),
154 "sid": "sch-def456",
155 ],
156 ]
157
158 for event in events {
159 state = foldRemind(state, event: event)
160 }
161
162 XCTAssertEqual(state.items.count, 2, "should have two reminders")
163 XCTAssertEqual(state.items[0].id, "remind-2", "first should be remind-2 (soonest)")
164 XCTAssertEqual(state.items[1].id, "remind-1", "second should be remind-1 (latest)")
165 XCTAssertTrue(state.items[0].when < state.items[1].when, "items should be sorted by when")
166 }
167
168 func testRemindIdempotence() {
169 var state = RemindState()
170
171 let event: [String: Any] = [
172 "type": "remind.add",
173 "id": "remind-1",
174 "text": "call plumber",
175 "when": Int64(1700003600000),
176 "ts": Int64(1700000000000),
177 "sid": "sch-abc123",
178 ]
179
180 // Fold the same event twice (e.g., duplicate delivery on reconnect)
181 state = foldRemind(state, event: event)
182 state = foldRemind(state, event: event)
183
184 XCTAssertEqual(state.items.count, 1, "should have exactly one reminder (duplicate add deduped)")
185 }
186
187 func testRemindGracePeriodPartition() {
188 // Grace period: reminders are kept visible for ~750ms after firing,
189 // then tucked into a "Previous reminders" section. This is derived in render
190 // from Date.now() and firedAt, never by a timer mutating state.
191 //
192 // Implementation note: keep the firedAt timestamp with each item; in the UI,
193 // partition items where fired && firedAt > (now - 750) into "active",
194 // others into "previous". Unfired items always stay visible.
195
196 let now: Int64 = 1700000000000
197 let gracePeriod: Int64 = 750
198
199 var state = RemindState(items: [
200 ReminderItem(id: "remind-1", text: "just fired", when: now, ts: now - 1000, sid: "sch-1", fired: true, firedAt: now - 100), // within grace
201 ReminderItem(id: "remind-2", text: "fired long ago", when: now - 2000, ts: now - 3000, sid: "sch-2", fired: true, firedAt: now - 1000), // past grace
202 ReminderItem(id: "remind-3", text: "not yet fired", when: now + 3600000, ts: now - 1000, sid: "sch-3", fired: false, firedAt: 0),
203 ])
204
205 // Partition logic
206 let active = state.items.filter { item in
207 !item.fired || (item.fired && item.firedAt > now - gracePeriod)
208 }
209 let archived = state.items.filter { item in
210 item.fired && item.firedAt <= now - gracePeriod
211 }
212
213 XCTAssertEqual(active.count, 2, "should have 2 active reminders (unfired + recently fired)")
214 XCTAssertEqual(archived.count, 1, "should have 1 archived reminder")
215 XCTAssertTrue(active.contains { $0.id == "remind-1" }, "recently fired reminder stays active")
216 XCTAssertTrue(active.contains { $0.id == "remind-3" }, "unfired reminder stays active")
217 XCTAssertTrue(archived.contains { $0.id == "remind-2" }, "old fired reminder is archived")
218 }
219
220 func testRemindOfflineScheduling() {
221 // When scheduling fails (offline), sid is empty. The client falls back to
222 // best-effort client-side firing (via a ticker). The remind item is still stored
223 // with sid: "", so any device can detect and handle failed schedules.
224
225 var state = RemindState()
226 let event: [String: Any] = [
227 "type": "remind.add",
228 "id": "remind-1",
229 "text": "call plumber",
230 "when": Int64(1700003600000),
231 "ts": Int64(1700000000000),
232 "sid": "", // empty = scheduling failed
233 ]
234
235 state = foldRemind(state, event: event)
236
237 XCTAssertEqual(state.items.count, 1)
238 let item = state.items[0]
239 XCTAssertEqual(item.sid, "", "sid should be empty for offline reminders")
240 XCTAssertEqual(item.fired, false, "should not be fired yet")
241 // Implementation note: the UI should render a visual indicator (e.g., a warning)
242 // and a ticker should fire this client-side at `when`.
243 }
244
245 func testRemindMultipleItems() {
246 var state = RemindState()
247
248 let events: [[String: Any]] = [
249 ["type": "remind.add", "id": "remind-1", "text": "standup", "when": Int64(1700000000000), "ts": Int64(1699999000000), "sid": "sch-1"],
250 ["type": "remind.add", "id": "remind-2", "text": "lunch", "when": Int64(1700003600000), "ts": Int64(1699999000000), "sid": "sch-2"],
251 ["type": "remind.add", "id": "remind-3", "text": "gym", "when": Int64(1700007200000), "ts": Int64(1699999000000), "sid": "sch-3"],
252 ]
253
254 for event in events {
255 state = foldRemind(state, event: event)
256 }
257
258 XCTAssertEqual(state.items.count, 3, "should have three reminders")
259 XCTAssertEqual(state.items[0].text, "standup", "first should be standup (soonest)")
260 XCTAssertEqual(state.items[1].text, "lunch")
261 XCTAssertEqual(state.items[2].text, "gym", "last should be gym (latest)")
262 }
263
264 func testRemindTextPreservation() {
265 let specialText = "Renew passport™ — expires 2026-08-01\n$120 fee"
266 var state = RemindState()
267
268 let event: [String: Any] = [
269 "type": "remind.add",
270 "id": "remind-1",
271 "text": specialText,
272 "when": Int64(1700003600000),
273 "ts": Int64(1700000000000),
274 "sid": "sch-abc",
275 ]
276
277 state = foldRemind(state, event: event)
278
279 XCTAssertEqual(state.items[0].text, specialText, "should preserve text exactly, including unicode and newlines")
280 }
281
282 func testRemindCancelUnfired() {
283 // Dismissing an unfired reminder should cancel its server schedule (sid).
284 // The fold just removes it; the UI layer handles the ctx.unschedule(sid) call.
285
286 var state = RemindState(items: [
287 ReminderItem(id: "remind-1", text: "call plumber", when: 1700003600000, ts: 1700000000000, sid: "sch-abc123", fired: false, firedAt: 0),
288 ])
289
290 let event: [String: Any] = [
291 "type": "remind.done",
292 "id": "remind-1",
293 ]
294
295 state = foldRemind(state, event: event)
296
297 XCTAssertEqual(state.items.count, 0, "unfired reminder should be removed")
298 // Implementation note: before folding, the UI should check `fired` and `sid`.
299 // If unfired and sid is set, send: ctx.unschedule(sid) to cancel the server schedule.
300 // The fold is just the state update; cancellation is a side effect in the UI.
301 }
302}
303