diff --git a/archreview/2026-07-12/remediation/00-tracking.md b/archreview/2026-07-12/remediation/00-tracking.md index ce5f608..8bb43c8 100644 --- a/archreview/2026-07-12/remediation/00-tracking.md +++ b/archreview/2026-07-12/remediation/00-tracking.md @@ -88,7 +88,7 @@ Full design + implementation for each row lives in the linked domain doc under i | IPC-24 | Medium | P0 | S | — | Not started | CI's unconditional Java churn-revert masks real drift | | IPC-25 | Medium | P0 | M | — | Not started | Stale Go/Python worker bindings: regenerate (pinned toolchains) + check-codegen Check 4 | | IPC-26 | Low | P2 | S | WRK-22 (mechanics) | Not started | Cancelled write leaves ghost frame — cancelled means never written | -| IPC-27 | Low | P2 | S | — | Not started | Descriptor-freshness test blind to enums/services/galaxy descriptor | +| IPC-27 | Low | P2 | S | — | Done | Descriptor-freshness test blind to enums/services/galaxy descriptor | | IPC-28 | Low | — | S | — | Done | docs/Grpc.md missing CommandTooLarge → ResourceExhausted mapping | | IPC-29 | Low | — | S | WRK-26 (discharged by) | Done (discharged by WRK-26) | WorkerFrameProtocol.md missing write-scheduling/sequencing section | | IPC-30 | Low | P0 | M | WRK-21 (same batch) | Done | Oversized event frame: keep session-fatal, make the death structured | @@ -177,3 +177,4 @@ Sequence these together rather than piecemeal — several are one change set spa | 2026-08-07 | **SEC-36 → `Done` (repo-side); live rotation operator-pending** (branch `fix/sec-36-ldap-secret`). **State reconciliation:** the plaintext-in-`appsettings.json` half was already discharged before this branch — HEAD ships `"ServiceAccountPassword": "${secret:ldap/mxgateway/bind}"` (a fail-closed encrypted-store reference, documented at `GatewayConfiguration.md:252`, tested by `PreHostSecretExpansionTests`), not the literal the spec's stale baseline assumed. **Deviation from spec step 2:** the `${secret:}` reference was **kept, not deleted** — deleting it would regress the shipped/documented/tested secret-store channel and the finding (committed plaintext) is already resolved for `appsettings.json`. The live residual leak was the literal value in `glauth.md`, `docs/GatewayTesting.md`, and the historical `archreview/*` SEC-06 evidence — all scrubbed to `` placeholders with a source-of-truth pointer (`scadaproj/infra/glauth/`). Added `mxaccessgw-server` to the server csproj (dev channel) and extended the `ValidateLdap` blank-password message to name both channels (dev user-secrets, deployed `MxGateway__Ldap__ServiceAccountPassword`) — asserted by the updated `Validate_Fails_WhenLdapEnabledAndServiceAccountPasswordBlank`. Docs same commit: `GatewayConfiguration.md` (three-channel + rotation note), `glauth.md` (placeholders + rotation-required note + runbook pointer), `docs/GatewayTesting.md` (live-LDAP env-var example de-secreted). New operator runbook `docs/runbooks/SEC-36-ldap-credential-rotation.md` carries the cutover order (generate in glauth → pre-stage NEW env var on `10.100.0.48` and on `wonder-app-vd03` only if `Ldap.Enabled` → rotate GLAuth on `10.100.0.35` → verify dashboard login → devs set user-secrets). **The live GLAuth rotation + NSSM staging remain the operator's to execute** and are tracked in that runbook. Verification (macOS): `dotnet build …Server` 0 warnings/0 errors; `--filter ~GatewayOptionsValidator` green; `git grep -i` for the old value returns nothing across tracked files. | | 2026-08-07 | **P1 doc-drift batch → `Done`: TST-27, WRK-26 (discharges IPC-29), CLI-42, CLI-43, IPC-28** (branch `fix/doc-drift-batch`). Doc-only; no source, proto, or test changes — cross-checked against HEAD in this worktree. **TST-27**: `docs/GatewayConfiguration.md`'s `ShowTagValues` row no longer says "Reserved" — it now states what `false` (default) does (`DashboardEventBroadcaster` blanks tag values from a deep-cloned `MxEvent` before the SignalR events-hub mirror, metadata still renders), the security relevance (the per-session hub ACL, SEC-25 roadmap item 12, still does not exist, so this redaction is the only thing between a low-trust Viewer and other sessions' tag values), and the honest scope limit (the flag does **not** cover `/browse`). **WRK-26** (discharges **IPC-29**): `docs/MxAccessWorkerInstanceDesign.md`'s "Outbound Queues" section rewritten from the stale five-level priority list to the two-class `Control`/`Event` scheduler actually shipped (`WorkerFrameWriter`/`WorkerFrameWritePriority.cs`), with the collapsed-decision rationale recorded, and the overflow paragraph rewritten to the implemented fail-fast (`WorkerFault` category `QueueOverflow` → fault frame written → `RunAsync` unwinds → generic `WorkerExitCode.UnexpectedFailure`, dedicated code still open). `docs/WorkerFrameProtocol.md` gained a new "Write Scheduling And Sequencing" section: the two priority classes, enqueue-then-contend/single-lock-holder-drains-all, write-time peek-stamp-commit sequencing, per-frame-rejection vs. stream-failure semantics, and flush coalescing — stated truthfully as landed (WRK-23's peek-stamp-commit is live at HEAD) or not (the drain loop still awaits each event `WriteAsync` individually, so WRK-25's N-events-one-flush batching has **not** landed and the section says so explicitly). Cancellation is deliberately **not** documented as a firm contract — a one-paragraph placeholder notes it is pending WRK-22, which has not landed (confirmed by reading `WorkerFrameWriter.cs`: no `Claimed`/tombstone machinery exists yet). `gateway.md:328-330` was cross-checked and left unchanged — its sequence prose (both sides stamp at write, per GWC-28) already reads true. **CLI-42**: `clients/rust/README.md` and `docs/ClientPackaging.md`'s Rust section now document the vendored proto layout matching `clients/rust/build.rs` exactly — repo-path-first resolution (`../../src/ZB.MOM.WW.MxGateway.Contracts/Protos`) falling back to `clients/rust/protos/` when the canonical path is absent (published-tarball case), the same-commit refresh rule enforced by `scripts/check-codegen.ps1` Check 3, and why `cargo package`/`cargo publish` run without `--no-verify` (matches `scripts/pack-clients.ps1:190-192`). **CLI-43**: `docs/style-guides/JavaStyleGuide.md` line 8 now says "Target Java 17 (the Ignition 8.3 baseline...)" mirroring the CLI-12 wording, matching the shipped `clients/java/build.gradle` toolchain-17 build. **IPC-28**: `docs/Grpc.md`'s exception-mapping prose gained `CommandTooLarge` → `ResourceExhausted` (verified against the live `switch` in `Grpc/MxAccessGatewayService.cs:950-960`), and the `Invoke` section gained one sentence on the oversized-payload path (`WorkerClient.InvokeAsync` rejects at the enqueue boundary per-correlation, session not faulted — verified against `WorkerClient.cs:220-234`), cross-referencing the headroom rule already documented in `docs/GatewayConfiguration.md:120-129`. Did not touch the DrainEvents-truncation row or the proto/`Generated/` trees — those belong to a parallel codegen task per the handoff note. **Source files cross-read for accuracy** (no edits): `src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs`, `.../WorkerFrameWritePriority.cs`, `.../WorkerPipeSession.cs` (confirmed two-class scheduler, WRK-21/23/28/30 landed, WRK-25/WRK-22 not landed), `src/ZB.MOM.WW.MxGateway.Worker/WorkerApplication.cs` (exit-code mapping), `src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventQueue.cs` (overflow fault path), `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardEventBroadcaster.cs` + `Configuration/DashboardOptions.cs` + `docs/GatewayDashboardDesign.md:170` (ShowTagValues), `src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs:940-963` + `Workers/WorkerClient.cs:205-244` + `Workers/WorkerClientErrorCode.cs` (CommandTooLarge mapping), `clients/rust/build.rs`, `clients/rust/Cargo.toml`, `scripts/check-codegen.ps1`, `scripts/pack-clients.ps1` (Rust vendoring), `gateway.md:326-360` (sequence-prose cross-check). Verification (greps, doc-only — no build required): `grep -n 'Reserved' docs/GatewayConfiguration.md` no longer matches the `ShowTagValues` row; `grep -n 'faults' docs/MxAccessWorkerInstanceDesign.md` shows no remaining five-level list; `grep -n 'scheduling' docs/WorkerFrameProtocol.md` finds the new section; `grep -rn 'Java 21' docs/style-guides/` empty; `grep -i vendored docs/ClientPackaging.md clients/rust/README.md` non-empty in both; `grep -n 'CommandTooLarge' docs/Grpc.md` shows the mapping. | | 2026-08-07 | **IPC-23 + IPC-24 + IPC-25 + IPC-32 → `Done`; GWC-25 deferred proto-comment resolved** (branch `fix/ipc-24-25-codegen`). The proto-comment/doc + regen wave for the P0 codegen-freshness cluster. **Proto comments (comment-only):** `mxaccess_worker.proto` `GatewayHello.max_frame_bytes` gained the every-frame-must-fit / reply-builders-truncate sentence (IPC-23); `mxaccess_gateway.proto` `DrainEventsReply` gained the count-cap+byte-cap / drain-until-empty comment (IPC-23); `mxaccess_gateway.proto` `ReplayGap.oldest_available_sequence` gained the empty-ring value definition (`highest observed + 1`, `oldest − 1` formula stays valid) resolving GWC-25's deferred amendment. **Regen wave:** `Contracts/Generated/{MxaccessGateway,MxaccessWorker}.cs` (XML-doc only, no descriptor delta), `clients/rust/protos/{mxaccess_gateway,mxaccess_worker}.proto` byte-copied, Go `internal/generated/{mxaccess_gateway,mxaccess_worker}.pb.go` (worker binding was genuinely stale — it lacked the whole `MaxFrameBytes` field/accessor/rawDesc, now refreshed; gateway.pb.go got the new doc comments), Python `generated/mxaccess_worker_pb2.py` (real descriptor delta — `max_frame_bytes` field + cascaded offsets), Java aggregates `MxaccessGateway.java`/`MxaccessWorker.java` (javadoc from the new proto comments — **zero** protobuf-version churn under the pinned toolchain), and the client descriptor set `mxaccessgw-client-v1.protoset`. **IPC-24:** regenerating Java on the pinned toolchain (grpc 1.76.0 / protobuf 4.33.1, Homebrew JDK 17) produced only the legitimate javadoc delta with no `validateProtobufGencodeVersion`/`major=/minor=/patch=` churn — so the preferred path was taken: the unconditional churn-revert step and its comment were **deleted** from `.gitea/workflows/ci.yml` (the `git diff --exit-code` gate is now a true message-level drift gate for the single-file Java aggregates). **IPC-25:** pinned the Go generators in `clients/go/generate-proto.ps1` (`protoc-gen-go` v1.36.11 assert, `protoc-gen-go-grpc` 1.6.2 assert, protoc warn-only; also fixed a latent `(if …)`→`$(if …)` parse bug that broke the script under pwsh 7); added **Check 4** to `scripts/check-codegen.ps1` (regenerate Go+Python bindings, fail on any diff, tool-missing FAILS not skips) and relabeled the banners `1/4`…`4/4` + header comment (**IPC-32** folded in); added the pinned-generator installs to the `portable` CI job before the codegen step. **Docs same commit:** `ClientProtoGeneration.md` (pinned-versions table + Go/Python Check-4 guard + Java-revert-deleted note), `Contracts.md` (four-check enumeration), `GatewayTesting.md` (java job + four-check prose), `clients/java/zb-mom-ww-mxgateway-client/build.gradle` checkGeneratedClean caveat. **IPC-23 DrainEvents-truncation doc deviation:** `docs/Grpc.md` has no DrainEvents row (it documents only the seven public RPCs; DrainEvents is a worker diagnostic), and `docs/WorkerFrameProtocol.md` (owned by the parallel doc batch) already carries the byte-cap/drain-until-empty prose via WRK-21 — so no Grpc.md edit was made. **Evidence (macOS):** `pwsh scripts/check-codegen.ps1` all four checks green (banners `1/4`…`4/4`); `dotnet build …NonWindows.slnx` 0 warnings/0 errors; `dotnet test …Tests --filter ~ClientProtoInputTests` green; `grep -c max_frame_bytes` → Go worker 2, Python worker 1 (both non-zero, were 0); `clients/go` gofmt clean + `go build` + `go test` ok; `clients/python` `pytest` 163 passed/1 skipped; Java `gradle generateProto` clean under JDK 17. **Negative-path proof:** added a scratch field to `mxaccess_worker.proto`, regenerated only `Contracts/Generated/`, reran `check-codegen.ps1` and confirmed **Check 4 fails naming both `clients/go/internal/generated` and `clients/python/…/generated`**, then reverted. **Not published** (CLI-39 gates Go/Python republish). | +| 2026-08-07 | **IPC-27 → `Done`** (branch `fix/ipc-27-descriptor-test`). `ClientProtoInputTests.Descriptor_ContainsEveryContractMessageAndField` renamed to `Descriptor_ContainsEveryContractSymbol` and extended: the reflection walk now also collects enums (top-level `file.EnumTypes` + nested `message.EnumTypes`, as `{enumFullName}` and `{enumFullName}/{valueName}`) and services/methods (`file.Services`, as `{serviceFullName}` and `{serviceFullName}/{methodName}`), and `GalaxyRepositoryReflection.Descriptor` was added to the enumerated contract files alongside the existing gateway/worker descriptors. The published-side `CollectPublishedSymbols` grew matching walks over `FileDescriptorProto.EnumType`/`Service` and `DescriptorProto.EnumType`; the comparison stays a flat, order-insensitive string-set diff with no protoc dependency. Docs: `docs/ClientProtoGeneration.md` and `docs/Contracts.md` updated from "message or field" to full symbol coverage (message/field/enum/enum value/service/method), both now naming `galaxy_repository.proto` explicitly. **Red-path proof performed:** pointed the test at the pre-IPC-01 stale protoset (`git cat-file -p 0f88a95:clients/proto/descriptors/mxaccessgw-client-v1.protoset`), confirmed it failed naming — among the missing symbols — `mxaccess_worker.v1.GatewayHello/max_frame_bytes`, enum values (`MxCommandKind/MX_COMMAND_KIND_SUBSCRIBE_ALARMS`, `AlarmProviderMode/*`, `MxEventFamily/MX_EVENT_FAMILY_ON_ALARM_PROVIDER_MODE_CHANGED`), services/methods (`MxAccessGateway/StreamAlarms`, `GalaxyRepository/BrowseChildren`), and the full `galaxy_repository.v1.*` surface; restored the real descriptor path afterward and re-ran green. Verification: `dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx` 0 warnings/0 errors; `dotnet test …Tests --filter FullyQualifiedName~ClientProtoInputTests` 5/5 passed. No proto, config, or CI change, matching the design's stated scope. | diff --git a/archreview/2026-07-12/remediation/30-contracts-ipc.md b/archreview/2026-07-12/remediation/30-contracts-ipc.md index 7b66eda..ce6467d 100644 --- a/archreview/2026-07-12/remediation/30-contracts-ipc.md +++ b/archreview/2026-07-12/remediation/30-contracts-ipc.md @@ -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-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-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-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 | diff --git a/docs/ClientProtoGeneration.md b/docs/ClientProtoGeneration.md index 270ccf5..9985ffd 100644 --- a/docs/ClientProtoGeneration.md +++ b/docs/ClientProtoGeneration.md @@ -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). The gateway test project carries an independent, protoc-free freshness guard: -`ClientProtoInputTests.Descriptor_ContainsEveryContractMessageAndField` reflects -over the in-process contract descriptors and fails if any contract message or -field is missing from the committed protoset. This is the primary CI gate for -descriptor staleness; a red test means "regenerate and commit the protoset." +`ClientProtoInputTests.Descriptor_ContainsEveryContractSymbol` reflects over the +in-process contract descriptors — `mxaccess_gateway.proto`, `mxaccess_worker.proto`, +and `galaxy_repository.proto` — and fails if any contract message, field, enum, +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 diff --git a/docs/Contracts.md b/docs/Contracts.md index fc53b25..cfb14a3 100644 --- a/docs/Contracts.md +++ b/docs/Contracts.md @@ -156,10 +156,11 @@ pwsh -File scripts/publish-client-proto-inputs.ps1 Freshness is guarded two ways so a skipped regeneration cannot ship silently: -- `ClientProtoInputTests.Descriptor_ContainsEveryContractMessageAndField` (gateway test project) - reflects over the in-process contract descriptors and fails if any message or field is missing - from the committed protoset. It is semantic (symbol presence), needs no protoc, and runs in the - Linux CI. A red test means "regenerate and commit the protoset." +- `ClientProtoInputTests.Descriptor_ContainsEveryContractSymbol` (gateway test project) + reflects over the in-process contract descriptors — including `galaxy_repository.proto` — and + fails if any message, field, enum, enum value, service, or method is missing from the committed + 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 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. diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Contracts/ClientProtoInputTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Contracts/ClientProtoInputTests.cs index b047347..8af2d58 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Contracts/ClientProtoInputTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Contracts/ClientProtoInputTests.cs @@ -3,22 +3,24 @@ using Google.Protobuf; using Google.Protobuf.Reflection; using ZB.MOM.WW.MxGateway.Contracts; using ZB.MOM.WW.MxGateway.Contracts.Proto; +using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; namespace ZB.MOM.WW.MxGateway.Tests.Contracts; public sealed class ClientProtoInputTests { /// - /// 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 - /// .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. + /// Guards the published client descriptor set against silent staleness. Every message, + /// field, enum, enum value, service, and method 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() + public void Descriptor_ContainsEveryContractSymbol() { DirectoryInfo repositoryRoot = FindRepositoryRoot(); string descriptorPath = Path.Combine( @@ -34,20 +36,65 @@ public sealed class ClientProtoInputTests HashSet publishedMessages = new(StringComparer.Ordinal); HashSet publishedFields = new(StringComparer.Ordinal); + HashSet publishedEnums = new(StringComparer.Ordinal); + HashSet publishedServices = new(StringComparer.Ordinal); foreach (FileDescriptorProto file in descriptorSet.File) { 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 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) { - 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, DescriptorProto message, HashSet messages, - HashSet fields) + HashSet fields, + HashSet enums) { string fullName = string.IsNullOrEmpty(package) ? message.Name : package + "." + message.Name; messages.Add(fullName); @@ -74,7 +122,26 @@ public sealed class ClientProtoInputTests 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 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, HashSet publishedMessages, HashSet publishedFields, + HashSet publishedEnums, List missing) { if (!publishedMessages.Contains(message.FullName)) @@ -100,7 +168,32 @@ public sealed class ClientProtoInputTests 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 publishedEnums, + List 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); + } } }