โ† run

go-06-job-scheduler

1.000
17/17 testsยท architecture
Challenge ยท difficulty 5/5
# Dependency-aware job scheduler

Implement a file **`solution.go`** in `package challenge` exporting:

```go
type Job struct {
    ID       string
    Deps     []string
    Priority int
}

func Schedule(jobs []Job) ([]string, error)
```

`Schedule` computes a valid execution order for `jobs` and returns the slice of
job IDs in that order. The order is a **topological order**: a job may appear in
the output only after every ID in its `Deps` has already appeared earlier.

## Selection rule (deterministic)

Many topological orders may exist; this challenge fixes a single one. Build the
output by repeating the following step until every job is scheduled:

1. Consider the set of **ready** jobs: not-yet-scheduled jobs whose dependencies
   are *all* already scheduled.
2. Among the ready jobs, pick the one with the **highest `Priority`**.
3. Break ties (equal `Priority`) by the **lexicographically smallest `ID`**
   (Go string `<`).
4. Append the chosen job's ID to the output, mark it scheduled, and repeat.

This makes the result unique and fully determined by the input.

## Rules

- The output is a slice of job IDs (`[]string`). Every job appears exactly once.
- An **empty input** (`nil` or `len 0`) returns an empty (`len 0`) slice and a
  `nil` error.
- Return a non-nil error and a `nil`/empty slice when any of these hold:
  - **Duplicate ID** โ€” two jobs share the same `ID`.
  - **Unknown dependency** โ€” some job's `Deps` references an ID that is not the
    `ID` of any job in `jobs`.
  - **Cycle** โ€” the dependencies form a cycle, so some jobs can never become
    ready. A self-dependency (`a` depends on `a`) is a cycle.
- Duplicate entries inside a single job's `Deps` are allowed and harmless: the
  dependency just needs to be satisfied once.

## Examples

Linear chain โ€” `c` depends on `b`, `b` depends on `a`:

```go
Schedule([]Job{
    {ID: "c", Deps: []string{"b"}},
    {ID: "b", Deps: []string{"a"}},
    {ID: "a"},
})
// โ†’ ["a", "b", "c"], nil
```

Priority tie-break โ€” `a` and `b` are both ready immediately; `b` has the higher
priority, so it runs first:

```go
Schedule([]Job{
    {ID: "a", Priority: 1},
    {ID: "b", Priority: 5},
})
// โ†’ ["b", "a"], nil
```

ID tie-break โ€” equal priorities, so the smaller ID wins:

```go
Schedule([]Job{
    {ID: "y", Priority: 0},
    {ID: "x", Priority: 0},
})
// โ†’ ["x", "y"], nil
```

Diamond โ€” `d` depends on both `b` and `c`, which both depend on `a`. With equal
priorities the smaller ID breaks the `b`/`c` tie:

```go
Schedule([]Job{
    {ID: "a"},
    {ID: "b", Deps: []string{"a"}},
    {ID: "c", Deps: []string{"a"}},
    {ID: "d", Deps: []string{"b", "c"}},
})
// โ†’ ["a", "b", "c", "d"], nil
```

Cycle โ€” returns an error:

```go
Schedule([]Job{
    {ID: "a", Deps: []string{"b"}},
    {ID: "b", Deps: []string{"a"}},
})
// โ†’ nil, <error>
```

Use only the Go standard library.
tests/solution_test.go
package challenge

import (
	"reflect"
	"testing"
)

func TestScheduleEmpty(t *testing.T) {
	got, err := Schedule(nil)
	if err != nil {
		t.Fatalf("Schedule(nil) error = %v, want nil", err)
	}
	if len(got) != 0 {
		t.Fatalf("Schedule(nil) = %v, want empty slice", got)
	}

	got, err = Schedule([]Job{})
	if err != nil {
		t.Fatalf("Schedule([]) error = %v, want nil", err)
	}
	if len(got) != 0 {
		t.Fatalf("Schedule([]) = %v, want empty slice", got)
	}
}

func TestScheduleSingle(t *testing.T) {
	got, err := Schedule([]Job{{ID: "only"}})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if want := []string{"only"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestScheduleLinearChain(t *testing.T) {
	jobs := []Job{
		{ID: "c", Deps: []string{"b"}},
		{ID: "b", Deps: []string{"a"}},
		{ID: "a"},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if want := []string{"a", "b", "c"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestScheduleDiamond(t *testing.T) {
	jobs := []Job{
		{ID: "d", Deps: []string{"b", "c"}},
		{ID: "b", Deps: []string{"a"}},
		{ID: "c", Deps: []string{"a"}},
		{ID: "a"},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// Equal priorities: a first, then b<c by ID, then d.
	if want := []string{"a", "b", "c", "d"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestSchedulePriorityTieBreak(t *testing.T) {
	jobs := []Job{
		{ID: "a", Priority: 1},
		{ID: "b", Priority: 5},
		{ID: "c", Priority: 3},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// All ready; ordered by descending priority: b(5), c(3), a(1).
	if want := []string{"b", "c", "a"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestScheduleSmallestIDTieBreak(t *testing.T) {
	jobs := []Job{
		{ID: "y", Priority: 0},
		{ID: "x", Priority: 0},
		{ID: "z", Priority: 0},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// Equal priorities: lexicographic by ID.
	if want := []string{"x", "y", "z"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestSchedulePriorityBeatsID(t *testing.T) {
	// "z" has higher priority than "a", so it must come first even though
	// "a" is the smaller ID.
	jobs := []Job{
		{ID: "a", Priority: 1},
		{ID: "z", Priority: 9},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if want := []string{"z", "a"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestScheduleIndependentJobs(t *testing.T) {
	jobs := []Job{
		{ID: "b"},
		{ID: "a"},
		{ID: "d"},
		{ID: "c"},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// No deps, all priority 0: pure lexicographic order.
	if want := []string{"a", "b", "c", "d"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestSchedulePriorityRespectsDependencies(t *testing.T) {
	// "low" has lower priority but is the only dependency-free job, so it
	// must run before the high-priority job that depends on it.
	jobs := []Job{
		{ID: "high", Deps: []string{"low"}, Priority: 100},
		{ID: "low", Priority: 1},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if want := []string{"low", "high"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestScheduleComplexDeterministic(t *testing.T) {
	jobs := []Job{
		{ID: "build", Deps: []string{"compile"}, Priority: 5},
		{ID: "compile", Deps: []string{"fetch"}, Priority: 5},
		{ID: "fetch", Priority: 5},
		{ID: "lint", Deps: []string{"fetch"}, Priority: 10},
		{ID: "test", Deps: []string{"build", "lint"}, Priority: 1},
		{ID: "docs", Deps: []string{"fetch"}, Priority: 10},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// fetch first (only ready). Then ready={compile(5), lint(10), docs(10)}.
	// docs and lint tie at 10 -> docs < lint by ID. Then lint. Then compile.
	// After compile -> build ready. build(5). Then test.
	want := []string{"fetch", "docs", "lint", "compile", "build", "test"}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestScheduleDuplicateDepsIgnored(t *testing.T) {
	jobs := []Job{
		{ID: "b", Deps: []string{"a", "a", "a"}},
		{ID: "a"},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if want := []string{"a", "b"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("Schedule = %v, want %v", got, want)
	}
}

func TestScheduleCycleError(t *testing.T) {
	jobs := []Job{
		{ID: "a", Deps: []string{"b"}},
		{ID: "b", Deps: []string{"a"}},
	}
	got, err := Schedule(jobs)
	if err == nil {
		t.Fatalf("expected error for cycle, got order %v", got)
	}
	if len(got) != 0 {
		t.Fatalf("on cycle error, want empty slice, got %v", got)
	}
}

func TestScheduleLongerCycleError(t *testing.T) {
	jobs := []Job{
		{ID: "x"}, // schedulable
		{ID: "a", Deps: []string{"c"}},
		{ID: "b", Deps: []string{"a"}},
		{ID: "c", Deps: []string{"b"}},
	}
	got, err := Schedule(jobs)
	if err == nil {
		t.Fatalf("expected error for 3-node cycle, got order %v", got)
	}
	if len(got) != 0 {
		t.Fatalf("on cycle error, want empty slice, got %v", got)
	}
}

func TestScheduleSelfDependencyError(t *testing.T) {
	jobs := []Job{
		{ID: "a", Deps: []string{"a"}},
	}
	got, err := Schedule(jobs)
	if err == nil {
		t.Fatalf("expected error for self-dependency, got order %v", got)
	}
	if len(got) != 0 {
		t.Fatalf("on self-dependency error, want empty slice, got %v", got)
	}
}

func TestScheduleUnknownDepError(t *testing.T) {
	jobs := []Job{
		{ID: "a", Deps: []string{"ghost"}},
	}
	got, err := Schedule(jobs)
	if err == nil {
		t.Fatalf("expected error for unknown dep, got order %v", got)
	}
	if len(got) != 0 {
		t.Fatalf("on unknown-dep error, want empty slice, got %v", got)
	}
}

func TestScheduleDuplicateIDError(t *testing.T) {
	jobs := []Job{
		{ID: "a"},
		{ID: "a"},
	}
	got, err := Schedule(jobs)
	if err == nil {
		t.Fatalf("expected error for duplicate ID, got order %v", got)
	}
	if len(got) != 0 {
		t.Fatalf("on duplicate-ID error, want empty slice, got %v", got)
	}
}

func TestScheduleOutputIsValidTopoOrder(t *testing.T) {
	jobs := []Job{
		{ID: "a"},
		{ID: "b", Deps: []string{"a"}},
		{ID: "c", Deps: []string{"a"}},
		{ID: "d", Deps: []string{"b", "c"}},
		{ID: "e", Deps: []string{"d"}},
	}
	got, err := Schedule(jobs)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(got) != len(jobs) {
		t.Fatalf("got %d ids, want %d", len(got), len(jobs))
	}
	pos := make(map[string]int, len(got))
	for i, id := range got {
		if _, dup := pos[id]; dup {
			t.Fatalf("id %q appears more than once in %v", id, got)
		}
		pos[id] = i
	}
	for _, j := range jobs {
		for _, dep := range j.Deps {
			if pos[dep] >= pos[j.ID] {
				t.Fatalf("dependency %q (pos %d) not before %q (pos %d) in %v",
					dep, pos[dep], j.ID, pos[j.ID], got)
			}
		}
	}
}
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```go
package challenge

import (
	"errors"
	"fmt"
	"sort"
)

type Job struct {
	ID       string
	Deps     []string
	Priority int
}

func Schedule(jobs []Job) ([]string, error) {
	// Handle empty input
	if len(jobs) == 0 {
		return []string{}, nil
	}

	// Build a map of ID to Job for quick lookup
	jobMap := make(map[string]Job)
	ids := make(map[string]bool)

	// Check for duplicate IDs and build job map
	for _, job := range jobs {
		if ids[job.ID] {
			return nil, fmt.Errorf("duplicate job ID: %s", job.ID)
		}
		ids[job.ID] = true
		jobMap[job.ID] = job
	}

	// Check for unknown dependencies
	for _, job := range jobs {
		for _, dep := range job.Deps {
			if !ids[dep] {
				return nil, fmt.Errorf("unknown dependency: %s", dep)
			}
		}
	}

	// Check for cycles using DFS with three states:
	// 0 = unvisited, 1 = visiting (in current path), 2 = visited (completed)
	state := make(map[string]int)
	for id := range ids {
		state[id] = 0
	}

	var hasCycle bool
	var dfs func(id string) bool
	dfs = func(id string) bool {
		if state[id] == 1 {
			return true // Cycle detected
		}
		if state[id] == 2 {
			return false // Already processed
		}

		state[id] = 1 // Mark as visiting
		job := jobMap[id]
		for _, dep := range job.Deps {
			if dfs(dep) {
				return true
			}
		}
		state[id] = 2 // Mark as visited
		return false
	}

	for id := range ids {
		if state[id] == 0 {
			if dfs(id) {
				hasCycle = true
				break
			}
		}
	}

	if hasCycle {
		return nil, errors.New("cycle detected in dependencies")
	}

	// Schedule jobs using the selection rule
	scheduled := make(map[string]bool)
	result := []string{}

	for len(result) < len(jobs) {
		// Find ready jobs (not scheduled, all deps satisfied)
		ready := []Job{}
		for _, job := range jobs {
			if !scheduled[job.ID] {
				allDepsSatisfied := true
				for _, dep := range job.Deps {
					if !scheduled[dep] {
						allDepsSatisfied = false
						break
					}
				}
				if allDepsSatisfied {
					ready = append(ready, job)
				}
			}
		}

		if len(ready) == 0 {
			return nil, errors.New("cycle detected in dependencies")
		}

		// Sort ready jobs by priority (descending) then by ID (ascending)
		sort.Slice(ready, func(i, j int) bool {
			if ready[i].Priority != ready[j].Priority {
				return ready[i].Priority > ready[j].Priority
			}
			return ready[i].ID < ready[j].ID
		})

		// Pick the first job
		chosen := ready[0]
		result = append(result, chosen.ID)
		scheduled[chosen.ID] = true
	}

	return result, nil
}
```

Test output (stdout)

n":"run","Package":"challenge","Test":"TestScheduleCycleError"}
{"Time":"2026-07-02T04:35:42.29715831+02:00","Action":"output","Package":"challenge","Test":"TestScheduleCycleError","Output":"=== RUN   TestScheduleCycleError\n"}
{"Time":"2026-07-02T04:35:42.29716055+02:00","Action":"output","Package":"challenge","Test":"TestScheduleCycleError","Output":"--- PASS: TestScheduleCycleError (0.00s)\n"}
{"Time":"2026-07-02T04:35:42.29716147+02:00","Action":"pass","Package":"challenge","Test":"TestScheduleCycleError","Elapsed":0}
{"Time":"2026-07-02T04:35:42.29716234+02:00","Action":"run","Package":"challenge","Test":"TestScheduleLongerCycleError"}
{"Time":"2026-07-02T04:35:42.297163119+02:00","Action":"output","Package":"challenge","Test":"TestScheduleLongerCycleError","Output":"=== RUN   TestScheduleLongerCycleError\n"}
{"Time":"2026-07-02T04:35:42.297164619+02:00","Action":"output","Package":"challenge","Test":"TestScheduleLongerCycleError","Output":"--- PASS: TestScheduleLongerCycleError (0.00s)\n"}
{"Time":"2026-07-02T04:35:42.297166309+02:00","Action":"pass","Package":"challenge","Test":"TestScheduleLongerCycleError","Elapsed":0}
{"Time":"2026-07-02T04:35:42.297167149+02:00","Action":"run","Package":"challenge","Test":"TestScheduleSelfDependencyError"}
{"Time":"2026-07-02T04:35:42.297167959+02:00","Action":"output","Package":"challenge","Test":"TestScheduleSelfDependencyError","Output":"=== RUN   TestScheduleSelfDependencyError\n"}
{"Time":"2026-07-02T04:35:42.297170839+02:00","Action":"output","Package":"challenge","Test":"TestScheduleSelfDependencyError","Output":"--- PASS: TestScheduleSelfDependencyError (0.00s)\n"}
{"Time":"2026-07-02T04:35:42.297171979+02:00","Action":"pass","Package":"challenge","Test":"TestScheduleSelfDependencyError","Elapsed":0}
{"Time":"2026-07-02T04:35:42.297173059+02:00","Action":"run","Package":"challenge","Test":"TestScheduleUnknownDepError"}
{"Time":"2026-07-02T04:35:42.297173839+02:00","Action":"output","Package":"challenge","Test":"TestScheduleUnknownDepError","Output":"=== RUN   TestScheduleUnknownDepError\n"}
{"Time":"2026-07-02T04:35:42.297176959+02:00","Action":"output","Package":"challenge","Test":"TestScheduleUnknownDepError","Output":"--- PASS: TestScheduleUnknownDepError (0.00s)\n"}
{"Time":"2026-07-02T04:35:42.297178129+02:00","Action":"pass","Package":"challenge","Test":"TestScheduleUnknownDepError","Elapsed":0}
{"Time":"2026-07-02T04:35:42.297179259+02:00","Action":"run","Package":"challenge","Test":"TestScheduleDuplicateIDError"}
{"Time":"2026-07-02T04:35:42.297180059+02:00","Action":"output","Package":"challenge","Test":"TestScheduleDuplicateIDError","Output":"=== RUN   TestScheduleDuplicateIDError\n"}
{"Time":"2026-07-02T04:35:42.297181419+02:00","Action":"output","Package":"challenge","Test":"TestScheduleDuplicateIDError","Output":"--- PASS: TestScheduleDuplicateIDError (0.00s)\n"}
{"Time":"2026-07-02T04:35:42.297182679+02:00","Action":"pass","Package":"challenge","Test":"TestScheduleDuplicateIDError","Elapsed":0}
{"Time":"2026-07-02T04:35:42.297184419+02:00","Action":"run","Package":"challenge","Test":"TestScheduleOutputIsValidTopoOrder"}
{"Time":"2026-07-02T04:35:42.297185409+02:00","Action":"output","Package":"challenge","Test":"TestScheduleOutputIsValidTopoOrder","Output":"=== RUN   TestScheduleOutputIsValidTopoOrder\n"}
{"Time":"2026-07-02T04:35:42.297202149+02:00","Action":"output","Package":"challenge","Test":"TestScheduleOutputIsValidTopoOrder","Output":"--- PASS: TestScheduleOutputIsValidTopoOrder (0.00s)\n"}
{"Time":"2026-07-02T04:35:42.297203469+02:00","Action":"pass","Package":"challenge","Test":"TestScheduleOutputIsValidTopoOrder","Elapsed":0}
{"Time":"2026-07-02T04:35:42.297204599+02:00","Action":"output","Package":"challenge","Output":"PASS\n"}
{"Time":"2026-07-02T04:35:42.297320829+02:00","Action":"output","Package":"challenge","Output":"ok  \tchallenge\t0.001s\n"}
{"Time":"2026-07-02T04:35:42.297340129+02:00","Action":"pass","Package":"challenge","Elapsed":0.001}
Reasoning (chain-of-thought)
The user wants me to implement a dependency-aware job scheduler in Go. Let me break down the requirements:

1. **Job struct**: Has ID, Deps (dependencies), and Priority
2. **Schedule function**: Takes a slice of Job, returns ([]string, error)
3. **Topological order**: Jobs must appear after their dependencies
4. **Selection rule**: 
   - Pick ready jobs (all deps satisfied)
   - Highest Priority first
   - Lexicographically smallest ID for ties
5. **Error conditions**:
   - Duplicate IDs
   - Unknown dependencies
   - Cycles

Let me think through the implementation:

1. First, validate the input:
   - Check for duplicate IDs
   - Check for unknown dependencies
   - Check for cycles

2. Then, implement the scheduling algorithm:
   - Maintain a set of scheduled jobs
   - In each iteration, find all ready jobs (not scheduled, all deps satisfied)
   - Sort ready jobs by priority (descending) then by ID (ascending)
   - Pick the first one, add to output, mark as scheduled
   - Repeat until all jobs are scheduled

Let me write the code:

```go
package challenge

type Job struct {
    ID       string
    Deps     []string
    Priority int
}

func Schedule(jobs []Job) ([]string, error) {
    // Handle empty input
    if len(jobs) == 0 {
        return []string{}, nil
    }
    
    // Build a map of ID to Job for quick lookup
    jobMap := make(map[string]Job)
    ids := make(map[string]bool)
    
    // Check for duplicate IDs and build job map
    for _, job := range jobs {
        if ids[job.ID] {
            return nil, fmt.Errorf("duplicate job ID: %s", job.ID)
        }
        ids[job.ID] = true
        jobMap[job.ID] = job
    }
    
    // Check for unknown dependencies
    for _, job := range jobs {
        for _, dep := range job.Deps {
            if !ids[dep] {
                return nil, fmt.Errorf("unknown dependency: %s", dep)
            }
        }
    }
    
    // Check for cycles using DFS
    // States: 0 = unvisited, 1 = visiting, 2 = visited
    state := make(map[string]int)
    for id := range ids {
        state[id] = 0
    }
    
    var hasCycle bool
    var dfs func(id string) bool
    dfs = func(id string) bool {
        if state[id] == 1 {
            return true // Cycle detected
        }
        if state[id] == 2 {
            return false // Already processed
        }
        
        state[id] = 1 // Mark as visiting
        job := jobMap[id]
        for _, dep := range job.Deps {
            if dfs(dep) {
                return true
            }
        }
        state[id] = 2 // Mark as visited
        return false
    }
    
    for id := range ids {
        if state[id] == 0 {
            if dfs(id) {
                hasCycle = true
                break
            }
        }
    }
    
    if hasCycle {
        return nil, fmt.Errorf("cycle detected in dependencies")
    }
    
    // Schedule jobs
    scheduled := make(map[string]bool)
    result := []string{}
    
    for len(result) < len(jobs) {
        // Find ready jobs
        ready := []Job{}
        for _, job := range jobs {
            if !scheduled[job.ID] {
                // Check if all dependencies are satisfied
                allDepsSatisfied := true
                for _, dep := range job.Deps {
                    if !scheduled[dep] {
                        allDepsSatisfied = false
                        break
                    }
                }
                if allDepsSatisfied {
                    ready = append(ready, job)
                }
            }
        }
        
        if len(ready) == 0 {
            return nil, fmt.Errorf("cycle detected in dependencies")
        }
        
        // Sort ready jobs by priority (descending) then by ID (ascending)
        sort.Slice(ready, func(i, j int) bool {
            if ready[i].Priority != ready[j].Priority {
                return ready[i].Priority > ready[j].Priority
            }
            return ready[i].ID < ready[j].ID
        })
        
        // Pick the first job
        chosen := ready[0]
        result = append(result, chosen.ID)
        scheduled[chosen.ID] = true
    }
    
    return result, nil
}
```

I'm realizing the cycle detection needs more careful consideration. The current implementation might not catch all cycle scenarios correctly. I'll need to refine the approach to ensure comprehensive cycle detection.

The key challenge is handling complex dependency graphs where cycles might not be immediately obvious. A more robust cycle detection algorithm will be crucial for ensuring the scheduling logic works correctly across different job configurations.

I'll focus on creating a more precise method to identify and handle potential circular dependencies before attempting to schedule jobs.