ReviewOS

also looking at this

stacks/wildloop

feat: native trail maps and complete native mobile screens

#1017
Draft chris wants to merge codex/trail-experience into main
60 files +2505 -823

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

tests/unit/native-record-screen.test.tsmodified+57-1
Changes to tests/unit/native-record-screen.test.ts
@@ -11,7 +11,7 @@ async function recover(userId=7, keychain=new Map([['auth_token','record-token']
1111}
1212describe('native Record checkpoint recovery',()=>{
1313 beforeAll(compileNativeBundle,30000)
14 it('durably registers a system recording ID before starting native GPS',async()=>{
14 it('registers a system ID before GPS and retains it through pause, resume and save',async()=>{
1515 const screen=await start('Record',{},['fetch','secureStorage','storage','storageCompareAndSet','storageSetIfAbsent','crypto','geolocation','locationRecordingOwnership','locationRecordingIntentIdentity','lifecycle','liveActivity'],new Map([['auth_token','record-token'],['wildloop-account',JSON.stringify({token:'record-token',user:{id:7}})]]))
1616 const stored=await waitForRequest(screen,0,r=>r.payload.module==='Storage' && r.payload.method==='get')
1717 answer(screen,stored,null)
@@ -29,6 +29,62 @@ describe('native Record checkpoint recovery',()=>{
2929 answer(screen,registration,true)
3030 const nativeStart=await waitForRequest(screen,0,r=>r.payload.method==='startRecording')
3131 expect(nativeStart.payload.args[0]).toMatchObject({recordingId:'782f0d4a-f626-41bc-8f17-2f67e9a099bf'})
32 const id='782f0d4a-f626-41bc-8f17-2f67e9a099bf'
33 const startedAt=Date.now()-30000
34 const locations=[{latitude:35.699,longitude:-82.38,timestamp:startedAt+1000,accuracy:5},{latitude:35.6991,longitude:-82.3801,timestamp:startedAt+2000,accuracy:5}]
35 answer(screen,nativeStart,{id,active:true,paused:false,startedAt})
36 const started=await waitForRequest(screen,requests(screen).indexOf(nativeStart)+1,r=>r.payload.method==='compareAndSet')
37 expect(started.payload.args[2]).toMatchObject({id,phase:'recording',startedAt})
38 answer(screen,started,true)
39 await waitForText(screen,'Recording continues with your phone locked.')
40 let before=requests(screen).length
41 await screen.event('record-pause','onPress')
42 const pause=await waitForRequest(screen,before,r=>r.payload.method==='pauseRecording')
43 expect(pause.payload.args[0]).toEqual({expectedId:id})
44 answer(screen,pause,{id,active:true,paused:true,startedAt})
45 const read=await waitForRequest(screen,before,r=>r.payload.method==='readRecording')
46 expect(read.payload.args[0]).toEqual({expectedId:id})
47 answer(screen,read,locations)
48 const paused=await waitForRequest(screen,before,r=>r.payload.method==='compareAndSet')
49 expect(paused.payload.args[2].pauses).toHaveLength(1)
50 answer(screen,paused,true)
51 await Bun.sleep(0)
52 before=requests(screen).length
53 await screen.event('record-pause','onPress')
54 const resume=await waitForRequest(screen,before,r=>r.payload.method==='resumeRecording')
55 expect(resume.payload.args[0]).toEqual({expectedId:id})
56 answer(screen,resume,{id,active:true,paused:false,startedAt})
57 const resumed=await waitForRequest(screen,before,r=>r.payload.method==='compareAndSet')
58 expect(resumed.payload.args[2].pauses[0].end).toBeNumber()
59 answer(screen,resumed,true)
60 await Bun.sleep(0)
61 before=requests(screen).length
62 await screen.event('record-finish','onPress')
63 const stop=await waitForRequest(screen,before,r=>r.payload.method==='stopRecording')
64 expect(stop.payload.args[0]).toEqual({expectedId:id})
65 answer(screen,stop,{id,active:false,paused:false,startedAt,locations})
66 const stopped=await waitForRequest(screen,before,r=>r.payload.method==='compareAndSet')
67 expect(stopped.payload.args[2]).toMatchObject({id,phase:'stopped'})
68 answer(screen,stopped,true)
69 const pending=await waitForRequest(screen,requests(screen).indexOf(stopped)+1,r=>r.payload.method==='compareAndSet')
70 expect(pending.payload.args[2].pending.upload_id).toBe(`run:${id}`)
71 expect(requests(screen).some(r=>r.payload.method==='acknowledgeRecording')).toBe(false)
72 answer(screen,pending,true)
73 const me=await waitForRequest(screen,before,r=>r.payload.module==='Network')
74 answer(screen,me,ok({user:{id:7}}))
75 const post=await waitForRequest(screen,requests(screen).indexOf(me)+1,r=>r.payload.module==='Network')
76 expect(JSON.parse(post.payload.args[0].body).upload_id).toBe(`run:${id}`)
77 answer(screen,post,ok({activity:{id:99,captureEligible:false}},201))
78 const ack=await waitForRequest(screen,before,r=>r.payload.method==='acknowledgeRecording')
79 expect(ack.payload.args[0]).toEqual({expectedId:id})
80 answer(screen,ack,true)
81 const clear=await waitForRequest(screen,requests(screen).indexOf(pending)+1,r=>r.payload.method==='compareAndSet')
82 expect(clear.payload.args[1]).toEqual(pending.payload.args[2])
83 expect(clear.payload.args[2]).toBeNull()
84 answer(screen,clear,true)
85 await Bun.sleep(0)
86 expect(screen.sent.some(r=>r.type==='NAVIGATE_OPEN' && JSON.stringify(r.payload).includes('/activity/99'))).toBe(true)
87 expect(requests(screen).some(r=>r.payload.method==='set' || r.payload.method==='remove')).toBe(false)
3288 })
3389 it('recovers a finished upload offline without starting another GPS recording',async()=>{
3490 const screen=await recover()