diff --git a/libraries/MTConnect.NET-JSON-cppagent/Devices/JsonDevicesHeader.cs b/libraries/MTConnect.NET-JSON-cppagent/Devices/JsonDevicesHeader.cs
index 3d2a679fd..5e770d32b 100644
--- a/libraries/MTConnect.NET-JSON-cppagent/Devices/JsonDevicesHeader.cs
+++ b/libraries/MTConnect.NET-JSON-cppagent/Devices/JsonDevicesHeader.cs
@@ -15,6 +15,14 @@ public class JsonDevicesHeader
[JsonPropertyName("version")]
public string Version { get; set; }
+ ///
+ /// Header-nested schemaVersion identifies the AGENT's
+ /// configured MTConnect Standard release — what the data inside
+ /// the document refers to. It is distinct from the top-level
+ /// envelope schemaVersion, which identifies the document
+ /// schema the producer chose to emit. The two fields are
+ /// populated from independent sources and are not interchangeable.
+ ///
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; }
diff --git a/libraries/MTConnect.NET-JSON-cppagent/Devices/JsonMTConnectDevices.cs b/libraries/MTConnect.NET-JSON-cppagent/Devices/JsonMTConnectDevices.cs
index daad1d0dd..8de189687 100644
--- a/libraries/MTConnect.NET-JSON-cppagent/Devices/JsonMTConnectDevices.cs
+++ b/libraries/MTConnect.NET-JSON-cppagent/Devices/JsonMTConnectDevices.cs
@@ -10,6 +10,14 @@ public class JsonMTConnectDevices
[JsonPropertyName("jsonVersion")]
public int JsonVersion { get; set; }
+ ///
+ /// Top-level schemaVersion identifies the envelope schema
+ /// this DOCUMENT conforms to — the wire format the producer chose
+ /// to emit. It is distinct from Header.schemaVersion, which
+ /// identifies the AGENT's configured MTConnect Standard release
+ /// (what the data inside refers to). The two fields are populated
+ /// from independent sources and are not interchangeable.
+ ///
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; }
@@ -26,16 +34,16 @@ public class JsonMTConnectDevices
public JsonMTConnectDevices()
{
JsonVersion = 2;
- SchemaVersion = "2.0";
}
public JsonMTConnectDevices(IDevicesResponseDocument document)
{
JsonVersion = 2;
- SchemaVersion = "2.0";
if (document != null)
{
+ SchemaVersion = document.Version?.ToString();
+
Header = new JsonDevicesHeader(document.Header);
Devices = new JsonDevices(document);
diff --git a/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonMTConnectStreams.cs b/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonMTConnectStreams.cs
index e4359e847..b63047d30 100644
--- a/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonMTConnectStreams.cs
+++ b/libraries/MTConnect.NET-JSON-cppagent/Streams/JsonMTConnectStreams.cs
@@ -11,6 +11,14 @@ public class JsonMTConnectStreams
[JsonPropertyName("jsonVersion")]
public int JsonVersion { get; set; }
+ ///
+ /// Top-level schemaVersion identifies the envelope schema
+ /// this DOCUMENT conforms to — the wire format the producer chose
+ /// to emit. It is distinct from Header.schemaVersion, which
+ /// identifies the AGENT's configured MTConnect Standard release
+ /// (what the data inside refers to). The two fields are populated
+ /// from independent sources and are not interchangeable.
+ ///
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; }
@@ -21,19 +29,18 @@ public class JsonMTConnectStreams
public JsonStreams Streams { get; set; }
- public JsonMTConnectStreams()
+ public JsonMTConnectStreams()
{
JsonVersion = 2;
- SchemaVersion = "2.0";
}
public JsonMTConnectStreams(IStreamsResponseOutputDocument streamsDocument)
{
JsonVersion = 2;
- SchemaVersion = "2.0";
if (streamsDocument != null)
{
+ SchemaVersion = streamsDocument.Version?.ToString();
Header = new JsonStreamsHeader(streamsDocument.Header);
Streams = new JsonStreams(streamsDocument);
}
diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/Devices/JsonDataItemPropertyCasingTests.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/Devices/JsonDataItemPropertyCasingTests.cs
new file mode 100644
index 000000000..606b0cd4e
--- /dev/null
+++ b/tests/MTConnect.NET-JSON-cppagent-Tests/Devices/JsonDataItemPropertyCasingTests.cs
@@ -0,0 +1,67 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using MTConnect.Devices.Json;
+using NUnit.Framework;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json.Serialization;
+
+namespace MTConnect.NET_JSON_cppagent_Tests.Devices
+{
+ ///
+ /// Pins the cppagent JSON wire-shape casing convention for
+ /// : complex object members are PascalCase,
+ /// scalar attribute members are camelCase. The cppagent reference
+ /// implementation distinguishes the two so consumers can tell at a
+ /// glance whether a key carries a nested object or a scalar.
+ ///
+ [TestFixture]
+ [Category("WireShape")]
+ public class JsonDataItemPropertyCasingTests
+ {
+ // CLR property name -> expected JSON key.
+ private static readonly (string Clr, string Json)[] PascalCaseObjects = new[]
+ {
+ ("Source", "Source"),
+ ("Constraints", "Constraints"),
+ ("Filters", "Filters"),
+ ("Definition", "Definition"),
+ ("Relationships", "Relationships"),
+ };
+
+ private static readonly (string Clr, string Json)[] CamelCaseScalars = new[]
+ {
+ ("DataItemCategory", "category"),
+ ("Id", "id"),
+ ("Type", "type"),
+ };
+
+ private static string GetJsonName(string clrPropertyName)
+ {
+ var prop = typeof(JsonDataItem).GetProperty(
+ clrPropertyName,
+ BindingFlags.Public | BindingFlags.Instance);
+ Assert.That(prop, Is.Not.Null,
+ $"Property {clrPropertyName} must exist on JsonDataItem.");
+ var attribute = prop!.GetCustomAttribute();
+ Assert.That(attribute, Is.Not.Null,
+ $"Property {clrPropertyName} must carry a [JsonPropertyName] attribute.");
+ return attribute!.Name;
+ }
+
+ [TestCaseSource(nameof(PascalCaseObjects))]
+ public void Complex_object_property_uses_PascalCase_json_key((string Clr, string Json) entry)
+ {
+ Assert.That(GetJsonName(entry.Clr), Is.EqualTo(entry.Json),
+ "Complex object members on JsonDataItem must remain PascalCase to match the cppagent JSON wire shape.");
+ }
+
+ [TestCaseSource(nameof(CamelCaseScalars))]
+ public void Scalar_attribute_property_uses_camelCase_json_key((string Clr, string Json) entry)
+ {
+ Assert.That(GetJsonName(entry.Clr), Is.EqualTo(entry.Json),
+ "Scalar attribute members on JsonDataItem must remain camelCase to match the cppagent JSON wire shape.");
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/Devices/JsonMTConnectDevicesSchemaVersionTests.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/Devices/JsonMTConnectDevicesSchemaVersionTests.cs
new file mode 100644
index 000000000..61eb0d636
--- /dev/null
+++ b/tests/MTConnect.NET-JSON-cppagent-Tests/Devices/JsonMTConnectDevicesSchemaVersionTests.cs
@@ -0,0 +1,34 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using MTConnect.NET_JSON_cppagent_Tests.TestHelpers;
+using NUnit.Framework;
+using System;
+
+namespace MTConnect.NET_JSON_cppagent_Tests.Devices
+{
+ ///
+ /// Asserts that the cppagent JSON-MQTT Devices envelope emits the
+ /// configured MTConnect release as schemaVersion instead of
+ /// the hardcoded "2.0" literal.
+ ///
+ /// Pre-fix: every case fails with Expected "<version>" / But was "2.0".
+ /// Post-fix: every case passes; the wire output matches cppagent's
+ /// two-segment format (e.g. "2.5" for v2.5).
+ ///
+ [TestFixture]
+ [Category("SchemaVersionFromConfiguration")]
+ public class JsonMTConnectDevicesSchemaVersionTests
+ {
+ [TestCaseSource(typeof(VersionMatrix), nameof(VersionMatrix.All))]
+ public void Devices_envelope_schemaVersion_equals_configured_release(Version configured)
+ {
+ var envelope = EnvelopeFixtures.BuildDevicesEnvelope(configured);
+
+ Assert.That(
+ envelope.SchemaVersion,
+ Is.EqualTo(configured.ToString()),
+ "Devices.schemaVersion must mirror AgentConfiguration.DefaultVersion (issue #128).");
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/Regressions/Issue128_HardcodedLiteralGuardTests.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/Regressions/Issue128_HardcodedLiteralGuardTests.cs
new file mode 100644
index 000000000..d8d461c96
--- /dev/null
+++ b/tests/MTConnect.NET-JSON-cppagent-Tests/Regressions/Issue128_HardcodedLiteralGuardTests.cs
@@ -0,0 +1,50 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using MTConnect.NET_JSON_cppagent_Tests.TestHelpers;
+using NUnit.Framework;
+using System.IO;
+using System.Text.RegularExpressions;
+
+namespace MTConnect.NET_JSON_cppagent_Tests.Regressions
+{
+ ///
+ /// Guard test that walks the JSON-cppagent library source tree and
+ /// fails if either touched envelope file re-introduces a hardcoded
+ /// SchemaVersion = "<literal>" assignment. Catches
+ /// copy-paste regressions even when the parametric matrix would
+ /// stay green (e.g. someone hardcodes "2.5" and ships it).
+ ///
+ [TestFixture]
+ public class Issue128_HardcodedLiteralGuardTests
+ {
+ // SchemaVersion = ""; — string-literal assignment.
+ private static readonly Regex HardcodedSchemaVersion =
+ new(@"SchemaVersion\s*=\s*""[^""]+""\s*;", RegexOptions.Compiled);
+
+ [TestCase("Streams/JsonMTConnectStreams.cs")]
+ [TestCase("Devices/JsonMTConnectDevices.cs")]
+ public void Source_file_must_not_hardcode_schemaVersion_literal(string relativePath)
+ {
+ var librarySourceDir = LocateLibrarySourceDir();
+ var fullPath = Path.Combine(librarySourceDir, relativePath);
+
+ Assert.That(File.Exists(fullPath), Is.True,
+ $"expected to find library source at {fullPath} (test must run from a checked-out repo)");
+
+ var source = File.ReadAllText(fullPath);
+ var match = HardcodedSchemaVersion.Match(source);
+
+ Assert.That(match.Success, Is.False,
+ $"{relativePath} contains a hardcoded `SchemaVersion = \"...\"` literal: '{match.Value}'. " +
+ "Issue #128 forbids re-introducing the hardcode — derive the value from the response document.");
+ }
+
+ private static string LocateLibrarySourceDir()
+ {
+ // Test binary lives at .../tests/MTConnect.NET-JSON-cppagent-Tests/bin/Debug/net8.0/.
+ // Find the repo root via the shared sentinel walk, then descend into the library.
+ return Path.Combine(RepoRootLocator.LocateRoot(), "libraries", "MTConnect.NET-JSON-cppagent");
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/Regressions/Issue128_SchemaVersionConfiguredTests.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/Regressions/Issue128_SchemaVersionConfiguredTests.cs
new file mode 100644
index 000000000..365ff4e84
--- /dev/null
+++ b/tests/MTConnect.NET-JSON-cppagent-Tests/Regressions/Issue128_SchemaVersionConfiguredTests.cs
@@ -0,0 +1,36 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using MTConnect.NET_JSON_cppagent_Tests.TestHelpers;
+using NUnit.Framework;
+using System;
+
+namespace MTConnect.NET_JSON_cppagent_Tests.Regressions
+{
+ ///
+ /// Pinned regression for
+ /// issue #128.
+ ///
+ /// JSON-cppagent envelopes (MTConnectStreams + MTConnectDevices)
+ /// MUST emit the configured MTConnect release as schemaVersion;
+ /// the pre-fix code stamped a literal "2.0" regardless of
+ /// AgentConfiguration.DefaultVersion.
+ ///
+ [TestFixture]
+ public class Issue128_SchemaVersionConfiguredTests
+ {
+ [TestCaseSource(typeof(VersionMatrix), nameof(VersionMatrix.All))]
+ public void Streams_schemaVersion_equals_configured(Version configured)
+ {
+ var envelope = EnvelopeFixtures.BuildStreamsEnvelope(configured);
+ Assert.That(envelope.SchemaVersion, Is.EqualTo(configured.ToString()));
+ }
+
+ [TestCaseSource(typeof(VersionMatrix), nameof(VersionMatrix.All))]
+ public void Devices_schemaVersion_equals_configured(Version configured)
+ {
+ var envelope = EnvelopeFixtures.BuildDevicesEnvelope(configured);
+ Assert.That(envelope.SchemaVersion, Is.EqualTo(configured.ToString()));
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/Regressions/SchemaVersionFieldsCoexistTests.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/Regressions/SchemaVersionFieldsCoexistTests.cs
new file mode 100644
index 000000000..fee1d490c
--- /dev/null
+++ b/tests/MTConnect.NET-JSON-cppagent-Tests/Regressions/SchemaVersionFieldsCoexistTests.cs
@@ -0,0 +1,155 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System.IO;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using MTConnect.Devices.Json;
+using MTConnect.NET_JSON_cppagent_Tests.TestHelpers;
+using MTConnect.Streams.Json;
+using NUnit.Framework;
+
+namespace MTConnect.NET_JSON_cppagent_Tests.Regressions
+{
+ ///
+ /// Pins the envelope-vs-Header schemaVersion contract for the
+ /// cppagent JSON wire format. Both fields must exist independently
+ /// and continue to be wired through their own
+ /// :
+ ///
+ /// - JsonMTConnectStreams.SchemaVersion -> JSON key "schemaVersion" at envelope root
+ /// - JsonMTConnectDevices.SchemaVersion -> JSON key "schemaVersion" at envelope root
+ /// - JsonDevicesHeader.SchemaVersion -> JSON key "schemaVersion" nested inside Header
+ ///
+ /// The two SchemaVersion fields are populated from independent
+ /// sources and are NOT interchangeable — the envelope field
+ /// identifies the document schema the producer emitted while the
+ /// Header field identifies the agent's configured MTConnect
+ /// Standard release. Removing either field, or repointing one to
+ /// the other's source, would silently change the wire shape and
+ /// corrupt downstream consumers.
+ ///
+ /// The fixture also pins that each field carries an XML doc
+ /// comment that explains the envelope-vs-Header semantics so
+ /// future maintainers cannot accidentally collapse the two.
+ /// XML doc presence is enforced by reading the source file
+ /// (parsed XML doc files are not deployed alongside the assembly
+ /// in this repo).
+ ///
+ /// Sources:
+ /// - cppagent JSON envelope: https://github.com/mtconnect/cppagent
+ /// v2.7.0.7 reference printer emits both fields independently.
+ /// - Issue: https://github.com/TrakHound/MTConnect.NET/issues/128
+ ///
+ [TestFixture]
+ [Category("WireShape")]
+ public class SchemaVersionFieldsCoexistTests
+ {
+ [TestCase(typeof(JsonMTConnectStreams), "envelope")]
+ [TestCase(typeof(JsonMTConnectDevices), "envelope")]
+ [TestCase(typeof(JsonDevicesHeader), "Header")]
+ public void SchemaVersion_property_exists_with_camelCase_json_key(
+ System.Type carrier, string surface)
+ {
+ var prop = carrier.GetProperty(
+ "SchemaVersion", BindingFlags.Public | BindingFlags.Instance);
+
+ Assert.That(prop, Is.Not.Null,
+ $"`{carrier.Name}` ({surface}) must expose a `SchemaVersion` property; " +
+ "removing it silently regresses the cppagent JSON wire shape.");
+
+ var attribute = prop!.GetCustomAttribute();
+ Assert.That(attribute, Is.Not.Null,
+ $"`{carrier.Name}.SchemaVersion` must carry a [JsonPropertyName] attribute; " +
+ "the JSON key cannot be inferred from the property name alone.");
+ Assert.That(attribute!.Name, Is.EqualTo("schemaVersion"),
+ $"`{carrier.Name}.SchemaVersion` must serialize as the camelCase JSON key " +
+ $"`schemaVersion` (the cppagent wire-shape convention for scalar attributes).");
+ }
+
+ [Test]
+ public void Streams_envelope_and_devices_envelope_carry_independent_SchemaVersion_fields()
+ {
+ // Both envelopes have their own SchemaVersion. They are NOT
+ // shared via inheritance or composition, so a future refactor
+ // that consolidates them would also need to update this pin.
+ var streamsProp = typeof(JsonMTConnectStreams).GetProperty(
+ "SchemaVersion", BindingFlags.Public | BindingFlags.Instance);
+ var devicesProp = typeof(JsonMTConnectDevices).GetProperty(
+ "SchemaVersion", BindingFlags.Public | BindingFlags.Instance);
+
+ Assert.That(streamsProp, Is.Not.Null);
+ Assert.That(devicesProp, Is.Not.Null);
+ Assert.That(streamsProp!.DeclaringType, Is.EqualTo(typeof(JsonMTConnectStreams)),
+ "JsonMTConnectStreams.SchemaVersion must be declared on the Streams envelope itself, " +
+ "not inherited from a shared base — the field is wired from streamsDocument.Version.");
+ Assert.That(devicesProp!.DeclaringType, Is.EqualTo(typeof(JsonMTConnectDevices)),
+ "JsonMTConnectDevices.SchemaVersion must be declared on the Devices envelope itself, " +
+ "not inherited from a shared base — the field is wired from document.Version.");
+ }
+
+ [Test]
+ public void Devices_envelope_SchemaVersion_distinct_from_Header_SchemaVersion()
+ {
+ // The Devices envelope has its own SchemaVersion AND nests a
+ // Header which also has its own SchemaVersion. Both must
+ // coexist — collapsing them would conflate "what wire format
+ // did the producer emit" with "what Standard release does the
+ // agent run".
+ var envelopeProp = typeof(JsonMTConnectDevices).GetProperty(
+ "SchemaVersion", BindingFlags.Public | BindingFlags.Instance);
+ var headerProp = typeof(JsonDevicesHeader).GetProperty(
+ "SchemaVersion", BindingFlags.Public | BindingFlags.Instance);
+
+ Assert.That(envelopeProp, Is.Not.Null);
+ Assert.That(headerProp, Is.Not.Null);
+ Assert.That(envelopeProp!.DeclaringType, Is.Not.EqualTo(headerProp!.DeclaringType),
+ "Envelope and Header SchemaVersion fields must live on distinct types so " +
+ "they can be populated from independent sources.");
+ }
+
+ // The XML doc presence guard. Reads the committed source files
+ // (XML doc XML output is not deployed) so a future maintainer
+ // cannot delete the doc comments without tripping a guard.
+ [TestCase(
+ "libraries/MTConnect.NET-JSON-cppagent/Streams/JsonMTConnectStreams.cs",
+ "envelope")]
+ [TestCase(
+ "libraries/MTConnect.NET-JSON-cppagent/Devices/JsonMTConnectDevices.cs",
+ "envelope")]
+ [TestCase(
+ "libraries/MTConnect.NET-JSON-cppagent/Devices/JsonDevicesHeader.cs",
+ "Header")]
+ public void SchemaVersion_property_carries_envelope_vs_Header_xml_doc(
+ string relativeSourcePath, string surface)
+ {
+ var path = Path.Combine(RepoRootLocator.LocateRoot(), relativeSourcePath);
+ Assert.That(File.Exists(path), Is.True, $"Expected source file at '{path}'.");
+
+ var text = File.ReadAllText(path);
+
+ // Locate the SchemaVersion property and walk back to the doc
+ // comment that immediately precedes it. The doc must contain
+ // the words "envelope" AND "Header" so the contrast between
+ // the two surfaces stays explicit.
+ var anchor = text.IndexOf("public string SchemaVersion", System.StringComparison.Ordinal);
+ Assert.That(anchor, Is.GreaterThan(0),
+ $"`{relativeSourcePath}` must declare `public string SchemaVersion`.");
+
+ // Look at the 600 chars preceding the property declaration —
+ // doc comments are bounded by `/// ` tags.
+ var windowStart = System.Math.Max(0, anchor - 800);
+ var window = text.Substring(windowStart, anchor - windowStart);
+
+ Assert.That(window, Does.Contain("///"),
+ $"`{relativeSourcePath}` ({surface}): the `SchemaVersion` property must " +
+ "carry an XML doc comment explaining the envelope-vs-Header semantics.");
+ Assert.That(window.ToLowerInvariant(), Does.Contain("envelope"),
+ $"`{relativeSourcePath}` ({surface}): SchemaVersion XML doc must mention " +
+ "the word \"envelope\" so the contrast with the Header field is explicit.");
+ Assert.That(window, Does.Contain("Header"),
+ $"`{relativeSourcePath}` ({surface}): SchemaVersion XML doc must mention " +
+ "the word \"Header\" so the contrast with the envelope field is explicit.");
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/Streams/JsonMTConnectStreamsSchemaVersionTests.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/Streams/JsonMTConnectStreamsSchemaVersionTests.cs
new file mode 100644
index 000000000..87e2b4577
--- /dev/null
+++ b/tests/MTConnect.NET-JSON-cppagent-Tests/Streams/JsonMTConnectStreamsSchemaVersionTests.cs
@@ -0,0 +1,34 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using MTConnect.NET_JSON_cppagent_Tests.TestHelpers;
+using NUnit.Framework;
+using System;
+
+namespace MTConnect.NET_JSON_cppagent_Tests.Streams
+{
+ ///
+ /// Asserts that the cppagent JSON-MQTT Streams envelope emits the
+ /// configured MTConnect release as schemaVersion instead of
+ /// the hardcoded "2.0" literal.
+ ///
+ /// Pre-fix: every case fails with Expected "<version>" / But was "2.0".
+ /// Post-fix: every case passes; the wire output matches cppagent's
+ /// two-segment format (e.g. "2.5" for v2.5).
+ ///
+ [TestFixture]
+ [Category("SchemaVersionFromConfiguration")]
+ public class JsonMTConnectStreamsSchemaVersionTests
+ {
+ [TestCaseSource(typeof(VersionMatrix), nameof(VersionMatrix.All))]
+ public void Streams_envelope_schemaVersion_equals_configured_release(Version configured)
+ {
+ var envelope = EnvelopeFixtures.BuildStreamsEnvelope(configured);
+
+ Assert.That(
+ envelope.SchemaVersion,
+ Is.EqualTo(configured.ToString()),
+ "Streams.schemaVersion must mirror AgentConfiguration.DefaultVersion (issue #128).");
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/TestHelpers/EnvelopeFixtures.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/TestHelpers/EnvelopeFixtures.cs
new file mode 100644
index 000000000..9db8c56fa
--- /dev/null
+++ b/tests/MTConnect.NET-JSON-cppagent-Tests/TestHelpers/EnvelopeFixtures.cs
@@ -0,0 +1,53 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using MTConnect.Devices;
+using MTConnect.Devices.Json;
+using MTConnect.Headers;
+using MTConnect.Streams.Json;
+using MTConnect.Streams.Output;
+using System;
+
+namespace MTConnect.NET_JSON_cppagent_Tests.TestHelpers
+{
+ ///
+ /// Builds minimal Streams / Devices response documents tagged with
+ /// the supplied configured version, then runs them through the
+ /// JSON-cppagent envelope ctor under test.
+ ///
+ internal static class EnvelopeFixtures
+ {
+ public static JsonMTConnectStreams BuildStreamsEnvelope(Version configured)
+ {
+ var doc = new StreamsResponseOutputDocument
+ {
+ Header = new MTConnectStreamsHeader
+ {
+ InstanceId = 1,
+ Version = configured.ToString(),
+ Sender = "test",
+ },
+ Streams = Array.Empty(),
+ Version = configured,
+ };
+
+ return new JsonMTConnectStreams(doc);
+ }
+
+ public static JsonMTConnectDevices BuildDevicesEnvelope(Version configured)
+ {
+ var doc = new DevicesResponseDocument
+ {
+ Header = new MTConnectDevicesHeader
+ {
+ InstanceId = 1,
+ Version = configured.ToString(),
+ Sender = "test",
+ },
+ Version = configured,
+ };
+
+ return new JsonMTConnectDevices(doc);
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/TestHelpers/VersionMatrix.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/TestHelpers/VersionMatrix.cs
new file mode 100644
index 000000000..588a5ba41
--- /dev/null
+++ b/tests/MTConnect.NET-JSON-cppagent-Tests/TestHelpers/VersionMatrix.cs
@@ -0,0 +1,49 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace MTConnect.NET_JSON_cppagent_Tests.TestHelpers
+{
+ ///
+ /// Reflects every public static readonly Version field on
+ /// MTConnectVersions so the parametric test cases stay in
+ /// lock-step with the library's declared release matrix.
+ ///
+ internal static class VersionMatrix
+ {
+ public static IEnumerable All => Versions().Select(v => new TestCaseDataWrapper(v));
+
+ public static IEnumerable Versions()
+ {
+ var versionsType = typeof(MTConnectVersions);
+ var fields = versionsType.GetFields(BindingFlags.Public | BindingFlags.Static)
+ .Where(f => f.FieldType == typeof(Version));
+
+ foreach (var f in fields)
+ {
+ var value = (Version?)f.GetValue(null);
+ if (value != null)
+ {
+ yield return value;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Wraps a in NUnit's TestCaseData so the
+ /// failure / success message names the version.
+ ///
+ internal class TestCaseDataWrapper : NUnit.Framework.TestCaseData
+ {
+ public TestCaseDataWrapper(Version version) : base(version)
+ {
+ SetArgDisplayNames(version.ToString());
+ }
+ }
+}