diff --git a/pkg/exchange/auto.go b/pkg/exchange/auto.go index e8397f56..27e06834 100644 --- a/pkg/exchange/auto.go +++ b/pkg/exchange/auto.go @@ -163,6 +163,11 @@ type ExchangeOutcome struct { ExchangeID string UtilizationPct float64 Error string + // OutcomeUnknown is true when the accept response was lost, so the exchange + // may have executed. It must be reconciled before any retry. The outcome + // stays in AutoExchangeResult.Failed (readers of Failed still see it) and + // halts the run (no further exchanges are attempted). + OutcomeUnknown bool // Simulated is true when RunAutoExchangeParams.DryRun was set. The outcome // was analyzed and quoted but no record was saved and no exchange was executed. Simulated bool @@ -573,6 +578,7 @@ func processAutoExchange(ctx context.Context, params RunAutoExchangeParams, rec logging.Errorf("exchange execution failed for %s: %v", rec.SourceRIID, execErr) outcome.Error = execErr.Error() if errors.Is(execErr, common.ErrOutcomeUnknown) { + outcome.OutcomeUnknown = true // The exchange may have happened: keep the record processing (still // counted against the cap) and stop, instead of marking it failed. return outcome, true diff --git a/pkg/exchange/outcome_test.go b/pkg/exchange/outcome_test.go index ba627a29..f5ca053e 100644 --- a/pkg/exchange/outcome_test.go +++ b/pkg/exchange/outcome_test.go @@ -3,6 +3,7 @@ package exchange import ( "context" "errors" + "fmt" "math/big" "net" "net/http" @@ -111,3 +112,32 @@ func TestRunAutoExchange_UnknownAcceptLeavesRecordProcessingAndHalts(t *testing. require.NotEmpty(t, store.savedRecords) assert.NotEqual(t, "failed", store.savedRecords[0].Status) } + +func TestRunAutoExchange_OutcomeUnknownFlag(t *testing.T) { + t.Parallel() + cases := []struct { + name string + execErr error + wantUnknown bool + wantFailed int + }{ + {"wrapped unknown sets the flag", fmt.Errorf("x: %w", common.ErrOutcomeUnknown), true, 0}, + {"plain error leaves it false", errors.New("boom"), false, 1}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + store := &mockExchangeStore{dailySpend: "0"} + client := &mockExchangeClient{quoteResult: defaultQuote(), executeErr: tc.execErr} + params := defaultParams(store, client) + params.Config.Mode = "auto" + + result, err := RunAutoExchange(context.Background(), params) + require.NoError(t, err) + assert.Equal(t, 1, client.executeCalls) + assert.Equal(t, tc.wantFailed, store.failCalls) + require.Len(t, result.Failed, 1) + assert.Equal(t, tc.wantUnknown, result.Failed[0].OutcomeUnknown) + }) + } +}