rdev/internal/adapter/templates/provider_test.go
jordan 39df51defd feat: Add multi-provider code agent interface with Claude Code and OpenCode adapters
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>
2026-01-27 09:25:51 -07:00

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