mirror of
https://github.com/hanzoai/insights.git
synced 2026-11-02 09:24:48 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
165 lines
6.3 KiB
Python
165 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Iterator
|
|
|
|
import pytest
|
|
from unittest.mock import patch
|
|
|
|
from click.testing import CliRunner
|
|
from insightscli_commands import ci_insights
|
|
|
|
|
|
@pytest.fixture
|
|
def runner() -> CliRunner:
|
|
return CliRunner()
|
|
|
|
|
|
@pytest.fixture
|
|
def ready() -> Iterator[None]:
|
|
"""Backend binary present and authenticated."""
|
|
with (
|
|
patch.object(ci_insights.shutil, "which", return_value="/usr/bin/mendral"),
|
|
patch.object(ci_insights, "_capture", return_value="Authenticated"),
|
|
):
|
|
yield
|
|
|
|
|
|
def test_missing_binary_reports_install_hint(runner: CliRunner) -> None:
|
|
with patch.object(ci_insights.shutil, "which", return_value=None):
|
|
result = runner.invoke(ci_insights.ci_insights, [])
|
|
assert result.exit_code != 0
|
|
assert "brew install mendral-ai/tap/mendral" in result.output
|
|
|
|
|
|
def test_unauthenticated_reports_login_hint(runner: CliRunner) -> None:
|
|
with (
|
|
patch.object(ci_insights.shutil, "which", return_value="/usr/bin/mendral"),
|
|
patch.object(ci_insights, "_capture", return_value="Not authenticated. Run 'mendral auth login'."),
|
|
):
|
|
result = runner.invoke(ci_insights.ci_insights, [])
|
|
assert result.exit_code != 0
|
|
assert "auth login" in result.output
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"argv, expected",
|
|
[
|
|
([], ("mendral", "here")),
|
|
(["search", "flaky timeout"], ("mendral", "insight", "search", "flaky timeout")),
|
|
(["view", "01ABC"], ("mendral", "insight", "view", "01ABC")),
|
|
(["view", "01ABC", "--json"], ("mendral", "insight", "view", "01ABC", "--json")),
|
|
],
|
|
)
|
|
def test_verb_maps_to_backend_call(runner: CliRunner, ready: None, argv: list[str], expected: tuple[str, ...]) -> None:
|
|
with patch.object(ci_insights, "_run", return_value=0) as run:
|
|
result = runner.invoke(ci_insights.ci_insights, argv)
|
|
assert result.exit_code == 0
|
|
run.assert_called_once_with(*expected)
|
|
|
|
|
|
@pytest.mark.parametrize("argv", [[], ["search", "x"], ["view", "01ABC"]])
|
|
def test_backend_exit_code_propagates(runner: CliRunner, ready: None, argv: list[str]) -> None:
|
|
with patch.object(ci_insights, "_run", return_value=7):
|
|
result = runner.invoke(ci_insights.ci_insights, argv)
|
|
assert result.exit_code == 7
|
|
|
|
|
|
@pytest.mark.parametrize("argv", [["--help"], ["search", "--help"], ["view", "--help"], ["plan", "--help"]])
|
|
def test_help_works_without_backend(runner: CliRunner, argv: list[str]) -> None:
|
|
with patch.object(ci_insights.shutil, "which", return_value=None):
|
|
result = runner.invoke(ci_insights.ci_insights, argv)
|
|
assert result.exit_code == 0
|
|
assert "Usage:" in result.output
|
|
assert "not found" not in result.output
|
|
|
|
|
|
_INSIGHT = {
|
|
"actions": [
|
|
{"id": "a1", "recommended": True, "status": "merged", "title": "Old fix", "full_plan": "merged plan"},
|
|
{"id": "a2", "recommended": True, "status": "proposed", "title": "New fix", "full_plan": "do X then Y"},
|
|
]
|
|
}
|
|
|
|
|
|
def test_plan_prints_actionable_recommended_plan(runner: CliRunner) -> None:
|
|
with (
|
|
patch.object(ci_insights.shutil, "which", return_value="/usr/bin/mendral"),
|
|
patch.object(ci_insights, "_capture", side_effect=["Authenticated", json.dumps(_INSIGHT)]),
|
|
):
|
|
result = runner.invoke(ci_insights.ci_insights, ["plan", "01XYZ"])
|
|
assert result.exit_code == 0
|
|
assert "New fix" in result.output
|
|
assert "do X then Y" in result.output
|
|
|
|
|
|
def test_plan_errors_when_no_actions(runner: CliRunner) -> None:
|
|
with (
|
|
patch.object(ci_insights.shutil, "which", return_value="/usr/bin/mendral"),
|
|
patch.object(ci_insights, "_capture", side_effect=["Authenticated", json.dumps({"actions": []})]),
|
|
):
|
|
result = runner.invoke(ci_insights.ci_insights, ["plan", "01XYZ"])
|
|
assert result.exit_code != 0
|
|
assert "No remediation plan" in result.output
|
|
|
|
|
|
@pytest.mark.parametrize("payload", ["", "not json{", '{"actions": [null]}'])
|
|
def test_plan_reports_clean_error_on_bad_response(runner: CliRunner, payload: str) -> None:
|
|
with (
|
|
patch.object(ci_insights.shutil, "which", return_value="/usr/bin/mendral"),
|
|
patch.object(ci_insights, "_capture", side_effect=["Authenticated", payload]),
|
|
):
|
|
result = runner.invoke(ci_insights.ci_insights, ["plan", "01XYZ"])
|
|
assert result.exit_code != 0
|
|
assert "Traceback" not in result.output
|
|
|
|
|
|
def test_plan_errors_when_recommended_action_has_no_plan_text(runner: CliRunner) -> None:
|
|
insight = {"actions": [{"id": "a", "recommended": True, "status": "proposed", "full_plan": ""}]}
|
|
with (
|
|
patch.object(ci_insights.shutil, "which", return_value="/usr/bin/mendral"),
|
|
patch.object(ci_insights, "_capture", side_effect=["Authenticated", json.dumps(insight)]),
|
|
):
|
|
result = runner.invoke(ci_insights.ci_insights, ["plan", "01XYZ"])
|
|
assert result.exit_code != 0
|
|
assert "no plan text" in result.output
|
|
|
|
|
|
def test_plan_warns_when_only_fix_is_merged(runner: CliRunner) -> None:
|
|
insight = {"actions": [{"id": "a", "recommended": True, "status": "merged", "title": "X", "full_plan": "the plan"}]}
|
|
with (
|
|
patch.object(ci_insights.shutil, "which", return_value="/usr/bin/mendral"),
|
|
patch.object(ci_insights, "_capture", side_effect=["Authenticated", json.dumps(insight)]),
|
|
):
|
|
result = runner.invoke(ci_insights.ci_insights, ["plan", "01XYZ"])
|
|
assert result.exit_code == 0
|
|
assert "already been merged" in result.output
|
|
assert "the plan" in result.output
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"actions, expected_id",
|
|
[
|
|
([], None),
|
|
([{"id": "a", "status": "merged"}], "a"),
|
|
(
|
|
[
|
|
{"id": "a", "recommended": True, "status": "merged"},
|
|
{"id": "b", "recommended": True, "status": "proposed"},
|
|
],
|
|
"b",
|
|
),
|
|
([{"id": "a", "recommended": True, "status": "merged"}], "a"),
|
|
(
|
|
[
|
|
{"id": "a", "recommended": False, "status": "proposed"},
|
|
{"id": "b", "recommended": True, "status": "rejected"},
|
|
],
|
|
"b",
|
|
),
|
|
],
|
|
)
|
|
def test_recommended_action_selection(actions: list[dict[str, object]], expected_id: str | None) -> None:
|
|
chosen = ci_insights._recommended_action(actions)
|
|
assert (chosen or {}).get("id") == expected_id
|