@@ -2,6 +2,7 @@ import { test, onTestFinished } from 'vitest';
22import assert from 'node:assert/strict' ;
33import { execFileSync } from 'node:child_process' ;
44import http from 'node:http' ;
5+ import fsSync from 'node:fs' ;
56import fs from 'node:fs/promises' ;
67import os from 'node:os' ;
78import path from 'node:path' ;
@@ -107,6 +108,44 @@ test('materializeInstallablePath rejects archive extraction when disabled', asyn
107108 }
108109} ) ;
109110
111+ test . sequential ( 'materializeInstallablePath extracts zip archives without ditto' , async ( ) => {
112+ const unzipPath = findExecutableInPath ( 'unzip' ) ;
113+ assert . ok ( unzipPath , 'unzip must be available for portable zip extraction' ) ;
114+
115+ const tempRoot = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'agent-device-install-source-unzip-' ) ) ;
116+ const archivePath = path . join ( tempRoot , 'bundle.zip' ) ;
117+ const binDir = path . join ( tempRoot , 'bin' ) ;
118+ const payloadDir = path . join ( tempRoot , 'payload' ) ;
119+ const apkPath = path . join ( payloadDir , 'Sample.apk' ) ;
120+ const previousPath = process . env . PATH ;
121+
122+ try {
123+ await fs . mkdir ( binDir ) ;
124+ await fs . symlink ( unzipPath , path . join ( binDir , 'unzip' ) ) ;
125+ await fs . mkdir ( payloadDir ) ;
126+ await fs . writeFile ( apkPath , 'placeholder apk' , 'utf8' ) ;
127+ execFileSync ( 'zip' , [ '-qr' , archivePath , 'payload' ] , { cwd : tempRoot } ) ;
128+
129+ process . env . PATH = binDir ;
130+ const result = await materializeInstallablePath ( {
131+ source : { kind : 'path' , path : archivePath } ,
132+ isInstallablePath : ( candidatePath , stat ) => stat . isFile ( ) && candidatePath . endsWith ( '.apk' ) ,
133+ installableLabel : 'Android installable (.apk or .aab)' ,
134+ allowArchiveExtraction : true ,
135+ } ) ;
136+
137+ try {
138+ assert . equal ( path . basename ( result . installablePath ) , 'Sample.apk' ) ;
139+ assert . equal ( await fs . readFile ( result . installablePath , 'utf8' ) , 'placeholder apk' ) ;
140+ } finally {
141+ await result . cleanup ( ) ;
142+ }
143+ } finally {
144+ process . env . PATH = previousPath ;
145+ await fs . rm ( tempRoot , { recursive : true , force : true } ) ;
146+ }
147+ } ) ;
148+
110149test ( 'prepareIosInstallArtifact rejects untrusted URL sources' , async ( ) => {
111150 await assert . rejects (
112151 async ( ) =>
@@ -160,3 +199,20 @@ test('prepareAndroidInstallArtifact resolves package identity for direct APK URL
160199 await result . cleanup ( ) ;
161200 }
162201} ) ;
202+
203+ function findExecutableInPath ( command : string ) : string | undefined {
204+ const pathValue = process . env . PATH ;
205+ if ( ! pathValue ) return undefined ;
206+ for ( const directory of pathValue . split ( path . delimiter ) ) {
207+ if ( ! directory ) continue ;
208+ const candidate = path . join ( directory , command ) ;
209+ try {
210+ if ( ! fsSync . statSync ( candidate ) . isFile ( ) ) continue ;
211+ fsSync . accessSync ( candidate , fsSync . constants . X_OK ) ;
212+ return candidate ;
213+ } catch {
214+ // Keep scanning PATH.
215+ }
216+ }
217+ return undefined ;
218+ }
0 commit comments