diff --git a/src/doberman/cli/main.py b/src/doberman/cli/main.py index 2cd2a2d..6869bb9 100644 --- a/src/doberman/cli/main.py +++ b/src/doberman/cli/main.py @@ -41,6 +41,7 @@ from doberman.demo import format_outcome_line, format_summary_table, run_demo from doberman.discovery.mcp_scan import MCP_CONFIG_FILES, scan_mcp_configs from doberman.discovery.scan import enumerate_capabilities, rate_capabilities, render_risk_map +from doberman.models import ActionType from doberman.policy.checklist import recommend_policy from doberman.policy.drift import ( _verify_possession_factor, @@ -84,6 +85,8 @@ def _ensure_encode_safe_stdio() -> None: _ensure_encode_safe_stdio() +_ACTION_WIDTH = max(len(action.value) for action in ActionType) + app = typer.Typer( help="Doberman - adaptive authorization layer for coding agents.", no_args_is_help=True, @@ -1325,7 +1328,7 @@ def log( reasons = ", ".join(json.loads(row["reason_codes_json"] or "[]")) or "-" auth = f"; auth={row['auth_result']}" if row["auth_result"] else "" typer.echo( - f"{row['ts']} {verdict_label_str(row['final_verdict'])} {row['action_type']:<13} " + f"{row['ts']} {verdict_label_str(row['final_verdict'])} {row['action_type']:<{_ACTION_WIDTH}} " f"{target} [{reasons}]{auth}" ) diff --git a/tests/unit/test_cli_log_jsonl.py b/tests/unit/test_cli_log_jsonl.py index e9c4d7d..64dcae0 100644 --- a/tests/unit/test_cli_log_jsonl.py +++ b/tests/unit/test_cli_log_jsonl.py @@ -197,3 +197,32 @@ async def _empty(*_a, **_k): result = runner.invoke(app, ["log", "--path", str(tmp_path)]) assert result.exit_code == 0 assert "(no decisions recorded yet)" in result.stdout + + +def test_log_human_columns_stay_aligned_for_long_action_types(tmp_path): + """Long action names must not shift the target and reason columns (#428).""" + import doberman.cli.main as main_mod + + rows = [ + {**_ROWS[0], "action_type": "git_op", "target_path_class": None}, + { + **_ROWS[0], + "id": 3, + "action_id": "act-3", + "action_type": "network_request", + "target_path_class": None, + }, + ] + + async def _rows(*_a, **_k): + return list(rows) + + with patch.object(main_mod, "read_decisions", _rows): + result = runner.invoke(app, ["log", "--path", str(tmp_path)]) + + assert result.exit_code == 0 + lines = [line for line in result.stdout.splitlines() if line.startswith("2026-")] + assert len(lines) == 2 + # The target is always the "-" placeholder, followed by two spaces and reasons. + target_offsets = {line.index(" - ") for line in lines} + assert len(target_offsets) == 1