test(handlers): add coverage for ProviderEndpointGone #2826

Merged
devops-engineer merged 3 commits from fix/provider-endpoint-gone-coverage into main 2026-06-14 07:15:28 +00:00
4 changed files with 68 additions and 6 deletions
@@ -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")
}
}