feat: bootstrap suitelink tag client codecs
This commit is contained in:
314
docs/plans/2026-03-16-suitelink-client-design.md
Normal file
314
docs/plans/2026-03-16-suitelink-client-design.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# SuiteLink Tag Client Design
|
||||
|
||||
## Goal
|
||||
|
||||
Build a cross-platform `.NET 10` C# client that communicates with AVEVA SuiteLink from macOS, Linux, and Windows for tag operations only.
|
||||
|
||||
The v1 scope is limited to:
|
||||
|
||||
- Connect to a SuiteLink endpoint
|
||||
- Subscribe to tags
|
||||
- Receive tag updates
|
||||
- Write tag values
|
||||
- Unsubscribe cleanly
|
||||
|
||||
The v1 scope explicitly excludes:
|
||||
|
||||
- AlarmMgr
|
||||
- Alarm and event handling
|
||||
- Secure SuiteLink V3 support
|
||||
- Automatic reconnect and subscription rebuild
|
||||
|
||||
## Constraints
|
||||
|
||||
- The protocol is proprietary and the current design is based on reverse-engineered public evidence plus AVEVA product documentation.
|
||||
- The initial target is non-encrypted SuiteLink V2 behavior, or servers configured for mixed mode that still permit legacy SuiteLink traffic.
|
||||
- The first implementation only supports primitive value types:
|
||||
- `bool`
|
||||
- `int32`
|
||||
- `float32`
|
||||
- `string`
|
||||
- The design should remain extensible for later support of `double`, `int64`, and `DateTime` if packet captures confirm their wire representation.
|
||||
|
||||
## Protocol Target
|
||||
|
||||
The target protocol surface is the normal SuiteLink tag exchange path, not AlarmMgr.
|
||||
|
||||
Observed normal message structure:
|
||||
|
||||
- `uint16 little-endian remaining_length`
|
||||
- `uint16 little-endian message_type`
|
||||
- payload
|
||||
- trailing byte `0xA5`
|
||||
|
||||
Observed message types to support:
|
||||
|
||||
- `CONNECT`
|
||||
- `ADVISE`
|
||||
- `ADVISE ACK`
|
||||
- `UPDATE`
|
||||
- `UNADVISE`
|
||||
- `UNADVISE ACK`
|
||||
- `POKE`
|
||||
- `POKE ACK`
|
||||
- `TIME`
|
||||
- Ping/pong keepalive messages
|
||||
|
||||
Observed normal wire value types:
|
||||
|
||||
- binary
|
||||
- integer
|
||||
- real
|
||||
- message
|
||||
|
||||
## Architecture
|
||||
|
||||
The client is split into three layers.
|
||||
|
||||
### Transport
|
||||
|
||||
Responsible for:
|
||||
|
||||
- Opening and closing TCP connections
|
||||
- Reading complete SuiteLink frames
|
||||
- Writing complete SuiteLink frames
|
||||
- Cancellation and socket lifetime management
|
||||
|
||||
### Protocol
|
||||
|
||||
Responsible for:
|
||||
|
||||
- Encoding the startup handshake
|
||||
- Encoding `CONNECT`
|
||||
- Encoding `ADVISE`, `UNADVISE`, and `POKE`
|
||||
- Decoding handshake acknowledgements
|
||||
- Decoding `ADVISE ACK`, `UPDATE`, and keepalive traffic
|
||||
- Converting between wire values and typed client values
|
||||
|
||||
### Client API
|
||||
|
||||
Responsible for:
|
||||
|
||||
- Exposing a minimal public API for connect, subscribe, read, write, and disconnect
|
||||
- Hiding protocol details such as server-assigned tag ids
|
||||
- Dispatching updates to user callbacks or future stream abstractions
|
||||
|
||||
## Session Model
|
||||
|
||||
The session uses one persistent TCP connection to one SuiteLink endpoint and one configured `application/topic` pair.
|
||||
|
||||
State model:
|
||||
|
||||
- `Disconnected`
|
||||
- `TcpConnected`
|
||||
- `HandshakeComplete`
|
||||
- `SessionConnected`
|
||||
- `Faulted`
|
||||
|
||||
Startup flow:
|
||||
|
||||
1. Open TCP connection.
|
||||
2. Send SuiteLink handshake for the normal tag protocol.
|
||||
3. Validate handshake acknowledgement.
|
||||
4. Send `CONNECT` with application, topic, and client identity fields.
|
||||
5. Transition to connected session state.
|
||||
6. Send `ADVISE` for one or more items.
|
||||
7. Capture `tag_id` mappings from server responses.
|
||||
8. Receive `UPDATE` frames and dispatch typed values to subscribers.
|
||||
9. Send `POKE` for writes.
|
||||
10. Send `UNADVISE` when a subscription is disposed.
|
||||
|
||||
Read behavior in v1 is implemented as a temporary subscription:
|
||||
|
||||
1. Send `ADVISE` for the requested item.
|
||||
2. Wait for the first matching `UPDATE`.
|
||||
3. Return the decoded value.
|
||||
4. Send `UNADVISE`.
|
||||
|
||||
This is preferred over inventing a direct read request that has not yet been proven by packet captures.
|
||||
|
||||
## Public API
|
||||
|
||||
```csharp
|
||||
public sealed class SuiteLinkClient : IAsyncDisposable
|
||||
{
|
||||
Task ConnectAsync(SuiteLinkConnectionOptions options, CancellationToken ct = default);
|
||||
Task DisconnectAsync(CancellationToken ct = default);
|
||||
|
||||
Task<SubscriptionHandle> SubscribeAsync(
|
||||
string itemName,
|
||||
Action<SuiteLinkTagUpdate> onUpdate,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task<SuiteLinkTagUpdate> ReadAsync(
|
||||
string itemName,
|
||||
TimeSpan timeout,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task WriteAsync(
|
||||
string itemName,
|
||||
SuiteLinkValue value,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
```
|
||||
|
||||
Supporting models:
|
||||
|
||||
- `SuiteLinkConnectionOptions`
|
||||
- `Host`
|
||||
- `Port` default `5413`
|
||||
- `Application`
|
||||
- `Topic`
|
||||
- `ClientName`
|
||||
- `ClientNode`
|
||||
- `UserName`
|
||||
- `ServerNode`
|
||||
- `SuiteLinkValue`
|
||||
- typed union for `bool`, `int`, `float`, and `string`
|
||||
- `SuiteLinkTagUpdate`
|
||||
- `ItemName`
|
||||
- `TagId`
|
||||
- `Value`
|
||||
- `Quality`
|
||||
- `ElapsedMilliseconds`
|
||||
- `ReceivedAtUtc`
|
||||
- `SubscriptionHandle`
|
||||
- caller-facing subscription lifetime object
|
||||
- disposes via `UNADVISE`
|
||||
|
||||
## Internal Components
|
||||
|
||||
### `SuiteLinkFrameReader`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Read complete normal SuiteLink frames from a `NetworkStream`
|
||||
- Parse the 2-byte little-endian remaining length
|
||||
- Validate trailing `0xA5`
|
||||
- Return frame payload slices to the codec
|
||||
|
||||
### `SuiteLinkFrameWriter`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Build frames in memory
|
||||
- Write little-endian lengths and message types
|
||||
- Encode UTF-16LE strings where required
|
||||
- Append trailing `0xA5`
|
||||
|
||||
### `SuiteLinkMessageCodec`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Encode handshake and normal session messages
|
||||
- Decode incoming acknowledgements and updates
|
||||
- Map wire value types to `SuiteLinkValue`
|
||||
|
||||
Expected methods:
|
||||
|
||||
- `EncodeHandshake`
|
||||
- `EncodeConnect`
|
||||
- `EncodeAdvise`
|
||||
- `EncodeUnadvise`
|
||||
- `EncodePoke`
|
||||
- `DecodeHandshakeAck`
|
||||
- `DecodeAdviseAck`
|
||||
- `DecodeUpdate`
|
||||
- `DecodeKeepAlive`
|
||||
|
||||
### `SuiteLinkSession`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Own the send and receive loops
|
||||
- Maintain session state
|
||||
- Track `itemName <-> tagId` mappings
|
||||
- Track active subscriptions
|
||||
- Route decoded updates to the correct subscriber callbacks
|
||||
|
||||
## Type Strategy
|
||||
|
||||
The first release supports only:
|
||||
|
||||
- `bool`
|
||||
- `int32`
|
||||
- `float32`
|
||||
- `string`
|
||||
|
||||
The public type wrapper must be extensible so later additions do not require replacing the whole API. The intended future-compatible additions are:
|
||||
|
||||
- `double`
|
||||
- `int64`
|
||||
- `DateTime`
|
||||
|
||||
The protocol layer should fail fast when an unsupported wire type or unsupported outgoing write type is encountered.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The v1 client should be explicit and conservative.
|
||||
|
||||
- Any malformed frame transitions the session to `Faulted`
|
||||
- Any unexpected message type during startup fails the connection attempt
|
||||
- Write attempts for unsupported types fail immediately
|
||||
- Reconnect is not automatic in v1
|
||||
- Subscription rebuild after reconnect is deferred to a later version
|
||||
|
||||
## Validation Strategy
|
||||
|
||||
Testing is divided into three levels.
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Validate:
|
||||
|
||||
- frame length handling
|
||||
- trailing marker validation
|
||||
- UTF-16LE string encoding/decoding
|
||||
- primitive value encoding/decoding
|
||||
|
||||
### Golden Packet Tests
|
||||
|
||||
Use known byte sequences and captures to verify:
|
||||
|
||||
- handshake
|
||||
- `CONNECT`
|
||||
- `ADVISE`
|
||||
- `ADVISE ACK`
|
||||
- `UPDATE`
|
||||
- `POKE`
|
||||
- `UNADVISE`
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Run against a real AVEVA/OI server configured to allow legacy or mixed-mode SuiteLink traffic.
|
||||
|
||||
Success criteria:
|
||||
|
||||
- connect successfully from macOS or Linux
|
||||
- subscribe to one boolean tag
|
||||
- subscribe to one integer tag
|
||||
- subscribe to one real tag
|
||||
- subscribe to one message tag
|
||||
- receive live updates for each
|
||||
- write to each supported tag type and verify the result
|
||||
- disconnect cleanly
|
||||
|
||||
## Non-Goals
|
||||
|
||||
The following are intentionally deferred:
|
||||
|
||||
- AlarmMgr support
|
||||
- Secure SuiteLink V3 support
|
||||
- automatic reconnect
|
||||
- batched subscription optimization
|
||||
- broad type support beyond the four proven primitive classes
|
||||
- production hardening for all undocumented server variants
|
||||
|
||||
## Recommended Next Step
|
||||
|
||||
Create a detailed implementation plan that:
|
||||
|
||||
- establishes the project structure
|
||||
- defines the test-first workflow
|
||||
- identifies capture-driven fixtures needed for codec tests
|
||||
- breaks the implementation into transport, codec, session, and API slices
|
||||
864
docs/plans/2026-03-16-suitelink-client-implementation-plan.md
Normal file
864
docs/plans/2026-03-16-suitelink-client-implementation-plan.md
Normal file
@@ -0,0 +1,864 @@
|
||||
# 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<byte>)`
|
||||
|
||||
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"
|
||||
```
|
||||
Reference in New Issue
Block a user