package claudewatcher import ( "errors" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func collect(t *testing.T, body string) ([]Turn, int64, error) { t.Helper() var out []Turn end, err := ParseStream(strings.NewReader(body), 0, nil, func(tr Turn) error { out = append(out, tr) return nil }) return out, end, err } func TestParseStream_UserTurnStringContent(t *testing.T) { body := `{"type":"user","sessionId":"S","timestamp":"2026-05-25T07:00:00Z","message":"hello world"} ` turns, end, err := collect(t, body) require.NoError(t, err) require.Len(t, turns, 1) assert.Equal(t, "user", turns[0].Type) assert.Equal(t, "S", turns[0].SessionID) assert.Equal(t, "hello world", turns[0].Content) assert.False(t, turns[0].Skip) assert.Equal(t, int64(len(body)), end) } func TestParseStream_UserTurnContentBlocks(t *testing.T) { body := `{"type":"user","sessionId":"S","timestamp":"2026-05-25T07:00:00Z","message":{"role":"user","content":[{"type":"text","text":"line 1"},{"type":"text","text":"line 2"}]}} ` turns, _, err := collect(t, body) require.NoError(t, err) require.Len(t, turns, 1) assert.Equal(t, "line 1\nline 2", turns[0].Content) } func TestParseStream_AssistantToolUse(t *testing.T) { body := `{"type":"assistant","sessionId":"S","timestamp":"2026-05-25T07:00:00Z","message":{"content":[{"type":"text","text":"calling now"},{"type":"tool_use","name":"Edit","input":{}}]}} ` turns, _, err := collect(t, body) require.NoError(t, err) require.Len(t, turns, 1) assert.Equal(t, "Edit", turns[0].ToolName) assert.Contains(t, turns[0].Content, "calling now") assert.Contains(t, turns[0].Content, "[tool_use:Edit]") } func TestParseStream_AssistantToolResult(t *testing.T) { body := `{"type":"user","sessionId":"S","timestamp":"2026-05-25T07:00:00Z","message":{"content":[{"type":"tool_result","content":"output of cmd"}]}} ` turns, _, err := collect(t, body) require.NoError(t, err) require.Len(t, turns, 1) assert.Contains(t, turns[0].Content, "[tool_result] output of cmd") } func TestParseStream_SkipsBookkeepingTypes(t *testing.T) { body := strings.Join([]string{ `{"type":"queue-operation","sessionId":"S","content":"x"}`, `{"type":"last-prompt","sessionId":"S","lastPrompt":"y"}`, `{"type":"permission-mode","sessionId":"S","permissionMode":"auto"}`, `{"type":"ai-title","sessionId":"S","aiTitle":"My session"}`, `{"type":"file-history-snapshot","messageId":"abc"}`, }, "\n") + "\n" turns, _, err := collect(t, body) require.NoError(t, err) require.Len(t, turns, 5) for _, tr := range turns { assert.True(t, tr.Skip, "expected Skip=true for %q", tr.Type) } } func TestParseStream_UnknownTypeIsSkip(t *testing.T) { body := `{"type":"future-thing","sessionId":"S"}` + "\n" turns, _, err := collect(t, body) require.NoError(t, err) require.Len(t, turns, 1) assert.True(t, turns[0].Skip) } func TestParseStream_MalformedLineIsSkippedNotFatal(t *testing.T) { body := strings.Join([]string{ `{"type":"user","sessionId":"S","message":"first"}`, `{not valid json`, `{"type":"user","sessionId":"S","message":"third"}`, }, "\n") + "\n" var warnings int var turns []Turn _, err := ParseStream(strings.NewReader(body), 0, func(format string, args ...any) { warnings++ }, func(tr Turn) error { turns = append(turns, tr) return nil }) require.NoError(t, err) require.Len(t, turns, 2, "first + third should make it through") assert.Equal(t, 1, warnings) } func TestParseStream_EmitErrStopHaltsCleanly(t *testing.T) { body := strings.Join([]string{ `{"type":"user","sessionId":"S","message":"a"}`, `{"type":"user","sessionId":"S","message":"b"}`, `{"type":"user","sessionId":"S","message":"c"}`, }, "\n") + "\n" count := 0 end, err := ParseStream(strings.NewReader(body), 0, nil, func(tr Turn) error { count++ if count == 2 { return ErrStop } return nil }) require.NoError(t, err) assert.Equal(t, 2, count) assert.Greater(t, end, int64(0)) } func TestParseStream_EmitOtherErrorPropagates(t *testing.T) { body := `{"type":"user","sessionId":"S","message":"a"}` + "\n" want := errors.New("boom") _, err := ParseStream(strings.NewReader(body), 0, nil, func(tr Turn) error { return want }) require.Error(t, err) assert.Contains(t, err.Error(), "boom") } func TestParseStream_AttachmentHookEvent(t *testing.T) { body := `{"type":"attachment","sessionId":"S","timestamp":"2026-05-25T07:00:00Z","attachment":{"type":"hook_success","hookName":"SessionStart:startup","hookEvent":"SessionStart","content":"hook body"}} ` turns, _, err := collect(t, body) require.NoError(t, err) require.Len(t, turns, 1) assert.Equal(t, "hook body", turns[0].Content) } func TestParseStream_OffsetAdvances(t *testing.T) { body := `{"type":"user","sessionId":"S","message":"a"}` + "\n" + `{"type":"user","sessionId":"S","message":"b"}` + "\n" var offsets []int64 _, err := ParseStream(strings.NewReader(body), 100, nil, func(tr Turn) error { offsets = append(offsets, tr.OffsetAfter) return nil }) require.NoError(t, err) require.Len(t, offsets, 2) assert.Greater(t, offsets[0], int64(100)) assert.Greater(t, offsets[1], offsets[0]) }