# SuiteLink Tag Client Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Build a cross-platform `.NET 10` C# SuiteLink V2 client for tag-only operations: connect, subscribe, receive updates, write values, and unsubscribe for `bool`, `int32`, `float32`, and `string`. **Architecture:** The implementation is split into transport, protocol codec, session state, and public client API layers. The first version targets normal non-encrypted SuiteLink tag traffic only and validates behavior with unit tests, golden packet tests, and optional live integration tests against an AVEVA/OI server in mixed or legacy mode. **Tech Stack:** .NET 10, C#, xUnit, `TcpClient`/`NetworkStream`, `System.Buffers`, `System.Buffers.Binary` --- ### Task 1: Create Solution Skeleton **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLink.Client.csproj` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Class1.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLink.Client.Tests.csproj` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/UnitTest1.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln` **Step 1: Write the failing structure check** Create a test project with one placeholder test: ```csharp using Xunit; namespace SuiteLink.Client.Tests; public sealed class UnitTest1 { [Fact] public void Placeholder() { Assert.True(true); } } ``` **Step 2: Run test to verify the solution builds** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln` Expected: PASS with one test executed **Step 3: Write minimal implementation** Create the library and solution structure only. Leave the default library type empty or minimal. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln /Users/dohertj2/Desktop/suitelinkclient/src /Users/dohertj2/Desktop/suitelinkclient/tests git commit -m "chore: scaffold suitelink client solution" ``` ### Task 2: Define Public Value And Option Models **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkConnectionOptions.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkValue.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkTagUpdate.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SubscriptionHandle.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkValueTests.cs` **Step 1: Write the failing test** ```csharp using Xunit; namespace SuiteLink.Client.Tests; public sealed class SuiteLinkValueTests { [Fact] public void BoolFactory_CreatesBoolValue() { var value = SuiteLinkValue.FromBoolean(true); Assert.True(value.TryGetBoolean(out var result)); Assert.True(result); } } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter BoolFactory_CreatesBoolValue` Expected: FAIL with missing type or method errors **Step 3: Write minimal implementation** Implement: - immutable `SuiteLinkConnectionOptions` - `SuiteLinkValue` discriminated wrapper for `bool`, `int`, `float`, `string` - `SuiteLinkTagUpdate` - `SubscriptionHandle` placeholder with async disposal hook Minimal `SuiteLinkValue` pattern: ```csharp public enum SuiteLinkValueKind { Boolean, Int32, Float32, String } ``` **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkValueTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkValueTests.cs git commit -m "feat: add public suitelink value models" ``` ### Task 3: Add Frame Reader And Writer **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkFrame.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkFrameWriter.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkFrameReader.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkFrameWriterTests.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkFrameReaderTests.cs` **Step 1: Write the failing test** ```csharp using Xunit; namespace SuiteLink.Client.Tests.Protocol; public sealed class SuiteLinkFrameWriterTests { [Fact] public void WriteFrame_AppendsLengthTypeAndMarker() { var bytes = SuiteLinkFrameWriter.WriteFrame(0x2440, []); Assert.Equal(5, bytes.Length); Assert.Equal(0x03, bytes[0]); Assert.Equal(0x00, bytes[1]); Assert.Equal(0x40, bytes[2]); Assert.Equal(0x24, bytes[3]); Assert.Equal(0xA5, bytes[4]); } } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter WriteFrame_AppendsLengthTypeAndMarker` Expected: FAIL with missing frame writer **Step 3: Write minimal implementation** Implement: - `SuiteLinkFrame` record holding message type and payload span or byte array - frame writer for normal SuiteLink messages - frame reader that: - reads two-byte remaining length - reads remaining bytes plus trailing marker - validates final `0xA5` **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkFrame` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol git commit -m "feat: add suitelink frame reader and writer" ``` ### Task 4: Add UTF-16LE And Primitive Wire Encoding Helpers **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkEncoding.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkEncodingTests.cs` **Step 1: Write the failing test** ```csharp using Xunit; namespace SuiteLink.Client.Tests.Protocol; public sealed class SuiteLinkEncodingTests { [Fact] public void EncodeLengthPrefixedUtf16_WritesCharacterCountThenUtf16Bytes() { var bytes = SuiteLinkEncoding.EncodeLengthPrefixedUtf16("AB"); Assert.Equal(1 + 4, bytes.Length); Assert.Equal(2, bytes[0]); Assert.Equal((byte)'A', bytes[1]); } } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter EncodeLengthPrefixedUtf16_WritesCharacterCountThenUtf16Bytes` Expected: FAIL **Step 3: Write minimal implementation** Implement helper methods for: - one-byte-length-prefixed UTF-16LE strings - null-terminated UTF-16LE strings - little-endian primitive reads/writes - FILETIME conversion helper if needed for future time messages **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkEncodingTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkEncoding.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkEncodingTests.cs git commit -m "feat: add suitelink encoding helpers" ``` ### Task 5: Encode Handshake And Connect Messages **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkHandshakeCodec.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkConnectCodec.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkHandshakeCodecTests.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkConnectCodecTests.cs` **Step 1: Write the failing test** Use known bytes captured from the reverse-engineered protocol and assert: ```csharp [Fact] public void EncodeConnect_WritesConnectMessageType() { var options = new SuiteLinkConnectionOptions( host: "127.0.0.1", application: "App", topic: "Topic", clientName: "Client", clientNode: "Node", userName: "User", serverNode: "Server"); var bytes = SuiteLinkConnectCodec.Encode(options); Assert.Equal(0x80, bytes[2]); Assert.Equal(0x01, bytes[3]); Assert.Equal(0xA5, bytes[^1]); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter EncodeConnect_WritesConnectMessageType` Expected: FAIL **Step 3: Write minimal implementation** Implement: - normal SuiteLink handshake encoder - handshake acknowledgement parser - connect encoder using the observed field order - isolate unknown fixed bytes in constants with comments pointing to capture evidence **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter CodecTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol git commit -m "feat: encode suitelink handshake and connect" ``` ### Task 6: Encode Advise And Unadvise Messages **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkSubscriptionCodec.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkSubscriptionCodecTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public void EncodeAdvise_WritesTagNameInUtf16() { var bytes = SuiteLinkSubscriptionCodec.EncodeAdvise("Pump001.Run"); Assert.Equal(0x80, bytes[2]); Assert.Equal(0x10, bytes[3]); Assert.Equal(0xA5, bytes[^1]); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter EncodeAdvise_WritesTagNameInUtf16` Expected: FAIL **Step 3: Write minimal implementation** Implement: - `EncodeAdvise(string itemName)` - `EncodeUnadvise(uint tagId)` - `DecodeAdviseAck(ReadOnlySpan)` Add a small result model for advise acknowledgements that captures `tagId`. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkSubscriptionCodecTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkSubscriptionCodecTests.cs git commit -m "feat: add advise and unadvise codec support" ``` ### Task 7: Decode Update Messages For Primitive Types **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkUpdateCodec.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkWireValueType.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkUpdateCodecTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public void DecodeUpdate_DecodesIntegerValue() { var frame = new byte[] { 0x0D, 0x00, 0x00, 0x09, 0x34, 0x12, 0x00, 0x00, 0x01, 0x00, 0xC0, 0x00, 0x02, 0x2A, 0x00, 0x00, 0x00, 0xA5 }; var update = SuiteLinkUpdateCodec.Decode(frame); Assert.Equal(0x1234u, update.TagId); Assert.True(update.Value.TryGetInt32(out var value)); Assert.Equal(42, value); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter DecodeUpdate_DecodesIntegerValue` Expected: FAIL **Step 3: Write minimal implementation** Implement decoding for: - binary to `bool` - integer to `int` - real to `float` - message to `string` Return a parsed update model containing: - `TagId` - `Quality` - `ElapsedMilliseconds` - `SuiteLinkValue` **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkUpdateCodecTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkUpdateCodecTests.cs git commit -m "feat: decode primitive suitelink update values" ``` ### Task 8: Encode Poke Messages For Primitive Writes **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol/SuiteLinkWriteCodec.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkWriteCodecTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public void EncodeWrite_Int32Value_WritesPokeMessage() { var bytes = SuiteLinkWriteCodec.Encode(0x1234, SuiteLinkValue.FromInt32(42)); Assert.Equal(0x08, bytes[2]); Assert.Equal(0x0B, bytes[3]); Assert.Equal(0xA5, bytes[^1]); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter EncodeWrite_Int32Value_WritesPokeMessage` Expected: FAIL **Step 3: Write minimal implementation** Implement primitive write encoding for: - `bool` - `int32` - `float32` - `string` if confirmed by packet format used for wire message values Reject unsupported value kinds with a clear exception. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkWriteCodecTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Protocol /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/SuiteLinkWriteCodecTests.cs git commit -m "feat: encode primitive suitelink writes" ``` ### Task 9: Implement Session State And Tag Mapping **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSessionState.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSession.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Internal/SuiteLinkSessionTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public void RegisterSubscription_MapsItemNameToTagId() { var session = new SuiteLinkSession(); session.RegisterSubscription("Pump001.Run", 0x1234); Assert.Equal(0x1234u, session.GetTagId("Pump001.Run")); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter RegisterSubscription_MapsItemNameToTagId` Expected: FAIL **Step 3: Write minimal implementation** Implement: - session state enum - tag map for `itemName -> tagId` - reverse lookup for `tagId -> itemName` - subscription callback registration - update dispatch helper Keep transport mocking simple. Do not implement full socket I/O in this task. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkSessionTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Internal/SuiteLinkSessionTests.cs git commit -m "feat: add session state and tag mapping" ``` ### Task 10: Implement Tcp Transport Loop **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Transport/SuiteLinkTcpTransport.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Transport/ISuiteLinkTransport.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Transport/SuiteLinkTcpTransportTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task SendAsync_WritesFrameToUnderlyingStream() { var stream = new MemoryStream(); var transport = new SuiteLinkTcpTransport(stream); await transport.SendAsync(new byte[] { 0x01, 0x02 }, CancellationToken.None); Assert.Equal(new byte[] { 0x01, 0x02 }, stream.ToArray()); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SendAsync_WritesFrameToUnderlyingStream` Expected: FAIL **Step 3: Write minimal implementation** Implement: - transport abstraction - stream-backed send and receive methods - real `TcpClient` constructor path - test-friendly stream injection path **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkTcpTransportTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Transport /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Transport/SuiteLinkTcpTransportTests.cs git commit -m "feat: add suitelink transport abstraction" ``` ### Task 11: Implement Public Client Connect And Disconnect **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SubscriptionHandle.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task ConnectAsync_TransitionsClientToConnectedState() { var client = new SuiteLinkClient(new FakeTransport()); await client.ConnectAsync(TestOptions.Create()); Assert.True(client.IsConnected); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter ConnectAsync_TransitionsClientToConnectedState` Expected: FAIL **Step 3: Write minimal implementation** Implement: - `SuiteLinkClient` - connect flow invoking handshake then connect codec - disconnect path - basic disposal Expose a minimal `IsConnected` property or equivalent state for tests. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkClientConnectionTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SubscriptionHandle.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs git commit -m "feat: implement suitelink client connection flow" ``` ### Task 12: Implement Subscribe And Read Flow **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSession.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task ReadAsync_ReturnsFirstUpdateForRequestedTag() { var client = TestClientFactory.CreateConnectedClientWithUpdate("Pump001.Run", SuiteLinkValue.FromBoolean(true)); var update = await client.ReadAsync("Pump001.Run", TimeSpan.FromSeconds(1)); Assert.True(update.Value.TryGetBoolean(out var value)); Assert.True(value); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter ReadAsync_ReturnsFirstUpdateForRequestedTag` Expected: FAIL **Step 3: Write minimal implementation** Implement: - `SubscribeAsync` - `ReadAsync` via temporary subscription - callback dispatch on incoming updates - unadvise when subscription handle is disposed **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkClientSubscriptionTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionTests.cs git commit -m "feat: implement suitelink subscribe and read flow" ``` ### Task 13: Implement Primitive Write Flow **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSession.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientWriteTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task WriteAsync_SendsPokeForSubscribedTag() { var client = TestClientFactory.CreateConnectedSubscribedClient("Pump001.Speed", 0x1234); await client.WriteAsync("Pump001.Speed", SuiteLinkValue.FromInt32(42)); Assert.Contains(client.SentFrames, frame => frame[2] == 0x08 && frame[3] == 0x0B); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter WriteAsync_SendsPokeForSubscribedTag` Expected: FAIL **Step 3: Write minimal implementation** Implement: - `WriteAsync` - tag lookup by item name - fail if item is unknown or not yet subscribed - use primitive write codec **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter SuiteLinkClientWriteTests` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientWriteTests.cs git commit -m "feat: implement primitive suitelink writes" ``` ### Task 14: Add Golden Packet Fixtures **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Fixtures/README.md` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Fixtures/*.bin` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol/*Tests.cs` **Step 1: Write the failing test** Add one fixture-backed assertion that compares encoded output to a stored handshake or connect frame. ```csharp [Fact] public void EncodeConnect_MatchesGoldenFixture() { var expected = File.ReadAllBytes("Fixtures/connect.bin"); var actual = SuiteLinkConnectCodec.Encode(TestOptions.Create()).ToArray(); Assert.Equal(expected, actual); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter MatchesGoldenFixture` Expected: FAIL until fixture and codec match **Step 3: Write minimal implementation** Add fixture files and normalize tests to read them from disk. Document where the fixture bytes came from and which captures or protocol references justify them. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter Fixture` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Fixtures /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/Protocol git commit -m "test: add golden packet fixtures for suitelink codec" ``` ### Task 15: Add Live Integration Test Harness **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/SuiteLink.Client.IntegrationTests.csproj` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/IntegrationSettings.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/TagRoundTripTests.cs` - Create: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/README.md` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln` **Step 1: Write the failing test** ```csharp [Fact(Skip = "Requires live AVEVA SuiteLink endpoint")] public async Task CanSubscribeAndWriteBooleanTag() { var client = new SuiteLinkClient(); await client.ConnectAsync(IntegrationSettings.Load()); } ``` **Step 2: Run test to verify the harness builds** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln --filter CanSubscribeAndWriteBooleanTag` Expected: PASS or SKIP with the test discovered **Step 3: Write minimal implementation** Add: - integration project - environment-based settings loader - skipped or conditional tests for boolean, integer, float, and string tags **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln` Expected: PASS with integration tests skipped by default **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln git commit -m "test: add suitelink integration test harness" ``` ### Task 16: Add Package Documentation **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/README.md` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-16-suitelink-client-design.md` **Step 1: Write the failing documentation check** Define the required README sections: - project purpose - supported protocol scope - supported types - unsupported features - local build and test commands - integration test setup **Step 2: Run documentation review** Run: `rg -n "Supported|Unsupported|Build|Test|Integration" /Users/dohertj2/Desktop/suitelinkclient/README.md` Expected: FAIL until README exists **Step 3: Write minimal implementation** Create a README that states: - v1 supports normal SuiteLink V2 tag operations only - v1 does not support AlarmMgr or secure V3 - primitive types only - exact `dotnet` commands for build and test **Step 4: Run documentation review** Run: `rg -n "Supported|Unsupported|Build|Test|Integration" /Users/dohertj2/Desktop/suitelinkclient/README.md` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/README.md /Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-16-suitelink-client-design.md git commit -m "docs: describe suitelink client scope and usage" ``` ### Task 17: Full Verification Pass **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-16-suitelink-client-implementation-plan.md` **Step 1: Run unit and integration-default test suite** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln` Expected: PASS with integration tests skipped by default **Step 2: Run build verification** Run: `dotnet build /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln -c Release` Expected: PASS **Step 3: Run formatting or analyzer checks if added** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.sln -c Release` Expected: PASS **Step 4: Update plan status notes if execution deviated** Add a short note to the plan if any task required deviation due to verified protocol differences. **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-16-suitelink-client-implementation-plan.md git commit -m "docs: finalize suitelink implementation verification" ```