fix(broker): upgrade pinned host protocol versions on startup (#289)
Pinned host records stored the protocol and catalogue versions of the build that pinned them. After a protocol bump the unchanged remote daemon identity was refused as a conflicting policy, and the picker reported it as an SSH connection failure. The broker now moves stored host policies to the running build's versions at startup, advancing the registration generation, and classifies conflicting-policy and incompatible probe errors as incompatible.
- commit
- b3c91968797e62720050f4d703fbb553e360f2f7
- author
- bnema · · 2026-09-25 20:53 UTC
- committer
- GitHub · · 2026-09-25 20:53 UTC
- parent
- def4c6e
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
5 files · +113 -0
A internal/app/broker_host_upgrade_test.go +67 -0
| @@ -0,0 +1,67 @@ | |||
| 1 | + | package app | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "path/filepath" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/vev/internal/adapters/brokerstore" | |
| 10 | + | "github.com/bnema/vev/internal/domain" | |
| 11 | + | "github.com/bnema/vev/internal/ports" | |
| 12 | + | "github.com/bnema/vev/internal/protocol" | |
| 13 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func TestUpgradeBrokerHostVersions(t *testing.T) { | |
| 17 | + | tests := []struct { | |
| 18 | + | name string | |
| 19 | + | policy func() ports.BrokerPolicy | |
| 20 | + | wantRevision uint64 // also the expected generation advance | |
| 21 | + | }{ | |
| 22 | + | { | |
| 23 | + | name: "older build policy is upgraded and identity kept", | |
| 24 | + | policy: func() ports.BrokerPolicy { | |
| 25 | + | p := remoteBrokerPolicy("") | |
| 26 | + | p.ProtocolVersion-- | |
| 27 | + | return p | |
| 28 | + | }, | |
| 29 | + | wantRevision: 1, | |
| 30 | + | }, | |
| 31 | + | { | |
| 32 | + | name: "current policy is left untouched", | |
| 33 | + | policy: func() ports.BrokerPolicy { return remoteBrokerPolicy("") }, | |
| 34 | + | wantRevision: 0, | |
| 35 | + | }, | |
| 36 | + | } | |
| 37 | + | for _, tt := range tests { | |
| 38 | + | t.Run(tt.name, func(t *testing.T) { | |
| 39 | + | registration, err := domain.NewRemoteRegistration("user@host", [16]byte{1}) | |
| 40 | + | require.NoError(t, err) | |
| 41 | + | policy := tt.policy() | |
| 42 | + | route, err := ports.BrokerRouteForTransport(policy.Transport, registration.Endpoint) | |
| 43 | + | require.NoError(t, err) | |
| 44 | + | record := ports.BrokerHostRecord{Registration: registration, Pinned: true, Policy: policy, Route: route, Identity: "daemon-identity"} | |
| 45 | + | ||
| 46 | + | store, err := brokerstore.Open(brokerstore.Options{Dir: filepath.Join(shortTempDir(t, "vevu"), "state"), InitialHosts: []ports.BrokerHostRecord{record}, InitialImportProvided: true}) | |
| 47 | + | require.NoError(t, err) | |
| 48 | + | t.Cleanup(func() { require.NoError(t, store.Close()) }) | |
| 49 | + | before, err := store.LoadHosts() | |
| 50 | + | require.NoError(t, err) | |
| 51 | + | ||
| 52 | + | require.NoError(t, upgradeBrokerHostVersions(store)) | |
| 53 | + | ||
| 54 | + | after, err := store.LoadHosts() | |
| 55 | + | require.NoError(t, err) | |
| 56 | + | require.Equal(t, before.Revision+tt.wantRevision, after.Revision) | |
| 57 | + | require.Len(t, after.Hosts, 1) | |
| 58 | + | got := after.Hosts[0] | |
| 59 | + | require.Equal(t, protocol.Version, got.Policy.ProtocolVersion) | |
| 60 | + | require.Equal(t, catalogue.RemoteCatalogSchemaVersion, got.Policy.CatalogSchemaVersion) | |
| 61 | + | require.Equal(t, ports.BrokerDaemonIdentity("daemon-identity"), got.Identity) | |
| 62 | + | require.Equal(t, registration.Endpoint, got.Registration.Endpoint) | |
| 63 | + | require.Equal(t, registration.Incarnation, got.Registration.Incarnation) | |
| 64 | + | require.Equal(t, registration.Generation+domain.RemoteGeneration(tt.wantRevision), got.Registration.Generation) | |
| 65 | + | }) | |
| 66 | + | } | |
| 67 | + | } | |
| @@ -0,0 +1,67 @@ | |||||
| 1 | + | package app | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "path/filepath" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/vev/internal/adapters/brokerstore" | |||
| 10 | + | "github.com/bnema/vev/internal/domain" | |||
| 11 | + | "github.com/bnema/vev/internal/ports" | |||
| 12 | + | "github.com/bnema/vev/internal/protocol" | |||
| 13 | + | "github.com/bnema/vev/internal/protocol/catalogue" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func TestUpgradeBrokerHostVersions(t *testing.T) { | |||
| 17 | + | tests := []struct { | |||
| 18 | + | name string | |||
| 19 | + | policy func() ports.BrokerPolicy | |||
| 20 | + | wantRevision uint64 // also the expected generation advance | |||
| 21 | + | }{ | |||
| 22 | + | { | |||
| 23 | + | name: "older build policy is upgraded and identity kept", | |||
| 24 | + | policy: func() ports.BrokerPolicy { | |||
| 25 | + | p := remoteBrokerPolicy("") | |||
| 26 | + | p.ProtocolVersion-- | |||
| 27 | + | return p | |||
| 28 | + | }, | |||
| 29 | + | wantRevision: 1, | |||
| 30 | + | }, | |||
| 31 | + | { | |||
| 32 | + | name: "current policy is left untouched", | |||
| 33 | + | policy: func() ports.BrokerPolicy { return remoteBrokerPolicy("") }, | |||
| 34 | + | wantRevision: 0, | |||
| 35 | + | }, | |||
| 36 | + | } | |||
| 37 | + | for _, tt := range tests { | |||
| 38 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 39 | + | registration, err := domain.NewRemoteRegistration("user@host", [16]byte{1}) | |||
| 40 | + | require.NoError(t, err) | |||
| 41 | + | policy := tt.policy() | |||
| 42 | + | route, err := ports.BrokerRouteForTransport(policy.Transport, registration.Endpoint) | |||
| 43 | + | require.NoError(t, err) | |||
| 44 | + | record := ports.BrokerHostRecord{Registration: registration, Pinned: true, Policy: policy, Route: route, Identity: "daemon-identity"} | |||
| 45 | + | ||||
| 46 | + | store, err := brokerstore.Open(brokerstore.Options{Dir: filepath.Join(shortTempDir(t, "vevu"), "state"), InitialHosts: []ports.BrokerHostRecord{record}, InitialImportProvided: true}) | |||
| 47 | + | require.NoError(t, err) | |||
| 48 | + | t.Cleanup(func() { require.NoError(t, store.Close()) }) | |||
| 49 | + | before, err := store.LoadHosts() | |||
| 50 | + | require.NoError(t, err) | |||
| 51 | + | ||||
| 52 | + | require.NoError(t, upgradeBrokerHostVersions(store)) | |||
| 53 | + | ||||
| 54 | + | after, err := store.LoadHosts() | |||
| 55 | + | require.NoError(t, err) | |||
| 56 | + | require.Equal(t, before.Revision+tt.wantRevision, after.Revision) | |||
| 57 | + | require.Len(t, after.Hosts, 1) | |||
| 58 | + | got := after.Hosts[0] | |||
| 59 | + | require.Equal(t, protocol.Version, got.Policy.ProtocolVersion) | |||
| 60 | + | require.Equal(t, catalogue.RemoteCatalogSchemaVersion, got.Policy.CatalogSchemaVersion) | |||
| 61 | + | require.Equal(t, ports.BrokerDaemonIdentity("daemon-identity"), got.Identity) | |||
| 62 | + | require.Equal(t, registration.Endpoint, got.Registration.Endpoint) | |||
| 63 | + | require.Equal(t, registration.Incarnation, got.Registration.Incarnation) | |||
| 64 | + | require.Equal(t, registration.Generation+domain.RemoteGeneration(tt.wantRevision), got.Registration.Generation) | |||
| 65 | + | }) | |||
| 66 | + | } | |||
| 67 | + | } | |||
M internal/app/broker_offline.go +3 -0
| @@ -270,6 +270,9 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker | |||
| 270 | 270 | return fmt.Errorf("vev: open broker sandbox store: %w", err) | |
| 271 | 271 | } | |
| 272 | 272 | defer func() { retErr = errors.Join(retErr, store.Close()) }() | |
| 273 | + | if err := upgradeBrokerHostVersions(store); err != nil { | |
| 274 | + | return err | |
| 275 | + | } | |
| 273 | 276 | ||
| 274 | 277 | epoch, err := deps.newEpoch() | |
| 275 | 278 | if err != nil { | |
| @@ -270,6 +270,9 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker | |||||
| 270 | return fmt.Errorf("vev: open broker sandbox store: %w", err) | 270 | return fmt.Errorf("vev: open broker sandbox store: %w", err) | ||
| 271 | } | 271 | } | ||
| 272 | defer func() { retErr = errors.Join(retErr, store.Close()) }() | 272 | defer func() { retErr = errors.Join(retErr, store.Close()) }() | ||
| 273 | + | if err := upgradeBrokerHostVersions(store); err != nil { | |||
| 274 | + | return err | |||
| 275 | + | } | |||
| 273 | 276 | ||||
| 274 | epoch, err := deps.newEpoch() | 277 | epoch, err := deps.newEpoch() | ||
| 275 | if err != nil { | 278 | if err != nil { | ||
M internal/app/broker_production.go +35 -0
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/vev/internal/adapters/brokerconfig" | |
| 10 | 10 | "github.com/bnema/vev/internal/adapters/ipc" | |
| 11 | + | "github.com/bnema/vev/internal/domain" | |
| 11 | 12 | "github.com/bnema/vev/internal/platform" | |
| 12 | 13 | "github.com/bnema/vev/internal/ports" | |
| 13 | 14 | "github.com/bnema/vev/internal/protocol" | |
| @@ -62,6 +63,40 @@ func remoteBrokerPolicy(transport string) ports.BrokerPolicy { | |||
| 62 | 63 | } | |
| 63 | 64 | } | |
| 64 | 65 | ||
| 66 | + | // upgradeBrokerHostVersions moves durable host policies to this build's | |
| 67 | + | // protocol and catalogue versions. Those fields describe the running binary, | |
| 68 | + | // not a user choice, so hosts pinned by an older build must follow an upgrade; | |
| 69 | + | // otherwise the unchanged daemon identity is refused as a conflicting policy. | |
| 70 | + | func upgradeBrokerHostVersions(store ports.BrokerHostStore) error { | |
| 71 | + | hosts, err := store.LoadHosts() | |
| 72 | + | if err != nil { | |
| 73 | + | return fmt.Errorf("vev: load broker hosts: %w", err) | |
| 74 | + | } | |
| 75 | + | changed := false | |
| 76 | + | records := ports.CloneBrokerHostRecords(hosts.Hosts) | |
| 77 | + | for i := range records { | |
| 78 | + | policy := &records[i].Policy | |
| 79 | + | if policy.ProtocolVersion == protocol.Version && policy.CatalogSchemaVersion == catalogue.RemoteCatalogSchemaVersion { | |
| 80 | + | continue | |
| 81 | + | } | |
| 82 | + | // A policy change advances the generation so stale observations are fenced. | |
| 83 | + | if records[i].Registration.Generation == ^domain.RemoteGeneration(0) { | |
| 84 | + | return ports.ErrBrokerRevisionExhausted | |
| 85 | + | } | |
| 86 | + | records[i].Registration.Generation++ | |
| 87 | + | policy.ProtocolVersion = protocol.Version | |
| 88 | + | policy.CatalogSchemaVersion = catalogue.RemoteCatalogSchemaVersion | |
| 89 | + | changed = true | |
| 90 | + | } | |
| 91 | + | if !changed { | |
| 92 | + | return nil | |
| 93 | + | } | |
| 94 | + | if err := store.ReplaceHosts(hosts.Revision, records); err != nil { | |
| 95 | + | return fmt.Errorf("vev: upgrade broker host versions: %w", err) | |
| 96 | + | } | |
| 97 | + | return nil | |
| 98 | + | } | |
| 99 | + | ||
| 65 | 100 | func localDaemonPolicy() ports.BrokerPolicy { | |
| 66 | 101 | return ports.BrokerPolicy{ | |
| 67 | 102 | ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | 8 | ||||
| 9 | "github.com/bnema/vev/internal/adapters/brokerconfig" | 9 | "github.com/bnema/vev/internal/adapters/brokerconfig" | ||
| 10 | "github.com/bnema/vev/internal/adapters/ipc" | 10 | "github.com/bnema/vev/internal/adapters/ipc" | ||
| 11 | + | "github.com/bnema/vev/internal/domain" | |||
| 11 | "github.com/bnema/vev/internal/platform" | 12 | "github.com/bnema/vev/internal/platform" | ||
| 12 | "github.com/bnema/vev/internal/ports" | 13 | "github.com/bnema/vev/internal/ports" | ||
| 13 | "github.com/bnema/vev/internal/protocol" | 14 | "github.com/bnema/vev/internal/protocol" | ||
| @@ -62,6 +63,40 @@ func remoteBrokerPolicy(transport string) ports.BrokerPolicy { | |||||
| 62 | } | 63 | } | ||
| 63 | } | 64 | } | ||
| 64 | 65 | ||||
| 66 | + | // upgradeBrokerHostVersions moves durable host policies to this build's | |||
| 67 | + | // protocol and catalogue versions. Those fields describe the running binary, | |||
| 68 | + | // not a user choice, so hosts pinned by an older build must follow an upgrade; | |||
| 69 | + | // otherwise the unchanged daemon identity is refused as a conflicting policy. | |||
| 70 | + | func upgradeBrokerHostVersions(store ports.BrokerHostStore) error { | |||
| 71 | + | hosts, err := store.LoadHosts() | |||
| 72 | + | if err != nil { | |||
| 73 | + | return fmt.Errorf("vev: load broker hosts: %w", err) | |||
| 74 | + | } | |||
| 75 | + | changed := false | |||
| 76 | + | records := ports.CloneBrokerHostRecords(hosts.Hosts) | |||
| 77 | + | for i := range records { | |||
| 78 | + | policy := &records[i].Policy | |||
| 79 | + | if policy.ProtocolVersion == protocol.Version && policy.CatalogSchemaVersion == catalogue.RemoteCatalogSchemaVersion { | |||
| 80 | + | continue | |||
| 81 | + | } | |||
| 82 | + | // A policy change advances the generation so stale observations are fenced. | |||
| 83 | + | if records[i].Registration.Generation == ^domain.RemoteGeneration(0) { | |||
| 84 | + | return ports.ErrBrokerRevisionExhausted | |||
| 85 | + | } | |||
| 86 | + | records[i].Registration.Generation++ | |||
| 87 | + | policy.ProtocolVersion = protocol.Version | |||
| 88 | + | policy.CatalogSchemaVersion = catalogue.RemoteCatalogSchemaVersion | |||
| 89 | + | changed = true | |||
| 90 | + | } | |||
| 91 | + | if !changed { | |||
| 92 | + | return nil | |||
| 93 | + | } | |||
| 94 | + | if err := store.ReplaceHosts(hosts.Revision, records); err != nil { | |||
| 95 | + | return fmt.Errorf("vev: upgrade broker host versions: %w", err) | |||
| 96 | + | } | |||
| 97 | + | return nil | |||
| 98 | + | } | |||
| 99 | + | ||||
| 65 | func localDaemonPolicy() ports.BrokerPolicy { | 100 | func localDaemonPolicy() ports.BrokerPolicy { | ||
| 66 | return ports.BrokerPolicy{ | 101 | return ports.BrokerPolicy{ | ||
| 67 | ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, | 102 | ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, | ||
M internal/usecase/broker/registry.go +4 -0
| @@ -1128,10 +1128,14 @@ func observationOutcome(result probeResult) (domain.RemoteFailureKind, domain.Re | |||
| 1128 | 1128 | if result.err != nil { | |
| 1129 | 1129 | kind := domain.RemoteFailureTransport | |
| 1130 | 1130 | var typed domain.RemoteFailure | |
| 1131 | + | var brokerErr ports.BrokerError | |
| 1131 | 1132 | switch { | |
| 1132 | 1133 | case errors.As(result.err, &typed) && typed.Kind != domain.RemoteFailureNone: | |
| 1133 | 1134 | // The dialer classified the failure (for example SSH authentication). | |
| 1134 | 1135 | kind = typed.Kind | |
| 1136 | + | case errors.As(result.err, &brokerErr) && (brokerErr.Code == ports.BrokerErrorConflictingPolicy || brokerErr.Code == ports.BrokerErrorIncompatible): | |
| 1137 | + | // SSH worked; the daemon's identity or version was refused. | |
| 1138 | + | kind = domain.RemoteFailureIncompatible | |
| 1135 | 1139 | case errors.Is(result.err, context.DeadlineExceeded): | |
| 1136 | 1140 | kind = domain.RemoteFailureTimeout | |
| 1137 | 1141 | } | |
| @@ -1128,10 +1128,14 @@ func observationOutcome(result probeResult) (domain.RemoteFailureKind, domain.Re | |||||
| 1128 | if result.err != nil { | 1128 | if result.err != nil { | ||
| 1129 | kind := domain.RemoteFailureTransport | 1129 | kind := domain.RemoteFailureTransport | ||
| 1130 | var typed domain.RemoteFailure | 1130 | var typed domain.RemoteFailure | ||
| 1131 | + | var brokerErr ports.BrokerError | |||
| 1131 | switch { | 1132 | switch { | ||
| 1132 | case errors.As(result.err, &typed) && typed.Kind != domain.RemoteFailureNone: | 1133 | case errors.As(result.err, &typed) && typed.Kind != domain.RemoteFailureNone: | ||
| 1133 | // The dialer classified the failure (for example SSH authentication). | 1134 | // The dialer classified the failure (for example SSH authentication). | ||
| 1134 | kind = typed.Kind | 1135 | kind = typed.Kind | ||
| 1136 | + | case errors.As(result.err, &brokerErr) && (brokerErr.Code == ports.BrokerErrorConflictingPolicy || brokerErr.Code == ports.BrokerErrorIncompatible): | |||
| 1137 | + | // SSH worked; the daemon's identity or version was refused. | |||
| 1138 | + | kind = domain.RemoteFailureIncompatible | |||
| 1135 | case errors.Is(result.err, context.DeadlineExceeded): | 1139 | case errors.Is(result.err, context.DeadlineExceeded): | ||
| 1136 | kind = domain.RemoteFailureTimeout | 1140 | kind = domain.RemoteFailureTimeout | ||
| 1137 | } | 1141 | } | ||
M internal/usecase/broker/registry_outcome_test.go +4 -0
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "github.com/stretchr/testify/require" | |
| 10 | 10 | ||
| 11 | 11 | "github.com/bnema/vev/internal/domain" | |
| 12 | + | "github.com/bnema/vev/internal/ports" | |
| 12 | 13 | ) | |
| 13 | 14 | ||
| 14 | 15 | func TestObservationOutcomeKeepsTypedDialFailure(t *testing.T) { | |
| @@ -22,6 +23,9 @@ func TestObservationOutcomeKeepsTypedDialFailure(t *testing.T) { | |||
| 22 | 23 | {name: "deadline is timeout", err: context.DeadlineExceeded, kind: domain.RemoteFailureTimeout, want: domain.RemoteAvailabilityUnreachable}, | |
| 23 | 24 | {name: "wrapped authentication", err: fmt.Errorf("dial: %w", domain.RemoteFailure{Kind: domain.RemoteFailureAuthentication, Err: errors.New("exit 255")}), kind: domain.RemoteFailureAuthentication, want: domain.RemoteAvailabilityAuthFailed}, | |
| 24 | 25 | {name: "wrapped trust", err: fmt.Errorf("dial: %w", domain.RemoteFailure{Kind: domain.RemoteFailureTrust}), kind: domain.RemoteFailureTrust, want: domain.RemoteAvailabilityUnreachable}, | |
| 26 | + | {name: "conflicting policy is incompatible", err: fmt.Errorf("bind: %w", ports.BrokerError{Code: ports.BrokerErrorConflictingPolicy}), kind: domain.RemoteFailureIncompatible, want: domain.RemoteAvailabilityIncompatible}, | |
| 27 | + | {name: "incompatible broker error", err: ports.BrokerError{Code: ports.BrokerErrorIncompatible}, kind: domain.RemoteFailureIncompatible, want: domain.RemoteAvailabilityIncompatible}, | |
| 28 | + | {name: "unavailable broker error stays transport", err: ports.BrokerError{Code: ports.BrokerErrorUnavailable}, kind: domain.RemoteFailureTransport, want: domain.RemoteAvailabilityUnreachable}, | |
| 25 | 29 | } | |
| 26 | 30 | for _, tt := range tests { | |
| 27 | 31 | t.Run(tt.name, func(t *testing.T) { | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | "github.com/stretchr/testify/require" | 9 | "github.com/stretchr/testify/require" | ||
| 10 | 10 | ||||
| 11 | "github.com/bnema/vev/internal/domain" | 11 | "github.com/bnema/vev/internal/domain" | ||
| 12 | + | "github.com/bnema/vev/internal/ports" | |||
| 12 | ) | 13 | ) | ||
| 13 | 14 | ||||
| 14 | func TestObservationOutcomeKeepsTypedDialFailure(t *testing.T) { | 15 | func TestObservationOutcomeKeepsTypedDialFailure(t *testing.T) { | ||
| @@ -22,6 +23,9 @@ func TestObservationOutcomeKeepsTypedDialFailure(t *testing.T) { | |||||
| 22 | {name: "deadline is timeout", err: context.DeadlineExceeded, kind: domain.RemoteFailureTimeout, want: domain.RemoteAvailabilityUnreachable}, | 23 | {name: "deadline is timeout", err: context.DeadlineExceeded, kind: domain.RemoteFailureTimeout, want: domain.RemoteAvailabilityUnreachable}, | ||
| 23 | {name: "wrapped authentication", err: fmt.Errorf("dial: %w", domain.RemoteFailure{Kind: domain.RemoteFailureAuthentication, Err: errors.New("exit 255")}), kind: domain.RemoteFailureAuthentication, want: domain.RemoteAvailabilityAuthFailed}, | 24 | {name: "wrapped authentication", err: fmt.Errorf("dial: %w", domain.RemoteFailure{Kind: domain.RemoteFailureAuthentication, Err: errors.New("exit 255")}), kind: domain.RemoteFailureAuthentication, want: domain.RemoteAvailabilityAuthFailed}, | ||
| 24 | {name: "wrapped trust", err: fmt.Errorf("dial: %w", domain.RemoteFailure{Kind: domain.RemoteFailureTrust}), kind: domain.RemoteFailureTrust, want: domain.RemoteAvailabilityUnreachable}, | 25 | {name: "wrapped trust", err: fmt.Errorf("dial: %w", domain.RemoteFailure{Kind: domain.RemoteFailureTrust}), kind: domain.RemoteFailureTrust, want: domain.RemoteAvailabilityUnreachable}, | ||
| 26 | + | {name: "conflicting policy is incompatible", err: fmt.Errorf("bind: %w", ports.BrokerError{Code: ports.BrokerErrorConflictingPolicy}), kind: domain.RemoteFailureIncompatible, want: domain.RemoteAvailabilityIncompatible}, | |||
| 27 | + | {name: "incompatible broker error", err: ports.BrokerError{Code: ports.BrokerErrorIncompatible}, kind: domain.RemoteFailureIncompatible, want: domain.RemoteAvailabilityIncompatible}, | |||
| 28 | + | {name: "unavailable broker error stays transport", err: ports.BrokerError{Code: ports.BrokerErrorUnavailable}, kind: domain.RemoteFailureTransport, want: domain.RemoteAvailabilityUnreachable}, | |||
| 25 | } | 29 | } | ||
| 26 | for _, tt := range tests { | 30 | for _, tt := range tests { | ||
| 27 | t.Run(tt.name, func(t *testing.T) { | 31 | t.Run(tt.name, func(t *testing.T) { | ||