test(handlers): add coverage for ProviderEndpointGone #2826
@@ -415,7 +415,9 @@ func (h *WorkspaceHandler) ProxyA2A(c *gin.Context) {
|
||||
detached := context.WithoutCancel(ctx)
|
||||
budget := canvasA2ASyncBudget() // local copy for the time.After below
|
||||
done := make(chan a2aResult, 1)
|
||||
h.asyncWG.Add(1)
|
||||
go func() {
|
||||
defer h.asyncWG.Done()
|
||||
s, b, pe := h.proxyA2ARequest(detached, workspaceID, body, callerID, true, isCanvasUser)
|
||||
done <- a2aResult{s, b, pe}
|
||||
}()
|
||||
|
||||
@@ -2976,6 +2976,7 @@ func TestProxyA2A_CanvasCapAndQueue(t *testing.T) {
|
||||
allowLoopbackForTest(t)
|
||||
broadcaster := newTestBroadcaster()
|
||||
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
||||
waitForHandlerAsyncBeforeDBCleanup(t, handler)
|
||||
|
||||
// Agent that holds the connection PAST the budget (bounded sleep — no
|
||||
// deadlock with agentServer.Close()). 600ms >> the 100ms budget, so the
|
||||
@@ -3257,7 +3258,7 @@ func TestProxyA2A_CanvasCapAndQueue_EndToEndContract(t *testing.T) {
|
||||
var sawA2AResponse bool
|
||||
var sawResponseBodyContent bool
|
||||
for time.Now().Before(deadline) {
|
||||
for _, c := range rec.calls {
|
||||
for _, c := range rec.snapshotCalls() {
|
||||
if c.eventType == "A2A_RESPONSE" && c.workspaceID == "ws-e2e" {
|
||||
// Assert the originating message_id is carried so the
|
||||
// canvas WS handler can attach the reply to the right
|
||||
@@ -3289,10 +3290,10 @@ func TestProxyA2A_CanvasCapAndQueue_EndToEndContract(t *testing.T) {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
if !sawA2AResponse {
|
||||
t.Fatalf("expected A2A_RESPONSE broadcast for ws-e2e with message_id=msg-e2e-001 within 2s; recorded: %+v", rec.calls)
|
||||
t.Fatalf("expected A2A_RESPONSE broadcast for ws-e2e with message_id=msg-e2e-001 within 2s; recorded: %+v", rec.snapshotCalls())
|
||||
}
|
||||
if !sawResponseBodyContent {
|
||||
t.Fatalf("expected A2A_RESPONSE payload to carry the agent's actual reply content (`reply:\"hello\"`) so the canvas can render it; recorded: %+v", rec.calls)
|
||||
t.Fatalf("expected A2A_RESPONSE payload to carry the agent's actual reply content (`reply:\"hello\"`) so the canvas can render it; recorded: %+v", rec.snapshotCalls())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3319,13 +3320,13 @@ func TestLogA2ASuccess_BroadcastsForCanvasUser(t *testing.T) {
|
||||
time.Sleep(80 * time.Millisecond)
|
||||
|
||||
got := false
|
||||
for _, c := range rec.calls {
|
||||
for _, c := range rec.snapshotCalls() {
|
||||
if c.eventType == "A2A_RESPONSE" && c.workspaceID == "ws-cu" {
|
||||
got = true
|
||||
}
|
||||
}
|
||||
if !got {
|
||||
t.Fatalf("expected A2A_RESPONSE broadcast for authenticated canvas user; recorded: %+v", rec.calls)
|
||||
t.Fatalf("expected A2A_RESPONSE broadcast for authenticated canvas user; recorded: %+v", rec.snapshotCalls())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3345,7 +3346,7 @@ func TestLogA2ASuccess_NoBroadcastForWorkspaceCaller(t *testing.T) {
|
||||
handler.logA2ASuccess(context.Background(), "ws-peer", "ws-other", false, []byte(`{}`), []byte(`{"result":"x"}`), "message/send", 200, 12)
|
||||
time.Sleep(80 * time.Millisecond)
|
||||
|
||||
for _, c := range rec.calls {
|
||||
for _, c := range rec.snapshotCalls() {
|
||||
if c.eventType == "A2A_RESPONSE" {
|
||||
t.Fatalf("unexpected A2A_RESPONSE broadcast for a workspace-to-workspace caller")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -849,6 +850,7 @@ func TestScanSessionSearchRows_RowsErrPropagates(t *testing.T) {
|
||||
// recordingBroadcaster records every BroadcastOnly invocation so a test
|
||||
// can assert what made it onto the wire. Implements events.EventEmitter.
|
||||
type recordingBroadcaster struct {
|
||||
mu sync.Mutex
|
||||
calls []recordedBroadcast
|
||||
}
|
||||
|
||||
@@ -867,6 +869,8 @@ func (c *recordingBroadcaster) BroadcastOnly(workspaceID string, eventType strin
|
||||
// what hub.Broadcast does before sending). json.RawMessage values in
|
||||
// the source payload survive the round-trip as their underlying JSON.
|
||||
raw, err := json.Marshal(payload)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.calls = append(c.calls, recordedBroadcast{workspaceID, eventType, nil})
|
||||
return
|
||||
@@ -879,6 +883,17 @@ func (c *recordingBroadcaster) BroadcastOnly(workspaceID string, eventType strin
|
||||
c.calls = append(c.calls, recordedBroadcast{workspaceID, eventType, out})
|
||||
}
|
||||
|
||||
// snapshotCalls returns a copy of the recorded calls under the mutex so
|
||||
// tests can assert concurrently with BroadcastOnly without triggering the
|
||||
// -race detector.
|
||||
func (c *recordingBroadcaster) snapshotCalls() []recordedBroadcast {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
out := make([]recordedBroadcast, len(c.calls))
|
||||
copy(out, c.calls)
|
||||
return out
|
||||
}
|
||||
|
||||
// TestLogActivity_Broadcast_IncludesRequestAndResponseBodies pins the
|
||||
// fix for the canvas Agent Comms "Delegating to <peer>" boilerplate
|
||||
// regression: without request_body/response_body in the live broadcast,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ProviderEndpointGone is wired for both GET and PUT
|
||||
// /workspaces/:id/provider. The handler has no side effects and no
|
||||
// dependencies, so a single test pins the retirement contract.
|
||||
func TestProviderEndpointGone(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/workspaces/ws-1/provider", nil)
|
||||
|
||||
ProviderEndpointGone(c)
|
||||
|
||||
if w.Code != http.StatusGone {
|
||||
t.Fatalf("expected 410, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Code string `json:"code"`
|
||||
Error string `json:"error"`
|
||||
Issue string `json:"issue"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("body parse: %v", err)
|
||||
}
|
||||
if body.Code != "PROVIDER_ENDPOINT_RETIRED" {
|
||||
t.Errorf("code: expected PROVIDER_ENDPOINT_RETIRED, got %q", body.Code)
|
||||
}
|
||||
if body.Issue != "internal#718" {
|
||||
t.Errorf("issue: expected internal#718, got %q", body.Issue)
|
||||
}
|
||||
if body.Error == "" {
|
||||
t.Errorf("error message should be present")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user