using System.Text.Json; using Google.Protobuf; using Google.Protobuf.Reflection; using ZB.MOM.WW.MxGateway.Contracts; using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Tests.Contracts; public sealed class ClientProtoInputTests { /// /// Guards the published client descriptor set against silent staleness (IPC-01). Every message /// and field compiled into the in-process contract (which the build regenerates from the current /// .proto sources) must appear in the committed protoset. A missing symbol means the /// descriptor was not regenerated after a proto change; run /// scripts/publish-client-proto-inputs.ps1 and commit the refreshed protoset. /// The check is semantic (symbol presence) rather than byte-wise, so it is independent of protoc /// version and does not require protoc on the test runner. /// [Fact] public void Descriptor_ContainsEveryContractMessageAndField() { DirectoryInfo repositoryRoot = FindRepositoryRoot(); string descriptorPath = Path.Combine( repositoryRoot.FullName, "clients", "proto", "descriptors", "mxaccessgw-client-v1.protoset"); Assert.True(File.Exists(descriptorPath), $"Expected descriptor set '{descriptorPath}' to exist."); FileDescriptorSet descriptorSet = FileDescriptorSet.Parser.ParseFrom(File.ReadAllBytes(descriptorPath)); HashSet publishedMessages = new(StringComparer.Ordinal); HashSet publishedFields = new(StringComparer.Ordinal); foreach (FileDescriptorProto file in descriptorSet.File) { foreach (DescriptorProto message in file.MessageType) { CollectPublishedSymbols(file.Package, message, publishedMessages, publishedFields); } } List missing = []; foreach (FileDescriptor file in new[] { MxaccessGatewayReflection.Descriptor, MxaccessWorkerReflection.Descriptor }) { foreach (MessageDescriptor message in file.MessageTypes) { CollectMissingContractSymbols(message, publishedMessages, publishedFields, missing); } } Assert.True( missing.Count == 0, "Published client descriptor is stale; regenerate with scripts/publish-client-proto-inputs.ps1 and commit. " + "Missing symbols: " + string.Join(", ", missing)); } private static void CollectPublishedSymbols( string package, DescriptorProto message, HashSet messages, HashSet fields) { string fullName = string.IsNullOrEmpty(package) ? message.Name : package + "." + message.Name; messages.Add(fullName); foreach (FieldDescriptorProto field in message.Field) { fields.Add(fullName + "/" + field.Name); } foreach (DescriptorProto nested in message.NestedType) { CollectPublishedSymbols(fullName, nested, messages, fields); } } private static void CollectMissingContractSymbols( MessageDescriptor message, HashSet publishedMessages, HashSet publishedFields, List missing) { if (!publishedMessages.Contains(message.FullName)) { missing.Add(message.FullName); } foreach (FieldDescriptor field in message.Fields.InDeclarationOrder()) { string key = message.FullName + "/" + field.Name; if (!publishedFields.Contains(key)) { missing.Add(key); } } foreach (MessageDescriptor nested in message.NestedTypes) { CollectMissingContractSymbols(nested, publishedMessages, publishedFields, missing); } } /// Verifies that the proto inputs manifest declares current protocol versions and existing source files. [Fact] public void Manifest_DeclaresCurrentProtocolVersionsAndExistingInputs() { DirectoryInfo repositoryRoot = FindRepositoryRoot(); string manifestPath = Path.Combine(repositoryRoot.FullName, "clients", "proto", "proto-inputs.json"); using JsonDocument manifest = JsonDocument.Parse(File.ReadAllText(manifestPath)); JsonElement root = manifest.RootElement; Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32()); Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, root.GetProperty("gatewayProtocolVersion").GetUInt32()); Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, root.GetProperty("workerProtocolVersion").GetUInt32()); string protoRoot = Path.Combine(repositoryRoot.FullName, root.GetProperty("protoRoot").GetString()!); foreach (JsonElement sourceFile in root.GetProperty("sourceFiles").EnumerateArray()) { string sourcePath = Path.Combine(protoRoot, sourceFile.GetProperty("path").GetString()!); Assert.True(File.Exists(sourcePath), $"Expected proto source file '{sourcePath}' to exist."); } foreach (JsonProperty output in root.GetProperty("generatedOutputs").EnumerateObject()) { string outputPath = Path.Combine(repositoryRoot.FullName, output.Value.GetString()!); Assert.True(Directory.Exists(outputPath), $"Expected generated output directory '{outputPath}' to exist."); } } /// Verifies that the OpenSessionReply fixture parses with the current contract version. [Fact] public void OpenSessionReplyFixture_ParsesWithCurrentContract() { OpenSessionReply reply = ParseFixture( "open-session-reply.ok.json", OpenSessionReply.Parser); Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, reply.GatewayProtocolVersion); Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, reply.WorkerProtocolVersion); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); } /// Verifies that the RegisterCommand fixture parses with the current contract version. [Fact] public void RegisterCommandRequestFixture_ParsesWithCurrentContract() { MxCommandRequest request = ParseFixture( "register-command-request.json", MxCommandRequest.Parser); Assert.Equal(MxCommandKind.Register, request.Command.Kind); Assert.Equal("fixture-client", request.Command.Register.ClientName); } /// Verifies that the OnDataChange event fixture parses with the current contract version. [Fact] public void OnDataChangeEventFixture_ParsesWithCurrentContract() { MxEvent gatewayEvent = ParseFixture( "on-data-change-event.json", MxEvent.Parser); Assert.Equal(MxEventFamily.OnDataChange, gatewayEvent.Family); Assert.Equal(1ul, gatewayEvent.WorkerSequence); Assert.Equal(MxDataType.Integer, gatewayEvent.Value.DataType); Assert.Equal(MxEvent.BodyOneofCase.OnDataChange, gatewayEvent.BodyCase); } private static T ParseFixture( string fixtureName, MessageParser parser) where T : IMessage { DirectoryInfo repositoryRoot = FindRepositoryRoot(); string fixturePath = Path.Combine(repositoryRoot.FullName, "clients", "proto", "fixtures", "golden", fixtureName); return parser.ParseJson(File.ReadAllText(fixturePath)); } private static DirectoryInfo FindRepositoryRoot() { DirectoryInfo? current = new(AppContext.BaseDirectory); while (current is not null) { if (File.Exists(Path.Combine(current.FullName, "CLAUDE.md")) && Directory.Exists(Path.Combine(current.FullName, "src")) && Directory.Exists(Path.Combine(current.FullName, "clients"))) { return current; } current = current.Parent; } throw new DirectoryNotFoundException("Could not locate the repository root from the test output directory."); } }