import json import tempfile import unittest from pathlib import Path from post_import_refresh import AutomationConfig, StepResult, build_steps, write_status class PostImportRefreshPlanTest(unittest.TestCase): def test_dry_run_is_the_default_and_never_enables_index_writes(self): config = AutomationConfig() steps = build_steps(config) commands = "\n".join(command for step in steps for command in step.commands) self.assertFalse(config.apply) self.assertIn("validate semantic index dry-run", [step.name for step in steps]) self.assertIn("semantic_index/refresh.sh", commands) self.assertNotIn("semantic_index/refresh.sh --apply", commands) self.assertNotIn("--force-rebuild", commands) self.assertNotIn("systemctl enable --now semantic-index-refresh.timer", commands) def test_plugin_reapply_happens_before_migrations_and_helpdesk_reset(self): names = [step.name for step in build_steps(AutomationConfig())] self.assertLess(names.index("reapply tracked plugins"), names.index("run plugin migrations")) self.assertLess(names.index("run plugin migrations"), names.index("reset Helpdesk mail settings")) def test_expected_plugins_are_reapplied_to_remote_redmine_tree(self): steps = build_steps(AutomationConfig()) plugin_step = next(step for step in steps if step.name == "reapply tracked plugins") commands = "\n".join(plugin_step.commands) self.assertIn("plugins/redmine_event_outbox", commands) self.assertIn("plugins/redmine_contacts", commands) self.assertIn("plugins/redmine_contacts_helpdesk", commands) self.assertNotIn("plugins/redmine_event_outbox/", commands) self.assertIn("reddev@192.168.50.170:/usr/share/redmine/plugins/", commands) def test_apply_mode_runs_mutating_validation_sequence(self): steps = build_steps(AutomationConfig(apply=True)) commands = "\n".join(command for step in steps for command in step.commands) self.assertIn("bundle exec rake redmine:plugins:migrate", commands) self.assertIn("./reset_helpdesk_mail_settings.py", commands) self.assertIn("touch tmp/restart.txt", commands) self.assertIn("./validate_test_instance.py", commands) def test_remote_write_steps_use_sudo_by_default(self): commands = "\n".join(command for step in build_steps(AutomationConfig()) for command in step.commands) self.assertIn("--rsync-path 'sudo rsync'", commands) self.assertIn("sudo mkdir -p", commands) self.assertIn("sudo chmod -R g+rwX", commands) def test_local_mode_emits_local_commands_without_ssh(self): config = AutomationConfig(local=True) commands = "\n".join(command for step in build_steps(config) for command in step.commands) self.assertNotIn("ssh -i", commands) self.assertNotIn("rsync-path", commands) self.assertIn("reset_helpdesk_mail_settings.py --local", commands) self.assertIn("validate_test_instance.py --local", commands) self.assertNotIn("--composer-bin", commands) self.assertIn("redmine_outbox_worker.py --local --status", commands) self.assertIn("/opt/lanscratch/redmine-post-import/repo/plugins/redmine_event_outbox", commands) self.assertIn("/usr/share/redmine/plugins/", commands) self.assertIn("cd /usr/share/redmine && RAILS_ENV=production bundle exec rake redmine:plugins:migrate", commands) def test_local_semantic_check_is_non_blocking_without_staged_venv(self): config = AutomationConfig(local=True) semantic_step = next(step for step in build_steps(config) if step.name == "validate semantic index dry-run") command = semantic_step.commands[0] self.assertIn("test -x /opt/lanscratch/redmine-post-import/repo/.venv/bin/python", command) self.assertIn("semantic index runtime missing; skipping dry-run", command) self.assertIn("else", command) def test_status_paths_default_to_lanscratch(self): config = AutomationConfig() self.assertEqual(Path("/opt/lanscratch/redmine-post-import/status"), config.status_dir) def test_write_status_updates_latest_and_success_only_on_success(self): with tempfile.TemporaryDirectory() as tmp: config = AutomationConfig(status_dir=Path(tmp)) failed = write_status( config, run_id="20260428T010000Z", status="failed", results=[StepResult("preflight", "test -d missing", 1)], failed_step="preflight", ) self.assertTrue((Path(tmp) / "latest.json").exists()) self.assertTrue((Path(tmp) / "runs" / "20260428T010000Z.json").exists()) self.assertFalse((Path(tmp) / "latest-success.json").exists()) self.assertEqual("failed", failed["status"]) successful = write_status( config, run_id="20260428T010100Z", status="success", results=[StepResult("preflight", "test -d plugins", 0)], ) latest_success = json.loads((Path(tmp) / "latest-success.json").read_text()) self.assertEqual(successful["run_id"], latest_success["run_id"]) self.assertEqual("success", latest_success["status"]) if __name__ == "__main__": unittest.main()