package effects import ( "context" "log/slog" "strings" "testing" coretypes "github.com/enmanuel/agents/pkg/llm" ) // TestIntegration_StreamToProgressReporter simulates the full flow: // parseStreamLine produces events → ProgressReporter consumes them → mock sender records calls. // This validates the complete pipeline from raw JSON lines to Matrix messages. func TestIntegration_StreamToProgressReporter(t *testing.T) { sender := &mockProgressSender{} pr := NewProgressReporter(sender, "!room:integration", slog.Default()) pr.minInterval = 0 // disable rate limiting for test fn := pr.StreamFunc() // Simulate a realistic stream-json session: // 1. Init event fn(coretypes.StreamEvent{Kind: coretypes.StreamInit}) // 2. Tool use: Bash fn(coretypes.StreamEvent{ Kind: coretypes.StreamToolUse, ToolName: "Bash", ToolInput: "git status", }) // 3. Tool use: Read fn(coretypes.StreamEvent{ Kind: coretypes.StreamToolUse, ToolName: "Read", ToolInput: "/home/user/project/main.go", }) // 4. Tool use: Edit fn(coretypes.StreamEvent{ Kind: coretypes.StreamToolUse, ToolName: "Edit", ToolInput: "/home/user/project/main.go", }) // 5. Text event (intermediate, should be ignored) fn(coretypes.StreamEvent{ Kind: coretypes.StreamText, Content: "I've made the changes...", }) // 6. Result event (should be ignored by progress reporter) fn(coretypes.StreamEvent{ Kind: coretypes.StreamResult, Content: "Here is the final answer with all changes applied.", }) // Verify sends: only 1 initial send if len(sender.sends) != 1 { t.Errorf("expected 1 send (init), got %d", len(sender.sends)) } if !strings.Contains(sender.sends[0], "Procesando") { t.Errorf("init message should contain 'Procesando', got %q", sender.sends[0]) } // Verify edits: 3 tool use events if len(sender.edits) != 3 { t.Fatalf("expected 3 edits (tool uses), got %d", len(sender.edits)) } // First edit: Bash if !strings.Contains(sender.edits[0], "Bash") { t.Errorf("edit[0] should mention Bash, got %q", sender.edits[0]) } if !strings.Contains(sender.edits[0], "git status") { t.Errorf("edit[0] should show input, got %q", sender.edits[0]) } // Second edit: Read if !strings.Contains(sender.edits[1], "Read") { t.Errorf("edit[1] should mention Read, got %q", sender.edits[1]) } // Third edit: Edit if !strings.Contains(sender.edits[2], "Edit") { t.Errorf("edit[2] should mention Edit, got %q", sender.edits[2]) } // All edits should target the same event ID for i, target := range sender.editTargets { if target != "$progress_msg_1" { t.Errorf("editTarget[%d] = %q, want %q", i, target, "$progress_msg_1") } } // Finalize pr.Finalize("\u2705 *Completado*") if len(sender.edits) != 4 { t.Fatalf("expected 4 edits (3 tools + 1 finalize), got %d", len(sender.edits)) } if !strings.Contains(sender.edits[3], "Completado") { t.Errorf("finalize edit should contain 'Completado', got %q", sender.edits[3]) } } // TestIntegration_NoStreamingNoSideEffects verifies that when streaming is // not enabled, no ProgressReporter is created and no Matrix side effects occur. // This is a regression test for the streaming=false default behavior. func TestIntegration_NoStreamingNoSideEffects(t *testing.T) { sender := &mockProgressSender{} // Simulate the handler check: streaming disabled → progress is nil var progress *ProgressReporter // nil, because streaming is disabled if progress != nil { t.Error("progress reporter should be nil when streaming is disabled") } // Verify no sends or edits happened if len(sender.sends) != 0 { t.Errorf("expected 0 sends, got %d", len(sender.sends)) } if len(sender.edits) != 0 { t.Errorf("expected 0 edits, got %d", len(sender.edits)) } } // TestIntegration_ProgressReporterWithSendError verifies that the reporter // handles send errors gracefully without panicking. func TestIntegration_ProgressReporterWithSendError(t *testing.T) { sender := &errorSender{} pr := NewProgressReporter(sender, "!room:test", slog.Default()) pr.minInterval = 0 fn := pr.StreamFunc() // Should not panic even when send fails fn(coretypes.StreamEvent{Kind: coretypes.StreamInit}) // EventID should be empty since send failed if pr.EventID() != "" { t.Errorf("expected empty EventID after send error, got %q", pr.EventID()) } // Finalize should be a no-op since no message was sent pr.Finalize("Done") } // errorSender always returns errors. type errorSender struct { fakeMatrixSender } func (e *errorSender) SendMarkdownGetID(_ context.Context, _, _ string) (string, error) { return "", context.DeadlineExceeded } func (e *errorSender) EditMessage(_ context.Context, _, _, _ string) error { return context.DeadlineExceeded }