Merge branch 'fix/ipc-27-descriptor-test'
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m14s
ci / java (push) Successful in 2m8s
ci / portable (push) Failing after 4m45s

This commit is contained in:
Joseph Doherty
2026-08-07 07:49:33 -04:00
5 changed files with 119 additions and 22 deletions
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ All `path:line` citations were re-verified against the working tree at `4f5371f`
| IPC-24 | Medium | P0 | S | — | Done | CI's unconditional Java churn-revert masks real generated-code drift for message-level proto changes | | IPC-24 | Medium | P0 | S | — | Done | CI's unconditional Java churn-revert masks real generated-code drift for message-level proto changes |
| IPC-25 | Medium | P0 | M | — | Done | Committed Go/Python worker bindings are stale at HEAD; no guard covers them | | IPC-25 | Medium | P0 | M | — | Done | Committed Go/Python worker bindings are stale at HEAD; no guard covers them |
| IPC-26 | Low | P2 | S¹ | WRK-22 | Not started | Cancelled write leaves a ghost frame that is still written (contract requirement here; fix mechanics in WRK-22) | | IPC-26 | Low | P2 | S¹ | WRK-22 | Not started | Cancelled write leaves a ghost frame that is still written (contract requirement here; fix mechanics in WRK-22) |
| IPC-27 | Low | P2 | S | — | Not started | Descriptor freshness test blind to enums, enum values, services/methods, and the Galaxy contract | | IPC-27 | Low | P2 | S | — | Done | Descriptor freshness test blind to enums, enum values, services/methods, and the Galaxy contract |
| IPC-28 | Low | — | S | — | Done | `docs/Grpc.md` omits the `CommandTooLarge``ResourceExhausted` mapping | | IPC-28 | Low | — | S | — | Done | `docs/Grpc.md` omits the `CommandTooLarge``ResourceExhausted` mapping |
| IPC-29 | Low | — | S | — | Done (discharged by WRK-26) | Worker writer priority scheduling and write-time sequence stamping undocumented in the frame-protocol doc | | IPC-29 | Low | — | S | — | Done (discharged by WRK-26) | Worker writer priority scheduling and write-time sequence stamping undocumented in the frame-protocol doc |
| IPC-30 | Low | P0 | M | WRK-21 (same file/batch) | Done | Oversized worker→gateway event frame is session-fatal — make the death deliberate, structured, and diagnosable | | IPC-30 | Low | P0 | M | WRK-21 (same file/batch) | Done | Oversized worker→gateway event frame is session-fatal — make the death deliberate, structured, and diagnosable |
+6 -4
View File
@@ -74,10 +74,12 @@ protoc-version encoding drift and does not false-fail across protoc releases
(it warns, rather than fails, when protoc is off the pin). (it warns, rather than fails, when protoc is off the pin).
The gateway test project carries an independent, protoc-free freshness guard: The gateway test project carries an independent, protoc-free freshness guard:
`ClientProtoInputTests.Descriptor_ContainsEveryContractMessageAndField` reflects `ClientProtoInputTests.Descriptor_ContainsEveryContractSymbol` reflects over the
over the in-process contract descriptors and fails if any contract message or in-process contract descriptors `mxaccess_gateway.proto`, `mxaccess_worker.proto`,
field is missing from the committed protoset. This is the primary CI gate for and `galaxy_repository.proto` — and fails if any contract message, field, enum,
descriptor staleness; a red test means "regenerate and commit the protoset." enum value, service, or method is missing from the committed protoset. This is
the primary CI gate for descriptor staleness; a red test means "regenerate and
commit the protoset."
### Pinned generator versions ### Pinned generator versions
+5 -4
View File
@@ -156,10 +156,11 @@ pwsh -File scripts/publish-client-proto-inputs.ps1
Freshness is guarded two ways so a skipped regeneration cannot ship silently: Freshness is guarded two ways so a skipped regeneration cannot ship silently:
- `ClientProtoInputTests.Descriptor_ContainsEveryContractMessageAndField` (gateway test project) - `ClientProtoInputTests.Descriptor_ContainsEveryContractSymbol` (gateway test project)
reflects over the in-process contract descriptors and fails if any message or field is missing reflects over the in-process contract descriptors — including `galaxy_repository.proto` — and
from the committed protoset. It is semantic (symbol presence), needs no protoc, and runs in the fails if any message, field, enum, enum value, service, or method is missing from the committed
Linux CI. A red test means "regenerate and commit the protoset." protoset. It is semantic (symbol presence), needs no protoc, and runs in the Linux CI. A red
test means "regenerate and commit the protoset."
- `pwsh -File scripts/publish-client-proto-inputs.ps1 -Check` rebuilds the descriptor and compares - `pwsh -File scripts/publish-client-proto-inputs.ps1 -Check` rebuilds the descriptor and compares
it to the committed one. The comparison normalizes both sides through the same protoc with it to the committed one. The comparison normalizes both sides through the same protoc with
`source_code_info` stripped, so it does not false-fail across protoc releases. `source_code_info` stripped, so it does not false-fail across protoc releases.
@@ -3,22 +3,24 @@ using Google.Protobuf;
using Google.Protobuf.Reflection; using Google.Protobuf.Reflection;
using ZB.MOM.WW.MxGateway.Contracts; using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
namespace ZB.MOM.WW.MxGateway.Tests.Contracts; namespace ZB.MOM.WW.MxGateway.Tests.Contracts;
public sealed class ClientProtoInputTests public sealed class ClientProtoInputTests
{ {
/// <summary> /// <summary>
/// Guards the published client descriptor set against silent staleness. Every message /// Guards the published client descriptor set against silent staleness. Every message,
/// and field compiled into the in-process contract (which the build regenerates from the current /// field, enum, enum value, service, and method compiled into the in-process contract
/// <c>.proto</c> sources) must appear in the committed protoset. A missing symbol means the /// (which the build regenerates from the current <c>.proto</c> sources) must appear in the
/// descriptor was not regenerated after a proto change; run /// committed protoset. A missing symbol means the descriptor was not regenerated after a
/// <c>scripts/publish-client-proto-inputs.ps1</c> and commit the refreshed protoset. /// proto change; run <c>scripts/publish-client-proto-inputs.ps1</c> and commit the
/// refreshed protoset.
/// The check is semantic (symbol presence) rather than byte-wise, so it is independent of protoc /// 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. /// version and does not require protoc on the test runner.
/// </summary> /// </summary>
[Fact] [Fact]
public void Descriptor_ContainsEveryContractMessageAndField() public void Descriptor_ContainsEveryContractSymbol()
{ {
DirectoryInfo repositoryRoot = FindRepositoryRoot(); DirectoryInfo repositoryRoot = FindRepositoryRoot();
string descriptorPath = Path.Combine( string descriptorPath = Path.Combine(
@@ -34,20 +36,65 @@ public sealed class ClientProtoInputTests
HashSet<string> publishedMessages = new(StringComparer.Ordinal); HashSet<string> publishedMessages = new(StringComparer.Ordinal);
HashSet<string> publishedFields = new(StringComparer.Ordinal); HashSet<string> publishedFields = new(StringComparer.Ordinal);
HashSet<string> publishedEnums = new(StringComparer.Ordinal);
HashSet<string> publishedServices = new(StringComparer.Ordinal);
foreach (FileDescriptorProto file in descriptorSet.File) foreach (FileDescriptorProto file in descriptorSet.File)
{ {
foreach (DescriptorProto message in file.MessageType) foreach (DescriptorProto message in file.MessageType)
{ {
CollectPublishedSymbols(file.Package, message, publishedMessages, publishedFields); CollectPublishedSymbols(file.Package, message, publishedMessages, publishedFields, publishedEnums);
}
foreach (EnumDescriptorProto enumType in file.EnumType)
{
CollectPublishedEnumSymbols(file.Package, enumType, publishedEnums);
}
foreach (ServiceDescriptorProto service in file.Service)
{
string serviceFullName = string.IsNullOrEmpty(file.Package) ? service.Name : file.Package + "." + service.Name;
publishedServices.Add(serviceFullName);
foreach (MethodDescriptorProto method in service.Method)
{
publishedServices.Add(serviceFullName + "/" + method.Name);
}
} }
} }
List<string> missing = []; List<string> missing = [];
foreach (FileDescriptor file in new[] { MxaccessGatewayReflection.Descriptor, MxaccessWorkerReflection.Descriptor }) FileDescriptor[] contractFiles =
[
MxaccessGatewayReflection.Descriptor,
MxaccessWorkerReflection.Descriptor,
GalaxyRepositoryReflection.Descriptor,
];
foreach (FileDescriptor file in contractFiles)
{ {
foreach (MessageDescriptor message in file.MessageTypes) foreach (MessageDescriptor message in file.MessageTypes)
{ {
CollectMissingContractSymbols(message, publishedMessages, publishedFields, missing); CollectMissingContractSymbols(message, publishedMessages, publishedFields, publishedEnums, missing);
}
foreach (EnumDescriptor enumType in file.EnumTypes)
{
CollectMissingEnumSymbols(enumType, publishedEnums, missing);
}
foreach (ServiceDescriptor service in file.Services)
{
if (!publishedServices.Contains(service.FullName))
{
missing.Add(service.FullName);
}
foreach (MethodDescriptor method in service.Methods)
{
string key = service.FullName + "/" + method.Name;
if (!publishedServices.Contains(key))
{
missing.Add(key);
}
}
} }
} }
@@ -62,7 +109,8 @@ public sealed class ClientProtoInputTests
string package, string package,
DescriptorProto message, DescriptorProto message,
HashSet<string> messages, HashSet<string> messages,
HashSet<string> fields) HashSet<string> fields,
HashSet<string> enums)
{ {
string fullName = string.IsNullOrEmpty(package) ? message.Name : package + "." + message.Name; string fullName = string.IsNullOrEmpty(package) ? message.Name : package + "." + message.Name;
messages.Add(fullName); messages.Add(fullName);
@@ -74,7 +122,26 @@ public sealed class ClientProtoInputTests
foreach (DescriptorProto nested in message.NestedType) foreach (DescriptorProto nested in message.NestedType)
{ {
CollectPublishedSymbols(fullName, nested, messages, fields); CollectPublishedSymbols(fullName, nested, messages, fields, enums);
}
foreach (EnumDescriptorProto enumType in message.EnumType)
{
CollectPublishedEnumSymbols(fullName, enumType, enums);
}
}
private static void CollectPublishedEnumSymbols(
string containingScope,
EnumDescriptorProto enumType,
HashSet<string> enums)
{
string enumFullName = string.IsNullOrEmpty(containingScope) ? enumType.Name : containingScope + "." + enumType.Name;
enums.Add(enumFullName);
foreach (EnumValueDescriptorProto value in enumType.Value)
{
enums.Add(enumFullName + "/" + value.Name);
} }
} }
@@ -82,6 +149,7 @@ public sealed class ClientProtoInputTests
MessageDescriptor message, MessageDescriptor message,
HashSet<string> publishedMessages, HashSet<string> publishedMessages,
HashSet<string> publishedFields, HashSet<string> publishedFields,
HashSet<string> publishedEnums,
List<string> missing) List<string> missing)
{ {
if (!publishedMessages.Contains(message.FullName)) if (!publishedMessages.Contains(message.FullName))
@@ -100,7 +168,32 @@ public sealed class ClientProtoInputTests
foreach (MessageDescriptor nested in message.NestedTypes) foreach (MessageDescriptor nested in message.NestedTypes)
{ {
CollectMissingContractSymbols(nested, publishedMessages, publishedFields, missing); CollectMissingContractSymbols(nested, publishedMessages, publishedFields, publishedEnums, missing);
}
foreach (EnumDescriptor enumType in message.EnumTypes)
{
CollectMissingEnumSymbols(enumType, publishedEnums, missing);
}
}
private static void CollectMissingEnumSymbols(
EnumDescriptor enumType,
HashSet<string> publishedEnums,
List<string> missing)
{
if (!publishedEnums.Contains(enumType.FullName))
{
missing.Add(enumType.FullName);
}
foreach (EnumValueDescriptor value in enumType.Values)
{
string key = enumType.FullName + "/" + value.Name;
if (!publishedEnums.Contains(key))
{
missing.Add(key);
}
} }
} }