+package m31
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "slices"
+ "testing"
+ "testing/synctest"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "onehouse/driver"
+ "onehouse/protocol"
+)
+
+func TestCommandsWriteContiguousRangesAndReplayResults(t *testing.T) {
+ c, client, transport := testConnection(t)
+ h := &commandHandler{connection: c}
+
+ var commands []driver.Command
+
+ for address := 5; address < 18; address++ {
+ commands = append(commands, testCommand(t, c.owner.topology.outputs[address].id, "true"))
+ }
+
+ slices.Reverse(commands)
+ require.NoError(t, h.handle(t.Context(), commands))
+ require.Len(t, client.writes, 1)
+ require.EqualValues(t, 5, client.writes[0].address)
+ require.True(t, client.writes[0].multiple)
+ require.Len(t, client.writes[0].values, 13)
+ require.Equal(t, 1, client.coilReads)
+
+ reports := transport.ofKind(protocol.Reported)
+ results := transport.ofKind(protocol.Result)
+
+ require.Len(t, reports, 13)
+ require.Len(t, results, 13)
+
+ for _, command := range commands {
+ for _, report := range reports {
+ if report.Address.Target == command.Capability {
+ require.Equal(t, command.Metadata.MessageID, report.Metadata.CausationID)
+ }
+ }
+ }
+
+ require.NoError(t, h.handle(t.Context(), commands))
+ require.Len(t, client.writes, 1)
+ require.Equal(t, 1, client.coilReads)
+
+ replayed := transport.ofKind(protocol.Result)[13:]
+ byCommand := make(map[string]protocol.Metadata)
+
+ for _, result := range results {
+ byCommand[result.Address.Target] = result.Metadata
+ }
+
+ for _, result := range replayed {
+ require.Equal(t, byCommand[result.Address.Target], result.Metadata)
+ }
+}
+
+func TestCommandsKeepGapsAndRejectNonsteadyMembers(t *testing.T) {
+ c, client, transport := testConnection(t)
+ h := &commandHandler{connection: c}
+ commands := []driver.Command{
+ testCommand(t, "m0.do0", "true"),
+ testCommand(t, "m0.do1", "true"),
+ testCommand(t, "m0.do2", "true"),
+ testCommand(t, "m0.do4", "true"),
+ testCommand(t, "m2.ao0", "2"),
+ testCommand(t, "m0.di0", "true"),
+ }
+ require.NoError(t, h.handle(t.Context(), commands))
+ require.Len(t, client.writes, 2)
+ require.EqualValues(t, 0, client.writes[0].address)
+ require.Len(t, client.writes[0].values, 2)
+ require.EqualValues(t, 4, client.writes[1].address)
+ require.False(t, client.writes[1].multiple)
+ require.False(t, client.coils[2])
+ require.False(t, client.coils[3])
+ require.Len(t, transport.ofKind(protocol.Result), 6)
+}
+
+func TestCommandsFailAllOnTransportErrorAndOnlyMismatchOtherwise(t *testing.T) {
+ testCases := []struct {
+ name string
+ change func(*fakeClient)
+ transportFailure bool
+ }{
+ {name: "fails all on write failure", transportFailure: true, change: func(c *fakeClient) {
+ c.writeErr = errors.New("write failed")
+ }},
+ {name: "fails all after a partial write", transportFailure: true, change: func(c *fakeClient) {
+ c.writeErr = errors.New("later write failed")
+ c.failAfterWrites = 1
+ }},
+ {name: "fails all on read failure", transportFailure: true, change: func(c *fakeClient) {
+ c.readErr = errors.New("read failed")
+ }},
+ {name: "fails all on short read", transportFailure: true, change: func(c *fakeClient) {
+ c.shortRead = true
+ }},
+ {name: "fails only mismatching member", change: func(c *fakeClient) {
+ c.mismatch = 1
+ }},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ c, client, transport := testConnection(t)
+ tc.change(client)
+
+ h := &commandHandler{connection: c}
+ commands := []driver.Command{testCommand(t, "m0.do0", "true"), testCommand(t, "m0.do1", "true"), testCommand(t, "m1.do7", "true")}
+ err := h.handle(t.Context(), commands)
+ if tc.transportFailure {
+ var failure *deviceError
+
+ require.ErrorAs(t, err, &failure)
+ } else {
+ require.NoError(t, err)
+ }
+
+ require.Len(t, transport.ofKind(protocol.Result), 3)
+
+ for index, command := range commands {
+ record, exists := c.owner.ledger.Lookup(command.Metadata.MessageID)
+ require.True(t, exists)
+
+ status := "completed"
+ if tc.transportFailure || index == 1 {
+ status = "failed"
+ }
+
+ require.Equal(t, status, record.Payload.Status)
+ }
+ })
+ }
+}
+
+func TestCommandsCompletePulseWithoutReadbackOrFeedback(t *testing.T) {
+ c, client, transport := testConnection(t)
+ h := &commandHandler{connection: c}
+ command := testCommand(t, "m0.do2", "true")
+ require.NoError(t, h.handle(t.Context(), []driver.Command{command}))
+ require.Len(t, client.writes, 1)
+ require.Zero(t, client.coilReads)
+ require.Empty(t, transport.ofKind(protocol.Reported))
+ require.NoError(t, h.handle(t.Context(), []driver.Command{command}))
+ require.Len(t, client.writes, 1)
+
+ command = testCommand(t, "m0.do2", "false")
+ require.NoError(t, h.handle(t.Context(), []driver.Command{command}))
+ require.Len(t, client.writes, 1)
+
+ record, exists := c.owner.ledger.Lookup(command.Metadata.MessageID)
+ require.True(t, exists)
+ require.Equal(t, "rejected", record.Payload.Status)
+}
+
+func TestCommandsWriteVoltageAndCheckFeedback(t *testing.T) {
+ testCases := []struct {
+ name string
+ change func(*fakeClient)
+ wantStatus string
+ wantError bool
+ }{
+ {name: "completes verified voltage", wantStatus: "completed", change: func(*fakeClient) {
+ }},
+ {name: "fails mismatching voltage", wantStatus: "failed", change: func(c *fakeClient) {
+ c.mismatch = 3
+ }},
+ {name: "fails register transport", wantStatus: "failed", wantError: true, change: func(c *fakeClient) {
+ c.registerErr = errors.New("register failed")
+ }},
+ {name: "fails analog read transport", wantStatus: "failed", wantError: true, change: func(c *fakeClient) {
+ c.registerReadErr = errors.New("read failed")
+ }},
+ {name: "fails unsafe feedback", wantStatus: "failed", wantError: true, change: func(c *fakeClient) {
+ c.analog[0] = 10001
+ }},
+ {name: "fails incomplete feedback", wantStatus: "failed", wantError: true, change: func(c *fakeClient) {
+ c.analog = c.analog[:4]
+ }},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ c, client, transport := testConnection(t)
+ tc.change(client)
+
+ h := &commandHandler{connection: c}
+ command := testCommand(t, "m2.ao3", "5.5")
+ err := h.handle(t.Context(), []driver.Command{command})
+ if tc.wantError {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ record, exists := c.owner.ledger.Lookup(command.Metadata.MessageID)
+ require.True(t, exists)
+ require.Equal(t, tc.wantStatus, record.Payload.Status)
+
+ if tc.wantStatus == "completed" {
+ require.EqualValues(t, 5500, client.analog[3])
+ require.Len(t, transport.messages, 2)
+ require.Equal(t, protocol.Reported, transport.messages[0].Address.Kind)
+ require.Equal(t, command.Metadata.MessageID, transport.messages[0].Metadata.CausationID)
+ }
+ })
+ }
+}
+
+func TestCommandsRememberBeforeFailedPublication(t *testing.T) {
+ for _, kind := range []protocol.Kind{protocol.Reported, protocol.Result} {
+ t.Run(fmt.Sprint("preserves first outcome after ", kind, " failure"), func(t *testing.T) {
+ c, client, transport := testConnection(t)
+ h := &commandHandler{connection: c}
+ failure := errors.New("publication failed")
+ transport.failKind, transport.err = kind, failure
+ commands := []driver.Command{testCommand(t, "m0.do0", "true"), testCommand(t, "m0.do1", "true")}
+ require.ErrorIs(t, h.handle(t.Context(), commands), failure)
+
+ record, exists := c.owner.ledger.Lookup(commands[1].Metadata.MessageID)
+ require.True(t, exists)
+ require.Equal(t, "completed", record.Payload.Status)
+
+ transport.err = nil
+
+ require.NoError(t, h.handle(t.Context(), commands))
+ require.Len(t, client.writes, 1)
+ })
+ }
+}
+
+func TestCommandsRecheckExpiryBeforeEachWriteAndHonorCancellation(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ c, client, _ := testConnection(t)
+ h := &commandHandler{connection: c}
+ first := testCommand(t, "m0.do0", "true")
+ second := testCommand(t, "m0.do4", "true")
+ expires := time.Now().Add(time.Millisecond)
+ second.Metadata.ExpiresAt = &expires
+ client.writeDelay = 2 * time.Millisecond
+
+ require.NoError(t, h.handle(t.Context(), []driver.Command{first, second}))
+ require.Len(t, client.writes, 1)
+
+ record, exists := c.owner.ledger.Lookup(second.Metadata.MessageID)
+ require.True(t, exists)
+ require.Equal(t, "rejected", record.Payload.Status)
+
+ ctx, cancel := context.WithCancel(t.Context())
+ cancel()
+
+ third := testCommand(t, "m0.do1", "true")
+ require.ErrorIs(t, h.handle(ctx, []driver.Command{third}), context.Canceled)
+ require.Len(t, client.writes, 1)
+ })
+}
+
+func TestCommandsCompleteUnchangedSingleWithoutState(t *testing.T) {
+ c, client, transport := testConnection(t)
+ h := &commandHandler{connection: c}
+ command := testCommand(t, "m0.do0", "false")
+ require.NoError(t, h.handle(t.Context(), []driver.Command{command}))
+ require.Len(t, client.writes, 1)
+ require.False(t, client.writes[0].multiple)
+ require.Len(t, transport.messages, 1)
+ require.Equal(t, protocol.Result, transport.messages[0].Address.Kind)
+}
+
+func TestCommandsReturnPublicationErrorsWithoutLosingOutcomes(t *testing.T) {
+ testCases := []struct {
+ name string
+ capability string
+ value string
+ offline bool
+ physicalFailure bool
+ }{
+ {name: "retains completed pulse after result failure", capability: "m0.do2", value: "true"},
+ {name: "retains voltage after result failure", capability: "m2.ao0", value: "5"},
+ {name: "returns rejection failure", capability: "m0.di0", value: "true"},
+ {name: "returns offline result failure", capability: "m0.do0", value: "true", offline: true},
+ {name: "returns offline rejection failure", capability: "m0.di0", value: "true", offline: true},
+ {name: "joins pulse and result failure", capability: "m0.do2", value: "true", physicalFailure: true},
+ {name: "joins batch and result failure", capability: "m0.do0", value: "true", physicalFailure: true},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ c, client, transport := testConnection(t)
+ h := &commandHandler{connection: c}
+ command := testCommand(t, tc.capability, tc.value)
+ sendErr := errors.New("send failed")
+ physicalErr := errors.New("physical failure")
+ transport.failKind, transport.err = protocol.Result, sendErr
+ if tc.physicalFailure {
+ client.writeErr = physicalErr
+ }
+
+ var err error
+
+ if tc.offline {
+ err = h.unavailable(t.Context(), []driver.Command{command})
+ } else {
+ err = h.handle(t.Context(), []driver.Command{command})
+ }
+
+ require.ErrorIs(t, err, sendErr)
+
+ if tc.physicalFailure {
+ require.ErrorIs(t, err, physicalErr)
+
+ var physical *deviceError
+
+ require.NotErrorAs(t, err, &physical)
+ }
+
+ record, exists := c.owner.ledger.Lookup(command.Metadata.MessageID)
+ require.True(t, exists)
+
+ transport.err, client.writeErr = nil, nil
+ count := len(client.writes)
+
+ require.NoError(t, h.handle(t.Context(), []driver.Command{command}))
+ require.Len(t, client.writes, count)
+
+ replay := transport.ofKind(protocol.Result)
+ require.Equal(t, record.Metadata, replay[len(replay)-1].Metadata)
+ })
+ }
+}