| 1 | { |
| 2 | "description": "Reminders app event vectors: ensures remind fold handlers produce consistent state. Source of truth: web/static/apps/remind.js", |
| 3 | "app": "remind", |
| 4 | "source": "web/static/apps/remind.js", |
| 5 | "state_shape": { "items": [{ "id": "string", "text": "string", "when": "number (epoch ms, due time)", "ts": "number (epoch ms, add time)", "sid": "string (server schedule id, empty if offline)", "fired": "boolean", "firedAt": "number (epoch ms)" }] }, |
| 6 | "vectors": [ |
| 7 | { |
| 8 | "name": "remind_add", |
| 9 | "description": "Add a reminder with a future due time (sid = server schedule id, empty if offline).", |
| 10 | "event": { |
| 11 | "type": "remind.add", |
| 12 | "id": "remind-1", |
| 13 | "text": "call plumber", |
| 14 | "when": 1700003600000, |
| 15 | "ts": 1700000000000, |
| 16 | "sid": "sch-abc123" |
| 17 | }, |
| 18 | "expected_state_update": { |
| 19 | "items": [ |
| 20 | { |
| 21 | "id": "remind-1", |
| 22 | "text": "call plumber", |
| 23 | "when": 1700003600000, |
| 24 | "ts": 1700000000000, |
| 25 | "sid": "sch-abc123", |
| 26 | "fired": false, |
| 27 | "firedAt": 0 |
| 28 | } |
| 29 | ] |
| 30 | }, |
| 31 | "note": "Items are sorted by `when` (soonest first). `sid` is the server's scheduled-message id; empty string if scheduling failed (client falls back to best-effort firing). `when` is absolute epoch ms. No chat projection (reminders are silent in chat)." |
| 32 | }, |
| 33 | { |
| 34 | "name": "remind_fire", |
| 35 | "description": "A scheduled reminder fires (delivered at the due time, either by server schedule or client best-effort).", |
| 36 | "event": { |
| 37 | "type": "remind.fire", |
| 38 | "id": "remind-1", |
| 39 | "notice": { |
| 40 | "title": "⏰ Reminder", |
| 41 | "body": "call plumber" |
| 42 | }, |
| 43 | "ts": 1700003600000 |
| 44 | }, |
| 45 | "initial_state": { |
| 46 | "items": [ |
| 47 | { |
| 48 | "id": "remind-1", |
| 49 | "text": "call plumber", |
| 50 | "when": 1700003600000, |
| 51 | "ts": 1700000000000, |
| 52 | "sid": "sch-abc123", |
| 53 | "fired": false, |
| 54 | "firedAt": 0 |
| 55 | } |
| 56 | ] |
| 57 | }, |
| 58 | "expected_state_update": { |
| 59 | "items": [ |
| 60 | { |
| 61 | "id": "remind-1", |
| 62 | "text": "call plumber", |
| 63 | "when": 1700003600000, |
| 64 | "ts": 1700000000000, |
| 65 | "sid": "sch-abc123", |
| 66 | "fired": true, |
| 67 | "firedAt": 1700003600000 |
| 68 | } |
| 69 | ] |
| 70 | }, |
| 71 | "expected_chat_projection": { |
| 72 | "text": "⏰ Reminder: call plumber", |
| 73 | "action": false |
| 74 | }, |
| 75 | "note": "`notice` has the title + body shown by push/toast (the text the user sees on alert). `firedAt` is the server delivery time (for grace-period animation, same 750ms linger-then-tuck as todo). When this event lands in the chat thread, it's surfaced as a message (not an action line) with the title+body from the notice." |
| 76 | }, |
| 77 | { |
| 78 | "name": "remind_done", |
| 79 | "description": "Dismiss a reminder (removes it from the list; if unfired, also cancels the server schedule via the sid).", |
| 80 | "event": { |
| 81 | "type": "remind.done", |
| 82 | "id": "remind-1" |
| 83 | }, |
| 84 | "initial_state": { |
| 85 | "items": [ |
| 86 | { |
| 87 | "id": "remind-1", |
| 88 | "text": "call plumber", |
| 89 | "when": 1700003600000, |
| 90 | "ts": 1700000000000, |
| 91 | "sid": "sch-abc123", |
| 92 | "fired": false, |
| 93 | "firedAt": 0 |
| 94 | } |
| 95 | ] |
| 96 | }, |
| 97 | "expected_state_update": { |
| 98 | "items": [] |
| 99 | }, |
| 100 | "note": "If `fired=false` and `sid` is set, the client sends an unschedule request to cancel the server's scheduled remind.fire. If the cancel races the delivery, the late remind.fire folds onto a missing id (a no-op)." |
| 101 | }, |
| 102 | { |
| 103 | "name": "remind_grace_period_partition", |
| 104 | "description": "Grace period: active (unfired or recently fired within 750ms) vs. archived (fired + >750ms ago).", |
| 105 | "note": "Like todo, this is derived in render from Date.now() and firedAt, never by a timer. Native should partition: unfired items + recently-fired (firedAt > now - 750ms) stay visible; older fired reminders move to 'Previous reminders' section.", |
| 106 | "constants": { |
| 107 | "REMIND_GRACE_MS": 750 |
| 108 | } |
| 109 | }, |
| 110 | { |
| 111 | "name": "remind_multiple_sorted", |
| 112 | "description": "Multiple reminders are kept sorted by `when` (soonest first), regardless of add order.", |
| 113 | "event_sequence": [ |
| 114 | { |
| 115 | "type": "remind.add", |
| 116 | "id": "remind-1", |
| 117 | "text": "late one", |
| 118 | "when": 1700010000000, |
| 119 | "ts": 1700000000000, |
| 120 | "sid": "sch-abc123" |
| 121 | }, |
| 122 | { |
| 123 | "type": "remind.add", |
| 124 | "id": "remind-2", |
| 125 | "text": "early one", |
| 126 | "when": 1700003600000, |
| 127 | "ts": 1700000100000, |
| 128 | "sid": "sch-def456" |
| 129 | } |
| 130 | ], |
| 131 | "expected_final_state": { |
| 132 | "items": [ |
| 133 | { |
| 134 | "id": "remind-2", |
| 135 | "text": "early one", |
| 136 | "when": 1700003600000, |
| 137 | "ts": 1700000100000, |
| 138 | "sid": "sch-def456", |
| 139 | "fired": false, |
| 140 | "firedAt": 0 |
| 141 | }, |
| 142 | { |
| 143 | "id": "remind-1", |
| 144 | "text": "late one", |
| 145 | "when": 1700010000000, |
| 146 | "ts": 1700000000000, |
| 147 | "sid": "sch-abc123", |
| 148 | "fired": false, |
| 149 | "firedAt": 0 |
| 150 | } |
| 151 | ] |
| 152 | }, |
| 153 | "note": "The list is re-sorted on every remind.add, so the order is always soonest-first regardless of user input order." |
| 154 | } |
| 155 | ] |
| 156 | } |
| 157 | |