package scheduler import ( "testing" "time" ) func TestTaskStateIsTerminal(t *testing.T) { terminal := []TaskState{StateDone, StateFailed, StateCancelled, StateRejected} for _, s := range terminal { if !s.IsTerminal() { t.Errorf("expected %q to be terminal", s) } } nonTerminal := []TaskState{StateQueued, StateAssigned, StateRunning} for _, s := range nonTerminal { if s.IsTerminal() { t.Errorf("expected %q to be non-terminal", s) } } } func TestTaskSetState_ValidTransitions(t *testing.T) { cases := []struct { from TaskState to TaskState }{ {StateQueued, StateAssigned}, {StateQueued, StateCancelled}, {StateQueued, StateFailed}, {StateQueued, StateRejected}, {StateAssigned, StateRunning}, {StateAssigned, StateCancelled}, {StateRunning, StateDone}, {StateRunning, StateFailed}, {StateRunning, StateCancelled}, } for _, tc := range cases { task := &Task{State: tc.from, CreatedAt: time.Now()} if err := task.SetState(tc.to); err != nil { t.Errorf("transition %s → %s should be valid, got: %v", tc.from, tc.to, err) } if task.GetState() != tc.to { t.Errorf("expected state %s, got %s", tc.to, task.GetState()) } } } func TestTaskSetState_InvalidTransitions(t *testing.T) { cases := []struct { from TaskState to TaskState }{ {StateQueued, StateRunning}, {StateQueued, StateDone}, {StateAssigned, StateDone}, {StateAssigned, StateFailed}, {StateRunning, StateQueued}, {StateRunning, StateAssigned}, } for _, tc := range cases { task := &Task{State: tc.from} if err := task.SetState(tc.to); err == nil { t.Errorf("transition %s → %s should be invalid", tc.from, tc.to) } } } func TestTaskSetState_TerminalBlocked(t *testing.T) { for _, terminal := range []TaskState{StateDone, StateFailed, StateCancelled, StateRejected} { task := &Task{State: terminal} if err := task.SetState(StateQueued); err == nil { t.Errorf("should not allow transition from terminal state %s", terminal) } } } func TestTaskSetState_SetsTimestamps(t *testing.T) { task := &Task{State: StateQueued, CreatedAt: time.Now()} if err := task.SetState(StateAssigned); err != nil { t.Fatal(err) } if task.StartedAt.IsZero() { t.Error("StartedAt should be set on assigned") } if err := task.SetState(StateRunning); err != nil { t.Fatal(err) } if err := task.SetState(StateDone); err != nil { t.Fatal(err) } if task.CompletedAt.IsZero() { t.Error("CompletedAt should be set on done") } if task.LatencyMs <= 0 { // Allow 0 only if instant (<1ms) t.Log("latency was 0, which can happen on fast machines") } } func TestTaskSnapshot(t *testing.T) { task := &Task{ ID: "tsk_test1", AgentID: "agent-1", Action: "click", State: StateQueued, } snap := task.Snapshot() if snap.ID != task.ID || snap.AgentID != task.AgentID { t.Error("snapshot should copy fields") } } func TestSubmitRequestValidate(t *testing.T) { valid := SubmitRequest{AgentID: "a", Action: "click"} if err := valid.Validate(); err != nil { t.Errorf("valid request should pass: %v", err) } noAgent := SubmitRequest{Action: "click"} if err := noAgent.Validate(); err == nil { t.Error("missing agentId should fail") } noAction := SubmitRequest{AgentID: "a"} if err := noAction.Validate(); err == nil { t.Error("missing action should fail") } } func TestGenerateTaskID(t *testing.T) { id := generateTaskID() if len(id) < 12 { t.Errorf("task ID too short: %s", id) } if id[:4] != "tsk_" { t.Errorf("task ID should start with tsk_: %s", id) } id2 := generateTaskID() if id == id2 { t.Error("consecutive task IDs should be unique") } }