@@ -2,6 +2,7 @@ package cmd
@@ -307,6 +308,93 @@ func TestRunStream_RunIDSuppressesLiveMessagesAndPrintsRunComplete(t *testing.T)
307 require.Equal(t, "streamed prefix final", buf.String())
308 require.Equal(t, "streamed prefix final", buf.String())
311+// TestRunStream_AgentErrorRunIDFiltersForeign verifies that an async
312+// agent error carrying a non-empty RunID is fatal only when it matches
313+// our run. A foreign RunID is ignored regardless of the event's
314+// SessionID, because RunID is the authoritative correlator and async
315+// errors share the agent event channel: without strict RunID matching
316+// an unrelated workspace failure would abort our run.
317+func TestRunStream_AgentErrorRunIDFiltersForeign(t *testing.T) {
320+ // Foreign RunID with a matching session is still foreign.
321+ s := &runStream{sessionID: "S", runID: "run-mine", out: &bytes.Buffer{}, read: map[string]int{}}
322+ done, err := s.handle(pubsub.Event[proto.AgentEvent]{Payload: proto.AgentEvent{
323+ Type: proto.AgentEventTypeError,
326+ Error: errors.New("foreign boom"),
328+ require.NoError(t, err, "foreign RunID error must not abort our run")
329+ require.False(t, done)
331+ // Foreign RunID with a different session is ignored.
332+ done, err = s.handle(pubsub.Event[proto.AgentEvent]{Payload: proto.AgentEvent{
333+ Type: proto.AgentEventTypeError,
336+ Error: errors.New("foreign boom"),
338+ require.NoError(t, err, "foreign RunID error must not abort our run")
339+ require.False(t, done)
341+ // Foreign RunID with a missing session is ignored.
342+ done, err = s.handle(pubsub.Event[proto.AgentEvent]{Payload: proto.AgentEvent{
343+ Type: proto.AgentEventTypeError,
345+ Error: errors.New("foreign boom"),
347+ require.NoError(t, err, "foreign RunID error must not abort our run")
348+ require.False(t, done)
350+ // Matching RunID is fatal.
351+ done, err = s.handle(pubsub.Event[proto.AgentEvent]{Payload: proto.AgentEvent{
352+ Type: proto.AgentEventTypeError,
355+ Error: errors.New("my boom"),
357+ require.Error(t, err, "matching RunID error must be fatal")
358+ require.True(t, done)
361+// TestRunStream_AgentErrorNoRunIDFiltersBySession verifies the
362+// compatibility fallback: when the event carries no RunID, attribution
363+// falls back to SessionID. An error for another session or with an
364+// empty session is ignored, while an error for our own session is fatal
365+// so a real failure is never dropped.
366+func TestRunStream_AgentErrorNoRunIDFiltersBySession(t *testing.T) {
369+ s := &runStream{sessionID: "S", out: &bytes.Buffer{}, read: map[string]int{}}
371+ // Empty RunID for another session is ignored.
372+ done, err := s.handle(pubsub.Event[proto.AgentEvent]{Payload: proto.AgentEvent{
373+ Type: proto.AgentEventTypeError,
375+ Error: errors.New("foreign boom"),
377+ require.NoError(t, err, "error for another session must not abort our run")
378+ require.False(t, done)
380+ // Empty RunID with an empty session is ignored.
381+ done, err = s.handle(pubsub.Event[proto.AgentEvent]{Payload: proto.AgentEvent{
382+ Type: proto.AgentEventTypeError,
383+ Error: errors.New("foreign boom"),
385+ require.NoError(t, err, "error with no session must not abort our run")
386+ require.False(t, done)
388+ // Empty RunID with a matching session is fatal.
389+ done, err = s.handle(pubsub.Event[proto.AgentEvent]{Payload: proto.AgentEvent{
390+ Type: proto.AgentEventTypeError,
392+ Error: errors.New("my boom"),
394+ require.Error(t, err, "error for our own session must be fatal")
395+ require.True(t, done)
310// TestRunStream_NoRunIDFallsBackToSessionID preserves the older
398// TestRunStream_NoRunIDFallsBackToSessionID preserves the older
311// behaviour for callers (and tests) that don't supply a RunID:
399// behaviour for callers (and tests) that don't supply a RunID:
312// SessionID-only matching still terminates the stream on the
400// SessionID-only matching still terminates the stream on the