Replace JoeAppEngine tags with TestChildObject tags (TestBool, TestInt, TestFloat, TestDouble, TestString, TestDateTime, and array variants) in Phase 4 and Phase 7 plans. These tags cover all TypedValue oneof cases for comprehensive v2 testing.
25 KiB
Phase 7: Integration Tests & Deployment — Implementation Plan
Date: 2026-03-21
Prerequisites: Phase 4 (Host complete) and Phase 6 (Client complete) both passing. All unit tests green.
Working Directory (Mac): /Users/dohertj2/Desktop/scadalink-design/lmxproxy
Working Directory (windev): C:\src\lmxproxy
windev SSH: ssh windev (alias configured in ~/.ssh/config, passwordless ed25519, user dohertj2)
Guardrails
- Never stop the v1 service until v2 is verified — deploy v2 on alternate ports first.
- Take a Veeam backup before cutover — provides rollback point.
- Integration tests run from Mac against windev — they use
Grpc.Net.Clientwhich is cross-platform. - All integration tests must pass before cutover.
- API keys: The existing
apikeys.jsonon windev is the source of truth for valid keys. Read it to get test keys. - Real MxAccess tags: Use the
TestChildObjecttags on windev's AVEVA System Platform instance. Available tags cover all TypedValue cases:TestChildObject.TestBool(bool)TestChildObject.TestInt(int)TestChildObject.TestFloat(float)TestChildObject.TestDouble(double)TestChildObject.TestString(string)TestChildObject.TestDateTime(datetime)TestChildObject.TestBoolArray[](bool array)TestChildObject.TestDateTimeArray[](datetime array)TestChildObject.TestDoubleArray[](double array)TestChildObject.TestFloatArray[](float array)TestChildObject.TestIntArray[](int array)TestChildObject.TestStringArray[](string array)
Step 1: Build Host on windev
1.1 Pull latest code
ssh windev "cd C:\src\lmxproxy && git pull"
If the repo doesn't exist on windev yet:
ssh windev "git clone https://gitea.dohertylan.com/dohertj2/lmxproxy.git C:\src\lmxproxy"
1.2 Publish Host binary
ssh windev "cd C:\src\lmxproxy && dotnet publish src/ZB.MOM.WW.LmxProxy.Host -c Release -r win-x86 --self-contained false -o C:\publish-v2\"
Expected output: C:\publish-v2\ZB.MOM.WW.LmxProxy.Host.exe plus dependencies.
1.3 Create v2 appsettings.json
Create C:\publish-v2\appsettings.json configured for testing on alternate ports:
ssh windev "powershell -Command \"@'
{
\"GrpcPort\": 50052,
\"ApiKeyConfigFile\": \"apikeys.json\",
\"Connection\": {
\"MonitorIntervalSeconds\": 5,
\"ConnectionTimeoutSeconds\": 30,
\"ReadTimeoutSeconds\": 5,
\"WriteTimeoutSeconds\": 5,
\"MaxConcurrentOperations\": 10,
\"AutoReconnect\": true
},
\"Subscription\": {
\"ChannelCapacity\": 1000,
\"ChannelFullMode\": \"DropOldest\"
},
\"HealthCheck\": {
\"Enabled\": true,
\"TestTagAddress\": \"TestChildObject.TestBool\",
\"MaxStaleDataMinutes\": 5
},
\"Tls\": {
\"Enabled\": false
},
\"WebServer\": {
\"Enabled\": true,
\"Port\": 8081
},
\"Serilog\": {
\"MinimumLevel\": {
\"Default\": \"Information\",
\"Override\": {
\"Microsoft\": \"Warning\",
\"System\": \"Warning\",
\"Grpc\": \"Information\"
}
},
\"WriteTo\": [
{ \"Name\": \"Console\" },
{
\"Name\": \"File\",
\"Args\": {
\"path\": \"logs/lmxproxy-v2-.txt\",
\"rollingInterval\": \"Day\",
\"retainedFileCountLimit\": 30
}
}
]
}
}
'@ | Set-Content -Path 'C:\publish-v2\appsettings.json' -Encoding UTF8\""
Key differences from production config: gRPC port is 50052 (not 50051), web port is 8081 (not 8080), log file prefix is lmxproxy-v2-.
1.4 Copy apikeys.json
If v2 should use the same API keys as v1:
ssh windev "copy C:\publish\apikeys.json C:\publish-v2\apikeys.json"
If C:\publish\apikeys.json doesn't exist (the v2 service will auto-generate one on first start):
ssh windev "if not exist C:\publish\apikeys.json echo No existing apikeys.json - v2 will auto-generate"
1.5 Verify the publish directory
ssh windev "dir C:\publish-v2\ZB.MOM.WW.LmxProxy.Host.exe && dir C:\publish-v2\appsettings.json"
Step 2: Deploy v2 Host Service
2.1 Install as a separate Topshelf service
The v2 service runs alongside v1 on different ports. Install with a distinct service name:
ssh windev "C:\publish-v2\ZB.MOM.WW.LmxProxy.Host.exe install -servicename \"ZB.MOM.WW.LmxProxy.Host.V2\" -displayname \"SCADA Bridge LMX Proxy V2\" -description \"LmxProxy v2 gRPC service (test deployment)\" --autostart"
2.2 Start the v2 service
ssh windev "sc start ZB.MOM.WW.LmxProxy.Host.V2"
2.3 Wait 10 seconds for startup, then verify
ssh windev "timeout /t 10 /nobreak >nul && sc query ZB.MOM.WW.LmxProxy.Host.V2"
Expected: STATE: 4 RUNNING.
2.4 Verify status page
From Mac, use curl to check the v2 status page:
curl -s http://10.100.0.48:8081/ | head -20
Expected: HTML containing "LmxProxy Status Dashboard".
curl -s http://10.100.0.48:8081/api/health
Expected: OK with HTTP 200.
curl -s http://10.100.0.48:8081/api/status | python3 -m json.tool | head -30
Expected: JSON with serviceName, connection.isConnected: true, version info.
2.5 Verify MxAccess connected
The status page should show MxAccess Connection: Connected. If it shows Disconnected, check the logs:
ssh windev "type C:\publish-v2\logs\lmxproxy-v2-*.txt | findstr /i \"error\""
2.6 Read the apikeys.json to get test keys
ssh windev "type C:\publish-v2\apikeys.json"
Record the ReadWrite and ReadOnly API keys for use in integration tests. Example structure:
{
"Keys": [
{ "Key": "abc123...", "Role": "ReadWrite", "Description": "Default ReadWrite key" },
{ "Key": "def456...", "Role": "ReadOnly", "Description": "Default ReadOnly key" }
]
}
Step 3: Create Integration Test Project
3.1 Create project
On windev (or Mac — the test project is .NET 10 and cross-platform):
cd /Users/dohertj2/Desktop/scadalink-design/lmxproxy
dotnet new xunit -n ZB.MOM.WW.LmxProxy.Client.IntegrationTests -o tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests --framework net10.0
3.2 Configure csproj
File: tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxProxy.Client\ZB.MOM.WW.LmxProxy.Client.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.test.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
3.3 Add to solution
Edit ZB.MOM.WW.LmxProxy.slnx:
<Solution>
<Folder Name="/src/">
<Project Path="src/ZB.MOM.WW.LmxProxy.Host/ZB.MOM.WW.LmxProxy.Host.csproj" />
<Project Path="src/ZB.MOM.WW.LmxProxy.Client/ZB.MOM.WW.LmxProxy.Client.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ZB.MOM.WW.LmxProxy.Host.Tests/ZB.MOM.WW.LmxProxy.Host.Tests.csproj" />
<Project Path="tests/ZB.MOM.WW.LmxProxy.Client.Tests/ZB.MOM.WW.LmxProxy.Client.Tests.csproj" />
<Project Path="tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests.csproj" />
</Folder>
</Solution>
3.4 Create test configuration
File: tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/appsettings.test.json
{
"LmxProxy": {
"Host": "10.100.0.48",
"Port": 50052,
"ReadWriteApiKey": "REPLACE_WITH_ACTUAL_KEY",
"ReadOnlyApiKey": "REPLACE_WITH_ACTUAL_KEY",
"InvalidApiKey": "invalid-key-that-does-not-exist"
}
}
IMPORTANT: After reading the actual apikeys.json from windev in Step 2.6, replace the placeholder values with the real keys.
3.5 Create test base class
File: tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/IntegrationTestBase.cs
using Microsoft.Extensions.Configuration;
using ZB.MOM.WW.LmxProxy.Client;
namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests;
public abstract class IntegrationTestBase : IAsyncLifetime
{
protected IConfiguration Configuration { get; }
protected string Host { get; }
protected int Port { get; }
protected string ReadWriteApiKey { get; }
protected string ReadOnlyApiKey { get; }
protected string InvalidApiKey { get; }
protected LmxProxyClient? Client { get; set; }
protected IntegrationTestBase()
{
Configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.test.json")
.Build();
var section = Configuration.GetSection("LmxProxy");
Host = section["Host"] ?? "10.100.0.48";
Port = int.Parse(section["Port"] ?? "50052");
ReadWriteApiKey = section["ReadWriteApiKey"] ?? throw new Exception("ReadWriteApiKey not configured");
ReadOnlyApiKey = section["ReadOnlyApiKey"] ?? throw new Exception("ReadOnlyApiKey not configured");
InvalidApiKey = section["InvalidApiKey"] ?? "invalid-key";
}
protected LmxProxyClient CreateClient(string? apiKey = null)
{
return new LmxProxyClientBuilder()
.WithHost(Host)
.WithPort(Port)
.WithApiKey(apiKey ?? ReadWriteApiKey)
.WithTimeout(TimeSpan.FromSeconds(10))
.WithRetryPolicy(2, TimeSpan.FromSeconds(1))
.WithMetrics()
.Build();
}
public virtual async Task InitializeAsync()
{
Client = CreateClient();
await Client.ConnectAsync();
}
public virtual async Task DisposeAsync()
{
if (Client is not null)
{
await Client.DisconnectAsync();
Client.Dispose();
}
}
}
Step 4: Integration Test Scenarios
4.1 Connection Lifecycle
File: tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ConnectionTests.cs
namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests;
public class ConnectionTests : IntegrationTestBase
{
[Fact]
public async Task ConnectAndDisconnect_Succeeds()
{
// Client is connected in InitializeAsync
Assert.True(await Client!.IsConnectedAsync());
await Client.DisconnectAsync();
Assert.False(await Client.IsConnectedAsync());
}
[Fact]
public async Task ConnectWithInvalidApiKey_Fails()
{
using var badClient = CreateClient(InvalidApiKey);
// Expect RpcException with StatusCode.Unauthenticated
var ex = await Assert.ThrowsAsync<Grpc.Core.RpcException>(
() => badClient.ConnectAsync());
Assert.Equal(Grpc.Core.StatusCode.Unauthenticated, ex.StatusCode);
}
[Fact]
public async Task DoubleConnect_IsIdempotent()
{
await Client!.ConnectAsync(); // Already connected — should be no-op
Assert.True(await Client.IsConnectedAsync());
}
}
4.2 Read Tests
File: tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ReadTests.cs
namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests;
public class ReadTests : IntegrationTestBase
{
[Fact]
public async Task Read_BoolTag_ReturnsBoolValue()
{
var vtq = await Client!.ReadAsync("TestChildObject.TestBool");
Assert.IsType<bool>(vtq.Value);
Assert.True(vtq.Quality.IsGood());
}
[Fact]
public async Task Read_IntTag_ReturnsIntValue()
{
var vtq = await Client!.ReadAsync("TestChildObject.TestInt");
Assert.True(vtq.Value is int or long);
Assert.True(vtq.Quality.IsGood());
}
[Fact]
public async Task Read_FloatTag_ReturnsFloatValue()
{
var vtq = await Client!.ReadAsync("TestChildObject.TestFloat");
Assert.True(vtq.Value is float or double);
Assert.True(vtq.Quality.IsGood());
}
[Fact]
public async Task Read_DoubleTag_ReturnsDoubleValue()
{
var vtq = await Client!.ReadAsync("TestChildObject.TestDouble");
Assert.IsType<double>(vtq.Value);
Assert.True(vtq.Quality.IsGood());
}
[Fact]
public async Task Read_StringTag_ReturnsStringValue()
{
var vtq = await Client!.ReadAsync("TestChildObject.TestString");
Assert.IsType<string>(vtq.Value);
Assert.True(vtq.Quality.IsGood());
}
[Fact]
public async Task Read_DateTimeTag_ReturnsDateTimeValue()
{
var vtq = await Client!.ReadAsync("TestChildObject.TestDateTime");
Assert.IsType<DateTime>(vtq.Value);
Assert.True(vtq.Quality.IsGood());
Assert.True(DateTime.UtcNow - vtq.Timestamp < TimeSpan.FromHours(1));
}
[Fact]
public async Task ReadBatch_MultiplesTags_ReturnsDictionary()
{
var tags = new[] { "TestChildObject.TestString", "TestChildObject.TestString" };
var results = await Client!.ReadBatchAsync(tags);
Assert.Equal(2, results.Count);
Assert.True(results.ContainsKey("TestChildObject.TestString"));
Assert.True(results.ContainsKey("TestChildObject.TestString"));
}
[Fact]
public async Task Read_NonexistentTag_ReturnsBadQuality()
{
// Reading a tag that doesn't exist should return Bad quality
// (or throw — depends on Host implementation. Adjust assertion accordingly.)
var vtq = await Client!.ReadAsync("NonExistent.Tag.12345");
// If the Host returns success=false, ReadAsync will throw.
// If it returns success=true with bad quality, check quality.
// Adjust based on actual behavior.
}
}
4.3 Write Tests
File: tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteTests.cs
using ZB.MOM.WW.LmxProxy.Client.Domain;
namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests;
public class WriteTests : IntegrationTestBase
{
[Fact]
public async Task WriteAndReadBack_StringValue()
{
string testValue = $"IntTest-{DateTime.UtcNow:HHmmss}";
// Write to a writable string tag
await Client!.WriteAsync("TestChildObject.TestString",
new TypedValue { StringValue = testValue });
// Read back and verify
await Task.Delay(500); // Allow time for write to propagate
var vtq = await Client.ReadAsync("TestChildObject.TestString");
Assert.Equal(testValue, vtq.Value);
}
[Fact]
public async Task WriteWithReadOnlyKey_ThrowsPermissionDenied()
{
using var readOnlyClient = CreateClient(ReadOnlyApiKey);
await readOnlyClient.ConnectAsync();
var ex = await Assert.ThrowsAsync<Grpc.Core.RpcException>(
() => readOnlyClient.WriteAsync("TestChildObject.TestString",
new TypedValue { StringValue = "should-fail" }));
Assert.Equal(Grpc.Core.StatusCode.PermissionDenied, ex.StatusCode);
}
}
4.4 Subscribe Tests
File: tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/SubscribeTests.cs
namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests;
public class SubscribeTests : IntegrationTestBase
{
[Fact]
public async Task Subscribe_ReceivesUpdates()
{
var received = new List<(string Tag, Vtq Vtq)>();
var receivedEvent = new TaskCompletionSource<bool>();
var subscription = await Client!.SubscribeAsync(
new[] { "TestChildObject.TestInt" },
(tag, vtq) =>
{
received.Add((tag, vtq));
if (received.Count >= 3)
receivedEvent.TrySetResult(true);
},
ex => receivedEvent.TrySetException(ex));
// Wait up to 30 seconds for at least 3 updates
var completed = await Task.WhenAny(receivedEvent.Task, Task.Delay(TimeSpan.FromSeconds(30)));
subscription.Dispose();
Assert.True(received.Count >= 1, $"Expected at least 1 update, got {received.Count}");
// Verify the VTQ has correct structure
var first = received[0];
Assert.Equal("TestChildObject.TestInt", first.Tag);
Assert.NotNull(first.Vtq.Value);
// ScanTime should be a DateTime value
Assert.True(first.Vtq.Timestamp > DateTime.MinValue);
}
}
4.5 WriteBatchAndWait Tests
File: tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteBatchAndWaitTests.cs
using ZB.MOM.WW.LmxProxy.Client.Domain;
namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests;
public class WriteBatchAndWaitTests : IntegrationTestBase
{
[Fact]
public async Task WriteBatchAndWait_TypeAwareComparison()
{
// This test requires a writable tag and a flag tag.
// Adjust tag names based on available tags in TestChildObject.
// Example: write values and poll a flag.
var values = new Dictionary<string, TypedValue>
{
["TestChildObject.TestString"] = new TypedValue { StringValue = "BatchTest" }
};
// Poll the same tag we wrote to (simple self-check)
var response = await Client!.WriteBatchAndWaitAsync(
values,
flagTag: "TestChildObject.TestString",
flagValue: new TypedValue { StringValue = "BatchTest" },
timeoutMs: 5000,
pollIntervalMs: 200);
Assert.True(response.Success);
Assert.True(response.FlagReached);
Assert.True(response.ElapsedMs < 5000);
}
}
4.6 CheckApiKey Tests
File: tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/CheckApiKeyTests.cs
namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests;
public class CheckApiKeyTests : IntegrationTestBase
{
[Fact]
public async Task CheckApiKey_ValidReadWrite_ReturnsValid()
{
var info = await Client!.CheckApiKeyAsync(ReadWriteApiKey);
Assert.True(info.IsValid);
}
[Fact]
public async Task CheckApiKey_ValidReadOnly_ReturnsValid()
{
var info = await Client!.CheckApiKeyAsync(ReadOnlyApiKey);
Assert.True(info.IsValid);
}
[Fact]
public async Task CheckApiKey_Invalid_ReturnsInvalid()
{
var info = await Client!.CheckApiKeyAsync("totally-invalid-key-12345");
Assert.False(info.IsValid);
}
}
Step 5: Run Integration Tests
5.1 Build the test project (from Mac)
cd /Users/dohertj2/Desktop/scadalink-design/lmxproxy
dotnet build tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests
5.2 Run integration tests against v2 on alternate port
dotnet test tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests --verbosity normal
All tests should pass against 10.100.0.48:50052.
5.3 Debug failures
If tests fail, check:
- v2 service is running:
ssh windev "sc query ZB.MOM.WW.LmxProxy.Host.V2" - v2 service logs:
ssh windev "type C:\publish-v2\logs\lmxproxy-v2-*.txt | findstr /i error" - Network connectivity:
curl -s http://10.100.0.48:8081/api/health - API keys match:
ssh windev "type C:\publish-v2\apikeys.json"
5.4 Verify metrics after test run
curl -s http://10.100.0.48:8081/api/status | python3 -m json.tool
Should show non-zero operation counts for Read, ReadBatch, Write, etc.
Step 6: Cutover
Only proceed if ALL integration tests pass.
6.1 Stop v1 service
ssh windev "sc stop ZB.MOM.WW.LmxProxy.Host"
Verify stopped:
ssh windev "sc query ZB.MOM.WW.LmxProxy.Host"
Expected: STATE: 1 STOPPED.
6.2 Stop v2 service
ssh windev "sc stop ZB.MOM.WW.LmxProxy.Host.V2"
6.3 Reconfigure v2 to production ports
Update C:\publish-v2\appsettings.json:
- Change
GrpcPortfrom50052to50051 - Change
WebServer.Portfrom8081to8080 - Change log file prefix from
lmxproxy-v2-tolmxproxy-
ssh windev "powershell -Command \"(Get-Content 'C:\publish-v2\appsettings.json') -replace '50052','50051' -replace '8081','8080' -replace 'lmxproxy-v2-','lmxproxy-' | Set-Content 'C:\publish-v2\appsettings.json'\""
6.4 Uninstall v1 service
ssh windev "C:\publish\ZB.MOM.WW.LmxProxy.Host.exe uninstall -servicename \"ZB.MOM.WW.LmxProxy.Host\""
6.5 Uninstall v2 test service and reinstall as production service
ssh windev "C:\publish-v2\ZB.MOM.WW.LmxProxy.Host.exe uninstall -servicename \"ZB.MOM.WW.LmxProxy.Host.V2\""
ssh windev "C:\publish-v2\ZB.MOM.WW.LmxProxy.Host.exe install -servicename \"ZB.MOM.WW.LmxProxy.Host\" -displayname \"SCADA Bridge LMX Proxy\" -description \"LmxProxy v2 gRPC service\" --autostart"
6.6 Start the production service
ssh windev "sc start ZB.MOM.WW.LmxProxy.Host"
6.7 Verify on production ports
ssh windev "timeout /t 10 /nobreak >nul && sc query ZB.MOM.WW.LmxProxy.Host"
Expected: STATE: 4 RUNNING.
curl -s http://10.100.0.48:8080/api/health
Expected: OK.
curl -s http://10.100.0.48:8080/api/status | python3 -m json.tool | head -15
Expected: Connected, version shows v2.
6.8 Update test configuration and re-run integration tests
Update tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/appsettings.test.json:
- Change
Portfrom50052to50051
dotnet test tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests --verbosity normal
All tests should pass on the production port.
6.9 Configure service recovery
ssh windev "sc failure ZB.MOM.WW.LmxProxy.Host reset= 86400 actions= restart/60000/restart/300000/restart/600000"
This configures: restart after 1 min on first failure, 5 min on second, 10 min on subsequent. Reset counter after 1 day (86400 seconds).
Step 7: Documentation Updates
7.1 Update windev.md
Add a section about the LmxProxy v2 service to /Users/dohertj2/Desktop/scadalink-design/windev.md:
## LmxProxy v2
| Field | Value |
|---|---|
| Service Name | ZB.MOM.WW.LmxProxy.Host |
| Display Name | SCADA Bridge LMX Proxy |
| gRPC Port | 50051 |
| Status Page | http://10.100.0.48:8080/ |
| Health Endpoint | http://10.100.0.48:8080/api/health |
| Publish Directory | C:\publish-v2\ |
| API Keys | C:\publish-v2\apikeys.json |
| Logs | C:\publish-v2\logs\ |
| Protocol | v2 (TypedValue + QualityCode) |
7.2 Update lmxproxy CLAUDE.md
If lmxproxy/CLAUDE.md references v1 behavior, update:
- Change "currently v1 protocol" references to "v2 protocol"
- Update publish directory references from
C:\publish\toC:\publish-v2\ - Update any value conversion notes (no more string heuristics)
7.3 Clean up v1 publish directory (optional)
ssh windev "if exist C:\publish\ ren C:\publish publish-v1-backup"
Step 8: Veeam Backup
8.1 Take incremental backup
ssh dohertj2@10.100.0.30 "powershell -Command \"Add-PSSnapin VeeamPSSnapin; Connect-VBRServer -Server localhost; Start-VBRJob -Job (Get-VBRJob -Name 'Backup WW_DEV_VM')\""
8.2 Wait for backup to complete (check status)
ssh dohertj2@10.100.0.30 "powershell -Command \"Add-PSSnapin VeeamPSSnapin; Connect-VBRServer -Server localhost; (Get-VBRJob -Name 'Backup WW_DEV_VM').FindLastSession() | Select-Object State, Result, CreationTime, EndTime\""
Expected: State: Stopped, Result: Success.
8.3 Get the restore point ID
ssh dohertj2@10.100.0.30 "powershell -Command \"Add-PSSnapin VeeamPSSnapin; Connect-VBRServer -Server localhost; Get-VBRRestorePoint -Backup (Get-VBRBackup -Name 'Backup WW_DEV_VM') | Select-Object Id, CreationTime, Type, @{N='SizeGB';E={[math]::Round(\`$_.ApproxSize/1GB,2)}} | Format-Table -AutoSize\""
8.4 Record in windev.md
Add a new row to the Restore Points table in windev.md:
| `XXXXXXXX` | 2026-XX-XX XX:XX | Increment | **Post-v2 deployment** — LmxProxy v2 live on port 50051 |
Replace placeholders with actual restore point ID and timestamp.
Completion Criteria
- v2 Host binary published to
C:\publish-v2\on windev - v2 service installed and running on alternate ports (50052/8081) — verified via status page
- Integration test project created at
tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ - All integration tests pass against v2 on alternate ports:
- Connect/disconnect lifecycle
- Read string tag
TestChildObject.TestString— value "JoeDev", Good quality - Read writable tag
TestChildObject.TestString - Write string then read-back verification
- ReadBatch multiple tags
- Subscribe to
TestChildObject.TestInt— verify updates received with TypedValue + QualityCode - WriteBatchAndWait with type-aware flag comparison
- CheckApiKey — valid ReadWrite, valid ReadOnly, invalid
- Write with ReadOnly key — PermissionDenied
- Connect with invalid API key — Unauthenticated
- v1 service stopped and uninstalled
- v2 service reconfigured to production ports (50051/8080) and reinstalled
- All integration tests pass on production ports
- Service recovery configured (restart on failure)
windev.mdupdated with v2 service detailslmxproxy/CLAUDE.mdupdated for v2- Veeam backup taken and restore point ID recorded in
windev.md - v1 publish directory backed up or removed