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) }