forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcold-start-cache.spec.mjs
More file actions
327 lines (292 loc) · 13.4 KB
/
Copy pathcold-start-cache.spec.mjs
File metadata and controls
327 lines (292 loc) · 13.4 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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/* eslint-disable @typescript-eslint/explicit-function-return-type -- plain-JS test harness */
// Cold-start cache-first E2E: proves the per-uid persistentCache mechanism end to
// end in the REAL built app (out/main/index.js) via Playwright's _electron.
//
// The mechanism: a surface persists its last-known rows to a per-uid localStorage
// snapshot; on the NEXT app launch it hydrates from that snapshot and renders
// instantly instead of flashing a spinner, then revalidates. This spec exercises
// the reference consumer (the Memories page) across TWO launches sharing one
// userData dir (so localStorage survives the restart — a real cold start):
// Launch 1: stub /v3/memories -> fixtures, open Memories, confirm the snapshot
// is written to localStorage under the per-uid key.
// Launch 2: ABORT /v3/memories (network down) and confirm the memories STILL
// render — they can only have come from the persisted snapshot.
//
// Fake auth (OMI_E2E_FAKE_AUTH) mounts the authed shell without Firebase, so it
// never sets omi.lastSignedInUid; the spec sets it explicitly to a fixed test uid.
import { describe, test } from 'node:test'
import assert from 'node:assert/strict'
import { _electron as electron } from 'playwright'
import { fileURLToPath } from 'node:url'
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const mainEntry = path.join(root, 'out', 'main', 'index.js')
const shotsDir = path.join(root, '.playwright-mcp', 'cold-start')
const baseEnv = {
...process.env,
OMI_E2E: '1',
OMI_E2E_FAKE_AUTH: '1',
OMI_AUTOMATION: '0',
OMI_SKIP_TUNNEL: '1'
}
const TEST_UID = 'coldstart-e2e-uid'
const SNAPSHOT_KEY = `omi.cache.memories.${TEST_UID}`
// Distinctive content so getByText can't match incidental UI copy.
const MEMORIES = [
{
id: 'cs-1',
uid: TEST_UID,
content: 'COLDSTART-FIXTURE-ALPHA is my first persisted memory.',
created_at: '2026-07-01T14:30:00Z',
updated_at: '2026-07-01T14:30:00Z'
},
{
id: 'cs-2',
uid: TEST_UID,
content: 'COLDSTART-FIXTURE-BRAVO is my second persisted memory.',
created_at: '2026-07-02T09:00:00Z',
updated_at: '2026-07-02T09:00:00Z'
}
]
const SECONDARY_HASHES = ['#/bar', '#/insight', '#/notch', '#/capture', '#/glow']
const isSecondary = (url) => SECONDARY_HASHES.some((h) => url.includes(h))
const json = (route, body) =>
route.fulfill({
status: 200,
contentType: 'application/json',
headers: { 'access-control-allow-origin': '*' },
body: JSON.stringify(body)
})
async function launch(userDataDir) {
const app = await electron.launch({
args: [mainEntry, `--user-data-dir=${userDataDir}`],
env: baseEnv
})
return app
}
async function mainPage(app) {
for (let i = 0; i < 120; i++) {
const page = (await app.windows()).find((w) => !isSecondary(w.url()))
if (page) {
const ready = await page
.evaluate(() => (document.querySelector('#root')?.childElementCount ?? 0) > 0)
.catch(() => false)
if (ready) return page
}
await new Promise((r) => setTimeout(r, 100))
}
throw new Error('main-window shell never mounted')
}
const openMemories = (page) =>
page.evaluate(() => {
window.location.hash = '#/memories'
})
describe('Cold-start cache-first — Memories renders from the persisted snapshot', () => {
test('launch 1 persists the snapshot; launch 2 renders it with the network down', async (t) => {
mkdirSync(shotsDir, { recursive: true })
// ONE userData dir shared by both launches = a real app restart.
const userDataDir = mkdtempSync(path.join(tmpdir(), 'omi-coldstart-e2e-'))
t.after(() => {
try {
rmSync(userDataDir, { recursive: true, force: true })
} catch {
/* best-effort */
}
})
// ── Launch 1: populate + persist ─────────────────────────────────────────
let app = await launch(userDataDir)
{
const page = await mainPage(app)
await page.setViewportSize({ width: 1280, height: 800 })
// Fake auth never sets the uid pointer the cache scopes on — set it here.
await page.evaluate((uid) => localStorage.setItem('omi.lastSignedInUid', uid), TEST_UID)
await page.route('**/v3/memories**', (route) => json(route, MEMORIES))
await openMemories(page)
await page
.getByText(/COLDSTART-FIXTURE-ALPHA/)
.waitFor({ state: 'visible', timeout: 20000 })
// The per-uid snapshot must now be written to localStorage.
const snapshot = await page.evaluate((k) => localStorage.getItem(k), SNAPSHOT_KEY)
assert.ok(snapshot, `launch 1 must persist the snapshot under ${SNAPSHOT_KEY}`)
const ids = JSON.parse(snapshot).map((m) => m.id)
assert.deepEqual(ids.sort(), ['cs-1', 'cs-2'], 'snapshot must hold the fetched memories')
await page.screenshot({ path: path.join(shotsDir, '01-launch1-populated.png') })
}
await app.close()
// ── Launch 2: network down, must render from the snapshot ─────────────────
app = await launch(userDataDir)
{
const page = await mainPage(app)
await page.setViewportSize({ width: 1280, height: 800 })
// Kill the memories network entirely. If memories still render, they came
// from the persisted snapshot — the whole point of cache-first cold start.
await page.route('**/v3/memories**', (route) => route.abort())
await openMemories(page)
await page
.getByText(/COLDSTART-FIXTURE-ALPHA/)
.waitFor({ state: 'visible', timeout: 20000 })
await page.getByText(/COLDSTART-FIXTURE-BRAVO/).waitFor({ state: 'visible' })
// The "No memories yet" empty state must NOT show — we have cached rows.
assert.equal(
await page.getByText('No memories yet').count(),
0,
'empty state must not show when a snapshot exists'
)
await page.screenshot({ path: path.join(shotsDir, '02-launch2-from-cache.png') })
}
await app.close()
})
})
// ── Conversations ────────────────────────────────────────────────────────────
const CONV_SNAPSHOT_KEY = `omi.cache.conversations.${TEST_UID}`
const CONVOS = [
{
id: 'cv-1',
structured: { title: 'COLDSTART-CONV-ALPHA', overview: 'first cached conversation' },
created_at: '2026-07-01T14:30:00Z'
},
{
id: 'cv-2',
structured: { title: 'COLDSTART-CONV-BRAVO', overview: 'second cached conversation' },
created_at: '2026-07-02T09:00:00Z'
}
]
const openConversations = (page) =>
page.evaluate(() => {
window.location.hash = '#/conversations'
})
// Match GET /v1/conversations exactly (not the folders sub-route) regardless of
// query string.
const isConversationsList = (url) => url.pathname.replace(/\/+$/, '') === '/v1/conversations'
describe('Cold-start cache-first — Conversations renders from the persisted snapshot', () => {
test('launch 1 persists; launch 2 keeps the cached list with the cloud fetch down', async (t) => {
mkdirSync(shotsDir, { recursive: true })
const userDataDir = mkdtempSync(path.join(tmpdir(), 'omi-coldstart-conv-e2e-'))
t.after(() => {
try {
rmSync(userDataDir, { recursive: true, force: true })
} catch {
/* best-effort */
}
})
// ── Launch 1: populate + persist ─────────────────────────────────────────
let app = await launch(userDataDir)
{
const page = await mainPage(app)
await page.setViewportSize({ width: 1280, height: 800 })
await page.evaluate((uid) => localStorage.setItem('omi.lastSignedInUid', uid), TEST_UID)
// Serve just the conversations list; other startup calls fail gracefully
// (fake auth, no tunnel) — narrow stub mirrors the Memories scenario.
await page.route(isConversationsList, (route) => json(route, CONVOS))
await openConversations(page)
await page
.getByText(/COLDSTART-CONV-ALPHA/)
.waitFor({ state: 'visible', timeout: 20000 })
const snapshot = await page.evaluate((k) => localStorage.getItem(k), CONV_SNAPSHOT_KEY)
assert.ok(snapshot, `launch 1 must persist the snapshot under ${CONV_SNAPSHOT_KEY}`)
const ids = JSON.parse(snapshot).map((r) => r.id)
assert.deepEqual(ids.sort(), ['cv-1', 'cv-2'], 'snapshot must hold the fetched conversations')
await page.screenshot({ path: path.join(shotsDir, '03-conv-launch1-populated.png') })
}
await app.close()
// ── Launch 2: cloud fetch down, must KEEP the cached list (no wipe) ───────
app = await launch(userDataDir)
{
const page = await mainPage(app)
await page.setViewportSize({ width: 1280, height: 800 })
// Abort just the conversations list so the revalidation's cloud fetch fails;
// the resilience guard must keep the hydrated rows shown.
await page.route(isConversationsList, (route) => route.abort())
await openConversations(page)
await page
.getByText(/COLDSTART-CONV-ALPHA/)
.waitFor({ state: 'visible', timeout: 20000 })
// Let the failing revalidation complete, then assert the rows did NOT get
// wiped down to empty (the bug the guard prevents).
await page.waitForTimeout(1500)
assert.ok(
await page.getByText(/COLDSTART-CONV-ALPHA/).isVisible(),
'cached conversation must stay on screen after a failed revalidation'
)
assert.ok(
await page.getByText(/COLDSTART-CONV-BRAVO/).isVisible(),
'all cached conversations must stay on screen after a failed revalidation'
)
await page.screenshot({ path: path.join(shotsDir, '04-conv-launch2-from-cache.png') })
}
await app.close()
})
})
// ── Apps / Marketplace ───────────────────────────────────────────────────────
const APPS_SNAPSHOT_KEY = `omi.cache.apps.${TEST_UID}`
// Minimal /v2/apps catalog: one 'popular' group with a distinctively named app.
const APPS_CATALOG = {
groups: [
{
capability: { id: 'popular', title: 'Popular' },
data: [{ id: 'coldstart-app', name: 'COLDSTART-APP-ALPHA', category: 'other' }]
}
]
}
const openApps = (page) =>
page.evaluate(() => {
window.location.hash = '#/apps'
})
const isAppsCatalog = (url) => url.pathname.replace(/\/+$/, '') === '/v2/apps'
const isAppsList = (url) => url.pathname.replace(/\/+$/, '') === '/v1/apps'
const isAppsEnabled = (url) => url.pathname.replace(/\/+$/, '') === '/v1/apps/enabled'
describe('Cold-start cache-first — Apps renders from the persisted snapshot', () => {
test('launch 1 persists; launch 2 keeps the cached grid with the catalog down', async (t) => {
mkdirSync(shotsDir, { recursive: true })
const userDataDir = mkdtempSync(path.join(tmpdir(), 'omi-coldstart-apps-e2e-'))
t.after(() => {
try {
rmSync(userDataDir, { recursive: true, force: true })
} catch {
/* best-effort */
}
})
// ── Launch 1: populate + persist ─────────────────────────────────────────
let app = await launch(userDataDir)
{
const page = await mainPage(app)
await page.setViewportSize({ width: 1280, height: 800 })
await page.evaluate((uid) => localStorage.setItem('omi.lastSignedInUid', uid), TEST_UID)
await page.route(isAppsCatalog, (route) => json(route, APPS_CATALOG))
await page.route(isAppsList, (route) => json(route, []))
await page.route(isAppsEnabled, (route) => json(route, []))
await openApps(page)
await page
.getByText(/COLDSTART-APP-ALPHA/)
.waitFor({ state: 'visible', timeout: 20000 })
const snapshot = await page.evaluate((k) => localStorage.getItem(k), APPS_SNAPSHOT_KEY)
assert.ok(snapshot, `launch 1 must persist the snapshot under ${APPS_SNAPSHOT_KEY}`)
const names = JSON.parse(snapshot).allApps.map((a) => a.name)
assert.ok(names.includes('COLDSTART-APP-ALPHA'), 'snapshot must hold the fetched apps')
await page.screenshot({ path: path.join(shotsDir, '05-apps-launch1-populated.png') })
}
await app.close()
// ── Launch 2: catalog down, must render the cached grid ───────────────────
app = await launch(userDataDir)
{
const page = await mainPage(app)
await page.setViewportSize({ width: 1280, height: 800 })
// Abort the catalog fetch (the one call load() doesn't .catch) so the load
// fails; the cached grid must stay on screen (load only overwrites on success).
await page.route(isAppsCatalog, (route) => route.abort())
await openApps(page)
await page
.getByText(/COLDSTART-APP-ALPHA/)
.waitFor({ state: 'visible', timeout: 20000 })
await page.waitForTimeout(1200)
assert.ok(
await page.getByText(/COLDSTART-APP-ALPHA/).isVisible(),
'cached app must stay on screen after a failed catalog fetch'
)
await page.screenshot({ path: path.join(shotsDir, '06-apps-launch2-from-cache.png') })
}
await app.close()
})
})