Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added internal/tui/bootstrap/.gitkeep
Empty file.
90 changes: 90 additions & 0 deletions internal/tui/bootstrap/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package bootstrap

import (
"context"
"fmt"

"neo-code/internal/config"
agentruntime "neo-code/internal/runtime"
)

// ProviderService 定义 TUI 需要注入的 provider 交互能力。
type ProviderService interface {
ListProviders(ctx context.Context) ([]config.ProviderCatalogItem, error)
SelectProvider(ctx context.Context, providerID string) (config.ProviderSelection, error)
ListModels(ctx context.Context) ([]config.ModelDescriptor, error)
ListModelsSnapshot(ctx context.Context) ([]config.ModelDescriptor, error)
SetCurrentModel(ctx context.Context, modelID string) (config.ProviderSelection, error)
}

// Options 定义 bootstrap 装配输入。
type Options struct {
Config *config.Config
ConfigManager *config.Manager
Runtime agentruntime.Runtime
ProviderService ProviderService
Mode Mode
Factory ServiceFactory
}

// Container 表示完成装配后供 TUI Core 使用的依赖集合。
type Container struct {
Config config.Config
ConfigManager *config.Manager
Runtime agentruntime.Runtime
ProviderService ProviderService
Mode Mode
}

// Build 执行 TUI bootstrap 装配,并返回可注入到 App/Core 的容器。
func Build(options Options) (Container, error) {
if options.ConfigManager == nil {
return Container{}, fmt.Errorf("tui bootstrap: config manager is nil")
}
if options.Runtime == nil {
return Container{}, fmt.Errorf("tui bootstrap: runtime is nil")
}
if options.ProviderService == nil {
return Container{}, fmt.Errorf("tui bootstrap: provider service is nil")
}

mode := NormalizeMode(options.Mode)
cfg := resolveConfigSnapshot(options.Config, options.ConfigManager)

factory := options.Factory
if factory == nil {
factory = passthroughFactory{}
}

runtimeSvc, err := factory.BuildRuntime(mode, options.Runtime)
if err != nil {
return Container{}, fmt.Errorf("tui bootstrap: build runtime: %w", err)
}
if runtimeSvc == nil {
return Container{}, fmt.Errorf("tui bootstrap: runtime factory returned nil")
}

providerSvc, err := factory.BuildProvider(mode, options.ProviderService)
if err != nil {
return Container{}, fmt.Errorf("tui bootstrap: build provider service: %w", err)
}
if providerSvc == nil {
return Container{}, fmt.Errorf("tui bootstrap: provider factory returned nil")
}

return Container{
Config: cfg,
ConfigManager: options.ConfigManager,
Runtime: runtimeSvc,
ProviderService: providerSvc,
Mode: mode,
}, nil
}

// resolveConfigSnapshot 返回用于本次 TUI 初始化的配置快照。
func resolveConfigSnapshot(cfg *config.Config, manager *config.Manager) config.Config {
if cfg == nil {
return manager.Get()
}
return cfg.Clone()
}
166 changes: 166 additions & 0 deletions internal/tui/bootstrap/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package bootstrap

import (
"context"
"testing"

"neo-code/internal/config"
agentruntime "neo-code/internal/runtime"
agentsession "neo-code/internal/session"
)

type testRuntime struct{}

func (r *testRuntime) Run(ctx context.Context, input agentruntime.UserInput) error {
return nil
}

func (r *testRuntime) Compact(ctx context.Context, input agentruntime.CompactInput) (agentruntime.CompactResult, error) {
return agentruntime.CompactResult{}, nil
}

func (r *testRuntime) ResolvePermission(ctx context.Context, input agentruntime.PermissionResolutionInput) error {
return nil
}

func (r *testRuntime) Events() <-chan agentruntime.RuntimeEvent {
ch := make(chan agentruntime.RuntimeEvent)
close(ch)
return ch
}

func (r *testRuntime) CancelActiveRun() bool {
return false
}

func (r *testRuntime) ListSessions(ctx context.Context) ([]agentsession.Summary, error) {
return nil, nil
}

func (r *testRuntime) LoadSession(ctx context.Context, id string) (agentsession.Session, error) {
return agentsession.Session{}, nil
}

func (r *testRuntime) SetSessionWorkdir(ctx context.Context, sessionID string, workdir string) (agentsession.Session, error) {
return agentsession.Session{}, nil
}

type testProviderService struct{}

func (s *testProviderService) ListProviders(ctx context.Context) ([]config.ProviderCatalogItem, error) {
return nil, nil
}

func (s *testProviderService) SelectProvider(ctx context.Context, providerID string) (config.ProviderSelection, error) {
return config.ProviderSelection{}, nil
}

func (s *testProviderService) ListModels(ctx context.Context) ([]config.ModelDescriptor, error) {
return nil, nil
}

func (s *testProviderService) ListModelsSnapshot(ctx context.Context) ([]config.ModelDescriptor, error) {
return nil, nil
}

func (s *testProviderService) SetCurrentModel(ctx context.Context, modelID string) (config.ProviderSelection, error) {
return config.ProviderSelection{}, nil
}

func TestBuild(t *testing.T) {
t.Run("success", func(t *testing.T) {
manager := &config.Manager{}
runtime := &testRuntime{}
providerSvc := &testProviderService{}

container, err := Build(Options{
ConfigManager: manager,
Runtime: runtime,
ProviderService: providerSvc,
})
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if container.ConfigManager != manager {
t.Error("expected ConfigManager to be set")
}
})

t.Run("nil config manager", func(t *testing.T) {
_, err := Build(Options{
ConfigManager: nil,
Runtime: &testRuntime{},
ProviderService: &testProviderService{},
})
if err == nil {
t.Fatal("expected error for nil config manager")
}
})

t.Run("nil runtime", func(t *testing.T) {
manager := &config.Manager{}
_, err := Build(Options{
ConfigManager: manager,
Runtime: nil,
ProviderService: &testProviderService{},
})
if err == nil {
t.Fatal("expected error for nil runtime")
}
})

t.Run("nil provider service", func(t *testing.T) {
manager := &config.Manager{}
_, err := Build(Options{
ConfigManager: manager,
Runtime: &testRuntime{},
ProviderService: nil,
})
if err == nil {
t.Fatal("expected error for nil provider service")
}
})
}

func TestResolveConfigSnapshot(t *testing.T) {
t.Run("nil config returns manager get", func(t *testing.T) {
manager := &config.Manager{}
cfg := resolveConfigSnapshot(nil, manager)
if cfg.Workdir == "" && cfg.Shell == "" {
t.Log("config returned from manager")
}
})

t.Run("config provided returns clone", func(t *testing.T) {
manager := &config.Manager{}
inputCfg := &config.Config{
Workdir: "/test",
}
cfg := resolveConfigSnapshot(inputCfg, manager)
if cfg.Workdir != "/test" {
t.Errorf("expected Workdir /test, got %s", cfg.Workdir)
}
})
}

func TestNormalizeMode(t *testing.T) {
tests := []struct {
name string
input Mode
want Mode
}{
{"empty becomes live", "", ModeLive},
{"live stays live", ModeLive, ModeLive},
{"offline stays offline", ModeOffline, ModeOffline},
{"mock stays mock", ModeMock, ModeMock},
{"unknown becomes live", Mode("unknown"), ModeLive},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NormalizeMode(tt.input); got != tt.want {
t.Errorf("NormalizeMode(%v) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
25 changes: 25 additions & 0 deletions internal/tui/bootstrap/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package bootstrap

import (
agentruntime "neo-code/internal/runtime"
)

// ServiceFactory 定义 runtime/provider 的可切换装配策略。
type ServiceFactory interface {
// BuildRuntime 根据 mode 返回实际注入到 TUI 的 runtime 实现。
BuildRuntime(mode Mode, current agentruntime.Runtime) (agentruntime.Runtime, error)
// BuildProvider 根据 mode 返回实际注入到 TUI 的 provider service 实现。
BuildProvider(mode Mode, current ProviderService) (ProviderService, error)
}

type passthroughFactory struct{}

// BuildRuntime 默认直接透传已有 runtime,不做替换。
func (passthroughFactory) BuildRuntime(mode Mode, current agentruntime.Runtime) (agentruntime.Runtime, error) {
return current, nil
}

// BuildProvider 默认直接透传已有 provider service,不做替换。
func (passthroughFactory) BuildProvider(mode Mode, current ProviderService) (ProviderService, error) {
return current, nil
}
27 changes: 27 additions & 0 deletions internal/tui/bootstrap/mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package bootstrap

import "strings"

// Mode 定义 TUI bootstrap 的装配模式。
type Mode string

const (
// ModeLive 表示使用真实依赖进行正常装配。
ModeLive Mode = "live"
// ModeOffline 表示使用离线装配策略(可由工厂映射为本地实现)。
ModeOffline Mode = "offline"
// ModeMock 表示使用 mock 装配策略(通常用于测试)。
ModeMock Mode = "mock"
)

// NormalizeMode 归一化 mode 输入,未知值默认回退到 live。
func NormalizeMode(mode Mode) Mode {
switch Mode(strings.ToLower(strings.TrimSpace(string(mode)))) {
case ModeOffline:
return ModeOffline
case ModeMock:
return ModeMock
default:
return ModeLive
}
}
Loading
Loading