feat(config): add merge_queue section to rig config schema

Add MergeQueueConfig struct and RigConfig type with:
- All merge queue settings (enabled, target_branch, on_conflict, etc.)
- Default values via DefaultMergeQueueConfig()
- Validation for on_conflict strategy and poll_interval duration
- Load/Save/Validate functions following existing config patterns
- Comprehensive tests for round-trip, custom config, and validation

Implements gt-h5n.8.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-19 00:29:40 -08:00
parent e799fe5491
commit 3f924234ad
3 changed files with 360 additions and 0 deletions

View File

@@ -130,3 +130,196 @@ func TestValidationErrors(t *testing.T) {
t.Error("expected error for missing role")
}
}
func TestRigConfigRoundTrip(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
original := NewRigConfig()
if err := SaveRigConfig(path, original); err != nil {
t.Fatalf("SaveRigConfig: %v", err)
}
loaded, err := LoadRigConfig(path)
if err != nil {
t.Fatalf("LoadRigConfig: %v", err)
}
if loaded.Type != "rig" {
t.Errorf("Type = %q, want 'rig'", loaded.Type)
}
if loaded.Version != CurrentRigConfigVersion {
t.Errorf("Version = %d, want %d", loaded.Version, CurrentRigConfigVersion)
}
if loaded.MergeQueue == nil {
t.Fatal("MergeQueue is nil")
}
if !loaded.MergeQueue.Enabled {
t.Error("MergeQueue.Enabled = false, want true")
}
if loaded.MergeQueue.TargetBranch != "main" {
t.Errorf("MergeQueue.TargetBranch = %q, want 'main'", loaded.MergeQueue.TargetBranch)
}
if loaded.MergeQueue.OnConflict != OnConflictAssignBack {
t.Errorf("MergeQueue.OnConflict = %q, want %q", loaded.MergeQueue.OnConflict, OnConflictAssignBack)
}
}
func TestRigConfigWithCustomMergeQueue(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
original := &RigConfig{
Type: "rig",
Version: 1,
MergeQueue: &MergeQueueConfig{
Enabled: true,
TargetBranch: "develop",
IntegrationBranches: false,
OnConflict: OnConflictAutoRebase,
RunTests: true,
TestCommand: "make test",
DeleteMergedBranches: false,
RetryFlakyTests: 3,
PollInterval: "1m",
MaxConcurrent: 2,
},
}
if err := SaveRigConfig(path, original); err != nil {
t.Fatalf("SaveRigConfig: %v", err)
}
loaded, err := LoadRigConfig(path)
if err != nil {
t.Fatalf("LoadRigConfig: %v", err)
}
mq := loaded.MergeQueue
if mq.TargetBranch != "develop" {
t.Errorf("TargetBranch = %q, want 'develop'", mq.TargetBranch)
}
if mq.OnConflict != OnConflictAutoRebase {
t.Errorf("OnConflict = %q, want %q", mq.OnConflict, OnConflictAutoRebase)
}
if mq.TestCommand != "make test" {
t.Errorf("TestCommand = %q, want 'make test'", mq.TestCommand)
}
if mq.RetryFlakyTests != 3 {
t.Errorf("RetryFlakyTests = %d, want 3", mq.RetryFlakyTests)
}
if mq.PollInterval != "1m" {
t.Errorf("PollInterval = %q, want '1m'", mq.PollInterval)
}
if mq.MaxConcurrent != 2 {
t.Errorf("MaxConcurrent = %d, want 2", mq.MaxConcurrent)
}
}
func TestRigConfigValidation(t *testing.T) {
tests := []struct {
name string
config *RigConfig
wantErr bool
}{
{
name: "valid config",
config: &RigConfig{
Type: "rig",
Version: 1,
MergeQueue: DefaultMergeQueueConfig(),
},
wantErr: false,
},
{
name: "valid config without merge queue",
config: &RigConfig{
Type: "rig",
Version: 1,
},
wantErr: false,
},
{
name: "wrong type",
config: &RigConfig{
Type: "wrong",
Version: 1,
},
wantErr: true,
},
{
name: "invalid on_conflict",
config: &RigConfig{
Type: "rig",
Version: 1,
MergeQueue: &MergeQueueConfig{
OnConflict: "invalid",
},
},
wantErr: true,
},
{
name: "invalid poll_interval",
config: &RigConfig{
Type: "rig",
Version: 1,
MergeQueue: &MergeQueueConfig{
PollInterval: "not-a-duration",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateRigConfig(tt.config)
if (err != nil) != tt.wantErr {
t.Errorf("validateRigConfig() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestDefaultMergeQueueConfig(t *testing.T) {
cfg := DefaultMergeQueueConfig()
if !cfg.Enabled {
t.Error("Enabled should be true by default")
}
if cfg.TargetBranch != "main" {
t.Errorf("TargetBranch = %q, want 'main'", cfg.TargetBranch)
}
if !cfg.IntegrationBranches {
t.Error("IntegrationBranches should be true by default")
}
if cfg.OnConflict != OnConflictAssignBack {
t.Errorf("OnConflict = %q, want %q", cfg.OnConflict, OnConflictAssignBack)
}
if !cfg.RunTests {
t.Error("RunTests should be true by default")
}
if cfg.TestCommand != "go test ./..." {
t.Errorf("TestCommand = %q, want 'go test ./...'", cfg.TestCommand)
}
if !cfg.DeleteMergedBranches {
t.Error("DeleteMergedBranches should be true by default")
}
if cfg.RetryFlakyTests != 1 {
t.Errorf("RetryFlakyTests = %d, want 1", cfg.RetryFlakyTests)
}
if cfg.PollInterval != "30s" {
t.Errorf("PollInterval = %q, want '30s'", cfg.PollInterval)
}
if cfg.MaxConcurrent != 1 {
t.Errorf("MaxConcurrent = %d, want 1", cfg.MaxConcurrent)
}
}
func TestLoadRigConfigNotFound(t *testing.T) {
_, err := LoadRigConfig("/nonexistent/path.json")
if err == nil {
t.Fatal("expected error for nonexistent file")
}
}