Implements weeks 1-4 of the multi-provider architecture: Week 1 - Foundation: - Add domain models (AgentProvider, AgentRequest, AgentEvent, AgentResult) - Define CodeAgent port interface with Execute, Cancel, Capabilities - Create thread-safe provider registry with first-registered default Week 2 - Claude Code Adapter: - Extract kubectl exec logic into CodeAgent implementation - Parse stream-json output format (init, message, tool_use, result) - Support session continuation via --resume flag Week 3 - OpenCode Adapter: - HTTP/SSE client for opencode serve API - Session management (create, send message, abort) - Event streaming with documented buffer rationale Week 4 - Quality & Polish: - Fix race condition in OpenCode Cancel method - Add AgentRequest.Validate() with ErrPromptRequired, ErrInvalidTimeout - Document DefaultAvailabilityTimeout constants - Add HTTP error context for debugging Also includes: - Work queue system with PostgreSQL adapter - Credential store for infrastructure secrets - Project templates with Woodpecker CI integration - Comprehensive test coverage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
144 lines
4.1 KiB
Go
144 lines
4.1 KiB
Go
package templates
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestInterpolateVars(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
vars map[string]string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "single variable",
|
|
content: "Hello {{PROJECT_NAME}}!",
|
|
vars: map[string]string{
|
|
"PROJECT_NAME": "myapp",
|
|
},
|
|
expected: "Hello myapp!",
|
|
},
|
|
{
|
|
name: "multiple variables",
|
|
content: "Project {{PROJECT_NAME}} at {{DOMAIN}} from {{GIT_URL}}",
|
|
vars: map[string]string{
|
|
"PROJECT_NAME": "myapp",
|
|
"DOMAIN": "myapp.threesix.ai",
|
|
"GIT_URL": "https://git.example.com/org/myapp.git",
|
|
},
|
|
expected: "Project myapp at myapp.threesix.ai from https://git.example.com/org/myapp.git",
|
|
},
|
|
{
|
|
name: "no variables",
|
|
content: "Static content with no placeholders",
|
|
vars: map[string]string{},
|
|
expected: "Static content with no placeholders",
|
|
},
|
|
{
|
|
name: "variable not in map",
|
|
content: "Hello {{UNKNOWN_VAR}}!",
|
|
vars: map[string]string{
|
|
"PROJECT_NAME": "myapp",
|
|
},
|
|
expected: "Hello {{UNKNOWN_VAR}}!",
|
|
},
|
|
{
|
|
name: "empty vars map",
|
|
content: "Hello {{PROJECT_NAME}}!",
|
|
vars: nil,
|
|
expected: "Hello {{PROJECT_NAME}}!",
|
|
},
|
|
{
|
|
name: "repeated variable",
|
|
content: "{{NAME}} and {{NAME}} again",
|
|
vars: map[string]string{
|
|
"NAME": "test",
|
|
},
|
|
expected: "test and test again",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := interpolateVars(tt.content, tt.vars)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidTemplateName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"valid simple", "default", true},
|
|
{"valid with dash", "astro-landing", true},
|
|
{"valid with numbers", "go-api-v2", true},
|
|
{"empty", "", false},
|
|
{"starts with number", "123template", false},
|
|
{"starts with dash", "-invalid", false},
|
|
{"contains uppercase", "MyTemplate", false},
|
|
{"contains underscore", "my_template", false},
|
|
{"contains dot", "my.template", false},
|
|
{"contains slash", "my/template", false},
|
|
{"path traversal attempt", "../evil", false},
|
|
{"too long", "a" + string(make([]byte, 64)), false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isValidTemplateName(tt.input)
|
|
assert.Equal(t, tt.expected, result, "isValidTemplateName(%q)", tt.input)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListTemplateFiles(t *testing.T) {
|
|
// Test that default template exists and has expected files
|
|
files, err := listTemplateFiles("default")
|
|
require.NoError(t, err)
|
|
|
|
// Should have at least these core files
|
|
assert.Contains(t, files, ".woodpecker.yml", "default template should have .woodpecker.yml")
|
|
assert.Contains(t, files, "Dockerfile", "default template should have Dockerfile")
|
|
assert.Contains(t, files, "README.md", "default template should have README.md")
|
|
}
|
|
|
|
func TestListTemplateFiles_InvalidTemplate(t *testing.T) {
|
|
_, err := listTemplateFiles("nonexistent-template")
|
|
assert.Error(t, err, "should error for nonexistent template")
|
|
}
|
|
|
|
func TestAvailableTemplates(t *testing.T) {
|
|
// Verify all declared templates actually exist
|
|
for _, tmpl := range availableTemplates {
|
|
t.Run(tmpl.Name, func(t *testing.T) {
|
|
files, err := listTemplateFiles(tmpl.Name)
|
|
require.NoError(t, err, "template %s should exist", tmpl.Name)
|
|
assert.NotEmpty(t, files, "template %s should have files", tmpl.Name)
|
|
|
|
// Every template should have these core files
|
|
assert.Contains(t, files, ".woodpecker.yml", "template %s should have .woodpecker.yml", tmpl.Name)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListTemplateFiles_TmplExtensionStripped(t *testing.T) {
|
|
// go-api template has go.mod.tmpl which should be listed as go.mod
|
|
files, err := listTemplateFiles("go-api")
|
|
require.NoError(t, err)
|
|
|
|
// Should contain go.mod (not go.mod.tmpl)
|
|
assert.Contains(t, files, "go.mod", "go.mod.tmpl should be listed as go.mod")
|
|
|
|
// Should NOT contain the .tmpl extension
|
|
for _, f := range files {
|
|
assert.NotContains(t, f, ".tmpl", "file %s should not have .tmpl extension", f)
|
|
}
|
|
}
|