Skip to content

Commit 2311a94

Browse files
authored
Merge 8ec6637 into ff8c228
2 parents ff8c228 + 8ec6637 commit 2311a94

12 files changed

Lines changed: 89 additions & 21 deletions

‎packages/contracts/src/screen-recording-runtime-host.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export type AppleScreenRecordingHost = Readonly<{
7777
device: DeviceInfo,
7878
appBundleId: string,
7979
signal?: AbortSignal,
80+
requestId?: string,
8081
): Promise<AppleScreenRecordingClockAnchor | undefined>;
8182
isRunnerBundleId(bundleId: string): Promise<boolean>;
8283
}>;

‎packages/contracts/src/screen-recording-runtime.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export type ScreenRecordingLiveHandle = LiveResourceHandle<ScreenRecordingComple
132132

133133
export type ScreenRecordingStartInput = Readonly<{
134134
sessionId: string;
135+
requestId?: string;
135136
outputPath: string;
136137
clientOutputPath?: string;
137138
scope: RecordingScope;

‎packages/daemon/src/sessions/handlers/record-runtime.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ function screenRecordingStartInput(
372372
): ScreenRecordingStartInput {
373373
return {
374374
sessionId: params.sessionName,
375+
requestId: params.req.meta?.requestId,
375376
outputPath: outputPaths.outputPath,
376377
clientOutputPath: outputPaths.clientOutputPath,
377378
scope: prepared.scope,

‎packages/platform-apple/src/recording/runtime.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ async function startAppleSimulatorRecording(params: AppleRecordingStartParams) {
103103
device,
104104
input.activeSessionApp.bundleId,
105105
signal,
106+
input.requestId,
106107
)
107108
: undefined;
108109
// The recorder owns its own file and the export is produced from a copy of it (ADR 0024 2.3), so a

‎src/platform-runtime-screen-recording-apple-host.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ export function createAppleScreenRecordingHost(
3737
await import('./platform-runtime-screen-recording-apple-runner-host.ts');
3838
await retrieveAppleRunnerRecording(device, remotePath, outputPath, host.env, signal);
3939
},
40-
captureClockAnchor: async (device, appBundleId, signal) => {
40+
captureClockAnchor: async (device, appBundleId, signal, requestId) => {
4141
const { captureAppleClockAnchor } =
4242
await import('./platform-runtime-screen-recording-apple-runner-host.ts');
43-
return await captureAppleClockAnchor(device, appBundleId, signal);
43+
return await captureAppleClockAnchor(device, appBundleId, signal, requestId);
4444
},
4545
isRunnerBundleId: async (bundleId) => {
4646
const { isAppleRunnerBundleId } =

‎src/platform-runtime-screen-recording-apple-runner-host.test.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
clearRequestAbortRegistration,
3+
markRequestCanceled,
4+
registerRequestAbort,
5+
} from '@agent-device/host-kit/request';
16
import { expect, test, vi } from 'vitest';
27
import {
38
captureAppleClockAnchor,
@@ -54,3 +59,43 @@ test('focused simulator-only transport never warms a local Apple runner', async
5459
);
5560
expect(runnerClient.run).not.toHaveBeenCalled();
5661
});
62+
63+
test.each([false, true])(
64+
'clock-anchor capture preserves request cancellation=%s with an independent caller signal',
65+
async (canceled) => {
66+
const requestId = `record-anchor-cancel-${canceled}`;
67+
const registration = registerRequestAbort(requestId)!;
68+
const callerSignal = new AbortController().signal;
69+
runnerClient.run.mockReset();
70+
runnerClient.run.mockImplementationOnce(async () => {
71+
if (canceled) {
72+
markRequestCanceled(requestId);
73+
throw registration.controller.signal.reason;
74+
}
75+
throw new Error('clock anchor unavailable');
76+
});
77+
try {
78+
const capture = captureAppleClockAnchor(
79+
simulator,
80+
'com.example.app',
81+
callerSignal,
82+
requestId,
83+
);
84+
if (canceled) {
85+
await expect(capture).rejects.toMatchObject({
86+
code: 'COMMAND_FAILED',
87+
details: { reason: 'request_canceled' },
88+
});
89+
} else {
90+
await expect(capture).resolves.toBeUndefined();
91+
}
92+
expect(runnerClient.run).toHaveBeenCalledOnce();
93+
const forwarded = runnerClient.run.mock.calls[0]?.[2];
94+
expect(forwarded?.requestId).toBe(requestId);
95+
expect(forwarded?.signal).toBe(callerSignal);
96+
expect(callerSignal.aborted).toBe(false);
97+
} finally {
98+
clearRequestAbortRegistration(registration);
99+
}
100+
},
101+
);

‎src/platform-runtime-screen-recording-apple-runner-host.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export async function captureAppleClockAnchor(
7272
device: DeviceInfo,
7373
appBundleId: string,
7474
signal?: AbortSignal,
75+
requestId?: string,
7576
) {
7677
try {
7778
const [simulatorTransport, runnerTransport] = await Promise.all([
@@ -93,7 +94,7 @@ export async function captureAppleClockAnchor(
9394
const result = await runAppleRunnerCommand(
9495
device,
9596
{ command: 'snapshot', appBundleId, interactiveOnly: true, depth: 1 },
96-
{ signal },
97+
{ signal, requestId },
9798
);
9899
const wallClockAtMs = Date.now();
99100
return typeof result.currentUptimeMs === 'number' &&
@@ -103,6 +104,8 @@ export async function captureAppleClockAnchor(
103104
: undefined;
104105
} catch {
105106
signal?.throwIfAborted();
107+
const { throwIfRequestCanceled } = await import('@agent-device/host-kit/request');
108+
throwIfRequestCanceled(requestId);
106109
return undefined;
107110
}
108111
}

‎src/platform-runtime-screen-recording-host.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ function createLazyAppleHost(host: HostEnvironment): ScreenRecordingRuntimeHost[
8686
await (await load()).inspectRunner(device, sessionId, authority),
8787
retrieveRunnerRecording: async (device, remotePath, outputPath, signal) =>
8888
await (await load()).retrieveRunnerRecording(device, remotePath, outputPath, signal),
89-
captureClockAnchor: async (device, appBundleId, signal) =>
90-
await (await load()).captureClockAnchor(device, appBundleId, signal),
89+
captureClockAnchor: async (device, appBundleId, signal, requestId) =>
90+
await (await load()).captureClockAnchor(device, appBundleId, signal, requestId),
9191
isRunnerBundleId: async (bundleId) => await (await load()).isRunnerBundleId(bundleId),
9292
});
9393
}

‎test/integration/command-coverage/declarations.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -854,8 +854,8 @@ const COMMAND_COVERAGE_DECLARATIONS = {
854854
'short visible fixture mutation produces a non-empty playable Android MP4',
855855
),
856856
iosSimulator: iosSimulator.live(
857-
'full:observability-artifacts',
858-
'visible mutation produces a playable MP4',
857+
'smoke:capture-close',
858+
'active-session recording captures a visible mutation into a playable MP4',
859859
),
860860
macos: macos.live(
861861
'provider:macos-recording',

‎test/integration/ios-simulator-e2e/live-full-scenarios.ts‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
assertElementTextAfterScrolling,
1010
assertFilesDiffer,
1111
assertJsonContains,
12-
assertMp4File,
1312
assertNonEmptyFile,
1413
assertWaitText,
1514
capturePng,
@@ -282,18 +281,6 @@ export async function assertObservabilityAndArtifacts(context: LiveContext): Pro
282281
assertNonEmptyFile(tracePath, 'trace');
283282
verifyCommand(context, C.trace, 'traced press produces a non-empty trace at the requested path');
284283

285-
const recordingPath = path.join(context.artifactDir, 'fixture.mp4');
286-
await runStep(context, 'start screen recording', [
287-
'record',
288-
'start',
289-
recordingPath,
290-
'--hide-touches',
291-
]);
292-
await runStep(context, 'record a visible mutation', ['press', 'id="automation-press"']);
293-
await runStep(context, 'stop screen recording', ['record', 'stop']);
294-
await assertMp4File(recordingPath);
295-
verifyCommand(context, C.record, 'visible mutation produces a non-empty playable MP4');
296-
297284
const batch = await runStep(context, 'run semantic read batch', [
298285
'batch',
299286
'--steps',

0 commit comments

Comments
 (0)