feat(templates): recover local override support for role templates

Cherry-picked from lost commit 123d0b2b. Adds ability to override
embedded role templates at town or rig level:
- Town: <townRoot>/templates/roles/<role>.md.tmpl
- Rig: <rigPath>/templates/roles/<role>.md.tmpl

New APIs:
- NewWithOverrides(townRoot, rigPath string)
- HasRoleOverride(role string) bool
- RoleOverrideCount() int

Recovered by: gt-vjhf

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dementus
2026-01-26 12:26:58 -08:00
committed by John Ogle
parent bad278d797
commit 149da3d2e2
3 changed files with 249 additions and 17 deletions
+162
View File
@@ -1,6 +1,7 @@
package templates
import (
"os"
"strings"
"testing"
)
@@ -295,3 +296,164 @@ func TestGetAllRoleTemplates_ContentValidity(t *testing.T) {
}
}
}
func TestNewWithOverrides_NoOverrides(t *testing.T) {
// When no override paths exist, should work like New()
tmpl, err := NewWithOverrides("/nonexistent/town", "/nonexistent/rig")
if err != nil {
t.Fatalf("NewWithOverrides() error = %v", err)
}
if tmpl == nil {
t.Fatal("NewWithOverrides() returned nil")
}
// Should have no overrides
if tmpl.RoleOverrideCount() != 0 {
t.Errorf("RoleOverrideCount() = %d, want 0", tmpl.RoleOverrideCount())
}
// Should still render embedded templates
data := RoleData{
Role: "polecat",
RigName: "myrig",
Polecat: "TestCat",
}
output, err := tmpl.RenderRole("polecat", data)
if err != nil {
t.Fatalf("RenderRole() error = %v", err)
}
if !strings.Contains(output, "Polecat Context") {
t.Error("embedded template not rendered correctly")
}
}
func TestNewWithOverrides_WithOverride(t *testing.T) {
// Create a temp directory with an override template
tempDir := t.TempDir()
overrideDir := tempDir + "/templates/roles"
if err := os.MkdirAll(overrideDir, 0755); err != nil {
t.Fatalf("failed to create override dir: %v", err)
}
// Write a simple override template
overrideContent := `# Custom Polecat Context
You are polecat {{ .Polecat }} with custom instructions.
Rig: {{ .RigName }}
`
if err := os.WriteFile(overrideDir+"/polecat.md.tmpl", []byte(overrideContent), 0644); err != nil {
t.Fatalf("failed to write override template: %v", err)
}
// Create Templates with the override
tmpl, err := NewWithOverrides("", tempDir)
if err != nil {
t.Fatalf("NewWithOverrides() error = %v", err)
}
// Should have one override
if tmpl.RoleOverrideCount() != 1 {
t.Errorf("RoleOverrideCount() = %d, want 1", tmpl.RoleOverrideCount())
}
// Should report polecat as having override
if !tmpl.HasRoleOverride("polecat") {
t.Error("HasRoleOverride('polecat') = false, want true")
}
if tmpl.HasRoleOverride("mayor") {
t.Error("HasRoleOverride('mayor') = true, want false")
}
// Render should use override
data := RoleData{
Role: "polecat",
RigName: "myrig",
Polecat: "TestCat",
}
output, err := tmpl.RenderRole("polecat", data)
if err != nil {
t.Fatalf("RenderRole() error = %v", err)
}
// Should contain custom content, not embedded
if !strings.Contains(output, "Custom Polecat Context") {
t.Error("override template not used - missing 'Custom Polecat Context'")
}
if strings.Contains(output, "Idle Polecat Heresy") {
t.Error("embedded template used instead of override")
}
if !strings.Contains(output, "TestCat") {
t.Error("template variable not expanded")
}
}
func TestNewWithOverrides_RigOverridesTown(t *testing.T) {
// Create temp dirs for both town and rig overrides
townDir := t.TempDir()
rigDir := t.TempDir()
townOverrideDir := townDir + "/templates/roles"
rigOverrideDir := rigDir + "/templates/roles"
if err := os.MkdirAll(townOverrideDir, 0755); err != nil {
t.Fatalf("failed to create town override dir: %v", err)
}
if err := os.MkdirAll(rigOverrideDir, 0755); err != nil {
t.Fatalf("failed to create rig override dir: %v", err)
}
// Write town override
townContent := "# Town Polecat\nTown override: {{ .Polecat }}"
if err := os.WriteFile(townOverrideDir+"/polecat.md.tmpl", []byte(townContent), 0644); err != nil {
t.Fatalf("failed to write town override: %v", err)
}
// Write rig override (should win)
rigContent := "# Rig Polecat\nRig override: {{ .Polecat }}"
if err := os.WriteFile(rigOverrideDir+"/polecat.md.tmpl", []byte(rigContent), 0644); err != nil {
t.Fatalf("failed to write rig override: %v", err)
}
// Create Templates with both overrides
tmpl, err := NewWithOverrides(townDir, rigDir)
if err != nil {
t.Fatalf("NewWithOverrides() error = %v", err)
}
// Render should use rig override (higher precedence)
data := RoleData{Polecat: "TestCat"}
output, err := tmpl.RenderRole("polecat", data)
if err != nil {
t.Fatalf("RenderRole() error = %v", err)
}
if !strings.Contains(output, "Rig Polecat") {
t.Error("rig override not used")
}
if strings.Contains(output, "Town Polecat") {
t.Error("town override used instead of rig override")
}
}
func TestNewWithOverrides_InvalidTemplate(t *testing.T) {
// Create a temp directory with an invalid template
tempDir := t.TempDir()
overrideDir := tempDir + "/templates/roles"
if err := os.MkdirAll(overrideDir, 0755); err != nil {
t.Fatalf("failed to create override dir: %v", err)
}
// Write an invalid template (unclosed action)
invalidContent := "# Invalid Template\n{{ .Polecat"
if err := os.WriteFile(overrideDir+"/polecat.md.tmpl", []byte(invalidContent), 0644); err != nil {
t.Fatalf("failed to write invalid template: %v", err)
}
// Should return error for invalid template
_, err := NewWithOverrides("", tempDir)
if err == nil {
t.Error("NewWithOverrides() should fail with invalid template")
}
if !strings.Contains(err.Error(), "parsing override template") {
t.Errorf("error should mention parsing override template: %v", err)
}
}