Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c44932a
docs(testing): seed issue-128 writeup skeleton
ottobolyos Apr 25, 2026
400fb1f
docs(testing): scope the schemaVersion hardcode defect
ottobolyos Apr 25, 2026
097cc74
test(json-cppagent-tests): add red tests for streams schemaVersion
ottobolyos Apr 25, 2026
0c2d7ef
test(json-cppagent-tests): add red tests for devices schemaVersion
ottobolyos Apr 25, 2026
0b4b815
docs(testing): document red-test matrix
ottobolyos Apr 25, 2026
23c11df
fix(json-cppagent): thread schemaVersion into streams envelope
ottobolyos Apr 25, 2026
e26b961
fix(json-cppagent): thread schemaVersion into devices envelope
ottobolyos Apr 25, 2026
808de2a
docs(testing): document library fix
ottobolyos Apr 25, 2026
d185411
test(json-cppagent-tests): pin issue-128 schemaVersion regression
ottobolyos Apr 25, 2026
06cc028
test(json-cppagent-tests): guard against hardcoded schemaVersion literal
ottobolyos Apr 25, 2026
7241bae
docs(testing): document regression pins and guard test
ottobolyos Apr 25, 2026
c1115d5
docs(testing): record e2e deferral pending bootstrap merge
ottobolyos Apr 25, 2026
e8b4d2e
docs(testing): author campaign summary
ottobolyos Apr 25, 2026
53a7005
docs(testing): cross-reference phase writeups
ottobolyos Apr 25, 2026
03e228c
chore(docs): remove internal planning leak from committed tree
ottobolyos Apr 27, 2026
ed7bbae
test(json-cppagent-tests): finalise issue-128 review-pass guards
ottobolyos Apr 27, 2026
17034e8
test(json-cppagent-tests): pin envelope-vs-Header SchemaVersion (red)
ottobolyos Apr 27, 2026
c8a5889
docs(json-cppagent): document envelope-vs-Header SchemaVersion semantics
ottobolyos Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ public class JsonDevicesHeader
[JsonPropertyName("version")]
public string Version { get; set; }

/// <summary>
/// Header-nested <c>schemaVersion</c> identifies the AGENT's
/// configured MTConnect Standard release — what the data inside
/// the document refers to. It is distinct from the top-level
/// envelope <c>schemaVersion</c>, which identifies the document
/// schema the producer chose to emit. The two fields are
/// populated from independent sources and are not interchangeable.
/// </summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ public class JsonMTConnectDevices
[JsonPropertyName("jsonVersion")]
public int JsonVersion { get; set; }

/// <summary>
/// Top-level <c>schemaVersion</c> identifies the envelope schema
/// this DOCUMENT conforms to — the wire format the producer chose
/// to emit. It is distinct from <c>Header.schemaVersion</c>, 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.
/// </summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; }

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ public class JsonMTConnectStreams
[JsonPropertyName("jsonVersion")]
public int JsonVersion { get; set; }

/// <summary>
/// Top-level <c>schemaVersion</c> identifies the envelope schema
/// this DOCUMENT conforms to — the wire format the producer chose
/// to emit. It is distinct from <c>Header.schemaVersion</c>, 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.
/// </summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; }

Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Pins the cppagent JSON wire-shape casing convention for
/// <see cref="JsonDataItem"/>: 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.
/// </summary>
[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<JsonPropertyNameAttribute>();
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.");
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Asserts that the cppagent JSON-MQTT Devices envelope emits the
/// configured MTConnect release as <c>schemaVersion</c> instead of
/// the hardcoded <c>"2.0"</c> literal.
///
/// Pre-fix: every case fails with <c>Expected "&lt;version&gt;" / But was "2.0"</c>.
/// Post-fix: every case passes; the wire output matches cppagent's
/// two-segment format (e.g. <c>"2.5"</c> for v2.5).
/// </summary>
[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).");
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Guard test that walks the JSON-cppagent library source tree and
/// fails if either touched envelope file re-introduces a hardcoded
/// <c>SchemaVersion = "&lt;literal&gt;"</c> assignment. Catches
/// copy-paste regressions even when the parametric matrix would
/// stay green (e.g. someone hardcodes <c>"2.5"</c> and ships it).
/// </summary>
[TestFixture]
public class Issue128_HardcodedLiteralGuardTests
{
// SchemaVersion = "<anything>"; — 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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Pinned regression for
/// <see href="https://github.com/TrakHound/MTConnect.NET/issues/128">issue #128</see>.
///
/// JSON-cppagent envelopes (<c>MTConnectStreams</c> + <c>MTConnectDevices</c>)
/// MUST emit the configured MTConnect release as <c>schemaVersion</c>;
/// the pre-fix code stamped a literal <c>"2.0"</c> regardless of
/// <c>AgentConfiguration.DefaultVersion</c>.
/// </summary>
[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()));
}
}
}
Loading