/*
┌──────────────────────────────────────────────────────────────────┐
│ 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 System.Text.Json;
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.TryPatch — the JSON Merge
/// Patch-style API that touches multiple fields at different depths in a
/// single call. Includes the polymorphic "$type" hint scenario.
///
public class TryPatchTests : BaseTest
{
[UnitySetUp]
public override IEnumerator SetUp() => base.SetUp();
[UnityTearDown]
public override IEnumerator TearDown() => base.TearDown();
// ─── TryPatch — multiple top-level fields at once ─────────────────────
[UnityTest]
public IEnumerator TryPatch_MultipleTopLevelFields()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco
{
globalOrbitSpeedMultiplier = 1f,
globalSizeMultiplier = 1f,
celestialBodies = new[]
{
new CelestialBody { orbitRadius = 10f, orbitSpeed = 1f },
}
};
object? obj = system;
var json = @"{
""globalOrbitSpeedMultiplier"": 5.0,
""globalSizeMultiplier"": 2.0
}";
var logs = new Logs();
var success = reflector.TryPatch(ref obj, json, logs: logs);
Debug.Log($"[TryPatch_Top] logs:\n{logs}");
Assert.IsTrue(success);
var result = (StarSystemPoco)obj!;
Assert.AreEqual(5f, result.globalOrbitSpeedMultiplier);
Assert.AreEqual(2f, result.globalSizeMultiplier);
Assert.AreEqual(10f, result.celestialBodies![0].orbitRadius, "Untouched array element must be preserved.");
yield return null;
}
// ─── TryPatch — multi-field touching nested array AND dictionary ──────
[UnityTest]
public IEnumerator TryPatch_NestedArrayAndDictionary()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco
{
celestialBodies = new[]
{
new CelestialBody { orbitRadius = 10f, orbitSpeed = 1f },
new CelestialBody { orbitRadius = 20f, orbitSpeed = 2f },
},
config = new Dictionary { ["timeout"] = 10, ["retries"] = 3 }
};
object? obj = system;
// Modify celestialBodies[0].orbitRadius AND config[timeout] in one patch.
var json = @"{
""celestialBodies"": {
""[0]"": {
""orbitRadius"": 42.0
}
},
""config"": {
""[timeout]"": 60
}
}";
var logs = new Logs();
var success = reflector.TryPatch(ref obj, json, logs: logs);
Debug.Log($"[TryPatch_NestedArrayAndDict] logs:\n{logs}");
Assert.IsTrue(success);
var result = (StarSystemPoco)obj!;
Assert.AreEqual(42f, result.celestialBodies![0].orbitRadius);
Assert.AreEqual(1f, result.celestialBodies![0].orbitSpeed, "Same-element sibling field must be untouched.");
Assert.AreEqual(20f, result.celestialBodies![1].orbitRadius, "Sibling array element must be untouched.");
Assert.AreEqual(60, result.config["timeout"]);
Assert.AreEqual(3, result.config["retries"], "Sibling dict entry must be untouched.");
yield return null;
}
// ─── TryPatch — JsonElement overload ──────────────────────────────────
[UnityTest]
public IEnumerator TryPatch_JsonElement_Overload()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var system = new StarSystemPoco { globalOrbitSpeedMultiplier = 1f };
object? obj = system;
using var doc = JsonDocument.Parse(@"{ ""globalOrbitSpeedMultiplier"": 9.0 }");
var logs = new Logs();
var success = reflector.TryPatch(ref obj, doc.RootElement, logs: logs);
Debug.Log($"[TryPatch_JsonElement] logs:\n{logs}");
Assert.IsTrue(success);
Assert.AreEqual(9f, ((StarSystemPoco)obj!).globalOrbitSpeedMultiplier);
yield return null;
}
// ─── TryPatch — "$type" polymorphic replacement ────────────────────────
[UnityTest]
public IEnumerator TryPatch_TypeHint_PolymorphicReplacement()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var container = new AnimalContainer { animal = new Animal { name = "Cat" } };
object? obj = container;
var dogTypeId = typeof(Dog).GetTypeId();
// The "$type" key tells TryPatch to replace the existing Animal instance
// with a fresh Dog instance, then apply the remaining fields onto it.
var json = $@"{{""animal"": {{""$type"": ""{dogTypeId}"", ""name"": ""Rex"", ""breed"": ""Husky""}}}}";
var logs = new Logs();
var success = reflector.TryPatch(ref obj, json, logs: logs);
Debug.Log($"[TryPatch_TypeHint] logs:\n{logs}");
Assert.IsTrue(success);
var result = (AnimalContainer)obj!;
Assert.IsInstanceOf(result.animal);
Assert.AreEqual("Rex", result.animal!.name);
Assert.AreEqual("Husky", ((Dog)result.animal).breed);
yield return null;
}
// ─── TryPatch — null sets field to null (RFC 7396) ────────────────────
[UnityTest]
public IEnumerator TryPatch_NullValue_SetsFieldToNull()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector
?? throw new Exception("Reflector is not available.");
var container = new AnimalContainer { animal = new Animal { name = "Cat" } };
object? obj = container;
var logs = new Logs();
var success = reflector.TryPatch(ref obj, @"{""animal"": null}", logs: logs);
Debug.Log($"[TryPatch_Null] logs:\n{logs}");
Assert.IsTrue(success);
Assert.IsNull(((AnimalContainer)obj!).animal);
yield return null;
}
// ─── TryPatch — invalid JSON returns false ────────────────────────────
[UnityTest]
public IEnumerator TryPatch_InvalidJson_ReturnsFalse()
{
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.TryPatch(ref obj, "{ this is not valid json }", logs: logs);
Assert.IsFalse(success);
var logsText = logs.ToString();
Debug.Log($"[TryPatch_InvalidJson] logs:\n{logsText}");
StringAssert.Contains("Failed to parse JSON patch", logsText);
Assert.AreEqual(1f, ((StarSystemPoco)obj!).globalOrbitSpeedMultiplier, "Graph must be untouched on parse failure.");
yield return null;
}
}
}