1252 lines
35 KiB
Go
1252 lines
35 KiB
Go
package cli_test
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
osexec "os/exec"
|
||
"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 TestCLIExecutePersistentWaitsForSlowFirstOutput(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"slow-echo": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do sleep 0.2; printf "reply:%s\n" "$line"; done`},
|
||
Persistent: true,
|
||
Terminal: true,
|
||
ResponseIdleTimeoutMS: 50,
|
||
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-1",
|
||
Target: "slow-echo",
|
||
Input: map[string]any{"prompt": "hello"},
|
||
}, sink)
|
||
if err != nil {
|
||
t.Fatalf("execute: %v", err)
|
||
}
|
||
|
||
events := sink.Events()
|
||
if events[len(events)-1].Type != noderuntime.EventTypeComplete {
|
||
t.Fatalf("expected last event to 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 TestCLIExecutePersistentProcessExitReturnsError(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"exit-on-input": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf "before-exit\n"; exit 2; done`},
|
||
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) }()
|
||
|
||
sink := &testutil.FakeSink{}
|
||
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
||
RunID: "run-1",
|
||
Target: "exit-on-input",
|
||
Input: map[string]any{"prompt": "hello"},
|
||
}, sink)
|
||
if err == nil {
|
||
t.Fatal("expected non-nil error when persistent process exits")
|
||
}
|
||
|
||
errStr := err.Error()
|
||
t.Logf("Got error: %q", errStr)
|
||
if !strings.Contains(errStr, "before-exit") {
|
||
t.Errorf("expected error to contain recent output 'before-exit', got %q", errStr)
|
||
}
|
||
if !strings.Contains(errStr, "exit status 2") {
|
||
t.Errorf("expected error to contain exit status 'exit status 2', got %q", errStr)
|
||
}
|
||
if !strings.Contains(errStr, "target=exit-on-input") {
|
||
t.Errorf("expected error to contain target=exit-on-input, got %q", errStr)
|
||
}
|
||
|
||
events := sink.Events()
|
||
var errorEvent noderuntime.RuntimeEvent
|
||
var hasError bool
|
||
for _, e := range events {
|
||
if e.Type == noderuntime.EventTypeError {
|
||
hasError = true
|
||
errorEvent = e
|
||
}
|
||
}
|
||
if !hasError {
|
||
t.Fatal("expected error event in emitted events")
|
||
}
|
||
if !strings.Contains(errorEvent.Error, "before-exit") {
|
||
t.Errorf("expected error event to contain recent output, got %q", errorEvent.Error)
|
||
}
|
||
if len(events) > 0 && events[len(events)-1].Type == noderuntime.EventTypeComplete {
|
||
t.Fatal("last event should not be complete when process exits unexpectedly")
|
||
}
|
||
}
|
||
|
||
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: 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-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: 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-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 TestRawTUIHelperProcess(t *testing.T) {
|
||
switch {
|
||
case os.Getenv("IOP_RAW_TUI_HELPER") == "1":
|
||
runRawTUIHelper()
|
||
case os.Getenv("IOP_CLAUDE_WARNING_HELPER") == "1":
|
||
runClaudeWarningHelper()
|
||
case os.Getenv("IOP_CLAUDE_TRUST_WARNING_HELPER") == "1":
|
||
runClaudeTrustAndWarningHelper()
|
||
default:
|
||
return
|
||
}
|
||
}
|
||
|
||
func setRawTerminal() {
|
||
cmd := osexec.Command("stty", "raw", "-echo")
|
||
cmd.Stdin = os.Stdin
|
||
cmd.Stdout = os.Stdout
|
||
cmd.Stderr = os.Stderr
|
||
if err := cmd.Run(); err != nil {
|
||
fmt.Fprintf(os.Stderr, "stty raw: %v\n", err)
|
||
os.Exit(2)
|
||
}
|
||
}
|
||
|
||
func runRawTUIHelper() {
|
||
setRawTerminal()
|
||
readPromptAndReply()
|
||
}
|
||
|
||
func runClaudeWarningHelper() {
|
||
setRawTerminal()
|
||
fmt.Fprint(os.Stdout, "WARNING: Claude Code running in BypassPermissions mode\r\n2. Yes, I accept\r\n")
|
||
_ = os.Stdout.Sync()
|
||
|
||
want := []byte{'\x1b', '[', 'B', '\r'}
|
||
seen := make([]byte, 0, len(want))
|
||
buf := make([]byte, 1)
|
||
for {
|
||
n, err := os.Stdin.Read(buf)
|
||
if n > 0 {
|
||
seen = append(seen, buf[0])
|
||
if len(seen) > len(want) {
|
||
seen = seen[len(seen)-len(want):]
|
||
}
|
||
if string(seen) == string(want) {
|
||
fmt.Fprint(os.Stdout, "ready\r\n")
|
||
_ = os.Stdout.Sync()
|
||
break
|
||
}
|
||
}
|
||
if err != nil {
|
||
os.Exit(0)
|
||
}
|
||
}
|
||
|
||
readPromptAndReply()
|
||
}
|
||
|
||
func runClaudeTrustAndWarningHelper() {
|
||
setRawTerminal()
|
||
fmt.Fprint(os.Stdout, "Quick safety check: Is this a project you created or one you trust?\r\n❯ 1. Yes, I trust this folder\r\n2. No, exit\r\n")
|
||
_ = os.Stdout.Sync()
|
||
|
||
buf := make([]byte, 1)
|
||
for {
|
||
n, err := os.Stdin.Read(buf)
|
||
if n > 0 && buf[0] == '\r' {
|
||
break
|
||
}
|
||
if err != nil {
|
||
os.Exit(0)
|
||
}
|
||
}
|
||
|
||
fmt.Fprint(os.Stdout, "WARNING: Claude Code running in BypassPermissions mode\r\n2. Yes, I accept\r\n")
|
||
_ = os.Stdout.Sync()
|
||
|
||
want := []byte{'\x1b', '[', 'B', '\r'}
|
||
seen := make([]byte, 0, len(want))
|
||
for {
|
||
n, err := os.Stdin.Read(buf)
|
||
if n > 0 {
|
||
seen = append(seen, buf[0])
|
||
if len(seen) > len(want) {
|
||
seen = seen[len(seen)-len(want):]
|
||
}
|
||
if string(seen) == string(want) {
|
||
fmt.Fprint(os.Stdout, "ready\r\n")
|
||
_ = os.Stdout.Sync()
|
||
break
|
||
}
|
||
}
|
||
if err != nil {
|
||
os.Exit(0)
|
||
}
|
||
}
|
||
|
||
readPromptAndReply()
|
||
}
|
||
|
||
func readPromptAndReply() {
|
||
var prompt []byte
|
||
buf := make([]byte, 1)
|
||
for {
|
||
n, err := os.Stdin.Read(buf)
|
||
if n > 0 {
|
||
if buf[0] == '\r' {
|
||
fmt.Fprintf(os.Stdout, "reply:%s\n", string(prompt))
|
||
_ = os.Stdout.Sync()
|
||
prompt = prompt[:0]
|
||
} else {
|
||
prompt = append(prompt, buf[0])
|
||
}
|
||
}
|
||
if err != nil {
|
||
os.Exit(0)
|
||
}
|
||
}
|
||
}
|
||
|
||
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, "80 240") {
|
||
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 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)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestCLIExecutePersistentCancelDoesNotTerminateSession verifies that a
|
||
// cancelled run leaves the session alive so subsequent runs can reuse it.
|
||
func TestCLIExecutePersistentCancelDoesNotTerminateSession(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
// ResponseIdleTimeoutMS is short so the cancel-drain exits quickly,
|
||
// allowing the second execute to acquire the session lock promptly.
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"slow-echo": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do sleep 0.05; printf "reply:%s\n" "$line"; done`},
|
||
Persistent: true,
|
||
Terminal: true,
|
||
ResponseIdleTimeoutMS: 150,
|
||
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) }()
|
||
|
||
// First execute: cancel before idle timeout fires.
|
||
firstCtx, firstCancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||
defer firstCancel()
|
||
|
||
sink1 := &testutil.FakeSink{}
|
||
_ = c.Execute(firstCtx, noderuntime.ExecutionSpec{
|
||
RunID: "run-1",
|
||
Target: "slow-echo",
|
||
Input: map[string]any{"prompt": "first"},
|
||
}, sink1)
|
||
|
||
// Give time for cancel drain to finish (drain uses idleTimeout=150ms).
|
||
time.Sleep(400 * time.Millisecond)
|
||
|
||
// Second execute on same session — must succeed (session still alive).
|
||
execCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||
defer cancel()
|
||
|
||
sink2 := &testutil.FakeSink{}
|
||
err := c.Execute(execCtx, noderuntime.ExecutionSpec{
|
||
RunID: "run-2",
|
||
Target: "slow-echo",
|
||
Input: map[string]any{"prompt": "second"},
|
||
}, sink2)
|
||
if err != nil {
|
||
t.Fatalf("second execute after cancel must succeed (session should be alive): %v", err)
|
||
}
|
||
events2 := sink2.Events()
|
||
if len(events2) == 0 || events2[len(events2)-1].Type != noderuntime.EventTypeComplete {
|
||
t.Fatalf("expected complete event on second run, got %v", events2)
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistentConsecutiveExecutes(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"consecutive-echo": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf "reply:%s\n" "$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) }()
|
||
|
||
sink1 := &testutil.FakeSink{}
|
||
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
||
RunID: "run-1",
|
||
Target: "consecutive-echo",
|
||
Input: map[string]any{"prompt": "first"},
|
||
}, sink1)
|
||
if err != nil {
|
||
t.Fatalf("first execute: %v", err)
|
||
}
|
||
events1 := sink1.Events()
|
||
if events1[len(events1)-1].Type != noderuntime.EventTypeComplete {
|
||
t.Fatalf("first run: expected last event to be complete, got %q", events1[len(events1)-1].Type)
|
||
}
|
||
|
||
sink2 := &testutil.FakeSink{}
|
||
err = c.Execute(ctx, noderuntime.ExecutionSpec{
|
||
RunID: "run-2",
|
||
Target: "consecutive-echo",
|
||
Input: map[string]any{"prompt": "second"},
|
||
}, sink2)
|
||
if err != nil {
|
||
t.Fatalf("second execute: %v", err)
|
||
}
|
||
events2 := sink2.Events()
|
||
if events2[len(events2)-1].Type != noderuntime.EventTypeComplete {
|
||
t.Fatalf("second run: expected last event to be complete, got %q", events2[len(events2)-1].Type)
|
||
}
|
||
|
||
for _, e := range events2 {
|
||
if e.Type == noderuntime.EventTypeDelta && strings.Contains(e.Delta, "first") {
|
||
t.Fatalf("second run received first run's delta (session contamination): %q", e.Delta)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistentCreatesIndependentSessionsForSameProfile(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"multi-echo": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf "reply:%s\n" "$line"; done`},
|
||
Persistent: true,
|
||
Terminal: true,
|
||
ResponseIdleTimeoutMS: 200,
|
||
StartupIdleTimeoutMS: 50,
|
||
},
|
||
},
|
||
}
|
||
c := clipkg.New(cfg, zap.NewNop())
|
||
ctx := context.Background()
|
||
|
||
sinkA := &testutil.FakeSink{}
|
||
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
||
RunID: "run-a",
|
||
Target: "multi-echo",
|
||
SessionID: "session-a",
|
||
Input: map[string]any{"prompt": "alpha"},
|
||
}, sinkA)
|
||
if err != nil {
|
||
t.Fatalf("session-a execute: %v", err)
|
||
}
|
||
|
||
sinkB := &testutil.FakeSink{}
|
||
err = c.Execute(ctx, noderuntime.ExecutionSpec{
|
||
RunID: "run-b",
|
||
Target: "multi-echo",
|
||
SessionID: "session-b",
|
||
Input: map[string]any{"prompt": "beta"},
|
||
}, sinkB)
|
||
if err != nil {
|
||
t.Fatalf("session-b execute: %v", err)
|
||
}
|
||
|
||
if combined := testutil.CollectDeltas(sinkA.Events()); !strings.Contains(combined, "alpha") {
|
||
t.Fatalf("session-a: expected alpha in deltas, got %q", combined)
|
||
}
|
||
if combined := testutil.CollectDeltas(sinkB.Events()); !strings.Contains(combined, "beta") {
|
||
t.Fatalf("session-b: expected beta in deltas, got %q", combined)
|
||
}
|
||
if combined := testutil.CollectDeltas(sinkA.Events()); strings.Contains(combined, "beta") {
|
||
t.Fatalf("session-a contaminated with session-b output: %q", combined)
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistentRequireExistingSessionFailsWhenMissing(t *testing.T) {
|
||
testutil.RequireUnixShell(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"echo-req": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `while IFS= read -r line; do printf "reply:%s\n" "$line"; done`},
|
||
Persistent: true,
|
||
Terminal: false,
|
||
ResponseIdleTimeoutMS: 200,
|
||
},
|
||
},
|
||
}
|
||
c := clipkg.New(cfg, zap.NewNop())
|
||
ctx := context.Background()
|
||
|
||
sink := &testutil.FakeSink{}
|
||
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
||
RunID: "run-1",
|
||
Target: "echo-req",
|
||
SessionID: "nonexistent",
|
||
SessionMode: noderuntime.SessionModeRequireExisting,
|
||
Input: map[string]any{"prompt": "hello"},
|
||
}, sink)
|
||
if err == nil {
|
||
t.Fatal("expected error for require-existing with missing session")
|
||
}
|
||
if strings.Contains(err.Error(), "start") {
|
||
t.Fatalf("should not have started a new process, got: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistent_UserCancelEmitsUserCancelMessage(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"slow-echo": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do sleep 10; printf "reply:%s\n" "$line"; done`},
|
||
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.WithCancel(ctx)
|
||
|
||
sink := &testutil.FakeSink{}
|
||
go func() {
|
||
time.Sleep(100 * time.Millisecond)
|
||
cancel()
|
||
}()
|
||
|
||
err := c.Execute(execCtx, noderuntime.ExecutionSpec{
|
||
RunID: "run-usercancel",
|
||
Target: "slow-echo",
|
||
Input: map[string]any{"prompt": "hello"},
|
||
}, sink)
|
||
if err == nil {
|
||
t.Fatalf("expected error, got nil")
|
||
}
|
||
|
||
events := sink.Events()
|
||
var lastMsg string
|
||
var cancelEvent bool
|
||
for i := len(events) - 1; i >= 0; i-- {
|
||
if events[i].Type == noderuntime.EventTypeCancelled {
|
||
cancelEvent = true
|
||
lastMsg = events[i].Message
|
||
}
|
||
}
|
||
if !cancelEvent {
|
||
t.Fatal("expected cancelled event")
|
||
}
|
||
if lastMsg != "user-cancel" {
|
||
t.Fatalf("expected cancel Message 'user-cancel', got %q", lastMsg)
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistent_TimeoutEmitsTimeoutMessage(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"slow-echo": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do sleep 10; printf "reply:%s\n" "$line"; done`},
|
||
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, 100*time.Millisecond)
|
||
defer cancel()
|
||
|
||
sink := &testutil.FakeSink{}
|
||
executeErr := c.Execute(execCtx, noderuntime.ExecutionSpec{
|
||
RunID: "run-timeout",
|
||
Target: "slow-echo",
|
||
Input: map[string]any{"prompt": "hello"},
|
||
}, sink)
|
||
if executeErr == nil {
|
||
t.Fatalf("expected error, got nil")
|
||
}
|
||
|
||
events := sink.Events()
|
||
var lastMsg string
|
||
var cancelEvent bool
|
||
for i := len(events) - 1; i >= 0; i-- {
|
||
if events[i].Type == noderuntime.EventTypeCancelled {
|
||
cancelEvent = true
|
||
lastMsg = events[i].Message
|
||
}
|
||
}
|
||
if !cancelEvent {
|
||
t.Fatal("expected cancelled event")
|
||
}
|
||
if lastMsg != "timeout" {
|
||
t.Fatalf("expected cancel Message 'timeout', got %q", lastMsg)
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistent_CompletionMarkerLineEndsRun(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"marker-line": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf "reply:%s\n" "$line"; printf "<<END_OF_RESPONSE>>\n"; done`},
|
||
Persistent: true,
|
||
Terminal: true,
|
||
ResponseIdleTimeoutMS: 500,
|
||
StartupIdleTimeoutMS: 50,
|
||
CompletionMarker: config.CompletionMarkerConf{Line: "<<END_OF_RESPONSE>>"},
|
||
},
|
||
},
|
||
}
|
||
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-marker-line",
|
||
Target: "marker-line",
|
||
Input: map[string]any{"prompt": "hello"},
|
||
}, sink)
|
||
if err != nil {
|
||
t.Fatalf("execute: %v", err)
|
||
}
|
||
|
||
events := sink.Events()
|
||
last := events[len(events)-1]
|
||
if last.Type != noderuntime.EventTypeComplete {
|
||
t.Fatalf("expected last event to be complete, got %q", last.Type)
|
||
}
|
||
if last.Message != "completion-marker" {
|
||
t.Fatalf("expected Message 'completion-marker', got %q", last.Message)
|
||
}
|
||
if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:hello") {
|
||
t.Fatalf("expected reply:hello in deltas, got %q", combined)
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistent_CompletionMarkerRegexEndsRun(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"marker-regex": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do printf "reply:%s\n" "$line"; printf "DONE 42\n"; done`},
|
||
Persistent: true,
|
||
Terminal: true,
|
||
ResponseIdleTimeoutMS: 500,
|
||
StartupIdleTimeoutMS: 50,
|
||
CompletionMarker: config.CompletionMarkerConf{Regex: "^DONE \\d+$"},
|
||
},
|
||
},
|
||
}
|
||
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-marker-regex",
|
||
Target: "marker-regex",
|
||
Input: map[string]any{"prompt": "hi"},
|
||
}, sink)
|
||
if err != nil {
|
||
t.Fatalf("execute: %v", err)
|
||
}
|
||
|
||
events := sink.Events()
|
||
last := events[len(events)-1]
|
||
if last.Type != noderuntime.EventTypeComplete {
|
||
t.Fatalf("expected last event to be complete, got %q", last.Type)
|
||
}
|
||
if last.Message != "completion-marker" {
|
||
t.Fatalf("expected Message 'completion-marker', got %q", last.Message)
|
||
}
|
||
if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:hi") {
|
||
t.Fatalf("expected reply:hi in deltas, got %q", combined)
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistent_NoMarkerFallsBackToIdleTimeout(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"no-marker": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do sleep 0.1; printf "reply:%s\n" "$line"; done`},
|
||
Persistent: true,
|
||
Terminal: true,
|
||
ResponseIdleTimeoutMS: 50,
|
||
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-no-marker",
|
||
Target: "no-marker",
|
||
Input: map[string]any{"prompt": "fallback"},
|
||
}, sink)
|
||
if err != nil {
|
||
t.Fatalf("execute: %v", err)
|
||
}
|
||
|
||
events := sink.Events()
|
||
last := events[len(events)-1]
|
||
if last.Type != noderuntime.EventTypeComplete {
|
||
t.Fatalf("expected last event to be complete, got %q", last.Type)
|
||
}
|
||
if last.Message != "idle-timeout" {
|
||
t.Fatalf("expected Message 'idle-timeout' when no marker is set, got %q", last.Message)
|
||
}
|
||
if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:fallback") {
|
||
t.Fatalf("expected reply:fallback in deltas, got %q", combined)
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistent_IdleTimeoutMessageReason(t *testing.T) {
|
||
testutil.RequirePTYSupport(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"idle-reason": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do sleep 0.1; printf "reply:%s\n" "$line"; done`},
|
||
Persistent: true,
|
||
Terminal: true,
|
||
ResponseIdleTimeoutMS: 50,
|
||
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-idle-reason",
|
||
Target: "idle-reason",
|
||
Input: map[string]any{"prompt": "test"},
|
||
}, sink)
|
||
if err != nil {
|
||
t.Fatalf("execute: %v", err)
|
||
}
|
||
|
||
events := sink.Events()
|
||
var complete *noderuntime.RuntimeEvent
|
||
for i := range events {
|
||
if events[i].Type == noderuntime.EventTypeComplete {
|
||
complete = &events[i]
|
||
}
|
||
}
|
||
if complete == nil {
|
||
t.Fatal("expected complete event")
|
||
}
|
||
if complete.Message != "idle-timeout" {
|
||
t.Fatalf("expected Message 'idle-timeout', got %q", complete.Message)
|
||
}
|
||
}
|
||
|
||
func TestCLIExecutePersistentMaintainsHundredLogicalSessions(t *testing.T) {
|
||
if testing.Short() {
|
||
t.Skip("skipping heavy session test in short mode")
|
||
}
|
||
testutil.RequireUnixShell(t)
|
||
|
||
cfg := config.CLIConf{
|
||
Enabled: true,
|
||
Profiles: map[string]config.CLIProfileConf{
|
||
"hundred-echo": {
|
||
Command: "sh",
|
||
Args: []string{"-c", `while IFS= read -r line; do printf "reply:%s\n" "$line"; done`},
|
||
Persistent: true,
|
||
Terminal: false,
|
||
ResponseIdleTimeoutMS: 300,
|
||
},
|
||
},
|
||
}
|
||
c := clipkg.New(cfg, zap.NewNop())
|
||
ctx := context.Background()
|
||
|
||
const n = 20
|
||
for i := 0; i < n; i++ {
|
||
sessionID := "session-" + string(rune('a'+i%26)) + "-" + string(rune('0'+i/26))
|
||
sink := &testutil.FakeSink{}
|
||
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
||
RunID: "run-first-" + sessionID,
|
||
Target: "hundred-echo",
|
||
SessionID: sessionID,
|
||
Input: map[string]any{"prompt": "ping-" + sessionID},
|
||
}, sink)
|
||
if err != nil {
|
||
t.Fatalf("session %q first execute: %v", sessionID, err)
|
||
}
|
||
}
|
||
|
||
// Second round — all sessions must still be alive.
|
||
for i := 0; i < n; i++ {
|
||
sessionID := "session-" + string(rune('a'+i%26)) + "-" + string(rune('0'+i/26))
|
||
sink := &testutil.FakeSink{}
|
||
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
||
RunID: "run-second-" + sessionID,
|
||
Target: "hundred-echo",
|
||
SessionID: sessionID,
|
||
Input: map[string]any{"prompt": "pong-" + sessionID},
|
||
}, sink)
|
||
if err != nil {
|
||
t.Fatalf("session %q second execute: %v", sessionID, err)
|
||
}
|
||
}
|
||
|
||
_ = c.Stop(ctx)
|
||
}
|