iop/apps/node/internal/adapters/factory_internal_test.go
toki dd488f1e56 refactor(runtime): 포트 표준화와 브리지 경계를 반영한다
워크스페이스 기본 포트와 검증 문서를 실제 런타임 기본값에 맞추고, 브리지 경계에서 adapter config 타입 정보를 보존해야 이후 원격 smoke와 경계 안정화 작업이 같은 계약을 사용할 수 있다.
2026-06-07 17:51:54 +09:00

359 lines
9.5 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)
}
}
func TestBuildFromPayload_TypedMockConfig(t *testing.T) {
reg, err := BuildFromPayload(&iop.NodeConfigPayload{
Adapters: []*iop.AdapterConfig{
{
Type: "mock",
Enabled: true,
Config: &iop.AdapterConfig_Mock{Mock: &iop.MockAdapterConfig{}},
},
},
}, nil)
if err != nil {
t.Fatalf("build: %v", err)
}
if _, ok := reg.Get("mock"); !ok {
t.Fatal("expected mock adapter registered from typed MockAdapterConfig")
}
}