@@ -2,6 +2,7 @@ package cmd
22
33import (
44 "bytes"
5+ "errors"
56 "testing"
67 "time"
78
@@ -307,6 +308,93 @@ func TestRunStream_RunIDSuppressesLiveMessagesAndPrintsRunComplete(t *testing.T)
307308 require.Equal(t, "streamed prefix final", buf.String())
308309}
309310
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) {
318+ t.Parallel()
319+
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,
324+ SessionID: "S",
325+ RunID: "run-other",
326+ Error: errors.New("foreign boom"),
327+ }}, nil)
328+ require.NoError(t, err, "foreign RunID error must not abort our run")
329+ require.False(t, done)
330+
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,
334+ SessionID: "other",
335+ RunID: "run-other",
336+ Error: errors.New("foreign boom"),
337+ }}, nil)
338+ require.NoError(t, err, "foreign RunID error must not abort our run")
339+ require.False(t, done)
340+
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,
344+ RunID: "run-other",
345+ Error: errors.New("foreign boom"),
346+ }}, nil)
347+ require.NoError(t, err, "foreign RunID error must not abort our run")
348+ require.False(t, done)
349+
350+ // Matching RunID is fatal.
351+ done, err = s.handle(pubsub.Event[proto.AgentEvent]{Payload: proto.AgentEvent{
352+ Type: proto.AgentEventTypeError,
353+ SessionID: "S",
354+ RunID: "run-mine",
355+ Error: errors.New("my boom"),
356+ }}, nil)
357+ require.Error(t, err, "matching RunID error must be fatal")
358+ require.True(t, done)
359+}
360+
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) {
367+ t.Parallel()
368+
369+ s := &runStream{sessionID: "S", out: &bytes.Buffer{}, read: map[string]int{}}
370+
371+ // Empty RunID for another session is ignored.
372+ done, err := s.handle(pubsub.Event[proto.AgentEvent]{Payload: proto.AgentEvent{
373+ Type: proto.AgentEventTypeError,
374+ SessionID: "other",
375+ Error: errors.New("foreign boom"),
376+ }}, nil)
377+ require.NoError(t, err, "error for another session must not abort our run")
378+ require.False(t, done)
379+
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"),
384+ }}, nil)
385+ require.NoError(t, err, "error with no session must not abort our run")
386+ require.False(t, done)
387+
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,
391+ SessionID: "S",
392+ Error: errors.New("my boom"),
393+ }}, nil)
394+ require.Error(t, err, "error for our own session must be fatal")
395+ require.True(t, done)
396+}
397+
310398// TestRunStream_NoRunIDFallsBackToSessionID preserves the older
311399// behaviour for callers (and tests) that don't supply a RunID:
312400// SessionID-only matching still terminates the stream on the