/// import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; const privateSource = (...parts: string[]): string => readFileSync( resolve( process.cwd(), 'plugins/McpAutomationBridge/Source/McpAutomationBridge/Private', ...parts, ), 'utf8', ); describe('plugin runtime behavior contracts', () => { it('applies byte and enum JSON settings before rejecting unsupported properties', () => { const source = privateSource( 'Foundation', 'Reflection', 'McpPropertyReflectionImport.cpp', ); const byteHandler = source.indexOf( 'if (FByteProperty* ByteProp = CastField(Property))', ); const enumHandler = source.indexOf( 'if (FEnumProperty* EnumProp = CastField(Property))', ); const unsupportedFallback = source.indexOf( 'Unsupported property type: %s', ); expect(byteHandler).toBeGreaterThan(-1); expect(enumHandler).toBeGreaterThan(byteHandler); expect(unsupportedFallback).toBeGreaterThan(enumHandler); }); it('validates enum inputs before conversion and rejects hidden sentinels', () => { const source = privateSource( 'Foundation', 'Reflection', 'McpPropertyReflectionImport.cpp', ); expect(source).toContain("Name[Index] == TEXT('\\0')"); expect(source).toContain('if (bContainsEmbeddedNull)'); expect(source).toContain('FMath::IsFinite(Value)'); expect(source).toContain('FMath::Frac(Value) != 0.0'); expect(source).toContain( 'Value < static_cast(MIN_int64)', ); expect(source).toContain('Value >= Int64UpperBound'); expect(source).toContain('IntValue < MIN_int32'); expect(source).toContain('IntValue > MAX_int32'); expect(source).toContain('Enum->GetIndexByValue(OutValue)'); expect(source).toContain('Enum->HasMetaData(TEXT("Hidden"), EnumIndex)'); expect(source).toContain('Enum->NumEnums() - 1'); expect(source).not.toContain('GenerateFullEnumName'); expect(source).not.toContain( 'OutValue == INDEX_NONE || !Enum->IsValidEnumValue(OutValue)', ); }); it('distinguishes omitted channel textures from failed channel texture loads', () => { const source = privateSource( 'Domains', 'Texture', 'McpAutomationBridge_TextureHandlersChannelPack.cpp', ); expect(source).not.toContain('Response->GetBoolField(TEXT("success"))'); for (const channel of ['Red', 'Green', 'Blue', 'Alpha']) { expect(source).toContain( `if (!${channel}Path.IsEmpty() && !${channel}Tex)`, ); } }); it('resolves Blueprint root aliases even when no explicit root node exists', () => { const supportSource = privateSource( 'Domains', 'SCS', 'McpAutomationBridge_SCSHandlers.cpp', ); const addSource = privateSource( 'Domains', 'SCS', 'McpAutomationBridge_SCSHandlersAddComponent.cpp', ); expect(supportSource).toContain('bool IsSCSRootAlias('); expect(supportSource).toMatch( /IsSCSRootAlias\(ExpectedParentName\)[\s\S]*IsSCSRootNode\(SCS, Node\)/u, ); expect(addSource).toContain('IsSCSRootAlias(ParentComponentName)'); expect(addSource).toMatch( /IsSCSRootAlias\(ParentComponentName\)[\s\S]*SCS->GetRootNodes\(\)/u, ); }); it('adds Niagara modules through the stack graph utility', () => { const source = privateSource( 'Domains', 'NiagaraGraph', 'McpAutomationBridge_NiagaraGraphHandlers.cpp', ); expect(source).toContain( 'FNiagaraStackGraphUtilities::AddScriptModuleToStack', ); expect(source).not.toContain( 'NewObject(TargetGraph)', ); }); it('splices the Niagara parameter-map chain before removing stack modules', () => { const handlerSource = privateSource( 'Domains', 'NiagaraGraph', 'McpAutomationBridge_NiagaraGraphHandlers.cpp', ); const removalSource = privateSource( 'Domains', 'NiagaraGraph', 'McpAutomationBridge_NiagaraGraphRemoval.cpp', ); expect(handlerSource).toContain('RemoveNiagaraGraphNodeSafely('); expect(handlerSource).not.toContain( 'TargetGraph->RemoveNode(TargetNode)', ); expect(removalSource).toMatch( /TryCreateConnection\(\s*PreviousOutputPin,\s*NextInputPin\)[\s\S]*RemoveNode\(TargetNode\)/u, ); }); it('prefers an existing Niagara connection before attempting auto-connect', () => { const source = privateSource( 'Domains', 'NiagaraGraph', 'McpAutomationBridge_NiagaraGraphPins.cpp', ); const existingConnectionCheck = source.indexOf( 'if (!FromCandidatePin->LinkedTo.IsEmpty())', ); const connectionAttempt = source.indexOf( 'TryCreateConnection(FromCandidatePin, ToCandidatePin)', ); expect(existingConnectionCheck).toBeGreaterThan(-1); expect(connectionAttempt).toBeGreaterThan(-1); expect(existingConnectionCheck).toBeLessThan(connectionAttempt); }); it('switches to a transient editor world before deleting active worlds', () => { const source = privateSource( 'Safety', 'McpSafeOperationsFolderDeleteAssets.h', ); expect(source).toContain('GEditor->NewMap(false)'); expect(source).not.toContain('TEXT("/Engine/Maps/'); }); it('verifies that map loading changed to the requested world package', () => { const source = privateSource('Safety', 'McpSafeOperationsMapLoad.h'); expect(source).toContain('ResolveExpectedMapPackageName('); expect(source).toMatch( /FEditorFileUtils::LoadMap\(\*MapPath\)[\s\S]*LoadedWorldPackageName\.Equals\(\s*ExpectedPackageName/u, ); expect(source).toContain( 'McpSafeLoadMap: Editor world does not match requested map', ); }); it('emits subscribed logs as schema-valid structured automation events', () => { const source = privateSource( 'Domains', 'Log', 'McpAutomationBridge_LogHandlers.cpp', ); const responseSource = privateSource( 'Core', 'Subsystem', 'McpAutomationBridgeSubsystemResponses.cpp', ); const transportHeader = privateSource( 'MCP', 'Transport', 'McpNativeTransport.h', ); const transportNotifications = privateSource( 'MCP', 'Transport', 'McpNativeTransportNotifications.cpp', ); expect(source).toContain( 'Event->SetStringField(TEXT("type"), TEXT("automation_event"))', ); expect(source).toContain( 'Event->SetObjectField(TEXT("payload"), Payload)', ); expect(source).toContain('StrongSubsystem->BroadcastAutomationEvent(Event)'); expect(source).not.toContain('StrongSubsystem->SendRawMessage'); expect(source).toContain('SendAutomationResponse('); expect(source).not.toContain('SendRawMessage(Serialized)'); expect(responseSource).toContain( 'UMcpAutomationBridgeSubsystem::BroadcastAutomationEvent', ); expect(responseSource).toContain('SendRawMessageToLogSubscribers'); expect(responseSource).toContain('SendRawMessageToSocket'); expect(responseSource).toContain( 'BroadcastLogEventNotification(Event)', ); expect(transportHeader).toContain('BroadcastNotification('); expect(transportHeader).toContain( 'BroadcastLogEventNotification(', ); expect(transportNotifications).toContain( 'FMcpNativeTransport::BroadcastNotification', ); expect(transportNotifications).toContain('IsLogAutomationEvent'); expect(transportNotifications).toContain('QueueNotificationEventWrites'); expect(transportNotifications).toContain( 'SubscribedSessions.Contains(Stream->SessionId)', ); expect(transportNotifications).toContain( 'TEXT("notifications/unreal/automation_event")', ); expect(source).toContain( 'SetLogEventSubscriptionForRequest', ); expect(source).toContain('ConnectionManager->SetLogSubscription'); expect(source).toContain('HasLogEventSubscribers'); expect(source).toContain('HasLogSubscribers'); expect(source).not.toContain( 'TEXT("{\\"event\\":\\"log\\",\\"category\\"', ); }); it('routes blueprint completion automation events through the subsystem broadcaster', () => { const eventSources = [ privateSource('Domains', 'Blueprint', 'Graph', 'McpAutomationBridge_BlueprintHandlersAddNodeResponse.cpp'), privateSource('Domains', 'Blueprint', 'Components', 'McpAutomationBridge_BlueprintHandlersModifyScsFinalize.cpp'), privateSource('Domains', 'Blueprint', 'Functions', 'McpAutomationBridge_BlueprintHandlersAddFunctionResult.cpp'), privateSource('Domains', 'Blueprint', 'Variables', 'McpAutomationBridge_BlueprintHandlersVariableMetadata.cpp'), privateSource('Domains', 'Blueprint', 'Events', 'McpAutomationBridge_BlueprintHandlersAddEventResult.cpp'), privateSource('Domains', 'Blueprint', 'Events', 'McpAutomationBridge_BlueprintHandlersRemoveEvent.cpp'), ]; for (const source of eventSources) { expect(source).toContain( 'SetStringField(TEXT("type"), TEXT("automation_event"))', ); expect(source).toContain('Bridge.BroadcastAutomationEvent(Notify, RequestingSocket)'); expect(source).not.toContain('SendControlMessage(Notify)'); } }); it('locks Unreal Insights tracing to loopback and profiling trace files', () => { const validation = privateSource( 'Domains', 'Insights', 'McpAutomationBridge_InsightsValidation.cpp', ); const traceControl = privateSource( 'Domains', 'Insights', 'McpAutomationBridge_InsightsTraceControl.cpp', ); const traceAnalysis = privateSource( 'Domains', 'Insights', 'McpAutomationBridge_InsightsTraceAnalysis.cpp', ); const traceState = privateSource( 'Domains', 'Insights', 'McpAutomationBridge_InsightsTraceState.cpp', ); expect(validation).toContain('IsLoopbackHost'); expect(validation).toContain('NON_LOOPBACK_HOST_DENIED'); expect(validation).toContain('FPaths::ProfilingDir()'); expect(validation).toContain( 'Trace file path must stay under the project Saved/Profiling directory.', ); expect(validation).toMatch( /bRequireExistingFile[\s\S]*GetExtension\(Path\)\.Equals\(TEXT\("utrace"\)/u, ); expect(traceControl).toContain('Mode == TEXT("relay")'); expect(traceControl).toContain('Mode == TEXT("none")'); expect(traceControl).toContain('Mode == TEXT("memory")'); expect(traceControl).not.toContain( 'FTraceAuxiliary::EConnectionType::None', ); expect(traceState).toMatch( /if \(!HasActiveTraceForStateChange\(\)\)[\s\S]*TRACE_PAUSE_FAILED/u, ); expect(traceState).toMatch( /if \(!HasActiveTraceForStateChange\(\)\)[\s\S]*TRACE_RESUME_FAILED/u, ); expect(traceAnalysis).toContain( 'TryResolveTracePath(Payload, true, false, false', ); }); it('bounds custom handler alias config before parsing', () => { const source = privateSource( 'Core', 'Subsystem', 'McpAutomationBridgeSubsystemCustomHandlerAliasConfig.cpp', ); const sizeCheck = source.indexOf('MaxAliasConfigBytes'); const loadFile = source.indexOf('FFileHelper::LoadFileToString'); expect(sizeCheck).toBeGreaterThan(-1); expect(loadFile).toBeGreaterThan(sizeCheck); expect(source).toContain('FileSize > MaxAliasConfigBytes'); expect(source).toContain('unsupported size'); expect(source).toContain('TryGetNumberField(TEXT("version"), Version)'); expect(source).toContain('FMath::IsNearlyEqual(Version, 1.0)'); expect(source).not.toContain('TryGetArrayField(TEXT("handlerAliases")'); expect(source).not.toContain('TryGetStringField(TEXT("action")'); expect(source).not.toContain('TryGetStringField(TEXT("targetAction")'); }); it('does not attach a transient nested emitter to a new Niagara system', () => { const source = privateSource( 'Domains', 'NiagaraAuthoring', 'McpAutomationBridge_NiagaraAuthoringHandlersSystems.cpp', ); expect(source).not.toContain( 'NewObject(NewSystem, FName(TEXT("DefaultEmitter")))', ); }); it('makes WebSocket listener shutdown idempotent and race-safe', () => { const header = privateSource( 'Transport', 'WebSocket', 'McpBridgeWebSocket.h', ); const lifecycleSource = privateSource( 'Transport', 'WebSocket', 'McpBridgeWebSocket.cpp', ); const serverSource = privateSource( 'Transport', 'WebSocket', 'McpBridgeWebSocketServer.cpp', ); expect(header).toContain('TAtomic bCloseStarted;'); expect(header).toContain('FCriticalSection ListenSocketMutex;'); expect(lifecycleSource).toContain('if (bCloseStarted.Exchange(true))'); expect(lifecycleSource).toContain('CloseListenSocket();'); expect(serverSource).toContain('ON_SCOPE_EXIT'); expect(serverSource).toContain('DestroyListenSocket();'); expect(serverSource).not.toMatch( /DestroySocket\(ListenSocket\)[\s\S]{0,80}ListenSocket = nullptr/u, ); }); });