Merge pull request #98 from agent-2/issue-35-parity-fixture-matrix
Issue #35: add parity fixture matrix
This commit was merged in pull request #98.
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"fixtureSet": "mxaccess-gateway-parity-fixture-matrix",
|
||||
"contractName": "mxaccess-gateway",
|
||||
"gatewayProtocolVersion": 1,
|
||||
"workerProtocolVersion": 1,
|
||||
"sourceCaptureRoot": "C:/Users/dohertj2/Desktop/mxaccess/captures",
|
||||
"sourceDocs": [
|
||||
"C:/Users/dohertj2/Desktop/mxaccess/docs/MXAccess-Public-API.md",
|
||||
"C:/Users/dohertj2/Desktop/mxaccess/docs/Current-Sprint-State.md"
|
||||
],
|
||||
"comparisonFormat": {
|
||||
"description": "Each parity run records the same command against direct MXAccess and the gateway-backed worker, then compares raw parity fields instead of client wrapper behavior.",
|
||||
"directMxAccess": {
|
||||
"requiredFields": [
|
||||
"method",
|
||||
"arguments",
|
||||
"returnedValue",
|
||||
"hresult",
|
||||
"exceptionType",
|
||||
"statuses",
|
||||
"events"
|
||||
]
|
||||
},
|
||||
"gatewayResult": {
|
||||
"requiredFields": [
|
||||
"kind",
|
||||
"protocolStatus",
|
||||
"returnValue",
|
||||
"hresult",
|
||||
"statuses",
|
||||
"diagnosticMessage",
|
||||
"events"
|
||||
]
|
||||
},
|
||||
"eventFields": [
|
||||
"family",
|
||||
"serverHandle",
|
||||
"itemHandle",
|
||||
"value",
|
||||
"quality",
|
||||
"sourceTimestamp",
|
||||
"statuses",
|
||||
"workerSequence",
|
||||
"workerTimestamp",
|
||||
"gatewayReceiveTimestamp",
|
||||
"hresult",
|
||||
"rawStatus"
|
||||
],
|
||||
"comparisonKeys": [
|
||||
"hresult",
|
||||
"exceptionType",
|
||||
"returnedValue",
|
||||
"statusArrayShape",
|
||||
"statusRawFields",
|
||||
"eventFamilyOrder",
|
||||
"eventPayloadShape",
|
||||
"valueProjection",
|
||||
"rawFallbackMetadata"
|
||||
]
|
||||
},
|
||||
"methodFixtures": [
|
||||
{
|
||||
"id": "method.register.basic",
|
||||
"method": "Register",
|
||||
"commandKind": "MX_COMMAND_KIND_REGISTER",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/001-register/harness.log",
|
||||
"captures/047-frida-com-proxy-register/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve returned server handle in returnValue and RegisterReply",
|
||||
"preserve success HRESULT as 0",
|
||||
"do not emit MXAccess events for register"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.unregister.basic",
|
||||
"method": "Unregister",
|
||||
"commandKind": "MX_COMMAND_KIND_UNREGISTER",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/001-register/harness.log",
|
||||
"captures/109-native-post-remove-errors/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve void return shape with explicit protocol success",
|
||||
"preserve HRESULT or COM exception details for invalid server handle",
|
||||
"close registered handle only after MXAccess succeeds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.add-item.scalar",
|
||||
"method": "AddItem",
|
||||
"commandKind": "MX_COMMAND_KIND_ADD_ITEM",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/002-add-remove-scalar/harness.log",
|
||||
"captures/006-add-invalid/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve returned item handle in returnValue and AddItemReply",
|
||||
"preserve invalid item reference HRESULT/status details",
|
||||
"do not prevalidate item definition in the gateway"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.add-item2.context",
|
||||
"method": "AddItem2",
|
||||
"commandKind": "MX_COMMAND_KIND_ADD_ITEM2",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/mxaccess-additem2-testint-context.log",
|
||||
"captures/121-frida-buffered-history-testhistoryvalue-context/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"pass item_definition and item_context exactly as supplied",
|
||||
"preserve returned item handle in returnValue and AddItem2Reply",
|
||||
"compare context-bearing reference resolution against direct MXAccess"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.remove-item.basic",
|
||||
"method": "RemoveItem",
|
||||
"commandKind": "MX_COMMAND_KIND_REMOVE_ITEM",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/002-add-remove-scalar/harness.log",
|
||||
"captures/109-native-post-remove-errors/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve void return shape with explicit protocol success",
|
||||
"preserve post-remove and invalid-handle HRESULT/status behavior",
|
||||
"remove diagnostic handle state only after MXAccess succeeds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.advise.supervisory-data-change",
|
||||
"method": "Advise",
|
||||
"commandKind": "MX_COMMAND_KIND_ADVISE",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/003-subscribe-scalars/harness.log",
|
||||
"captures/058-frida-subscribe-testint/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve successful command reply shape",
|
||||
"forward OnDataChange with value, quality, timestamp, and status array",
|
||||
"preserve per-worker event order"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.unadvise.basic",
|
||||
"method": "UnAdvise",
|
||||
"commandKind": "MX_COMMAND_KIND_UN_ADVISE",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/058-frida-subscribe-testint/harness.log",
|
||||
"captures/007-subscribe-invalid/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve void return shape with explicit protocol success",
|
||||
"preserve invalid item handle HRESULT/status behavior",
|
||||
"do not distinguish plain and supervisory cleanup beyond MXAccess behavior"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.advise-supervisory.basic",
|
||||
"method": "AdviseSupervisory",
|
||||
"commandKind": "MX_COMMAND_KIND_ADVISE_SUPERVISORY",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/058-frida-subscribe-testint/harness.log",
|
||||
"captures/105-frida-advise-shortdesc-prebound-fixed/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"keep AdviseSupervisory distinct from plain Advise in command kind",
|
||||
"forward native OnDataChange only when MXAccess emits it",
|
||||
"compare supervisory item status arrays without normalization"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.add-buffered-item.context",
|
||||
"method": "AddBufferedItem",
|
||||
"commandKind": "MX_COMMAND_KIND_ADD_BUFFERED_ITEM",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/079-frida-add-buffered-advise-testint/harness.log",
|
||||
"captures/120-frida-buffered-history-testhistoryvalue/harness.log",
|
||||
"captures/121-frida-buffered-history-testhistoryvalue-context/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"pass item_definition and item_context exactly as supplied",
|
||||
"preserve returned buffered item handle in returnValue and AddBufferedItemReply",
|
||||
"keep buffered registration distinct from normal AddItem2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.set-buffered-update-interval.basic",
|
||||
"method": "SetBufferedUpdateInterval",
|
||||
"commandKind": "MX_COMMAND_KIND_SET_BUFFERED_UPDATE_INTERVAL",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/mxaccess-set-buffered-interval-1000.log",
|
||||
"captures/079-frida-add-buffered-advise-testint/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve requested update interval without clamping in the gateway",
|
||||
"preserve void return shape with explicit protocol success",
|
||||
"compare buffered event cadence only in opt-in live runs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.suspend.scan-state",
|
||||
"method": "Suspend",
|
||||
"commandKind": "MX_COMMAND_KIND_SUSPEND",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/077-frida-suspend-advised-scanstate/harness.log",
|
||||
"captures/118-frida-suspend-advised-scanstate-long/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve out MxStatus in SuspendReply and repeated statuses",
|
||||
"preserve HRESULT separately from status detail",
|
||||
"do not synthesize OperationComplete if native MXAccess does not raise it"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.activate.scan-state",
|
||||
"method": "Activate",
|
||||
"commandKind": "MX_COMMAND_KIND_ACTIVATE",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/078-frida-activate-advised-scanstate/harness.log",
|
||||
"captures/119-frida-activate-advised-scanstate-long/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve out MxStatus in ActivateReply and repeated statuses",
|
||||
"preserve HRESULT separately from status detail",
|
||||
"do not synthesize OperationComplete if native MXAccess does not raise it"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.write.value-status-matrix",
|
||||
"method": "Write",
|
||||
"commandKind": "MX_COMMAND_KIND_WRITE",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/023-frida-write-test-int-sequence-109-111/harness.log",
|
||||
"captures/024-frida-write-test-bool-sequence/harness.log",
|
||||
"captures/089-frida-write-testint-wrong-type/harness.log",
|
||||
"captures/090-frida-write-invalid-reference/harness.log",
|
||||
"captures/107-native-write-testint-current/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve scalar and array value projections plus raw fallback metadata",
|
||||
"preserve wrong-type and invalid-reference HRESULT/status arrays",
|
||||
"forward OnWriteComplete only when native MXAccess emits it"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.write2.timestamped",
|
||||
"method": "Write2",
|
||||
"commandKind": "MX_COMMAND_KIND_WRITE2",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/042-frida-write2-test-int-timestamp/harness.log",
|
||||
"captures/066-frida-write2-test-bool-timestamp/harness.log",
|
||||
"captures/075-frida-write2-test-datetime-array-timestamp/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve timestamp_value as an MXAccess VARIANT projection",
|
||||
"preserve write value shape and HRESULT/status arrays",
|
||||
"compare timestamped write completion events against direct MXAccess"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.write-secured.rejection-gap",
|
||||
"method": "WriteSecured",
|
||||
"commandKind": "MX_COMMAND_KIND_WRITE_SECURED",
|
||||
"status": "documented_gap",
|
||||
"captureReferences": [
|
||||
"captures/036-frida-write-secured-test-int/harness.log",
|
||||
"captures/111-frida-write-secured-auth-protectedvalue/harness.log",
|
||||
"captures/112-frida-write-secured-auth-verified-protectedvalue1/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve observed 0x80004021 rejection before a value-bearing NMX body",
|
||||
"preserve current_user_id and verifier_user_id only as command inputs, not logs",
|
||||
"upgrade this gap to planned_fixture when a successful direct WriteSecured path is observed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.write-secured2.authenticated",
|
||||
"method": "WriteSecured2",
|
||||
"commandKind": "MX_COMMAND_KIND_WRITE_SECURED2",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/113-frida-write-secured2-auth-protectedvalue/harness.log",
|
||||
"captures/116-frida-write-secured2-auth-verified-protectedvalue1/harness.log",
|
||||
"captures/117-frida-write-secured2-auth-testint/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve authenticated timestamped secured write body shape",
|
||||
"preserve HRESULT/status arrays without logging credential-bearing values",
|
||||
"do not synthesize OnWriteComplete when direct MXAccess does not emit it"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.authenticate-user.basic",
|
||||
"method": "AuthenticateUser",
|
||||
"commandKind": "MX_COMMAND_KIND_AUTHENTICATE_USER",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/087-frida-authenticate-administrator-empty/harness.log",
|
||||
"captures/088-frida-authenticate-invalid-empty/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve returned user id in returnValue and AuthenticateUserReply",
|
||||
"preserve invalid credential HRESULT/status behavior",
|
||||
"redact verify_user_password from logs and diagnostics"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "method.archestra-user-to-id.basic",
|
||||
"method": "ArchestrAUserToId",
|
||||
"commandKind": "MX_COMMAND_KIND_ARCHESTRA_USER_TO_ID",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/mxaccess-user-map-administrator.log",
|
||||
"captures/mxaccess-user-map-invalid.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve returned user id in returnValue and ArchestrAUserToIdReply",
|
||||
"preserve invalid user GUID HRESULT/status behavior",
|
||||
"compare raw mapping behavior without normalizing unknown users"
|
||||
]
|
||||
}
|
||||
],
|
||||
"eventFixtures": [
|
||||
{
|
||||
"id": "event.on-data-change.scalar",
|
||||
"family": "MX_EVENT_FAMILY_ON_DATA_CHANGE",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/003-subscribe-scalars/harness.log",
|
||||
"captures/106-native-subscribe-testint-current/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve value, quality, timestamp, status array, and worker sequence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "event.on-write-complete.status",
|
||||
"family": "MX_EVENT_FAMILY_ON_WRITE_COMPLETE",
|
||||
"status": "planned_fixture",
|
||||
"captureReferences": [
|
||||
"captures/008-write-test-int-same-value/harness.log",
|
||||
"captures/107-native-write-testint-current/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve write-complete status array and optional HRESULT"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "event.operation-complete.native-trigger-gap",
|
||||
"family": "MX_EVENT_FAMILY_OPERATION_COMPLETE",
|
||||
"status": "documented_gap",
|
||||
"captureReferences": [
|
||||
"captures/077-frida-suspend-advised-scanstate/harness.log",
|
||||
"captures/118-frida-suspend-advised-scanstate-long/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"do not synthesize OperationComplete from Write or OnWriteComplete",
|
||||
"upgrade this gap when a public MXAccess trigger emits event family 3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "event.on-buffered-data-change.batch-gap",
|
||||
"family": "MX_EVENT_FAMILY_ON_BUFFERED_DATA_CHANGE",
|
||||
"status": "documented_gap",
|
||||
"captureReferences": [
|
||||
"captures/120-frida-buffered-history-testhistoryvalue/harness.log",
|
||||
"captures/122-frida-buffered-history-testhistoryvalue-plainadvise/harness.log"
|
||||
],
|
||||
"assertions": [
|
||||
"preserve raw buffered metadata until a public multi-sample event payload is observed",
|
||||
"upgrade this gap when OnBufferedDataChange batches are captured from MXAccess"
|
||||
]
|
||||
}
|
||||
],
|
||||
"scenarioGroups": [
|
||||
{
|
||||
"id": "invalid_handles",
|
||||
"description": "Invalid server, item, post-remove, and invalid-reference cases keep MXAccess-owned HRESULT and status behavior.",
|
||||
"fixtureIds": [
|
||||
"method.add-item.scalar",
|
||||
"method.remove-item.basic",
|
||||
"method.unadvise.basic",
|
||||
"method.write.value-status-matrix",
|
||||
"method.unregister.basic"
|
||||
],
|
||||
"captureReferences": [
|
||||
"captures/006-add-invalid/harness.log",
|
||||
"captures/007-subscribe-invalid/harness.log",
|
||||
"captures/109-native-post-remove-errors/harness.log",
|
||||
"captures/110-native-invalid-handle-errors/harness.log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "write_statuses",
|
||||
"description": "Write success, wrong type, invalid reference, scalar arrays, and completion-status cases compare HRESULT, status array, value projection, and event shape.",
|
||||
"fixtureIds": [
|
||||
"method.write.value-status-matrix",
|
||||
"method.write2.timestamped",
|
||||
"event.on-write-complete.status"
|
||||
],
|
||||
"captureReferences": [
|
||||
"captures/089-frida-write-testint-wrong-type/harness.log",
|
||||
"captures/090-frida-write-invalid-reference/harness.log",
|
||||
"captures/091-frida-write-testint-double-type/harness.log",
|
||||
"captures/097-frida-write-bool-array-pattern/harness.log",
|
||||
"captures/107-native-write-testint-current/harness.log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "secured_writes",
|
||||
"description": "Secured writes include observed WriteSecured rejection and authenticated WriteSecured2 success paths without logging credential-bearing values.",
|
||||
"fixtureIds": [
|
||||
"method.write-secured.rejection-gap",
|
||||
"method.write-secured2.authenticated",
|
||||
"method.authenticate-user.basic"
|
||||
],
|
||||
"captureReferences": [
|
||||
"captures/036-frida-write-secured-test-int/harness.log",
|
||||
"captures/111-frida-write-secured-auth-protectedvalue/harness.log",
|
||||
"captures/113-frida-write-secured2-auth-protectedvalue/harness.log",
|
||||
"captures/117-frida-write-secured2-auth-testint/harness.log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "add_item_context",
|
||||
"description": "Context-bearing item registration compares AddItem2 and buffered AddBufferedItem argument preservation.",
|
||||
"fixtureIds": [
|
||||
"method.add-item2.context",
|
||||
"method.add-buffered-item.context"
|
||||
],
|
||||
"captureReferences": [
|
||||
"captures/mxaccess-additem2-testint-context.log",
|
||||
"captures/121-frida-buffered-history-testhistoryvalue-context/harness.log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "buffered_registration",
|
||||
"description": "Buffered registration and interval setup are tracked separately from normal advice until a public buffered data-change batch is captured.",
|
||||
"fixtureIds": [
|
||||
"method.add-buffered-item.context",
|
||||
"method.set-buffered-update-interval.basic",
|
||||
"event.on-buffered-data-change.batch-gap"
|
||||
],
|
||||
"captureReferences": [
|
||||
"captures/079-frida-add-buffered-advise-testint/harness.log",
|
||||
"captures/120-frida-buffered-history-testhistoryvalue/harness.log",
|
||||
"captures/122-frida-buffered-history-testhistoryvalue-plainadvise/harness.log"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -76,6 +76,13 @@ stdout/stderr lines emitted during the run.
|
||||
|
||||
## Focused Commands
|
||||
|
||||
Run the parity fixture matrix tests after changing the integration parity
|
||||
scenario list:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~ParityFixtureMatrixTests
|
||||
```
|
||||
|
||||
Run the fake worker tests after changing gateway worker IPC, session startup, or
|
||||
event streaming behavior:
|
||||
|
||||
@@ -95,6 +102,7 @@ dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Parity Fixture Matrix](./ParityFixtureMatrix.md)
|
||||
- [Gateway Process Design](./gateway-process-design.md)
|
||||
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
|
||||
- [MXAccess Worker Instance Detailed Design](./mxaccess-worker-instance-design.md)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Parity Fixture Matrix
|
||||
|
||||
The parity fixture matrix defines the live-test scenarios used to compare
|
||||
direct MXAccess behavior with the gateway-backed worker. It is a planning and
|
||||
validation fixture, not a source of synthetic MXAccess behavior.
|
||||
|
||||
The matrix lives in
|
||||
`clients/proto/fixtures/parity/parity-fixture-matrix.json`. It references the
|
||||
local MXAccess capture set under
|
||||
`C:/Users/dohertj2/Desktop/mxaccess/captures` and keeps capture paths relative
|
||||
to that root so the repository does not copy raw capture artifacts.
|
||||
|
||||
## Scope
|
||||
|
||||
The matrix covers every public `LMXProxyServerClass` method represented by the
|
||||
gateway contract:
|
||||
|
||||
- `Register`
|
||||
- `Unregister`
|
||||
- `AddItem`
|
||||
- `AddItem2`
|
||||
- `RemoveItem`
|
||||
- `Advise`
|
||||
- `UnAdvise`
|
||||
- `AdviseSupervisory`
|
||||
- `AddBufferedItem`
|
||||
- `SetBufferedUpdateInterval`
|
||||
- `Suspend`
|
||||
- `Activate`
|
||||
- `Write`
|
||||
- `Write2`
|
||||
- `WriteSecured`
|
||||
- `WriteSecured2`
|
||||
- `AuthenticateUser`
|
||||
- `ArchestrAUserToId`
|
||||
|
||||
Each entry is either a `planned_fixture` or a `documented_gap`.
|
||||
`WriteSecured` remains a documented gap because the current captures show
|
||||
`0x80004021` before MXAccess emits a value-bearing write body.
|
||||
`OperationComplete` and public `OnBufferedDataChange` batches also remain
|
||||
documented gaps because no capture in the current set proves those public event
|
||||
payloads from native MXAccess.
|
||||
|
||||
## Required Scenario Groups
|
||||
|
||||
The matrix pins the high-risk parity scenarios from the integration milestone:
|
||||
|
||||
| Scenario | Purpose |
|
||||
|----------|---------|
|
||||
| `invalid_handles` | Preserves invalid server, item, post-remove, and invalid-reference HRESULT/status behavior. |
|
||||
| `write_statuses` | Compares successful writes, wrong-type writes, invalid references, arrays, and write-complete status arrays. |
|
||||
| `secured_writes` | Covers observed `WriteSecured` rejection and authenticated `WriteSecured2` paths without logging credential-bearing values. |
|
||||
| `add_item_context` | Ensures `AddItem2` and buffered registration pass context strings exactly as supplied. |
|
||||
| `buffered_registration` | Tracks buffered item registration and interval setup separately from normal advice. |
|
||||
|
||||
## Comparison Format
|
||||
|
||||
Each live parity fixture should record one direct MXAccess result and one
|
||||
gateway result for the same operation.
|
||||
|
||||
Direct MXAccess records include:
|
||||
|
||||
- method name,
|
||||
- arguments after redaction,
|
||||
- returned value,
|
||||
- HRESULT,
|
||||
- exception type,
|
||||
- `MXSTATUS_PROXY[]` values,
|
||||
- native event records in observed order.
|
||||
|
||||
Gateway records include:
|
||||
|
||||
- `MxCommandKind`,
|
||||
- `ProtocolStatus`,
|
||||
- `MxCommandReply.ReturnValue`,
|
||||
- `MxCommandReply.Hresult`,
|
||||
- repeated `MxCommandReply.Statuses`,
|
||||
- safe diagnostic message,
|
||||
- streamed `MxEvent` records in worker-sequence order.
|
||||
|
||||
Compare HRESULT, exception type, returned value, status array shape, raw status
|
||||
fields, event family order, event payload shape, value projection, and raw
|
||||
fallback metadata. The gateway must not convert an MXAccess command failure
|
||||
into a transport failure when the worker captured HRESULT or status details.
|
||||
|
||||
## Validation
|
||||
|
||||
Run the parity fixture matrix tests after changing the matrix:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~ParityFixtureMatrixTests
|
||||
```
|
||||
|
||||
Live MXAccess execution remains opt-in. The matrix defines which scenarios to
|
||||
run when the installed MXAccess COM component and provider state are available;
|
||||
normal unit tests only validate the repository fixture shape.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Gateway Testing](./GatewayTesting.md)
|
||||
- [MXAccess Worker Instance Detailed Design](./mxaccess-worker-instance-design.md)
|
||||
- [Protobuf Contracts](./Contracts.md)
|
||||
@@ -0,0 +1,293 @@
|
||||
using System.Text.Json;
|
||||
using MxGateway.Contracts;
|
||||
|
||||
namespace MxGateway.Tests.Contracts;
|
||||
|
||||
public sealed class ParityFixtureMatrixTests
|
||||
{
|
||||
[Fact]
|
||||
public void Matrix_DeclaresCurrentProtocolVersionsAndComparisonFields()
|
||||
{
|
||||
using JsonDocument matrix = LoadParityMatrix();
|
||||
JsonElement root = matrix.RootElement;
|
||||
|
||||
Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
|
||||
Assert.Equal("mxaccess-gateway-parity-fixture-matrix", root.GetProperty("fixtureSet").GetString());
|
||||
Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, root.GetProperty("gatewayProtocolVersion").GetUInt32());
|
||||
Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, root.GetProperty("workerProtocolVersion").GetUInt32());
|
||||
|
||||
JsonElement comparisonFormat = root.GetProperty("comparisonFormat");
|
||||
AssertRequiredFields(
|
||||
comparisonFormat.GetProperty("directMxAccess").GetProperty("requiredFields"),
|
||||
"method",
|
||||
"arguments",
|
||||
"returnedValue",
|
||||
"hresult",
|
||||
"statuses",
|
||||
"events");
|
||||
AssertRequiredFields(
|
||||
comparisonFormat.GetProperty("gatewayResult").GetProperty("requiredFields"),
|
||||
"kind",
|
||||
"protocolStatus",
|
||||
"returnValue",
|
||||
"hresult",
|
||||
"statuses",
|
||||
"events");
|
||||
AssertRequiredFields(
|
||||
comparisonFormat.GetProperty("eventFields"),
|
||||
"family",
|
||||
"value",
|
||||
"quality",
|
||||
"sourceTimestamp",
|
||||
"statuses",
|
||||
"workerSequence");
|
||||
AssertRequiredFields(
|
||||
comparisonFormat.GetProperty("comparisonKeys"),
|
||||
"hresult",
|
||||
"statusArrayShape",
|
||||
"statusRawFields",
|
||||
"eventFamilyOrder",
|
||||
"eventPayloadShape",
|
||||
"valueProjection",
|
||||
"rawFallbackMetadata");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matrix_CoversEveryPublicMxAccessMethod()
|
||||
{
|
||||
using JsonDocument matrix = LoadParityMatrix();
|
||||
JsonElement methodFixtures = matrix.RootElement.GetProperty("methodFixtures");
|
||||
|
||||
Dictionary<string, JsonElement> fixturesByMethod = [];
|
||||
HashSet<string> ids = new(StringComparer.Ordinal);
|
||||
|
||||
foreach (JsonElement fixture in methodFixtures.EnumerateArray())
|
||||
{
|
||||
string id = fixture.GetProperty("id").GetString()!;
|
||||
string method = fixture.GetProperty("method").GetString()!;
|
||||
string commandKind = fixture.GetProperty("commandKind").GetString()!;
|
||||
string status = fixture.GetProperty("status").GetString()!;
|
||||
|
||||
Assert.True(ids.Add(id), $"Duplicate parity fixture id '{id}'.");
|
||||
Assert.True(fixturesByMethod.TryAdd(method, fixture), $"Duplicate parity method '{method}'.");
|
||||
Assert.StartsWith("MX_COMMAND_KIND_", commandKind, StringComparison.Ordinal);
|
||||
Assert.Contains(status, KnownFixtureStatuses);
|
||||
Assert.NotEmpty(fixture.GetProperty("assertions").EnumerateArray());
|
||||
AssertCaptureReferencesAreRelative(fixture.GetProperty("captureReferences"));
|
||||
}
|
||||
|
||||
Assert.Equal(ExpectedPublicMethods.Order(StringComparer.Ordinal), fixturesByMethod.Keys.Order(StringComparer.Ordinal));
|
||||
|
||||
foreach (string method in ExpectedPublicMethods)
|
||||
{
|
||||
JsonElement fixture = fixturesByMethod[method];
|
||||
string status = fixture.GetProperty("status").GetString()!;
|
||||
|
||||
Assert.True(
|
||||
status == "planned_fixture" || status == "documented_gap",
|
||||
$"Method '{method}' must have a planned parity fixture or documented gap.");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matrix_CoversRequiredParityScenarioGroups()
|
||||
{
|
||||
using JsonDocument matrix = LoadParityMatrix();
|
||||
HashSet<string> knownFixtureIds = GetFixtureIds(matrix.RootElement);
|
||||
Dictionary<string, JsonElement> groupsById = [];
|
||||
|
||||
foreach (JsonElement group in matrix.RootElement.GetProperty("scenarioGroups").EnumerateArray())
|
||||
{
|
||||
string id = group.GetProperty("id").GetString()!;
|
||||
|
||||
Assert.True(groupsById.TryAdd(id, group), $"Duplicate parity scenario group '{id}'.");
|
||||
Assert.NotEmpty(group.GetProperty("description").GetString()!);
|
||||
Assert.NotEmpty(group.GetProperty("fixtureIds").EnumerateArray());
|
||||
AssertCaptureReferencesAreRelative(group.GetProperty("captureReferences"));
|
||||
|
||||
foreach (JsonElement fixtureIdElement in group.GetProperty("fixtureIds").EnumerateArray())
|
||||
{
|
||||
string fixtureId = fixtureIdElement.GetString()!;
|
||||
Assert.Contains(fixtureId, knownFixtureIds);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string requiredGroup in RequiredScenarioGroups)
|
||||
{
|
||||
Assert.True(groupsById.ContainsKey(requiredGroup), $"Missing required parity scenario group '{requiredGroup}'.");
|
||||
}
|
||||
|
||||
AssertScenarioCovers(groupsById["invalid_handles"], "method.remove-item.basic", "method.write.value-status-matrix");
|
||||
AssertScenarioCovers(groupsById["write_statuses"], "method.write.value-status-matrix", "event.on-write-complete.status");
|
||||
AssertScenarioCovers(groupsById["secured_writes"], "method.write-secured.rejection-gap", "method.write-secured2.authenticated");
|
||||
AssertScenarioCovers(groupsById["add_item_context"], "method.add-item2.context", "method.add-buffered-item.context");
|
||||
AssertScenarioCovers(groupsById["buffered_registration"], "method.add-buffered-item.context", "event.on-buffered-data-change.batch-gap");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matrix_CoversEveryPublicMxAccessEventFamily()
|
||||
{
|
||||
using JsonDocument matrix = LoadParityMatrix();
|
||||
Dictionary<string, JsonElement> fixturesByFamily = [];
|
||||
|
||||
foreach (JsonElement fixture in matrix.RootElement.GetProperty("eventFixtures").EnumerateArray())
|
||||
{
|
||||
string family = fixture.GetProperty("family").GetString()!;
|
||||
string status = fixture.GetProperty("status").GetString()!;
|
||||
|
||||
Assert.True(fixturesByFamily.TryAdd(family, fixture), $"Duplicate parity event family '{family}'.");
|
||||
Assert.Contains(status, KnownFixtureStatuses);
|
||||
Assert.NotEmpty(fixture.GetProperty("assertions").EnumerateArray());
|
||||
AssertCaptureReferencesAreRelative(fixture.GetProperty("captureReferences"));
|
||||
}
|
||||
|
||||
foreach (string eventFamily in ExpectedEventFamilies)
|
||||
{
|
||||
Assert.True(fixturesByFamily.ContainsKey(eventFamily), $"Missing parity fixture for event family '{eventFamily}'.");
|
||||
}
|
||||
|
||||
Assert.Equal("documented_gap", fixturesByFamily["MX_EVENT_FAMILY_OPERATION_COMPLETE"].GetProperty("status").GetString());
|
||||
Assert.Equal("documented_gap", fixturesByFamily["MX_EVENT_FAMILY_ON_BUFFERED_DATA_CHANGE"].GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
private static readonly string[] ExpectedPublicMethods =
|
||||
[
|
||||
"Register",
|
||||
"Unregister",
|
||||
"AddItem",
|
||||
"AddItem2",
|
||||
"RemoveItem",
|
||||
"Advise",
|
||||
"UnAdvise",
|
||||
"AdviseSupervisory",
|
||||
"AddBufferedItem",
|
||||
"SetBufferedUpdateInterval",
|
||||
"Suspend",
|
||||
"Activate",
|
||||
"Write",
|
||||
"Write2",
|
||||
"WriteSecured",
|
||||
"WriteSecured2",
|
||||
"AuthenticateUser",
|
||||
"ArchestrAUserToId",
|
||||
];
|
||||
|
||||
private static readonly string[] ExpectedEventFamilies =
|
||||
[
|
||||
"MX_EVENT_FAMILY_ON_DATA_CHANGE",
|
||||
"MX_EVENT_FAMILY_ON_WRITE_COMPLETE",
|
||||
"MX_EVENT_FAMILY_OPERATION_COMPLETE",
|
||||
"MX_EVENT_FAMILY_ON_BUFFERED_DATA_CHANGE",
|
||||
];
|
||||
|
||||
private static readonly string[] RequiredScenarioGroups =
|
||||
[
|
||||
"invalid_handles",
|
||||
"write_statuses",
|
||||
"secured_writes",
|
||||
"add_item_context",
|
||||
"buffered_registration",
|
||||
];
|
||||
|
||||
private static readonly string[] KnownFixtureStatuses =
|
||||
[
|
||||
"planned_fixture",
|
||||
"documented_gap",
|
||||
];
|
||||
|
||||
private static void AssertRequiredFields(
|
||||
JsonElement fields,
|
||||
params string[] expectedFields)
|
||||
{
|
||||
HashSet<string> declared = fields
|
||||
.EnumerateArray()
|
||||
.Select(field => field.GetString()!)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (string expectedField in expectedFields)
|
||||
{
|
||||
Assert.Contains(expectedField, declared);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertCaptureReferencesAreRelative(JsonElement captureReferences)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
foreach (JsonElement captureReference in captureReferences.EnumerateArray())
|
||||
{
|
||||
string path = captureReference.GetString()!;
|
||||
|
||||
Assert.StartsWith("captures/", path, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\\", path, StringComparison.Ordinal);
|
||||
Assert.False(Path.IsPathRooted(path), $"Capture reference '{path}' must be relative.");
|
||||
count++;
|
||||
}
|
||||
|
||||
Assert.True(count > 0, "Each parity fixture must reference at least one MXAccess capture.");
|
||||
}
|
||||
|
||||
private static void AssertScenarioCovers(
|
||||
JsonElement group,
|
||||
params string[] fixtureIds)
|
||||
{
|
||||
HashSet<string> declared = group
|
||||
.GetProperty("fixtureIds")
|
||||
.EnumerateArray()
|
||||
.Select(fixtureId => fixtureId.GetString()!)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (string fixtureId in fixtureIds)
|
||||
{
|
||||
Assert.Contains(fixtureId, declared);
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<string> GetFixtureIds(JsonElement root)
|
||||
{
|
||||
HashSet<string> ids = new(StringComparer.Ordinal);
|
||||
|
||||
foreach (JsonElement fixture in root.GetProperty("methodFixtures").EnumerateArray())
|
||||
{
|
||||
ids.Add(fixture.GetProperty("id").GetString()!);
|
||||
}
|
||||
|
||||
foreach (JsonElement fixture in root.GetProperty("eventFixtures").EnumerateArray())
|
||||
{
|
||||
ids.Add(fixture.GetProperty("id").GetString()!);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static JsonDocument LoadParityMatrix()
|
||||
{
|
||||
return JsonDocument.Parse(File.ReadAllText(Path.Combine(GetParityFixtureRoot().FullName, "parity-fixture-matrix.json")));
|
||||
}
|
||||
|
||||
private static DirectoryInfo GetParityFixtureRoot()
|
||||
{
|
||||
DirectoryInfo repositoryRoot = FindRepositoryRoot();
|
||||
|
||||
return new DirectoryInfo(Path.Combine(repositoryRoot.FullName, "clients", "proto", "fixtures", "parity"));
|
||||
}
|
||||
|
||||
private static DirectoryInfo FindRepositoryRoot()
|
||||
{
|
||||
DirectoryInfo? current = new(AppContext.BaseDirectory);
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "AGENTS.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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user