codex-exec 모드를 명시적으로 지정하기 위해 CLIProfileConf에 mode와 resume_args 필드를 추가한다. 기존 isCodexExecProfile()의 명명 기반 판별을 삭제하고 설정 기반으로 전환한다. 주요 변경사항: - CLIProfileConf에 mode, resume_args 필드 추가 - codex_exec.go에서 isCodexExecProfile(), codexResumeOptions() 삭제 - resume_args를 사용하여 resume 명령어 구성 - edge.yaml에 codex 프로필 mode와 resume_args 설정 추가 - codex-exec 모드 전용 테스트 케이스 추가
268 lines
6.4 KiB
Go
268 lines
6.4 KiB
Go
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",
|
|
Model: "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",
|
|
Model: "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",
|
|
Model: "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",
|
|
Model: "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",
|
|
Model: "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
|
|
}
|