using System.Text.Json.Nodes; namespace MoShiro.Backend.Tests; /// 真实的后端 + 真实的 Agent + 真实的 Probe(都在进程内,回环网络)。 [Collection("probe-e2e")] public sealed class ProbeEndToEndTests { private readonly BackendFixture _backend; public ProbeEndToEndTests(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 Probe_GetsTheAgentListFromThePanel_RunsTheLineTasks_AndTheBackendStoresMergedChunks() { await using AgentHarness agent = await AgentHarness.StartAsync(_backend); await agent.OverrideLineTasksAsync(rateHz: 30); await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend); await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.IsConnected(agent.Id)), because: "the panel sends the agent list to the probe, which then connects"); await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue()); await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.Connection(agent.Id)!.RoleCount == 2), because: "udp_echo and tcp_echo"); // 每 10 秒切一块,回显类再等 2 秒宽限 await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "udp_echo") >= 1 && await ChunkCountAsync(agent, "tcp_echo") >= 1, TimeSpan.FromSeconds(40)); Assert.True(agent.App.Counters.Fragments >= 2); Assert.Equal(0, agent.App.Counters.FragmentsDiscarded); // 时钟偏移已经估计出来:进程内两端共用同一个单调时钟,所以偏移应当接近 0(真实线路上是两台机器各自的时钟原点之差) Assert.InRange(probe.App.Connection(agent.Id)!.ClockOffsetUs, -20_000, 20_000); } [Fact] public async Task Probe_IntroducesItselfToThePanel_AndARevokedProbeIsLockedOut() { await _backend.SiteMustBePublicAsync(); await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend); string path = "/api/admin/probes/" + probe.ProbeId; // 直连面板:在线,hello 里的系统、架构和版本都被记下来了 await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, path))["online"]!.GetValue(), because: "the probe links to the panel"); await BackendFixture.EventuallyAsync( async () => (await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/nodes"))["items"]!.AsArray().Any(n => n!["id"]!.GetValue() == probe.ProbeId && n["os"] is not null && n["arch"] is not null), because: "the platform comes from the probe's own hello"); // 吊销:面板立刻断开它,之后连不上 using HttpResponseMessage revoked = await _backend.Admin.PostAsync(path + "/revoke", null); revoked.EnsureSuccessStatusCode(); await BackendFixture.EventuallyAsync(async () => !(await _backend.GetJsonAsync(_backend.Admin, path))["online"]!.GetValue() && !probe.App.Link.IsConnected, because: "a revoked probe is disconnected and cannot come back"); } [Fact] public async Task Probe_FollowsTheAgentListThePanelKnows_AndSavesItForTheNextStart() { await using AgentHarness first = await AgentHarness.StartAsync(_backend); await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend); await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.IsConnected(first.Id))); await using AgentHarness second = await AgentHarness.StartAsync(_backend); await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.IsConnected(second.Id)), TimeSpan.FromSeconds(30), "a new agent should be picked up through agents_changed: " + string.Join(";", probe.App.Directory.Agents.Select(a => $"{a.AgentId}@{a.Host}:{a.TcpPort}")) + " v" + probe.App.Directory.Version + " want " + second.Id); Assert.Contains(probe.App.Directory.Agents, a => a.AgentId == second.Id); Assert.True(File.Exists(Path.Combine(probe.Directory, "agents.json"))); // 停用一个 Agent:Probe 断开它 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(!probe.App.IsConnected(second.Id)), TimeSpan.FromSeconds(30)); } [Fact] public async Task Hy2Ladder_WithTheRealProbe_Succeeds_AndEveryFragmentIsStored() { await using AgentHarness agent = await AgentHarness.StartAsync(_backend); await agent.OverrideLineTasksAsync(); await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend); await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue()); using HttpResponseMessage accepted = await _backend.Admin.PostAsync($"/api/admin/lines/{agent.Id}~{probe.ProbeId}/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, "udp_train") >= 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_WithTheRealProbe_RunsTcpBulkAndStoresBytesAndKernelRows() { await using AgentHarness agent = await AgentHarness.StartAsync(_backend); await agent.OverrideLineTasksAsync(); await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend); await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue()); using HttpResponseMessage accepted = await _backend.Admin.PostAsync($"/api/admin/lines/{agent.Id}~{probe.ProbeId}/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_bulk") >= 1, because: "the bulk phase stores its fragments"); Assert.Equal(0, agent.App.Counters.FragmentsDiscarded); } }