///
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
const environmentSource = (filename: string): string =>
readFileSync(
resolve(
process.cwd(),
'plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/Environment',
filename,
),
'utf8',
);
const repositorySource = (filename: string): string =>
readFileSync(resolve(process.cwd(), filename), 'utf8');
const countPureLines = (source: string): number =>
source
.split(/\r?\n/u)
.filter((line) => !/^\s*$/u.test(line) && !/^\s*(?:#|\/\/)/u.test(line))
.length;
const adapterFiles = [
'McpAutomationBridge_EnvironmentHandlersBuildAdapters.cpp',
'McpAutomationBridge_EnvironmentHandlersBuildDeletion.cpp',
'McpAutomationBridge_EnvironmentHandlersBuildLandscapeFoliage.cpp',
'McpAutomationBridge_EnvironmentHandlersBuildSkyWeather.cpp',
'McpAutomationBridge_EnvironmentHandlersBuildSnapshots.cpp',
'McpAutomationBridge_EnvironmentHandlersBuildWater.cpp',
'McpAutomationBridge_EnvironmentSnapshotPaths.cpp',
'McpAutomationBridge_EnvironmentHandlersSnapshotData.cpp',
] as const;
describe('environment build adapter contracts', () => {
it('keeps responsibility-based environment build shards within 250 pure lines', () => {
// Given
const sources = adapterFiles.map((file) => ({
file,
source: environmentSource(file),
}));
// When
const oversized = sources
.map(({ file, source }) => ({ file, pureLines: countPureLines(source) }))
.filter(({ pureLines }) => pureLines > 250);
// Then
expect(oversized).toEqual([]);
});
it('evaluates environment operations before recording their result', () => {
// Given
const operationSources = [
environmentSource(
'McpAutomationBridge_EnvironmentHandlersBuildLandscapeFoliage.cpp',
),
environmentSource(
'McpAutomationBridge_EnvironmentHandlersBuildSkyWeather.cpp',
),
environmentSource('McpAutomationBridge_EnvironmentHandlersBuildWater.cpp'),
].join('\n');
// When
const directResultCalls = operationSources.match(
/MarkActorConfigurationResult\(\s*Context,\s*Mcp/gu,
);
// Then
expect(directResultCalls).toBeNull();
expect(operationSources).toContain('const bool bResult = Mcp');
});
it('configures environment actor components instead of only actor wrappers', () => {
// Given
const source = environmentSource(
'McpAutomationBridge_EnvironmentHandlersBuildSkyWeather.cpp',
);
const adapterSource = environmentSource(
'McpAutomationBridge_EnvironmentHandlersBuildAdapters.cpp',
);
// When
const requiredComponentClasses = [
'/Script/Engine.SkyAtmosphereComponent',
'/Script/Engine.SkyLightComponent',
'/Script/Engine.DirectionalLightComponent',
'/Script/Engine.ExponentialHeightFogComponent',
'/Script/Engine.VolumetricCloudComponent',
'/Script/Engine.WindDirectionalSourceComponent',
];
// Then
for (const classPath of requiredComponentClasses) {
expect(source).toContain(classPath);
}
expect(source).toContain('TEXT("componentPath")');
expect(source).toContain('TEXT("COMPONENT_CREATION_FAILED")');
expect(source).toContain('TEXT("configurationErrors")');
expect(source).toContain('TEXT("CONFIGURATION_FAILED")');
expect(adapterSource).toContain('TEXT("componentPath")');
expect(adapterSource).toContain('TEXT("COMPONENT_CREATION_FAILED")');
expect(adapterSource).toContain('TEXT("configurationErrors")');
expect(adapterSource).toContain('TEXT("CONFIGURATION_FAILED")');
});
it('deletes exact actor paths and rejects ambiguous names through the editor subsystem', () => {
// Given
const source = environmentSource(
'McpAutomationBridge_EnvironmentHandlersBuildDeletion.cpp',
);
// When
const destroyResult = source.indexOf('ActorSubsystem->DestroyActors(ActorsToDestroy)');
const deletedTarget = source.indexOf(
'DeletedTargets.Add(ResolvedTarget.Target)',
destroyResult,
);
// Then
expect(source).toContain('GetEditorSubsystem()');
expect(source).toContain('Actor->GetPathName().Equals(Target');
expect(source).toContain('TEXT("ambiguousTargets")');
expect(source).toContain('TEXT("missingTargets")');
expect(source).toContain('TEXT("AMBIGUOUS_ACTOR")');
expect(source).toContain('TEXT("DELETE_PARTIAL")');
expect(source).toContain('TSet ResolvedActors');
expect(source).toContain('ResolvedActors.Contains(Actor)');
expect(source).toContain('ResolvedActors.Add(Actor)');
expect(source).not.toContain('EditorDestroyActor');
expect(destroyResult).toBeGreaterThan(-1);
expect(deletedTarget).toBeGreaterThan(destroyResult);
expect(source).toContain('!AmbiguousTargets.IsEmpty() || !MissingTargets.IsEmpty()');
expect(source).toContain('const TArray RemainingActors');
expect(source).toContain('!RemainingActors.Contains(ResolvedTarget.Actor)');
expect(source).toContain('GEditor->UndoTransaction(false)');
expect(source).toContain('TEXT("rolledBack")');
expect(source).toContain('ResolvedTarget.ActorPath, RestoredActors');
expect(source).toContain('TEXT("DELETE_ROLLBACK_FAILED")');
expect(source.indexOf('if (!AmbiguousTargets.IsEmpty() || !MissingTargets.IsEmpty())')).toBeLessThan(
destroyResult,
);
});
it('bounds snapshot JSON and resolves paths through the project security boundary', () => {
// Given
const handlerSource = environmentSource(
'McpAutomationBridge_EnvironmentHandlersBuildSnapshots.cpp',
);
const pathSource = environmentSource(
'McpAutomationBridge_EnvironmentSnapshotPaths.cpp',
);
// When
const pathValidation = pathSource.indexOf('McpResolveProjectFilePath(');
const actionHandler = handlerSource.slice(
handlerSource.indexOf('bool HandleBuildSnapshotAction'),
);
const resolverCall = actionHandler.indexOf(
'McpResolveEnvironmentSnapshotPath(',
);
const actionDispatch = actionHandler.indexOf('return LowerSub ==');
const fileAccess = handlerSource.search(
/(?:OpenRead|SaveStringToFile)\(/u,
);
// Then
expect(handlerSource).toContain('MaxSnapshotBytes');
expect(handlerSource).toContain('PlatformFile.OpenRead(*AbsolutePath)');
expect(handlerSource).toContain('ReadHandle->Size()');
expect(handlerSource).toContain('ReadHandle->Read(');
expect(handlerSource).toContain('FileSize < 0');
expect(handlerSource).toContain('FJsonSerializer::Deserialize');
expect(handlerSource).toContain('FJsonSerializer::Serialize');
expect(handlerSource).toContain('FFileHelper::SaveStringToFile');
expect(pathSource).toContain('CanonicalizeAllowedSnapshotRelativePath');
expect(pathSource).toContain('tmp/unreal-mcp/');
expect(pathSource).toContain('Saved/unreal-mcp/');
expect(handlerSource).not.toContain('MarkUnsupported');
expect(pathValidation).toBeGreaterThan(-1);
expect(resolverCall).toBeGreaterThan(-1);
expect(fileAccess).toBeGreaterThan(-1);
expect(actionDispatch).toBeGreaterThan(resolverCall);
});
it('canonicalizes leading dot segments before enforcing the snapshot allowlist', () => {
// Given
const source = environmentSource(
'McpAutomationBridge_EnvironmentSnapshotPaths.cpp',
);
// When
const leadingDotCanonicalization = source.indexOf(
'while (Path.StartsWith(TEXT("./")',
);
const allowlistValidation = source.indexOf(
'CanonicalizeAllowedSnapshotRelativePath(RelativePath)',
);
// Then
expect(leadingDotCanonicalization).toBeGreaterThan(-1);
expect(allowlistValidation).toBeGreaterThan(leadingDotCanonicalization);
});
it('captures and restores directional-light and skylight snapshot state', () => {
// Given
const source = environmentSource(
'McpAutomationBridge_EnvironmentHandlersSnapshotData.cpp',
);
// When
const requiredState = [
'TEXT("timeOfDay")',
'TEXT("directionalLightRotation")',
'TEXT("sunIntensity")',
'TEXT("skylightIntensity")',
'SetActorRotation',
'SetIntensity',
];
// Then
for (const stateContract of requiredState) {
expect(source).toContain(stateContract);
}
expect(source).toContain('TActorIterator');
expect(source).toContain('TActorIterator');
expect(source).toContain('TNumericLimits::Max()');
expect(source).toContain('RotationTimeOfDay');
expect(source).toContain('bConsistentTime');
expect(source).toContain('TryGetStringField(TEXT("directionalLightActorPath")');
expect(source).toContain('TryGetStringField(TEXT("skyLightActorPath")');
expect(source).toContain('Actor->GetPathName().Equals(ActorPath');
expect(source).toContain('Actor->GetPackage()->GetName().Equals(ActorPath');
expect(source).toContain(
'FindSnapshotDirectionalLight(DirectionalLightActorPath)',
);
expect(source).toContain('FindSnapshotSkyLight(SkyLightActorPath)');
expect(source).toContain('ReadSnapshotNumber(*RotationObject, TEXT("pitch")');
expect(source).toContain('ReadSnapshotNumber(*RotationObject, TEXT("yaw")');
expect(source).toContain('ReadSnapshotNumber(*RotationObject, TEXT("roll")');
});
it('derives rotation only for unversioned snapshots with no rotation field', () => {
// Given
const source = environmentSource(
'McpAutomationBridge_EnvironmentHandlersSnapshotData.cpp',
);
// When
const legacyFallback = source.indexOf(
'else if (!bHasVersion && bValidLightingFields)',
);
// Then
expect(source).toContain(
'const bool bHasVersion = VersionField != nullptr',
);
expect(source).toContain(
'if (Snapshot->HasField(TEXT("directionalLightRotation")))',
);
expect(legacyFallback).toBeGreaterThan(-1);
expect(source.indexOf('RotationFromTimeOfDay(TimeOfDay)')).toBeGreaterThan(
legacyFallback,
);
});
it('accepts only the supported integral snapshot version', () => {
// Given
const source = environmentSource(
'McpAutomationBridge_EnvironmentHandlersSnapshotData.cpp',
);
// When
const versionRead = source.indexOf(
'TryGetNumberField(TEXT("version"), SnapshotVersion)',
);
const supportedVersionCheck = source.indexOf('SnapshotVersion != 1.0');
// Then
expect(versionRead).toBeGreaterThan(-1);
expect(source).toContain('(*VersionField)->Type != EJson::Number');
expect(source).toContain('!FMath::IsFinite(SnapshotVersion)');
expect(supportedVersionCheck).toBeGreaterThan(versionRead);
});
it('mutates and verifies snapshot lighting before and after import', () => {
// Given
const source = repositorySource(
'tests/mcp-tools/world/build-environment.test.mjs',
);
// When
const skyLightSetup = source.indexOf('configure_sky_light');
const directionalLightSetup = source.indexOf(
'configure_directional_light_atmosphere',
);
const exportSnapshot = source.indexOf('export_snapshot');
const mutateSun = source.indexOf('CONFIG: mutate snapshot sun rotation');
const mutatedReadback = source.indexOf('INFO: snapshot sun rotation mutated');
const importSnapshot = source.indexOf(
'ACTION: import_snapshot restores lighting',
);
const restoredReadback = source.indexOf(
'INFO: snapshot sun rotation restored',
);
// Then
expect(skyLightSetup).toBeGreaterThan(-1);
expect(directionalLightSetup).toBeGreaterThan(-1);
expect(exportSnapshot).toBeGreaterThan(skyLightSetup);
expect(exportSnapshot).toBeGreaterThan(directionalLightSetup);
expect(mutateSun).toBeGreaterThan(exportSnapshot);
expect(mutatedReadback).toBeGreaterThan(mutateSun);
expect(importSnapshot).toBeGreaterThan(exportSnapshot);
expect(importSnapshot).toBeGreaterThan(mutatedReadback);
expect(restoredReadback).toBeGreaterThan(importSnapshot);
expect(source).toContain(
'structuredContent.result.data.rotation.0',
);
expect(source).toContain('structuredContent.result.data.value');
expect(source).toContain(
'structuredContent.result.directionalLightRotation.pitch',
);
expect(source).toContain('MUTATED_SUN_INTENSITY');
expect(source).toContain('ORIGINAL_SKY_INTENSITY');
expect(source).toContain(
"directionalLightActorPath: '${captured:snapshotSunPath}'",
);
expect(source).toContain(
"skyLightActorPath: '${captured:snapshotSkyPath}'",
);
});
it('does not route the skylight SourceType enum through generic settings', () => {
// Given
const source = repositorySource(
'tests/mcp-tools/world/build-environment.test.mjs',
);
const snapshotSetup = source.slice(
source.indexOf('CONFIG: configure snapshot skylight'),
source.indexOf('ACTION: export_snapshot'),
);
// When
const unsupportedSourceType = snapshotSetup.indexOf('SourceType');
// Then
expect(unsupportedSourceType).toBe(-1);
});
it('creates a truthful three-actor sky rig for create_sky_sphere', () => {
// Given
const source = environmentSource(
'McpAutomationBridge_EnvironmentHandlersBuildSkyWeather.cpp',
);
// When
const skySphereBranch = source.slice(
source.indexOf('LowerSub == TEXT("create_sky_sphere")'),
source.indexOf('LowerSub == TEXT("configure_sky_atmosphere")'),
);
// Then
expect(skySphereBranch).toContain('CreateSkySphereRig(Context)');
expect(source).toContain(
'TEXT("createdActorCount"), CreatedActorCount',
);
expect(source).toContain('TEXT("configuredActorCount"), ConfiguredActorCount');
expect(source).toContain('bOutCreated = !bExisted && !ActorPath.IsEmpty()');
expect(source).toContain('TEXT("skyAtmosphereActorPath")');
expect(source).toContain('TEXT("directionalLightActorPath")');
expect(source).toContain('TEXT("skyLightActorPath")');
});
});