fix(alembic): handle fileConfig parsing errors with user-friendly diagnostic #8288

Merged
HAL9000 merged 3 commits from bugfix/m3-error-handling-fileconfig-unhandled-exception into master 2026-06-02 07:57:28 +00:00
5 changed files with 232 additions and 2 deletions
+7 -1
View File
@@ -165,6 +165,12 @@ ensuring data is stored with proper parameter values.
### Fixed
- **fileConfig error handling in alembic env.py** (#7874): Wrapped the `fileConfig()`
call in `alembic/env.py` with a `try/except` block that catches malformed INI logging
configuration and emits a clear, user-actionable error message to stderr (including the
config file path and guidance on the `[loggers]` section) before exiting with code 1.
Includes Behave scenarios for the error-handling logic.
- **`agents project context show` JSON/YAML output** (#6323): Fixed structured output to include spec-required envelope (`command`, `status`, `exit_code`, `data`, `timing`, `messages`) and all four data sections (`context_policy`, `limits`, `summarization`, `current_usage`). Rich output now renders four panels including the new `Current Usage` panel.
### Added
@@ -1132,4 +1138,4 @@ iteration` and data corruption under concurrent plan execution. All public
- **TUI -- Permission Question Widget**: A new inline `PermissionQuestionWidget`
renders permission requests directly in the conversation stream for single-key
operations. Users can allow/reject with single-key shortcuts (`a`/`A`/`r`/`R`),
navigate with arrow keys, confirm with `Enter`, or press `v` to open the full
navigate with arrow keys, confirm with `Enter`, or press `v` to open the full
+1
View File
@@ -58,3 +58,4 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the config-actor combined-format support fix (PR #11232 / issue #11189): added ``_detect_nested_config_actor()``, ``_flatten_config_actor()``, and handling in ``ActorConfiguration.from_blob()`` to transparently flatten the nested ``config.actor`` block from both compact-string and nested-dict forms so v3 detection, schema validation, and canonicalisation see flat data — eliminating the ``"provider is required"`` crash.
* HAL 9000 has contributed the actor compiler `actor_ref` field fix (issue #1429): corrected `_map_node()` and `compile_actor()` in `src/cleveragents/actor/compiler.py` to read `actor_ref` from the top-level `NodeDefinition.actor_ref` field instead of `node.config.get("actor_ref")`, resolving silent failures on all SUBGRAPH nodes where `subgraph_refs` was always empty and `NodeConfig.subgraph` was always `None`.
* HAL 9000 has contributed the removal of the unsupported executable resource type (PR #3248 / issue #3077): removed `executable` from `LSP_RESOURCE_TYPES` and `BUILTIN_TYPE_NAMES`, updated `agents resource list` CLI table columns to the spec-required `[Name, ID, Type, Phys/Virt, Children, Projects]`, deleted orphaned `examples/resource-types/executable.yaml`, and updated related BDD test coverage.
* HAL 9000 has contributed the alembic fileConfig error handling fix (PR #8288 / issue #7874): wrapped the `fileConfig()` call in `alembic/env.py` with a `try/except` block to catch malformed INI logging configuration and emit clear, actionable error messages to stderr.
@@ -0,0 +1,164 @@
"""Step definitions for alembic fileConfig error handling (issue #7874).
Each scenario uses its own isolated temp directory, preventing cross-test
contamination. The malformed / valid INI files are written into those dirs
and the ``alembic`` CLI invocation is executed from within them so that
``fileConfig()`` reads directly from the target file under test.
"""
from __future__ import annotations
import os
import subprocess
import tempfile
from pathlib import Path
from typing import Any
from behave import given, then, when
# Absolute path to the project's alembic migrations package so that the
# subprocess invocations execute the real env.py (not a stub), ensuring the
# production fileConfig() error-handling path is covered by these scenarios.
_MIGRATIONS_DIR = str(
Path(__file__).resolve().parent.parent.parent
/ "src"
/ "cleveragents"
/ "infrastructure"
/ "database"
/ "migrations"
)
def _write_malformed_ini(directory: Path) -> Path:
"""Write a deliberately malformed alembic.ini to *directory*."""
ini = directory / "alembic.ini"
ini.write_text(
"[alembic]\n"
f"script_location = {_MIGRATIONS_DIR}\n"
"\n"
# Reference a logger that does NOT exist anywhere. fileConfig()
# raises ``KeyError`` at startup when the referenced key is absent
# from the INI file, producing an unhandled exception before any
# migration runs.
"[loggers]\n"
"keys = root,invalid_nonexistent_logger\n"
"\n",
)
return ini
def _write_valid_ini(directory: Path) -> Path:
"""Write a fully-valid alembic.ini to *directory*."""
ini = directory / "alembic.ini"
ini.write_text(
"[alembic]\n"
f"script_location = {_MIGRATIONS_DIR}\n"
"\n"
"[loggers]\n"
"keys = root\n"
"\n"
"[handlers]\n"
"keys = stream_handler\n"
"\n"
"[formatters]\n"
"keys = simple\n"
"\n"
"[logger_root]\n"
"level = WARN\n"
"handlers = stream_handler\n"
"\n"
"[handler_stream_handler]\n"
"class = StreamHandler\n"
"args = (sys.stderr,)\n"
"\n"
"[formatter_simple]\n"
"format = %(levelname)s:%(name)s:%(message)s\n",
)
return ini
@given("a fresh temporary directory")
def step_given_fresh_temp_dir(context: Any) -> None:
context.test_dir = Path(tempfile.mkdtemp(prefix="fileconfig_test_"))
@when(
"the alembic.ini contains an invalid logger section"
" that references nonexistent loggers"
)
def step_when_invalid_log_section(context: Any) -> None:
"""Write a malformed alembic.ini to the test directory."""
_write_malformed_ini(context.test_dir)
@when("the alembic.ini contains a malformed logging configuration")
def step_when_malformed_config(context: Any) -> None:
"""Alias for when steps that write a malformed config."""
_write_malformed_ini(context.test_dir)
@when("migrations are invoked normally")
def step_when_invocations_normal(context: Any) -> None:
"""Invoke ``alembic current`` to trigger env.py startup (fileConfig)."""
env = {
**os.environ,
"CLEVERAGENTS_DATABASE_URL": f"sqlite:///{context.test_dir}/test.db",
}
result = subprocess.run(
["alembic", "current"],
cwd=context.test_dir,
capture_output=True,
text=True,
timeout=10,
env=env,
)
context.migration_result = result
@when("the alembic.ini contains a valid logger configuration")
def step_when_valid_config(context: Any) -> None:
"""Write a fully-valid alembic.ini to the test directory."""
_write_valid_ini(context.test_dir)
@then(
'the error output on stderr includes actionable guidance about "[loggers]" section'
)
def step_then_stderr_includes_guidance(context: Any) -> None:
"""Assert that the error message contains ``[loggers]`` guidance."""
result = _assert_invocation(context)
assert result.returncode != 0, (
f"Expected non-zero exit code on configuration error. Got: {result.returncode}"
)
stderr = result.stderr
assert "[loggers]" in stderr or "loggers" in stderr.lower(), (
f"Error must be user-actionable and mention [loggers]. Got:\n{stderr}"
)
@then('the error output on stderr includes the config file path "alembic.ini"')
def step_then_stderr_includes_alembic_ini(context: Any) -> None:
"""Assert that the error message contains the config file path."""
result = _assert_invocation(context)
stderr = result.stderr
assert "alembic.ini" in stderr, f"Error must include 'alembic.ini'. Got:\n{stderr}"
@then("no error message appears on stderr")
def step_then_no_stderr_error(context: Any) -> None:
"""Assert that the alembic invocation produced no stderr output."""
result = _assert_invocation(context)
assert not result.stderr.strip(), (
f"Unexpected stderr when config is valid:\n{result.stderr}"
)
def _assert_invocation(context: Any) -> subprocess.CompletedProcess[str]:
"""Utility: assert that a migration invocation has been recorded."""
if not hasattr(context, "migration_result"):
msg = (
"No migration result recorded. Did all required *When* steps execute? "
"The test flow must be: Given … → When … → When … → Then …"
)
raise AssertionError(msg)
return context.migration_result
@@ -0,0 +1,42 @@
@tdd_issue @tdd_issue_7874
Feature: Alembic fileConfig error handling (issue #7874)
As an operator running Alembic migrations, I want to receive a clear,
actionable error message when alembic.ini contains malformed logging
configuration, so that I can diagnose and fix the issue without being
confronted by an unhandled traceback.
Background:
Given a fresh temporary directory
Scenario Outline: fileConfig failure exits with non-zero exit code
When the alembic.ini contains an invalid logger section that references nonexistent loggers
And migrations are invoked normally
Then the error output on stderr includes actionable guidance about "[loggers]" section
Examples: malformed configurations
| description |
| references undefined logger key |
Scenario Outline: fileConfig failure message includes the config file path
When the alembic.ini contains a malformed logging configuration
And migrations are invoked normally
Then the error output on stderr includes the config file path "alembic.ini"
Examples: malformed configurations
| description |
| references undefined logger key |
Scenario Outline: fileConfig failure message is user-actionable
When the alembic.ini contains a malformed logging configuration
And migrations are invoked normally
Then the error output on stderr includes actionable guidance about "[loggers]" section
Examples: malformed configurations
| description |
| references undefined logger key |
Scenario: fileConfig succeeds silently when INI file is valid
When the alembic.ini contains a valid logger configuration
And migrations are invoked normally
Then no error message appears on stderr
@@ -1,5 +1,6 @@
import logging
import os
import sys
from logging.config import fileConfig
from pathlib import Path
@@ -27,7 +28,23 @@ if config is not None:
# application loggers that were created before this point, which causes
# test failures when log capture handlers are attached to those loggers.
if config.config_file_name is not None:
fileConfig(config.config_file_name, disable_existing_loggers=False)
try:
fileConfig(config.config_file_name, disable_existing_loggers=False)
except Exception as exc:
# fileConfig() can raise configparser.Error, KeyError, ValueError, or
# other exceptions when the INI file is malformed or contains an invalid
# logging configuration section. Per the project's "fail fast" philosophy
# (CONTRIBUTING.md), we add context (file path, nature of error) and
# re-raise so the operator receives a clear, actionable diagnostic rather
# than a raw traceback with no indication of which file caused the failure.
config_path = config.config_file_name
print(
f"ERROR: Failed to configure logging from '{config_path}': {exc}\n"
f"Check that '{config_path}' contains a valid [loggers] section "
f"and that all referenced handlers and formatters are defined.",
file=sys.stderr,
)
raise SystemExit(1) from exc
# Override the database URL from environment or use default
# This allows flexible configuration based on deployment