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-manual-activity-flow.test.tsmodified+38-1
Changes to tests/unit/native-manual-activity-flow.test.ts
@@ -1,13 +1,50 @@
11import { beforeAll, describe, expect, it } from 'bun:test'
22import { readFileSync } from 'node:fs'
33import { join } from 'node:path'
4import { answer, compileNativeBundle, nativeBundle, nodesByTestId, requests, start, waitForRequest } from '../fixtures/nativeScreen'
4import { answer, compileNativeBundle, nativeBundle, nativeNodeProps, nodesByTestId, requests, start, waitForRequest, waitForText } from '../fixtures/nativeScreen'
55
66const ROOT = join(import.meta.dir, '../..')
77
88beforeAll(compileNativeBundle, 30000)
99
1010describe('Wildloop native manual activity flow', () => {
11 it('keeps a system UUID and the exact manual payload when retrying a failed save',async()=>{
12 const manual=await start('ManualActivity',{},['fetch','secureStorage','crypto','haptics','lifecycle','dialog'],new Map([['auth_token','native-token']]))
13 await manual.event('distance-input','onChangeText',{text:'3.1'})
14 await manual.event('duration-input','onChangeText',{text:'28:00'})
15 await manual.event('save-activity','onPress')
16 const uuid=await waitForRequest(manual,0,r=>r.payload.module==='Crypto' && r.payload.method==='randomUUID')
17 answer(manual,uuid,'2b53ece4-8df1-49fa-9e70-752ddc901cf7')
18 const post=await waitForRequest(manual,0,r=>r.payload.module==='Network')
19 expect(JSON.parse(post.payload.args[0].body).upload_id).toBe('manual:2b53ece4-8df1-49fa-9e70-752ddc901cf7')
20 answer(manual,post,{status:503,body:JSON.stringify({message:'Try again later'})})
21 await waitForText(manual,'Try again later')
22 const before=requests(manual).length
23 await manual.event('save-activity','onPress')
24 const again=await waitForRequest(manual,before,r=>r.payload.module==='Network')
25 expect(again.payload.args[0].body).toBe(post.payload.args[0].body)
26 expect(requests(manual).filter(r=>r.payload.module==='Crypto')).toHaveLength(1)
27 })
28 it('clears private manual notes and ignores a save response after an account change',async()=>{
29 const keychain=new Map([['auth_token','native-token']])
30 const manual=await start('ManualActivity',{},['fetch','secureStorage','crypto','haptics','lifecycle','dialog'],keychain)
31 await manual.event('distance-input','onChangeText',{text:'3'})
32 await manual.event('duration-input','onChangeText',{text:'30:00'})
33 await manual.event('notes-input','onChangeText',{text:'private route notes'})
34 await manual.event('save-activity','onPress')
35 // The original screen has no UUID request. Answer one only when the real
36 // host protocol asks for it, so both versions reach the held HTTP response.
37 const uuid=requests(manual).find(r=>r.payload.module==='Crypto')
38 if(uuid) answer(manual,uuid,'b6506ad4-d9d2-40df-8a32-bf4194adce26')
39 const post=await waitForRequest(manual,0,r=>r.payload.module==='Network')
40 keychain.set('auth_token','other-token')
41 await manual.scope.craft.snapshots.set('wildloop-session',{user:{id:13},version:1})
42 answer(manual,post,{status:201,body:JSON.stringify({activity:{id:42,userId:12}})})
43 await Bun.sleep(0)
44 expect(nativeNodeProps(manual,'notes-input').value).toBe('')
45 expect(requests(manual).some(r=>r.payload.module==='Haptics')).toBe(false)
46 expect(manual.sent.some(r=>r.type==='NAVIGATE')).toBe(false)
47 })
1148 it('uses app components and sends the real activity payload through the Craft protocol', async () => {
1249 expect(nativeBundle()).not.toContain('WebView')
1350 expect(readFileSync(join(ROOT, 'resources/native/ManualActivity.stx'), 'utf8')).toContain('<NativeRunMetric')