import subprocess import tempfile import unittest from pathlib import Path import release_manager class VersionTests(unittest.TestCase): def test_bumps_each_semver_component(self): self.assertEqual(release_manager.bump_semver("0.1.2", "patch"), "0.1.3") self.assertEqual(release_manager.bump_semver("0.1.2", "minor"), "0.2.0") self.assertEqual(release_manager.bump_semver("0.1.2", "major"), "1.0.0") def test_android_code_is_monotonic(self): self.assertEqual(release_manager.semantic_version_code("0.1.3"), 1003) self.assertEqual(release_manager.suggest_version_code("0.1.3", 5000), 5001) def test_rejects_non_release_semver(self): with self.assertRaises(ValueError): release_manager.parse_semver("1.2") class SafetyTests(unittest.TestCase): def test_safe_update_options_are_enabled_by_default(self): self.assertTrue(release_manager.DEFAULT_GIT_TRUENAS_ONLY) self.assertTrue(release_manager.DEFAULT_RUN_TESTS) def test_detects_outputs_and_secrets(self): paths = [ "src/game/example.ts", "dist-android/assets/app.js", "android/app/release.apk", ".env.production", "FINE.key.ts", "scripts/content/publish-content.mjs", "public/data/healer-man-loot.sqlite", ] self.assertEqual( release_manager.find_dangerous_staged_paths(paths), [ "dist-android/assets/app.js", "android/app/release.apk", ".env.production", ], ) def test_truenas_commands_pin_expected_commit(self): block = release_manager.generate_truenas_deploy_commands("abc123") self.assertIn("EXPECTED_COMMIT='abc123'", block) self.assertIn("pull --ff-only", block) self.assertIn('test "$ACTUAL_COMMIT" = "$EXPECTED_COMMIT"', block) def test_full_release_publishes_verified_content_before_building(self): checks = release_manager.full_release_checks("npm") commands = [command for command, _label in checks] publish_index = commands.index(["npm", "run", "content:publish"]) self.assertLess(publish_index, commands.index(["npm", "run", "build"])) self.assertLess(publish_index, commands.index(["npm", "run", "android:sync"])) def test_git_and_truenas_checks_skip_only_the_android_stage(self): checks = release_manager.full_release_checks("npm", include_android=False) commands = [command for command, _label in checks] self.assertIn(["npm", "run", "content:publish"], commands) self.assertIn(["npm", "run", "build"], commands) self.assertNotIn(["npm", "run", "android:sync"], commands) def test_disabled_test_suites_keep_safety_and_build_checks(self): checks = release_manager.full_release_checks( "npm", include_android=False, include_tests=False, ) commands = [command for command, _label in checks] self.assertEqual( commands, [ ["npm", "run", "content:publish"], ["npm", "run", "build"], ], ) def test_only_test_failures_may_be_overridden(self): checks = release_manager.full_release_checks("npm") overridable = { label for _command, label in checks if release_manager.can_override_release_check(label) } self.assertEqual( overridable, { "Content tests", "Server tests", "Application tests", "Shelved-mode application tests", }, ) self.assertFalse(release_manager.can_override_release_check("Production build")) self.assertFalse(release_manager.can_override_release_check("Push main")) def test_failed_test_warning_includes_command_and_specific_output(self): failure = release_manager.ReleaseCommandError( "Application tests", ["npm", "test"], 1, "\x1b[31mFAIL\x1b[39m src/game/example.test.ts\n" "Error: expected 2 to be 3", ) warning = release_manager.format_failed_test_warning(failure) self.assertIn("Application tests failed with exit code 1", warning) self.assertIn("npm test", warning) self.assertIn("FAIL src/game/example.test.ts", warning) self.assertIn("Error: expected 2 to be 3", warning) self.assertNotIn("\x1b", warning) class ReleaseCandidateTests(unittest.TestCase): def test_checks_untracked_files_without_changing_the_real_index(self): with tempfile.TemporaryDirectory() as temp_dir: project_root = Path(temp_dir) def git(*args): subprocess.run( ["git", *args], cwd=project_root, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) git("init", "--quiet") (project_root / "tracked.ts").write_text("export const tracked = true;\n") git("add", "tracked.ts") git( "-c", "user.name=Healer Man Tests", "-c", "user.email=tests@example.invalid", "commit", "--quiet", "-m", "Initial commit", ) untracked = project_root / "untracked.ts" untracked.write_text("export const untracked = true;\n\n") with self.assertRaisesRegex( release_manager.ReleaseError, "untracked.ts.*new blank line at EOF" ): release_manager.validate_release_candidates(project_root) untracked.write_text("export const untracked = true;\n") self.assertEqual( release_manager.validate_release_candidates(project_root), ["untracked.ts"], ) self.assertEqual( subprocess.run( ["git", "diff", "--cached", "--name-only"], cwd=project_root, text=True, stdout=subprocess.PIPE, check=True, ).stdout, "", ) if __name__ == "__main__": unittest.main()