@@ -306,4 +306,228 @@ public void Operations_AfterDispose_DoNotThrow()
306306 _ = m_Manager . ExtensionExists ( "test" ) ;
307307 _ = m_Manager . GetExtensionsVersion ( ) ;
308308 }
309+
310+ /// <summary>
311+ /// Verifies that constructor handles directory creation failure gracefully.
312+ /// </summary>
313+ [ Fact ]
314+ public void Constructor_WhenDirectoryCreationFails_HandlesGracefully ( )
315+ {
316+ // Arrange
317+ _ = m_FileSystemMock . Setup ( fs => fs . DirectoryExists ( It . IsAny < string > ( ) ) ) . Returns ( false ) ;
318+ _ = m_FileSystemMock . Setup ( fs => fs . CreateDirectory ( It . IsAny < string > ( ) ) ) ;
319+ _ = m_FileSystemMock . SetupSequence ( fs => fs . DirectoryExists ( It . IsAny < string > ( ) ) )
320+ . Returns ( false ) // First check - doesn't exist
321+ . Returns ( false ) ; // After creation - still doesn't exist (creation failed)
322+
323+ // Act - should not throw
324+ m_Manager = new Manager ( m_FileSystemMock . Object , m_Settings . Object ) ;
325+
326+ // Assert
327+ _ = m_Manager . Should ( ) . NotBeNull ( ) ;
328+ }
329+
330+ /// <summary>
331+ /// Verifies that ValidateExtension returns invalid when script file does not exist.
332+ /// </summary>
333+ [ Fact ]
334+ public void ValidateExtension_WhenScriptFileDoesNotExist_ReturnsInvalid ( )
335+ {
336+ // Arrange
337+ m_Manager = new Manager ( m_FileSystemMock . Object , m_Settings . Object ) ;
338+ var extensionName = "TestExtension" ;
339+ var metadata = new Nexus . Engine . Extensions . Models . ExtensionMetadata
340+ {
341+ Name = extensionName ,
342+ ScriptFile = "script.ps1" ,
343+ ScriptType = "powershell" ,
344+ FullScriptPath = "C:\\ extensions\\ TestExtension\\ script.ps1" ,
345+ } ;
346+
347+ // Manually add extension to manager's internal dictionary via reflection
348+ var extensionsField = typeof ( Manager ) . GetField ( "m_Extensions" , System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
349+ if ( extensionsField ? . GetValue ( m_Manager ) is System . Collections . Generic . Dictionary < string , Nexus . Engine . Extensions . Models . ExtensionMetadata > extensions )
350+ {
351+ extensions [ extensionName ] = metadata ;
352+ }
353+
354+ // Setup file system to return false for file exists check
355+ _ = m_FileSystemMock . Setup ( fs => fs . FileExists ( It . IsAny < string > ( ) ) ) . Returns ( false ) ;
356+
357+ // Act
358+ var ( isValid , errorMessage ) = m_Manager . ValidateExtension ( extensionName ) ;
359+
360+ // Assert
361+ _ = isValid . Should ( ) . BeFalse ( ) ;
362+ _ = errorMessage . Should ( ) . Contain ( "not found" ) ;
363+ }
364+
365+ /// <summary>
366+ /// Verifies that ValidateExtension returns invalid when script file is null or empty.
367+ /// </summary>
368+ [ Fact ]
369+ public void ValidateExtension_WhenScriptFileIsEmpty_ReturnsInvalid ( )
370+ {
371+ // Arrange
372+ m_Manager = new Manager ( m_FileSystemMock . Object , m_Settings . Object ) ;
373+ var extensionName = "TestExtension" ;
374+ var metadata = new Nexus . Engine . Extensions . Models . ExtensionMetadata
375+ {
376+ Name = extensionName ,
377+ ScriptFile = string . Empty ,
378+ ScriptType = "powershell" ,
379+ } ;
380+
381+ // Manually add extension to manager's internal dictionary via reflection
382+ var extensionsField = typeof ( Manager ) . GetField ( "m_Extensions" , System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
383+ if ( extensionsField ? . GetValue ( m_Manager ) is System . Collections . Generic . Dictionary < string , Nexus . Engine . Extensions . Models . ExtensionMetadata > extensions )
384+ {
385+ extensions [ extensionName ] = metadata ;
386+ }
387+
388+ // Act
389+ var ( isValid , errorMessage ) = m_Manager . ValidateExtension ( extensionName ) ;
390+
391+ // Assert
392+ _ = isValid . Should ( ) . BeFalse ( ) ;
393+ _ = errorMessage . Should ( ) . Contain ( "no script file" ) ;
394+ }
395+
396+ /// <summary>
397+ /// Verifies that ValidateExtension returns invalid when script type is unsupported.
398+ /// </summary>
399+ [ Fact ]
400+ public void ValidateExtension_WhenScriptTypeIsUnsupported_ReturnsInvalid ( )
401+ {
402+ // Arrange
403+ m_Manager = new Manager ( m_FileSystemMock . Object , m_Settings . Object ) ;
404+ var extensionName = "TestExtension" ;
405+ var metadata = new Nexus . Engine . Extensions . Models . ExtensionMetadata
406+ {
407+ Name = extensionName ,
408+ ScriptFile = "script.sh" ,
409+ ScriptType = "bash" ,
410+ FullScriptPath = "C:\\ extensions\\ TestExtension\\ script.sh" ,
411+ } ;
412+
413+ // Manually add extension to manager's internal dictionary via reflection
414+ var extensionsField = typeof ( Manager ) . GetField ( "m_Extensions" , System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
415+ if ( extensionsField ? . GetValue ( m_Manager ) is System . Collections . Generic . Dictionary < string , Nexus . Engine . Extensions . Models . ExtensionMetadata > extensions )
416+ {
417+ extensions [ extensionName ] = metadata ;
418+ }
419+
420+ // Setup file system to return true for file exists check
421+ _ = m_FileSystemMock . Setup ( fs => fs . FileExists ( It . IsAny < string > ( ) ) ) . Returns ( true ) ;
422+
423+ // Act
424+ var ( isValid , errorMessage ) = m_Manager . ValidateExtension ( extensionName ) ;
425+
426+ // Assert
427+ _ = isValid . Should ( ) . BeFalse ( ) ;
428+ _ = errorMessage . Should ( ) . Contain ( "unsupported script type" ) ;
429+ }
430+
431+ /// <summary>
432+ /// Verifies that ValidateExtension returns invalid when script type is null or empty.
433+ /// </summary>
434+ [ Fact ]
435+ public void ValidateExtension_WhenScriptTypeIsEmpty_ReturnsInvalid ( )
436+ {
437+ // Arrange
438+ m_Manager = new Manager ( m_FileSystemMock . Object , m_Settings . Object ) ;
439+ var extensionName = "TestExtension" ;
440+ var metadata = new Nexus . Engine . Extensions . Models . ExtensionMetadata
441+ {
442+ Name = extensionName ,
443+ ScriptFile = "script.ps1" ,
444+ ScriptType = string . Empty ,
445+ FullScriptPath = "C:\\ extensions\\ TestExtension\\ script.ps1" ,
446+ } ;
447+
448+ // Manually add extension to manager's internal dictionary via reflection
449+ var extensionsField = typeof ( Manager ) . GetField ( "m_Extensions" , System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
450+ if ( extensionsField ? . GetValue ( m_Manager ) is System . Collections . Generic . Dictionary < string , Nexus . Engine . Extensions . Models . ExtensionMetadata > extensions )
451+ {
452+ extensions [ extensionName ] = metadata ;
453+ }
454+
455+ // Setup file system to return true for file exists check
456+ _ = m_FileSystemMock . Setup ( fs => fs . FileExists ( It . IsAny < string > ( ) ) ) . Returns ( true ) ;
457+
458+ // Act
459+ var ( isValid , errorMessage ) = m_Manager . ValidateExtension ( extensionName ) ;
460+
461+ // Assert
462+ _ = isValid . Should ( ) . BeFalse ( ) ;
463+ _ = errorMessage . Should ( ) . Contain ( "no script type" ) ;
464+ }
465+
466+ /// <summary>
467+ /// Verifies that constructor handles absolute path configuration correctly.
468+ /// </summary>
469+ [ Fact ]
470+ public void Constructor_WithAbsolutePathConfiguration_UsesPathAsIs ( )
471+ {
472+ // Arrange
473+ var absolutePath = "C:\\ Absolute\\ Extensions\\ Path" ;
474+ var sharedConfig = new SharedConfiguration
475+ {
476+ McpNexus = new McpNexusSettings
477+ {
478+ Extensions = new ExtensionsSettings
479+ {
480+ ExtensionsPath = absolutePath ,
481+ CallbackPort = 0 ,
482+ } ,
483+ } ,
484+ } ;
485+ _ = m_Settings . Setup ( s => s . Get ( ) ) . Returns ( sharedConfig ) ;
486+ _ = m_FileSystemMock . Setup ( fs => fs . DirectoryExists ( It . Is < string > ( p => p == absolutePath ) ) ) . Returns ( true ) ;
487+
488+ // Act
489+ m_Manager = new Manager ( m_FileSystemMock . Object , m_Settings . Object ) ;
490+
491+ // Assert
492+ _ = m_Manager . Should ( ) . NotBeNull ( ) ;
493+ m_FileSystemMock . Verify ( fs => fs . DirectoryExists ( It . Is < string > ( p => p == absolutePath ) ) , Times . AtLeastOnce ) ;
494+ }
495+
496+ /// <summary>
497+ /// Verifies that LoadExtensionsAsync handles directory that doesn't exist.
498+ /// </summary>
499+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
500+ [ Fact ]
501+ public async Task LoadExtensionsAsync_WhenDirectoryDoesNotExist_CreatesDirectory ( )
502+ {
503+ // Arrange
504+ m_Manager = new Manager ( m_FileSystemMock . Object , m_Settings . Object ) ;
505+ _ = m_FileSystemMock . Setup ( fs => fs . DirectoryExists ( It . IsAny < string > ( ) ) ) . Returns ( false ) ;
506+
507+ // Act
508+ await m_Manager . LoadExtensionsAsync ( ) ;
509+
510+ // Assert
511+ m_FileSystemMock . Verify ( fs => fs . CreateDirectory ( It . IsAny < string > ( ) ) , Times . AtLeastOnce ) ;
512+ }
513+
514+ /// <summary>
515+ /// Verifies that LoadExtensionsAsync propagates file system errors from GetFiles.
516+ /// </summary>
517+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
518+ [ Fact ]
519+ public async Task LoadExtensionsAsync_WhenGetFilesThrowsException_PropagatesException ( )
520+ {
521+ // Arrange
522+ m_Manager = new Manager ( m_FileSystemMock . Object , m_Settings . Object ) ;
523+
524+ // Reset mock after constructor setup
525+ m_FileSystemMock . Reset ( ) ;
526+ _ = m_FileSystemMock . Setup ( fs => fs . DirectoryExists ( It . IsAny < string > ( ) ) ) . Returns ( true ) ;
527+ _ = m_FileSystemMock . Setup ( fs => fs . GetFiles ( It . IsAny < string > ( ) , It . IsAny < string > ( ) , It . IsAny < SearchOption > ( ) ) )
528+ . Throws ( new UnauthorizedAccessException ( "Access denied" ) ) ;
529+
530+ // Act & Assert - exception should propagate since GetFiles is not wrapped in try-catch
531+ _ = await Assert . ThrowsAsync < UnauthorizedAccessException > ( async ( ) => await m_Manager . LoadExtensionsAsync ( ) ) ;
532+ }
309533}
0 commit comments