agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry. Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
This commit is contained in:
1 parent
d5ebf29e57
commit
ad5efc62ff
206 files changed
+4202
-4964
No files matched your search
@@ -1,3 +1,4 @@
|
||||
using Fleet.Core;
|
||||
using System.Text.Json.Nodes;
|
||||
using MoShiro.Measurement;
|
||||
using MoShiro.Protocol;
|
||||
@@ -5,7 +6,7 @@ using MoShiro.TestSupport;
|
||||
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>真实的后端 + 真实的 Agent(进程内)+ 假 Probe:覆盖 Agent 与 Backend 的链路、Probe 与 Agent 的点对点会话和数据面。</summary>
|
||||
/// <summary>真实的后端 + 真实的 Agent(进程内)+ 拨号侧模拟器:覆盖 Agent 与 Backend 的链路、Dialer 与 Agent 的点对点会话和数据面。</summary>
|
||||
// 使用真实 Agent/QUIC/TCP 会话的端到端验证放在慢测集合,避免和大量 API/单元测试争抢本机套接字。
|
||||
[Trait("Category", "Slow")]
|
||||
public sealed class AgentEndToEndTests : IClassFixture<BackendFixture>
|
||||
@@ -17,30 +18,17 @@ public sealed class AgentEndToEndTests : IClassFixture<BackendFixture>
|
||||
_backend = backend;
|
||||
}
|
||||
|
||||
private async Task<(AgentHarness Agent, ProbeSimulator Probe, LineTask Udp)> LineAsync()
|
||||
private async Task<(AgentHarness Agent, DialerSimulator Dialer, LineTask Udp)> LineAsync()
|
||||
{
|
||||
AgentHarness agent = await AgentHarness.StartAsync(_backend);
|
||||
await agent.OverrideLineTasksAsync(rateHz: 30);
|
||||
ProbeSimulator probe = await agent.ConnectProbeAsync();
|
||||
LineTaskSet set = await probe.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo" && t.Port == agent.App.QuicPorts[0]));
|
||||
return (agent, probe, set.Tasks.Single(t => t.Kind == "udp_echo"));
|
||||
DialerSimulator dialer = await agent.ConnectDialerAsync();
|
||||
LineTaskSet set = await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo" && t.Port == agent.App.QuicPorts[0]));
|
||||
return (agent, dialer, set.Tasks.Single(t => t.Kind == "udp_echo"));
|
||||
}
|
||||
|
||||
private async Task<JsonNode> AgentViewAsync(AgentHarness agent) => await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + agent.Id);
|
||||
|
||||
[Fact]
|
||||
public async Task Agent_ConnectsAndReportsItsRealPorts_AndTheProbeSessionIsVisibleToTheBackend()
|
||||
{
|
||||
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
|
||||
JsonNode view = await AgentViewAsync(agent);
|
||||
Assert.Equal("active", view["status"]!.GetValue<string>());
|
||||
Assert.False(view["probe_connected"]!.GetValue<bool>());
|
||||
|
||||
await using ProbeSimulator probe = await agent.ConnectProbeAsync();
|
||||
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue<bool>());
|
||||
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["runtime_status"]!.GetValue<string>() == "running");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Agent_OrderlyShutdownIsReportedAsStoppingRatherThanOffline()
|
||||
{
|
||||
@@ -57,11 +45,11 @@ public sealed class AgentEndToEndTests : IClassFixture<BackendFixture>
|
||||
[Fact]
|
||||
public async Task UdpEcho_EndToEnd_FragmentsAreMergedWithAgentTimestamps_AndStoredByTheBackend()
|
||||
{
|
||||
(AgentHarness agent, ProbeSimulator probe, LineTask udp) = await LineAsync();
|
||||
(AgentHarness agent, DialerSimulator dialer, LineTask udp) = await LineAsync();
|
||||
await using (agent)
|
||||
await using (probe)
|
||||
await using (dialer)
|
||||
{
|
||||
(int sent, int received) = await probe.RunUdpEchoAsync(udp, 40, TimeSpan.FromMilliseconds(40));
|
||||
(int sent, int received) = await dialer.RunUdpEchoAsync(udp, 40, TimeSpan.FromMilliseconds(40));
|
||||
Assert.Equal(40, sent);
|
||||
Assert.Equal(40, received);
|
||||
|
||||
@@ -84,12 +72,12 @@ public sealed class AgentEndToEndTests : IClassFixture<BackendFixture>
|
||||
[InlineData(true)]
|
||||
public async Task TcpEcho_EndToEnd_AuthenticatedFirstFrame_PlainOrTls(bool tls)
|
||||
{
|
||||
(AgentHarness agent, ProbeSimulator probe, _) = await LineAsync();
|
||||
(AgentHarness agent, DialerSimulator dialer, _) = await LineAsync();
|
||||
await using (agent)
|
||||
await using (probe)
|
||||
await using (dialer)
|
||||
{
|
||||
LineTask tcp = (await probe.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "tcp_echo" && t.Port == agent.App.TcpPorts[0]))).Tasks.Single(t => t.Kind == "tcp_echo");
|
||||
(int sent, int received) = await probe.RunTcpEchoAsync(tcp, agent.App.TcpPorts[0], 15, TimeSpan.FromMilliseconds(60), tls);
|
||||
LineTask tcp = (await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "tcp_echo" && t.Port == agent.App.TcpPorts[0]))).Tasks.Single(t => t.Kind == "tcp_echo");
|
||||
(int sent, int received) = await dialer.RunTcpEchoAsync(tcp, agent.App.TcpPorts[0], 15, TimeSpan.FromMilliseconds(60), tls);
|
||||
Assert.Equal(15, sent);
|
||||
Assert.Equal(15, received);
|
||||
await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/raw/chunks?agent_id={agent.Id}"))["items"]!.AsArray().Any(c => c!["kind"]!.GetValue<string>() == "tcp_echo"));
|
||||
@@ -97,9 +85,9 @@ public sealed class AgentEndToEndTests : IClassFixture<BackendFixture>
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ARestartedProbe_NeverInheritsTheAgentRecordsOfItsPreviousSession()
|
||||
public async Task ARestartedDialer_NeverInheritsTheAgentRecordsOfItsPreviousSession()
|
||||
{
|
||||
(AgentHarness agent, ProbeSimulator first, LineTask udp) = await LineAsync();
|
||||
(AgentHarness agent, DialerSimulator first, LineTask udp) = await LineAsync();
|
||||
await using (agent)
|
||||
{
|
||||
await first.RunUdpEchoAsync(udp, 20, TimeSpan.FromMilliseconds(40));
|
||||
@@ -107,8 +95,8 @@ public sealed class AgentEndToEndTests : IClassFixture<BackendFixture>
|
||||
Assert.True(records.Contains(5), "the agent recorded t2/t3 for the first session's packets");
|
||||
await first.DisposeAsync();
|
||||
|
||||
// The probe process restarts: a new session with its own streams; sequence numbers start from 0 again
|
||||
await using ProbeSimulator second = first.Restarted();
|
||||
// The dialer process restarts: a new session with its own streams; sequence numbers start from 0 again
|
||||
await using DialerSimulator second = first.Restarted();
|
||||
await second.ConnectAsync("127.0.0.1", agent.App.ControlPort, agent.TlsPublicKey, agent.App.QuicPorts[0], agent.NodeId);
|
||||
LineTaskSet set = await second.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo" && t.Port == agent.App.QuicPorts[0]));
|
||||
LineTask udp2 = set.Tasks.Single(t => t.Kind == "udp_echo");
|
||||
@@ -125,26 +113,26 @@ public sealed class AgentEndToEndTests : IClassFixture<BackendFixture>
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SeveralProbes_AreReportedTogether_CanBeDroppedOneByOne_AndARevokedOneIsKickedOut()
|
||||
public async Task SeveralDialers_AreReportedTogether_CanBeDroppedOneByOne_AndADisabledOneIsKickedOut()
|
||||
{
|
||||
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
|
||||
await agent.OverrideLineTasksAsync(rateHz: 30);
|
||||
await using ProbeSimulator a = await agent.ConnectProbeAsync();
|
||||
await using ProbeSimulator b = await agent.ConnectProbeAsync();
|
||||
await using DialerSimulator a = await agent.ConnectDialerAsync();
|
||||
await using DialerSimulator b = await agent.ConnectDialerAsync();
|
||||
|
||||
// The agent reports both sessions with version and address; the probe view shows the agent it is connected to
|
||||
// The agent reports both sessions with version and address; the dialer view shows the agent it is connected to
|
||||
JsonNode view = null!;
|
||||
await BackendFixture.EventuallyAsync(
|
||||
async () =>
|
||||
{
|
||||
view = await AgentViewAsync(agent);
|
||||
return view["probes"]!.AsArray().Count == 2;
|
||||
return view["dialers"]!.AsArray().Count == 2;
|
||||
},
|
||||
because: "the agent reports every connected probe");
|
||||
Assert.All(view["probes"]!.AsArray(), p => Assert.Equal("0.0.0-sim", p!["version"]!.GetValue<string>()));
|
||||
Assert.Equal("127.0.0.1", view["probes"]![0]!["remote"]!.GetValue<string>());
|
||||
JsonNode probeView = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/probes/" + a.ProbeId);
|
||||
Assert.Equal(agent.Id, probeView["connections"]![0]!["agent_id"]!.GetValue<string>());
|
||||
because: "the agent reports every connected dialer");
|
||||
Assert.All(view["dialers"]!.AsArray(), p => Assert.Equal("0.0.0-sim", p!["version"]!.GetValue<string>()));
|
||||
Assert.Equal("127.0.0.1", view["dialers"]![0]!["remote"]!.GetValue<string>());
|
||||
JsonNode dialerView = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + a.DialerId);
|
||||
Assert.Equal(agent.Id, dialerView["outgoing"]![0]!["listener_id"]!.GetValue<string>());
|
||||
|
||||
// Both get their own task set (own streams)
|
||||
LineTaskSet setA = await a.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo"));
|
||||
@@ -152,56 +140,44 @@ public sealed class AgentEndToEndTests : IClassFixture<BackendFixture>
|
||||
Assert.NotEqual(setA.Tasks.Single(t => t.Kind == "udp_echo").StreamId, setB.Tasks.Single(t => t.Kind == "udp_echo").StreamId);
|
||||
|
||||
// Drop one: the other stays
|
||||
using HttpResponseMessage dropped = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.Id}/probes/{a.ProbeId}/disconnect", null);
|
||||
using HttpResponseMessage dropped = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.Id}/dialers/{a.DialerId}/disconnect", null);
|
||||
Assert.Equal(System.Net.HttpStatusCode.NoContent, dropped.StatusCode);
|
||||
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probes"]!.AsArray().Count == 1, because: "only the dropped probe leaves");
|
||||
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["dialers"]!.AsArray().Count == 1, because: "only the dropped dialer leaves");
|
||||
Assert.True(b.IsConnected);
|
||||
using HttpResponseMessage again = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.Id}/probes/{a.ProbeId}/disconnect", null);
|
||||
using HttpResponseMessage again = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.Id}/dialers/{a.DialerId}/disconnect", null);
|
||||
Assert.Equal(System.Net.HttpStatusCode.Conflict, again.StatusCode);
|
||||
|
||||
// Revoke the remaining one: the agent closes its session
|
||||
using HttpResponseMessage revoked = await _backend.Admin.PostAsync($"/api/admin/probes/{b.ProbeId}/revoke", null);
|
||||
// Disable the remaining Agent: the listener closes its session
|
||||
using HttpResponseMessage revoked = await _backend.Admin.PostAsync($"/api/admin/agents/{b.DialerId}/disable", null);
|
||||
revoked.EnsureSuccessStatusCode();
|
||||
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probes"]!.AsArray().Count == 0, because: "a revoked probe is disconnected");
|
||||
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["dialers"]!.AsArray().Count == 0, because: "a disabled Agent is disconnected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PeerHandshake_RejectsAProbeWhoseKeyTheBackendNeverIssued()
|
||||
public async Task PeerHandshake_RejectsADialerWhoseKeyTheBackendNeverIssued()
|
||||
{
|
||||
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
|
||||
await agent.CreateProbeAsync();
|
||||
await using var stranger = new ProbeSimulator("prb_stranger", Fleet.Identity.NodeIdentity.Generate());
|
||||
await agent.CreateDialerAsync();
|
||||
await using var stranger = new DialerSimulator("agt_stranger", Fleet.Identity.NodeIdentity.Generate());
|
||||
await Assert.ThrowsAnyAsync<Exception>(() => stranger.ConnectAsync("127.0.0.1", agent.App.ControlPort, agent.TlsPublicKey, agent.App.QuicPorts[0], agent.NodeId));
|
||||
|
||||
// 证书公钥不对(中间人)也不会连上
|
||||
await using ProbeSimulator probe = await agent.CreateProbeAsync();
|
||||
await Assert.ThrowsAnyAsync<Exception>(() => probe.ConnectAsync("127.0.0.1", agent.App.ControlPort, Fleet.Identity.NodeIdentity.Generate().PublicKeySpki, agent.App.QuicPorts[0], agent.NodeId));
|
||||
await using DialerSimulator dialer = await agent.CreateDialerAsync();
|
||||
await Assert.ThrowsAnyAsync<Exception>(() => dialer.ConnectAsync("127.0.0.1", agent.App.ControlPort, Fleet.Identity.NodeIdentity.Generate().PublicKeySpki, agent.App.QuicPorts[0], agent.NodeId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeDisconnect_IsReportedToTheBackend()
|
||||
{
|
||||
(AgentHarness agent, ProbeSimulator probe, _) = await LineAsync();
|
||||
await using (agent)
|
||||
{
|
||||
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue<bool>());
|
||||
await probe.DisposeAsync();
|
||||
await BackendFixture.EventuallyAsync(async () => !(await AgentViewAsync(agent))["probe_connected"]!.GetValue<bool>());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_IsRefusedWhenNoProbeIsConnected_AndWhenAPhaseExceedsTheLocalLimits()
|
||||
public async Task Test_IsRefusedWhenNoDialerIsConnected_AndWhenAPhaseExceedsTheLocalLimits()
|
||||
{
|
||||
await using AgentHarness agent = await AgentHarness.StartAsync(_backend, config => config.Limits.MaxTotalMbps = 20);
|
||||
await agent.OverrideLineTasksAsync();
|
||||
|
||||
var phase = new TestPhase { Kind = "udp_train", DurationSeconds = 1, Params = { ["port"] = System.Text.Json.JsonSerializer.SerializeToElement(agent.App.QuicPorts[0]) } };
|
||||
var run = new TestRunParams { TestId = new TestId("tst_x"), ProbeId = new ProbeId("prb_nobody"), Plan = "x", Phases = [phase] };
|
||||
Assert.Equal("probe_offline", agent.App.Tests.Start(run).Error.Code.Value);
|
||||
var run = new TestRunParams { TestId = new TestId("tst_x"), DialerId = new NodeId("agt_nobody"), Plan = "x", Phases = [phase] };
|
||||
Assert.Equal("dialer_offline", agent.App.Tests.Start(run).Error.Code.Value);
|
||||
|
||||
await using ProbeSimulator probe = await agent.ConnectProbeAsync();
|
||||
run.ProbeId = new ProbeId(probe.ProbeId);
|
||||
await using DialerSimulator dialer = await agent.ConnectDialerAsync();
|
||||
run.DialerId = new NodeId(dialer.DialerId);
|
||||
phase.Params["mbps"] = System.Text.Json.JsonSerializer.SerializeToElement(50);
|
||||
Assert.Equal("budget_exceeded", agent.App.Tests.Start(run).Error.Code.Value);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user