@@ -2,65 +2,124 @@ import assert from 'node:assert/strict';
22import fs from 'node:fs' ;
33import path from 'node:path' ;
44import { test } from 'vitest' ;
5- import { assertRecordingStarted , assertRecordingStopped , assertRpcOk } from './assertions.ts' ;
5+ import {
6+ assertRecordingStarted ,
7+ assertRecordingStopped ,
8+ assertRpcError ,
9+ assertRpcOk ,
10+ } from './assertions.ts' ;
611import { PROVIDER_SCENARIO_MACOS } from './fixtures.ts' ;
7- import { createProviderScenarioTempPath , withProviderScenarioResource } from './harness.ts' ;
12+ import {
13+ createProviderScenarioTempPath ,
14+ type ProviderScenarioHarness ,
15+ withProviderScenarioResource ,
16+ } from './harness.ts' ;
817import { createMacOsDesktopWorld } from './macos-world.ts' ;
918import {
1019 createAppleRunnerProviderFromTranscript ,
1120 createAppleRunnerScreenRecordingTransportFromTranscript ,
1221} from './providers.ts' ;
22+ import { screenRecordingResourceStore } from '../../../src/daemon/screen-recording-resource-store.ts' ;
1323import { createProviderTranscript } from './transcript.ts' ;
1424
1525test ( 'Provider-backed integration macOS recording uses focused exact runner authority' , async ( ) => {
1626 const recordingPath = createProviderScenarioTempPath (
1727 'agent-device-provider-scenario-macos-record' ,
1828 'mp4' ,
1929 ) ;
20- const runnerTranscript = createProviderTranscript ( [
21- {
22- command : 'macos.runner.recordStart' ,
23- deviceId : PROVIDER_SCENARIO_MACOS . id ,
24- platform : 'apple' ,
25- request : {
26- command : 'recordStart' ,
27- outPath : recordingPath ,
30+ const { runnerTranscript, appleRunnerProvider, appleRunnerScreenRecordingTransport } =
31+ createMacOsRecordingScenario ( recordingPath , ( outputPath ) =>
32+ fs . copyFileSync (
33+ path . join ( process . cwd ( ) , 'website/docs/public/agent-device-contacts.mp4' ) ,
34+ outputPath ,
35+ ) ,
36+ ) ;
37+ await withProviderScenarioResource (
38+ async ( ) =>
39+ await createMacOsDesktopWorld ( {
40+ appleRunnerProvider,
41+ appleRunnerScreenRecordingTransport,
42+ } ) ,
43+ async ( { daemon } ) => {
44+ const open = await daemon . callCommand ( 'open' , [ 'settings' ] , { platform : 'macos' } ) ;
45+ assert . equal ( assertRpcOk ( open ) . appBundleId , 'com.apple.systempreferences' ) ;
46+
47+ const recordStart = await daemon . callCommand ( 'record' , [ 'start' , recordingPath ] , {
48+ hideTouches : true ,
2849 fps : 30 ,
29- appBundleId : 'com.apple.systempreferences' ,
30- } ,
31- result : { runnerSessionId : 'macos-runner-recording-1' } ,
50+ } ) ;
51+ assertRecordingStarted ( recordStart , { outPath : recordingPath , showTouches : false } ) ;
52+
53+ const recordStop = await daemon . callCommand ( 'record' , [ 'stop' ] ) ;
54+ assertRecordingStopped ( recordStop , recordingPath , { showTouches : false } ) ;
55+
56+ runnerTranscript . assertComplete ( ) ;
3257 } ,
33- {
34- command : 'macos.runner.recordStop' ,
35- deviceId : PROVIDER_SCENARIO_MACOS . id ,
36- platform : 'apple' ,
37- request : { command : 'recordStop' , appBundleId : 'com.apple.systempreferences' } ,
38- result : { } ,
58+ ) ;
59+ } ) ;
60+
61+ test ( 'Provider-backed integration macOS recording rejects runner-stop failure and retains cleanup state' , async ( ) => {
62+ const recordingPath = createProviderScenarioTempPath (
63+ 'agent-device-provider-scenario-macos-stop-failure' ,
64+ 'mp4' ,
65+ ) ;
66+ const stopError = 'macOS runner recordStop rejected' ;
67+ const { runnerTranscript, appleRunnerProvider, appleRunnerScreenRecordingTransport } =
68+ createMacOsRecordingScenario ( recordingPath , undefined , stopError ) ;
69+
70+ await withProviderScenarioResource (
71+ async ( ) =>
72+ await createMacOsDesktopWorld ( {
73+ appleRunnerProvider,
74+ appleRunnerScreenRecordingTransport,
75+ } ) ,
76+ async ( { daemon } ) => {
77+ await openMacOsSettings ( daemon ) ;
78+
79+ const recordStart = await daemon . callCommand ( 'record' , [ 'start' , recordingPath ] , {
80+ hideTouches : true ,
81+ fps : 30 ,
82+ } ) ;
83+ assertRecordingStarted ( recordStart , { outPath : recordingPath , showTouches : false } ) ;
84+
85+ const recordStop = await daemon . callCommand ( 'record' , [ 'stop' ] ) ;
86+ assertRpcError ( recordStop , 'COMMAND_FAILED' , / r e c o r d S t o p r e j e c t e d / ) ;
87+ assert . equal ( recordStop . json ?. result , undefined ) ;
88+
89+ const session = daemon . session ( ) ;
90+ assert . ok ( session ?. screenRecording , 'failed stop must retain the live session resource' ) ;
91+ const resource = screenRecordingResourceStore . read (
92+ screenRecordingResourceStore . resolvePath ( daemon . sessionDir ( ) ) ,
93+ ) ;
94+ assert . equal ( resource . status , 'decoded' ) ;
95+ if ( resource . status === 'decoded' ) {
96+ assert . equal ( resource . envelope . lifecycle , 'open' ) ;
97+ assert . equal ( resource . envelope . metadata ?. phase , 'cleanup-pending' ) ;
98+ assert . equal ( resource . envelope . metadata ?. cleanupStatus , 'cleanup-pending' ) ;
99+ }
100+ runnerTranscript . assertComplete ( ) ;
39101 } ,
40- ] ) ;
41- const appleRunnerProvider = createAppleRunnerProviderFromTranscript (
42- runnerTranscript ,
43- 'macos.runner' ,
44102 ) ;
45- const appleRunnerScreenRecordingTransport =
46- createAppleRunnerScreenRecordingTransportFromTranscript (
47- runnerTranscript ,
48- 'macos.runner' ,
49- ( outputPath ) =>
50- fs . copyFileSync (
51- path . join ( process . cwd ( ) , 'website/docs/public/agent-device-contacts.mp4' ) ,
52- outputPath ,
53- ) ,
103+ } ) ;
104+
105+ test ( 'Provider-backed integration macOS recording rejects an invalid MP4 before artifact publication' , async ( ) => {
106+ const recordingPath = createProviderScenarioTempPath (
107+ 'agent-device-provider-scenario-macos-invalid-video' ,
108+ 'mp4' ,
109+ ) ;
110+ const { runnerTranscript, appleRunnerProvider, appleRunnerScreenRecordingTransport } =
111+ createMacOsRecordingScenario ( recordingPath , ( outputPath ) =>
112+ fs . writeFileSync ( outputPath , Buffer . from ( 'not an MP4' ) ) ,
54113 ) ;
114+
55115 await withProviderScenarioResource (
56116 async ( ) =>
57117 await createMacOsDesktopWorld ( {
58118 appleRunnerProvider,
59119 appleRunnerScreenRecordingTransport,
60120 } ) ,
61121 async ( { daemon } ) => {
62- const open = await daemon . callCommand ( 'open' , [ 'settings' ] , { platform : 'macos' } ) ;
63- assert . equal ( assertRpcOk ( open ) . appBundleId , 'com.apple.systempreferences' ) ;
122+ await openMacOsSettings ( daemon ) ;
64123
65124 const recordStart = await daemon . callCommand ( 'record' , [ 'start' , recordingPath ] , {
66125 hideTouches : true ,
@@ -69,9 +128,64 @@ test('Provider-backed integration macOS recording uses focused exact runner auth
69128 assertRecordingStarted ( recordStart , { outPath : recordingPath , showTouches : false } ) ;
70129
71130 const recordStop = await daemon . callCommand ( 'record' , [ 'stop' ] ) ;
72- assertRecordingStopped ( recordStop , recordingPath , { showTouches : false } ) ;
131+ assertRpcError ( recordStop , 'COMMAND_FAILED' , / w a s n o t f i n a l i z e d i n t o a p l a y a b l e v i d e o / ) ;
132+ assert . equal ( recordStop . json ?. result , undefined ) ;
133+ assert . equal ( daemon . session ( ) ?. screenRecording , undefined ) ;
73134
135+ const resource = screenRecordingResourceStore . read (
136+ screenRecordingResourceStore . resolvePath ( daemon . sessionDir ( ) ) ,
137+ ) ;
138+ assert . equal ( resource . status , 'decoded' ) ;
139+ if ( resource . status === 'decoded' ) {
140+ assert . equal ( resource . envelope . lifecycle , 'completed' ) ;
141+ assert . equal ( resource . envelope . metadata ?. phase , 'completed' ) ;
142+ assert . equal ( resource . envelope . metadata ?. cleanupStatus , 'cleaned' ) ;
143+ }
74144 runnerTranscript . assertComplete ( ) ;
75145 } ,
76146 ) ;
77147} ) ;
148+
149+ function createMacOsRecordingScenario (
150+ recordingPath : string ,
151+ onStopped ?: ( outputPath : string ) => void ,
152+ stopError ?: string ,
153+ ) {
154+ const runnerTranscript = createProviderTranscript ( [
155+ {
156+ command : 'macos.runner.recordStart' ,
157+ deviceId : PROVIDER_SCENARIO_MACOS . id ,
158+ platform : 'apple' ,
159+ request : {
160+ command : 'recordStart' ,
161+ outPath : recordingPath ,
162+ fps : 30 ,
163+ appBundleId : 'com.apple.systempreferences' ,
164+ } ,
165+ result : { runnerSessionId : 'macos-runner-recording-1' } ,
166+ } ,
167+ {
168+ command : 'macos.runner.recordStop' ,
169+ deviceId : PROVIDER_SCENARIO_MACOS . id ,
170+ platform : 'apple' ,
171+ request : { command : 'recordStop' , appBundleId : 'com.apple.systempreferences' } ,
172+ ...( stopError === undefined ? { result : { } } : { error : stopError } ) ,
173+ } ,
174+ ] ) ;
175+ return {
176+ runnerTranscript,
177+ appleRunnerProvider : createAppleRunnerProviderFromTranscript ( runnerTranscript , 'macos.runner' ) ,
178+ appleRunnerScreenRecordingTransport : createAppleRunnerScreenRecordingTransportFromTranscript (
179+ runnerTranscript ,
180+ 'macos.runner' ,
181+ onStopped ,
182+ ) ,
183+ } ;
184+ }
185+
186+ async function openMacOsSettings (
187+ daemon : Pick < ProviderScenarioHarness , 'callCommand' > ,
188+ ) : Promise < void > {
189+ const open = await daemon . callCommand ( 'open' , [ 'settings' ] , { platform : 'macos' } ) ;
190+ assert . equal ( assertRpcOk ( open ) . appBundleId , 'com.apple.systempreferences' ) ;
191+ }
0 commit comments