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.
127 lines
4.6 KiB
C#
127 lines
4.6 KiB
C#
using Fleet.Core;
|
|
using Fleet.Hosting;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Agent.Tests;
|
|
|
|
public sealed class DialDirectoryTests
|
|
{
|
|
[Fact]
|
|
public void DialDirectory_DualStack_PersistsBothPaths_AndRemovingIpv6KeepsIpv4()
|
|
{
|
|
string directory = NewDirectory();
|
|
try
|
|
{
|
|
var agents = new DialDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance);
|
|
DialTargets list = List(1, "agt_dual");
|
|
list.Targets[0].Ipv6Host = "2001:db8::1";
|
|
agents.Apply(list);
|
|
Assert.Equal(new[] { "ipv4", "ipv6" }, agents.Targets.Select(a => a.IpVersion));
|
|
var reloaded = new DialDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance);
|
|
Assert.Equal(2, reloaded.Targets.Count);
|
|
Assert.Equal("2001:db8::1", reloaded.Targets[1].Host);
|
|
list.Version = 2;
|
|
list.Targets[0].Ipv6Host = null;
|
|
reloaded.Apply(list);
|
|
Assert.Equal("ipv4", Assert.Single(reloaded.Targets).IpVersion);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ClockEstimator_UsesTheSampleWithTheShortestRoundTrip()
|
|
{
|
|
var clock = new ClockEstimator();
|
|
Assert.False(clock.HasEstimate);
|
|
|
|
// Offset 1000 with a 50 ms round trip, offset 5000 with a 2 ms round trip (symmetry assumption is far better here)
|
|
clock.Add(sendUs: 0, listenerUs: 25_000 + 1000, receiveUs: 50_000);
|
|
clock.Add(sendUs: 100_000, listenerUs: 101_000 + 5000, receiveUs: 102_000);
|
|
Assert.True(clock.HasEstimate);
|
|
Assert.Equal(5000, clock.OffsetUs);
|
|
|
|
// Slow samples do not displace the good one
|
|
clock.Add(sendUs: 200_000, listenerUs: 300_000, receiveUs: 400_000);
|
|
Assert.Equal(5000, clock.OffsetUs);
|
|
}
|
|
|
|
private static string NewDirectory()
|
|
{
|
|
string path = Path.Combine(Path.GetTempPath(), "dial-tests-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
return path;
|
|
}
|
|
|
|
private static DialTargets List(long version, params string[] ids) => new()
|
|
{
|
|
Version = version,
|
|
Targets = [.. ids.Select(id => new DialTarget { NodeId = new NodeId(id), Host = id + ".example", TcpPort = 7443, PublicKey = "c3BraQ==" })],
|
|
};
|
|
|
|
[Fact]
|
|
public void DialDirectory_StartsEmpty_IgnoresStaleLists_PersistsChanges_AndReloadsThem()
|
|
{
|
|
string directory = NewDirectory();
|
|
try
|
|
{
|
|
var directoryA = new DialDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance);
|
|
int changes = 0;
|
|
directoryA.Changed += () => changes++;
|
|
Assert.Empty(directoryA.Targets);
|
|
|
|
directoryA.Apply(List(5, "agt_1", "agt_2"));
|
|
Assert.Equal(1, changes);
|
|
Assert.Equal(["agt_1", "agt_2"], directoryA.Targets.Select(a => a.NodeId));
|
|
|
|
directoryA.Apply(List(4, "agt_old")); // out-of-order older push
|
|
directoryA.Apply(List(5, "agt_1", "agt_2")); // same version and content
|
|
Assert.Equal(1, changes);
|
|
Assert.Equal(5, directoryA.Version);
|
|
|
|
// The next start uses the saved list while the panel is not reachable yet
|
|
var directoryB = new DialDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance);
|
|
Assert.Equal(["agt_1", "agt_2"], directoryB.Targets.Select(a => a.NodeId));
|
|
Assert.Equal(5, directoryB.Version);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DialDirectory_StartsEmpty_WhenTheSavedFileIsCorrupt()
|
|
{
|
|
string directory = NewDirectory();
|
|
try
|
|
{
|
|
new ConfigStore(directory).Write(DialDirectory.FileName, "{ not json"u8.ToArray());
|
|
var loaded = new DialDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance);
|
|
Assert.Empty(loaded.Targets);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfigStore_CreatesSubdirectories_ForFreshVolumes()
|
|
{
|
|
// 新建的命名卷里没有 update/:面板转来的更新清单要能写进去
|
|
string directory = NewDirectory();
|
|
try
|
|
{
|
|
new ConfigStore(directory).Write("update/offer.manifest", "{}"u8.ToArray());
|
|
Assert.True(File.Exists(Path.Combine(directory, "update", "offer.manifest")));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, recursive: true);
|
|
}
|
|
}
|
|
}
|