df58bc25a3
---ci--- project: orca phase: 3 milestone: v0.3 status: complete requirements: covered: [REQ-022, REQ-030, REQ-032] partial: [] ---/ci--- v0.3 milestone merged to main. Includes all v0.2 work (P08-P10) that was previously on the milestone branch but not yet merged to main, plus the v0.3 completion work (iter.Seq streaming + doctor network/db). v0.2 phases included: P08 (mTLS), P09 (scheduling), P10 (security scan). v0.3 phases: P0 (pre-execution), P1 (iter.Seq streaming), P2 (doctor), P3 (final review+ship). Total: 40 requirements, all complete. No new go.mod dependencies. Full test suite passes under -race. gofmt + go vet clean.
134 lines
3.5 KiB
Go
134 lines
3.5 KiB
Go
package transport
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestIdempotencyStorePutGet(t *testing.T) {
|
|
s := NewIdempotencyStore()
|
|
if _, ok := s.Get("missing"); ok {
|
|
t.Fatal("expected missing key to return ok=false")
|
|
}
|
|
s.Put("k1", "job-1")
|
|
if jobID, ok := s.Get("k1"); !ok || jobID != "job-1" {
|
|
t.Errorf("Get(k1): got (%q, %v), want (job-1, true)", jobID, ok)
|
|
}
|
|
}
|
|
|
|
func TestIdempotencyStoreExpiry(t *testing.T) {
|
|
s := NewIdempotencyStore()
|
|
// Manually insert an expired entry.
|
|
s.entries["expired"] = dedupeEntry{
|
|
key: "expired",
|
|
jobID: "old-job",
|
|
expiresAt: time.Now().Add(-1 * time.Minute),
|
|
}
|
|
if _, ok := s.Get("expired"); ok {
|
|
t.Fatal("expected expired entry to return ok=false")
|
|
}
|
|
if _, exists := s.entries["expired"]; exists {
|
|
t.Error("expected expired entry to be removed by Get")
|
|
}
|
|
}
|
|
|
|
func TestIdempotencyStoreContext(t *testing.T) {
|
|
ctx := WithIdempotencyKey(context.Background(), "key-1")
|
|
if got := IdempotencyKeyFromContext(ctx); got != "key-1" {
|
|
t.Errorf("IdempotencyKeyFromContext: got %q, want key-1", got)
|
|
}
|
|
ctx2 := context.Background()
|
|
if got := IdempotencyKeyFromContext(ctx2); got != "" {
|
|
t.Errorf("IdempotencyKeyFromContext(empty): got %q, want \"\"", got)
|
|
}
|
|
}
|
|
|
|
func TestRetrySucceedsAfterTransient(t *testing.T) {
|
|
calls := 0
|
|
got, err := Do(context.Background(), DefaultRetryPolicy(),
|
|
func(_ context.Context, attempt int) (string, bool, error) {
|
|
calls++
|
|
if attempt < 3 {
|
|
return "", true, errors.New("connection refused: try again")
|
|
}
|
|
return "ok", true, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Do: %v", err)
|
|
}
|
|
if got != "ok" {
|
|
t.Errorf("Do: got %q, want ok", got)
|
|
}
|
|
if calls != 3 {
|
|
t.Errorf("Do: got %d calls, want 3", calls)
|
|
}
|
|
}
|
|
|
|
func TestRetryNoKeyOnTransient(t *testing.T) {
|
|
// Without an idempotency key AND a non-idempotent verb, a
|
|
// transient error on the first attempt must NOT retry (REQ-037).
|
|
calls := 0
|
|
_, err := Do(context.Background(), DefaultRetryPolicy(),
|
|
func(_ context.Context, _ int) (string, bool, error) {
|
|
calls++
|
|
return "", false, errors.New("connection refused")
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if calls != 1 {
|
|
t.Errorf("expected 1 call (no retry without key), got %d", calls)
|
|
}
|
|
}
|
|
|
|
func TestRetryPermanentError(t *testing.T) {
|
|
calls := 0
|
|
_, err := Do(context.Background(), DefaultRetryPolicy(),
|
|
func(_ context.Context, _ int) (string, bool, error) {
|
|
calls++
|
|
return "", true, ErrPermanent
|
|
})
|
|
if !errors.Is(err, ErrPermanent) {
|
|
t.Errorf("expected ErrPermanent, got %v", err)
|
|
}
|
|
if calls != 1 {
|
|
t.Errorf("expected 1 call (permanent = no retry), got %d", calls)
|
|
}
|
|
}
|
|
|
|
func TestRetryContextCancel(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // cancel immediately
|
|
calls := 0
|
|
_, err := Do(ctx, DefaultRetryPolicy(),
|
|
func(_ context.Context, _ int) (string, bool, error) {
|
|
calls++
|
|
return "", true, errors.New("EOF")
|
|
})
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Errorf("expected context.Canceled, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIsTransient(t *testing.T) {
|
|
cases := []struct {
|
|
err error
|
|
want bool
|
|
}{
|
|
{nil, false},
|
|
{errors.New("connection refused"), true},
|
|
{errors.New("i/o timeout"), true},
|
|
{errors.New("EOF"), true},
|
|
{errors.New("no such host"), true},
|
|
{errors.New("connection reset by peer"), true},
|
|
{errors.New("invalid spec"), false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := IsTransient(c.err); got != c.want {
|
|
t.Errorf("IsTransient(%v): got %v, want %v", c.err, got, c.want)
|
|
}
|
|
}
|
|
}
|