opencode-cursor-agent/internal/converter/convert_test.go

306 lines
9.2 KiB
Go

package converter
import (
"fmt"
"strings"
"testing"
)
func TestConvertLineAssistant(t *testing.T) {
line := `{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello, world!"}]}}`
result := ConvertLine(line, "chat-123")
if result.Skip {
t.Error("expected not Skip")
}
if result.Done {
t.Error("expected not Done")
}
if result.Error != nil {
t.Fatalf("unexpected error: %v", result.Error)
}
if result.Chunk == nil {
t.Fatal("expected Chunk, got nil")
}
if result.Chunk.ID != "chat-123" {
t.Errorf("Chunk.ID = %q, want %q", result.Chunk.ID, "chat-123")
}
if result.Chunk.Object != "chat.completion.chunk" {
t.Errorf("Chunk.Object = %q, want %q", result.Chunk.Object, "chat.completion.chunk")
}
if len(result.Chunk.Choices) != 1 {
t.Fatalf("len(Choices) = %d, want 1", len(result.Chunk.Choices))
}
if *result.Chunk.Choices[0].Delta.Content != "Hello, world!" {
t.Errorf("Delta.Content = %q, want %q", *result.Chunk.Choices[0].Delta.Content, "Hello, world!")
}
}
func TestConvertLineSystem(t *testing.T) {
line := `{"type":"system","message":{"role":"system","content":"init"}}`
result := ConvertLine(line, "chat-123")
if !result.Skip {
t.Error("expected Skip for system line")
}
if result.Chunk != nil {
t.Error("expected nil Chunk for system line")
}
if result.Error != nil {
t.Errorf("unexpected error: %v", result.Error)
}
}
func TestConvertLineUser(t *testing.T) {
line := `{"type":"user","message":{"role":"user","content":"hello"}}`
result := ConvertLine(line, "chat-123")
if !result.Skip {
t.Error("expected Skip for user line")
}
if result.Chunk != nil {
t.Error("expected nil Chunk for user line")
}
if result.Error != nil {
t.Errorf("unexpected error: %v", result.Error)
}
}
func TestConvertLineResultSuccess(t *testing.T) {
line := `{"type":"result","subtype":"success","result":"done","usage":{"inputTokens":100,"outputTokens":50}}`
result := ConvertLine(line, "chat-123")
if !result.Done {
t.Error("expected Done")
}
if result.Skip {
t.Error("expected not Skip")
}
if result.Error != nil {
t.Fatalf("unexpected error: %v", result.Error)
}
if result.Usage == nil {
t.Fatal("expected Usage, got nil")
}
if result.Usage.InputTokens != 100 {
t.Errorf("Usage.InputTokens = %d, want 100", result.Usage.InputTokens)
}
if result.Usage.OutputTokens != 50 {
t.Errorf("Usage.OutputTokens = %d, want 50", result.Usage.OutputTokens)
}
}
func TestConvertLineResultError(t *testing.T) {
line := `{"type":"result","is_error":true,"result":"something went wrong"}`
result := ConvertLine(line, "chat-123")
if result.Error == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(result.Error.Error(), "something went wrong") {
t.Errorf("error = %q, want to contain %q", result.Error.Error(), "something went wrong")
}
if result.Done {
t.Error("expected not Done for error")
}
}
func TestConvertLineEmpty(t *testing.T) {
tests := []string{"", " ", "\n", " \n "}
for _, line := range tests {
result := ConvertLine(line, "chat-123")
if !result.Skip {
t.Errorf("expected Skip for empty/whitespace line %q", line)
}
if result.Error != nil {
t.Errorf("unexpected error for empty line: %v", result.Error)
}
}
}
func TestConvertLineInvalidJSON(t *testing.T) {
line := `{"type":"assistant", invalid json}`
result := ConvertLine(line, "chat-123")
if result.Error == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
if !strings.Contains(result.Error.Error(), "unmarshal error") {
t.Errorf("error = %q, want to contain %q", result.Error.Error(), "unmarshal error")
}
}
func TestExtractContent(t *testing.T) {
t.Run("nil message", func(t *testing.T) {
result := ExtractContent(nil)
if result != "" {
t.Errorf("ExtractContent(nil) = %q, want empty", result)
}
})
t.Run("single content", func(t *testing.T) {
msg := &CursorMessage{
Role: "assistant",
Content: []CursorContent{
{Type: "text", Text: "Hello"},
},
}
result := ExtractContent(msg)
if result != "Hello" {
t.Errorf("ExtractContent() = %q, want %q", result, "Hello")
}
})
t.Run("multiple content", func(t *testing.T) {
msg := &CursorMessage{
Role: "assistant",
Content: []CursorContent{
{Type: "text", Text: "Hello"},
{Type: "text", Text: ", "},
{Type: "text", Text: "world!"},
},
}
result := ExtractContent(msg)
if result != "Hello, world!" {
t.Errorf("ExtractContent() = %q, want %q", result, "Hello, world!")
}
})
t.Run("empty content", func(t *testing.T) {
msg := &CursorMessage{
Role: "assistant",
Content: []CursorContent{},
}
result := ExtractContent(msg)
if result != "" {
t.Errorf("ExtractContent() = %q, want empty", result)
}
})
}
func TestStreamParser_OnlyEmitsNewDeltaFromAccumulatedAssistantMessages(t *testing.T) {
parser := NewStreamParser("chat-123")
first := parser.Parse(`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hel"}]}}`)
if first.Error != nil {
t.Fatalf("unexpected error on first chunk: %v", first.Error)
}
if first.Chunk == nil || first.Chunk.Choices[0].Delta.Content == nil {
t.Fatal("expected first chunk content")
}
if got := *first.Chunk.Choices[0].Delta.Content; got != "Hel" {
t.Fatalf("first delta = %q, want %q", got, "Hel")
}
second := parser.Parse(`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}`)
if second.Error != nil {
t.Fatalf("unexpected error on second chunk: %v", second.Error)
}
if second.Chunk == nil || second.Chunk.Choices[0].Delta.Content == nil {
t.Fatal("expected second chunk content")
}
if got := *second.Chunk.Choices[0].Delta.Content; got != "lo" {
t.Fatalf("second delta = %q, want %q", got, "lo")
}
}
func TestStreamParser_SkipsFinalDuplicateAssistantMessage(t *testing.T) {
parser := NewStreamParser("chat-123")
first := parser.Parse(`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}`)
if first.Skip || first.Error != nil || first.Chunk == nil {
t.Fatalf("expected first assistant chunk, got %+v", first)
}
duplicate := parser.Parse(`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}`)
if !duplicate.Skip {
t.Fatalf("expected duplicate assistant message to be skipped, got %+v", duplicate)
}
}
func TestStreamParser_ResultIncludesUsage(t *testing.T) {
parser := NewStreamParser("chat-123")
result := parser.Parse(`{"type":"result","subtype":"success","usage":{"inputTokens":10,"outputTokens":4}}`)
if !result.Done {
t.Fatal("expected result.Done")
}
if result.Usage == nil {
t.Fatal("expected usage")
}
if result.Usage.InputTokens != 10 || result.Usage.OutputTokens != 4 {
t.Fatalf("unexpected usage: %+v", result.Usage)
}
}
func TestStreamParser_CanReconstructFinalContentFromIncrementalAssistantMessages(t *testing.T) {
parser := NewStreamParser("chat-123")
lines := []string{
`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"你"}]}}`,
`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"你好"}]}}`,
`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"你好,世界"}]}}`,
}
var content strings.Builder
for i, line := range lines {
result := parser.Parse(line)
if result.Error != nil {
t.Fatalf("line %d unexpected error: %v", i, result.Error)
}
if result.Skip {
continue
}
if result.Chunk == nil || result.Chunk.Choices[0].Delta.Content == nil {
t.Fatalf("line %d expected chunk content, got %+v", i, result)
}
content.WriteString(*result.Chunk.Choices[0].Delta.Content)
}
if got := content.String(); got != "你好,世界" {
t.Fatalf("reconstructed content = %q, want %q", got, "你好,世界")
}
}
func TestStreamParser_RawTextFallbackSkipsExactDuplicates(t *testing.T) {
parser := NewStreamParser("chat-123")
first := parser.ParseRawText("plain chunk")
if first.Skip || first.Chunk == nil || first.Chunk.Choices[0].Delta.Content == nil {
t.Fatalf("expected raw text chunk, got %+v", first)
}
duplicate := parser.ParseRawText("plain chunk")
if !duplicate.Skip {
t.Fatalf("expected duplicate raw text to be skipped, got %+v", duplicate)
}
}
func TestStreamParser_IncrementalFragmentsAccumulateAndSkipFinalDuplicate(t *testing.T) {
parser := NewStreamParser("chat-123")
fragments := []string{"你", "好,", "世", "界!"}
var got strings.Builder
for i, fr := range fragments {
line := fmt.Sprintf(`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":%q}]}}`, fr)
res := parser.Parse(line)
if res.Skip || res.Error != nil || res.Chunk == nil {
t.Fatalf("fragment %d: expected delta chunk, got %+v", i, res)
}
got.WriteString(*res.Chunk.Choices[0].Delta.Content)
}
if got.String() != "你好,世界!" {
t.Fatalf("reconstructed = %q, want 你好,世界!", got.String())
}
final := parser.Parse(`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"你好,世界!"}]}}`)
if !final.Skip {
t.Fatalf("expected final duplicate cumulative message to be skipped, got %+v", final)
}
}
func TestNewStreamParser_DoesNotExistYet(t *testing.T) {
_ = fmt.Sprintf
}