268 lines
8.2 KiB
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)
|
|
}
|