Files
MoShiro/backend/tests/MoShiro.Backend.Tests/ProtocolDeclarationTests.cs
T
justme e014a19554
ci/woodpecker/push/main-amd64-build Pipeline was successful
docs: reorganize into design, security, api, ops and dev; drop stale designs; refresh the diagrams [CI]
- Group docs by category with an index (docs/README.md); fix every link,
  code comment and the protocol conformance test that read docs/protocol.md.
- access.md and security.md rewritten to what is implemented: removed the
  unadopted member-proof chain, roles and recovery key that do not exist,
  the old implementation order and the budgets; gaps now carry a status.
- enrollment, deployment, api: the install flow is the static, hash-checked
  script with environment variables; no default admin.
- README and architecture: new topology and update-flow diagrams (operator
  passkey, CI signing, registry feed, auth and trust modules).
2026-10-08 19:37:56 +08:00

95 lines
4.0 KiB
C#

using System.Reflection;
using Fleet.Rpc;
using Fleet.Server;
using Microsoft.Extensions.DependencyInjection;
using MoShiro.Probe;
using MoShiro.Protocol;
namespace MoShiro.Backend.Tests;
/// <summary>
/// 协议方向声明(<see cref="MoShiroMethodTable"/>)和实际代码的对账:每个方法常量都声明了,每一侧该处理的方法都注册了处理函数,
/// 二进制通道都有对应的收件箱,文档里写了每一个方法。新增方法绕不过声明,声明了没人处理也会被发现。
/// 注册处理函数时路由器本身还会核对方向、类型和角色,不符就在启动时失败。
/// </summary>
[Collection("probe-e2e")]
public sealed class ProtocolDeclarationTests
{
private static readonly MethodTable s_table = MoShiroMethodTable.Table;
private readonly BackendFixture _backend;
public ProtocolDeclarationTests(BackendFixture backend)
{
_backend = backend;
}
private static IEnumerable<string> ConstantsOf(Type type) =>
type.GetFields(BindingFlags.Public | BindingFlags.Static).Where(f => f.IsLiteral && f.FieldType == typeof(string)).Select(f => (string)f.GetRawConstantValue()!);
[Fact]
public void EveryMethodConstant_IsDeclared()
{
HashSet<string> declared = [.. s_table.Methods.Select(m => m.Name)];
string[] undeclared = [.. ConstantsOf(typeof(FleetMethods)).Concat(ConstantsOf(typeof(PeerMethods))).Concat(ConstantsOf(typeof(MoShiroMethods))).Where(n => !declared.Contains(n))];
Assert.Empty(undeclared);
}
[Fact]
public void EveryDeclaredMethod_IsDocumented()
{
string root = FindRoot();
string docs = File.ReadAllText(Path.Combine(root, "docs", "design", "protocol.md")) + File.ReadAllText(Path.Combine(root, "backend", "fleet", "README.md"));
string[] missing = [.. s_table.Methods.Select(m => m.Name).Distinct().Where(n => !docs.Contains($"`{n}`", StringComparison.Ordinal))];
Assert.Empty(missing);
}
[Fact]
public void Backend_HandlesEveryMethodNodesMayInitiate_AndHasAnInboxForEveryDeclaredChannel()
{
FleetRouter router = _backend.Services.GetRequiredService<FleetRouter>();
Assert.Empty(router.Table.MissingHandlers(RpcSide.Server, null, router.Methods));
byte[] inboxes = [.. _backend.Services.GetServices<IInbox>().Select(i => i.Channel).Order()];
byte[] declared = [.. s_table.Channels.Where(c => c.Plane == RpcPlane.Node).Select(c => c.Channel).Order()];
Assert.Equal(declared, inboxes);
}
[Fact]
public async Task Agent_HandlesEveryMethodItShould_OnBothPlanes()
{
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
Assert.Empty(s_table.MissingHandlers(RpcSide.Node, NodeRoles.Agent, agent.App.Link.Router.Methods));
// 对等面:Agent 是监听方,要处理 Probe 发来的方法
Assert.Empty(s_table.MissingHandlers(RpcSide.Listener, null, agent.App.PeerRouter.Methods));
}
[Fact]
public async Task Probe_HandlesEveryMethodItShould_OnBothPlanes()
{
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
Assert.Empty(s_table.MissingHandlers(RpcSide.Node, NodeRoles.Probe, probe.App.Link.Router.Methods));
// 对等面:Probe 是拨号方,要处理 Agent 发来的方法
Assert.Empty(s_table.MissingHandlers(RpcSide.Dialer, null, AgentConnection.Router.Methods));
}
[Fact]
public void RolesOfNodeIds_FollowThePrefix()
{
Assert.Equal(NodeRoles.Agent, NodeRoles.Of(AgentIds.Prefix + "_01ABC"));
Assert.Equal(NodeRoles.Probe, NodeRoles.Of(ProbeId.Prefix + "_01ABC"));
Assert.Null(NodeRoles.Of("usr_01ABC"));
}
private static string FindRoot()
{
string? dir = AppContext.BaseDirectory;
while (dir is not null && !File.Exists(Path.Combine(dir, "docs", "design", "protocol.md")))
{
dir = Path.GetDirectoryName(dir);
}
return dir ?? throw new InvalidOperationException("repository root not found");
}
}