summaryrefslogtreecommitdiff
path: root/internal/executor/stream_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/executor/stream_test.go')
-rw-r--r--internal/executor/stream_test.go85
1 files changed, 85 insertions, 0 deletions
diff --git a/internal/executor/stream_test.go b/internal/executor/stream_test.go
new file mode 100644
index 0000000..10eb858
--- /dev/null
+++ b/internal/executor/stream_test.go
@@ -0,0 +1,85 @@
+package executor
+
+import (
+ "io"
+ "log/slog"
+ "strings"
+ "testing"
+)
+
+// streamLine builds a single-line stream-json message.
+func streamLine(json string) string { return json + "\n" }
+
+func TestParseStream_ResultIsError_ReturnsError(t *testing.T) {
+ input := streamLine(`{"type":"result","subtype":"error_during_execution","is_error":true,"result":"something went wrong"}`)
+ _, err := parseStream(strings.NewReader(input), io.Discard, slog.New(slog.NewTextHandler(io.Discard, nil)))
+ if err == nil {
+ t.Fatal("expected error when result.is_error=true, got nil")
+ }
+ if !strings.Contains(err.Error(), "something went wrong") {
+ t.Errorf("error should contain result text, got: %v", err)
+ }
+}
+
+func TestParseStream_PermissionDenied_ReturnsError(t *testing.T) {
+ // Simulate the permission denial tool_result that Claude Code emits
+ // when permission_mode is not bypassPermissions.
+ input := streamLine(`{"type":"user","message":{"role":"user","content":[{"type":"tool_result","is_error":true,"content":"Claude requested permissions to write to /foo/bar.go, but you haven't granted it yet.","tool_use_id":"tu_abc"}]}}`) +
+ streamLine(`{"type":"result","subtype":"success","is_error":false,"result":"I need permission","total_cost_usd":0.1}`)
+
+ _, err := parseStream(strings.NewReader(input), io.Discard, slog.New(slog.NewTextHandler(io.Discard, nil)))
+ if err == nil {
+ t.Fatal("expected error for permission denial, got nil")
+ }
+ if !strings.Contains(err.Error(), "permission") {
+ t.Errorf("error should mention permission, got: %v", err)
+ }
+}
+
+func TestParseStream_Success_ReturnsNilError(t *testing.T) {
+ input := streamLine(`{"type":"assistant","message":{"content":[{"type":"text","text":"Done."}]}}`) +
+ streamLine(`{"type":"result","subtype":"success","is_error":false,"result":"All tests pass.","total_cost_usd":0.05}`)
+
+ _, err := parseStream(strings.NewReader(input), io.Discard, slog.New(slog.NewTextHandler(io.Discard, nil)))
+ if err != nil {
+ t.Fatalf("expected nil error for success stream, got: %v", err)
+ }
+}
+
+func TestParseStream_ExtractsCostFromResultMessage(t *testing.T) {
+ input := streamLine(`{"type":"result","subtype":"success","is_error":false,"result":"done","total_cost_usd":1.2345}`)
+
+ cost, err := parseStream(strings.NewReader(input), io.Discard, slog.New(slog.NewTextHandler(io.Discard, nil)))
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if cost != 1.2345 {
+ t.Errorf("want cost 1.2345, got %f", cost)
+ }
+}
+
+func TestParseStream_ExtractsCostFromLegacyCostUSD(t *testing.T) {
+ // Some versions emit cost_usd at the top level rather than total_cost_usd.
+ input := streamLine(`{"type":"result","subtype":"success","is_error":false,"result":"done","cost_usd":0.99}`)
+
+ cost, err := parseStream(strings.NewReader(input), io.Discard, slog.New(slog.NewTextHandler(io.Discard, nil)))
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if cost != 0.99 {
+ t.Errorf("want cost 0.99, got %f", cost)
+ }
+}
+
+func TestParseStream_NonToolResultIsError_DoesNotFail(t *testing.T) {
+ // A tool_result with is_error:true that is NOT a permission denial
+ // (e.g. the tool ran but the command failed) should not mark the task failed —
+ // the agent can recover from individual tool errors.
+ input := streamLine(`{"type":"user","message":{"role":"user","content":[{"type":"tool_result","is_error":true,"content":"exit status 1","tool_use_id":"tu_xyz"}]}}`) +
+ streamLine(`{"type":"result","subtype":"success","is_error":false,"result":"Fixed it.","total_cost_usd":0.2}`)
+
+ _, err := parseStream(strings.NewReader(input), io.Discard, slog.New(slog.NewTextHandler(io.Discard, nil)))
+ if err != nil {
+ t.Fatalf("non-permission tool errors should not fail the task, got: %v", err)
+ }
+}