Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions cmd/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ 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 == "" {
continue
}
var resp components.ChatResponse
if err := json.Unmarshal([]byte(line), &resp); err != nil {
parseErrors++
continue
}
for _, msg := range resp.Messages {
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
40 changes: 36 additions & 4 deletions cmd/chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand All @@ -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) {
Expand Down
Loading