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); } } }