diff --git a/main.py b/main.py index f6d894a6..32dce915 100644 --- a/main.py +++ b/main.py @@ -138,18 +138,73 @@ "runtime_recovery_not_active", "recovery_control_state_invalid", "recovery_active_binding_invalid", + "authority_source_incomplete", + "authority_file_digest_mismatch", + "authority_file_invalid", + "authority_file_unreadable", + "authority_file_changed", + "authority_json_invalid", + "authority_duplicate_field", + "authority_non_finite_value", + "authority_fields_invalid", + "authority_decision_not_approve", + "authority_scope_not_live", + "strategy_revision_unavailable", + "strategy_revision_mismatch", + "runner_revision_unavailable", + "runner_revision_mismatch", + "config_digest_mismatch", + "runtime_target_missing", + "runtime_target_identity_incomplete", + "runtime_target_identity_ambiguous", + "runtime_target_mismatch", + "authority_expired_or_not_effective", }) _RUNTIME_ERROR_NOTIFICATION_REASONS = { "recovery_control_read_failed": "runtime_error_reason_recovery_control_read_failed", "runtime_recovery_not_active": "runtime_error_reason_recovery_not_active", "recovery_control_state_invalid": "runtime_error_reason_recovery_control_state_invalid", "recovery_active_binding_invalid": "runtime_error_reason_recovery_active_binding_invalid", + "runner_revision_mismatch": "runtime_error_reason_runner_revision_mismatch", +} +_AUTHORITY_ERROR_REASON_CODES = { + "authority source parameters are incomplete": "authority_source_incomplete", + "authority file digest mismatch": "authority_file_digest_mismatch", + "authority file must be a regular non-symlink file": "authority_file_invalid", + "authority file size is invalid": "authority_file_invalid", + "authority file cannot be read": "authority_file_unreadable", + "authority file changed while reading": "authority_file_changed", + "invalid authority JSON": "authority_json_invalid", + "duplicate authority field": "authority_duplicate_field", + "non-finite JSON value": "authority_non_finite_value", + "authority fields are unsupported or incomplete": "authority_fields_invalid", + "authority decision is not APPROVE": "authority_decision_not_approve", + "authority scope is not LIVE": "authority_scope_not_live", + "installed strategy revision is unavailable": "strategy_revision_unavailable", + "strategy revision mismatch": "strategy_revision_mismatch", + "runner revision is unavailable": "runner_revision_unavailable", + "runner revision mismatch": "runner_revision_mismatch", + "runner checkout has tracked modifications": "runner_revision_unavailable", + "config digest mismatch": "config_digest_mismatch", + "runtime target is missing": "runtime_target_missing", + "runtime target identity is incomplete": "runtime_target_identity_incomplete", + "runtime target account identity is ambiguous": "runtime_target_identity_ambiguous", + "runtime target mismatch": "runtime_target_mismatch", + "authority expired or not yet effective": "authority_expired_or_not_effective", } def _runtime_setup_reason(exc): reason = str(exc) - return reason if reason in _SAFE_RUNTIME_SETUP_REASONS else "runtime_startup_failed" + if reason in _SAFE_RUNTIME_SETUP_REASONS: + return reason + authority_prefix = "live risk authority configuration invalid: " + if reason.startswith(authority_prefix): + authority_reason = reason[len(authority_prefix):] + authority_reason = _AUTHORITY_ERROR_REASON_CODES.get(authority_reason, authority_reason) + if authority_reason in _SAFE_RUNTIME_SETUP_REASONS: + return authority_reason + return "runtime_startup_failed" def _load_import_safe_strategy_runtime(): diff --git a/notify_i18n_support.py b/notify_i18n_support.py index a4f89891..d11c6b9c 100644 --- a/notify_i18n_support.py +++ b/notify_i18n_support.py @@ -21,6 +21,7 @@ "runtime_error_reason_recovery_not_active": "Recovery authorization is not active; trading did not start.", "runtime_error_reason_recovery_control_state_invalid": "Recovery configuration is invalid; trading did not start.", "runtime_error_reason_recovery_active_binding_invalid": "Recovery authorization binding is invalid; trading did not start.", + "runtime_error_reason_runner_revision_mismatch": "The approved runtime revision does not match this runner; trading did not start.", "runtime_error_reason_generic": "The runtime could not start safely; inspect the startup diagnostics.", "runtime_health_monitor_started": "Health monitor started", "runtime_setup_failed": "Strategy startup failed: runtime_setup_failed", @@ -162,6 +163,7 @@ "runtime_error_reason_recovery_not_active": "恢复授权尚未激活,本次未启动交易。", "runtime_error_reason_recovery_control_state_invalid": "恢复配置状态无效,本次未启动交易。", "runtime_error_reason_recovery_active_binding_invalid": "恢复授权绑定无效,本次未启动交易。", + "runtime_error_reason_runner_revision_mismatch": "当前运行版本未获得现有风险授权,本次未启动交易。", "runtime_error_reason_generic": "运行时无法安全启动,请查看启动诊断。", "runtime_health_monitor_started": "运行监测已启动", "runtime_setup_failed": "策略启动失败:runtime_setup_failed", diff --git a/tests/test_main_runtime_error_notification.py b/tests/test_main_runtime_error_notification.py index 5d87397c..7dd5d64b 100644 --- a/tests/test_main_runtime_error_notification.py +++ b/tests/test_main_runtime_error_notification.py @@ -140,6 +140,28 @@ def test_runtime_error_notification_uses_fixed_human_reason_text(self): self.assertIn("The runtime could not start safely", message) self.assertNotIn("untrusted-provider-detail", message) + def test_runtime_setup_reason_projects_safe_authority_reason(self): + self.assertEqual( + main._runtime_setup_reason( + ValueError("live risk authority configuration invalid: runner revision mismatch") + ), + "runner_revision_mismatch", + ) + self.assertEqual( + main._runtime_setup_reason( + ValueError("live risk authority configuration invalid: provider-secret-detail") + ), + "runtime_startup_failed", + ) + + def test_runtime_error_notification_uses_fixed_authority_reason_text(self): + with patch.dict(os.environ, {"NOTIFY_LANG": "zh-CN", "STRATEGY_PROFILE": "crypto_live_pool_rotation"}): + message = main._runtime_error_notification_message( + RuntimeError("runner_revision_mismatch") + ) + self.assertIn("当前运行版本未获得现有风险授权,本次未启动交易。", message) + self.assertNotIn("runner_revision_mismatch", message) + def test_main_reports_only_fixed_recovery_read_reason(self): observed = []