yao/agent/sandbox/v2/opencode/roles_test.go
Max 194faac9b7 feat(agent): enhance sandbox V2 initialization and role management
- Refactored the initSandboxV2 function to return a structured result, consolidating the runner, computer, configuration, cleanup function, loading message ID, and roles into a single return type.
- Updated the Stream method to utilize the new sandboxV2InitResult structure, improving clarity and reducing complexity in handling sandbox initialization.
- Introduced role management enhancements, allowing for pre-resolved role connectors to be passed through the request, streamlining connector resolution during execution.
- Adjusted various components to support the new roles structure, ensuring consistent handling across the agent's sandbox operations.
- Added logging for connector resolution and role management, improving diagnostics and traceability during sandbox execution.
- Updated .gitignore to include tools/TOOL-REGISTRATION.md for better project organization.
2026-05-03 19:11:29 +08:00

459 lines
14 KiB
Go

package opencode
import (
"encoding/json"
"testing"
"github.com/yaoapp/gou/connector"
gouTypes "github.com/yaoapp/gou/types"
"github.com/yaoapp/xun/dbal/query"
"github.com/yaoapp/xun/dbal/schema"
"github.com/yaoapp/yao/agent/sandbox/v2/types"
)
type fakeConn struct {
id string
typ int
settings map[string]interface{}
}
func (f *fakeConn) Register(string, string, []byte) error { return nil }
func (f *fakeConn) Query() (query.Query, error) { return nil, nil }
func (f *fakeConn) Schema() (schema.Schema, error) { return nil, nil }
func (f *fakeConn) Close() error { return nil }
func (f *fakeConn) ID() string { return f.id }
func (f *fakeConn) Is(t int) bool { return f.typ == t }
func (f *fakeConn) Setting() map[string]interface{} { return f.settings }
func (f *fakeConn) GetMetaInfo() gouTypes.MetaInfo { return gouTypes.MetaInfo{} }
func newFakeOpenAI(id, host, model, key string) *fakeConn {
return &fakeConn{
id: id,
typ: connector.OPENAI,
settings: map[string]interface{}{
"host": host,
"model": model,
"key": key,
},
}
}
func newFakeAnthropic(id, host, model, key string) *fakeConn {
return &fakeConn{
id: id,
typ: connector.ANTHROPIC,
settings: map[string]interface{}{
"host": host,
"model": model,
"key": key,
},
}
}
// ---------------------------------------------------------------------------
// injectRoleProviders tests
// ---------------------------------------------------------------------------
func TestInjectRoleProviders_VisionCustomProvider(t *testing.T) {
visionConn := newFakeOpenAI("vis", "https://api.mymaas.com/v1", "gpt-4o-mini", "sk-vis")
primaryConn := newFakeOpenAI("primary", "https://api.deepseek.com", "deepseek-v4-flash", "sk-ds")
cfg := map[string]any{
"provider": map[string]any{"custom": map[string]any{"npm": "@ai-sdk/openai-compatible"}},
"model": "custom/deepseek-v4-flash",
"enabled_providers": []string{"custom"},
}
req := &types.PrepareRequest{
Connector: primaryConn,
Config: &types.SandboxConfig{},
Roles: map[string]connector.Connector{
"default": primaryConn,
"vision": visionConn,
},
}
injectRoleProviders(cfg, req, primaryConn)
providers := cfg["provider"].(map[string]any)
visionBlock, ok := providers["vision"]
if !ok {
t.Fatal("should have injected 'vision' provider block")
}
vBlock := visionBlock.(map[string]any)
models := vBlock["models"].(map[string]any)
if _, ok := models["gpt-4o-mini"]; !ok {
t.Fatal("vision provider should contain gpt-4o-mini model")
}
enabled := cfg["enabled_providers"].([]string)
hasVision := false
for _, e := range enabled {
if e == "vision" {
hasVision = true
}
}
if !hasVision {
t.Error("enabled_providers should contain 'vision'")
}
}
func TestInjectRoleProviders_VisionNativeOpenAI(t *testing.T) {
visionConn := newFakeOpenAI("vis", "", "gpt-4o-mini", "sk-oai")
primaryConn := newFakeOpenAI("primary", "https://api.deepseek.com", "deepseek-v4-flash", "sk-ds")
cfg := map[string]any{
"provider": map[string]any{"custom": map[string]any{"npm": "@ai-sdk/openai-compatible"}},
"model": "custom/deepseek-v4-flash",
"enabled_providers": []string{"custom"},
}
req := &types.PrepareRequest{
Connector: primaryConn,
Config: &types.SandboxConfig{},
Roles: map[string]connector.Connector{
"default": primaryConn,
"vision": visionConn,
},
}
injectRoleProviders(cfg, req, primaryConn)
providers := cfg["provider"].(map[string]any)
if _, ok := providers["vision"]; !ok {
t.Fatal("should have separate 'vision' provider (different host from primary)")
}
}
func TestInjectRoleProviders_LightWithDifferentHost(t *testing.T) {
lightConn := newFakeOpenAI("moonshot", "https://api.moonshot.cn/v1", "moonshot-v1-8k", "sk-moon")
primaryConn := newFakeOpenAI("primary", "https://api.deepseek.com", "deepseek-v4-flash", "sk-ds")
cfg := map[string]any{
"provider": map[string]any{"custom": map[string]any{"npm": "@ai-sdk/openai-compatible"}},
"model": "custom/deepseek-v4-flash",
"enabled_providers": []string{"custom"},
}
req := &types.PrepareRequest{
Connector: primaryConn,
Config: &types.SandboxConfig{},
Roles: map[string]connector.Connector{
"default": primaryConn,
"light": lightConn,
},
}
injectRoleProviders(cfg, req, primaryConn)
providers := cfg["provider"].(map[string]any)
if _, ok := providers["light"]; !ok {
t.Fatal("light role should have its own provider block when host differs from primary")
}
smallModel, ok := cfg["small_model"].(string)
if !ok || smallModel == "" {
t.Fatal("small_model should be set for light role")
}
if smallModel != "light/moonshot-v1-8k" {
t.Errorf("small_model = %q, want 'light/moonshot-v1-8k'", smallModel)
}
enabled := cfg["enabled_providers"].([]string)
hasLight := false
for _, e := range enabled {
if e == "light" {
hasLight = true
}
}
if !hasLight {
t.Error("enabled_providers should contain 'light'")
}
}
func TestInjectRoleProviders_LightSameHostAsPrimary(t *testing.T) {
lightConn := newFakeOpenAI("ds-light", "https://api.deepseek.com", "deepseek-chat", "sk-ds")
primaryConn := newFakeOpenAI("primary", "https://api.deepseek.com", "deepseek-v4-flash", "sk-ds")
primaryProviderID, primaryCfg, modelStr := buildProviderConfig(primaryConn)
cfg := map[string]any{
"provider": map[string]any{primaryProviderID: primaryCfg},
"model": modelStr,
"enabled_providers": []string{primaryProviderID},
}
req := &types.PrepareRequest{
Connector: primaryConn,
Config: &types.SandboxConfig{},
Roles: map[string]connector.Connector{
"default": primaryConn,
"light": lightConn,
},
}
injectRoleProviders(cfg, req, primaryConn)
providers := cfg["provider"].(map[string]any)
if _, ok := providers["light"]; ok {
t.Error("light should merge into primary block when same host, not create separate block")
}
customBlock := providers["custom"].(map[string]any)
models := customBlock["models"].(map[string]any)
if _, ok := models["deepseek-chat"]; !ok {
t.Error("light model should be merged into primary's 'custom' provider models")
}
smallModel := cfg["small_model"].(string)
if smallModel != "custom/deepseek-chat" {
t.Errorf("small_model = %q, want 'custom/deepseek-chat'", smallModel)
}
}
func TestInjectRoleProviders_NoConnectors(t *testing.T) {
cfg := map[string]any{
"provider": map[string]any{"openai": map[string]any{}},
"model": "openai/gpt-4o",
"enabled_providers": []string{"openai"},
}
primaryConn := newFakeOpenAI("primary", "", "gpt-4o", "sk-oai")
req := &types.PrepareRequest{
Connector: primaryConn,
Config: &types.SandboxConfig{},
}
injectRoleProviders(cfg, req, primaryConn)
enabled := cfg["enabled_providers"].([]string)
if len(enabled) != 1 || enabled[0] != "openai" {
t.Errorf("enabled_providers should be unchanged: %v", enabled)
}
}
func TestInjectRoleProviders_AnthropicVision(t *testing.T) {
visionConn := newFakeAnthropic("claude-vis", "https://api.anthropic.com", "claude-sonnet-4-5-20250929", "sk-ant")
primaryConn := newFakeOpenAI("primary", "https://api.deepseek.com", "deepseek-v4-flash", "sk-ds")
cfg := map[string]any{
"provider": map[string]any{"custom": map[string]any{"npm": "@ai-sdk/openai-compatible"}},
"model": "custom/deepseek-v4-flash",
"enabled_providers": []string{"custom"},
}
req := &types.PrepareRequest{
Connector: primaryConn,
Config: &types.SandboxConfig{},
Roles: map[string]connector.Connector{
"default": primaryConn,
"vision": visionConn,
},
}
injectRoleProviders(cfg, req, primaryConn)
providers := cfg["provider"].(map[string]any)
visionBlock, ok := providers["vision"]
if !ok {
t.Fatal("should inject 'vision' provider for Anthropic connector")
}
vBlock := visionBlock.(map[string]any)
if vBlock["npm"] != nil {
t.Error("Anthropic provider should NOT have npm field")
}
}
// ---------------------------------------------------------------------------
// buildEnv role injection tests
// ---------------------------------------------------------------------------
func TestInjectRoleEnvVars_Vision(t *testing.T) {
visionConn := newFakeOpenAI("vis", "https://api.mymaas.com/v1", "gpt-4o-mini", "sk-vis-key")
req := &types.StreamRequest{
Config: &types.SandboxConfig{},
Roles: map[string]connector.Connector{
"vision": visionConn,
},
}
env := map[string]string{}
injectRoleEnvVars(env, req)
if env["YAO_VISION_KEY"] != "sk-vis-key" {
t.Errorf("YAO_VISION_KEY = %q, want 'sk-vis-key'", env["YAO_VISION_KEY"])
}
if env["YAO_VISION_BASE_URL"] != "https://api.mymaas.com/v1" {
t.Errorf("YAO_VISION_BASE_URL = %q, want 'https://api.mymaas.com/v1'", env["YAO_VISION_BASE_URL"])
}
if env["YAO_VISION_MODEL"] != "gpt-4o-mini" {
t.Errorf("YAO_VISION_MODEL = %q, want 'gpt-4o-mini'", env["YAO_VISION_MODEL"])
}
}
func TestInjectRoleEnvVars_Light(t *testing.T) {
lightConn := newFakeOpenAI("moon", "https://api.moonshot.cn/v1", "moonshot-v1-8k", "sk-moon")
req := &types.StreamRequest{
Config: &types.SandboxConfig{},
Roles: map[string]connector.Connector{
"light": lightConn,
},
}
env := map[string]string{}
injectRoleEnvVars(env, req)
if env["YAO_LIGHT_KEY"] != "sk-moon" {
t.Errorf("YAO_LIGHT_KEY = %q, want 'sk-moon'", env["YAO_LIGHT_KEY"])
}
if env["YAO_LIGHT_BASE_URL"] != "https://api.moonshot.cn/v1" {
t.Errorf("YAO_LIGHT_BASE_URL = %q, want 'https://api.moonshot.cn/v1'", env["YAO_LIGHT_BASE_URL"])
}
if env["YAO_LIGHT_MODEL"] != "moonshot-v1-8k" {
t.Errorf("YAO_LIGHT_MODEL = %q, want 'moonshot-v1-8k'", env["YAO_LIGHT_MODEL"])
}
}
func TestInjectRoleEnvVars_NoConnectors(t *testing.T) {
req := &types.StreamRequest{
Config: &types.SandboxConfig{},
}
env := map[string]string{}
injectRoleEnvVars(env, req)
for _, prefix := range []string{"YAO_VISION", "YAO_LIGHT"} {
for _, suffix := range []string{"_KEY", "_BASE_URL", "_MODEL"} {
if v, ok := env[prefix+suffix]; ok {
t.Errorf("unexpected env %s=%s with no connectors", prefix+suffix, v)
}
}
}
}
func TestInjectRoleEnvVars_MultipleRoles(t *testing.T) {
visionConn := newFakeOpenAI("vis", "https://api.vision.com", "vis-model", "sk-vis")
lightConn := newFakeOpenAI("light-c", "https://api.light.com", "light-model", "sk-light")
req := &types.StreamRequest{
Config: &types.SandboxConfig{},
Roles: map[string]connector.Connector{
"vision": visionConn,
"light": lightConn,
},
}
env := map[string]string{}
injectRoleEnvVars(env, req)
if env["YAO_VISION_KEY"] != "sk-vis" {
t.Errorf("YAO_VISION_KEY = %q", env["YAO_VISION_KEY"])
}
if env["YAO_LIGHT_KEY"] != "sk-light" {
t.Errorf("YAO_LIGHT_KEY = %q", env["YAO_LIGHT_KEY"])
}
}
// ---------------------------------------------------------------------------
// Full integration: buildOpenCodeConfig with role connectors
// ---------------------------------------------------------------------------
func TestBuildOpenCodeConfig_WithVisionAndLight(t *testing.T) {
visionConn := newFakeOpenAI("vis", "https://api.mymaas.com/v1", "gpt-4o-mini", "sk-vis")
lightConn := newFakeOpenAI("moon", "https://api.moonshot.cn/v1", "moonshot-v1-8k", "sk-moon")
primaryConn := newFakeOpenAI("primary", "https://api.deepseek.com", "deepseek-v4-flash", "sk-ds")
req := &types.PrepareRequest{
AssistantID: "test-assistant",
Connector: primaryConn,
Config: &types.SandboxConfig{},
Roles: map[string]connector.Connector{
"default": primaryConn,
"vision": visionConn,
"light": lightConn,
},
}
data := buildOpenCodeConfig(req, nil)
var cfg map[string]any
if err := json.Unmarshal(data, &cfg); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
providers := cfg["provider"].(map[string]any)
if _, ok := providers["custom"]; !ok {
t.Error("should have 'custom' provider for primary DeepSeek")
}
if _, ok := providers["vision"]; !ok {
t.Error("should have 'vision' provider block")
}
if _, ok := providers["light"]; !ok {
t.Error("should have 'light' provider block (different host from primary)")
}
if cfg["model"] != "custom/deepseek-v4-flash" {
t.Errorf("model = %v, want custom/deepseek-v4-flash", cfg["model"])
}
if cfg["small_model"] != "light/moonshot-v1-8k" {
t.Errorf("small_model = %v, want light/moonshot-v1-8k", cfg["small_model"])
}
enabled := cfg["enabled_providers"].([]any)
enabledSet := map[string]bool{}
for _, e := range enabled {
enabledSet[e.(string)] = true
}
for _, want := range []string{"custom", "vision", "light"} {
if !enabledSet[want] {
t.Errorf("enabled_providers should contain %q", want)
}
}
}
// ---------------------------------------------------------------------------
// resolvePrimaryConnector tests
// ---------------------------------------------------------------------------
func TestResolvePrimaryConnector_HeavyConfigured(t *testing.T) {
defaultConn := newFakeOpenAI("default", "https://api.deepseek.com", "deepseek-chat", "sk-ds")
heavyConn := newFakeOpenAI("heavy", "https://api.openai.com", "o3-pro", "sk-oai")
roles := map[string]connector.Connector{
"default": defaultConn,
"heavy": heavyConn,
}
result := resolvePrimaryConnector(defaultConn, roles)
if result != heavyConn {
t.Error("should return heavy connector when configured")
}
}
func TestResolvePrimaryConnector_NoHeavy(t *testing.T) {
defaultConn := newFakeOpenAI("default", "https://api.deepseek.com", "deepseek-chat", "sk-ds")
lightConn := newFakeOpenAI("light", "https://api.moonshot.cn", "moon-v1", "sk-moon")
roles := map[string]connector.Connector{
"default": defaultConn,
"light": lightConn,
}
result := resolvePrimaryConnector(defaultConn, roles)
if result != defaultConn {
t.Error("should fallback to default when heavy not configured")
}
}
func TestResolvePrimaryConnector_NilRoles(t *testing.T) {
defaultConn := newFakeOpenAI("default", "https://api.deepseek.com", "deepseek-chat", "sk-ds")
result := resolvePrimaryConnector(defaultConn, nil)
if result != defaultConn {
t.Error("should return default when roles is nil")
}
}