@@ -122,11 +122,12 @@ func postAgent(t *testing.T, c *controllerV1, ctx context.Context, wsID, session
124// TestPostAgent_ReturnsOKOnContextCanceled verifies that when another
124// TestPostAgent_ReturnsOKOnContextCanceled verifies that when another
125−// client cancels the session mid-turn, the prompting client's still
125+// client cancels the session mid-turn, the prompting client's POST is
126−// open POST receives 200 (not 500). The agent surfaces the
126+// unaffected: SendMessage is fire-and-forget, so the handler returns
127+// 200 immediately without waiting for the turn. A run that later
128+// returns context.Canceled never surfaces as a 500 to the prompter;
127−// FinishReasonCanceled marker to every SSE subscriber via the
129+// the FinishReasonCanceled marker reaches SSE subscribers via the
128−// assistant message; the HTTP response from the prompter should not
130+// assistant message instead.
129−// double as an error signal.
130func TestPostAgent_ReturnsOKOnContextCanceled(t *testing.T) {
131func TestPostAgent_ReturnsOKOnContextCanceled(t *testing.T) {
131 t.Parallel()
132 t.Parallel()
@@ -135,33 +136,26 @@ func TestPostAgent_ReturnsOKOnContextCanceled(t *testing.T) {
136 c, wsID := buildAgentWorkspace(t, coord)
137 c, wsID := buildAgentWorkspace(t, coord)
138− done := make(chan *httptest.ResponseRecorder, 1)
139+ // The handler returns immediately, before the dispatched run is
139− go func() {
140+ // released, because the run no longer owns the HTTP response.
140− done <- postAgent(t, c, t.Context(), wsID, "S1")
141+ rec := postAgent(t, c, t.Context(), wsID, "S1")
141− }()
142+ require.Equal(t, http.StatusOK, rec.Code, "fire-and-forget SendMessage must return 200 without waiting for the run")
143− // Wait until Run is in flight, then release it to return
144+ // The run is dispatched on a goroutine; let it return
144− // context.Canceled.
145+ // context.Canceled. Nothing from that path reaches the (already
146+ // returned) handler.
146 case <-coord.entered:
148 case <-coord.entered:
147 case <-time.After(2 * time.Second):
149 case <-time.After(2 * time.Second):
148− t.Fatal("coordinator Run was never entered")
150+ t.Fatal("dispatched run was never entered")
150 close(coord.release)
152 close(coord.release)
154− require.Equal(t, http.StatusOK, rec.Code, "context.Canceled from another client's cancel must not surface as 500")
155− case <-time.After(2 * time.Second):
156− t.Fatal("handler did not return after coordinator returned context.Canceled")
160−// TestPostAgent_DetachesRequestContext verifies that canceling the
155+// TestPostAgent_DetachesRequestContext verifies that the dispatched run
161−// prompting client's HTTP request context does not cancel the
156+// is bound to the workspace context, not the prompting client's HTTP
162−// in-flight agent run. The coordinator must observe a context whose
157+// request context. Canceling the request context must neither cancel
163−// Done channel never fires from the request side; only the explicit
158+// the run nor be observed by the coordinator.
164−// cancel endpoint may end the run.
165func TestPostAgent_DetachesRequestContext(t *testing.T) {
159func TestPostAgent_DetachesRequestContext(t *testing.T) {
166 t.Parallel()
160 t.Parallel()
@@ -172,46 +166,31 @@ func TestPostAgent_DetachesRequestContext(t *testing.T) {
173 reqCtx, cancelReq := context.WithCancel(context.Background())
167 reqCtx, cancelReq := context.WithCancel(context.Background())
175− done := make(chan *httptest.ResponseRecorder, 1)
169+ // The handler returns immediately; the run keeps executing on its
176− go func() {
170+ // own goroutine bound to the workspace context.
177− done <- postAgent(t, c, reqCtx, wsID, "S1")
171+ rec := postAgent(t, c, reqCtx, wsID, "S1")
178− }()
172+ require.Equal(t, http.StatusOK, rec.Code)
180− // Wait until Run is in flight, then drop the prompting client.
182 case <-coord.entered:
175 case <-coord.entered:
183 case <-time.After(2 * time.Second):
176 case <-time.After(2 * time.Second):
184− t.Fatal("coordinator Run was never entered")
177+ t.Fatal("dispatched run was never entered")
180+ // Drop the prompting client. This must not reach the run.
186 cancelReq()
181 cancelReq()
188− // The captured ctx must be detached: context.WithoutCancel
189− // returns a ctx with Done() == nil so request cancellation cannot
191 got := coord.capturedCtx()
183 got := coord.capturedCtx()
192 require.NotNil(t, got)
184 require.NotNil(t, got)
193− require.Nil(t, got.Done(), "coordinator ctx must be detached from r.Context() via context.WithoutCancel")
185+ // Compare by identity (pointer), not reflect.DeepEqual: deep
186+ // comparison would traverse context internals that the runtime
187+ // mutates concurrently.
188+ require.False(t, got == reqCtx, "run ctx must not be the request ctx")
194− require.NoError(t, got.Err(), "coordinator ctx must not inherit cancellation from the dropped request")
189+ require.NoError(t, got.Err(), "run ctx must not inherit cancellation from the dropped request")
196− // Confirm Run is still running: it should not have completed
191+ // Release the run so it returns cleanly.
197− // just because the request ctx was canceled.
200− t.Fatal("handler returned before run completed; request ctx cancellation leaked into the run")
201− case <-time.After(50 * time.Millisecond):
204− // Release the run; the handler should now complete cleanly.
205 close(coord.release)
192 close(coord.release)
206− select {
193+ require.Eventually(t, func() bool {
208− // Writing to a recorder whose request ctx was canceled
209− // still works; in production the TCP write would silently
210− // fail, which is fine because the run already completed and
211− // SSE subscribers have the result.
212− require.Equal(t, http.StatusOK, rec.Code)
213− case <-time.After(2 * time.Second):
214− t.Fatal("handler did not return after release")
216− require.Equal(t, int32(1), coord.ranCount.Load())
194+ return coord.ranCount.Load() == 1
195+ }, 2*time.Second, 10*time.Millisecond)