ReviewOS

also looking at this

craft-native/craft

fix(native): preserve workspace sources and native overlay lifecycle

#384
Merged chris wants to merge codex/windows-zig-cache-diagnostics into main
20 files +500 -227

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/android/README.mdmodified+3-2
Changes to packages/android/README.md
@@ -110,9 +110,10 @@ The stock Play software CI verifier uses that cold-provisioning profile: the
110110API 35 image took 943,520 ms in the retained diagnostic run
111111[39113277828](https://github.com/craft-native/craft/actions/runs/39113277828).
112112This is OS provisioning before app installation; app and performance budgets
113are unchanged. Both modes require a readable event-log stream, SettingsProvider,
113are unchanged. Both modes require a complete event-history read, SettingsProvider,
114114the package manager and System UI to be ready, with HOME resolved beyond the
115automatic Android SDK setup activity. Logs record the budget, actual
115automatic Android SDK setup activity and its launcher package owning the current
116input-focus window. Logs record the budget, actual
116117boot duration, host CPU interval, selected image, acceleration
117118probe, process identity and boot result. Disposal reaps only the owned process
118119and deletes only its disposable AVD; the managed SDK and other devices remain
packages/android/src/emulator.tsmodified+8-2
Changes to packages/android/src/emulator.ts
@@ -168,8 +168,9 @@ export async function createAndroidEmulator(options: AndroidEmulatorOptions): Pr
168168 const systemUi = await run([adb, '-s', serial, 'shell', 'pidof', 'com.android.systemui'], undefined, probeTimeout(), true)
169169 // Native acceptance must be able to observe platform crashes before
170170 // launching any app. boot_completed/System UI can precede logd's ADB
171 // event stream; declaring readiness then loses that first health check.
172 const events = await run([adb, '-s', serial, 'logcat', '-b', 'events', '-d', '-t', '1'], undefined, probeTimeout(), true)
171 // event stream. Require a complete history read so a partial transport
172 // read cannot admit the device before its first health check.
173 const events = await run([adb, '-s', serial, 'logcat', '-b', 'events', '-d', '-v', 'threadtime'], undefined, probeTimeout(), true)
173174 // AOSP's first boot can report all those services ready while its
174175 // automatic SDK setup activity still owns HOME. Launching HOME then
175176 // opens that transient activity; after it finishes UI automation has
@@ -180,10 +181,15 @@ export async function createAndroidEmulator(options: AndroidEmulatorOptions): Pr
180181 const temporaryHome = homeActivity.startsWith('com.android.sdksetup/')
181182 || /^com\.android\.settings\/(?:\.|com\.android\.settings\.)FallbackHome$/.test(homeActivity)
182183 || /^android\/.*(?:Resolver|Chooser)Activity$/.test(homeActivity)
184 // Resolving HOME can precede its first input window during cold
185 // provisioning. Hand off only once the launcher actually has focus.
186 const windows = await run([adb, '-s', serial, 'shell', 'dumpsys', 'window'], undefined, probeTimeout(), true)
187 const focusedPackage = windows.stdout.match(/^\s*mCurrentFocus\s*=\s*Window\{[^}\r\n]*\bu\d+\s+([\w.]+)\//m)?.[1]
183188 booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0
184189 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim())
185190 && (!requiresPlayStore || (playStore?.code === 0 && /^package:\/\S+/m.test(playStore.stdout)))
186191 && home.code === 0 && /^[\w.]+\/[\w.$]+$/.test(homeActivity) && !temporaryHome
192 && windows.code === 0 && focusedPackage === homeActivity.split('/')[0]
187193 }
188194 }
189195 const checkedAt = Date.now()
packages/typescript/src/__tests__/android-emulator.test.tsmodified+46-2
Changes to packages/typescript/src/__tests__/android-emulator.test.ts
@@ -5,7 +5,7 @@ import { join } from 'node:path'
55import { createAndroidEmulator } from '../../../android/src/emulator'
66import type { AndroidEmulatorOptions } from '../../../android/src/emulator'
77
8function fixture(hardware: boolean, delayedSettings = false, delayedEvents = false, delayedHome = false, connection: 'normal' | 'stale' | 'offline' = 'normal', bootstrapHome = 'com.android.sdksetup/.DefaultActivity', playStore: 'ready' | 'delayed' | 'missing' = 'ready'): { root: string, options: AndroidEmulatorOptions } {
8function fixture(hardware: boolean, delayedSettings = false, delayedEvents = false, delayedHome = false, connection: 'normal' | 'stale' | 'offline' = 'normal', bootstrapHome = 'com.android.sdksetup/.DefaultActivity', playStore: 'ready' | 'delayed' | 'missing' = 'ready', homeFocus: 'ready' | 'delayed' | 'foreign' = 'ready'): { root: string, options: AndroidEmulatorOptions } {
99 const root = mkdtempSync(join(tmpdir(), 'craft emulator fixture '))
1010 const sdk = join(root, 'managed sdk')
1111 for (const directory of ['bin', 'platform-tools', 'emulator']) mkdirSync(join(sdk, directory), { recursive: true })
@@ -56,9 +56,20 @@ else
5656 else
5757 printf 'com.android.launcher3/.Launcher\\n'
5858 fi ;;
59 *'logcat -b events -d -t 1')
59 *'dumpsys window')
60 if [ '${homeFocus}' = foreign ]; then
61 printf 'mCurrentFocus=Window{abc u0 com.android.settings/com.android.settings.FallbackHome}\\nmFocusedApp=ActivityRecord{com.android.launcher3/.Launcher}\\n'
62 elif [ '${homeFocus}' = delayed ] && [ ! -f "$ANDROID_HOME/home-focused" ]; then
63 touch "$ANDROID_HOME/home-focused"
64 printf 'mCurrentFocus=null\\n'
65 else
66 printf 'mCurrentFocus=Window{abc u0 com.android.launcher3/com.android.launcher3.Launcher}\\n'
67 fi ;;
68 *'logcat -b events -d -t 1') printf 'latest event ready\\n' ;;
69 *'logcat -b events -d -v threadtime')
6070 if [ '${delayedEvents ? 'yes' : 'no'}' = yes ] && [ ! -f "$ANDROID_HOME/events-ready" ]; then
6171 touch "$ANDROID_HOME/events-ready"
72 printf 'partial boot history\\n'
6273 exit 255
6374 fi
6475 printf 'event stream ready\\n' ;;
@@ -170,6 +181,7 @@ test('boot and System UI do not admit a device before its event log is readable'
170181 const trace = readFileSync(join(options.artifactsDirectory, 'device-commands.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line))
171182 const events = trace.filter(entry => entry.args.includes('logcat'))
172183 expect(events.map(entry => entry.code)).toEqual([255, 0])
184 expect(events[0].stdout).toContain('partial boot history')
173185 expect(events.every(entry => entry.args.includes('-d') && entry.args.includes('events'))).toBe(true)
174186 const identity = JSON.parse(readFileSync(join(options.artifactsDirectory, 'android-emulator.json'), 'utf8'))
175187 expect(Date.parse(events[1].finishedAt)).toBeLessThanOrEqual(Date.parse(identity.bootedAt))
@@ -293,3 +305,35 @@ test('software execution is explicit and cannot be mistaken for hardware acceler
293305 }
294306 finally { await session?.dispose(); rmSync(root, { recursive: true, force: true }) }
295307})
308
309
310test('a resolved HOME waits for its actual input window before handing off the device', async () => {
311 const { root, options } = fixture(true, false, false, false, 'normal', 'com.android.sdksetup/.DefaultActivity', 'ready', 'delayed')
312 let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined
313 try {
314 session = await createAndroidEmulator(options)
315 expect(existsSync(join(options.sdkRoot, 'home-focused'))).toBe(true)
316 const trace = readFileSync(join(options.artifactsDirectory, 'device-commands.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line))
317 const focus = trace.filter(entry => entry.args.includes('window'))
318 expect(focus).toHaveLength(2)
319 expect(focus[0].stdout.trim()).toBe('mCurrentFocus=null')
320 expect(focus[1].stdout).toContain('com.android.launcher3/com.android.launcher3.Launcher')
321 const identity = JSON.parse(readFileSync(join(options.artifactsDirectory, 'android-emulator.json'), 'utf8'))
322 expect(Date.parse(focus[1].finishedAt)).toBeLessThanOrEqual(Date.parse(identity.bootedAt))
323 expect(identity.bootTimeoutMs).toBe(600_000)
324 }
325 finally { await session?.dispose(); rmSync(root, { recursive: true, force: true }) }
326}, 10_000)
327
328test('a foreign focused window cannot admit an otherwise ready emulator', async () => {
329 const { root, options } = fixture(true, false, false, false, 'normal', 'com.android.sdksetup/.DefaultActivity', 'ready', 'foreign')
330 options.bootTimeoutMs = 5000
331 let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined
332 try {
333 await expect(createAndroidEmulator(options).then(value => { session = value; return value })).rejects.toThrow('did not boot within 5000ms')
334 const identity = JSON.parse(readFileSync(join(options.artifactsDirectory, 'android-emulator.json'), 'utf8'))
335 expect(identity.status).toBe('failed')
336 expect(existsSync(join(options.sdkRoot, 'deleted'))).toBe(true)
337 }
338 finally { await session?.dispose(); rmSync(root, { recursive: true, force: true }) }
339}, 10_000)