@mflrevan/ucp 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using NUnit.Framework;
4
+ using UnityEngine;
5
+
6
+ namespace UCP.Bridge.Tests
7
+ {
8
+ /// <summary>
9
+ /// Regression coverage for the serializer that turns arbitrary <see cref="IUCPScript"/> return
10
+ /// values into JSON. The reflection walk used to be unbounded, so returning anything holding a
11
+ /// UnityEngine math struct (Vector3.normalized returns another Vector3, forever) overflowed the
12
+ /// stack -- which .NET cannot catch and which killed the editor process outright.
13
+ /// </summary>
14
+ public class MiniJsonSerializerTests
15
+ {
16
+ private static Dictionary<string, object> Roundtrip(object value)
17
+ {
18
+ var json = MiniJson.Serialize(new { value });
19
+ var parsed = MiniJson.Deserialize(json) as Dictionary<string, object>;
20
+ Assert.IsNotNull(parsed, $"Serializer produced unparseable JSON: {json}");
21
+ return parsed;
22
+ }
23
+
24
+ [Test]
25
+ public void SerializesVector3WithoutStackOverflow()
26
+ {
27
+ // The original crash repro: `return new { pos = Vector3.zero }` from a UCP script.
28
+ var json = MiniJson.Serialize(new { pos = Vector3.zero });
29
+
30
+ Assert.AreEqual("{\"pos\":{\"x\":0,\"y\":0,\"z\":0}}", json);
31
+ }
32
+
33
+ [Test]
34
+ public void SerializesUnityMathStructsAsPlainShapes()
35
+ {
36
+ Assert.AreEqual("{\"x\":1,\"y\":2}", MiniJson.Serialize(new Vector2(1f, 2f)));
37
+ Assert.AreEqual("{\"x\":1,\"y\":2,\"z\":3,\"w\":4}", MiniJson.Serialize(new Vector4(1f, 2f, 3f, 4f)));
38
+ Assert.AreEqual("{\"x\":0,\"y\":0,\"z\":0,\"w\":1}", MiniJson.Serialize(Quaternion.identity));
39
+ Assert.AreEqual("{\"r\":1,\"g\":0,\"b\":0,\"a\":1}", MiniJson.Serialize(Color.red));
40
+ Assert.AreEqual("{\"x\":1,\"y\":2,\"z\":3}", MiniJson.Serialize(new Vector3Int(1, 2, 3)));
41
+ Assert.AreEqual(
42
+ "{\"x\":1,\"y\":2,\"width\":3,\"height\":4}",
43
+ MiniJson.Serialize(new Rect(1f, 2f, 3f, 4f)));
44
+ Assert.AreEqual(
45
+ "{\"center\":{\"x\":0,\"y\":0,\"z\":0},\"size\":{\"x\":2,\"y\":2,\"z\":2}}",
46
+ MiniJson.Serialize(new Bounds(Vector3.zero, Vector3.one * 2f)));
47
+ }
48
+
49
+ [Test]
50
+ public void SerializesQuaternionNestedInAnonymousResult()
51
+ {
52
+ var parsed = Roundtrip(new { rot = Quaternion.Euler(0f, 90f, 0f), pos = Vector3.one });
53
+ Assert.IsInstanceOf<Dictionary<string, object>>(parsed["value"]);
54
+ }
55
+
56
+ [Test]
57
+ public void SerializesUnityObjectAsIdentityInsteadOfWalkingTheSceneGraph()
58
+ {
59
+ var go = new GameObject("MiniJsonProbe");
60
+ try
61
+ {
62
+ // GameObject.transform.gameObject is a cycle; walking it never terminates.
63
+ var parsed = Roundtrip(go);
64
+ var identity = (Dictionary<string, object>)parsed["value"];
65
+
66
+ Assert.AreEqual("MiniJsonProbe", identity["name"]);
67
+ Assert.AreEqual("GameObject", identity["type"]);
68
+ Assert.IsTrue(identity.ContainsKey("instanceId"));
69
+
70
+ // Components are the same story via Component.gameObject.
71
+ var componentJson = MiniJson.Serialize(go.transform);
72
+ Assert.IsTrue(componentJson.Contains("\"type\":\"Transform\""), componentJson);
73
+ }
74
+ finally
75
+ {
76
+ UnityEngine.Object.DestroyImmediate(go);
77
+ }
78
+ }
79
+
80
+ [Test]
81
+ public void DestroyedUnityObjectSerializesAsNull()
82
+ {
83
+ var go = new GameObject("MiniJsonDestroyed");
84
+ UnityEngine.Object.DestroyImmediate(go);
85
+
86
+ Assert.AreEqual("{\"value\":null}", MiniJson.Serialize(new { value = go }));
87
+ }
88
+
89
+ [Test]
90
+ public void ReferenceCyclesAreBrokenInsteadOfRecursingForever()
91
+ {
92
+ var a = new Node { Label = "a" };
93
+ var b = new Node { Label = "b", Next = a };
94
+ a.Next = b;
95
+
96
+ var json = MiniJson.Serialize(a);
97
+
98
+ Assert.IsTrue(json.Contains("<ucp:cycle>"), json);
99
+ Assert.IsNotNull(MiniJson.Deserialize(json));
100
+ }
101
+
102
+ [Test]
103
+ public void SelfReferencingDictionaryIsBroken()
104
+ {
105
+ var dict = new Dictionary<string, object> { ["name"] = "root" };
106
+ dict["self"] = dict;
107
+
108
+ var json = MiniJson.Serialize(dict);
109
+
110
+ Assert.IsTrue(json.Contains("<ucp:cycle>"), json);
111
+ Assert.IsNotNull(MiniJson.Deserialize(json));
112
+ }
113
+
114
+ [Test]
115
+ public void UnboundedComputedPropertyRecursionIsDepthCapped()
116
+ {
117
+ // Mirrors the Vector3.normalized shape for a type the serializer has no special case
118
+ // for: every read allocates a fresh instance, so reference tracking cannot help and
119
+ // only the depth cap prevents a stack overflow.
120
+ var json = MiniJson.Serialize(new Fractal());
121
+
122
+ Assert.IsTrue(json.Contains("<ucp:max-depth>"), json);
123
+ Assert.IsNotNull(MiniJson.Deserialize(json));
124
+ }
125
+
126
+ [Test]
127
+ public void ThrowingGetterDoesNotCorruptTheDocument()
128
+ {
129
+ var json = MiniJson.Serialize(new Explosive());
130
+
131
+ Assert.IsFalse(json.Contains("boom"), json);
132
+ var parsed = MiniJson.Deserialize(json) as Dictionary<string, object>;
133
+ Assert.IsNotNull(parsed, json);
134
+ Assert.AreEqual("ok", parsed["safe"]);
135
+ }
136
+
137
+ [Test]
138
+ public void NonFiniteFloatsSerializeAsNullRatherThanInvalidJson()
139
+ {
140
+ // Degenerate bounds and zero-length normalize hand out NaN routinely; raw NaN/Infinity
141
+ // are not valid JSON and made the whole response unparseable on the CLI side.
142
+ var json = MiniJson.Serialize(new { a = float.NaN, b = float.PositiveInfinity, c = double.NaN });
143
+
144
+ Assert.AreEqual("{\"a\":null,\"b\":null,\"c\":null}", json);
145
+ Assert.IsNotNull(MiniJson.Deserialize(json));
146
+ }
147
+
148
+ [Test]
149
+ public void UnsignedAndWideIntegersSerializeAsNumbers()
150
+ {
151
+ var json = MiniJson.Serialize(new { a = (uint)7, b = (ushort)8, c = (byte)9, d = 10UL });
152
+
153
+ Assert.AreEqual("{\"a\":7,\"b\":8,\"c\":9,\"d\":10}", json);
154
+ }
155
+
156
+ [Test]
157
+ public void NonListEnumerablesSerializeAsArrays()
158
+ {
159
+ var json = MiniJson.Serialize(new { items = new HashSet<int> { 1 } });
160
+
161
+ Assert.AreEqual("{\"items\":[1]}", json);
162
+ }
163
+
164
+ [Test]
165
+ public void EnumsStillSerializeAsIntegers()
166
+ {
167
+ Assert.AreEqual("{\"value\":2}", MiniJson.Serialize(new { value = SampleEnum.Two }));
168
+ }
169
+
170
+ [Test]
171
+ public void ParserRejectsTruncatedInputInsteadOfHanging()
172
+ {
173
+ // A truncated string literal used to spin the reader on end-of-input forever, wedging
174
+ // the editor's main thread.
175
+ Assert.Throws<FormatException>(() => MiniJson.Deserialize("{\"a\": \"unterminated"));
176
+ }
177
+
178
+ [Test]
179
+ public void ParserRejectsPathologicallyNestedInput()
180
+ {
181
+ var deep = new string('[', 1000);
182
+ Assert.Throws<FormatException>(() => MiniJson.Deserialize(deep));
183
+ }
184
+
185
+ [Test]
186
+ public void OrdinaryPayloadsAreUnchanged()
187
+ {
188
+ var json = MiniJson.Serialize(new Dictionary<string, object>
189
+ {
190
+ ["name"] = "cube",
191
+ ["active"] = true,
192
+ ["children"] = new List<object> { 1L, 2.5, null },
193
+ });
194
+
195
+ Assert.AreEqual("{\"name\":\"cube\",\"active\":true,\"children\":[1,2.5,null]}", json);
196
+ }
197
+
198
+ private enum SampleEnum
199
+ {
200
+ One = 1,
201
+ Two = 2,
202
+ }
203
+
204
+ private sealed class Node
205
+ {
206
+ public string Label;
207
+ public Node Next;
208
+ }
209
+
210
+ private sealed class Fractal
211
+ {
212
+ public Fractal Child => new Fractal();
213
+ }
214
+
215
+ private sealed class Explosive
216
+ {
217
+ public string Safe => "ok";
218
+ public string Boom => throw new InvalidOperationException("boom");
219
+ }
220
+ }
221
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 5082955dbcce78c4986000348e3a5d92
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.ucp.bridge",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "displayName": "Unity Control Protocol Bridge",
5
5
  "description": "WebSocket bridge for programmatic Unity Editor control via CLI and AI agents.",
6
6
  "unity": "2021.3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mflrevan/ucp",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Unity Control Protocol - CLI for programmatic Unity Editor control",
5
5
  "license": "MIT",
6
6
  "repository": {