package store import ( "testing" ) func TestCreateChain_SeedsPositionsCorrectly(t *testing.T) { s := newNativeTasksTestStore(t) chain, err := s.CreateChain("Ham Radio Track", []string{"Study Technician", "Pass Technician exam", "Study General"}) if err != nil { t.Fatalf("CreateChain: %v", err) } if chain.Status != "active" { t.Errorf("chain.Status = %q, want active", chain.Status) } tasks, err := s.GetChainTasks(chain.ID) if err != nil { t.Fatalf("GetChainTasks: %v", err) } if len(tasks) != 3 { t.Fatalf("len(tasks) = %d, want 3", len(tasks)) } if !tasks[0].ChainUnlocked || tasks[0].DueDate == nil { t.Errorf("position 0: ChainUnlocked=%v DueDate=%v, want unlocked with a due date", tasks[0].ChainUnlocked, tasks[0].DueDate) } for i := 1; i < 3; i++ { if tasks[i].ChainUnlocked || tasks[i].DueDate != nil { t.Errorf("position %d: ChainUnlocked=%v DueDate=%v, want locked with no due date", i, tasks[i].ChainUnlocked, tasks[i].DueDate) } } if tasks[0].Content != "Study Technician" || tasks[1].Content != "Pass Technician exam" || tasks[2].Content != "Study General" { t.Errorf("unexpected content order: %q, %q, %q", tasks[0].Content, tasks[1].Content, tasks[2].Content) } } func TestCompleteNativeTask_AdvancesChain(t *testing.T) { s := newNativeTasksTestStore(t) chain, err := s.CreateChain("Track", []string{"Step 1", "Step 2"}) if err != nil { t.Fatal(err) } tasks, err := s.GetChainTasks(chain.ID) if err != nil { t.Fatal(err) } if err := s.CompleteNativeTask(tasks[0].ID); err != nil { t.Fatalf("CompleteNativeTask: %v", err) } after, err := s.GetChainTasks(chain.ID) if err != nil { t.Fatal(err) } if !after[1].ChainUnlocked || after[1].DueDate == nil { t.Errorf("position 1 after completing position 0: ChainUnlocked=%v DueDate=%v, want unlocked with a due date", after[1].ChainUnlocked, after[1].DueDate) } updatedChain, err := s.GetChain(chain.ID) if err != nil { t.Fatal(err) } if updatedChain.Status != "active" { t.Errorf("chain.Status = %q, want active (not yet done)", updatedChain.Status) } } func TestCompleteNativeTask_LastPosition_MarksChainCompleted(t *testing.T) { s := newNativeTasksTestStore(t) chain, err := s.CreateChain("Track", []string{"Only step"}) if err != nil { t.Fatal(err) } tasks, err := s.GetChainTasks(chain.ID) if err != nil { t.Fatal(err) } if err := s.CompleteNativeTask(tasks[0].ID); err != nil { t.Fatalf("CompleteNativeTask: %v", err) } updatedChain, err := s.GetChain(chain.ID) if err != nil { t.Fatal(err) } if updatedChain.Status != "completed" { t.Errorf("chain.Status = %q, want completed", updatedChain.Status) } } func TestCompleteNativeTask_PausedChain_DoesNotAdvance(t *testing.T) { s := newNativeTasksTestStore(t) chain, err := s.CreateChain("Track", []string{"Step 1", "Step 2"}) if err != nil { t.Fatal(err) } if err := s.SetChainStatus(chain.ID, "paused"); err != nil { t.Fatal(err) } tasks, err := s.GetChainTasks(chain.ID) if err != nil { t.Fatal(err) } if err := s.CompleteNativeTask(tasks[0].ID); err != nil { t.Fatalf("CompleteNativeTask: %v", err) } after, err := s.GetChainTasks(chain.ID) if err != nil { t.Fatal(err) } if after[1].ChainUnlocked { t.Error("position 1 should still be locked while chain is paused") } // Resuming re-enables advancement on the *next* completion. if err := s.SetChainStatus(chain.ID, "active"); err != nil { t.Fatal(err) } if err := s.CompleteNativeTask(tasks[1].ID); err != nil { t.Fatalf("CompleteNativeTask after resume: %v", err) } updatedChain, err := s.GetChain(chain.ID) if err != nil { t.Fatal(err) } if updatedChain.Status != "completed" { t.Errorf("chain.Status = %q, want completed after resuming and finishing the last step", updatedChain.Status) } } func TestGetUndatedNativeTasks_ExcludesLockedChainTasks(t *testing.T) { s := newNativeTasksTestStore(t) chain, err := s.CreateChain("Track", []string{"Step 1", "Step 2", "Step 3"}) if err != nil { t.Fatal(err) } _ = chain undated, err := s.GetUndatedNativeTasks() if err != nil { t.Fatalf("GetUndatedNativeTasks: %v", err) } for _, task := range undated { if task.ChainID != "" && !task.ChainUnlocked { t.Errorf("locked chain task %q leaked into GetUndatedNativeTasks", task.ID) } } } func TestGetChain_UnknownID_ReturnsErrNotFound(t *testing.T) { s := newNativeTasksTestStore(t) if _, err := s.GetChain("does-not-exist"); err != ErrNativeTaskNotFound { t.Errorf("err = %v, want ErrNativeTaskNotFound", err) } }