1package agent
2
3import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "net/http"
10 "net/http/httptest"
11 "os"
12 "strings"
13 "testing"
14)
15
16func TestRunOrchestratedStreamUsesStreamRenderer(t *testing.T) {
17 home := t.TempDir()
18 t.Setenv("ZUT_HOME", home)
19 server := orchestratedModeTestServer("stream answer")
20 defer server.Close()
21
22 output, runErr := captureTestStdout(t, func() error {
23 return runOrchestratedStreamMode(context.Background(), Args{
24 Mode: ModeStream,
25 Orchestrate: true,
26 Provider: "openai",
27 Model: "gpt-5",
28 BaseURL: server.URL,
29 APIKey: "test-key",
30 Prompt: "say hello",
31 }, "test")
32 })
33 if runErr != nil {
34 t.Fatalf("runOrchestratedStreamMode error: %v", runErr)
35 }
36 if got := output; got != "stream answer\n" {
37 t.Fatalf("stdout = %q, want one rendered answer newline", got)
38 }
39}
40
41func TestRunOrchestratedJSONRetainsParentEventsAsJSONL(t *testing.T) {
42 home := t.TempDir()
43 t.Setenv("ZUT_HOME", home)
44 server := orchestratedModeTestServer("json answer")
45 defer server.Close()
46
47 output, runErr := captureTestStdout(t, func() error {
48 return runOrchestratedJSONMode(context.Background(), Args{
49 Mode: ModeJSON,
50 Orchestrate: true,
51 Provider: "openai",
52 Model: "gpt-5",
53 BaseURL: server.URL,
54 APIKey: "test-key",
55 Prompt: "say hello",
56 }, "test")
57 })
58 if runErr != nil {
59 t.Fatalf("runOrchestratedJSONMode error: %v", runErr)
60 }
61
62 seen := map[string]bool{}
63 scanner := bufio.NewScanner(strings.NewReader(output))
64 for scanner.Scan() {
65 var event map[string]any
66 if err := json.Unmarshal(scanner.Bytes(), &event); err != nil {
67 t.Fatalf("invalid JSONL line %q: %v", scanner.Text(), err)
68 }
69 if typ, ok := event["type"].(string); ok {
70 seen[typ] = true
71 }
72 }
73 if err := scanner.Err(); err != nil {
74 t.Fatal(err)
75 }
76 for _, typ := range []string{"user_message", "assistant_message", "turn_end"} {
77 if !seen[typ] {
78 t.Fatalf("JSONL omitted %s event: %s", typ, output)
79 }
80 }
81}
82
83func captureTestStdout(t *testing.T, run func() error) (string, error) {
84 t.Helper()
85 oldStdout := os.Stdout
86 reader, writer, err := os.Pipe()
87 if err != nil {
88 return "", err
89 }
90 os.Stdout = writer
91 t.Cleanup(func() {
92 os.Stdout = oldStdout
93 _ = writer.Close()
94 _ = reader.Close()
95 })
96 readDone := make(chan struct {
97 output []byte
98 err error
99 }, 1)
100 go func() {
101 output, readErr := io.ReadAll(reader)
102 readDone <- struct {
103 output []byte
104 err error
105 }{output: output, err: readErr}
106 }()
107
108 runErr := run()
109 _ = writer.Close()
110 os.Stdout = oldStdout
111 result := <-readDone
112 _ = reader.Close()
113 return string(result.output), errors.Join(runErr, result.err)
114}
115
116func orchestratedModeTestServer(answer string) *httptest.Server {
117 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
118 w.Header().Set("content-type", "text/event-stream")
119 _, _ = io.WriteString(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\""+answer+"\"},\"finish_reason\":\"stop\"}]}\n\n")
120 _, _ = io.WriteString(w, "data: [DONE]\n\n")
121 }))
122}