package oneshot_test import ( "context" "strings" "testing" "time" "go.uber.org/zap" clipkg "iop/apps/node/internal/adapters/cli" "iop/apps/node/internal/adapters/cli/internal/testutil" noderuntime "iop/apps/node/internal/runtime" "iop/packages/config" ) func TestCLIExecuteOneShotPassesPromptAsArg(t *testing.T) { testutil.RequireUnixShell(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "echo": { Command: "sh", Args: []string{"-c", `printf "reply:%s\n" "$1"`, "sh"}, }, }, } c := clipkg.New(cfg, zap.NewNop()) sink := &testutil.FakeSink{} err := c.Execute(context.Background(), noderuntime.ExecutionSpec{ RunID: "run-1", Model: "echo", Input: map[string]any{"prompt": "hello"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } events := sink.Events() var types []string for _, e := range events { types = append(types, string(e.Type)) } if len(events) < 3 { t.Fatalf("expected at least 3 events (start/delta/complete), got %v", types) } if events[0].Type != noderuntime.EventTypeStart { t.Fatalf("first event should be start, got %q", events[0].Type) } if events[len(events)-1].Type != noderuntime.EventTypeComplete { t.Fatalf("last event should be complete, got %q", events[len(events)-1].Type) } if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:hello") { t.Fatalf("expected reply:hello in deltas, got %q", combined) } } func TestCLIExecuteOneShotStreamJSONParsesAssistantContent(t *testing.T) { testutil.RequireUnixShell(t) script := `cat <<'EOF' {"type":"init","timestamp":"2026-05-03T00:00:00.000Z","session_id":"abc"} {"type":"message","role":"user","content":"hi"} {"type":"message","role":"assistant","content":"Hello","delta":true} {"type":"message","role":"assistant","content":" world","delta":true} {"type":"result","status":"success"} EOF` cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "streamy": { Command: "sh", Args: []string{"-c", script, "sh"}, OutputFormat: "stream-json", }, }, } c := clipkg.New(cfg, zap.NewNop()) sink := &testutil.FakeSink{} if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{ RunID: "run-sj", Model: "streamy", Input: map[string]any{"prompt": "hi"}, }, sink); err != nil { t.Fatalf("execute: %v", err) } combined := testutil.CollectDeltas(sink.Events()) if combined != "Hello world" { t.Fatalf("expected stream-json deltas to concat to %q, got %q", "Hello world", combined) } } func TestCLIExecuteOneShotCodexJSONParsesAgentMessage(t *testing.T) { testutil.RequireUnixShell(t) script := `cat <<'EOF' {"type":"thread.started","thread_id":"019d-aaa"} {"type":"turn.started"} {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Hi there."}} {"type":"turn.completed","usage":{"input_tokens":10,"output_tokens":3}} EOF` cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "codexy": { Command: "sh", Args: []string{"-c", script, "sh"}, OutputFormat: "codex-json", }, }, } c := clipkg.New(cfg, zap.NewNop()) sink := &testutil.FakeSink{} if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{ RunID: "run-cj", Model: "codexy", Input: map[string]any{"prompt": "hi"}, }, sink); err != nil { t.Fatalf("execute: %v", err) } combined := testutil.CollectDeltas(sink.Events()) if combined != "Hi there." { t.Fatalf("expected codex-json deltas to be %q, got %q", "Hi there.", combined) } } func TestCLIExecuteOneShotClaudeJSONParsesContentBlockDeltas(t *testing.T) { testutil.RequireUnixShell(t) script := `cat <<'EOF' {"type":"system","subtype":"init","session_id":"abc"} {"type":"stream_event","event":{"type":"message_start","message":{"id":"msg_1"}}} {"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}} {"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}} {"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}} {"type":"assistant","message":{"id":"msg_1","content":[{"type":"text","text":"Hello world"}]}} {"type":"stream_event","event":{"type":"content_block_stop","index":0}} {"type":"stream_event","event":{"type":"message_stop"}} {"type":"result","subtype":"success","is_error":false,"result":"Hello world"} EOF` cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "claudish": { Command: "sh", Args: []string{"-c", script, "sh"}, OutputFormat: "claude-json", }, }, } c := clipkg.New(cfg, zap.NewNop()) sink := &testutil.FakeSink{} if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{ RunID: "run-cl", Model: "claudish", Input: map[string]any{"prompt": "hi"}, }, sink); err != nil { t.Fatalf("execute: %v", err) } combined := testutil.CollectDeltas(sink.Events()) if combined != "Hello world" { t.Fatalf("expected claude-json deltas to be %q, got %q", "Hello world", combined) } } func TestCLIExecuteOneShotDrainsStderrConcurrently(t *testing.T) { testutil.RequireUnixShell(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "noisy": { Command: "sh", Args: []string{ "-c", `i=0; while [ "$i" -lt 20000 ]; do echo "warning line $i" >&2; i=$((i+1)); done; printf "reply:%s\n" "$1"`, "sh", }, }, }, } c := clipkg.New(cfg, zap.NewNop()) sink := &testutil.FakeSink{} ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() err := c.Execute(ctx, noderuntime.ExecutionSpec{ RunID: "run-noisy", Model: "noisy", Input: map[string]any{"prompt": "hello"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } if combined := testutil.CollectDeltas(sink.Events()); !strings.Contains(combined, "reply:hello") { t.Fatalf("expected reply:hello in deltas, got %q", combined) } } func TestCLIExecuteOneShotStreamsStdoutChunks(t *testing.T) { testutil.RequireUnixShell(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "chunky": { Command: "sh", Args: []string{ "-c", `printf "first"; sleep 0.2; printf "second"`, "sh", }, }, }, } c := clipkg.New(cfg, zap.NewNop()) sink := &testutil.FakeSink{} err := c.Execute(context.Background(), noderuntime.ExecutionSpec{ RunID: "run-chunky", Model: "chunky", Input: map[string]any{"prompt": "unused"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } var deltas []string for _, event := range sink.Events() { if event.Type == noderuntime.EventTypeDelta { deltas = append(deltas, event.Delta) } } if len(deltas) < 2 { t.Fatalf("expected stdout chunks to be emitted separately, got %v", deltas) } if got := strings.Join(deltas, ""); got != "firstsecond" { t.Fatalf("combined deltas: got %q want firstsecond", got) } }