using Fleet.Core; using Fleet.Hosting; using MoShiro.Protocol; namespace MoShiro.Probe.Tests; public sealed class ProbeTests { [Fact] public void AgentDirectory_DualStack_PersistsBothPaths_AndRemovingIpv6KeepsIpv4() { string directory = NewDirectory(); try { var agents = new AgentDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance); ProbeAgentList list = List(1, "agt_dual"); list.Agents[0].Ipv6Host = "2001:db8::1"; agents.Apply(list); Assert.Equal(new[] { "ipv4", "ipv6" }, agents.Agents.Select(a => a.IpVersion)); var reloaded = new AgentDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance); Assert.Equal(2, reloaded.Agents.Count); Assert.Equal("2001:db8::1", reloaded.Agents[1].Host); list.Version = 2; list.Agents[0].Ipv6Host = null; reloaded.Apply(list); Assert.Equal("ipv4", Assert.Single(reloaded.Agents).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, agentUs: 25_000 + 1000, receiveUs: 50_000); clock.Add(sendUs: 100_000, agentUs: 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, agentUs: 300_000, receiveUs: 400_000); Assert.Equal(5000, clock.OffsetUs); } private static string NewDirectory() { string path = Path.Combine(Path.GetTempPath(), "probe-tests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } private static ProbeAgentList List(long version, params string[] ids) => new() { Version = version, Agents = [.. ids.Select(id => new AgentEndpoint { AgentId = new NodeId(id), Host = id + ".example", TcpPort = 7443, PublicKey = "c3BraQ==" })], }; [Fact] public void AgentDirectory_StartsEmpty_IgnoresStaleLists_PersistsChanges_AndReloadsThem() { string directory = NewDirectory(); try { var directoryA = new AgentDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance); int changes = 0; directoryA.Changed += () => changes++; Assert.Empty(directoryA.Agents); directoryA.Apply(List(5, "agt_1", "agt_2")); Assert.Equal(1, changes); Assert.Equal(["agt_1", "agt_2"], directoryA.Agents.Select(a => a.AgentId)); 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 AgentDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance); Assert.Equal(["agt_1", "agt_2"], directoryB.Agents.Select(a => a.AgentId)); Assert.Equal(5, directoryB.Version); } finally { Directory.Delete(directory, recursive: true); } } [Fact] public void AgentDirectory_StartsEmpty_WhenTheSavedFileIsCorrupt() { string directory = NewDirectory(); try { new ConfigStore(directory).Write(AgentDirectory.FileName, "{ not json"u8.ToArray()); var loaded = new AgentDirectory(new ConfigStore(directory), directory, NullNodeLog.Instance); Assert.Empty(loaded.Agents); } 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); } } }