fix(process): reject duplicate runner spec names

This commit is contained in:
Virgil 2026-04-04 00:21:22 +00:00
parent fa79e4eee7
commit f98bbad5ac
2 changed files with 56 additions and 0 deletions

View file

@ -16,6 +16,9 @@ type Runner struct {
// ErrRunnerNoService is returned when a runner was created without a service.
var ErrRunnerNoService = coreerr.E("", "runner service is nil", nil)
// ErrRunnerInvalidSpecName is returned when a RunSpec name is empty or duplicated.
var ErrRunnerInvalidSpecName = coreerr.E("", "runner spec names must be non-empty and unique", nil)
// NewRunner creates a runner for the given service.
func NewRunner(svc *Service) *Runner {
return &Runner{service: svc}
@ -74,6 +77,9 @@ func (r *Runner) RunAll(ctx context.Context, specs []RunSpec) (*RunAllResult, er
if err := r.ensureService(); err != nil {
return nil, err
}
if err := validateSpecs(specs); err != nil {
return nil, err
}
start := time.Now()
// Build dependency graph
@ -239,6 +245,9 @@ func (r *Runner) RunSequential(ctx context.Context, specs []RunSpec) (*RunAllRes
if err := r.ensureService(); err != nil {
return nil, err
}
if err := validateSpecs(specs); err != nil {
return nil, err
}
start := time.Now()
results := make([]RunResult, 0, len(specs))
@ -282,6 +291,9 @@ func (r *Runner) RunParallel(ctx context.Context, specs []RunSpec) (*RunAllResul
if err := r.ensureService(); err != nil {
return nil, err
}
if err := validateSpecs(specs); err != nil {
return nil, err
}
start := time.Now()
results := make([]RunResult, len(specs))
@ -312,3 +324,17 @@ func (r *Runner) RunParallel(ctx context.Context, specs []RunSpec) (*RunAllResul
return aggResult, nil
}
func validateSpecs(specs []RunSpec) error {
seen := make(map[string]struct{}, len(specs))
for _, spec := range specs {
if spec.Name == "" {
return coreerr.E("Runner.validateSpecs", "runner spec name is required", ErrRunnerInvalidSpecName)
}
if _, ok := seen[spec.Name]; ok {
return coreerr.E("Runner.validateSpecs", "runner spec name is duplicated", ErrRunnerInvalidSpecName)
}
seen[spec.Name] = struct{}{}
}
return nil
}

View file

@ -243,3 +243,33 @@ func TestRunner_NilService(t *testing.T) {
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerNoService)
}
func TestRunner_InvalidSpecNames(t *testing.T) {
runner := newTestRunner(t)
t.Run("rejects empty names", func(t *testing.T) {
_, err := runner.RunSequential(context.Background(), []RunSpec{
{Name: "", Command: "echo", Args: []string{"a"}},
})
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerInvalidSpecName)
})
t.Run("rejects duplicate names", func(t *testing.T) {
_, err := runner.RunAll(context.Background(), []RunSpec{
{Name: "same", Command: "echo", Args: []string{"a"}},
{Name: "same", Command: "echo", Args: []string{"b"}},
})
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerInvalidSpecName)
})
t.Run("rejects duplicate names in parallel mode", func(t *testing.T) {
_, err := runner.RunParallel(context.Background(), []RunSpec{
{Name: "one", Command: "echo", Args: []string{"a"}},
{Name: "one", Command: "echo", Args: []string{"b"}},
})
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerInvalidSpecName)
})
}