@@ -345,6 +345,131 @@ public void DaemonServiceComposition_ConfigurePlugins_LocksApiV1WebsocketPathAnd
345345 Assert . Contains ( "options.SetAutoPong(true)" , source , StringComparison . Ordinal ) ;
346346 }
347347
348+ [ Fact ]
349+ [ Trait ( "Category" , "Inbound" ) ]
350+ [ Trait ( "Category" , "DaemonInbound" ) ]
351+ [ Trait ( "Category" , "DaemonInboundStatic" ) ]
352+ [ Trait ( "Category" , "Documentation" ) ]
353+ public void ApplicationStartupLog_PointsToApifoxDocsOnly ( )
354+ {
355+ var source = ReadSourceFile ( "MCServerLauncher.Daemon/Application.cs" ) ;
356+
357+ Assert . Contains ( "var remoteAddress = GetBoundRemoteAddress();" , source , StringComparison . Ordinal ) ;
358+ Assert . Contains ( "[Remote] Apifox docs available at http://{RemoteAddress}/apifox.json" , source ,
359+ StringComparison . Ordinal ) ;
360+ Assert . DoesNotContain ( "[Remote] Apifox docs available at http://0.0.0.0:{0}/apifox.json" , source ,
361+ StringComparison . Ordinal ) ;
362+ Assert . DoesNotContain ( "postman_collection.json" , source , StringComparison . OrdinalIgnoreCase ) ;
363+ }
364+
365+ [ Fact ]
366+ [ Trait ( "Category" , "Inbound" ) ]
367+ [ Trait ( "Category" , "DaemonInbound" ) ]
368+ [ Trait ( "Category" , "DaemonInboundStatic" ) ]
369+ [ Trait ( "Category" , "Documentation" ) ]
370+ public void DaemonAssembly_EmbedsApifoxProjectAndProtocolDocsOnly ( )
371+ {
372+ var resourceNames = typeof ( HttpPlugin ) . Assembly . GetManifestResourceNames ( ) ;
373+
374+ Assert . DoesNotContain ( "MCServerLauncher.Daemon.Resources.Docs.openapi.json" , resourceNames ) ;
375+ Assert . DoesNotContain ( "MCServerLauncher.Daemon.Resources.Docs.postman_collection.json" , resourceNames ) ;
376+ Assert . Contains ( "MCServerLauncher.Daemon.Resources.Docs.apifox.json" , resourceNames ) ;
377+ Assert . DoesNotContain ( "MCServerLauncher.Daemon.Resources.Docs.swagger.index.html" , resourceNames ) ;
378+ Assert . DoesNotContain ( "MCServerLauncher.Daemon.Resources.Docs.swagger.swagger-ui.css" , resourceNames ) ;
379+ Assert . DoesNotContain ( "MCServerLauncher.Daemon.Resources.Docs.swagger.swagger-ui-bundle.js" , resourceNames ) ;
380+ Assert . DoesNotContain ( "MCServerLauncher.Daemon.Resources.Docs.swagger.swagger-ui-standalone-preset.js" , resourceNames ) ;
381+ Assert . Contains ( "MCServerLauncher.Daemon.Resources.Docs.protocol.topics.connection.md" , resourceNames ) ;
382+ Assert . Contains ( "MCServerLauncher.Daemon.Resources.Docs.protocol.topics.actions.md" , resourceNames ) ;
383+ }
384+
385+ [ Fact ]
386+ [ Trait ( "Category" , "Inbound" ) ]
387+ [ Trait ( "Category" , "DaemonInbound" ) ]
388+ [ Trait ( "Category" , "DaemonInboundStatic" ) ]
389+ [ Trait ( "Category" , "Documentation" ) ]
390+ public void HttpPluginFile_ExposesEmbeddedApifoxProjectAndProtocolDocs ( )
391+ {
392+ AssertFileContains ( "MCServerLauncher.Daemon/Remote/HttpPlugin.cs" ,
393+ "EmbeddedDocumentation.TryGetResource(request.URL, out var document)" ) ;
394+ AssertFileDoesNotContain ( "MCServerLauncher.Daemon/Remote/EmbeddedDocumentation.cs" ,
395+ "\" /openapi.json\" " ) ;
396+ AssertFileDoesNotContain ( "MCServerLauncher.Daemon/Remote/EmbeddedDocumentation.cs" ,
397+ "\" /postman_collection.json\" " ) ;
398+ AssertFileContains ( "MCServerLauncher.Daemon/Remote/EmbeddedDocumentation.cs" ,
399+ "\" /apifox.json\" " ) ;
400+ AssertFileContains ( "MCServerLauncher.Daemon/Remote/EmbeddedDocumentation.cs" ,
401+ "\" /docs/protocol/\" " ) ;
402+ AssertFileDoesNotContain ( "MCServerLauncher.Daemon/Remote/EmbeddedDocumentation.cs" ,
403+ "\" /swagger\" " ) ;
404+ }
405+
406+ [ Fact ]
407+ [ Trait ( "Category" , "Inbound" ) ]
408+ [ Trait ( "Category" , "DaemonInbound" ) ]
409+ [ Trait ( "Category" , "DaemonInboundStatic" ) ]
410+ [ Trait ( "Category" , "Documentation" ) ]
411+ public void HttpPluginFile_DoesNotExposeActionHttpBridge ( )
412+ {
413+ AssertFileDoesNotContain ( "MCServerLauncher.Daemon/Remote/HttpPlugin.cs" ,
414+ "HttpActionBridge.TryGetActionName(request.URL, out var actionName)" ) ;
415+ AssertFileDoesNotContain ( "MCServerLauncher.Daemon/Remote/HttpPlugin.cs" ,
416+ "HttpActionBridge.HandleAsync" ) ;
417+ Assert . False ( File . Exists ( Path . Combine ( ResolveRepoRoot ( ) , "MCServerLauncher.Daemon/Remote/HttpActionBridge.cs" ) ) ) ;
418+ }
419+
420+ [ Fact ]
421+ [ Trait ( "Category" , "Inbound" ) ]
422+ [ Trait ( "Category" , "DaemonInbound" ) ]
423+ [ Trait ( "Category" , "DaemonInboundStatic" ) ]
424+ [ Trait ( "Category" , "Documentation" ) ]
425+ public void OpenApiArtifacts_AreNotGeneratedOrEmbedded ( )
426+ {
427+ var repoRoot = ResolveRepoRoot ( ) ;
428+ Assert . False ( File . Exists ( Path . Combine ( repoRoot , "MCServerLauncher.Daemon/.Resources/Docs/openapi.json" ) ) ) ;
429+ Assert . False ( File . Exists ( Path . Combine ( repoRoot , "../mcsl-future-protocol/openapi.json" ) ) ) ;
430+ AssertFileDoesNotContain ( "MCServerLauncher.Daemon/MCServerLauncher.Daemon.csproj" ,
431+ ".Resources\\ Docs\\ openapi.json" ) ;
432+ }
433+
434+ [ Fact ]
435+ [ Trait ( "Category" , "Inbound" ) ]
436+ [ Trait ( "Category" , "DaemonInbound" ) ]
437+ [ Trait ( "Category" , "DaemonInboundStatic" ) ]
438+ [ Trait ( "Category" , "Documentation" ) ]
439+ public void PostmanArtifacts_AreNotGeneratedOrEmbedded ( )
440+ {
441+ var repoRoot = ResolveRepoRoot ( ) ;
442+
443+ Assert . False ( File . Exists ( Path . Combine ( repoRoot , "MCServerLauncher.Daemon/.Resources/Docs/postman_collection.json" ) ) ) ;
444+ AssertFileDoesNotContain ( "MCServerLauncher.Daemon/MCServerLauncher.Daemon.csproj" ,
445+ ".Resources\\ Docs\\ postman_collection.json" ) ;
446+ AssertFileDoesNotContain ( "MCServerLauncher.Daemon/Remote/EmbeddedDocumentation.cs" ,
447+ "postman_collection.json" ) ;
448+ }
449+
450+ [ Fact ]
451+ [ Trait ( "Category" , "Inbound" ) ]
452+ [ Trait ( "Category" , "DaemonInbound" ) ]
453+ [ Trait ( "Category" , "DaemonInboundStatic" ) ]
454+ [ Trait ( "Category" , "Documentation" ) ]
455+ public void EmbeddedApifoxProject_DocumentsWebSocketActionsAsWebSocketApis ( )
456+ {
457+ var repoRoot = ResolveRepoRoot ( ) ;
458+ var apifoxPath = Path . Combine ( repoRoot , "MCServerLauncher.Daemon/.Resources/Docs/apifox.json" ) ;
459+ using var apifox = JsonDocument . Parse ( File . ReadAllText ( apifoxPath ) ) ;
460+ var root = apifox . RootElement ;
461+
462+ Assert . Equal ( "1.0.0" , root . GetProperty ( "apifoxProject" ) . GetString ( ) ) ;
463+ Assert . Contains ( "{{wsUrl}}?token={{token}}" , root . GetRawText ( ) , StringComparison . Ordinal ) ;
464+ Assert . DoesNotContain ( "/api/v1/actions/" , root . GetRawText ( ) , StringComparison . Ordinal ) ;
465+ Assert . Contains ( "protocol/topics/actions.md" , root . GetRawText ( ) , StringComparison . Ordinal ) ;
466+ Assert . Contains ( "protocol/topics/models.md" , root . GetRawText ( ) , StringComparison . Ordinal ) ;
467+ Assert . Contains ( "ActionResponse" , root . GetRawText ( ) , StringComparison . Ordinal ) ;
468+ Assert . Contains ( "SystemInfo" , root . GetRawText ( ) , StringComparison . Ordinal ) ;
469+ AssertApifoxWebSocketActionsMatchActionType ( root ) ;
470+ AssertApifoxProjectIncludesMfpSchemasAndDocs ( root ) ;
471+ }
472+
348473 [ Fact ]
349474 [ Trait ( "Category" , "Inbound" ) ]
350475 [ Trait ( "Category" , "DaemonInbound" ) ]
@@ -408,6 +533,16 @@ private static void AssertFileDoesNotContain(string relativePath, string forbidd
408533 Assert . DoesNotContain ( forbiddenText , source , StringComparison . Ordinal ) ;
409534 }
410535
536+ private static void AssertSchemaReferenceExists ( JsonElement schemaReference , JsonElement schemas , string context )
537+ {
538+ var reference = schemaReference . GetProperty ( "$ref" ) . GetString ( ) ;
539+ Assert . False ( string . IsNullOrWhiteSpace ( reference ) , $ "{ context } schema reference is empty") ;
540+ const string prefix = "#/components/schemas/" ;
541+ Assert . StartsWith ( prefix , reference , StringComparison . Ordinal ) ;
542+ var schemaName = reference [ prefix . Length ..] ;
543+ Assert . True ( schemas . TryGetProperty ( schemaName , out _ ) , $ "{ context } schema '{ schemaName } ' is missing") ;
544+ }
545+
411546 private static string ResolveRepoRoot ( )
412547 {
413548 var dir = AppDomain . CurrentDomain . BaseDirectory ;
@@ -425,6 +560,102 @@ private static string ReadSourceFile(string relativePath)
425560 return File . ReadAllText ( Path . Combine ( repoRoot , relativePath ) ) ;
426561 }
427562
563+ private static void AssertApifoxWebSocketActionsMatchActionType ( JsonElement root )
564+ {
565+ var expected = Enum . GetNames < ActionType > ( )
566+ . Select ( JsonNamingPolicy . SnakeCaseLower . ConvertName )
567+ . Order ( StringComparer . Ordinal )
568+ . ToArray ( ) ;
569+
570+ Assert . Equal ( "根目录" , root . GetProperty ( "webSocketCollection" ) [ 0 ] . GetProperty ( "name" ) . GetString ( ) ) ;
571+ Assert . Equal ( "WebSocket actions" , root . GetProperty ( "webSocketCollection" ) [ 0 ]
572+ . GetProperty ( "items" ) [ 0 ]
573+ . GetProperty ( "name" )
574+ . GetString ( ) ) ;
575+
576+ var webSocketItems = root . GetProperty ( "webSocketCollection" )
577+ . EnumerateArray ( )
578+ . SelectMany ( EnumerateApifoxApiItems )
579+ . ToArray ( ) ;
580+
581+ var actions = webSocketItems
582+ . Select ( item => item . GetProperty ( "name" ) . GetString ( ) )
583+ . Order ( StringComparer . Ordinal )
584+ . ToArray ( ) ;
585+
586+ Assert . Equal ( expected , actions ) ;
587+
588+ foreach ( var api in webSocketItems . Select ( item => item . GetProperty ( "api" ) ) )
589+ {
590+ Assert . Equal ( "{{wsUrl}}?token={{token}}" , api . GetProperty ( "path" ) . GetString ( ) ) ;
591+ Assert . False ( api . TryGetProperty ( "type" , out _ ) , "Native Apifox WebSocket APIs omit api.type" ) ;
592+ Assert . False ( api . TryGetProperty ( "method" , out _ ) , "Apifox WebSocket APIs must not masquerade as HTTP requests" ) ;
593+ Assert . True ( api . GetProperty ( "requestBody" ) . TryGetProperty ( "message" , out var message ) ) ;
594+ Assert . False ( string . IsNullOrWhiteSpace ( message . GetString ( ) ) ) ;
595+ Assert . Equal ( JsonValueKind . Array , api . GetProperty ( "requestBody" ) . GetProperty ( "parameters" ) . ValueKind ) ;
596+ Assert . Equal ( JsonValueKind . Array , api . GetProperty ( "parameters" ) . GetProperty ( "query" ) . ValueKind ) ;
597+ Assert . Equal ( JsonValueKind . Array , api . GetProperty ( "parameters" ) . GetProperty ( "path" ) . ValueKind ) ;
598+ Assert . Equal ( JsonValueKind . Array , api . GetProperty ( "parameters" ) . GetProperty ( "cookie" ) . ValueKind ) ;
599+ Assert . Equal ( JsonValueKind . Array , api . GetProperty ( "parameters" ) . GetProperty ( "header" ) . ValueKind ) ;
600+ Assert . Equal ( JsonValueKind . Array , api . GetProperty ( "commonParameters" ) . GetProperty ( "query" ) . ValueKind ) ;
601+ Assert . Equal ( JsonValueKind . Array , api . GetProperty ( "commonParameters" ) . GetProperty ( "body" ) . ValueKind ) ;
602+ Assert . Equal ( JsonValueKind . Array , api . GetProperty ( "commonParameters" ) . GetProperty ( "cookie" ) . ValueKind ) ;
603+ Assert . Equal ( JsonValueKind . Array , api . GetProperty ( "commonParameters" ) . GetProperty ( "header" ) . ValueKind ) ;
604+ }
605+ }
606+
607+ private static IEnumerable < JsonElement > EnumerateApifoxApiItems ( JsonElement item )
608+ {
609+ if ( item . ValueKind != JsonValueKind . Object )
610+ {
611+ yield break ;
612+ }
613+
614+ if ( item . TryGetProperty ( "api" , out _ ) )
615+ {
616+ yield return item ;
617+ }
618+
619+ if ( ! item . TryGetProperty ( "items" , out var children ) )
620+ {
621+ yield break ;
622+ }
623+
624+ foreach ( var child in children . EnumerateArray ( ) )
625+ {
626+ foreach ( var apiItem in EnumerateApifoxApiItems ( child ) )
627+ {
628+ yield return apiItem ;
629+ }
630+ }
631+ }
632+
633+ private static void AssertApifoxProjectIncludesMfpSchemasAndDocs ( JsonElement root )
634+ {
635+ var schemaNames = root . GetProperty ( "schemaCollection" )
636+ . EnumerateArray ( )
637+ . SelectMany ( group => group . GetProperty ( "items" ) . EnumerateArray ( ) )
638+ . Select ( item => item . GetProperty ( "name" ) . GetString ( ) )
639+ . ToHashSet ( StringComparer . Ordinal ) ;
640+
641+ Assert . Contains ( "ActionRequest" , schemaNames ) ;
642+ Assert . Contains ( "ActionResponse" , schemaNames ) ;
643+ Assert . Contains ( "EventPacket" , schemaNames ) ;
644+ Assert . Contains ( "SystemInfo" , schemaNames ) ;
645+ Assert . Contains ( "DaemonReport" , schemaNames ) ;
646+ Assert . Contains ( "InstanceConfig" , schemaNames ) ;
647+
648+ var docNames = root . GetProperty ( "docCollection" )
649+ . EnumerateArray ( )
650+ . SelectMany ( group => group . GetProperty ( "items" ) . EnumerateArray ( ) )
651+ . Select ( item => item . GetProperty ( "name" ) . GetString ( ) )
652+ . ToHashSet ( StringComparer . Ordinal ) ;
653+
654+ Assert . Contains ( "MFP actions" , docNames ) ;
655+ Assert . Contains ( "MFP models" , docNames ) ;
656+ Assert . Contains ( "MFP connection" , docNames ) ;
657+ }
658+
428659 private sealed class FakeExecutor : IActionExecutor
429660 {
430661 public FakeExecutor ( IReadOnlyDictionary < ActionType , ActionHandlerMeta > ? handlerMetas = null )
0 commit comments