211 lines
10 KiB
C#
211 lines
10 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using MCPForUnity.Editor.Services.AssetGen.Http;
|
|
using MCPForUnity.Editor.Services.AssetGen.Providers;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
|
|
namespace MCPForUnityTests.Editor.AssetGen
|
|
{
|
|
/// <summary>
|
|
/// Exercises the Meshy adapter against a <see cref="FakeHttpTransport"/> — no network.
|
|
/// Asserts request shaping (endpoint, method, Bearer scheme), the task id field, and
|
|
/// poll-state mapping. The bearer secret is never asserted beyond the "Bearer " prefix.
|
|
/// </summary>
|
|
public class MeshyAdapterTests
|
|
{
|
|
private static string ProjectRoot()
|
|
{
|
|
string dp = Application.dataPath.Replace('\\', '/');
|
|
return dp.Substring(0, dp.Length - "Assets".Length);
|
|
}
|
|
|
|
private static string WriteProjectFile(string rel, byte[] bytes)
|
|
{
|
|
string abs = Path.Combine(ProjectRoot(), rel).Replace('\\', '/');
|
|
Directory.CreateDirectory(Path.GetDirectoryName(abs));
|
|
File.WriteAllBytes(abs, bytes);
|
|
return rel;
|
|
}
|
|
|
|
private static HttpResult Json(string json, int status = 200)
|
|
=> new HttpResult
|
|
{
|
|
Status = status,
|
|
IsSuccess = status >= 200 && status < 300,
|
|
Text = json,
|
|
Body = Encoding.UTF8.GetBytes(json)
|
|
};
|
|
|
|
[Test]
|
|
public void Submit_PostsTextEndpoint_WithBearerHeader_AndReturnsResultId()
|
|
{
|
|
var http = new FakeHttpTransport { Handler = _ => Json("{\"result\":\"task_meshy_1\"}") };
|
|
var adapter = new MeshyAdapter();
|
|
var req = new ModelGenRequest { Provider = "meshy", Mode = "text", Prompt = "a brass lantern", Format = "glb" };
|
|
|
|
string taskId = adapter.SubmitAsync(req, "msy_secret_value", http, CancellationToken.None)
|
|
.GetAwaiter().GetResult();
|
|
|
|
Assert.AreEqual("task_meshy_1", taskId);
|
|
HttpRequestSpec rec = http.RecordedRequests[0];
|
|
StringAssert.Contains("/openapi/v2/text-to-3d", rec.Url);
|
|
Assert.AreEqual("POST", rec.Method);
|
|
Assert.IsTrue(rec.Headers.ContainsKey("Authorization"));
|
|
StringAssert.StartsWith("Bearer ", rec.Headers["Authorization"]);
|
|
|
|
string body = Encoding.UTF8.GetString(rec.Body);
|
|
StringAssert.Contains("a brass lantern", body);
|
|
StringAssert.Contains("preview", body);
|
|
}
|
|
|
|
[Test]
|
|
public void Poll_Succeeded_ReturnsGlbUrl()
|
|
{
|
|
var http = new FakeHttpTransport
|
|
{
|
|
Handler = _ => Json(
|
|
"{\"status\":\"SUCCEEDED\",\"progress\":100," +
|
|
"\"model_urls\":{\"glb\":\"https://assets.meshy.ai/model.glb\",\"fbx\":\"https://assets.meshy.ai/model.fbx\"}}")
|
|
};
|
|
var adapter = new MeshyAdapter();
|
|
// Texture=false -> single-phase (no refine), so a SUCCEEDED preview surfaces directly.
|
|
var req = new ModelGenRequest { Provider = "meshy", Mode = "text", Prompt = "x", Format = "glb", Texture = false };
|
|
adapter.SubmitAsync(req, "k", new FakeHttpTransport { Handler = _ => Json("{\"result\":\"id1\"}") }, CancellationToken.None)
|
|
.GetAwaiter().GetResult();
|
|
|
|
ProviderPollResult res = adapter.PollAsync("id1", "k", http, CancellationToken.None).GetAwaiter().GetResult();
|
|
|
|
Assert.AreEqual(ProviderPollState.Succeeded, res.State);
|
|
Assert.AreEqual("https://assets.meshy.ai/model.glb", res.DownloadUrl);
|
|
Assert.AreEqual(1f, res.Progress, 0.001f);
|
|
}
|
|
|
|
[Test]
|
|
public void Submit_NoTaskIdInResponse_ErrorIncludesBody()
|
|
{
|
|
var fake = new FakeHttpTransport { Handler = _ => Json("{\"message\":\"quota exceeded\"}") };
|
|
var adapter = new MeshyAdapter();
|
|
var req = new ModelGenRequest { Provider = "meshy", Mode = "text", Prompt = "x", Texture = false };
|
|
|
|
var ex = Assert.Throws<System.Exception>(() =>
|
|
adapter.SubmitAsync(req, "k", fake, CancellationToken.None).GetAwaiter().GetResult());
|
|
StringAssert.Contains("quota exceeded", ex.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void Poll_InProgress_ReturnsRunning_WithProgress()
|
|
{
|
|
var http = new FakeHttpTransport { Handler = _ => Json("{\"status\":\"IN_PROGRESS\",\"progress\":37}") };
|
|
var adapter = new MeshyAdapter();
|
|
// Submit single-phase (Texture=false) so progress is reported raw (not split across refine).
|
|
adapter.SubmitAsync(
|
|
new ModelGenRequest { Provider = "meshy", Mode = "text", Prompt = "x", Texture = false },
|
|
"k", new FakeHttpTransport { Handler = _ => Json("{\"result\":\"id1\"}") }, CancellationToken.None)
|
|
.GetAwaiter().GetResult();
|
|
|
|
ProviderPollResult res = adapter.PollAsync("id1", "k", http, CancellationToken.None).GetAwaiter().GetResult();
|
|
|
|
Assert.AreEqual(ProviderPollState.Running, res.State);
|
|
Assert.AreEqual(0.37f, res.Progress, 0.001f);
|
|
}
|
|
|
|
[Test]
|
|
public void Submit_ImageMode_PollsV1ImageEndpoint()
|
|
{
|
|
var submitFake = new FakeHttpTransport { Handler = _ => Json("{\"result\":\"img1\"}") };
|
|
var pollFake = new FakeHttpTransport
|
|
{
|
|
Handler = _ => Json("{\"status\":\"SUCCEEDED\",\"progress\":100,\"model_urls\":{\"glb\":\"https://m/i.glb\"}}")
|
|
};
|
|
var adapter = new MeshyAdapter();
|
|
var req = new ModelGenRequest { Provider = "meshy", Mode = "image", ImageUrl = "https://ex.com/ref.png", Format = "glb" };
|
|
|
|
string id = adapter.SubmitAsync(req, "k", submitFake, CancellationToken.None).GetAwaiter().GetResult();
|
|
Assert.AreEqual("img1", id);
|
|
StringAssert.Contains("/openapi/v1/image-to-3d", submitFake.RecordedRequests[0].Url);
|
|
|
|
ProviderPollResult res = adapter.PollAsync(id, "k", pollFake, CancellationToken.None).GetAwaiter().GetResult();
|
|
|
|
// Image tasks must be polled at the v1 image endpoint, not the v2 text endpoint.
|
|
StringAssert.Contains("/openapi/v1/image-to-3d/img1", pollFake.RecordedRequests[0].Url);
|
|
Assert.AreEqual(ProviderPollState.Succeeded, res.State);
|
|
Assert.AreEqual("https://m/i.glb", res.DownloadUrl);
|
|
}
|
|
|
|
[Test]
|
|
public void Submit_ImageMode_LocalPath_SendsDataUri()
|
|
{
|
|
string rel = WriteProjectFile("Assets/Generated/__assetgen_meshy_adapter/ref.png", new byte[] { 137, 80, 78, 71 });
|
|
try
|
|
{
|
|
var fake = new FakeHttpTransport { Handler = _ => Json("{\"result\":\"id1\"}") };
|
|
var adapter = new MeshyAdapter();
|
|
var req = new ModelGenRequest { Provider = "meshy", Mode = "image", ImagePath = rel, Format = "glb" };
|
|
|
|
adapter.SubmitAsync(req, "k", fake, CancellationToken.None).GetAwaiter().GetResult();
|
|
|
|
HttpRequestSpec rec = fake.RecordedRequests[0];
|
|
StringAssert.Contains("/openapi/v1/image-to-3d", rec.Url);
|
|
StringAssert.Contains("data:image/png;base64,", Encoding.UTF8.GetString(rec.Body));
|
|
}
|
|
finally { try { Directory.Delete(Path.Combine(ProjectRoot(), "Assets/Generated/__assetgen_meshy_adapter"), true); } catch { } }
|
|
}
|
|
|
|
[Test]
|
|
public void TextWithTexture_PreviewSucceeded_SubmitsRefine_ThenReturnsTexturedModel()
|
|
{
|
|
var submitFake = new FakeHttpTransport { Handler = _ => Json("{\"result\":\"prev1\"}") };
|
|
var pollFake = new FakeHttpTransport
|
|
{
|
|
Handler = spec => spec.Method == "POST"
|
|
? Json("{\"result\":\"refine1\"}") // refine submit
|
|
: Json("{\"status\":\"SUCCEEDED\",\"progress\":100,\"model_urls\":{\"glb\":\"https://m/refined.glb\"}}")
|
|
};
|
|
var adapter = new MeshyAdapter();
|
|
// Texture defaults to true -> two-phase preview+refine.
|
|
var req = new ModelGenRequest { Provider = "meshy", Mode = "text", Prompt = "a chair", Format = "glb" };
|
|
|
|
string previewId = adapter.SubmitAsync(req, "k", submitFake, CancellationToken.None).GetAwaiter().GetResult();
|
|
Assert.AreEqual("prev1", previewId);
|
|
|
|
// Poll #1: preview SUCCEEDED -> adapter submits a refine task and reports Running.
|
|
ProviderPollResult p1 = adapter.PollAsync(previewId, "k", pollFake, CancellationToken.None).GetAwaiter().GetResult();
|
|
Assert.AreEqual(ProviderPollState.Running, p1.State);
|
|
|
|
bool refinePosted = false;
|
|
foreach (HttpRequestSpec r in pollFake.RecordedRequests)
|
|
{
|
|
if (r.Method == "POST" && r.Body != null)
|
|
{
|
|
string b = Encoding.UTF8.GetString(r.Body);
|
|
if (b.Contains("refine") && b.Contains("prev1")) refinePosted = true;
|
|
}
|
|
}
|
|
Assert.IsTrue(refinePosted, "expected a refine POST carrying the preview_task_id");
|
|
|
|
// Poll #2: the refine task SUCCEEDED -> textured model url surfaced.
|
|
ProviderPollResult p2 = adapter.PollAsync(previewId, "k", pollFake, CancellationToken.None).GetAwaiter().GetResult();
|
|
Assert.AreEqual(ProviderPollState.Succeeded, p2.State);
|
|
Assert.AreEqual("https://m/refined.glb", p2.DownloadUrl);
|
|
}
|
|
|
|
[Test]
|
|
public void Poll_Failed_MapsFailed_WithError()
|
|
{
|
|
var http = new FakeHttpTransport
|
|
{
|
|
Handler = _ => Json("{\"status\":\"FAILED\",\"progress\":0,\"task_error\":{\"message\":\"render error\"}}")
|
|
};
|
|
var adapter = new MeshyAdapter();
|
|
|
|
ProviderPollResult res = adapter.PollAsync("id1", "k", http, CancellationToken.None).GetAwaiter().GetResult();
|
|
|
|
Assert.AreEqual(ProviderPollState.Failed, res.State);
|
|
Assert.IsNotEmpty(res.Error);
|
|
}
|
|
}
|
|
}
|