forked from OurHike/OurHike
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwrongWayAlert.test.ts
More file actions
172 lines (131 loc) · 5.69 KB
/
Copy pathwrongWayAlert.test.ts
File metadata and controls
172 lines (131 loc) · 5.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createWrongWayMonitor } from './wrongWayAlert'
import { CUE_PERSISTENCE_MS, PUSH_PERSISTENCE_MS } from './wrongWay'
// WIREFRAMES.md §9, wiring D4's pure detection math to the push chokepoint.
//
// Three properties matter more than the rest:
//
// 1. A CUE NEVER PUSHES. The in-app cue is the conservative first beat;
// escalating to an interrupt is a separate, later decision. If the cue
// could push, the whole graduated design collapses into one alarm.
//
// 2. IT WON'T ASK AGAIN. WIREFRAMES.md says so outright. `detectWrongWay`
// reports 'push' for every sample once divergence is sustained, so
// without suppression this would fire on every GPS tick - turning the one
// notification OurHike sends into a stream of them, which is the fastest
// possible way to lose the trust it was designed around.
//
// 3. THE ALERT IS LOCAL. Detection is local maths and the notification is
// local; the backend relay is telemetry. Being offline is the normal case
// on this trail, and it must never suppress the alert.
const diverging = (atMs: number) => ({
timestampMs: atMs,
distanceFromTrailFt: 150,
bearingDeltaDeg: 175,
})
const onTrack = (atMs: number) => ({
timestampMs: atMs,
distanceFromTrailFt: 10,
bearingDeltaDeg: 0,
})
const CUE_TRACE = [diverging(0), diverging(CUE_PERSISTENCE_MS + 1000)]
const PUSH_TRACE = [...CUE_TRACE, diverging(PUSH_PERSISTENCE_MS + 1000)]
function monitor(overrides: Record<string, unknown> = {}) {
const publish = vi.fn().mockResolvedValue(true)
const relay = vi.fn().mockResolvedValue(undefined)
const instance = createWrongWayMonitor(
{ enabled: true, hikeId: 'h1', direction: 'NOBO', ...overrides },
{ publish, relay },
)
return { instance, publish, relay }
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('createWrongWayMonitor', () => {
it('does nothing while on track', async () => {
const { instance, publish } = monitor()
const outcome = await instance.observe([onTrack(0), onTrack(60_000)])
expect(outcome).toMatchObject({ cue: false, pushed: false })
expect(publish).not.toHaveBeenCalled()
})
it('raises an in-app cue without ever pushing', async () => {
const { instance, publish } = monitor()
const outcome = await instance.observe(CUE_TRACE)
expect(outcome.cue).toBe(true)
expect(outcome.pushed).toBe(false)
expect(publish).not.toHaveBeenCalled()
})
it('pushes once divergence is sustained past the push threshold', async () => {
const { instance, publish } = monitor()
const outcome = await instance.observe(PUSH_TRACE)
expect(outcome.pushed).toBe(true)
expect(publish).toHaveBeenCalledTimes(1)
})
it('does not ask again for the same episode', async () => {
// The trace keeps reporting 'push' on every later sample; only the first
// should reach the notification.
const { instance, publish } = monitor()
await instance.observe(PUSH_TRACE)
await instance.observe([...PUSH_TRACE, diverging(PUSH_PERSISTENCE_MS + 60_000)])
expect(publish).toHaveBeenCalledTimes(1)
})
it('may push again after the hiker gets back on track and later diverges anew', async () => {
const { instance, publish } = monitor()
await instance.observe(PUSH_TRACE)
await instance.observe([onTrack(PUSH_PERSISTENCE_MS + 120_000)])
await instance.observe([
diverging(PUSH_PERSISTENCE_MS + 200_000),
diverging(PUSH_PERSISTENCE_MS * 2 + 400_000),
])
expect(publish).toHaveBeenCalledTimes(2)
})
it('mentions the hike direction, so the alert says why it thinks this', async () => {
const { instance, publish } = monitor()
await instance.observe(PUSH_TRACE)
expect(publish.mock.calls[0][0].body).toMatch(/NOBO/)
})
it('stays silent entirely when the hiker has turned the alert off', async () => {
const { instance, publish } = monitor({ enabled: false })
const outcome = await instance.observe(PUSH_TRACE)
expect(outcome).toMatchObject({ cue: false, pushed: false })
expect(publish).not.toHaveBeenCalled()
})
it('still alerts when the backend relay fails - the alert is local', async () => {
const publish = vi.fn().mockResolvedValue(true)
const relay = vi.fn().mockRejectedValue(new TypeError('Failed to fetch'))
const instance = createWrongWayMonitor(
{ enabled: true, hikeId: 'h1', direction: 'NOBO' },
{ publish, relay },
)
const outcome = await instance.observe(PUSH_TRACE)
expect(outcome.pushed).toBe(true)
expect(publish).toHaveBeenCalledTimes(1)
})
it('never rejects because the relay did', async () => {
const publish = vi.fn().mockResolvedValue(true)
const relay = vi.fn().mockRejectedValue(new TypeError('Failed to fetch'))
const instance = createWrongWayMonitor(
{ enabled: true, hikeId: 'h1', direction: 'NOBO' },
{ publish, relay },
)
await expect(instance.observe(PUSH_TRACE)).resolves.toBeDefined()
})
it('relays the event when a hike is under way', async () => {
const { instance, relay } = monitor()
await instance.observe(PUSH_TRACE)
expect(relay).toHaveBeenCalledWith(expect.objectContaining({ hikeId: 'h1' }))
})
it('skips the relay when there is no hike to attach it to', async () => {
const { instance, relay, publish } = monitor({ hikeId: null })
await instance.observe(PUSH_TRACE)
expect(relay).not.toHaveBeenCalled()
// ...but the alert itself is unaffected.
expect(publish).toHaveBeenCalledTimes(1)
})
it('does not relay a mere cue - only a real escalation is worth reporting', async () => {
const { instance, relay } = monitor()
await instance.observe(CUE_TRACE)
expect(relay).not.toHaveBeenCalled()
})
})