package cli_test import ( "context" "errors" "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/go/config" "os" osexec "os/exec" "strings" "testing" "time" ) func TestCLIStartPersistentTerminalAndExecuteWritesPrompt(t *testing.T) { testutil.RequirePTYSupport(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "echo-persistent": { Command: "sh", Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf "reply:%s\n" "$line"; done`}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 300, StartupIdleTimeoutMS: 50, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() sink := &testutil.FakeSink{} err := c.Execute(ctx, noderuntime.ExecutionSpec{ RunID: "run-1", Target: "echo-persistent", Input: map[string]any{"prompt": "hello"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } events := sink.Events() var started, completed bool for _, e := range events { switch e.Type { case noderuntime.EventTypeStart: started = true case noderuntime.EventTypeComplete: completed = true } } if !started { t.Fatal("expected start event") } if !completed { t.Fatal("expected complete event") } if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:hello") { t.Fatalf("expected reply:hello in deltas, got %q", combined) } } func TestCLIExecutePersistentTerminalSendsCarriageReturn(t *testing.T) { testutil.RequirePTYSupport(t) if _, err := osexec.LookPath("stty"); err != nil { t.Skip("stty required") } cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "raw-tui": { Command: os.Args[0], Args: []string{"-test.run=TestRawTUIHelperProcess", "--"}, Env: []string{"IOP_RAW_TUI_HELPER=1"}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 100, StartupIdleTimeoutMS: 50, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() execCtx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(execCtx, noderuntime.ExecutionSpec{ RunID: "run-raw-tui", Target: "raw-tui", Input: map[string]any{"prompt": "hello"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } events := sink.Events() if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:hello") { t.Fatalf("expected reply:hello in deltas, got %q", combined) } if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete { t.Fatalf("expected complete event, got %s", last.Type) } } func TestCLIExecutePersistentTerminalAcceptsClaudeBypassWarning(t *testing.T) { testutil.RequirePTYSupport(t) if _, err := osexec.LookPath("stty"); err != nil { t.Skip("stty required") } cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "claude-warning": { Command: os.Args[0], Args: []string{"-test.run=TestRawTUIHelperProcess", "--"}, Env: []string{"IOP_CLAUDE_WARNING_HELPER=1"}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 100, StartupIdleTimeoutMS: 250, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() execCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(execCtx, noderuntime.ExecutionSpec{ RunID: "run-claude-warning", Target: "claude-warning", 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 TestCLIExecutePersistentTerminalAcceptsClaudeWorkspaceTrustAndBypassWarning(t *testing.T) { testutil.RequirePTYSupport(t) if _, err := osexec.LookPath("stty"); err != nil { t.Skip("stty required") } cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "claude-trust-warning": { Command: os.Args[0], Args: []string{"-test.run=TestRawTUIHelperProcess", "--"}, Env: []string{"IOP_CLAUDE_TRUST_WARNING_HELPER=1"}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 100, StartupIdleTimeoutMS: 250, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() execCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(execCtx, noderuntime.ExecutionSpec{ RunID: "run-claude-trust-warning", Target: "claude-trust-warning", 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 TestCLIExecutePersistentClaudeTUICancelsSessionLimitUpgradePrompt(t *testing.T) { testutil.RequirePTYSupport(t) if _, err := osexec.LookPath("stty"); err != nil { t.Skip("stty required") } cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "claude-tui": { Command: os.Args[0], Args: []string{"-test.run=TestRawTUIHelperProcess", "--"}, Env: []string{"IOP_CLAUDE_RATE_LIMIT_HELPER=1"}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 500, StartupIdleTimeoutMS: 50, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() execCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(execCtx, noderuntime.ExecutionSpec{ RunID: "run-claude-rate-limit", Target: "claude-tui", Input: map[string]any{"prompt": "hello"}, }, sink) if !errors.Is(err, noderuntime.ErrRunCancelled) { t.Fatalf("expected run cancelled, got %v", err) } var cancelMsg string for _, ev := range sink.Events() { if ev.Type == noderuntime.EventTypeCancelled { cancelMsg = ev.Message } if ev.Type == noderuntime.EventTypeError { t.Fatalf("expected cancellation, got error event %q", ev.Error) } } if !strings.Contains(cancelMsg, "session limit") { t.Fatalf("expected cancelled event to mention session limit, got %q", cancelMsg) } } func TestCLIExecutePersistentClaudeTUIReplaysPromptAfterStartupReadyRace(t *testing.T) { testutil.RequirePTYSupport(t) if _, err := osexec.LookPath("stty"); err != nil { t.Skip("stty required") } cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "claude-tui": { Command: os.Args[0], Args: []string{"-test.run=TestRawTUIHelperProcess", "--"}, Env: []string{"IOP_CLAUDE_READY_REPLAY_HELPER=1"}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 100, StartupIdleTimeoutMS: 40, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() execCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(execCtx, noderuntime.ExecutionSpec{ RunID: "run-claude-ready-race", Target: "claude-tui", Input: map[string]any{"prompt": "hello"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } if combined := testutil.CollectDeltas(sink.Events()); combined != "replay:hello" { t.Fatalf("expected replayed prompt response, got %q", combined) } } func TestCLIStartPersistentTerminalEmitsRawChunksWithoutNewline(t *testing.T) { testutil.RequirePTYSupport(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "raw-terminal": { Command: "sh", Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf "chunk:%s" "$line"; done`}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 100, StartupIdleTimeoutMS: 50, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() runCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(runCtx, noderuntime.ExecutionSpec{ RunID: "run-raw", Target: "raw-terminal", Input: map[string]any{"prompt": "hello"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } events := sink.Events() if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "chunk:hello") { t.Fatalf("expected raw chunk in deltas, got %q", combined) } if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete { t.Fatalf("expected complete event, got %s", last.Type) } } func TestCLIStartPersistentTerminalUsesWidePTY(t *testing.T) { testutil.RequirePTYSupport(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "size-terminal": { Command: "sh", Args: []string{"-c", `stty -echo; while IFS= read -r line; do stty size; done`}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 100, StartupIdleTimeoutMS: 50, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() runCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(runCtx, noderuntime.ExecutionSpec{ RunID: "run-size", Target: "size-terminal", Input: map[string]any{"prompt": "hello"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } if combined := testutil.CollectDeltas(sink.Events()); !strings.Contains(combined, "720 1024") { t.Fatalf("expected wide PTY size in deltas, got %q", combined) } } func TestCLIExecutePersistentClaudeTUIFiltersTerminalChrome(t *testing.T) { testutil.RequirePTYSupport(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "claude-tui": { Command: "sh", Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf '\033[2C\r\033[7A\342\227\217\033[1Cmessage:%s\r\n\302\267 Cogitating... (1s, 1 tokens)\r\n\342\235\257 ' "$line"; done`}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 100, StartupIdleTimeoutMS: 50, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() runCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(runCtx, noderuntime.ExecutionSpec{ RunID: "run-filter", Target: "claude-tui", Input: map[string]any{"prompt": "hello"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } events := sink.Events() combined := testutil.CollectDeltas(events) if combined != "message:hello" { t.Fatalf("expected only assistant message delta, got %q", combined) } if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete || last.Message != "prompt-ready" { t.Fatalf("expected prompt-ready completion, got type=%q message=%q", last.Type, last.Message) } } func TestCLIExecutePersistentClaudeTUIStreamsLineBeforeIdle(t *testing.T) { testutil.RequirePTYSupport(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "claude-tui": { Command: "sh", Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf 'sud fragment\r\n\342\234\242 Dilly-dallying\342\200\246\r\n* Dilly-dallying\342\200\246\r\n'; sleep 0.05; printf '\342\227\217 sudo runs commands\r\n\342\235\257 '; sleep 0.15; printf '\r\033[2A\033[2K\342\227\217 sudo runs commands with elevated privileges.\r\n\342\235\257 '; done`}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 300, StartupIdleTimeoutMS: 50, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() runCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(runCtx, noderuntime.ExecutionSpec{ RunID: "run-growing", Target: "claude-tui", Input: map[string]any{"prompt": "explain sudo"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } events := sink.Events() combined := testutil.CollectDeltas(events) if combined != "sudo runs commands with elevated privileges." { t.Fatalf("expected full growing message, got %q", combined) } if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete || last.Message != "prompt-ready" { t.Fatalf("expected prompt-ready completion, got type=%q message=%q", last.Type, last.Message) } } func TestCLIExecutePersistentClaudeTUIWaitsForPromptReadyAfterPartialOutput(t *testing.T) { testutil.RequirePTYSupport(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "claude-tui": { Command: "sh", Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf '\342\227\217 first chunk\r\n\342\234\242 Dilly-dallying\342\200\246\r\n'; sleep 0.15; printf 'second chunk\r\n\342\235\257 '; done`}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 60, StartupIdleTimeoutMS: 50, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() runCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(runCtx, noderuntime.ExecutionSpec{ RunID: "run-delayed-tail", Target: "claude-tui", Input: map[string]any{"prompt": "write two chunks"}, }, sink) if err != nil { t.Fatalf("execute: %v", err) } events := sink.Events() combined := testutil.CollectDeltas(events) if !strings.Contains(combined, "first chunk") || !strings.Contains(combined, "second chunk") || strings.Index(combined, "first chunk") > strings.Index(combined, "second chunk") { t.Fatalf("expected full delayed message, got %q", combined) } if last := events[len(events)-1]; last.Type != noderuntime.EventTypeComplete || last.Message != "prompt-ready" { t.Fatalf("expected prompt-ready completion, got type=%q message=%q", last.Type, last.Message) } } func TestCLIExecutePersistentClaudeTUIDoesNotCompleteOnStatusOnlyIdle(t *testing.T) { testutil.RequirePTYSupport(t) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "claude-tui": { Command: "sh", Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf '3\r\n\r\nThundering\342\200\2466\r\n'; done`}, Persistent: true, Terminal: true, ResponseIdleTimeoutMS: 60, StartupIdleTimeoutMS: 50, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() if err := c.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = c.Stop(ctx) }() runCtx, cancel := context.WithTimeout(ctx, 220*time.Millisecond) defer cancel() sink := &testutil.FakeSink{} err := c.Execute(runCtx, noderuntime.ExecutionSpec{ RunID: "run-status-only", Target: "claude-tui", Input: map[string]any{"prompt": "hello"}, }, sink) if err == nil { t.Fatal("execute completed successfully before any assistant message") } for _, event := range sink.Events() { if event.Type == noderuntime.EventTypeDelta { t.Fatalf("status-only output should not be emitted as delta: %q", event.Delta) } if event.Type == noderuntime.EventTypeComplete { t.Fatalf("status-only output should not emit complete: %+v", event) } } }