| 1 | //! Preserved changelog parsing and host parity tests, outside the portable group. |
| 2 | |
| 3 | use crate::commands::CommandResult; |
| 4 | use crate::commands::groups::debug::change::*; |
| 5 | use crate::tui::app::AppAction; |
| 6 | use codewhale_localization::{MessageId, tr}; |
| 7 | const CODEWHALE_CHANGELOG: &str = include_str!("../../CHANGELOG.md"); |
| 8 | fn change(app: &mut App, version: Option<&str>) -> CommandResult { |
| 9 | crate::commands::execute( |
| 10 | &version.map_or_else(|| "/change".to_string(), |arg| format!("/change {arg}")), |
| 11 | app, |
| 12 | ) |
| 13 | } |
| 14 | use crate::config::Config; |
| 15 | use crate::test_support::{EnvVarGuard, lock_test_env}; |
| 16 | use crate::tui::app::{App, TuiOptions}; |
| 17 | use codewhale_localization::Locale; |
| 18 | fn make_app(tmpdir: &tempfile::TempDir, locale: Locale, has_api_key: bool) -> App { |
| 19 | let mut config = Config::default(); |
| 20 | if has_api_key { |
| 21 | config.set_legacy_root(Some("test-key".to_string()), None); |
| 22 | } |
| 23 | let mut app = App::new( |
| 24 | TuiOptions { |
| 25 | skills_dir: tmpdir.path().join("skills"), |
| 26 | memory_path: tmpdir.path().join("memory.md"), |
| 27 | notes_path: tmpdir.path().join("notes.txt"), |
| 28 | mcp_config_path: tmpdir.path().join("mcp.json"), |
| 29 | ..crate::test_support::test_tui_options(tmpdir.path()) |
| 30 | }, |
| 31 | &config, |
| 32 | ); |
| 33 | app.ui_locale = locale; |
| 34 | app.api_provider = crate::config::ProviderKind::Deepseek; |
| 35 | app.model_ids_passthrough = false; |
| 36 | app.onboarding_needs_api_key = !has_api_key; |
| 37 | app |
| 38 | } |
| 39 | |
| 40 | #[test] |
| 41 | fn extract_latest_section_finds_first_version() { |
| 42 | let content = "\n\ |
| 43 | ## [0.8.26] - 2026-05-09\n\ |
| 44 | \n\ |
| 45 | A security + polish release.\n\ |
| 46 | \n\ |
| 47 | ### Fixed\n\ |
| 48 | \n\ |
| 49 | - Fixed something\n\ |
| 50 | \n\ |
| 51 | ## [0.8.25] - 2026-05-09\n\ |
| 52 | \n\ |
| 53 | A stabilization release.\n"; |
| 54 | let section = extract_latest_changelog_section(content).expect("should find a section"); |
| 55 | assert!(section.contains("0.8.26")); |
| 56 | assert!(section.contains("Fixed something")); |
| 57 | assert!(!section.contains("0.8.25")); |
| 58 | } |
| 59 | |
| 60 | #[test] |
| 61 | fn extract_latest_section_handles_0_8_29_style_fixture() { |
| 62 | let content = "\n\ |
| 63 | # Changelog\n\ |
| 64 | \n\ |
| 65 | ## [0.8.29] - 2026-05-11\n\ |
| 66 | \n\ |
| 67 | Release candidate polish.\n\ |
| 68 | \n\ |
| 69 | ### Added\n\ |
| 70 | - New note-management command.\n\ |
| 71 | \n\ |
| 72 | ## [0.8.28] - 2026-05-10\n\ |
| 73 | \n\ |
| 74 | Previous release.\n"; |
| 75 | let section = extract_latest_changelog_section(content).expect("should find a section"); |
| 76 | assert!(section.contains("0.8.29")); |
| 77 | assert!(section.contains("2026-05-11")); |
| 78 | assert!(section.contains("New note-management command")); |
| 79 | assert!(!section.contains("0.8.28")); |
| 80 | } |
| 81 | |
| 82 | #[test] |
| 83 | fn extract_latest_section_returns_none_for_empty_content() { |
| 84 | assert!(extract_latest_changelog_section("").is_none()); |
| 85 | } |
| 86 | |
| 87 | #[test] |
| 88 | fn extract_latest_section_returns_none_for_no_version_headers() { |
| 89 | let content = "# Just a heading\n\nSome text\n"; |
| 90 | assert!(extract_latest_changelog_section(content).is_none()); |
| 91 | } |
| 92 | |
| 93 | #[test] |
| 94 | fn extract_latest_section_handles_single_version() { |
| 95 | let content = "\n## [0.8.26] - 2026-05-09\n\nOnly one version.\n"; |
| 96 | let section = extract_latest_changelog_section(content).expect("should find a section"); |
| 97 | assert!(section.contains("0.8.26")); |
| 98 | assert!(section.contains("Only one version")); |
| 99 | } |
| 100 | |
| 101 | #[test] |
| 102 | fn extract_latest_section_handles_subheadings() { |
| 103 | let content = "\n\ |
| 104 | ## [0.8.26] - 2026-05-09\n\ |
| 105 | \n\ |
| 106 | ### Added\n\ |
| 107 | - New feature A\n\ |
| 108 | \n\ |
| 109 | ### Fixed\n\ |
| 110 | - Fixed bug B\n\ |
| 111 | \n\ |
| 112 | ## [0.8.25] - 2026-05-09\n\ |
| 113 | "; |
| 114 | let section = extract_latest_changelog_section(content).expect("should find a section"); |
| 115 | assert!(section.contains("New feature A")); |
| 116 | assert!(section.contains("Fixed bug B")); |
| 117 | assert!(!section.contains("0.8.25")); |
| 118 | } |
| 119 | |
| 120 | #[test] |
| 121 | fn change_uses_bundled_release_notes_without_workspace_changelog() { |
| 122 | let tmp = tempfile::TempDir::new().unwrap(); |
| 123 | let mut app = make_app(&tmp, Locale::En, false); |
| 124 | let result = change(&mut app, None); |
| 125 | assert!(!result.is_error); |
| 126 | let msg = result.message.expect("should have a message"); |
| 127 | let expected = extract_latest_changelog_section(CODEWHALE_CHANGELOG) |
| 128 | .expect("bundled changelog should have a release section"); |
| 129 | assert!(msg.contains(expected.lines().next().unwrap())); |
| 130 | } |
| 131 | |
| 132 | #[test] |
| 133 | fn change_ignores_workspace_changelog() { |
| 134 | let tmp = tempfile::TempDir::new().unwrap(); |
| 135 | std::fs::write( |
| 136 | tmp.path().join("CHANGELOG.md"), |
| 137 | "\n## [9.9.9] - 2099-01-01\n\nWorkspace changelog.\n", |
| 138 | ) |
| 139 | .unwrap(); |
| 140 | let mut app = make_app(&tmp, Locale::En, false); |
| 141 | let result = change(&mut app, None); |
| 142 | assert!(!result.is_error); |
| 143 | let msg = result.message.expect("should have a message"); |
| 144 | assert!(!msg.contains("9.9.9")); |
| 145 | assert!(!msg.contains("Workspace changelog")); |
| 146 | } |
| 147 | |
| 148 | #[test] |
| 149 | fn change_in_english_returns_message_without_action() { |
| 150 | let tmp = tempfile::TempDir::new().unwrap(); |
| 151 | let mut app = make_app(&tmp, Locale::En, true); |
| 152 | let result = change(&mut app, None); |
| 153 | assert!(!result.is_error); |
| 154 | let msg = result.message.expect("should have a message"); |
| 155 | let expected = extract_latest_changelog_section(CODEWHALE_CHANGELOG) |
| 156 | .expect("bundled changelog should have a release section"); |
| 157 | assert!(msg.contains(expected.lines().next().unwrap())); |
| 158 | assert!( |
| 159 | result.action.is_none(), |
| 160 | "English locale should not send translation" |
| 161 | ); |
| 162 | } |
| 163 | |
| 164 | #[test] |
| 165 | fn change_in_non_english_also_sends_translation_action() { |
| 166 | for (locale, _label) in [ |
| 167 | (Locale::ZhHans, "zh-Hans"), |
| 168 | (Locale::Ja, "ja"), |
| 169 | (Locale::PtBr, "pt-BR"), |
| 170 | ] { |
| 171 | let tmp = tempfile::TempDir::new().unwrap(); |
| 172 | let mut app = make_app(&tmp, locale, true); |
| 173 | let result = change(&mut app, None); |
| 174 | assert!(!result.is_error, "Failed for locale {locale:?}"); |
| 175 | let msg = result.message.expect("should have a message"); |
| 176 | assert!(msg.contains(&*tr(locale, MessageId::CmdChangeTranslationQueued))); |
| 177 | assert!( |
| 178 | matches!(result.action, Some(AppAction::SendMessage(_))), |
| 179 | "Non-English locale should send translation, got {:?}", |
| 180 | result.action |
| 181 | ); |
| 182 | if let Some(AppAction::SendMessage(prompt)) = &result.action { |
| 183 | let expected = extract_latest_changelog_section(CODEWHALE_CHANGELOG) |
| 184 | .expect("bundled changelog should have a release section"); |
| 185 | assert!(prompt.contains(expected.lines().next().unwrap())); |
| 186 | let prev_ver = extract_previous_version_number(CODEWHALE_CHANGELOG) |
| 187 | .expect("bundled changelog should have a previous release"); |
| 188 | assert!( |
| 189 | prompt.contains(&prev_ver), |
| 190 | "translation prompt should include previous-version hint: {prompt}" |
| 191 | ); |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | #[test] |
| 197 | fn change_in_non_english_without_api_key_uses_explicit_fallback() { |
| 198 | let tmp = tempfile::TempDir::new().unwrap(); |
| 199 | let _lock = lock_test_env(); |
| 200 | let _config_path = EnvVarGuard::set("DEEPSEEK_CONFIG_PATH", tmp.path().join("config.toml")); |
| 201 | let _deepseek_key = EnvVarGuard::remove("DEEPSEEK_API_KEY"); |
| 202 | let _deepseek_provider = EnvVarGuard::remove("DEEPSEEK_PROVIDER"); |
| 203 | let _codewhale_provider = EnvVarGuard::remove("CODEWHALE_PROVIDER"); |
| 204 | let mut app = make_app(&tmp, Locale::ZhHans, false); |
| 205 | let result = change(&mut app, None); |
| 206 | assert!(!result.is_error); |
| 207 | let msg = result.message.expect("should have a message"); |
| 208 | assert!(msg.contains(&*tr( |
| 209 | Locale::ZhHans, |
| 210 | MessageId::CmdChangeTranslationUnavailable |
| 211 | ))); |
| 212 | assert!( |
| 213 | result.action.is_none(), |
| 214 | "missing API key should not send translation" |
| 215 | ); |
| 216 | } |
| 217 | |
| 218 | #[test] |
| 219 | fn change_in_non_english_offline_uses_explicit_fallback() { |
| 220 | let tmp = tempfile::TempDir::new().unwrap(); |
| 221 | let mut app = make_app(&tmp, Locale::Ja, true); |
| 222 | app.offline_mode = true; |
| 223 | let result = change(&mut app, None); |
| 224 | assert!(!result.is_error); |
| 225 | let msg = result.message.expect("should have a message"); |
| 226 | assert!(msg.contains(&*tr(Locale::Ja, MessageId::CmdChangeTranslationUnavailable))); |
| 227 | assert!( |
| 228 | result.action.is_none(), |
| 229 | "offline mode should not send translation" |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | #[test] |
| 234 | fn extract_latest_ignores_lines_before_first_version() { |
| 235 | let content = "\n\ |
| 236 | # Changelog\n\ |
| 237 | \n\ |
| 238 | Some intro text.\n\ |
| 239 | \n\ |
| 240 | ## [0.8.26] - 2026-05-09\n\ |
| 241 | \n\ |
| 242 | Content\n\ |
| 243 | "; |
| 244 | let section = extract_latest_changelog_section(content).expect("should find a section"); |
| 245 | assert!(section.contains("0.8.26")); |
| 246 | assert!(!section.contains("Changelog")); |
| 247 | assert!(!section.contains("intro text")); |
| 248 | } |
| 249 | |
| 250 | #[test] |
| 251 | fn extract_latest_skips_empty_unreleased_section() { |
| 252 | let content = "\n\ |
| 253 | ## [Unreleased]\n\ |
| 254 | \n\ |
| 255 | ## [0.8.32] - 2026-05-12\n\ |
| 256 | \n\ |
| 257 | A release with content.\n\ |
| 258 | \n\ |
| 259 | ### Fixed\n\ |
| 260 | - Something fixed\n\ |
| 261 | \n\ |
| 262 | ## [0.8.31] - 2026-05-11\n\ |
| 263 | \n\ |
| 264 | Previous release.\n"; |
| 265 | let section = extract_latest_changelog_section(content).expect("should skip Unreleased"); |
| 266 | assert!(section.contains("0.8.32")); |
| 267 | assert!(section.contains("Something fixed")); |
| 268 | assert!(!section.contains("Unreleased")); |
| 269 | assert!(!section.contains("0.8.31")); |
| 270 | } |
| 271 | |
| 272 | #[test] |
| 273 | fn extract_latest_skips_entirely_empty_unreleased() { |
| 274 | // `## [Unreleased]` followed immediately by the next version heading. |
| 275 | let content = "\n\ |
| 276 | ## [Unreleased]\n\ |
| 277 | ## [0.8.32] - 2026-05-12\n\ |
| 278 | \n\ |
| 279 | Content here.\n"; |
| 280 | let section = extract_latest_changelog_section(content).expect("should find 0.8.32"); |
| 281 | assert!(section.contains("0.8.32")); |
| 282 | assert!(!section.contains("Unreleased")); |
| 283 | } |
| 284 | |
| 285 | #[test] |
| 286 | fn extract_latest_returns_none_when_all_sections_empty() { |
| 287 | let content = "\n\ |
| 288 | ## [Unreleased]\n\ |
| 289 | ## [Future]\n"; |
| 290 | assert!(extract_latest_changelog_section(content).is_none()); |
| 291 | } |
| 292 | |
| 293 | #[test] |
| 294 | fn extract_latest_skips_multiple_empty_sections() { |
| 295 | let content = "\n\ |
| 296 | ## [Unreleased]\n\ |
| 297 | \n\ |
| 298 | ## [Next]\n\ |
| 299 | \n\ |
| 300 | ## [0.8.32] - 2026-05-12\n\ |
| 301 | \n\ |
| 302 | Real content.\n"; |
| 303 | let section = extract_latest_changelog_section(content).expect("should find 0.8.32"); |
| 304 | assert!(section.contains("0.8.32")); |
| 305 | assert!(section.contains("Real content")); |
| 306 | } |
| 307 | |
| 308 | #[test] |
| 309 | fn extract_by_version_finds_exact_version() { |
| 310 | let content = "\n\ |
| 311 | ## [0.8.32] - 2026-05-12\n\ |
| 312 | \n\ |
| 313 | Release content.\n\ |
| 314 | \n\ |
| 315 | ## [0.8.31] - 2026-05-11\n\ |
| 316 | \n\ |
| 317 | Earlier release.\n"; |
| 318 | let section = |
| 319 | extract_changelog_section_by_version(content, "0.8.31").expect("should find 0.8.31"); |
| 320 | assert!(section.contains("0.8.31")); |
| 321 | assert!(section.contains("Earlier release")); |
| 322 | assert!(!section.contains("0.8.32")); |
| 323 | } |
| 324 | |
| 325 | #[test] |
| 326 | fn extract_by_version_returns_none_for_missing_version() { |
| 327 | let content = "\n\ |
| 328 | ## [0.8.32] - 2026-05-12\n\ |
| 329 | \n\ |
| 330 | Content.\n"; |
| 331 | assert!(extract_changelog_section_by_version(content, "9.9.9").is_none()); |
| 332 | } |
| 333 | |
| 334 | #[test] |
| 335 | fn extract_by_version_finds_version_without_date() { |
| 336 | let content = "\n\ |
| 337 | ## [Unreleased]\n\ |
| 338 | \n\ |
| 339 | Nothing.\n"; |
| 340 | let section = extract_changelog_section_by_version(content, "Unreleased") |
| 341 | .expect("should find Unreleased"); |
| 342 | assert!(section.contains("Unreleased")); |
| 343 | assert!(section.contains("Nothing")); |
| 344 | } |
| 345 | |
| 346 | #[test] |
| 347 | fn extract_by_version_respects_empty_sections() { |
| 348 | // `## [0.8.32]` is empty, should return None for it |
| 349 | let content = "\n\ |
| 350 | ## [0.8.32] - 2026-05-12\n\ |
| 351 | ## [0.8.31] - 2026-05-11\n\ |
| 352 | \n\ |
| 353 | Content.\n"; |
| 354 | assert!(extract_changelog_section_by_version(content, "0.8.32").is_none()); |
| 355 | } |
| 356 | |
| 357 | #[test] |
| 358 | fn change_with_version_arg_shows_older_release() { |
| 359 | let tmp = tempfile::TempDir::new().unwrap(); |
| 360 | let mut app = make_app(&tmp, Locale::En, false); |
| 361 | let result = change(&mut app, Some("0.8.1")); |
| 362 | // 0.8.1 is a very old release; if it exists, the result should not be an error. |
| 363 | // If that exact version doesn't exist in the bundled changelog, we still |
| 364 | // expect a proper error message referencing the version. |
| 365 | if result.is_error { |
| 366 | let msg = result.message.as_deref().unwrap_or(""); |
| 367 | assert!(msg.contains("0.8.1"), "error should mention version: {msg}"); |
| 368 | } else { |
| 369 | let msg = result.message.expect("should have a message"); |
| 370 | assert!(msg.contains("0.8.1")); |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | #[test] |
| 375 | fn change_with_empty_version_arg_acts_as_default() { |
| 376 | let tmp = tempfile::TempDir::new().unwrap(); |
| 377 | let mut app = make_app(&tmp, Locale::En, false); |
| 378 | let result_default = change(&mut app, None); |
| 379 | assert!(!result_default.is_error); |
| 380 | |
| 381 | let mut app2 = make_app(&tmp, Locale::En, false); |
| 382 | let result_empty = change(&mut app2, Some("")); |
| 383 | assert!(!result_empty.is_error); |
| 384 | |
| 385 | // Both should have the same message content |
| 386 | let msg_default = result_default.message.as_deref().unwrap_or(""); |
| 387 | let msg_empty = result_empty.message.as_deref().unwrap_or(""); |
| 388 | assert_eq!(msg_default, msg_empty); |
| 389 | } |
| 390 | |
| 391 | #[test] |
| 392 | fn change_with_nonexistent_version_returns_error() { |
| 393 | let tmp = tempfile::TempDir::new().unwrap(); |
| 394 | let mut app = make_app(&tmp, Locale::En, false); |
| 395 | let result = change(&mut app, Some("99.99.99")); |
| 396 | assert!(result.is_error); |
| 397 | let msg = result.message.as_deref().unwrap_or(""); |
| 398 | assert!( |
| 399 | msg.contains("99.99.99"), |
| 400 | "error should mention version: {msg}" |
| 401 | ); |
| 402 | } |
| 403 | |
| 404 | #[test] |
| 405 | fn extract_by_version_ignores_substring_matches() { |
| 406 | let content = |
| 407 | "\n## [0.8.1] - 2026-01-01\n\nContent A.\n\n## [0.8.10] - 2026-01-10\n\nContent B.\n"; |
| 408 | let section = |
| 409 | extract_changelog_section_by_version(content, "0.8.1").expect("should find 0.8.1"); |
| 410 | assert!(section.contains("Content A")); |
| 411 | assert!(!section.contains("Content B")); |
| 412 | } |
| 413 | |
| 414 | // --- extract_previous_version_number tests --- |
| 415 | |
| 416 | #[test] |
| 417 | fn prev_version_finds_second_heading() { |
| 418 | let content = "\n\ |
| 419 | ## [0.8.32] - 2026-05-12\n\ |
| 420 | \n\ |
| 421 | Release content.\n\ |
| 422 | \n\ |
| 423 | ## [0.8.31] - 2026-05-11\n\ |
| 424 | \n\ |
| 425 | Earlier release.\n"; |
| 426 | let prev = extract_previous_version_number(content).expect("should find 0.8.31"); |
| 427 | assert_eq!(prev, "0.8.31"); |
| 428 | } |
| 429 | |
| 430 | #[test] |
| 431 | fn prev_version_skips_empty_unreleased_section() { |
| 432 | let content = "\n\ |
| 433 | ## [Unreleased]\n\ |
| 434 | \n\ |
| 435 | ## [0.8.32] - 2026-05-12\n\ |
| 436 | \n\ |
| 437 | Actual release.\n\ |
| 438 | \n\ |
| 439 | ## [0.8.31] - 2026-05-11\n\ |
| 440 | \n\ |
| 441 | Older release.\n"; |
| 442 | let prev = |
| 443 | extract_previous_version_number(content).expect("should skip Unreleased and find 0.8.31"); |
| 444 | assert_eq!(prev, "0.8.31"); |
| 445 | } |
| 446 | |
| 447 | #[test] |
| 448 | fn prev_version_returns_none_for_single_version() { |
| 449 | let content = "\n## [0.8.32] - 2026-05-12\n\nOnly one version.\n"; |
| 450 | assert!(extract_previous_version_number(content).is_none()); |
| 451 | } |
| 452 | |
| 453 | #[test] |
| 454 | fn prev_version_returns_none_for_empty_content() { |
| 455 | assert!(extract_previous_version_number("").is_none()); |
| 456 | } |
| 457 | |
| 458 | #[test] |
| 459 | fn prev_version_returns_none_for_no_version_headers() { |
| 460 | let content = "# Just a heading\n\nNo versions here.\n"; |
| 461 | assert!(extract_previous_version_number(content).is_none()); |
| 462 | } |
| 463 | |
| 464 | #[test] |
| 465 | fn prev_version_handles_adjacent_headings() { |
| 466 | let content = "\n\ |
| 467 | ## [0.8.32] - 2026-05-12\n\ |
| 468 | \n\ |
| 469 | Content.\n\ |
| 470 | ## [0.8.31] - 2026-05-11\n\ |
| 471 | \n\ |
| 472 | Older content.\n"; |
| 473 | let prev = extract_previous_version_number(content) |
| 474 | .expect("should find 0.8.31 even with no blank line after section"); |
| 475 | assert_eq!(prev, "0.8.31"); |
| 476 | } |
| 477 | |
| 478 | #[test] |
| 479 | fn prev_version_skips_multiple_empty_sections() { |
| 480 | let content = "\n\ |
| 481 | ## [Unreleased]\n\ |
| 482 | \n\ |
| 483 | ## [Future]\n\ |
| 484 | \n\ |
| 485 | ## [0.8.32] - 2026-05-12\n\ |
| 486 | \n\ |
| 487 | Real release.\n\ |
| 488 | \n\ |
| 489 | ## [0.8.31] - 2026-05-11\n\ |
| 490 | \n\ |
| 491 | Older release.\n"; |
| 492 | let prev = extract_previous_version_number(content) |
| 493 | .expect("should skip Unreleased and Future, find 0.8.31"); |
| 494 | assert_eq!(prev, "0.8.31"); |
| 495 | } |
| 496 | |
| 497 | #[test] |
| 498 | fn prev_version_after_explicit_version_finds_next_older_release() { |
| 499 | let content = "\n\ |
| 500 | ## [0.8.32] - 2026-05-12\n\ |
| 501 | \n\ |
| 502 | Current release.\n\ |
| 503 | \n\ |
| 504 | ## [0.8.31] - 2026-05-11\n\ |
| 505 | \n\ |
| 506 | Requested release.\n\ |
| 507 | \n\ |
| 508 | ## [0.8.30] - 2026-05-10\n\ |
| 509 | \n\ |
| 510 | Older release.\n"; |
| 511 | let prev = extract_previous_version_number_after_version(content, "0.8.31") |
| 512 | .expect("should find 0.8.30"); |
| 513 | assert_eq!(prev, "0.8.30"); |
| 514 | } |
| 515 | |
| 516 | #[test] |
| 517 | fn prev_version_after_explicit_version_skips_empty_sections() { |
| 518 | let content = "\n\ |
| 519 | ## [0.8.32] - 2026-05-12\n\ |
| 520 | \n\ |
| 521 | Current release.\n\ |
| 522 | \n\ |
| 523 | ## [0.8.31] - 2026-05-11\n\ |
| 524 | \n\ |
| 525 | Requested release.\n\ |
| 526 | \n\ |
| 527 | ## [Future]\n\ |
| 528 | \n\ |
| 529 | ## [0.8.30] - 2026-05-10\n\ |
| 530 | \n\ |
| 531 | Older release.\n"; |
| 532 | let prev = extract_previous_version_number_after_version(content, "0.8.31") |
| 533 | .expect("should skip Future and find 0.8.30"); |
| 534 | assert_eq!(prev, "0.8.30"); |
| 535 | } |
| 536 | |
| 537 | // --- change() output hint tests --- |
| 538 | |
| 539 | #[test] |
| 540 | fn change_without_args_includes_previous_version_hint() { |
| 541 | let tmp = tempfile::TempDir::new().unwrap(); |
| 542 | let mut app = make_app(&tmp, Locale::En, false); |
| 543 | let result = change(&mut app, None); |
| 544 | assert!(!result.is_error); |
| 545 | let msg = result.message.expect("should have a message"); |
| 546 | // The previous version hint should be part of the output. |
| 547 | // We can't assert an exact version number since the changelog changes, |
| 548 | // but the hint message key should appear. |
| 549 | assert!( |
| 550 | msg.contains("Previous version:") || msg.contains("run `/change"), |
| 551 | "expected previous-version hint in output, got: {msg}" |
| 552 | ); |
| 553 | } |
| 554 | |
| 555 | #[test] |
| 556 | fn change_with_explicit_version_includes_previous_hint() { |
| 557 | let tmp = tempfile::TempDir::new().unwrap(); |
| 558 | let mut app = make_app(&tmp, Locale::En, false); |
| 559 | // Derive versions from the bundled changelog: it only embeds a recent |
| 560 | // slice of releases, so hardcoded versions would age out of it. |
| 561 | let explicit = extract_previous_version_number(CODEWHALE_CHANGELOG) |
| 562 | .expect("bundled changelog should have a previous release"); |
| 563 | let expected_prev = |
| 564 | extract_previous_version_number_after_version(CODEWHALE_CHANGELOG, &explicit) |
| 565 | .expect("bundled changelog should have at least three releases"); |
| 566 | let result = change(&mut app, Some(&explicit)); |
| 567 | assert!(!result.is_error); |
| 568 | let msg = result.message.as_deref().unwrap_or(""); |
| 569 | assert!( |
| 570 | msg.contains("Previous version:") && msg.contains(&expected_prev), |
| 571 | "explicit version should show previous-version hint: {msg}" |
| 572 | ); |
| 573 | } |
| 574 | |
| 575 | #[test] |
| 576 | fn change_hint_uses_localized_template() { |
| 577 | let tmp = tempfile::TempDir::new().unwrap(); |
| 578 | let mut app = make_app(&tmp, Locale::ZhHans, true); |
| 579 | let result = change(&mut app, None); |
| 580 | assert!(!result.is_error); |
| 581 | let msg = result.message.expect("should have a message"); |
| 582 | // zh-Hans template: "上一个版本:" |
| 583 | assert!( |
| 584 | msg.contains("上一个版本"), |
| 585 | "zh-Hans output should contain localized hint: {msg}" |
| 586 | ); |
| 587 | } |
| 588 | |
| 589 | #[test] |
| 590 | fn change_hint_in_japanese() { |
| 591 | let tmp = tempfile::TempDir::new().unwrap(); |
| 592 | let mut app = make_app(&tmp, Locale::Ja, true); |
| 593 | let result = change(&mut app, None); |
| 594 | assert!(!result.is_error); |
| 595 | let msg = result.message.expect("should have a message"); |
| 596 | assert!( |
| 597 | msg.contains("前のバージョン"), |
| 598 | "ja output should contain localized hint: {msg}" |
| 599 | ); |
| 600 | } |
| 601 | |
| 602 | #[test] |
| 603 | fn change_hint_in_portuguese() { |
| 604 | let tmp = tempfile::TempDir::new().unwrap(); |
| 605 | let mut app = make_app(&tmp, Locale::PtBr, true); |
| 606 | let result = change(&mut app, None); |
| 607 | assert!(!result.is_error); |
| 608 | let msg = result.message.expect("should have a message"); |
| 609 | assert!( |
| 610 | msg.contains("Versão anterior"), |
| 611 | "pt-BR output should contain localized hint: {msg}" |
| 612 | ); |
| 613 | } |
| 614 |