iop/apps/node/internal/adapters/factory_internal_test.go
toki f01c7e5ecd refactor(node): CLI 실행 경계를 정리한다
터미널 세션 core와 CLI mode executor 경계를 분리해 후속 remote terminal bridge가 재사용할 내부 실행 기반을 만든다.

adapter 기본값과 vLLM experimental surface도 명시해 production 실행 경로가 mock fallback이나 미구현 실행으로 숨지 않게 한다.
2026-06-06 14:57:49 +09:00

341 lines
9.1 KiB
Go

package adapters
import (
"strings"
"testing"
"google.golang.org/protobuf/types/known/structpb"
iop "iop/proto/gen/iop"
)
func TestCLIConfFromProto_ProfileRuntimeOptions(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"claude": {
Command: "claude",
Args: []string{"-p"},
OutputFormat: "claude-json",
},
},
}
cfg := cliConfFromProto(m)
prof, ok := cfg.Profiles["claude"]
if !ok {
t.Fatal("expected claude profile")
}
if prof.Command != "claude" {
t.Fatalf("command: got %q", prof.Command)
}
if len(prof.Args) != 1 || prof.Args[0] != "-p" {
t.Fatalf("args: got %#v", prof.Args)
}
if prof.OutputFormat != "claude-json" {
t.Fatalf("output_format: got %q", prof.OutputFormat)
}
}
func TestCLIConfFromProto_ClaudeHeadlessBypassProfile(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"claude": {
Command: "claude",
Args: []string{"-p", "--dangerously-skip-permissions"},
},
},
}
cfg := cliConfFromProto(m)
prof, ok := cfg.Profiles["claude"]
if !ok {
t.Fatal("expected claude profile")
}
if len(prof.Args) != 2 || prof.Args[0] != "-p" || prof.Args[1] != "--dangerously-skip-permissions" {
t.Fatalf("args: got %#v", prof.Args)
}
if prof.Persistent || prof.Terminal {
t.Fatalf("expected persistent/terminal=false")
}
}
func TestCLIConfFromProto_AntigravityHeadlessProfile(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"antigravity": {
Command: "agy",
Args: []string{"--dangerously-skip-permissions", "--print"},
},
},
}
cfg := cliConfFromProto(m)
prof, ok := cfg.Profiles["antigravity"]
if !ok {
t.Fatal("expected antigravity profile")
}
if len(prof.Args) != 2 || prof.Args[0] != "--dangerously-skip-permissions" || prof.Args[1] != "--print" {
t.Fatalf("args: got %#v", prof.Args)
}
}
func TestCLIConfFromProto_ClineConfigProfile(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"cline-dgx": {
Command: "/config/.npm-global/bin/cline",
Args: []string{"-y", "--json", "--config", "/config/.cline/profiles/ollama-dgx"},
OutputFormat: "cline-json",
},
},
}
cfg := cliConfFromProto(m)
prof, ok := cfg.Profiles["cline-dgx"]
if !ok {
t.Fatal("expected cline-dgx profile")
}
if prof.Command != "/config/.npm-global/bin/cline" {
t.Fatalf("command: got %q", prof.Command)
}
if len(prof.Args) != 4 {
t.Fatalf("args: got %#v", prof.Args)
}
if prof.OutputFormat != "cline-json" {
t.Fatalf("output_format: got %q", prof.OutputFormat)
}
}
func TestCLIConfFromProto_ProfileCompletionMarker(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"codex": {
Command: "codex",
Persistent: true,
CompletionMarker: &iop.CLICompletionMarker{
Line: "[[DONE]]",
Regex: `^\[\[DONE\]\]$`,
},
},
},
}
cfg := cliConfFromProto(m)
prof := cfg.Profiles["codex"]
if prof.CompletionMarker.Line != "[[DONE]]" {
t.Fatalf("line: got %q", prof.CompletionMarker.Line)
}
if prof.CompletionMarker.Regex != `^\[\[DONE\]\]$` {
t.Fatalf("regex: got %q", prof.CompletionMarker.Regex)
}
}
func TestCLIConfFromProto_PersistentAndIdleTimeouts(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"codex": {
Command: "codex",
Persistent: true,
ResponseIdleTimeoutMs: 1500,
StartupIdleTimeoutMs: 300,
},
},
}
cfg := cliConfFromProto(m)
prof := cfg.Profiles["codex"]
if !prof.Persistent {
t.Fatal("persistent")
}
if prof.ResponseIdleTimeoutMS != 1500 {
t.Fatalf("response_idle_timeout_ms: got %d", prof.ResponseIdleTimeoutMS)
}
if prof.StartupIdleTimeoutMS != 300 {
t.Fatalf("startup_idle_timeout_ms: got %d", prof.StartupIdleTimeoutMS)
}
}
func TestCLIConfFromProto_NilSettings(t *testing.T) {
cfg := cliConfFromProto(&iop.CLIAdapterConfig{})
if !cfg.Enabled {
t.Fatal("expected enabled=true")
}
if cfg.Profiles == nil {
t.Fatal("profiles map should be initialized")
}
}
func TestCLIConfFromProto_OpencodeSSEMode(t *testing.T) {
m := &iop.CLIAdapterConfig{
Profiles: map[string]*iop.CLIProfileConfig{
"opencode": {
Command: "/usr/local/bin/opencode",
Args: []string{"--model", "m1", "--dangerously-skip-permissions"},
Mode: "opencode-sse",
},
},
}
cfg := cliConfFromProto(m)
prof, ok := cfg.Profiles["opencode"]
if !ok {
t.Fatal("expected opencode profile")
}
if prof.Mode != "opencode-sse" {
t.Errorf("mode: got %q", prof.Mode)
}
if len(prof.Args) != 3 {
t.Fatalf("args: got %#v", prof.Args)
}
}
func TestCLIConfFromStruct_OpencodeSSEModeLegacy(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{
"profiles": map[string]any{
"opencode": map[string]any{
"command": "/usr/local/bin/opencode",
"args": []any{"--model", "m1"},
"mode": "opencode-sse",
},
},
})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := cliConfFromStruct(st)
prof, ok := cfg.Profiles["opencode"]
if !ok {
t.Fatal("expected opencode profile")
}
if prof.Mode != "opencode-sse" {
t.Errorf("mode: got %q", prof.Mode)
}
}
// Legacy structpb fallback regression tests — these exercise the path taken
// when an older edge sends AdapterConfig.settings instead of the typed oneof.
func TestOllamaConfFromStruct_LegacyPayload(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{
"base_url": "http://legacy:11434",
"context_size": float64(262144),
})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := ollamaConfFromStruct(st)
if !cfg.Enabled {
t.Fatal("expected enabled=true")
}
if cfg.BaseURL != "http://legacy:11434" {
t.Fatalf("base_url: got %q", cfg.BaseURL)
}
if cfg.ContextSize != 262144 {
t.Fatalf("context_size: got %d", cfg.ContextSize)
}
}
func TestVllmConfFromStruct_LegacyPayload(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{"endpoint": "http://legacy:8000"})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := vllmConfFromStruct(st)
if !cfg.Enabled {
t.Fatal("expected enabled=true")
}
if cfg.Endpoint != "http://legacy:8000" {
t.Fatalf("endpoint: got %q", cfg.Endpoint)
}
}
func TestCLIConfFromStruct_LegacyPayload(t *testing.T) {
st, err := structpb.NewStruct(map[string]any{
"profiles": map[string]any{
"codex": map[string]any{
"command": "codex",
"args": []any{"--legacy"},
"env": []any{"K=V"},
"persistent": true,
"terminal": false,
"response_idle_timeout_ms": 1500,
"startup_idle_timeout_ms": 300,
"output_format": "codex-json",
"completion_marker": map[string]any{
"line": "[[DONE]]",
"regex": `^\[\[DONE\]\]$`,
},
},
},
})
if err != nil {
t.Fatalf("structpb: %v", err)
}
cfg := cliConfFromStruct(st)
prof, ok := cfg.Profiles["codex"]
if !ok {
t.Fatal("expected codex profile")
}
if prof.Command != "codex" {
t.Fatalf("command: got %q", prof.Command)
}
if len(prof.Args) != 1 || prof.Args[0] != "--legacy" {
t.Fatalf("args: got %#v", prof.Args)
}
if len(prof.Env) != 1 || prof.Env[0] != "K=V" {
t.Fatalf("env: got %#v", prof.Env)
}
if !prof.Persistent {
t.Fatal("persistent")
}
if prof.ResponseIdleTimeoutMS != 1500 {
t.Fatalf("response_idle_timeout_ms: got %d", prof.ResponseIdleTimeoutMS)
}
if prof.StartupIdleTimeoutMS != 300 {
t.Fatalf("startup_idle_timeout_ms: got %d", prof.StartupIdleTimeoutMS)
}
if prof.OutputFormat != "codex-json" {
t.Fatalf("output_format: got %q", prof.OutputFormat)
}
if prof.CompletionMarker.Line != "[[DONE]]" || prof.CompletionMarker.Regex != `^\[\[DONE\]\]$` {
t.Fatalf("completion_marker: got %+v", prof.CompletionMarker)
}
}
// End-to-end fallback through BuildFromPayload: oneof empty, settings populated.
func TestBuildFromPayload_LegacySettingsFallback(t *testing.T) {
ollamaSt, _ := structpb.NewStruct(map[string]any{"base_url": "http://legacy:11434"})
cliSt, _ := structpb.NewStruct(map[string]any{
"profiles": map[string]any{
"codex": map[string]any{
"command": "codex",
"args": []any{"--legacy"},
},
},
})
payload := &iop.NodeConfigPayload{
Adapters: []*iop.AdapterConfig{
{Type: "ollama", Enabled: true, Settings: ollamaSt},
{Type: "cli", Enabled: true, Settings: cliSt},
},
}
reg, err := BuildFromPayload(payload, nil)
if err != nil {
t.Fatalf("build: %v", err)
}
for _, name := range []string{"ollama", "cli"} {
if _, ok := reg.Get(name); !ok {
t.Fatalf("expected %s adapter registered from legacy settings", name)
}
}
}
func TestBuildFromPayload_LegacyVllmSettingsRejected(t *testing.T) {
vllmSt, _ := structpb.NewStruct(map[string]any{"endpoint": "http://legacy:8000"})
_, err := BuildFromPayload(&iop.NodeConfigPayload{
Adapters: []*iop.AdapterConfig{
{Type: "vllm", Enabled: true, Settings: vllmSt},
},
}, nil)
if err == nil {
t.Fatal("expected vllm disabled error")
}
if !strings.Contains(err.Error(), "vllm adapter is experimental and disabled") {
t.Fatalf("expected vllm disabled error, got %v", err)
}
}