@@ -346,6 +346,163 @@ func TestCLIAsyncPut(t *testing.T) {
346346 t .Logf ("TestCLIAsyncPut complete, total time: %s" , time .Since (startTime ))
347347}
348348
349+ // TestCLIAsyncPrestage tests the pelican object prestage --async command
350+ func TestCLIAsyncPrestage (t * testing.T ) {
351+ // Reset test state
352+ server_utils .ResetTestState ()
353+
354+ // Create test federation
355+ fed := fed_test_utils .NewFedTest (t , testOriginConfig )
356+
357+ // Create temporary directory
358+ tempDir := t .TempDir ()
359+
360+ // Create token
361+ err := param .Set (param .IssuerKeysDirectory .GetName (), t .TempDir ())
362+ require .NoError (t , err )
363+ issuer , err := config .GetServerIssuerURL ()
364+ require .NoError (t , err )
365+
366+ tokenConfig := token .NewWLCGToken ()
367+ tokenConfig .Lifetime = time .Minute * 5
368+ tokenConfig .Issuer = issuer
369+ tokenConfig .Subject = "test-cli-async-prestage"
370+ tokenConfig .AddAudienceAny ()
371+
372+ scopes := []token_scopes.TokenScope {}
373+ readScope , err := token_scopes .Wlcg_Storage_Read .Path ("/" )
374+ require .NoError (t , err )
375+ scopes = append (scopes , readScope )
376+ modScope , err := token_scopes .Wlcg_Storage_Modify .Path ("/" )
377+ require .NoError (t , err )
378+ scopes = append (scopes , modScope )
379+ tokenConfig .AddScopes (scopes ... )
380+
381+ tkn , err := tokenConfig .CreateToken ()
382+ require .NoError (t , err )
383+
384+ tokenFile := filepath .Join (tempDir , "token" )
385+ err = os .WriteFile (tokenFile , []byte (tkn ), 0644 )
386+ require .NoError (t , err )
387+
388+ // Set up client API server
389+ serverConfig , _ := client_agent .CreateTestServerConfig (t )
390+
391+ egrp , egrpCtx := errgroup .WithContext (context .Background ())
392+ ctx := context .WithValue (egrpCtx , config .EgrpKey , egrp )
393+
394+ server , err := client_agent .NewServer (ctx , serverConfig )
395+ require .NoError (t , err )
396+
397+ err = server .Start ()
398+ require .NoError (t , err )
399+
400+ t .Cleanup (func () {
401+ err := server .Shutdown ()
402+ assert .NoError (t , err )
403+ })
404+
405+ // Build pelican binary
406+ pelicanBin := buildPelicanBinary (t )
407+
408+ // Create test file and upload it first
409+ testContent := []byte ("Test file for async prestage\n " )
410+ uploadFile := filepath .Join (tempDir , "prestage-upload.txt" )
411+ err = os .WriteFile (uploadFile , testContent , 0644 )
412+ require .NoError (t , err )
413+
414+ federationPrefix := fed .Exports [0 ].FederationPrefix
415+ discoveryUrl , err := url .Parse (param .Federation_DiscoveryUrl .GetString ())
416+ require .NoError (t , err )
417+ uploadURL := fmt .Sprintf ("pelican://%s%s/prestage-test.txt" , discoveryUrl .Host , federationPrefix )
418+
419+ // Upload file first using async + wait
420+ uploadCmd := exec .Command (pelicanBin , "object" , "put" , "--async" , "--wait" , uploadFile , uploadURL , "--token" , tokenFile )
421+ uploadCmd .Env = append (os .Environ (), fmt .Sprintf ("PELICAN_CLIENTAGENT_SOCKET=%s" , serverConfig .SocketPath ))
422+ output , err := uploadCmd .CombinedOutput ()
423+ require .NoError (t , err , "Failed to upload file: %s" , output )
424+
425+ // Test async prestage without --wait
426+ t .Run ("AsyncPrestageWithoutWait" , func (t * testing.T ) {
427+ cmd := exec .Command (pelicanBin , "object" , "prestage" , "--async" , uploadURL , "--token" , tokenFile )
428+ cmd .Env = append (os .Environ (), fmt .Sprintf ("PELICAN_CLIENTAGENT_SOCKET=%s" , serverConfig .SocketPath ))
429+
430+ output , err := cmd .CombinedOutput ()
431+ require .NoError (t , err , "Failed to run async prestage: %s" , output )
432+
433+ outputStr := string (output )
434+ t .Logf ("Command output: %s" , outputStr )
435+
436+ // Should contain job ID
437+ assert .Contains (t , outputStr , "Job created:" )
438+ assert .Contains (t , outputStr , "Check status with: pelican job status" )
439+
440+ // Extract job ID from output
441+ re := regexp .MustCompile (`Job created: ([a-f0-9-]+)` )
442+ matches := re .FindStringSubmatch (outputStr )
443+ require .Len (t , matches , 2 , "Could not extract job ID from output" )
444+ jobID := matches [1 ]
445+ t .Logf ("Created job ID: %s" , jobID )
446+ })
447+
448+ // Test async prestage with --wait
449+ t .Run ("AsyncPrestageWithWait" , func (t * testing.T ) {
450+ cmd := exec .Command (pelicanBin , "object" , "prestage" , "--async" , "--wait" , uploadURL , "--token" , tokenFile )
451+ cmd .Env = append (os .Environ (), fmt .Sprintf ("PELICAN_CLIENTAGENT_SOCKET=%s" , serverConfig .SocketPath ))
452+
453+ output , err := cmd .CombinedOutput ()
454+ outputStr := string (output )
455+ t .Logf ("Command output: %s" , outputStr )
456+
457+ // Should contain job creation message
458+ assert .Contains (t , outputStr , "Job created:" )
459+ assert .Contains (t , outputStr , "Waiting for job to complete" )
460+
461+ // If prestage failed, log the client-agent log for debugging
462+ if err != nil {
463+ t .Logf ("Prestage failed with error: %v" , err )
464+
465+ // Extract job ID to query its status
466+ re := regexp .MustCompile (`Job created: ([a-f0-9-]+)` )
467+ matches := re .FindStringSubmatch (outputStr )
468+ if len (matches ) >= 2 {
469+ jobID := matches [1 ]
470+ t .Logf ("Querying failed job ID: %s" , jobID )
471+
472+ // Query the job status via API to get detailed error info
473+ apiClient , apiErr := apiclient .NewAPIClient (serverConfig .SocketPath )
474+ if apiErr != nil {
475+ t .Logf ("Failed to create API client: %v" , apiErr )
476+ } else {
477+ ctx := context .Background ()
478+ if jobStatus , statusErr := apiClient .GetJobStatus (ctx , jobID ); statusErr == nil {
479+ t .Logf ("Job Status: %s" , jobStatus .Status )
480+ if jobStatus .Error != "" {
481+ t .Logf ("Job Error: %s" , jobStatus .Error )
482+ }
483+ if len (jobStatus .Transfers ) > 0 {
484+ for i , transfer := range jobStatus .Transfers {
485+ t .Logf ("Transfer %d: Status=%s, Operation=%s, Source=%s" ,
486+ i , transfer .Status , transfer .Operation , transfer .Source )
487+ if transfer .Error != "" {
488+ t .Logf (" Transfer %d Error: %s" , i , transfer .Error )
489+ }
490+ }
491+ }
492+ } else {
493+ t .Logf ("Failed to get job status: %v" , statusErr )
494+ }
495+ }
496+ }
497+
498+ require .NoError (t , err , "Prestage operation should not fail" )
499+ }
500+
501+ // Should contain completion message
502+ assert .Contains (t , outputStr , "Job completed successfully" )
503+ })
504+ }
505+
349506// TestCLIJobCommands tests the pelican job subcommands
350507func TestCLIJobCommands (t * testing.T ) {
351508 // Reset test state
0 commit comments