diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd1888b73..9b74095056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Fixed (android): a chunked `record stop` (recordings over 170 s) no longer warns that screenrecord + stopped before record stop at the 180 s limit. Rotation always ends every earlier chunk before + stop, so the warning now fires only when the last chunk's recorder had already exited. - Fixed: an iOS snapshot whose XCTest query-sweep tier cannot read the screen no longer ends the runner process. On a live React Native feed (Bluesky Home, images re-rendering) the AX server rejects each of the sweep's 19 element-type queries with `kAXErrorIllegalArgument`, and XCTest diff --git a/packages/platform-android/src/recording/chunks.test.ts b/packages/platform-android/src/recording/chunks.test.ts index e65c3476cf..f924a9b128 100644 --- a/packages/platform-android/src/recording/chunks.test.ts +++ b/packages/platform-android/src/recording/chunks.test.ts @@ -66,6 +66,52 @@ test('returns secondary client paths, split/180s warnings, and skips chunked tou } }); +async function finishedWarning(params: { + rotate: boolean; + exitedBeforeStop: readonly string[]; +}): Promise { + vi.useFakeTimers(); + try { + let nextPid = 41; + const exited = new Set(params.exitedBeforeStop); + const runtime = await start({ + start: async () => recordingProcess(String(++nextPid)), + inspect: async ({ pid }: { pid: string }) => (exited.has(pid) ? 'missing' : 'owned-alive'), + stop: async ({ pid }: { pid: string }) => { + if (exited.has(pid)) return 'already-missing' as const; + exited.add(pid); + return 'stopped' as const; + }, + }); + const started = await runtime.screenRecordingStart(recordingInput()); + const handle = started.pendingHandle.transfer(); + if (params.rotate) await vi.advanceTimersByTimeAsync(170_000); + const finishing = handle.finish(); + await vi.advanceTimersByTimeAsync(1_000); + const outcome = await finishing; + expect(outcome.status).toBe('completed'); + return outcome.status === 'completed' ? outcome.result.warning : undefined; + } finally { + vi.useRealTimers(); + } +} + +test('omits the 180s limit warning when only a rotated-out chunk exited before record stop', async () => { + // Rotation stops chunk 42, so it is already missing at record stop; chunk 43 is still recording. + const warning = await finishedWarning({ rotate: true, exitedBeforeStop: [] }); + expect(warning).toContain('split into multiple MP4 chunks'); + expect(warning).not.toContain('likely after reaching the 180s platform limit'); +}); + +test('warns about the 180s limit when the last chunk exited before record stop', async () => { + await expect(finishedWarning({ rotate: false, exitedBeforeStop: ['42'] })).resolves.toContain( + 'likely after reaching the 180s platform limit', + ); + await expect(finishedWarning({ rotate: true, exitedBeforeStop: ['43'] })).resolves.toContain( + 'likely after reaching the 180s platform limit', + ); +}); + test('continues through every owned chunk after a stop or removal failure', async () => { const chunks = [ { diff --git a/packages/platform-android/src/recording/chunks.ts b/packages/platform-android/src/recording/chunks.ts index 83d470b417..d7288c11db 100644 --- a/packages/platform-android/src/recording/chunks.ts +++ b/packages/platform-android/src/recording/chunks.ts @@ -80,21 +80,27 @@ function validProcessIdentity( ); } +/** + * Resolves whether the active (last) chunk's recorder had already exited, i.e. the video ends before + * record stop. Earlier chunks always end before stop because rotation replaced them. + */ export async function stopOwnedChunks( transport: Transport, chunks: readonly NativeChunk[], ): Promise { - let reachedLimit = false; + const active = chunks.at(-1); + let activeAlreadyExited = false; let failure: unknown; for (const chunk of [...chunks].reverse()) { try { - reachedLimit = (await stopChunk(transport, chunk)) || reachedLimit; + const alreadyExited = await stopChunk(transport, chunk); + if (chunk === active) activeAlreadyExited = alreadyExited; } catch (error) { failure ??= error; } } if (failure) throw failure; - return reachedLimit; + return activeAlreadyExited; } export async function waitForStableArtifacts(