feat(roles): add config-based role definition system (Phase 1)

Replace role beads with embedded TOML config files for role definitions.
This is Phase 1 of gt-y1uvb - adds the config infrastructure without
yet switching the daemon to use it.

New files:
- internal/config/roles.go: RoleDefinition types, LoadRoleDefinition()
  with layered override resolution (builtin → town → rig)
- internal/config/roles/*.toml: 7 embedded role definitions
- internal/config/roles_test.go: unit tests

New command:
- gt role def <role>: displays effective role configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/max
2026-01-19 14:59:47 -08:00
committed by Steve Yegge
parent b8eb936219
commit 544cacf36d
10 changed files with 829 additions and 0 deletions

View File

@@ -100,6 +100,23 @@ Examples:
RunE: runRoleEnv,
}
var roleDefCmd = &cobra.Command{
Use: "def <role>",
Short: "Display role definition (session, health, env config)",
Long: `Display the effective role definition after all overrides are applied.
Role configuration is layered:
1. Built-in defaults (embedded in binary)
2. Town-level overrides (~/.gt/roles/<role>.toml)
3. Rig-level overrides (<rig>/roles/<role>.toml)
Examples:
gt role def witness # Show witness role definition
gt role def crew # Show crew role definition`,
Args: cobra.ExactArgs(1),
RunE: runRoleDef,
}
// Flags for role home command
var (
roleRig string
@@ -113,6 +130,7 @@ func init() {
roleCmd.AddCommand(roleDetectCmd)
roleCmd.AddCommand(roleListCmd)
roleCmd.AddCommand(roleEnvCmd)
roleCmd.AddCommand(roleDefCmd)
// Add --rig and --polecat flags to home command for overrides
roleHomeCmd.Flags().StringVar(&roleRig, "rig", "", "Rig name (required for rig-specific roles)")
@@ -526,3 +544,83 @@ func runRoleEnv(cmd *cobra.Command, args []string) error {
return nil
}
func runRoleDef(cmd *cobra.Command, args []string) error {
roleName := args[0]
// Validate role name
validRoles := config.AllRoles()
isValid := false
for _, r := range validRoles {
if r == roleName {
isValid = true
break
}
}
if !isValid {
return fmt.Errorf("unknown role %q - valid roles: %s", roleName, strings.Join(validRoles, ", "))
}
// Determine town root and rig path
townRoot, _ := workspace.FindFromCwd()
rigPath := ""
if townRoot != "" {
// Try to get rig path if we're in a rig directory
if rigInfo, err := GetRole(); err == nil && rigInfo.Rig != "" {
rigPath = filepath.Join(townRoot, rigInfo.Rig)
}
}
// Load role definition with overrides
def, err := config.LoadRoleDefinition(townRoot, rigPath, roleName)
if err != nil {
return fmt.Errorf("loading role definition: %w", err)
}
// Display role info
fmt.Printf("%s %s\n", style.Bold.Render("Role:"), def.Role)
fmt.Printf("%s %s\n", style.Bold.Render("Scope:"), def.Scope)
fmt.Println()
// Session config
fmt.Println(style.Bold.Render("[session]"))
fmt.Printf(" pattern = %q\n", def.Session.Pattern)
fmt.Printf(" work_dir = %q\n", def.Session.WorkDir)
fmt.Printf(" needs_pre_sync = %v\n", def.Session.NeedsPreSync)
if def.Session.StartCommand != "" {
fmt.Printf(" start_command = %q\n", def.Session.StartCommand)
}
fmt.Println()
// Environment variables
if len(def.Env) > 0 {
fmt.Println(style.Bold.Render("[env]"))
envKeys := make([]string, 0, len(def.Env))
for k := range def.Env {
envKeys = append(envKeys, k)
}
sort.Strings(envKeys)
for _, k := range envKeys {
fmt.Printf(" %s = %q\n", k, def.Env[k])
}
fmt.Println()
}
// Health config
fmt.Println(style.Bold.Render("[health]"))
fmt.Printf(" ping_timeout = %q\n", def.Health.PingTimeout.String())
fmt.Printf(" consecutive_failures = %d\n", def.Health.ConsecutiveFailures)
fmt.Printf(" kill_cooldown = %q\n", def.Health.KillCooldown.String())
fmt.Printf(" stuck_threshold = %q\n", def.Health.StuckThreshold.String())
fmt.Println()
// Prompts
if def.Nudge != "" {
fmt.Printf("%s %s\n", style.Bold.Render("Nudge:"), def.Nudge)
}
if def.PromptTemplate != "" {
fmt.Printf("%s %s\n", style.Bold.Render("Template:"), def.PromptTemplate)
}
return nil
}

298
internal/config/roles.go Normal file
View File

@@ -0,0 +1,298 @@
// Package config provides role configuration for Gas Town agents.
package config
import (
"embed"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/BurntSushi/toml"
)
//go:embed roles/*.toml
var defaultRolesFS embed.FS
// RoleDefinition contains all configuration for a role type.
// This replaces the role bead system with config files.
type RoleDefinition struct {
// Role is the role identifier (mayor, deacon, witness, refinery, polecat, crew, dog).
Role string `toml:"role"`
// Scope is "town" or "rig" - determines where the agent runs.
Scope string `toml:"scope"`
// Session contains tmux session configuration.
Session RoleSessionConfig `toml:"session"`
// Env contains environment variables to set in the session.
Env map[string]string `toml:"env,omitempty"`
// Health contains health check configuration.
Health RoleHealthConfig `toml:"health"`
// Nudge is the initial prompt sent when starting the agent.
Nudge string `toml:"nudge,omitempty"`
// PromptTemplate is the name of the role's prompt template file.
PromptTemplate string `toml:"prompt_template,omitempty"`
}
// RoleSessionConfig contains session-related configuration.
type RoleSessionConfig struct {
// Pattern is the tmux session name pattern.
// Supports placeholders: {rig}, {name}, {role}
// Examples: "hq-mayor", "gt-{rig}-witness", "gt-{rig}-{name}"
Pattern string `toml:"pattern"`
// WorkDir is the working directory pattern.
// Supports placeholders: {town}, {rig}, {name}, {role}
// Examples: "{town}", "{town}/{rig}/witness"
WorkDir string `toml:"work_dir"`
// NeedsPreSync indicates if workspace needs git sync before starting.
NeedsPreSync bool `toml:"needs_pre_sync"`
// StartCommand is the command to run after creating the session.
// Default: "exec claude --dangerously-skip-permissions"
StartCommand string `toml:"start_command,omitempty"`
}
// RoleHealthConfig contains health check thresholds.
type RoleHealthConfig struct {
// PingTimeout is how long to wait for a health check response.
PingTimeout Duration `toml:"ping_timeout"`
// ConsecutiveFailures is how many failed health checks before force-kill.
ConsecutiveFailures int `toml:"consecutive_failures"`
// KillCooldown is the minimum time between force-kills.
KillCooldown Duration `toml:"kill_cooldown"`
// StuckThreshold is how long a wisp can be in_progress before considered stuck.
StuckThreshold Duration `toml:"stuck_threshold"`
}
// Duration is a wrapper for time.Duration that supports TOML marshaling.
type Duration struct {
time.Duration
}
// UnmarshalText implements encoding.TextUnmarshaler for Duration.
func (d *Duration) UnmarshalText(text []byte) error {
parsed, err := time.ParseDuration(string(text))
if err != nil {
return fmt.Errorf("invalid duration %q: %w", string(text), err)
}
d.Duration = parsed
return nil
}
// MarshalText implements encoding.TextMarshaler for Duration.
func (d Duration) MarshalText() ([]byte, error) {
return []byte(d.Duration.String()), nil
}
// String returns the duration as a string.
func (d Duration) String() string {
return d.Duration.String()
}
// AllRoles returns the list of all known role names.
func AllRoles() []string {
return []string{"mayor", "deacon", "dog", "witness", "refinery", "polecat", "crew"}
}
// TownRoles returns roles that operate at town scope.
func TownRoles() []string {
return []string{"mayor", "deacon", "dog"}
}
// RigRoles returns roles that operate at rig scope.
func RigRoles() []string {
return []string{"witness", "refinery", "polecat", "crew"}
}
// isValidRoleName checks if the given name is a known role.
func isValidRoleName(name string) bool {
for _, r := range AllRoles() {
if r == name {
return true
}
}
return false
}
// LoadRoleDefinition loads role configuration with override resolution.
// Resolution order (later overrides earlier):
// 1. Built-in defaults (embedded in binary)
// 2. Town-level overrides (<town>/roles/<role>.toml)
// 3. Rig-level overrides (<rig>/roles/<role>.toml)
//
// Each layer merges with (not replaces) the previous. Users only specify
// fields they want to change.
func LoadRoleDefinition(townRoot, rigPath, roleName string) (*RoleDefinition, error) {
// Validate role name
if !isValidRoleName(roleName) {
return nil, fmt.Errorf("unknown role %q - valid roles: %v", roleName, AllRoles())
}
// 1. Load built-in defaults
def, err := loadBuiltinRoleDefinition(roleName)
if err != nil {
return nil, fmt.Errorf("loading built-in role %s: %w", roleName, err)
}
// 2. Apply town-level overrides if present
townOverridePath := filepath.Join(townRoot, "roles", roleName+".toml")
if override, err := loadRoleOverride(townOverridePath); err == nil {
mergeRoleDefinition(def, override)
}
// 3. Apply rig-level overrides if present (only for rig-scoped roles)
if rigPath != "" {
rigOverridePath := filepath.Join(rigPath, "roles", roleName+".toml")
if override, err := loadRoleOverride(rigOverridePath); err == nil {
mergeRoleDefinition(def, override)
}
}
return def, nil
}
// loadBuiltinRoleDefinition loads a role definition from embedded defaults.
func loadBuiltinRoleDefinition(roleName string) (*RoleDefinition, error) {
data, err := defaultRolesFS.ReadFile("roles/" + roleName + ".toml")
if err != nil {
return nil, fmt.Errorf("role %s not found in defaults: %w", roleName, err)
}
var def RoleDefinition
if err := toml.Unmarshal(data, &def); err != nil {
return nil, fmt.Errorf("parsing role %s: %w", roleName, err)
}
return &def, nil
}
// loadRoleOverride loads a role override from a file path.
// Returns nil, nil if file doesn't exist.
func loadRoleOverride(path string) (*RoleDefinition, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, err // Signal no override exists
}
return nil, fmt.Errorf("reading %s: %w", path, err)
}
var def RoleDefinition
if err := toml.Unmarshal(data, &def); err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
return &def, nil
}
// mergeRoleDefinition merges override into base.
// Only non-zero values in override are applied.
func mergeRoleDefinition(base, override *RoleDefinition) {
if override == nil {
return
}
// Role and Scope are immutable
// (can't change a witness to a mayor via override)
// Session config
if override.Session.Pattern != "" {
base.Session.Pattern = override.Session.Pattern
}
if override.Session.WorkDir != "" {
base.Session.WorkDir = override.Session.WorkDir
}
// NeedsPreSync can only be enabled via override, not disabled.
// This is intentional: if a role's builtin requires pre-sync (e.g., refinery),
// disabling it would break the role's assumptions about workspace state.
if override.Session.NeedsPreSync {
base.Session.NeedsPreSync = true
}
if override.Session.StartCommand != "" {
base.Session.StartCommand = override.Session.StartCommand
}
// Env vars (merge, don't replace)
if override.Env != nil {
if base.Env == nil {
base.Env = make(map[string]string)
}
for k, v := range override.Env {
base.Env[k] = v
}
}
// Health config
if override.Health.PingTimeout.Duration != 0 {
base.Health.PingTimeout = override.Health.PingTimeout
}
if override.Health.ConsecutiveFailures != 0 {
base.Health.ConsecutiveFailures = override.Health.ConsecutiveFailures
}
if override.Health.KillCooldown.Duration != 0 {
base.Health.KillCooldown = override.Health.KillCooldown
}
if override.Health.StuckThreshold.Duration != 0 {
base.Health.StuckThreshold = override.Health.StuckThreshold
}
// Prompts
if override.Nudge != "" {
base.Nudge = override.Nudge
}
if override.PromptTemplate != "" {
base.PromptTemplate = override.PromptTemplate
}
}
// ExpandPattern expands placeholders in a pattern string.
// Supported placeholders: {town}, {rig}, {name}, {role}
func ExpandPattern(pattern, townRoot, rig, name, role string) string {
result := pattern
result = strings.ReplaceAll(result, "{town}", townRoot)
result = strings.ReplaceAll(result, "{rig}", rig)
result = strings.ReplaceAll(result, "{name}", name)
result = strings.ReplaceAll(result, "{role}", role)
return result
}
// ToLegacyRoleConfig converts a RoleDefinition to the legacy RoleConfig format
// for backward compatibility with existing daemon code.
func (rd *RoleDefinition) ToLegacyRoleConfig() *LegacyRoleConfig {
return &LegacyRoleConfig{
SessionPattern: rd.Session.Pattern,
WorkDirPattern: rd.Session.WorkDir,
NeedsPreSync: rd.Session.NeedsPreSync,
StartCommand: rd.Session.StartCommand,
EnvVars: rd.Env,
PingTimeout: rd.Health.PingTimeout.String(),
ConsecutiveFailures: rd.Health.ConsecutiveFailures,
KillCooldown: rd.Health.KillCooldown.String(),
StuckThreshold: rd.Health.StuckThreshold.String(),
}
}
// LegacyRoleConfig matches the old beads.RoleConfig struct for compatibility.
// This allows gradual migration without breaking existing code.
type LegacyRoleConfig struct {
SessionPattern string
WorkDirPattern string
NeedsPreSync bool
StartCommand string
EnvVars map[string]string
PingTimeout string
ConsecutiveFailures int
KillCooldown string
StuckThreshold string
}

View File

@@ -0,0 +1,23 @@
# Crew role definition
# Persistent user-managed workspaces. Multiple per rig.
role = "crew"
scope = "rig"
nudge = "Check your hook and mail, then act accordingly."
prompt_template = "crew.md.tmpl"
[session]
pattern = "gt-{rig}-crew-{name}"
work_dir = "{town}/{rig}/crew/{name}"
needs_pre_sync = true
start_command = "exec claude --dangerously-skip-permissions"
[env]
GT_ROLE = "crew"
GT_SCOPE = "rig"
[health]
ping_timeout = "30s"
consecutive_failures = 3
kill_cooldown = "5m"
stuck_threshold = "4h"

View File

@@ -0,0 +1,23 @@
# Deacon role definition
# Daemon beacon for heartbeats and monitoring. One per town.
role = "deacon"
scope = "town"
nudge = "Run 'gt prime' to check patrol status and begin heartbeat cycle."
prompt_template = "deacon.md.tmpl"
[session]
pattern = "hq-deacon"
work_dir = "{town}"
needs_pre_sync = false
start_command = "exec claude --dangerously-skip-permissions"
[env]
GT_ROLE = "deacon"
GT_SCOPE = "town"
[health]
ping_timeout = "30s"
consecutive_failures = 3
kill_cooldown = "5m"
stuck_threshold = "1h"

View File

@@ -0,0 +1,23 @@
# Dog role definition
# Town-level workers for cross-rig tasks. Dispatched by Deacon.
role = "dog"
scope = "town"
nudge = "Check your hook for work assignments."
prompt_template = "dog.md.tmpl"
[session]
pattern = "gt-dog-{name}"
work_dir = "{town}/deacon/dogs/{name}"
needs_pre_sync = false
start_command = "exec claude --dangerously-skip-permissions"
[env]
GT_ROLE = "dog"
GT_SCOPE = "town"
[health]
ping_timeout = "30s"
consecutive_failures = 3
kill_cooldown = "5m"
stuck_threshold = "2h"

View File

@@ -0,0 +1,23 @@
# Mayor role definition
# Global coordinator for cross-rig work. One per town.
role = "mayor"
scope = "town"
nudge = "Check mail and hook status, then act accordingly."
prompt_template = "mayor.md.tmpl"
[session]
pattern = "hq-mayor"
work_dir = "{town}"
needs_pre_sync = false
start_command = "exec claude --dangerously-skip-permissions"
[env]
GT_ROLE = "mayor"
GT_SCOPE = "town"
[health]
ping_timeout = "30s"
consecutive_failures = 3
kill_cooldown = "5m"
stuck_threshold = "1h"

View File

@@ -0,0 +1,23 @@
# Polecat role definition
# Ephemeral workers for batch work dispatch. Multiple per rig.
role = "polecat"
scope = "rig"
nudge = "Check your hook for work assignments."
prompt_template = "polecat.md.tmpl"
[session]
pattern = "gt-{rig}-{name}"
work_dir = "{town}/{rig}/polecats/{name}"
needs_pre_sync = true
start_command = "exec claude --dangerously-skip-permissions"
[env]
GT_ROLE = "polecat"
GT_SCOPE = "rig"
[health]
ping_timeout = "30s"
consecutive_failures = 3
kill_cooldown = "5m"
stuck_threshold = "2h"

View File

@@ -0,0 +1,23 @@
# Refinery role definition
# Merge queue processor with verification gates. One per rig.
role = "refinery"
scope = "rig"
nudge = "Run 'gt prime' to check merge queue and begin processing."
prompt_template = "refinery.md.tmpl"
[session]
pattern = "gt-{rig}-refinery"
work_dir = "{town}/{rig}/refinery/rig"
needs_pre_sync = true
start_command = "exec claude --dangerously-skip-permissions"
[env]
GT_ROLE = "refinery"
GT_SCOPE = "rig"
[health]
ping_timeout = "30s"
consecutive_failures = 3
kill_cooldown = "5m"
stuck_threshold = "2h"

View File

@@ -0,0 +1,23 @@
# Witness role definition
# Per-rig worker monitor with progressive nudging. One per rig.
role = "witness"
scope = "rig"
nudge = "Run 'gt prime' to check worker status and begin patrol cycle."
prompt_template = "witness.md.tmpl"
[session]
pattern = "gt-{rig}-witness"
work_dir = "{town}/{rig}/witness"
needs_pre_sync = false
start_command = "exec claude --dangerously-skip-permissions"
[env]
GT_ROLE = "witness"
GT_SCOPE = "rig"
[health]
ping_timeout = "30s"
consecutive_failures = 3
kill_cooldown = "5m"
stuck_threshold = "1h"

View File

@@ -0,0 +1,272 @@
package config
import (
"strings"
"testing"
"time"
)
func TestLoadBuiltinRoleDefinition(t *testing.T) {
tests := []struct {
name string
role string
wantScope string
wantPattern string
wantPreSync bool
}{
{
name: "mayor",
role: "mayor",
wantScope: "town",
wantPattern: "hq-mayor",
wantPreSync: false,
},
{
name: "deacon",
role: "deacon",
wantScope: "town",
wantPattern: "hq-deacon",
wantPreSync: false,
},
{
name: "witness",
role: "witness",
wantScope: "rig",
wantPattern: "gt-{rig}-witness",
wantPreSync: false,
},
{
name: "refinery",
role: "refinery",
wantScope: "rig",
wantPattern: "gt-{rig}-refinery",
wantPreSync: true,
},
{
name: "polecat",
role: "polecat",
wantScope: "rig",
wantPattern: "gt-{rig}-{name}",
wantPreSync: true,
},
{
name: "crew",
role: "crew",
wantScope: "rig",
wantPattern: "gt-{rig}-crew-{name}",
wantPreSync: true,
},
{
name: "dog",
role: "dog",
wantScope: "town",
wantPattern: "gt-dog-{name}",
wantPreSync: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
def, err := loadBuiltinRoleDefinition(tt.role)
if err != nil {
t.Fatalf("loadBuiltinRoleDefinition(%s) error: %v", tt.role, err)
}
if def.Role != tt.role {
t.Errorf("Role = %q, want %q", def.Role, tt.role)
}
if def.Scope != tt.wantScope {
t.Errorf("Scope = %q, want %q", def.Scope, tt.wantScope)
}
if def.Session.Pattern != tt.wantPattern {
t.Errorf("Session.Pattern = %q, want %q", def.Session.Pattern, tt.wantPattern)
}
if def.Session.NeedsPreSync != tt.wantPreSync {
t.Errorf("Session.NeedsPreSync = %v, want %v", def.Session.NeedsPreSync, tt.wantPreSync)
}
// Verify health config has reasonable defaults
if def.Health.PingTimeout.Duration == 0 {
t.Error("Health.PingTimeout should not be zero")
}
if def.Health.ConsecutiveFailures == 0 {
t.Error("Health.ConsecutiveFailures should not be zero")
}
})
}
}
func TestLoadBuiltinRoleDefinition_UnknownRole(t *testing.T) {
_, err := loadBuiltinRoleDefinition("nonexistent")
if err == nil {
t.Error("expected error for unknown role, got nil")
}
}
func TestLoadRoleDefinition_UnknownRole(t *testing.T) {
_, err := LoadRoleDefinition("/tmp/town", "", "nonexistent")
if err == nil {
t.Error("expected error for unknown role, got nil")
}
// Should have a clear error message, not a cryptic embed error
if !strings.Contains(err.Error(), "unknown role") {
t.Errorf("error should mention 'unknown role', got: %v", err)
}
}
func TestAllRoles(t *testing.T) {
roles := AllRoles()
if len(roles) != 7 {
t.Errorf("AllRoles() returned %d roles, want 7", len(roles))
}
expected := map[string]bool{
"mayor": true,
"deacon": true,
"dog": true,
"witness": true,
"refinery": true,
"polecat": true,
"crew": true,
}
for _, r := range roles {
if !expected[r] {
t.Errorf("unexpected role %q in AllRoles()", r)
}
}
}
func TestTownRoles(t *testing.T) {
roles := TownRoles()
if len(roles) != 3 {
t.Errorf("TownRoles() returned %d roles, want 3", len(roles))
}
for _, r := range roles {
def, err := loadBuiltinRoleDefinition(r)
if err != nil {
t.Fatalf("loadBuiltinRoleDefinition(%s) error: %v", r, err)
}
if def.Scope != "town" {
t.Errorf("role %s has scope %q, expected 'town'", r, def.Scope)
}
}
}
func TestRigRoles(t *testing.T) {
roles := RigRoles()
if len(roles) != 4 {
t.Errorf("RigRoles() returned %d roles, want 4", len(roles))
}
for _, r := range roles {
def, err := loadBuiltinRoleDefinition(r)
if err != nil {
t.Fatalf("loadBuiltinRoleDefinition(%s) error: %v", r, err)
}
if def.Scope != "rig" {
t.Errorf("role %s has scope %q, expected 'rig'", r, def.Scope)
}
}
}
func TestExpandPattern(t *testing.T) {
tests := []struct {
pattern string
town string
rig string
name string
role string
expected string
}{
{
pattern: "{town}",
town: "/home/user/gt",
expected: "/home/user/gt",
},
{
pattern: "gt-{rig}-witness",
rig: "gastown",
expected: "gt-gastown-witness",
},
{
pattern: "{town}/{rig}/crew/{name}",
town: "/home/user/gt",
rig: "gastown",
name: "max",
expected: "/home/user/gt/gastown/crew/max",
},
}
for _, tt := range tests {
t.Run(tt.pattern, func(t *testing.T) {
got := ExpandPattern(tt.pattern, tt.town, tt.rig, tt.name, tt.role)
if got != tt.expected {
t.Errorf("ExpandPattern() = %q, want %q", got, tt.expected)
}
})
}
}
func TestDuration_UnmarshalText(t *testing.T) {
tests := []struct {
input string
expected time.Duration
}{
{"30s", 30 * time.Second},
{"5m", 5 * time.Minute},
{"1h", time.Hour},
{"1h30m", time.Hour + 30*time.Minute},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
var d Duration
if err := d.UnmarshalText([]byte(tt.input)); err != nil {
t.Fatalf("UnmarshalText() error: %v", err)
}
if d.Duration != tt.expected {
t.Errorf("Duration = %v, want %v", d.Duration, tt.expected)
}
})
}
}
func TestToLegacyRoleConfig(t *testing.T) {
def := &RoleDefinition{
Role: "witness",
Scope: "rig",
Session: RoleSessionConfig{
Pattern: "gt-{rig}-witness",
WorkDir: "{town}/{rig}/witness",
NeedsPreSync: false,
StartCommand: "exec claude",
},
Env: map[string]string{"GT_ROLE": "witness"},
Health: RoleHealthConfig{
PingTimeout: Duration{30 * time.Second},
ConsecutiveFailures: 3,
KillCooldown: Duration{5 * time.Minute},
StuckThreshold: Duration{time.Hour},
},
}
legacy := def.ToLegacyRoleConfig()
if legacy.SessionPattern != "gt-{rig}-witness" {
t.Errorf("SessionPattern = %q, want %q", legacy.SessionPattern, "gt-{rig}-witness")
}
if legacy.WorkDirPattern != "{town}/{rig}/witness" {
t.Errorf("WorkDirPattern = %q, want %q", legacy.WorkDirPattern, "{town}/{rig}/witness")
}
if legacy.NeedsPreSync != false {
t.Errorf("NeedsPreSync = %v, want false", legacy.NeedsPreSync)
}
if legacy.PingTimeout != "30s" {
t.Errorf("PingTimeout = %q, want %q", legacy.PingTimeout, "30s")
}
if legacy.ConsecutiveFailures != 3 {
t.Errorf("ConsecutiveFailures = %d, want 3", legacy.ConsecutiveFailures)
}
}