From 4ccc052f5b7a515463f909a7d87333428d33dfa1 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 22:59:27 +0000 Subject: [PATCH] test: tests de integracion para streaming + ProgressReporter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agrega tests de integracion end-to-end que validan el pipeline completo: streaming events → ProgressReporter → mock sender → Matrix messages. - TestIntegration_StreamToProgressReporter: simula sesion completa con init, 3 tool_use, text, result y finalize — verifica 1 send + 4 edits - TestIntegration_NoStreamingNoSideEffects: verifica que streaming=false no genera ningun side effect (regression test) - TestIntegration_ProgressReporterWithSendError: verifica que errores de envio no causan panic y se manejan gracefully Co-Authored-By: Claude Opus 4.6 (1M context) --- shell/effects/progress_integration_test.go | 162 +++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 shell/effects/progress_integration_test.go diff --git a/shell/effects/progress_integration_test.go b/shell/effects/progress_integration_test.go new file mode 100644 index 0000000..1d4685b --- /dev/null +++ b/shell/effects/progress_integration_test.go @@ -0,0 +1,162 @@ +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 +}