package main import ( "context" "errors" "fmt" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "go-micro.dev/v6/flow" ) // TestUniverseHarnessContract makes the 0→hero harness part of the ordinary // Go test contract. The harness boots real services, a durable workflow, an // agent, scoped state, and the A2A gateway with only the LLM mocked; running it // here prevents the full services → agents → workflows lifecycle from silently // drifting while developers rely on `go test ./...`. func TestUniverseHarnessContract(t *testing.T) { if testing.Short() { t.Skip("universe harness boots an end-to-end system; skipped with -short") } if code := runUniverse("mock"); code != 0 { t.Fatalf("universe harness exited with code %d", code) } } func TestA2AReachableRetriesTransientTimeout(t *testing.T) { var calls int64 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got, want := r.URL.Path, "/agents/concierge"; got != want { t.Fatalf("path = %q, want %q", got, want) } if atomic.AddInt64(&calls, 1) == 1 { time.Sleep(5 * time.Second) return } w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, `{"jsonrpc":"2.0","id":1,"result":{"kind":"task","id":"task-1","contextId":"ctx-1","status":{"state":"completed"},"artifacts":[{"artifactId":"artifact-1","parts":[{"kind":"text","text":"concierge reachable"}]}]}}`) })) defer srv.Close() ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second) defer cancel() if err := a2aReachable(ctx, srv.URL, "concierge"); err != nil { t.Fatalf("a2aReachable returned error: %v", err) } if got := atomic.LoadInt64(&calls); got < 2 { t.Fatalf("A2A calls = %d, want retry after transient timeout", got) } } func TestNotifyStepCompletesAfterObservedSideEffectTimeout(t *testing.T) { ntf := new(Notify) before := atomic.LoadInt64(&ntf.sent) go func() { time.Sleep(30 * time.Millisecond) var rsp SendResponse if err := ntf.Send(context.Background(), &SendRequest{ To: "buyer@acme.com", Message: "Your order is confirmed.", }, &rsp); err != nil { t.Errorf("send notification: %v", err) } }() out, err := completeNotifyOnObservedSideEffect( context.Background(), flow.State{Data: []byte(`{"order":"order-1"}`)}, ntf, before, time.Second, errors.New("client observed timeout"), ) if err != nil { t.Fatalf("notify completion returned error: %v", err) } if got := out.String(); got != "Buyer notified." { t.Fatalf("result = %q, want Buyer notified.", got) } if got := atomic.LoadInt64(&ntf.sent); got != 1 { t.Fatalf("notifications sent = %d, want 1", got) } var rsp SendResponse if err := ntf.Send(context.Background(), &SendRequest{ To: "buyer@acme.com", Message: "Your order is confirmed.", }, &rsp); err != nil { t.Fatalf("duplicate send: %v", err) } if got := atomic.LoadInt64(&ntf.sent); got != 1 { t.Fatalf("notifications sent after duplicate = %d, want 1", got) } } func TestNotifyStepWaitsForObservedSideEffectAfterCanceledDispatchContext(t *testing.T) { ntf := new(Notify) before := atomic.LoadInt64(&ntf.sent) ctx, cancel := context.WithCancel(context.Background()) cancel() go func() { time.Sleep(30 * time.Millisecond) var rsp SendResponse if err := ntf.Send(context.Background(), &SendRequest{ To: "buyer@acme.com", Message: "Your order is confirmed.", }, &rsp); err != nil { t.Errorf("send notification: %v", err) } }() out, err := completeNotifyOnObservedSideEffect( ctx, flow.State{Data: []byte(`{"order":"order-1"}`)}, ntf, before, time.Second, errors.New("client observed timeout"), ) if err != nil { t.Fatalf("notify completion returned error: %v", err) } if got := out.String(); got != "Buyer notified." { t.Fatalf("result = %q, want Buyer notified.", got) } if got := atomic.LoadInt64(&ntf.sent); got != 1 { t.Fatalf("notifications sent = %d, want 1", got) } } func TestNotifyStepRejectsClaimedCompletionWithoutSideEffect(t *testing.T) { ntf := new(Notify) before := atomic.LoadInt64(&ntf.sent) _, err := completeNotifyOnObservedSideEffect( context.Background(), flow.State{Data: []byte(`claimed success`)}, ntf, before, 25*time.Millisecond, nil, ) if err == nil { t.Fatal("notify completion returned nil, want missing buyer notification error") } want := `concierge completed without notifying buyer: notify count stayed at 0; expected recipient buyer@acme.com, buyer, or buyer-of-order-; no rejected notify call observed` if got := err.Error(); got != want { t.Fatalf("error = %q, want %q", got, want) } } func TestNotifySuppressesEquivalentConfirmationMessages(t *testing.T) { ntf := new(Notify) ctx := context.Background() for _, req := range []*SendRequest{ {To: "buyer@acme.com", Message: "Your order order-1 has been confirmed."}, {To: "buyer@acme.com", Message: "order-1 confirmed"}, } { var rsp SendResponse if err := ntf.Send(ctx, req, &rsp); err != nil { t.Fatalf("send notification %q: %v", req.Message, err) } if !rsp.Sent { t.Fatalf("send notification %q did not report sent", req.Message) } } if got := atomic.LoadInt64(&ntf.sent); got != 1 { t.Fatalf("equivalent confirmation notifications sent = %d, want 1", got) } } func TestNotifyAcceptsBuyerAlias(t *testing.T) { ntf := new(Notify) ctx := context.Background() var rsp SendResponse if err := ntf.Send(ctx, &SendRequest{ To: "buyer", Message: "Your order order-1 has been confirmed.", }, &rsp); err != nil { t.Fatalf("send buyer alias notification: %v", err) } if !rsp.Sent { t.Fatal("buyer alias notification did not report sent") } if got := atomic.LoadInt64(&ntf.sent); got != 1 { t.Fatalf("buyer alias notifications sent = %d, want 1", got) } if err := ntf.Send(ctx, &SendRequest{ To: "buyer@acme.com", Message: "order-1 confirmed", }, &rsp); err != nil { t.Fatalf("send canonical buyer notification: %v", err) } if !rsp.Sent { t.Fatal("canonical buyer notification did not report sent") } if got := atomic.LoadInt64(&ntf.sent); got != 1 { t.Fatalf("alias/canonical confirmation notifications sent = %d, want 1", got) } } func TestNotifyIgnoresNonBuyerRecipients(t *testing.T) { ntf := new(Notify) ctx := context.Background() var rsp SendResponse if err := ntf.Send(ctx, &SendRequest{ To: "order-1", Message: "order-1 confirmed", }, &rsp); err != nil { t.Fatalf("send non-buyer notification: %v", err) } if rsp.Sent { t.Fatal("non-buyer notification reported sent") } if got := atomic.LoadInt64(&ntf.sent); got != 0 { t.Fatalf("non-buyer notifications sent = %d, want 0", got) } if err := ntf.Send(ctx, &SendRequest{ To: "buyer@acme.com", Message: "Your order order-1 has been confirmed.", }, &rsp); err != nil { t.Fatalf("send buyer notification: %v", err) } if !rsp.Sent { t.Fatal("buyer notification did not report sent") } if got := atomic.LoadInt64(&ntf.sent); got != 1 { t.Fatalf("buyer notifications sent = %d, want 1", got) } } func TestNotifyAcceptsOrderScopedBuyerRecipient(t *testing.T) { ntf := new(Notify) ctx := context.Background() var rsp SendResponse if err := ntf.Send(ctx, &SendRequest{ To: "buyer-of-order-1", Message: "order-1 confirmed", }, &rsp); err != nil { t.Fatalf("send order-scoped buyer notification: %v", err) } if !rsp.Sent { t.Fatal("order-scoped buyer notification did not report sent") } if got := atomic.LoadInt64(&ntf.sent); got != 1 { t.Fatalf("order-scoped buyer notifications sent = %d, want 1", got) } if err := ntf.Send(ctx, &SendRequest{ To: "non-buyer", Message: "order-1 confirmed", }, &rsp); err != nil { t.Fatalf("send hyphenated non-buyer notification: %v", err) } if rsp.Sent { t.Fatal("hyphenated non-buyer notification reported sent") } if got := atomic.LoadInt64(&ntf.sent); got != 1 { t.Fatalf("notifications sent after hyphenated non-buyer = %d, want 1", got) } } func TestNotifyStepReportsRejectedRecipientDiagnostics(t *testing.T) { ntf := new(Notify) var rsp SendResponse if err := ntf.Send(context.Background(), &SendRequest{ To: "order-1", Message: "order-1 confirmed", }, &rsp); err != nil { t.Fatalf("send rejected notification: %v", err) } _, err := completeNotifyOnObservedSideEffect( context.Background(), flow.State{Data: []byte(`claimed success`)}, ntf, 0, 25*time.Millisecond, nil, ) if err == nil { t.Fatal("notify completion returned nil, want diagnostics") } want := `concierge completed without notifying buyer: notify count stayed at 0; expected recipient buyer@acme.com, buyer, or buyer-of-order-; last notify args to="order-1" message="order-1 confirmed"` if got := err.Error(); got != want { t.Fatalf("error = %q, want %q", got, want) } }