package driver import ( "errors" "testing" "time" "github.com/stretchr/testify/require" "onehouse/protocol" ) func TestLedgerReplaysFirstResultWithoutAnotherAction(t *testing.T) { ledger, err := NewLedger(testDeviceID) require.NoError(t, err) transport := &recordingTransport{} publisher := NewPublisher(transport) actions := 0 var first ResultRecord for attempt := range 3 { result, exists := ledger.Lookup(testCommandID) if !exists { actions++ result, err = ledger.Remember(testCommandID, protocol.ResultPayload{Status: "completed"}) require.NoError(t, err) first = result } if attempt == 0 { transport.setError(errors.New("delivery uncertain")) require.Error(t, publisher.Result(t.Context(), testDeviceID, testCommandID, result.Metadata, result.Payload)) transport.setError(nil) } else { require.NoError(t, publisher.Result(t.Context(), testDeviceID, testCommandID, result.Metadata, result.Payload)) } require.Equal(t, first, result) } require.Equal(t, 1, actions) packets := transport.messages() require.Len(t, packets, 3) require.Equal(t, packets[0], packets[1]) require.Equal(t, packets[0], packets[2]) ignored, err := ledger.Remember(testCommandID, protocol.ResultPayload{ Status: "failed", Reason: "different outcome", }) require.NoError(t, err) require.Equal(t, first, ignored) } func TestLedgerEvictsOldestAndKeepsDevicesIndependent(t *testing.T) { ledger, err := NewLedger(testDeviceID) require.NoError(t, err) first, err := ledger.Remember(testCommandID, protocol.ResultPayload{ Status: "rejected", Reason: "invalid output", }) require.NoError(t, err) other, err := NewLedger(testOtherDeviceID) require.NoError(t, err) independent, err := other.Remember(testCommandID, protocol.ResultPayload{ Status: "failed", Reason: "unavailable", }) require.NoError(t, err) require.NotEqual(t, first.Payload, independent.Payload) lastID := "" for range commandResultCapacity { lastID, err = protocol.NewID(time.Now().UTC()) require.NoError(t, err) _, err = ledger.Remember(lastID, protocol.ResultPayload{Status: "completed"}) require.NoError(t, err) } _, exists := ledger.Lookup(testCommandID) require.False(t, exists) _, exists = ledger.Lookup(lastID) require.True(t, exists) _, exists = other.Lookup(testCommandID) require.True(t, exists) } func TestLedgerRejectsInvalidResultsAndCopiesReturnedTimes(t *testing.T) { _, err := NewLedger("invalid") require.Error(t, err) ledger, err := NewLedger(testDeviceID) require.NoError(t, err) testCases := []struct { name string payload protocol.ResultPayload }{ { name: "failure requires a reason", payload: protocol.ResultPayload{Status: "failed"}, }, { name: "completion rejects a reason", payload: protocol.ResultPayload{ Status: "completed", Reason: "not allowed", }, }, { name: "unknown status rejects", payload: protocol.ResultPayload{Status: "invalid"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { _, err = ledger.Remember(testCommandID, tc.payload) require.Error(t, err) _, exists := ledger.Lookup(testCommandID) require.False(t, exists) }) } _, err = ledger.Remember("invalid", protocol.ResultPayload{Status: "completed"}) require.Error(t, err) result, err := ledger.Remember(testCommandID, protocol.ResultPayload{Status: "completed"}) require.NoError(t, err) observation := *result.Metadata.SentAt *result.Metadata.SentAt = observation.Add(time.Hour) first, exists := ledger.Lookup(testCommandID) require.True(t, exists) require.Equal(t, observation, *first.Metadata.SentAt) *first.Metadata.SentAt = observation.Add(time.Hour) second, err := ledger.Remember(testCommandID, protocol.ResultPayload{}) require.NoError(t, err) require.Equal(t, observation, *second.Metadata.SentAt) }