ReviewOS

also looking at this

craft-native/craft

feat(maps): add key-free native Android maps and shared map controls

#363
Merged chris wants to merge codex/wildloop-native-map-providers into main
28 files +1857 -481

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.

packages/typescript/src/__tests__/android-adb.test.tsadded+65-0
Changes to packages/typescript/src/__tests__/android-adb.test.ts
@@ -0,0 +1,65 @@
1import { afterEach, expect, it } from 'bun:test'
2import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
3import { tmpdir } from 'node:os'
4import { join } from 'node:path'
5import { requireAndroidEmulatorRoot } from '../android-adb'
6
7const roots: string[] = []
8afterEach(async () => {
9 for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
10})
11
12async function fixture(rootReply: 'closed' | 'denied' | 'success', uid = '0', reconnect = true) {
13 const root = await mkdtemp(join(tmpdir(), 'craft-adb-root-'))
14 roots.push(root)
15 const adb = join(root, 'adb')
16 const log = join(root, 'commands.jsonl')
17 await writeFile(adb, `#!/usr/bin/env bun
18import { appendFileSync } from 'node:fs'
19const args = process.argv.slice(2)
20appendFileSync(${JSON.stringify(log)}, JSON.stringify(args)+'\\n')
21if (args[2] === 'root') {
22 if (${JSON.stringify(rootReply)} === 'closed') { console.error('adb: unable to connect for root: closed'); process.exit(1) }
23 if (${JSON.stringify(rootReply)} === 'denied') { console.error('adbd cannot run as root in production builds'); process.exit(1) }
24 console.log('restarting adbd as root')
25}
26if (args[2] === 'wait-for-device' && !${reconnect}) { console.error('transport unavailable'); process.exit(1) }
27if (args[2] === 'shell') console.log(${JSON.stringify(uid)})
28`)
29 await chmod(adb, 0o755)
30 return { adb, commands: async () => (await readFile(log, 'utf8')).trim().split('\n').map(line => JSON.parse(line)) }
31}
32
33it('reconnects after the root daemon closes the transport, then proves root without repeating the request', async () => {
34 const test = await fixture('closed')
35 await requireAndroidEmulatorRoot(test.adb, 'emulator-5582')
36 expect(await test.commands()).toEqual([
37 ['-s', 'emulator-5582', 'root'],
38 ['-s', 'emulator-5582', 'wait-for-device'],
39 ['-s', 'emulator-5582', 'shell', 'id', '-u'],
40 ])
41})
42
43it('requires root after an acknowledged daemon restart too', async () => {
44 const test = await fixture('success')
45 await requireAndroidEmulatorRoot(test.adb, 'emulator-5582')
46 expect((await test.commands()).at(-1)).toEqual(['-s', 'emulator-5582', 'shell', 'id', '-u'])
47})
48
49it('rejects production images without attempting to reconnect or fault their storage', async () => {
50 const test = await fixture('denied')
51 await expect(requireAndroidEmulatorRoot(test.adb, 'emulator-5582')).rejects.toThrow('production builds')
52 expect((await test.commands()).length).toBe(1)
53})
54
55it('rejects a reconnected daemon that still runs as the shell user', async () => {
56 const test = await fixture('closed', '2000')
57 await expect(requireAndroidEmulatorRoot(test.adb, 'emulator-5582')).rejects.toThrow('rootable')
58})
59
60it('preserves reconnect failures and refuses physical devices before executing ADB', async () => {
61 const test = await fixture('closed', '0', false)
62 await expect(requireAndroidEmulatorRoot(test.adb, 'emulator-5582')).rejects.toThrow('transport unavailable')
63 await expect(requireAndroidEmulatorRoot(test.adb, 'phone')).rejects.toThrow('emulator serial')
64 expect((await test.commands()).length).toBe(2)
65})
packages/typescript/src/android-adb.tsadded+24-0
Changes to packages/typescript/src/android-adb.ts
@@ -0,0 +1,24 @@
1/** Restart adbd once and require a rooted, explicitly selected disposable emulator. */
2export async function requireAndroidEmulatorRoot(adb: string, serial: string, timeoutMs = 60_000): Promise<void> {
3 if (!/^emulator-\d+$/.test(serial)) throw new Error('An explicit disposable Android emulator serial is required')
4 const deadline = Date.now() + timeoutMs
5 async function run(args: string[]): Promise<{ code: number, output: string }> {
6 const remaining = deadline - Date.now()
7 if (remaining <= 0) throw new Error('Android root reconnect deadline exceeded')
8 const child = Bun.spawn([adb, '-s', serial, ...args], { stdout: 'pipe', stderr: 'pipe', timeout: remaining })
9 const [code, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()])
10 return { code, output: `${stdout}\n${stderr}`.trim() }
11 }
12 const root = await run(['root'])
13 // Root changes restart adbd. Some versions close the transport before they
14 // acknowledge the request; that response requires reconnection and UID proof.
15 if (root.code !== 0 && !/^adb: unable to connect for root: closed$/.test(root.output)) {
16 throw new Error(`adb root failed (${root.code}): ${root.output}`)
17 }
18 const connected = await run(['wait-for-device'])
19 if (connected.code !== 0) throw new Error(`Android root reconnect failed (${connected.code}): ${connected.output}`)
20 const identity = await run(['shell', 'id', '-u'])
21 if (identity.code !== 0 || identity.output !== '0') {
22 throw new Error(`Storage fault tests require a rootable disposable AOSP emulator: ${identity.output}`)
23 }
24}
packages/typescript/src/mobile-media-testing.tsmodified+2-3
Changes to packages/typescript/src/mobile-media-testing.ts
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs'
33import { mkdir, mkdtemp, rm } from 'node:fs/promises'
44import { homedir, tmpdir } from 'node:os'
55import { isAbsolute, join } from 'node:path'
6import { requireAndroidEmulatorRoot } from './android-adb.js'
67
78export interface MobileMediaCacheTestOptions {
89 platform: 'ios' | 'android'
@@ -165,9 +166,7 @@ export async function verifyMobileMediaCache(options: MobileMediaCacheTestOption
165166 if (!interrupted && (!result.includes('PASS:') || result.includes('FAIL:'))) throw new Error(result)
166167 print(result)
167168 }
168 await run([adb, '-s', serial, 'root'])
169 await run([adb, '-s', serial, 'wait-for-device'])
170 if ((await run([adb, '-s', serial, 'shell', 'id', '-u'])).trim() !== '0') throw new Error('Storage fault tests require a rootable disposable AOSP emulator')
169 await requireAndroidEmulatorRoot(adb, serial)
171170 await run([adb, '-s', serial, 'shell', 'run-as', packageName, 'mkdir', '-p', 'files/craft-media'])
172171 const cache = `/data/user/0/${packageName}/files/craft-media`
173172 const owner = (await run([adb, '-s', serial, 'shell', 'stat', '-c', '%u:%g', cache])).trim()