diff --git a/cmd/chat.go b/cmd/chat.go index 7417e9e..96e6618 100644 --- a/cmd/chat.go +++ b/cmd/chat.go @@ -19,8 +19,9 @@ import ( // contentOnly extracts the user-visible text from a chat NDJSON stream. // It skips UPDATE/CONTROL/DEBUG messages (internal reasoning/progress) // and only collects CONTENT message fragments, stripping stage preamble lines. -func contentOnly(ndjson string) string { +func contentOnly(ndjson string) (string, error) { var sb strings.Builder + var parseErrors int for _, line := range strings.Split(ndjson, "\n") { line = strings.TrimSpace(line) if line == "" { @@ -28,6 +29,7 @@ func contentOnly(ndjson string) string { } var resp components.ChatResponse if err := json.Unmarshal([]byte(line), &resp); err != nil { + parseErrors++ continue } for _, msg := range resp.Messages { @@ -42,7 +44,11 @@ func contentOnly(ndjson string) string { } } } - return sb.String() + result := sb.String() + if result == "" && parseErrors > 0 { + return "", fmt.Errorf("failed to parse %d response lines", parseErrors) + } + return result, nil } // NewCmdChat creates and returns the chat command. @@ -192,7 +198,10 @@ func executeChat(cmd *cobra.Command, chatReq components.ChatRequest, showSpinner return nil } - text := contentOnly(*resp.ChatRequestStream) + text, err := contentOnly(*resp.ChatRequestStream) + if err != nil { + return err + } if text == "" { return nil } diff --git a/cmd/chat_test.go b/cmd/chat_test.go index 1d6d3ff..ddc975f 100644 --- a/cmd/chat_test.go +++ b/cmd/chat_test.go @@ -11,6 +11,39 @@ import ( "github.com/stretchr/testify/require" ) +func TestContentOnly(t *testing.T) { + t.Run("valid NDJSON with CONTENT messages", func(t *testing.T) { + ndjson := `{"messages":[{"author":"GLEAN_AI","messageType":"CONTENT","fragments":[{"text":"Hello"}]}]} +{"messages":[{"author":"GLEAN_AI","messageType":"CONTENT","fragments":[{"text":" world"}]}]}` + result, err := contentOnly(ndjson) + require.NoError(t, err) + assert.Equal(t, "Hello world", result) + }) + + t.Run("all corrupt lines returns error", func(t *testing.T) { + ndjson := "{not json}\n{also bad}" + result, err := contentOnly(ndjson) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse 2 response lines") + assert.Empty(t, result) + }) + + t.Run("mixed valid and corrupt lines returns content", func(t *testing.T) { + ndjson := `{not json} +{"messages":[{"author":"GLEAN_AI","messageType":"CONTENT","fragments":[{"text":"valid"}]}]} +{also bad}` + result, err := contentOnly(ndjson) + require.NoError(t, err) + assert.Equal(t, "valid", result) + }) + + t.Run("empty input returns empty string no error", func(t *testing.T) { + result, err := contentOnly("") + require.NoError(t, err) + assert.Empty(t, result) + }) +} + func TestChatJSONPayloadSetsStreamTrue(t *testing.T) { fixtures := testutils.NewFixtures(t, "basic_chat_response.json") response := fixtures.LoadAsStream("basic_chat_response") @@ -126,8 +159,6 @@ How can I help? }) t.Run("chat with error response", func(t *testing.T) { - // Fully invalid NDJSON is silently skipped — agent-first design means - // we don't fail the command for malformed lines; we just produce no output. response := fixtures.LoadAsStream("error_response") _, cleanup := testutils.SetupTestWithResponse(t, response) defer cleanup() @@ -136,10 +167,11 @@ How can I help? cmd := NewCmdChat() cmd.SetOut(b) cmd.SetArgs([]string{"Test error"}) + cmd.SilenceUsage = true err := cmd.Execute() - require.NoError(t, err) - assert.Empty(t, b.String()) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse") }) t.Run("chat with invalid JSON response", func(t *testing.T) {