using System.Text.Json.Nodes;
namespace MoShiro.Backend.Tests;
/// 真实的后端 + 真实的 Agent + 真实的拨号侧 Agent(都在进程内,回环网络)。
// 慢的端到端测试(真实时间的阶梯、要等测量块出来):CI 里单独一批跑(Category=Slow),不拦着出镜像,见 ci/verify-backend.sh
[Trait("Category", "Slow")]
public sealed class DialerEndToEndTests : IClassFixture
{
private readonly BackendFixture _backend;
public DialerEndToEndTests(BackendFixture backend)
{
_backend = backend;
}
private async Task AgentViewAsync(AgentHarness agent) => await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + agent.Id);
private async Task ChunkCountAsync(AgentHarness agent, string kind) =>
(await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/raw/chunks?agent_id={agent.Id}"))["items"]!.AsArray().Count(c => c!["kind"]!.GetValue() == kind);
[Fact]
public async Task Dialer_FollowsTheAgentListThePanelKnows_AndSavesItForTheNextStart()
{
await using AgentHarness first = await AgentHarness.StartAsync(_backend);
await using DialerHarness dialer = await DialerHarness.StartAsync(_backend);
await BackendFixture.EventuallyAsync(() => Task.FromResult(dialer.App.Dialer.IsConnected(first.Id)));
await using AgentHarness second = await AgentHarness.StartAsync(_backend);
await BackendFixture.EventuallyAsync(() => Task.FromResult(dialer.App.Dialer.IsConnected(second.Id)), TimeSpan.FromSeconds(30), "a new Agent should arrive through dial_targets state sync: " + string.Join(";", dialer.App.Dialer.Directory.Targets.Select(a => $"{a.NodeId}@{a.Host}:{a.TcpPort}")) + " v" + dialer.App.Dialer.Directory.Version + " want " + second.Id);
Assert.Contains(dialer.App.Dialer.Directory.Targets, a => a.NodeId == second.Id);
Assert.True(File.Exists(Path.Combine(dialer.Directory, "dial_targets.json")));
// 停用一个 Agent:Dialer 断开它
using HttpResponseMessage disabled = await _backend.Admin.PostAsync($"/api/admin/agents/{second.Id}/disable", BackendFixture.Json(new JsonObject()));
Assert.True(disabled.IsSuccessStatusCode, await disabled.Content.ReadAsStringAsync());
await BackendFixture.EventuallyAsync(() => Task.FromResult(!dialer.App.Dialer.IsConnected(second.Id)), TimeSpan.FromSeconds(30));
}
[Fact]
public async Task Hy2Ladder_WithTheRealDialer_Succeeds_AndEveryFragmentIsStored()
{
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
await agent.OverrideLineTasksAsync();
await using DialerHarness dialer = await DialerHarness.StartAsync(_backend);
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["dialer_connected"]!.GetValue());
using HttpResponseMessage accepted = await _backend.Admin.PostAsync($"/api/admin/lines/{agent.Id}~{dialer.DialerId}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "hy2_ladder", ["params"] = new JsonObject { ["size"] = 1000, ["duration_s"] = 5 } }));
Assert.True(accepted.StatusCode == System.Net.HttpStatusCode.Accepted, await accepted.Content.ReadAsStringAsync());
string testId = JsonNode.Parse(await accepted.Content.ReadAsStringAsync())!["id"]!.GetValue();
JsonNode test = null!;
await BackendFixture.EventuallyAsync(
async () =>
{
test = await _backend.GetJsonAsync(_backend.Admin, "/api/public/tests/" + testId);
return test["status"]!.GetValue() is "succeeded" or "failed" or "aborted";
},
TimeSpan.FromSeconds(90));
Assert.True(test["status"]!.GetValue() == "succeeded", test.ToJsonString());
await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "quic_stream") >= 5, because: "the 100 Mbit/s phase (5 s of 1000 B packets, over 50000 records) is split into two fragments");
Assert.Equal(0, agent.App.Counters.FragmentsDiscarded);
}
[Fact]
public async Task RealitySession_WithTheRealDialer_RunsTcpBulkAndStoresBytesAndKernelRows()
{
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
await agent.OverrideLineTasksAsync();
await using DialerHarness dialer = await DialerHarness.StartAsync(_backend);
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["dialer_connected"]!.GetValue());
using HttpResponseMessage accepted = await _backend.Admin.PostAsync($"/api/admin/lines/{agent.Id}~{dialer.DialerId}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "reality_session" }));
Assert.True(accepted.StatusCode == System.Net.HttpStatusCode.Accepted, await accepted.Content.ReadAsStringAsync());
string testId = JsonNode.Parse(await accepted.Content.ReadAsStringAsync())!["id"]!.GetValue();
JsonNode test = null!;
await BackendFixture.EventuallyAsync(
async () =>
{
test = await _backend.GetJsonAsync(_backend.Admin, "/api/public/tests/" + testId);
return test["status"]!.GetValue() is "succeeded" or "failed" or "aborted";
},
TimeSpan.FromSeconds(150));
Assert.True(test["status"]!.GetValue() == "succeeded", test.ToJsonString());
await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "tcp_transfer") >= 1, because: "the bulk phase stores its fragments");
Assert.Equal(0, agent.App.Counters.FragmentsDiscarded);
}
}