/*
┌──────────────────────────────────────────────────────────────────┐
│ Author: Ivan Murzak (https://github.com/IvanMurzak) │
│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
│ Copyright (c) 2025 Ivan Murzak │
│ Licensed under the Apache License, Version 2.0. │
│ See the LICENSE file in the project root for more information. │
└──────────────────────────────────────────────────────────────────┘
*/
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using com.IvanMurzak.ReflectorNet;
using com.IvanMurzak.ReflectorNet.Model;
using com.IvanMurzak.ReflectorNet.Utils;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace com.IvanMurzak.Unity.MCP.Editor.Tests.AtomicApi
{
///
/// EditMode coverage for ReflectorNet.Reflector.TryModifyAt — the atomic
/// path-based field/array-element/dictionary-entry modification API.
/// The tests run in a real Unity Editor process (managed assembly load
/// context) to validate that the API behaves correctly when consumed by
/// the Unity-MCP plugin.
///
public class TryModifyAtTests : BaseTest
{
[UnitySetUp]
public override IEnumerator SetUp() => base.SetUp();
[UnityTearDown]
public override IEnumerator TearDown() => base.TearDown();
// ─── TryModifyAt — root field ─────────────────────────────────────────
[UnityTest]
public IEnumerator TryModifyAt_RootField()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco { globalOrbitSpeedMultiplier = 1f, globalSizeMultiplier = 2f };
object? obj = system;
var success = reflector.TryModifyAt(ref obj, "globalOrbitSpeedMultiplier", 5f);
Assert.IsTrue(success, "TryModifyAt should succeed on a root field.");
var result = (StarSystemPoco)obj!;
Assert.AreEqual(5f, result.globalOrbitSpeedMultiplier);
Assert.AreEqual(2f, result.globalSizeMultiplier, "Sibling root field must be untouched.");
yield return null;
}
// ─── TryModifyAt — nested field via array element ─────────────────────
[UnityTest]
public IEnumerator TryModifyAt_ArrayElementField()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco
{
celestialBodies = new[]
{
new CelestialBody { orbitRadius = 10f, orbitSpeed = 1f, name = "alpha" },
new CelestialBody { orbitRadius = 20f, orbitSpeed = 2f, name = "beta" },
}
};
object? obj = system;
var success = reflector.TryModifyAt(ref obj, "celestialBodies/[0]/orbitRadius", 999f);
Assert.IsTrue(success);
var result = (StarSystemPoco)obj!;
Assert.AreEqual(999f, result.celestialBodies![0].orbitRadius);
Assert.AreEqual(1f, result.celestialBodies![0].orbitSpeed, "Sibling field on same element must be untouched.");
Assert.AreEqual(20f, result.celestialBodies![1].orbitRadius, "Sibling array element must be untouched.");
Assert.AreEqual(2f, result.celestialBodies![1].orbitSpeed, "Sibling array element must be untouched.");
yield return null;
}
// ─── TryModifyAt — Dictionary string key ──────────────────────────────
[UnityTest]
public IEnumerator TryModifyAt_DictionaryStringKey()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco
{
config = new Dictionary { ["timeout"] = 10, ["retries"] = 3 }
};
object? obj = system;
var success = reflector.TryModifyAt(ref obj, "config/[timeout]", 60);
Assert.IsTrue(success);
var result = (StarSystemPoco)obj!;
Assert.AreEqual(60, result.config["timeout"]);
Assert.AreEqual(3, result.config["retries"], "Sibling dict entry must be untouched.");
yield return null;
}
// ─── TryModifyAt — Dictionary integer key ─────────────────────────────
[UnityTest]
public IEnumerator TryModifyAt_DictionaryIntKey()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco
{
lookup = new Dictionary { [1] = "one", [2] = "two", [3] = "three" }
};
object? obj = system;
var success = reflector.TryModifyAt(ref obj, "lookup/[2]", "TWO");
Assert.IsTrue(success);
var result = (StarSystemPoco)obj!;
Assert.AreEqual("TWO", result.lookup[2]);
Assert.AreEqual("one", result.lookup[1], "Sibling dict entry must be untouched.");
Assert.AreEqual("three", result.lookup[3], "Sibling dict entry must be untouched.");
yield return null;
}
// ─── TryModifyAt — partial element patch via SerializedMember ─────────
// Mirrors ReflectorNet's own TryModifyAt_PartialPatch_ArrayElement test:
// navigate to an array element and apply a partial SerializedMember
// patch — only the specified field changes, sibling fields are
// preserved.
[UnityTest]
public IEnumerator TryModifyAt_PartialPatch_ArrayElement()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco
{
celestialBodies = new[]
{
new CelestialBody { orbitRadius = 10f, orbitSpeed = 3f, name = "alpha" },
new CelestialBody { orbitRadius = 20f, orbitSpeed = 4f, name = "beta" },
}
};
object? obj = system;
// Build a SerializedMember that only specifies orbitRadius, then apply at [1].
var patch = new SerializedMember { typeName = typeof(CelestialBody).GetTypeId() };
patch.SetFieldValue(reflector, "orbitRadius", 777f);
var logs = new Logs();
var success = reflector.TryModifyAt(ref obj, "celestialBodies/[1]", patch, logs: logs);
Debug.Log($"[TryModifyAt_PartialPatch] logs:\n{logs}");
Assert.IsTrue(success);
var result = (StarSystemPoco)obj!;
Assert.AreEqual(777f, result.celestialBodies![1].orbitRadius);
Assert.AreEqual(4f, result.celestialBodies![1].orbitSpeed,
"Sibling field on the same element must be untouched by partial patch.");
Assert.AreEqual("beta", result.celestialBodies![1].name,
"Sibling field on the same element must be untouched by partial patch.");
Assert.AreEqual(10f, result.celestialBodies![0].orbitRadius,
"Sibling array element must be untouched.");
yield return null;
}
// ─── TryModifyAt — out-of-range index logs error ──────────────────────
[UnityTest]
public IEnumerator TryModifyAt_OutOfRangeIndex_LogsError_LeavesGraphUntouched()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco
{
celestialBodies = new[]
{
new CelestialBody { orbitRadius = 1f },
new CelestialBody { orbitRadius = 2f },
}
};
object? obj = system;
var logs = new Logs();
var success = reflector.TryModifyAt(
ref obj, "celestialBodies/[99]/orbitRadius", 0f, logs: logs);
Assert.IsFalse(success);
var logsText = logs.ToString();
Debug.Log($"[TryModifyAt_OutOfRange] logs:\n{logsText}");
StringAssert.Contains("[99]", logsText);
StringAssert.Contains("out of range", logsText);
// Graph must be untouched
Assert.AreEqual(1f, ((StarSystemPoco)obj!).celestialBodies![0].orbitRadius);
Assert.AreEqual(2f, ((StarSystemPoco)obj!).celestialBodies![1].orbitRadius);
yield return null;
}
// ─── TryModifyAt — missing member logs detailed error ─────────────────
[UnityTest]
public IEnumerator TryModifyAt_MissingMember_LogsDetailedError()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco { globalOrbitSpeedMultiplier = 1f };
object? obj = system;
var logs = new Logs();
var success = reflector.TryModifyAt(ref obj, "doesNotExist", 5f, logs: logs);
Assert.IsFalse(success);
var logsText = logs.ToString();
Debug.Log($"[TryModifyAt_MissingMember] logs:\n{logsText}");
StringAssert.Contains("doesNotExist", logsText);
StringAssert.Contains("not found", logsText);
// Sibling left untouched
Assert.AreEqual(1f, ((StarSystemPoco)obj!).globalOrbitSpeedMultiplier);
yield return null;
}
}
}