- edge node의 runtime 재연결 시 config refresh機制 구현 - node adapter에 config set 및 diff 기능 추가 - proto runtime에 config refresh 관련 스키마 추가 - edge transport server에 config refresh 핸들러 추가 - 관련 테스트 코드 및 archive 작업 기록 추가
303 lines
7.7 KiB
Go
303 lines
7.7 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"iop/apps/node/internal/adapters"
|
|
"iop/apps/node/internal/runtime"
|
|
)
|
|
|
|
type stubAdapter struct{ name string }
|
|
|
|
func (s *stubAdapter) Name() string { return s.name }
|
|
func (s *stubAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
|
|
return runtime.Capabilities{AdapterName: s.name}, nil
|
|
}
|
|
func (s *stubAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
|
|
return nil
|
|
}
|
|
|
|
func TestResolve_PreservesSessionFields(t *testing.T) {
|
|
reg := adapters.NewRegistry()
|
|
reg.Register(&stubAdapter{name: "cli"})
|
|
r := New(reg, zap.NewNop())
|
|
|
|
req := runtime.RunRequest{
|
|
RunID: "run-1",
|
|
Adapter: "cli",
|
|
Target: "codex",
|
|
SessionID: "session-a",
|
|
SessionMode: runtime.SessionModeRequireExisting,
|
|
Background: true,
|
|
Workspace: "/config/workspace/iop",
|
|
}
|
|
|
|
spec, err := r.Resolve(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
if spec.SessionID != req.SessionID {
|
|
t.Errorf("SessionID: got %q want %q", spec.SessionID, req.SessionID)
|
|
}
|
|
if spec.SessionMode != req.SessionMode {
|
|
t.Errorf("SessionMode: got %q want %q", spec.SessionMode, req.SessionMode)
|
|
}
|
|
if spec.Background != req.Background {
|
|
t.Errorf("Background: got %v want %v", spec.Background, req.Background)
|
|
}
|
|
if spec.Workspace != req.Workspace {
|
|
t.Errorf("Workspace: got %q want %q", spec.Workspace, req.Workspace)
|
|
}
|
|
}
|
|
|
|
func TestResolveAdapter_Found(t *testing.T) {
|
|
reg := adapters.NewRegistry()
|
|
expected := &stubAdapter{name: "cli"}
|
|
reg.Register(expected)
|
|
r := New(reg, zap.NewNop())
|
|
|
|
req := runtime.RunRequest{
|
|
RunID: "run-1",
|
|
Adapter: "cli",
|
|
Target: "codex",
|
|
}
|
|
|
|
spec, adapter, err := r.ResolveAdapter(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("ResolveAdapter: %v", err)
|
|
}
|
|
if spec.Adapter != "cli" {
|
|
t.Errorf("expected adapter name 'cli', got %q", spec.Adapter)
|
|
}
|
|
if adapter != expected {
|
|
t.Fatal("expected same adapter instance")
|
|
}
|
|
}
|
|
|
|
func TestResolveAdapter_NotFound(t *testing.T) {
|
|
reg := adapters.NewRegistry()
|
|
r := New(reg, zap.NewNop())
|
|
|
|
req := runtime.RunRequest{
|
|
RunID: "run-1",
|
|
Adapter: "nonexistent",
|
|
Target: "codex",
|
|
}
|
|
|
|
_, _, err := r.ResolveAdapter(context.Background(), req)
|
|
if err == nil {
|
|
t.Fatal("expected error for unregistered adapter")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found") {
|
|
t.Fatalf("expected 'not found' in error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveAdapter_RequiresExplicitAdapter(t *testing.T) {
|
|
reg := adapters.NewRegistry()
|
|
reg.Register(&stubAdapter{name: "mock"})
|
|
r := New(reg, zap.NewNop())
|
|
|
|
req := runtime.RunRequest{
|
|
RunID: "run-1",
|
|
Target: "mock-echo",
|
|
}
|
|
|
|
_, _, err := r.ResolveAdapter(context.Background(), req)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty adapter")
|
|
}
|
|
if !strings.Contains(err.Error(), "adapter is required") {
|
|
t.Fatalf("expected adapter required error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveAdapter_InstanceKeyLookup(t *testing.T) {
|
|
reg := adapters.NewRegistry()
|
|
local := &stubAdapter{name: "ollama@local"}
|
|
dgx := &stubAdapter{name: "ollama@dgx"}
|
|
reg.RegisterKeyed("ollama@local", "ollama", local)
|
|
reg.RegisterKeyed("ollama@dgx", "ollama", dgx)
|
|
r := New(reg, zap.NewNop())
|
|
|
|
spec, adapter, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{
|
|
RunID: "run-1",
|
|
Adapter: "ollama@local",
|
|
Target: "llama3",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ResolveAdapter: %v", err)
|
|
}
|
|
if spec.Adapter != "ollama@local" {
|
|
t.Errorf("spec.Adapter: got %q want %q", spec.Adapter, "ollama@local")
|
|
}
|
|
if adapter != local {
|
|
t.Fatal("expected ollama@local adapter instance")
|
|
}
|
|
|
|
spec2, adapter2, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{
|
|
RunID: "run-2",
|
|
Adapter: "ollama@dgx",
|
|
Target: "mixtral",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ResolveAdapter dgx: %v", err)
|
|
}
|
|
if spec2.Adapter != "ollama@dgx" {
|
|
t.Errorf("spec2.Adapter: got %q want %q", spec2.Adapter, "ollama@dgx")
|
|
}
|
|
if adapter2 != dgx {
|
|
t.Fatal("expected ollama@dgx adapter instance")
|
|
}
|
|
}
|
|
|
|
func TestResolveAdapter_AmbiguousLegacyLookup(t *testing.T) {
|
|
reg := adapters.NewRegistry()
|
|
reg.RegisterKeyed("ollama@local", "ollama", &stubAdapter{name: "ollama@local"})
|
|
reg.RegisterKeyed("ollama@dgx", "ollama", &stubAdapter{name: "ollama@dgx"})
|
|
r := New(reg, zap.NewNop())
|
|
|
|
_, _, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{
|
|
RunID: "run-1",
|
|
Adapter: "ollama",
|
|
Target: "llama3",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for ambiguous adapter type-name with multiple instances")
|
|
}
|
|
if !strings.Contains(err.Error(), "ambiguous") {
|
|
t.Errorf("expected 'ambiguous' in error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveAdapter_LegacyTypeName_SingleInstance(t *testing.T) {
|
|
reg := adapters.NewRegistry()
|
|
single := &stubAdapter{name: "ollama@prod"}
|
|
reg.RegisterKeyed("ollama@prod", "ollama", single)
|
|
r := New(reg, zap.NewNop())
|
|
|
|
spec, adapter, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{
|
|
RunID: "run-1",
|
|
Adapter: "ollama",
|
|
Target: "llama3",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ResolveAdapter legacy single: %v", err)
|
|
}
|
|
if spec.Adapter != "ollama" {
|
|
t.Errorf("spec.Adapter: got %q want %q", spec.Adapter, "ollama")
|
|
}
|
|
if adapter != single {
|
|
t.Fatal("expected the single ollama instance")
|
|
}
|
|
}
|
|
|
|
func TestGetAdapter_AmbiguousReturnsNotFound(t *testing.T) {
|
|
reg := adapters.NewRegistry()
|
|
reg.RegisterKeyed("ollama@a", "ollama", &stubAdapter{name: "ollama@a"})
|
|
reg.RegisterKeyed("ollama@b", "ollama", &stubAdapter{name: "ollama@b"})
|
|
r := New(reg, zap.NewNop())
|
|
|
|
_, ok := r.GetAdapter("ollama")
|
|
if ok {
|
|
t.Fatal("expected GetAdapter to return false for ambiguous type name")
|
|
}
|
|
}
|
|
|
|
func TestRouterSetRegistryAffectsSubsequentResolve(t *testing.T) {
|
|
reg1 := adapters.NewRegistry()
|
|
reg1.Register(&stubAdapter{name: "adapter1"})
|
|
r := New(reg1, zap.NewNop())
|
|
|
|
// Resolving adapter1 should succeed initially.
|
|
_, _, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{
|
|
RunID: "run-1",
|
|
Adapter: "adapter1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("resolve adapter1 initially: %v", err)
|
|
}
|
|
|
|
// Resolving adapter2 should fail initially.
|
|
_, _, err = r.ResolveAdapter(context.Background(), runtime.RunRequest{
|
|
RunID: "run-2",
|
|
Adapter: "adapter2",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error resolving adapter2 initially")
|
|
}
|
|
|
|
// Swap registry with one containing only adapter2.
|
|
reg2 := adapters.NewRegistry()
|
|
reg2.Register(&stubAdapter{name: "adapter2"})
|
|
r.SetRegistry(reg2)
|
|
|
|
// Resolving adapter1 should now fail.
|
|
_, _, err = r.ResolveAdapter(context.Background(), runtime.RunRequest{
|
|
RunID: "run-3",
|
|
Adapter: "adapter1",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error resolving adapter1 after swap")
|
|
}
|
|
|
|
// Resolving adapter2 should now succeed.
|
|
_, _, err = r.ResolveAdapter(context.Background(), runtime.RunRequest{
|
|
RunID: "run-4",
|
|
Adapter: "adapter2",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("resolve adapter2 after swap: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRouterResolveAdapterConcurrentSetRegistryDoesNotDeadlock(t *testing.T) {
|
|
reg1 := adapters.NewRegistry()
|
|
reg1.Register(&stubAdapter{name: "cli"})
|
|
r := New(reg1, zap.NewNop())
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
|
defer cancel()
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
_, _, err := r.ResolveAdapter(ctx, runtime.RunRequest{
|
|
RunID: "run-1",
|
|
Adapter: "cli",
|
|
})
|
|
_ = err
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
reg2 := adapters.NewRegistry()
|
|
reg2.Register(&stubAdapter{name: "cli"})
|
|
emptyReg := adapters.NewRegistry()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
close(done)
|
|
return
|
|
default:
|
|
r.SetRegistry(reg2)
|
|
r.SetRegistry(emptyReg)
|
|
}
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Success! Did not deadlock within timeout.
|
|
}
|
|
}
|