"""Step definitions for db_cli_coverage.feature. These steps target specific uncovered lines in cleveragents/cli/commands/db.py: - Lines 60-64: migrate() — imports alembic, gets runner, calls revision, echoes - Lines 85, 87: upgrade() with non-head revision — imports alembic, calls upgrade - Line 94: upgrade() with non-rich format — echoes format_output - Line 123: downgrade() with non-rich format — echoes format_output - Line 148: current() with non-rich format — echoes format_output - Lines 156, 158, 160, 161: history() with rich format - Lines 163, 165-169: history() with non-rich format — ScriptDirectory walk """ from types import SimpleNamespace from unittest.mock import MagicMock, patch import alembic.command import alembic.script from behave import given, then, when import cleveragents.cli.commands.db as db_module from cleveragents.cli.commands.db import ( current, downgrade, history, migrate, upgrade, ) # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the db commands module is imported") def step_db_module_imported(context): """Ensure the db module is importable.""" assert db_module is not None # --------------------------------------------------------------------------- # Mock setup helpers # --------------------------------------------------------------------------- def _make_mock_runner(current_revision="abc123def", pending=None): """Build a mock MigrationRunner with configurable behaviour.""" runner = MagicMock() runner.get_current_revision.return_value = current_revision runner.get_pending_migrations.return_value = pending or [] runner.alembic_cfg = MagicMock(name="alembic_cfg") runner.init_or_upgrade = MagicMock() return runner def _install_runner_and_echo(context, mock_runner): """Shared helper: patch _get_runner and typer.echo, register cleanups.""" context.mock_runner = mock_runner patcher1 = patch.object(db_module, "_get_runner", return_value=context.mock_runner) patcher1.start() context.add_cleanup(patcher1.stop) context.echo_calls = [] patcher2 = patch.object( db_module.typer, "echo", side_effect=lambda msg: context.echo_calls.append(msg) ) patcher2.start() context.add_cleanup(patcher2.stop) @given("the migration runner is mocked") def step_mock_runner(context): _install_runner_and_echo(context, _make_mock_runner()) @given("the migration runner is mocked with pending migrations") def step_mock_runner_with_pending(context): _install_runner_and_echo( context, _make_mock_runner(current_revision="rev001", pending=["rev002", "rev003"]), ) @given("the migration runner is mocked with no current revision") def step_mock_runner_no_revision(context): _install_runner_and_echo( context, _make_mock_runner(current_revision=None, pending=["rev001"]), ) @given("alembic command module is mocked") def step_mock_alembic_command(context): """Mock alembic.command functions in-place.""" p1 = patch.object(alembic.command, "revision", MagicMock()) p2 = patch.object(alembic.command, "upgrade", MagicMock()) p3 = patch.object(alembic.command, "downgrade", MagicMock()) p4 = patch.object(alembic.command, "history", MagicMock()) context.mock_alembic_revision = p1.start() context.mock_alembic_upgrade = p2.start() context.mock_alembic_downgrade = p3.start() context.mock_alembic_history = p4.start() context.add_cleanup(p1.stop) context.add_cleanup(p2.stop) context.add_cleanup(p3.stop) context.add_cleanup(p4.stop) @given("format_output is mocked") def step_mock_format_output(context): context.mock_format_output = MagicMock(return_value="") p = patch.object(db_module, "format_output", context.mock_format_output) p.start() context.add_cleanup(p.stop) @given("alembic ScriptDirectory is mocked with sample revisions") def step_mock_script_directory(context): rev1 = SimpleNamespace(revision="rev001", down_revision=None, doc="Initial schema") rev2 = SimpleNamespace(revision="rev002", down_revision="rev001", doc="Add users") rev3 = SimpleNamespace(revision="rev003", down_revision="rev002", doc="") mock_script = MagicMock() mock_script.walk_revisions.return_value = [rev3, rev2, rev1] mock_cls = MagicMock() mock_cls.from_config.return_value = mock_script context.mock_script_directory = mock_cls p = patch.object(alembic.script, "ScriptDirectory", mock_cls) p.start() context.add_cleanup(p.stop) @given('the db command parameters are revision "{revision}" and format "{fmt}"') def step_set_db_params(context, revision, fmt): context.db_revision = revision context.db_fmt = fmt @given('the db command format is "{fmt}"') def step_set_db_format(context, fmt): context.db_fmt = fmt # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @when('I invoke the db migrate command with message "{message}"') def step_invoke_migrate_with_message(context, message): migrate(message=message, fmt="rich") @when("I invoke the db migrate command with default message") def step_invoke_migrate_default(context): migrate(fmt="rich") @when("I invoke the db upgrade command with stored params") def step_invoke_upgrade_stored(context): upgrade(revision=context.db_revision, fmt=context.db_fmt) @when("I invoke the db downgrade command with stored params") def step_invoke_downgrade_stored(context): downgrade(revision=context.db_revision, fmt=context.db_fmt) @when("I invoke the db current command in rich format") def step_invoke_current_rich(context): current(fmt="rich") @when("I invoke the db current command with stored format") def step_invoke_current_stored(context): current(fmt=context.db_fmt) @when("I invoke the db history command in rich format") def step_invoke_history_rich(context): history(fmt="rich") @when("I invoke the db history command with stored format") def step_invoke_history_stored(context): history(fmt=context.db_fmt) # --------------------------------------------------------------------------- # Then steps # --------------------------------------------------------------------------- @then( 'alembic revision should have been called with message "{message}" and autogenerate True' ) def step_verify_alembic_revision(context, message): context.mock_alembic_revision.assert_called_once_with( context.mock_runner.alembic_cfg, message=message, autogenerate=True, ) @then('alembic upgrade should have been called with revision "{revision}"') def step_verify_alembic_upgrade(context, revision): context.mock_alembic_upgrade.assert_called_once_with( context.mock_runner.alembic_cfg, revision, ) @then("the runner init_or_upgrade should have been called") def step_verify_init_or_upgrade(context): context.mock_runner.init_or_upgrade.assert_called_once() @then("the runner get_current_revision should have been called") def step_verify_get_current_revision(context): context.mock_runner.get_current_revision.assert_called() @then('alembic downgrade should have been called with revision "{revision}"') def step_verify_alembic_downgrade(context, revision): context.mock_alembic_downgrade.assert_called_once_with( context.mock_runner.alembic_cfg, revision, ) @then("alembic history should have been called with the runner config") def step_verify_alembic_history(context): context.mock_alembic_history.assert_called_once_with( context.mock_runner.alembic_cfg, ) @then('typer should echo a db message containing "{substring}"') def step_verify_echo_contains(context, substring): assert any(substring in str(msg) for msg in context.echo_calls), ( f"Expected echo containing '{substring}', got: {context.echo_calls}" ) @then("format_output should have been called with status ok data") def step_verify_format_output_status_ok(context): context.mock_format_output.assert_called_once() data = context.mock_format_output.call_args[0][0] assert data["status"] == "ok", f"Expected status 'ok', got {data}" assert "current_revision" in data @then("the formatted output should have been echoed") def step_verify_formatted_echoed(context): assert "" in context.echo_calls, ( f"Expected '' in echo calls, got: {context.echo_calls}" ) @then("format_output should have been called with current revision data") def step_verify_format_output_current(context): context.mock_format_output.assert_called_once() data = context.mock_format_output.call_args[0][0] assert "current_revision" in data assert "pending_count" in data assert "pending_revisions" in data @then("format_output should have been called with revision list data") def step_verify_format_output_revisions(context): context.mock_format_output.assert_called_once() data = context.mock_format_output.call_args[0][0] assert isinstance(data, list), f"Expected list, got {type(data)}" assert len(data) == 3 assert data[0]["revision"] == "rev003" assert data[1]["revision"] == "rev002" assert data[2]["revision"] == "rev001" for item in data: assert "revision" in item assert "down_revision" in item assert "description" in item