220 lines
6.7 KiB
Python
220 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from wf_cli.app import app
|
|
from wf_cli.explain import (
|
|
DEFAULT_EXPLAIN_REGISTRY,
|
|
ExplainInputError,
|
|
extract_explain_codes,
|
|
parse_explain_input,
|
|
)
|
|
|
|
|
|
def test_explain_registry_returns_full_card_for_known_code() -> None:
|
|
card = DEFAULT_EXPLAIN_REGISTRY.get("source_missing")
|
|
|
|
assert card.code == "source_missing"
|
|
assert "source" in card.summary.lower()
|
|
assert card.why_it_happens
|
|
assert card.how_to_fix
|
|
assert card.related_docs
|
|
|
|
|
|
def test_explain_registry_list_is_lean() -> None:
|
|
entries = DEFAULT_EXPLAIN_REGISTRY.list_entries()
|
|
|
|
source_missing = next(entry for entry in entries if entry.code == "source_missing")
|
|
assert source_missing.code == "source_missing"
|
|
assert "source" in source_missing.summary.lower()
|
|
assert not hasattr(source_missing, "how_to_fix")
|
|
|
|
|
|
def test_parse_explain_input_accepts_direct_code() -> None:
|
|
assert parse_explain_input("source_missing") == ["source_missing"]
|
|
|
|
|
|
def test_parse_explain_input_extracts_error_code() -> None:
|
|
raw = '{"error": {"code": "deployment_unrunnable"}}'
|
|
|
|
assert parse_explain_input(raw) == ["deployment_unrunnable"]
|
|
|
|
|
|
def test_parse_explain_input_extracts_diagnostic_codes_in_order() -> None:
|
|
raw = """
|
|
{
|
|
"diagnostics": [
|
|
{"code": "source_missing"},
|
|
{"code": "schema_changed"},
|
|
{"code": "source_missing"}
|
|
]
|
|
}
|
|
"""
|
|
|
|
assert parse_explain_input(raw) == ["source_missing", "schema_changed"]
|
|
|
|
|
|
def test_extract_explain_codes_accepts_diagnostic_list() -> None:
|
|
value = [{"code": "binding_missing"}, {"code": "capability_missing"}]
|
|
|
|
assert extract_explain_codes(value) == ["binding_missing", "capability_missing"]
|
|
|
|
|
|
def test_parse_explain_input_rejects_json_without_codes() -> None:
|
|
with pytest.raises(ExplainInputError, match="no explainable code"):
|
|
parse_explain_input('{"status": "failed"}')
|
|
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
def test_wf_explain_code_outputs_full_json_card() -> None:
|
|
result = runner.invoke(app, ["explain", "source_missing"])
|
|
|
|
assert result.exit_code == 0
|
|
payload = json.loads(result.output)
|
|
assert payload["code"] == "source_missing"
|
|
assert payload["summary"]
|
|
assert payload["why_it_happens"]
|
|
assert payload["how_to_fix"]
|
|
|
|
|
|
def test_wf_explain_list_outputs_lean_json_index() -> None:
|
|
result = runner.invoke(app, ["explain", "--list"])
|
|
|
|
assert result.exit_code == 0
|
|
payload = json.loads(result.output)
|
|
first = payload["entries"][0]
|
|
assert first["code"]
|
|
assert first["summary"]
|
|
assert "how_to_fix" not in first
|
|
|
|
|
|
def test_wf_explain_markdown_format() -> None:
|
|
result = runner.invoke(app, ["explain", "source_missing", "--format", "markdown"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "# source_missing" in result.output
|
|
assert "## How To Fix" in result.output
|
|
|
|
|
|
def test_wf_explain_compact_format() -> None:
|
|
result = runner.invoke(app, ["explain", "source_missing", "--format", "compact"])
|
|
|
|
assert result.exit_code == 0
|
|
assert result.output.strip().startswith("source_missing: ")
|
|
|
|
|
|
def test_wf_explain_input_file_outputs_multiple_cards(tmp_path) -> None:
|
|
error_file = tmp_path / "error.json"
|
|
error_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"diagnostics": [
|
|
{"code": "source_missing"},
|
|
{"code": "schema_changed"},
|
|
{"code": "source_missing"},
|
|
]
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = runner.invoke(app, ["explain", "--input-file", str(error_file)])
|
|
|
|
assert result.exit_code == 0
|
|
payload = json.loads(result.output)
|
|
assert len(payload["entries"]) == 2
|
|
assert payload["entries"][0]["code"] == "source_missing"
|
|
assert payload["entries"][1]["code"] == "schema_changed"
|
|
|
|
|
|
def test_wf_explain_stdin_outputs_multiple_cards() -> None:
|
|
result = runner.invoke(
|
|
app,
|
|
["explain", "--stdin"],
|
|
input='{"error": {"code": "deployment_unrunnable"}}',
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
payload = json.loads(result.output)
|
|
assert payload["entries"][0]["code"] == "deployment_unrunnable"
|
|
|
|
|
|
def test_wf_explain_unknown_code_fails_clearly() -> None:
|
|
result = runner.invoke(app, ["explain", "not_a_real_code"])
|
|
|
|
assert result.exit_code != 0
|
|
assert "not_a_real_code" in result.output
|
|
|
|
|
|
def test_wf_explain_list_rejects_other_input_modes() -> None:
|
|
result = runner.invoke(app, ["explain", "source_missing", "--list"])
|
|
|
|
assert result.exit_code != 0
|
|
assert "--list cannot be combined" in result.output
|
|
|
|
|
|
def test_explain_related_docs_do_not_point_to_planning_artifacts() -> None:
|
|
for entry in DEFAULT_EXPLAIN_REGISTRY.list_full_entries():
|
|
for related_doc in entry.related_docs:
|
|
assert "docs/superpowers/" not in related_doc
|
|
|
|
|
|
def test_explain_registry_covers_core_validation_codes() -> None:
|
|
from wf_core.validation.issues import ValidationIssueCode
|
|
|
|
expected = {
|
|
ValidationIssueCode.INVALID_SOURCE_PATH.value,
|
|
ValidationIssueCode.INVALID_DESTINATION_PATH.value,
|
|
ValidationIssueCode.UNKNOWN_EDGE_DESTINATION.value,
|
|
ValidationIssueCode.UNDECLARED_EDGE_OUTCOME.value,
|
|
ValidationIssueCode.MISSING_OUTCOME_EDGE.value,
|
|
}
|
|
registry_codes = {
|
|
entry.code for entry in DEFAULT_EXPLAIN_REGISTRY.list_full_entries()
|
|
}
|
|
|
|
assert expected <= registry_codes
|
|
|
|
|
|
def test_explain_unknown_edge_destination_mentions_forward_route_repair() -> None:
|
|
card = DEFAULT_EXPLAIN_REGISTRY.get("unknown_edge_destination")
|
|
|
|
text = "\n".join(card.how_to_fix)
|
|
|
|
assert "wf draft handle" in text
|
|
assert "wf draft branch" in text
|
|
assert "target step first" in text.lower()
|
|
|
|
|
|
def test_explain_registry_uses_exported_draft_codes() -> None:
|
|
from wf_artifacts.draft_workspaces.api import REVISION_CONFLICT_CODE
|
|
from wf_artifacts.drafts.api import (
|
|
DRAFT_INVALID_CODE,
|
|
PATCH_INVALID_CODE,
|
|
UNKNOWN_OUTCOME_CODE,
|
|
)
|
|
|
|
registry_codes = {
|
|
entry.code for entry in DEFAULT_EXPLAIN_REGISTRY.list_full_entries()
|
|
}
|
|
|
|
assert DRAFT_INVALID_CODE in registry_codes
|
|
assert PATCH_INVALID_CODE in registry_codes
|
|
assert UNKNOWN_OUTCOME_CODE in registry_codes
|
|
assert REVISION_CONFLICT_CODE in registry_codes
|
|
|
|
|
|
def test_explain_related_doc_files_exist() -> None:
|
|
repo_root = Path(__file__).resolve().parents[2]
|
|
for entry in DEFAULT_EXPLAIN_REGISTRY.list_full_entries():
|
|
for related_doc in entry.related_docs:
|
|
path_text = related_doc.split("#", 1)[0]
|
|
if path_text.startswith("docs/"):
|
|
assert (repo_root / path_text).exists(), path_text
|