feat(logging): unify canonical task CoT events
This commit is contained in:
@@ -38,6 +38,16 @@ def _make_task_context():
|
||||
return ctx
|
||||
|
||||
|
||||
def _make_logging_task_context():
|
||||
"""Create a context whose task logger records structured metadata calls."""
|
||||
ctx = MagicMock()
|
||||
ctx.logger.with_source.side_effect = lambda source: ctx.logger
|
||||
ctx.logger.info = MagicMock()
|
||||
ctx.logger.warning = MagicMock()
|
||||
ctx.logger.error = MagicMock()
|
||||
return ctx
|
||||
|
||||
|
||||
def _make_dashboard(dash_id=1, title="Test Dash"):
|
||||
return {"id": dash_id, "slug": f"slug-{dash_id}", "dashboard_title": title}
|
||||
|
||||
@@ -1220,6 +1230,7 @@ class TestMigrationPluginCompositeKeyFallback:
|
||||
|
||||
report = {"changed": 1, "unchanged": 0, "skipped_missing": 0, "failed": 0, "errors": []}
|
||||
mock_sync = AsyncMock(return_value=report)
|
||||
context = _make_logging_task_context()
|
||||
|
||||
with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
|
||||
patch('src.plugins.migration.SupersetClient') as MockSC, \
|
||||
@@ -1239,7 +1250,7 @@ class TestMigrationPluginCompositeKeyFallback:
|
||||
"replace_db_config": False,
|
||||
"sync_dataset_composite_keys": True,
|
||||
"composite_key_mutation_server": "target",
|
||||
})
|
||||
}, context=context)
|
||||
|
||||
assert result["status"] == "PARTIAL_SUCCESS"
|
||||
entry = result["failed_dashboards"][0]
|
||||
@@ -1248,6 +1259,68 @@ class TestMigrationPluginCompositeKeyFallback:
|
||||
assert entry["composite_key_mutation_server"] == "target"
|
||||
assert entry["composite_key_retried"] is True
|
||||
|
||||
recovery_calls = [
|
||||
call for call in context.logger.error.call_args_list
|
||||
if call.args and "Recovery retry import failed" in call.args[0]
|
||||
]
|
||||
assert recovery_calls
|
||||
recovery_metadata = recovery_calls[0].kwargs["extra"]
|
||||
assert recovery_metadata["dashboard_id"] == 1
|
||||
assert recovery_metadata["recovery_phase"] == "failed"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_composite_key_fallback_logs_full_recovery_timeline(self):
|
||||
"""Successful fallback emits task metadata for each user-visible recovery phase."""
|
||||
plugin = MigrationPlugin()
|
||||
src_env = _make_env("env-1", "Source")
|
||||
tgt_env = _make_env("env-2", "Target")
|
||||
mock_cm = MagicMock()
|
||||
mock_cm.get_environments.return_value = [src_env, tgt_env]
|
||||
mock_src_client = _make_mock_superset_client()
|
||||
mock_src_client.get_dashboards = AsyncMock(return_value=(True, [_make_dashboard(1, "Dash")]))
|
||||
mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
|
||||
mock_tgt_client = _make_mock_superset_client()
|
||||
mock_tgt_client.import_dashboard = AsyncMock(side_effect=[RuntimeError("Generic import boom"), None])
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.transform_zip.return_value = True
|
||||
mock_engine.read_dataset_contracts_from_zip.return_value = [
|
||||
{"uuid": "ds-1", "database_uuid": "tgt-db", "catalog": None, "schema": "public", "table_name": "users"}
|
||||
]
|
||||
report = {"changed": 1, "unchanged": 0, "skipped_missing": 0, "failed": 0, "errors": []}
|
||||
context = _make_logging_task_context()
|
||||
|
||||
with patch("src.plugins.migration.get_config_manager", return_value=mock_cm), \
|
||||
patch("src.plugins.migration.SupersetClient") as MockSC, \
|
||||
patch("src.plugins.migration.MigrationEngine", return_value=mock_engine), \
|
||||
patch("src.plugins.migration.create_temp_file") as mock_ctf, \
|
||||
patch("src.plugins.migration.sync_dataset_composite_keys", new=AsyncMock(return_value=report)), \
|
||||
patch("src.plugins.migration.IdMappingService", return_value=_make_mock_mapping_service()), \
|
||||
patch("src.plugins.migration.SessionLocal"):
|
||||
MockSC.side_effect = [mock_src_client, mock_tgt_client]
|
||||
mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")
|
||||
result = await plugin.execute({
|
||||
"source_env_id": "env-1",
|
||||
"target_env_id": "env-2",
|
||||
"selected_ids": [1],
|
||||
"replace_db_config": False,
|
||||
"sync_dataset_composite_keys": True,
|
||||
"composite_key_mutation_server": "target",
|
||||
}, context=context)
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
warning_metadata = context.logger.warning.call_args.kwargs["extra"]
|
||||
assert warning_metadata["recovery_phase"] == "started"
|
||||
assert warning_metadata["attempt"] == "initial"
|
||||
metadata_calls = [
|
||||
call.kwargs["extra"]
|
||||
for call in context.logger.info.call_args_list
|
||||
if "extra" in call.kwargs and "recovery_phase" in call.kwargs["extra"]
|
||||
]
|
||||
assert [metadata["recovery_phase"] for metadata in metadata_calls] == [
|
||||
"sync", "retry_import", "completed"
|
||||
]
|
||||
assert metadata_calls[1]["sync_changed"] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_composite_key_never_syncs_password_failure(self):
|
||||
"""Password-required failures never trigger the composite-key fallback."""
|
||||
@@ -1339,3 +1412,11 @@ class TestMigrationPluginCompositeKeyFallback:
|
||||
entry = result["failed_dashboards"][0]
|
||||
assert "composite_key_retried" not in entry
|
||||
# #endregion Test.MigrationPlugin
|
||||
# #region Test.Migration.RecoveryLogs [C:3] [TYPE Module] [SEMANTICS test,migration,recovery,logging]
|
||||
# @BRIEF Verify migration recovery emits structured, user-facing phase events.
|
||||
# @RELATION BINDS_TO -> [Plugin.Migration.Execute]
|
||||
# @TEST_CONTRACT: [InitialImportFailure + RecoveryResult] -> [RecoveryPhaseLogs]
|
||||
# @TEST_SCENARIO: recovery_success -> Initial failure is followed by sync, retry, and success events.
|
||||
# @TEST_EDGE: external_fail -> Recovery failure remains visible with dashboard context.
|
||||
# @TEST_INVARIANT: Per-dashboard export/transform/import failure never aborts the batch -> VERIFIED_BY: [recovery_success, external_fail]
|
||||
# #endregion Test.Migration.RecoveryLogs
|
||||
|
||||
Reference in New Issue
Block a user