test: add 18 self-heal coverage tests for flow close_ops (codecov patch)
This commit is contained in:
@@ -24,6 +24,18 @@ def _import_close_all_plans_impl():
|
||||
return close_all_plans_impl
|
||||
|
||||
|
||||
def _import_find_unregistered():
|
||||
from aipass.flow.apps.handlers.plan.close_ops import _find_unregistered_plan_file
|
||||
|
||||
return _find_unregistered_plan_file
|
||||
|
||||
|
||||
def _import_self_heal():
|
||||
from aipass.flow.apps.handlers.plan.close_ops import _self_heal_unregistered_plan
|
||||
|
||||
return _self_heal_unregistered_plan
|
||||
|
||||
|
||||
def _make_deps(**overrides) -> dict:
|
||||
"""Build a full set of injected dependency kwargs with MagicMock defaults."""
|
||||
deps = {
|
||||
@@ -453,3 +465,465 @@ class TestCloseAllException:
|
||||
assert result["success"] is False
|
||||
assert result["total"] == 0
|
||||
assert any("Error" in m.get("text", "") for m in result["messages"])
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 4. _find_unregistered_plan_file
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestFindUnregisteredPlanFile:
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.FLOW_ROOT")
|
||||
def test_finds_matching_file(self, mock_flow_root, tmp_path):
|
||||
find_fn = _import_find_unregistered()
|
||||
mock_flow_root.parent = tmp_path
|
||||
branch_dir = tmp_path / "somebranch"
|
||||
branch_dir.mkdir()
|
||||
plan_file = branch_dir / "DPLAN-0176_design_topic_2026-05-10.md"
|
||||
plan_file.write_text("# Plan", encoding="utf-8")
|
||||
|
||||
result = find_fn("DPLAN", "0176")
|
||||
assert result is not None
|
||||
assert result.name == "DPLAN-0176_design_topic_2026-05-10.md"
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.FLOW_ROOT")
|
||||
def test_skips_backup_directory(self, mock_flow_root, tmp_path):
|
||||
find_fn = _import_find_unregistered()
|
||||
mock_flow_root.parent = tmp_path
|
||||
backup_dir = tmp_path / ".backup" / "processed_plans"
|
||||
backup_dir.mkdir(parents=True)
|
||||
(backup_dir / "FPLAN-0099_old_2026-01-01.md").write_text("# Old", encoding="utf-8")
|
||||
|
||||
result = find_fn("FPLAN", "0099")
|
||||
assert result is None
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.FLOW_ROOT")
|
||||
def test_skips_archive_directory(self, mock_flow_root, tmp_path):
|
||||
find_fn = _import_find_unregistered()
|
||||
mock_flow_root.parent = tmp_path
|
||||
archive_dir = tmp_path / "branch" / ".archive"
|
||||
archive_dir.mkdir(parents=True)
|
||||
(archive_dir / "FPLAN-0050_archived_2026-02-01.md").write_text("# Archived", encoding="utf-8")
|
||||
|
||||
result = find_fn("FPLAN", "0050")
|
||||
assert result is None
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.FLOW_ROOT")
|
||||
def test_skips_processed_plans_directory(self, mock_flow_root, tmp_path):
|
||||
find_fn = _import_find_unregistered()
|
||||
mock_flow_root.parent = tmp_path
|
||||
proc_dir = tmp_path / "processed_plans"
|
||||
proc_dir.mkdir()
|
||||
(proc_dir / "FPLAN-0077_done_2026-03-01.md").write_text("# Done", encoding="utf-8")
|
||||
|
||||
result = find_fn("FPLAN", "0077")
|
||||
assert result is None
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.FLOW_ROOT")
|
||||
def test_returns_none_when_no_match(self, mock_flow_root, tmp_path):
|
||||
find_fn = _import_find_unregistered()
|
||||
mock_flow_root.parent = tmp_path
|
||||
branch_dir = tmp_path / "branch"
|
||||
branch_dir.mkdir()
|
||||
(branch_dir / "FPLAN-0001_other_2026-01-01.md").write_text("# Other", encoding="utf-8")
|
||||
|
||||
result = find_fn("FPLAN", "9999")
|
||||
assert result is None
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.FLOW_ROOT")
|
||||
def test_skips_git_directory(self, mock_flow_root, tmp_path):
|
||||
find_fn = _import_find_unregistered()
|
||||
mock_flow_root.parent = tmp_path
|
||||
git_dir = tmp_path / ".git" / "refs"
|
||||
git_dir.mkdir(parents=True)
|
||||
(git_dir / "FPLAN-0010_gitfile_2026-01-01.md").write_text("# Git", encoding="utf-8")
|
||||
|
||||
result = find_fn("FPLAN", "0010")
|
||||
assert result is None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 5. _self_heal_unregistered_plan
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestSelfHealNoCollision:
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.discover_plan_types", create=True)
|
||||
def test_registers_with_original_key(self, _mock_discover, tmp_path):
|
||||
heal_fn = _import_self_heal()
|
||||
plan_file = tmp_path / "DPLAN-0176_design_topic_2026-05-10.md"
|
||||
plan_file.write_text("# Plan", encoding="utf-8")
|
||||
registry = {"plans": {}, "next_number": 175}
|
||||
save_fn = MagicMock()
|
||||
load_fn = MagicMock()
|
||||
messages = []
|
||||
|
||||
with patch(
|
||||
"aipass.flow.apps.handlers.plan.close_ops.discover_plan_types",
|
||||
return_value={},
|
||||
create=True,
|
||||
):
|
||||
actual_key, updated_reg = heal_fn(
|
||||
"DPLAN", "0176", plan_file, registry, "dplan_registry.json", save_fn, load_fn, messages
|
||||
)
|
||||
|
||||
assert actual_key == "0176"
|
||||
assert "0176" in updated_reg["plans"]
|
||||
assert updated_reg["plans"]["0176"]["self_healed"] is True
|
||||
assert updated_reg["plans"]["0176"]["status"] == "open"
|
||||
assert updated_reg["next_number"] == 177
|
||||
save_fn.assert_called_once_with(registry, registry_file="dplan_registry.json")
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.discover_plan_types", create=True)
|
||||
def test_extracts_subject_from_filename(self, _mock_discover, tmp_path):
|
||||
heal_fn = _import_self_heal()
|
||||
plan_file = tmp_path / "FPLAN-0042_my_great_feature_2026-04-01.md"
|
||||
plan_file.write_text("# Plan", encoding="utf-8")
|
||||
registry = {"plans": {}, "next_number": 40}
|
||||
messages = []
|
||||
|
||||
with patch(
|
||||
"aipass.flow.apps.handlers.plan.close_ops.discover_plan_types",
|
||||
return_value={},
|
||||
create=True,
|
||||
):
|
||||
actual_key, updated_reg = heal_fn(
|
||||
"FPLAN", "0042", plan_file, registry, "fplan_registry.json", MagicMock(), MagicMock(), messages
|
||||
)
|
||||
|
||||
assert updated_reg["plans"][actual_key]["subject"] == "my great feature"
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.discover_plan_types", create=True)
|
||||
def test_emits_self_heal_warning_message(self, _mock_discover, tmp_path):
|
||||
heal_fn = _import_self_heal()
|
||||
plan_file = tmp_path / "FPLAN-0001_test_2026-01-01.md"
|
||||
plan_file.write_text("# Plan", encoding="utf-8")
|
||||
registry = {"plans": {}, "next_number": 1}
|
||||
messages = []
|
||||
|
||||
with patch(
|
||||
"aipass.flow.apps.handlers.plan.close_ops.discover_plan_types",
|
||||
return_value={},
|
||||
create=True,
|
||||
):
|
||||
heal_fn("FPLAN", "0001", plan_file, registry, "fplan_registry.json", MagicMock(), MagicMock(), messages)
|
||||
|
||||
assert any("self-heal" in m.get("text", "").lower() for m in messages)
|
||||
assert any("Registered" in m.get("text", "") for m in messages)
|
||||
|
||||
|
||||
class TestSelfHealSamePrefixCollision:
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.discover_plan_types", create=True)
|
||||
def test_bumps_to_next_number(self, _mock_discover, tmp_path):
|
||||
heal_fn = _import_self_heal()
|
||||
plan_file = tmp_path / "FPLAN-0005_colliding_2026-03-01.md"
|
||||
plan_file.write_text("# Plan", encoding="utf-8")
|
||||
registry = {
|
||||
"plans": {"0005": {"status": "closed", "subject": "existing"}},
|
||||
"next_number": 10,
|
||||
}
|
||||
messages = []
|
||||
|
||||
with patch(
|
||||
"aipass.flow.apps.handlers.plan.close_ops.discover_plan_types",
|
||||
return_value={},
|
||||
create=True,
|
||||
):
|
||||
actual_key, updated_reg = heal_fn(
|
||||
"FPLAN", "0005", plan_file, registry, "fplan_registry.json", MagicMock(), MagicMock(), messages
|
||||
)
|
||||
|
||||
assert actual_key == "0010"
|
||||
assert "0010" in updated_reg["plans"]
|
||||
assert "0005" in updated_reg["plans"]
|
||||
assert updated_reg["next_number"] == 11
|
||||
assert any("Bumping" in m.get("text", "") for m in messages)
|
||||
|
||||
|
||||
class TestSelfHealCrossPrefixCollision:
|
||||
def test_notes_cross_prefix_collision(self, tmp_path):
|
||||
heal_fn = _import_self_heal()
|
||||
plan_file = tmp_path / "DPLAN-0013_design_2026-03-01.md"
|
||||
plan_file.write_text("# Plan", encoding="utf-8")
|
||||
registry = {"plans": {}, "next_number": 13}
|
||||
save_fn = MagicMock()
|
||||
other_registry = {"plans": {"0013": {"status": "closed", "subject": "fplan thing"}}}
|
||||
load_fn = MagicMock(return_value=other_registry)
|
||||
messages = []
|
||||
|
||||
with patch(
|
||||
"aipass.flow.apps.handlers.plan.close_ops.discover_plan_types",
|
||||
return_value={
|
||||
"flow_plans": {"prefix": "FPLAN", "registry_file": "fplan_registry.json"},
|
||||
"dev_plans": {"prefix": "DPLAN", "registry_file": "dplan_registry.json"},
|
||||
},
|
||||
create=True,
|
||||
):
|
||||
actual_key, updated_reg = heal_fn(
|
||||
"DPLAN", "0013", plan_file, registry, "dplan_registry.json", save_fn, load_fn, messages
|
||||
)
|
||||
|
||||
assert actual_key == "0013"
|
||||
assert any("FPLAN-0013 also exists" in m.get("text", "") for m in messages)
|
||||
|
||||
def test_skips_own_prefix_in_cross_check(self, tmp_path):
|
||||
heal_fn = _import_self_heal()
|
||||
plan_file = tmp_path / "FPLAN-0020_test_2026-03-01.md"
|
||||
plan_file.write_text("# Plan", encoding="utf-8")
|
||||
registry = {"plans": {}, "next_number": 20}
|
||||
load_fn = MagicMock()
|
||||
messages = []
|
||||
|
||||
with patch(
|
||||
"aipass.flow.apps.handlers.template.plan_type_loader.discover_plan_types",
|
||||
return_value={
|
||||
"flow_plans": {"prefix": "FPLAN", "registry_file": "fplan_registry.json"},
|
||||
},
|
||||
):
|
||||
heal_fn("FPLAN", "0020", plan_file, registry, "fplan_registry.json", MagicMock(), load_fn, messages)
|
||||
|
||||
load_fn.assert_not_called()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 6. close_plan_impl — self-heal integration path
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestClosePlanImplSelfHeal:
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops._resolve_registry_file", return_value="dplan_registry.json")
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.subprocess")
|
||||
def test_triggers_self_heal_when_not_in_registry(self, mock_subprocess, _mock_resolve, tmp_path):
|
||||
close_plan_impl = _import_close_plan_impl()
|
||||
|
||||
plan_file = tmp_path / "DPLAN-0176_design_topic_2026-05-10.md"
|
||||
plan_file.write_text("# Real content\nDesign notes here.", encoding="utf-8")
|
||||
|
||||
registry = {"plans": {}, "next_number": 175}
|
||||
deps = _make_deps()
|
||||
deps["load_registry"].return_value = registry
|
||||
deps["validate_plan_exists"].return_value = (False, "Plan 0176 not found")
|
||||
|
||||
with (
|
||||
patch(
|
||||
"aipass.flow.apps.handlers.plan.close_ops._find_unregistered_plan_file",
|
||||
return_value=plan_file,
|
||||
),
|
||||
patch(
|
||||
"aipass.flow.apps.handlers.plan.close_ops._self_heal_unregistered_plan",
|
||||
return_value=(
|
||||
"0176",
|
||||
{
|
||||
"plans": {
|
||||
"0176": {
|
||||
"status": "open",
|
||||
"subject": "design topic",
|
||||
"file_path": str(plan_file),
|
||||
"location": str(tmp_path),
|
||||
"self_healed": True,
|
||||
}
|
||||
},
|
||||
"next_number": 177,
|
||||
},
|
||||
),
|
||||
) as mock_heal,
|
||||
patch("aipass.flow.apps.handlers.mbank.process.archive_plan", return_value=True),
|
||||
patch("aipass.flow.apps.handlers.plan.close_ops.json_handler"),
|
||||
patch("aipass.flow.apps.handlers.plan.append_closed_plan.append_to_closed_plans", create=True),
|
||||
):
|
||||
result = close_plan_impl(plan_num="DPLAN-0176", **deps)
|
||||
|
||||
mock_heal.assert_called_once()
|
||||
assert result["success"] is True
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops._resolve_registry_file", return_value="fplan_registry.json")
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops._find_unregistered_plan_file", return_value=None)
|
||||
def test_returns_not_found_when_no_file_on_disk(self, _mock_find, _mock_resolve):
|
||||
close_plan_impl = _import_close_plan_impl()
|
||||
deps = _make_deps()
|
||||
deps["load_registry"].return_value = {"plans": {}}
|
||||
deps["validate_plan_exists"].return_value = (False, "Plan 9990 not found")
|
||||
|
||||
result = close_plan_impl(plan_num="FPLAN-9990", **deps)
|
||||
assert result["success"] is False
|
||||
assert result["messages"][0]["text"] == "not_found"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 7. [VERIFY] block — physical state checks after self-heal
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestSelfHealVerifyBlock:
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops._resolve_registry_file", return_value="fplan_registry.json")
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.subprocess")
|
||||
def test_verify_all_pass(self, mock_subprocess, _mock_resolve, tmp_path):
|
||||
close_plan_impl = _import_close_plan_impl()
|
||||
|
||||
plan_file = tmp_path / "FPLAN-0042_test_2026-03-01.md"
|
||||
plan_file.write_text("# Real content here.", encoding="utf-8")
|
||||
processed_dir = tmp_path / "processed"
|
||||
processed_dir.mkdir()
|
||||
dest_file = processed_dir / plan_file.name
|
||||
dest_file.write_text("# Archived", encoding="utf-8")
|
||||
|
||||
registry = {
|
||||
"plans": {
|
||||
"42": {
|
||||
"status": "open",
|
||||
"subject": "test",
|
||||
"file_path": str(plan_file),
|
||||
"location": str(tmp_path),
|
||||
"self_healed": True,
|
||||
}
|
||||
},
|
||||
"next_number": 43,
|
||||
}
|
||||
|
||||
deps = _make_deps()
|
||||
deps["load_registry"].return_value = registry
|
||||
deps["validate_plan_exists"].return_value = (True, None)
|
||||
|
||||
with (
|
||||
patch("aipass.flow.apps.handlers.mbank.process.archive_plan", return_value=True),
|
||||
patch(
|
||||
"aipass.flow.apps.handlers.mbank.process.PROCESSED_PLANS_DIR",
|
||||
processed_dir,
|
||||
),
|
||||
patch("aipass.flow.apps.handlers.plan.close_ops.json_handler"),
|
||||
patch("aipass.flow.apps.handlers.plan.append_closed_plan.append_to_closed_plans", create=True),
|
||||
):
|
||||
plan_file.unlink()
|
||||
result = close_plan_impl(plan_num="FPLAN-0042", **deps)
|
||||
|
||||
assert result["success"] is True
|
||||
verify_msgs = [
|
||||
m
|
||||
for m in result["messages"]
|
||||
if "[VERIFY]" in m.get("text", "") or "[OK]" in m.get("text", "") or "[FAIL]" in m.get("text", "")
|
||||
]
|
||||
assert len(verify_msgs) >= 1
|
||||
ok_msgs = [m for m in result["messages"] if "[OK]" in m.get("text", "")]
|
||||
assert len(ok_msgs) >= 2
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops._resolve_registry_file", return_value="fplan_registry.json")
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.subprocess")
|
||||
def test_verify_fails_when_file_not_in_processed(self, mock_subprocess, _mock_resolve, tmp_path):
|
||||
close_plan_impl = _import_close_plan_impl()
|
||||
|
||||
plan_file = tmp_path / "FPLAN-0055_missing_2026-03-01.md"
|
||||
plan_file.write_text("# Content", encoding="utf-8")
|
||||
processed_dir = tmp_path / "processed_empty"
|
||||
processed_dir.mkdir()
|
||||
|
||||
registry = {
|
||||
"plans": {
|
||||
"55": {
|
||||
"status": "open",
|
||||
"subject": "missing test",
|
||||
"file_path": str(plan_file),
|
||||
"location": str(tmp_path),
|
||||
"self_healed": True,
|
||||
}
|
||||
},
|
||||
"next_number": 56,
|
||||
}
|
||||
|
||||
deps = _make_deps()
|
||||
deps["load_registry"].return_value = registry
|
||||
deps["validate_plan_exists"].return_value = (True, None)
|
||||
|
||||
with (
|
||||
patch("aipass.flow.apps.handlers.mbank.process.archive_plan", return_value=True),
|
||||
patch(
|
||||
"aipass.flow.apps.handlers.mbank.process.PROCESSED_PLANS_DIR",
|
||||
processed_dir,
|
||||
),
|
||||
patch("aipass.flow.apps.handlers.plan.close_ops.json_handler"),
|
||||
patch("aipass.flow.apps.handlers.plan.append_closed_plan.append_to_closed_plans", create=True),
|
||||
):
|
||||
result = close_plan_impl(plan_num="FPLAN-0055", **deps)
|
||||
|
||||
assert result["success"] is True
|
||||
fail_msgs = [m for m in result["messages"] if "[FAIL]" in m.get("text", "")]
|
||||
assert len(fail_msgs) >= 1
|
||||
assert any("NOT found in processed_plans" in m.get("text", "") for m in fail_msgs)
|
||||
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops._resolve_registry_file", return_value="fplan_registry.json")
|
||||
@patch("aipass.flow.apps.handlers.plan.close_ops.subprocess")
|
||||
def test_verify_fails_when_source_still_exists(self, mock_subprocess, _mock_resolve, tmp_path):
|
||||
close_plan_impl = _import_close_plan_impl()
|
||||
|
||||
plan_file = tmp_path / "FPLAN-0060_leftover_2026-03-01.md"
|
||||
plan_file.write_text("# Content", encoding="utf-8")
|
||||
processed_dir = tmp_path / "processed"
|
||||
processed_dir.mkdir()
|
||||
(processed_dir / plan_file.name).write_text("# Archived copy", encoding="utf-8")
|
||||
|
||||
registry = {
|
||||
"plans": {
|
||||
"60": {
|
||||
"status": "open",
|
||||
"subject": "leftover test",
|
||||
"file_path": str(plan_file),
|
||||
"location": str(tmp_path),
|
||||
"self_healed": True,
|
||||
}
|
||||
},
|
||||
"next_number": 61,
|
||||
}
|
||||
|
||||
deps = _make_deps()
|
||||
deps["load_registry"].return_value = registry
|
||||
deps["validate_plan_exists"].return_value = (True, None)
|
||||
|
||||
with (
|
||||
patch("aipass.flow.apps.handlers.mbank.process.archive_plan", return_value=True),
|
||||
patch(
|
||||
"aipass.flow.apps.handlers.mbank.process.PROCESSED_PLANS_DIR",
|
||||
processed_dir,
|
||||
),
|
||||
patch("aipass.flow.apps.handlers.plan.close_ops.json_handler"),
|
||||
patch("aipass.flow.apps.handlers.plan.append_closed_plan.append_to_closed_plans", create=True),
|
||||
):
|
||||
result = close_plan_impl(plan_num="FPLAN-0060", **deps)
|
||||
|
||||
assert result["success"] is True
|
||||
fail_msgs = [m for m in result["messages"] if "[FAIL]" in m.get("text", "")]
|
||||
assert any("Source file still exists" in m.get("text", "") for m in fail_msgs)
|
||||
|
||||
def test_verify_skipped_for_non_self_healed_plans(self, tmp_path):
|
||||
close_plan_impl = _import_close_plan_impl()
|
||||
|
||||
plan_file = tmp_path / "FPLAN-0070_normal_2026-03-01.md"
|
||||
plan_file.write_text("# Normal plan content", encoding="utf-8")
|
||||
|
||||
registry = {
|
||||
"plans": {
|
||||
"70": {
|
||||
"status": "open",
|
||||
"subject": "normal plan",
|
||||
"file_path": str(plan_file),
|
||||
"location": str(tmp_path),
|
||||
}
|
||||
},
|
||||
"next_number": 71,
|
||||
}
|
||||
|
||||
deps = _make_deps()
|
||||
deps["load_registry"].return_value = registry
|
||||
deps["validate_plan_exists"].return_value = (True, None)
|
||||
|
||||
with (
|
||||
patch("aipass.flow.apps.handlers.plan.close_ops._resolve_registry_file", return_value=None),
|
||||
patch("aipass.flow.apps.handlers.plan.close_ops._find_plan_across_registries", return_value=None),
|
||||
patch("aipass.flow.apps.handlers.plan.close_ops.subprocess"),
|
||||
patch("aipass.flow.apps.handlers.mbank.process.archive_plan", return_value=True),
|
||||
patch("aipass.flow.apps.handlers.plan.close_ops.json_handler"),
|
||||
patch("aipass.flow.apps.handlers.plan.append_closed_plan.append_to_closed_plans", create=True),
|
||||
):
|
||||
result = close_plan_impl(plan_num="70", **deps)
|
||||
|
||||
assert result["success"] is True
|
||||
verify_msgs = [m for m in result["messages"] if "[VERIFY]" in m.get("text", "")]
|
||||
assert len(verify_msgs) == 0
|
||||
|
||||
Reference in New Issue
Block a user