package cli_test import ( "context" "os" "path/filepath" "strings" "testing" "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 TestCLIStartSkipsCodexExecPersistentStartup(t *testing.T) { testutil.RequireUnixShell(t) codex := writeFakeCodex(t, `#!/usr/bin/env sh echo "codex should not be started during adapter startup" >&2 exit 42 `) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "codex": { Command: codex, Args: []string{"exec", "--dangerously-bypass-approvals-and-sandbox"}, Persistent: true, Mode: "codex-exec", ResumeArgs: []string{"exec", "resume", "--json"}, }, }, } c := clipkg.New(cfg, zap.NewNop()) if err := c.Start(context.Background()); err != nil { t.Fatalf("Start should not invoke codex exec: %v", err) } } func TestCLIExecuteCodexExecPersistentResumesLogicalSession(t *testing.T) { testutil.RequireUnixShell(t) codex := writeFakeCodex(t, `#!/usr/bin/env sh if [ "$1" != "exec" ]; then echo "unexpected command: $*" >&2 exit 2 fi if [ "$2" = "resume" ]; then shift 2 for arg in "$@"; do if [ "$arg" = "--color" ]; then echo "resume received unsupported --color option" >&2 exit 12 fi done session="" prompt="" for arg in "$@"; do case "$arg" in --*) continue ;; esac if [ -z "$session" ]; then session="$arg" else prompt="$arg" fi done printf "session id: %s\n" "$session" >&2 printf "reply:resume:%s:%s\n" "$session" "$prompt" exit 0 fi last="" for arg in "$@"; do last="$arg" done printf "session id: codex-session-1\n" >&2 printf "reply:first:%s\n" "$last" `) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "codex": { Command: codex, Args: []string{ "exec", "--dangerously-bypass-approvals-and-sandbox", "--color", "never", "--skip-git-repo-check", }, Mode: "codex-exec", ResumeArgs: []string{"exec", "resume", "--json"}, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() first := &testutil.FakeSink{} if err := c.Execute(ctx, noderuntime.ExecutionSpec{ RunID: "run-1", Target: "codex", SessionID: "session-a", Input: map[string]any{"prompt": "first"}, }, first); err != nil { t.Fatalf("first execute: %v", err) } if combined := testutil.CollectDeltas(first.Events()); !strings.Contains(combined, "reply:first:first") { t.Fatalf("first execute output: %q", combined) } second := &testutil.FakeSink{} if err := c.Execute(ctx, noderuntime.ExecutionSpec{ RunID: "run-2", Target: "codex", SessionID: "session-a", Input: map[string]any{"prompt": "second"}, }, second); err != nil { t.Fatalf("second execute: %v", err) } if combined := testutil.CollectDeltas(second.Events()); !strings.Contains(combined, "reply:resume:codex-session-1:second") { t.Fatalf("second execute output: %q", combined) } } func TestCLIExecuteCodexExecJSONFormatStreamsAgentMessageAndResumes(t *testing.T) { testutil.RequireUnixShell(t) codex := writeFakeCodex(t, `#!/usr/bin/env sh if [ "$1" != "exec" ]; then echo "unexpected command: $*" >&2 exit 2 fi if [ "$2" = "resume" ]; then shift 2 session="" prompt="" for arg in "$@"; do case "$arg" in --*) continue ;; esac if [ -z "$session" ]; then session="$arg" else prompt="$arg" fi done printf '{"type":"thread.started","thread_id":"%s"}\n' "$session" printf '{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"resume:%s:%s"}}\n' "$session" "$prompt" exit 0 fi last="" for arg in "$@"; do last="$arg" done printf '{"type":"thread.started","thread_id":"codex-session-json-1"}\n' printf '{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"first:%s"}}\n' "$last" `) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "codex": { Command: codex, Args: []string{ "exec", "--dangerously-bypass-approvals-and-sandbox", "--color", "never", "--skip-git-repo-check", "--json", }, Persistent: true, OutputFormat: "codex-json", Mode: "codex-exec", ResumeArgs: []string{"exec", "resume", "--json"}, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() first := &testutil.FakeSink{} if err := c.Execute(ctx, noderuntime.ExecutionSpec{ RunID: "run-1", Target: "codex", SessionID: "session-a", Input: map[string]any{"prompt": "hello"}, }, first); err != nil { t.Fatalf("first execute: %v", err) } if combined := testutil.CollectDeltas(first.Events()); combined != "first:hello" { t.Fatalf("first deltas: got %q want %q", combined, "first:hello") } second := &testutil.FakeSink{} if err := c.Execute(ctx, noderuntime.ExecutionSpec{ RunID: "run-2", Target: "codex", SessionID: "session-a", Input: map[string]any{"prompt": "again"}, }, second); err != nil { t.Fatalf("second execute: %v", err) } if combined := testutil.CollectDeltas(second.Events()); combined != "resume:codex-session-json-1:again" { t.Fatalf("second deltas: got %q", combined) } } func TestCLIExecuteCodexExec_MissingResumeArgsErrors(t *testing.T) { testutil.RequireUnixShell(t) codex := writeFakeCodex(t, `#!/usr/bin/env sh echo "should not be called when resume_args is missing" >&2 exit 42 `) cfg := config.CLIConf{ Enabled: true, Profiles: map[string]config.CLIProfileConf{ "codex": { Command: codex, Args: []string{"exec", "--json"}, Mode: "codex-exec", ResumeArgs: nil, }, }, } c := clipkg.New(cfg, zap.NewNop()) ctx := context.Background() sink := &testutil.FakeSink{} err := c.Execute(ctx, noderuntime.ExecutionSpec{ RunID: "run-1", Target: "codex", SessionID: "session-b", Input: map[string]any{"prompt": "test"}, }, sink) if err == nil { t.Fatalf("expected error due to missing resume_args, got nil") } if !strings.Contains(err.Error(), "resume_args") { t.Fatalf("expected error to mention resume_args, got: %v", err) } } func writeFakeCodex(t *testing.T, script string) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "codex") if err := os.WriteFile(path, []byte(script), 0o755); err != nil { t.Fatalf("write fake codex: %v", err) } return path }