package main import ( "os" "testing" ) // withTempConfigDir подменяет XDG_CONFIG_HOME на временную — так // os.UserConfigDir() на Linux/macOS будет смотреть в неё, и тесты // не трогают реальный конфиг пользователя. func withTempConfigDir(t *testing.T) { t.Helper() dir := t.TempDir() oldXDG, hadXDG := os.LookupEnv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) t.Cleanup(func() { if hadXDG { os.Setenv("XDG_CONFIG_HOME", oldXDG) } else { os.Unsetenv("XDG_CONFIG_HOME") } }) } func TestLoadCorpseStarchGameWithNoSaveFile(t *testing.T) { withTempConfigDir(t) _, err := LoadCorpseStarchGame() if err != ErrCorpseStarchNoSave { t.Fatalf("ожидалась ErrCorpseStarchNoSave при отсутствии файла, получено: %v", err) } } func TestSaveAndLoadRoundTrip(t *testing.T) { withTempConfigDir(t) g := NewCorpseStarchGame(csT(0)) g.Starch = 123.5 g.TotalEarned = 999 g.ClickYield = 4 g.ClickUpgrades = 3 g.ServitorCounts[0] = 5 g.ServitorCounts[1] = 2 g.Corruption = 2 g.MissionsDone = 1 g.Log = []string{"a", "b", "c"} if err := SaveCorpseStarchGame(g); err != nil { t.Fatalf("неожиданная ошибка сохранения: %v", err) } loaded, err := LoadCorpseStarchGame() if err != nil { t.Fatalf("неожиданная ошибка загрузки: %v", err) } if loaded.Starch != g.Starch || loaded.TotalEarned != g.TotalEarned { t.Errorf("запас/накопленное не совпадают после загрузки: %+v vs %+v", loaded, g) } if loaded.ClickYield != g.ClickYield || loaded.ClickUpgrades != g.ClickUpgrades { t.Errorf("параметры клика не совпадают после загрузки") } if loaded.ServitorCounts[0] != 5 || loaded.ServitorCounts[1] != 2 { t.Errorf("счётчики сервиторов не совпадают после загрузки: %v", loaded.ServitorCounts) } if loaded.Corruption != 2 || loaded.MissionsDone != 1 { t.Errorf("скверна/завершённые миссии не совпадают после загрузки") } if len(loaded.Log) != 3 { t.Errorf("журнал должен был сохраниться полностью, получено %v", loaded.Log) } } func TestSaveAndLoadPreservesActiveMission(t *testing.T) { withTempConfigDir(t) g := NewCorpseStarchGame(csT(0)) g.TotalEarned = corpseStarchUnlockMissions g.StartMission(csT(100), 1) if err := SaveCorpseStarchGame(g); err != nil { t.Fatalf("неожиданная ошибка сохранения: %v", err) } loaded, err := LoadCorpseStarchGame() if err != nil { t.Fatalf("неожиданная ошибка загрузки: %v", err) } if loaded.ActiveMission == nil { t.Fatalf("активная миссия должна была сохраниться") } if loaded.ActiveMission.DefIdx != 1 { t.Errorf("ожидался индекс миссии 1, получено %d", loaded.ActiveMission.DefIdx) } if !loaded.ActiveMission.StartedAt.Equal(csT(100)) { t.Errorf("время начала миссии должно было сохраниться точно, получено %v", loaded.ActiveMission.StartedAt) } } // TestOfflineProgressAccruesOnLoad проверяет ключевое ожидаемое // поведение: если между сохранением и загрузкой прошло реальное // время, пассивный доход от сервиторов должен по-честному // начислиться при первом Tick после загрузки — ровно так же, как // если бы игрок всё это время не отходил от экрана. func TestOfflineProgressAccruesOnLoad(t *testing.T) { withTempConfigDir(t) g := NewCorpseStarchGame(csT(0)) g.ServitorCounts[0] = 10 // 10 * 0.5/сек = 5/сек if err := SaveCorpseStarchGame(g); err != nil { t.Fatalf("неожиданная ошибка сохранения: %v", err) } loaded, err := LoadCorpseStarchGame() if err != nil { t.Fatalf("неожиданная ошибка загрузки: %v", err) } loaded.Tick(csT(100)) if loaded.Starch != 500 { t.Errorf("ожидался начисленный доход за 100 секунд отсутствия (500), получено %v", loaded.Starch) } } func TestDeleteCorpseStarchSave(t *testing.T) { withTempConfigDir(t) g := NewCorpseStarchGame(csT(0)) if err := SaveCorpseStarchGame(g); err != nil { t.Fatalf("неожиданная ошибка сохранения: %v", err) } if err := DeleteCorpseStarchSave(); err != nil { t.Fatalf("неожиданная ошибка удаления: %v", err) } if _, err := LoadCorpseStarchGame(); err != ErrCorpseStarchNoSave { t.Errorf("после удаления файла загрузка должна вернуть ErrCorpseStarchNoSave, получено: %v", err) } if err := DeleteCorpseStarchSave(); err != nil { t.Errorf("повторное удаление отсутствующего файла не должно быть ошибкой, получено: %v", err) } } func TestLoadCorruptedSaveFileReturnsError(t *testing.T) { withTempConfigDir(t) path, err := corpseStarchSavePath() if err != nil { t.Fatalf("неожиданная ошибка получения пути: %v", err) } dir := path[:len(path)-len("/corpsestarch_save.json")] if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("неожиданная ошибка создания директории: %v", err) } if err := os.WriteFile(path, []byte("это не json"), 0o644); err != nil { t.Fatalf("неожиданная ошибка записи повреждённого файла: %v", err) } if _, err := LoadCorpseStarchGame(); err == nil { t.Fatalf("ожидалась ошибка при загрузке повреждённого файла") } } func TestSaveOverwritesPreviousSave(t *testing.T) { withTempConfigDir(t) g1 := NewCorpseStarchGame(csT(0)) g1.Starch = 10 SaveCorpseStarchGame(g1) g2 := NewCorpseStarchGame(csT(0)) g2.Starch = 999 SaveCorpseStarchGame(g2) loaded, err := LoadCorpseStarchGame() if err != nil { t.Fatalf("неожиданная ошибка загрузки: %v", err) } if loaded.Starch != 999 { t.Errorf("повторное сохранение должно было перезаписать файл, получено %v", loaded.Starch) } }