Files
GoLoom/backend/internal/config/loader_test.go

268 lines
8.2 KiB
Go

package config
import (
"os"
"testing"
"ai-agent-scaffold-go/internal/model"
"github.com/stretchr/testify/assert"
)
// ============================================================
// 辅助函数
// ============================================================
func newTestTable() model.AiAgentConfigTable {
return model.AiAgentConfigTable{
AppName: "test-app",
Agent: model.AgentSummary{AgentID: "10001", AgentName: "test", AgentDesc: "test agent"},
Module: model.AgentModule{
AiAPI: model.AiAPIConfig{BaseURL: "http://localhost:8080", APIKey: "test-key"},
ChatModel: model.ChatModelConfig{Model: "gpt-4"},
Agents: []model.AgentConfig{{Name: "bot", Instruction: "hello"}},
Runner: model.RunnerConfig{AgentName: "bot"},
},
}
}
func newTestYAML() []byte {
return []byte(`
ai:
agent:
config:
tables:
test-app:
app-name: test-app
agent:
agent-id: "10001"
agent-name: test
agent-desc: test agent
module:
ai-api:
base-url: "http://localhost:8080"
api-key: "test-key"
chat-model:
model: "gpt-4"
agents:
- name: bot
instruction: hello
runner:
agent-name: bot
`)
}
// ============================================================
// expandEnvPlaceholders 测试
// ============================================================
func TestExpandEnvPlaceholders_SetVar_ReturnsValue(t *testing.T) {
t.Setenv("TEST_URL", "http://example.com")
result := expandEnvPlaceholders("url=${TEST_URL}")
assert.Equal(t, "url=http://example.com", result)
}
func TestExpandEnvPlaceholders_UnsetVar_NoDefault_ReturnsEmpty(t *testing.T) {
os.Unsetenv("TEST_MISSING_VAR")
result := expandEnvPlaceholders("url=${TEST_MISSING_VAR}")
assert.Equal(t, "url=", result)
}
func TestExpandEnvPlaceholders_UnsetVar_WithDefault_ReturnsDefault(t *testing.T) {
os.Unsetenv("TEST_DEFAULT_VAR")
result := expandEnvPlaceholders("url=${TEST_DEFAULT_VAR:-http://localhost}")
assert.Equal(t, "url=http://localhost", result)
}
func TestExpandEnvPlaceholders_SetVar_IgnoresDefault(t *testing.T) {
t.Setenv("TEST_OVERRIDE", "http://real.com")
result := expandEnvPlaceholders("url=${TEST_OVERRIDE:-http://default.com}")
assert.Equal(t, "url=http://real.com", result)
}
func TestExpandEnvPlaceholders_MultipleVars(t *testing.T) {
t.Setenv("VAR_A", "aaa")
t.Setenv("VAR_B", "bbb")
result := expandEnvPlaceholders("${VAR_A}-${VAR_B}")
assert.Equal(t, "aaa-bbb", result)
}
// ============================================================
// validateTable 测试 — table-driven
// ============================================================
func TestValidateTable(t *testing.T) {
tests := []struct {
name string
modify func(*model.AiAgentConfigTable)
wantErr string
}{
{"missing app-name", func(t *model.AiAgentConfigTable) { t.AppName = "" }, "app-name"},
{"missing agent-id", func(t *model.AiAgentConfigTable) { t.Agent.AgentID = "" }, "agent.agent-id"},
{"missing base-url", func(t *model.AiAgentConfigTable) { t.Module.AiAPI.BaseURL = "" }, "module.ai-api.base-url"},
{"missing api-key", func(t *model.AiAgentConfigTable) { t.Module.AiAPI.APIKey = "" }, "module.ai-api.api-key"},
{"missing model", func(t *model.AiAgentConfigTable) { t.Module.ChatModel.Model = "" }, "module.chat-model.model"},
{"missing runner agent-name", func(t *model.AiAgentConfigTable) { t.Module.Runner.AgentName = "" }, "module.runner.agent-name"},
{"missing agents", func(t *model.AiAgentConfigTable) { t.Module.Agents = nil }, "module.agents"},
{"empty agent name", func(t *model.AiAgentConfigTable) { t.Module.Agents[0].Name = "" }, "module.agents[0].name"},
{"empty agent instruction", func(t *model.AiAgentConfigTable) { t.Module.Agents[0].Instruction = "" }, "module.agents[0].instruction"},
{"invalid workflow type", func(t *model.AiAgentConfigTable) {
t.Module.AgentWorkflows = []model.AgentWorkflowConfig{
{Type: "bad", Name: "wf"},
}
}, "module.agent-workflows[0].type is invalid"},
{"missing workflow name", func(t *model.AiAgentConfigTable) {
t.Module.AgentWorkflows = []model.AgentWorkflowConfig{
{Type: model.WorkflowTypeSequential, Name: ""},
}
}, "module.agent-workflows[0].name is required"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
table := newTestTable()
tt.modify(&table)
err := validateTable("test", table)
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
})
}
}
func TestValidateTable_ValidTable_NoError(t *testing.T) {
table := newTestTable()
err := validateTable("test", table)
assert.NoError(t, err)
}
// ============================================================
// normalizeDefaults 测试
// ============================================================
func TestNormalizeDefaults_SetsCompletionsPath(t *testing.T) {
table := newTestTable()
table.Module.AiAPI.CompletionsPath = ""
normalizeDefaults(&table)
assert.Equal(t, "v1/chat/completions", table.Module.AiAPI.CompletionsPath)
}
func TestNormalizeDefaults_SetsMaxIterations(t *testing.T) {
table := newTestTable()
table.Module.AgentWorkflows = []model.AgentWorkflowConfig{
{Type: model.WorkflowTypeLoop, Name: "loop", MaxIterations: 0},
}
normalizeDefaults(&table)
assert.Equal(t, 3, table.Module.AgentWorkflows[0].MaxIterations)
}
// ============================================================
// LoadAgentTables 测试
// ============================================================
func TestLoadAgentTables_ValidYAML_ReturnsTable(t *testing.T) {
tables, err := LoadAgentTables(newTestYAML())
assert.NoError(t, err)
assert.Len(t, tables, 1)
table, ok := tables["test-app"]
assert.True(t, ok)
assert.Equal(t, "test-app", table.AppName)
assert.Equal(t, "10001", table.Agent.AgentID)
assert.Equal(t, "gpt-4", table.Module.ChatModel.Model)
}
func TestLoadAgentTables_WithEnvVar_Substitutes(t *testing.T) {
t.Setenv("TEST_LLM_URL", "http://llm.example.com")
yaml := []byte(`
ai:
agent:
config:
tables:
t:
app-name: app
agent:
agent-id: "1"
agent-name: n
agent-desc: d
module:
ai-api:
base-url: "${TEST_LLM_URL}"
api-key: k
chat-model:
model: m
agents:
- name: bot
instruction: hi
runner:
agent-name: bot
`)
tables, err := LoadAgentTables(yaml)
assert.NoError(t, err)
assert.Equal(t, "http://llm.example.com", tables["t"].Module.AiAPI.BaseURL)
}
func TestLoadAgentTables_EmptyTables_ReturnsError(t *testing.T) {
yaml := []byte(`
ai:
agent:
config:
tables: {}
`)
_, err := LoadAgentTables(yaml)
assert.Error(t, err)
assert.Contains(t, err.Error(), "agent config tables are required")
}
func TestLoadAgentTables_InvalidYAML_ReturnsError(t *testing.T) {
_, err := LoadAgentTables([]byte("not: [valid: yaml"))
assert.Error(t, err)
}
func TestLoadAgentTables_MissingRequiredField_ReturnsError(t *testing.T) {
yaml := []byte(`
ai:
agent:
config:
tables:
t:
app-name: ""
agent:
agent-id: "1"
module:
ai-api:
base-url: u
api-key: k
chat-model:
model: m
agents:
- name: bot
instruction: hi
runner:
agent-name: bot
`)
_, err := LoadAgentTables(yaml)
assert.Error(t, err)
assert.Contains(t, err.Error(), "app-name")
}
// ============================================================
// LoadAgentTablesFile 测试
// ============================================================
func TestLoadAgentTablesFile_ValidFile_ReturnsTable(t *testing.T) {
tmp, err := os.CreateTemp("", "agent-*.yaml")
assert.NoError(t, err)
defer os.Remove(tmp.Name())
_, err = tmp.Write(newTestYAML())
assert.NoError(t, err)
tmp.Close()
tables, err := LoadAgentTablesFile(tmp.Name())
assert.NoError(t, err)
assert.Len(t, tables, 1)
}
func TestLoadAgentTablesFile_FileNotFound_ReturnsError(t *testing.T) {
_, err := LoadAgentTablesFile("/nonexistent/path.yaml")
assert.Error(t, err)
}