Compare commits
4 Commits
dog/charli
...
nux/poleca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123d0b2bed | ||
|
|
6be7fdd76c | ||
|
|
f0014bb21a | ||
| c9f844b477 |
@@ -182,6 +182,22 @@ Examples:
|
|||||||
RunE: runDogDispatch,
|
RunE: runDogDispatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dogDoneCmd = &cobra.Command{
|
||||||
|
Use: "done [name]",
|
||||||
|
Short: "Mark a dog as idle (work complete)",
|
||||||
|
Long: `Mark a dog as idle after completing its work.
|
||||||
|
|
||||||
|
Dogs call this command after finishing plugin execution to reset their state
|
||||||
|
to idle, allowing them to receive new work dispatches.
|
||||||
|
|
||||||
|
If no name is provided, attempts to detect the current dog from BD_ACTOR.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt dog done alpha # Explicit dog name
|
||||||
|
gt dog done # Auto-detect from BD_ACTOR (e.g., "deacon/dogs/alpha")`,
|
||||||
|
RunE: runDogDone,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// List flags
|
// List flags
|
||||||
dogListCmd.Flags().BoolVar(&dogListJSON, "json", false, "Output as JSON")
|
dogListCmd.Flags().BoolVar(&dogListJSON, "json", false, "Output as JSON")
|
||||||
@@ -212,6 +228,7 @@ func init() {
|
|||||||
dogCmd.AddCommand(dogCallCmd)
|
dogCmd.AddCommand(dogCallCmd)
|
||||||
dogCmd.AddCommand(dogStatusCmd)
|
dogCmd.AddCommand(dogStatusCmd)
|
||||||
dogCmd.AddCommand(dogDispatchCmd)
|
dogCmd.AddCommand(dogDispatchCmd)
|
||||||
|
dogCmd.AddCommand(dogDoneCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(dogCmd)
|
rootCmd.AddCommand(dogCmd)
|
||||||
}
|
}
|
||||||
@@ -500,6 +517,34 @@ func runDogStatus(cmd *cobra.Command, args []string) error {
|
|||||||
return showPackStatus(mgr)
|
return showPackStatus(mgr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runDogDone(cmd *cobra.Command, args []string) error {
|
||||||
|
mgr, err := getDogManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if len(args) > 0 {
|
||||||
|
name = args[0]
|
||||||
|
} else {
|
||||||
|
// Try to detect from BD_ACTOR (e.g., "deacon/dogs/alpha")
|
||||||
|
actor := os.Getenv("BD_ACTOR")
|
||||||
|
if actor != "" && strings.HasPrefix(actor, "deacon/dogs/") {
|
||||||
|
name = strings.TrimPrefix(actor, "deacon/dogs/")
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("no dog name provided and could not detect from BD_ACTOR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.ClearWork(name); err != nil {
|
||||||
|
return fmt.Errorf("marking dog %s as done: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ %s marked as idle (ready for new work)\n", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func showDogStatus(mgr *dog.Manager, name string) error {
|
func showDogStatus(mgr *dog.Manager, name string) error {
|
||||||
d, err := mgr.Get(name)
|
d, err := mgr.Get(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -791,6 +836,35 @@ func runDogDispatch(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("sending plugin mail to dog: %w", err)
|
return fmt.Errorf("sending plugin mail to dog: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spawn a session for the dog to execute the work.
|
||||||
|
// Without a session, the dog's mail inbox is never checked.
|
||||||
|
// See: https://github.com/steveyegge/gastown/issues/XXX (dog dispatch doesn't execute)
|
||||||
|
t := tmux.NewTmux()
|
||||||
|
townName, err := workspace.GetTownName(townRoot)
|
||||||
|
if err != nil {
|
||||||
|
townName = "gt" // fallback
|
||||||
|
}
|
||||||
|
dogSessionName := fmt.Sprintf("gt-%s-deacon-%s", townName, targetDog.Name)
|
||||||
|
|
||||||
|
// Kill any stale session first
|
||||||
|
if has, _ := t.HasSession(dogSessionName); has {
|
||||||
|
_ = t.KillSessionWithProcesses(dogSessionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build startup command with initial prompt to check mail and execute plugin
|
||||||
|
// Use BuildDogStartupCommand to properly set BD_ACTOR=deacon/dogs/<name> in the startup command
|
||||||
|
initialPrompt := fmt.Sprintf("I am dog %s. Check my mail inbox with 'gt mail inbox' and execute the plugin instructions I received.", targetDog.Name)
|
||||||
|
startCmd := config.BuildDogStartupCommand(targetDog.Name, townRoot, targetDog.Path, initialPrompt)
|
||||||
|
|
||||||
|
// Create session from dog's directory
|
||||||
|
if err := t.NewSessionWithCommand(dogSessionName, targetDog.Path, startCmd); err != nil {
|
||||||
|
if !dogDispatchJSON {
|
||||||
|
fmt.Printf(" Warning: could not spawn dog session: %v\n", err)
|
||||||
|
}
|
||||||
|
// Non-fatal: mail was sent, dog is marked as working, but no session to execute
|
||||||
|
// The deacon or human can manually start the session later
|
||||||
|
}
|
||||||
|
|
||||||
// Success - output result
|
// Success - output result
|
||||||
if dogDispatchJSON {
|
if dogDispatchJSON {
|
||||||
return json.NewEncoder(os.Stdout).Encode(result)
|
return json.NewEncoder(os.Stdout).Encode(result)
|
||||||
|
|||||||
@@ -129,6 +129,13 @@ func detectSenderFromRole(role string) string {
|
|||||||
return fmt.Sprintf("%s/refinery", rig)
|
return fmt.Sprintf("%s/refinery", rig)
|
||||||
}
|
}
|
||||||
return detectSenderFromCwd()
|
return detectSenderFromCwd()
|
||||||
|
case "dog":
|
||||||
|
// Dogs use BD_ACTOR directly (set by BuildDogStartupCommand)
|
||||||
|
actor := os.Getenv("BD_ACTOR")
|
||||||
|
if actor != "" {
|
||||||
|
return actor
|
||||||
|
}
|
||||||
|
return detectSenderFromCwd()
|
||||||
default:
|
default:
|
||||||
// Unknown role, try cwd detection
|
// Unknown role, try cwd detection
|
||||||
return detectSenderFromCwd()
|
return detectSenderFromCwd()
|
||||||
|
|||||||
@@ -18,8 +18,14 @@ import (
|
|||||||
|
|
||||||
// outputPrimeContext outputs the role-specific context using templates or fallback.
|
// outputPrimeContext outputs the role-specific context using templates or fallback.
|
||||||
func outputPrimeContext(ctx RoleContext) error {
|
func outputPrimeContext(ctx RoleContext) error {
|
||||||
// Try to use templates first
|
// Calculate rig path for template overrides
|
||||||
tmpl, err := templates.New()
|
var rigPath string
|
||||||
|
if ctx.Rig != "" && ctx.TownRoot != "" {
|
||||||
|
rigPath = filepath.Join(ctx.TownRoot, ctx.Rig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to use templates with override support
|
||||||
|
tmpl, err := templates.NewWithOverrides(ctx.TownRoot, rigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fall back to hardcoded output if templates fail
|
// Fall back to hardcoded output if templates fail
|
||||||
return outputPrimeContextFallback(ctx)
|
return outputPrimeContextFallback(ctx)
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ func AgentEnv(cfg AgentEnvConfig) map[string]string {
|
|||||||
env["GIT_AUTHOR_NAME"] = "boot"
|
env["GIT_AUTHOR_NAME"] = "boot"
|
||||||
env["GIT_AUTHOR_EMAIL"] = "boot@gastown.local"
|
env["GIT_AUTHOR_EMAIL"] = "boot@gastown.local"
|
||||||
|
|
||||||
|
case "dog":
|
||||||
|
env["BD_ACTOR"] = fmt.Sprintf("deacon/dogs/%s", cfg.AgentName)
|
||||||
|
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("dog-%s", cfg.AgentName)
|
||||||
|
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("dog-%s@gastown.local", cfg.AgentName)
|
||||||
|
|
||||||
case "witness":
|
case "witness":
|
||||||
env["GT_RIG"] = cfg.Rig
|
env["GT_RIG"] = cfg.Rig
|
||||||
env["BD_ACTOR"] = fmt.Sprintf("%s/witness", cfg.Rig)
|
env["BD_ACTOR"] = fmt.Sprintf("%s/witness", cfg.Rig)
|
||||||
@@ -128,7 +133,7 @@ func AgentEnvSimple(role, rig, agentName string) map[string]string {
|
|||||||
|
|
||||||
// ShellQuote returns a shell-safe quoted string.
|
// ShellQuote returns a shell-safe quoted string.
|
||||||
// Values containing special characters are wrapped in single quotes.
|
// Values containing special characters are wrapped in single quotes.
|
||||||
// Single quotes within the value are escaped using the '\'' idiom.
|
// Single quotes within the value are escaped using the '\” idiom.
|
||||||
func ShellQuote(s string) string {
|
func ShellQuote(s string) string {
|
||||||
// Check if quoting is needed (contains shell special chars)
|
// Check if quoting is needed (contains shell special chars)
|
||||||
needsQuoting := false
|
needsQuoting := false
|
||||||
|
|||||||
@@ -125,6 +125,23 @@ func TestAgentEnv_Boot(t *testing.T) {
|
|||||||
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgentEnv_Dog(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
env := AgentEnv(AgentEnvConfig{
|
||||||
|
Role: "dog",
|
||||||
|
AgentName: "alpha",
|
||||||
|
TownRoot: "/town",
|
||||||
|
})
|
||||||
|
|
||||||
|
assertEnv(t, env, "GT_ROLE", "dog")
|
||||||
|
assertEnv(t, env, "BD_ACTOR", "deacon/dogs/alpha")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_NAME", "dog-alpha")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "dog-alpha@gastown.local")
|
||||||
|
assertEnv(t, env, "GT_ROOT", "/town")
|
||||||
|
assertNotSet(t, env, "GT_RIG")
|
||||||
|
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgentEnv_WithRuntimeConfigDir(t *testing.T) {
|
func TestAgentEnv_WithRuntimeConfigDir(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
env := AgentEnv(AgentEnvConfig{
|
env := AgentEnv(AgentEnvConfig{
|
||||||
|
|||||||
@@ -1457,6 +1457,17 @@ func BuildPolecatStartupCommandWithAgentOverride(rigName, polecatName, rigPath,
|
|||||||
return BuildStartupCommandWithAgentOverride(envVars, rigPath, prompt, agentOverride)
|
return BuildStartupCommandWithAgentOverride(envVars, rigPath, prompt, agentOverride)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildDogStartupCommand builds the startup command for a deacon dog.
|
||||||
|
// Sets GT_ROLE, BD_ACTOR, GIT_AUTHOR_NAME, and GT_ROOT.
|
||||||
|
func BuildDogStartupCommand(dogName, townRoot, dogPath, prompt string) string {
|
||||||
|
envVars := AgentEnv(AgentEnvConfig{
|
||||||
|
Role: "dog",
|
||||||
|
AgentName: dogName,
|
||||||
|
TownRoot: townRoot,
|
||||||
|
})
|
||||||
|
return BuildStartupCommand(envVars, dogPath, prompt)
|
||||||
|
}
|
||||||
|
|
||||||
// BuildCrewStartupCommand builds the startup command for a crew member.
|
// BuildCrewStartupCommand builds the startup command for a crew member.
|
||||||
// Sets GT_ROLE, GT_RIG, GT_CREW, BD_ACTOR, GIT_AUTHOR_NAME, and GT_ROOT.
|
// Sets GT_ROLE, GT_RIG, GT_CREW, BD_ACTOR, GIT_AUTHOR_NAME, and GT_ROOT.
|
||||||
func BuildCrewStartupCommand(rigName, crewName, rigPath, prompt string) string {
|
func BuildCrewStartupCommand(rigName, crewName, rigPath, prompt string) string {
|
||||||
|
|||||||
@@ -20,22 +20,25 @@ var commandsFS embed.FS
|
|||||||
type Templates struct {
|
type Templates struct {
|
||||||
roleTemplates *template.Template
|
roleTemplates *template.Template
|
||||||
messageTemplates *template.Template
|
messageTemplates *template.Template
|
||||||
|
// roleOverrides maps role name to override template content.
|
||||||
|
// When set, these are used instead of embedded templates.
|
||||||
|
roleOverrides map[string]*template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoleData contains information for rendering role contexts.
|
// RoleData contains information for rendering role contexts.
|
||||||
type RoleData struct {
|
type RoleData struct {
|
||||||
Role string // mayor, witness, refinery, polecat, crew, deacon
|
Role string // mayor, witness, refinery, polecat, crew, deacon
|
||||||
RigName string // e.g., "greenplace"
|
RigName string // e.g., "greenplace"
|
||||||
TownRoot string // e.g., "/Users/steve/ai"
|
TownRoot string // e.g., "/Users/steve/ai"
|
||||||
TownName string // e.g., "ai" - the town identifier for session names
|
TownName string // e.g., "ai" - the town identifier for session names
|
||||||
WorkDir string // current working directory
|
WorkDir string // current working directory
|
||||||
DefaultBranch string // default branch for merges (e.g., "main", "develop")
|
DefaultBranch string // default branch for merges (e.g., "main", "develop")
|
||||||
Polecat string // polecat name (for polecat role)
|
Polecat string // polecat name (for polecat role)
|
||||||
Polecats []string // list of polecats (for witness role)
|
Polecats []string // list of polecats (for witness role)
|
||||||
BeadsDir string // BEADS_DIR path
|
BeadsDir string // BEADS_DIR path
|
||||||
IssuePrefix string // beads issue prefix
|
IssuePrefix string // beads issue prefix
|
||||||
MayorSession string // e.g., "gt-ai-mayor" - dynamic mayor session name
|
MayorSession string // e.g., "gt-ai-mayor" - dynamic mayor session name
|
||||||
DeaconSession string // e.g., "gt-ai-deacon" - dynamic deacon session name
|
DeaconSession string // e.g., "gt-ai-deacon" - dynamic deacon session name
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpawnData contains information for spawn assignment messages.
|
// SpawnData contains information for spawn assignment messages.
|
||||||
@@ -81,11 +84,24 @@ type HandoffData struct {
|
|||||||
GitDirty bool
|
GitDirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Templates instance.
|
// New creates a new Templates instance with only embedded templates.
|
||||||
func New() (*Templates, error) {
|
func New() (*Templates, error) {
|
||||||
t := &Templates{}
|
return NewWithOverrides("", "")
|
||||||
|
}
|
||||||
|
|
||||||
// Parse role templates
|
// NewWithOverrides creates a Templates instance that checks for local overrides.
|
||||||
|
// Override resolution order (later overrides earlier):
|
||||||
|
// 1. Built-in templates (embedded in binary)
|
||||||
|
// 2. Town-level overrides (<townRoot>/templates/roles/<role>.md.tmpl)
|
||||||
|
// 3. Rig-level overrides (<rigPath>/templates/roles/<role>.md.tmpl)
|
||||||
|
//
|
||||||
|
// If townRoot or rigPath is empty, that level is skipped.
|
||||||
|
func NewWithOverrides(townRoot, rigPath string) (*Templates, error) {
|
||||||
|
t := &Templates{
|
||||||
|
roleOverrides: make(map[string]*template.Template),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse embedded role templates
|
||||||
roleTempl, err := template.ParseFS(templateFS, "roles/*.md.tmpl")
|
roleTempl, err := template.ParseFS(templateFS, "roles/*.md.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing role templates: %w", err)
|
return nil, fmt.Errorf("parsing role templates: %w", err)
|
||||||
@@ -99,14 +115,51 @@ func New() (*Templates, error) {
|
|||||||
}
|
}
|
||||||
t.messageTemplates = msgTempl
|
t.messageTemplates = msgTempl
|
||||||
|
|
||||||
|
// Load role overrides from filesystem
|
||||||
|
// Check both town and rig levels, rig takes precedence
|
||||||
|
roles := []string{"mayor", "witness", "refinery", "polecat", "crew", "deacon"}
|
||||||
|
overridePaths := []string{}
|
||||||
|
if townRoot != "" {
|
||||||
|
overridePaths = append(overridePaths, filepath.Join(townRoot, "templates", "roles"))
|
||||||
|
}
|
||||||
|
if rigPath != "" {
|
||||||
|
overridePaths = append(overridePaths, filepath.Join(rigPath, "templates", "roles"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, role := range roles {
|
||||||
|
templateName := role + ".md.tmpl"
|
||||||
|
// Check each override path in order (later paths override earlier)
|
||||||
|
for _, overrideDir := range overridePaths {
|
||||||
|
overridePath := filepath.Join(overrideDir, templateName)
|
||||||
|
if content, err := os.ReadFile(overridePath); err == nil {
|
||||||
|
tmpl, err := template.New(templateName).Parse(string(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing override template %s: %w", overridePath, err)
|
||||||
|
}
|
||||||
|
t.roleOverrides[role] = tmpl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderRole renders a role context template.
|
// RenderRole renders a role context template.
|
||||||
|
// If an override template exists for the role, it is used instead of the embedded template.
|
||||||
func (t *Templates) RenderRole(role string, data RoleData) (string, error) {
|
func (t *Templates) RenderRole(role string, data RoleData) (string, error) {
|
||||||
templateName := role + ".md.tmpl"
|
templateName := role + ".md.tmpl"
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
// Check for override template first
|
||||||
|
if override, ok := t.roleOverrides[role]; ok {
|
||||||
|
if err := override.Execute(&buf, data); err != nil {
|
||||||
|
return "", fmt.Errorf("rendering override template %s: %w", templateName, err)
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to embedded template
|
||||||
if err := t.roleTemplates.ExecuteTemplate(&buf, templateName, data); err != nil {
|
if err := t.roleTemplates.ExecuteTemplate(&buf, templateName, data); err != nil {
|
||||||
return "", fmt.Errorf("rendering role template %s: %w", templateName, err)
|
return "", fmt.Errorf("rendering role template %s: %w", templateName, err)
|
||||||
}
|
}
|
||||||
@@ -131,6 +184,17 @@ func (t *Templates) RoleNames() []string {
|
|||||||
return []string{"mayor", "witness", "refinery", "polecat", "crew", "deacon"}
|
return []string{"mayor", "witness", "refinery", "polecat", "crew", "deacon"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasRoleOverride returns true if the given role has a local override template.
|
||||||
|
func (t *Templates) HasRoleOverride(role string) bool {
|
||||||
|
_, ok := t.roleOverrides[role]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoleOverrideCount returns the number of role templates that have local overrides.
|
||||||
|
func (t *Templates) RoleOverrideCount() int {
|
||||||
|
return len(t.roleOverrides)
|
||||||
|
}
|
||||||
|
|
||||||
// MessageNames returns the list of available message templates.
|
// MessageNames returns the list of available message templates.
|
||||||
func (t *Templates) MessageNames() []string {
|
func (t *Templates) MessageNames() []string {
|
||||||
return []string{"spawn", "nudge", "escalation", "handoff"}
|
return []string{"spawn", "nudge", "escalation", "handoff"}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user