fix(flow): dashboard refresh no longer zeroes non-flow branch plan counts

PLANS.central.json only held Flow's own plans (location==FLOW_ROOT filter),
so every dashboard refresh overwrote each branch's real active_plans with 0.
Central is now comprehensive — all plans grouped per-branch. Devpulse shows
its 12 open plans again. +1 regression test (734 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-06-02 15:22:12 -07:00
co-authored by Claude Opus 4.8
parent 95894e4ca7
commit ed58eb7aa6
2 changed files with 309 additions and 253 deletions
@@ -9,14 +9,13 @@
"""
Push to Plans Central Handler
Pushes Flow's plan data to the central PLANS.central.json file at .ai_central.
Pushes plan data for ALL branches to the central PLANS.central.json file at .ai_central.
This handler follows the 3-tier logging standard (no Prax imports, no logging).
Features:
- Reads fplan_registry.json to get Flow's plans
- Extracts only plans where location='flow' (Flow's own plans)
- Updates branches.flow section in PLANS.central.json
- Preserves all other branch sections
- Reads all per-type plan registries
- Groups plans by branch (derived from location path)
- Updates per-branch sections in PLANS.central.json
- Calls aggregate_central_impl to rebuild top-level active_plans
- Calculates global statistics across all branches
- Pure handler - returns boolean for success/failure
@@ -124,26 +123,29 @@ def _load_registry() -> Dict[str, Any]:
return merged
def _extract_flow_plans(registry: Dict[str, Any]) -> tuple[List[Dict], List[Dict]]:
"""Extract Flow's own plans from registry
def _extract_plans_by_branch(registry: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
"""Extract plans from registry grouped by branch.
Args:
registry: The fplan_registry.json data
registry: Merged registry data (all plan types)
Returns:
Tuple of (active_plans, recently_closed_plans)
Dict mapping branch_name -> branch section with active_plans,
recently_closed, and statistics.
"""
plans = registry.get("plans", {})
active = []
closed = []
branch_buckets: Dict[str, Dict[str, List]] = {}
for plan_key, plan_data in plans.items():
# Only include plans where location is 'flow' (Flow's own plans)
location = plan_data.get("location", "")
if location != str(FLOW_ROOT):
if not location:
continue
# plan_key is composite PREFIX-NNNN from merged registry
branch_name = Path(location).name
if branch_name not in branch_buckets:
branch_buckets[branch_name] = {"active": [], "closed": [], "location": location}
plan_entry = {
"plan_id": plan_key,
"subject": plan_data.get("subject", ""),
@@ -151,24 +153,35 @@ def _extract_flow_plans(registry: Dict[str, Any]) -> tuple[List[Dict], List[Dict
"created": plan_data.get("created", ""),
"file_path": plan_data.get("file_path", ""),
"relative_path": plan_data.get("relative_path", ""),
"branch": branch_name,
}
if plan_data.get("status") == "open":
active.append(plan_entry)
branch_buckets[branch_name]["active"].append(plan_entry)
else:
# Add closed metadata
plan_entry["closed"] = plan_data.get("closed", "")
plan_entry["closed_reason"] = plan_data.get("closed_reason", "")
closed.append(plan_entry)
branch_buckets[branch_name]["closed"].append(plan_entry)
# Sort active by created date (newest first)
active.sort(key=lambda x: x.get("created", ""), reverse=True)
result: Dict[str, Dict[str, Any]] = {}
for branch_name, bucket in branch_buckets.items():
active = sorted(bucket["active"], key=lambda x: x.get("created", ""), reverse=True)
closed = sorted(bucket["closed"], key=lambda x: x.get("closed", ""), reverse=True)
recently_closed = closed[:5]
# Sort closed by closed date (newest first) and limit to last 5
closed.sort(key=lambda x: x.get("closed", ""), reverse=True)
recently_closed = closed[:5]
result[branch_name] = {
"branch_name": branch_name.upper(),
"branch_path": bucket["location"],
"active_plans": active,
"recently_closed": recently_closed,
"statistics": {
"active_count": len(active),
"total_closed": len(closed),
"recently_closed_included": len(recently_closed),
},
}
return active, recently_closed
return result
def _load_central() -> Dict[str, Any]:
@@ -223,80 +236,45 @@ def _calculate_global_statistics(central_data: Dict[str, Any]) -> Dict[str, int]
def push_to_plans_central() -> bool:
"""Push Flow's plan data to .ai_central/PLANS.central.json
"""Push plan data for ALL branches to .ai_central/PLANS.central.json
Algorithm:
1. Read fplan_registry.json
2. Extract only plans where location='flow' (Flow's own plans)
3. Format for central structure with branch metadata
4. Read existing PLANS.central.json if exists
5. Update ONLY branches.flow section
6. Update global_statistics (total counts across all branches)
7. Preserve ALL other branch sections
8. Write back to PLANS.central.json
9. Call aggregate_central_impl to rebuild top-level active_plans with validation
1. Read all per-type plan registries
2. Group plans by branch (derived from location path)
3. Build per-branch sections with active/closed/stats
4. Write all branch sections to PLANS.central.json
5. Update global_statistics (total counts across all branches)
6. Call aggregate_central_impl to rebuild top-level active_plans with validation
Returns:
True on success, False on failure
"""
try:
# Ensure .ai_central directory exists
AI_CENTRAL_DIR.mkdir(parents=True, exist_ok=True)
# Load registry
registry = _load_registry()
branch_sections = _extract_plans_by_branch(registry)
# Extract Flow's plans
active_plans, recently_closed = _extract_flow_plans(registry)
# Build Flow's branch section
now = datetime.now(timezone.utc).isoformat()
flow_section = {
"branch_name": "FLOW",
"branch_path": str(FLOW_ROOT),
"last_updated": now,
"active_plans": active_plans,
"recently_closed": recently_closed,
"statistics": {
"active_count": len(active_plans),
"total_closed": len(
[
p
for p in registry.get("plans", {}).values()
if p.get("location") == str(FLOW_ROOT) and p.get("status") == "closed"
]
),
},
}
for section in branch_sections.values():
section["last_updated"] = now
# Load existing central file
central_data = _load_central()
# Update Flow's section
if "branches" not in central_data:
central_data["branches"] = {}
central_data["branches"]["flow"] = flow_section
# Update global statistics
central_data["branches"] = branch_sections
central_data["global_statistics"] = _calculate_global_statistics(central_data)
# Update generated_at timestamp
central_data["generated_at"] = now
# Write back to central file
with open(CENTRAL_FILE, "w", encoding="utf-8") as f:
json.dump(central_data, f, indent=2, ensure_ascii=False)
# Call aggregate_central_impl to rebuild top-level arrays with validation
# This ensures active_plans is built from all branches and validates files exist
aggregate_central_impl(heal=True, central_file=CENTRAL_FILE, central_dir=AI_CENTRAL_DIR)
total_active = sum(len(s.get("active_plans", [])) for s in branch_sections.values())
json_handler.log_operation(
"plans_central_pushed",
{
"active_plans": len(active_plans),
"recently_closed": len(recently_closed),
"branches_reporting": central_data["global_statistics"].get("branches_reporting", 0),
"active_plans": total_active,
"branches_reporting": len(branch_sections),
"success": True,
},
)
+259 -181
View File
@@ -312,81 +312,74 @@ class TestLoadRegistry:
# =============================================
# 4. _extract_flow_plans
# 4. _extract_plans_by_branch
# =============================================
class TestExtractFlowPlans:
"""Tests for _extract_flow_plans."""
class TestExtractPlansByBranch:
"""Tests for _extract_plans_by_branch."""
def test_extracts_active_plans_matching_flow_root(self):
"""Returns active plans where location matches FLOW_ROOT."""
def test_groups_plans_by_branch(self):
"""Groups plans into per-branch sections by location path name."""
mod = _import_mod()
flow_root = str(mod.FLOW_ROOT)
registry = {
"plans": {
"FPLAN-0001": {
"subject": "Plan A",
"subject": "Flow plan",
"status": "open",
"created": "2026-04-20",
"file_path": "/p/FPLAN-0001.md",
"location": flow_root,
"relative_path": "FPLAN-0001.md",
"location": "/repo/src/aipass/flow",
},
"DPLAN-0001": {
"subject": "Devpulse plan",
"status": "open",
"created": "2026-04-21",
"file_path": "/p/DPLAN-0001.md",
"location": "/repo/src/aipass/devpulse",
},
}
}
active, _ = mod._extract_flow_plans(registry)
assert len(active) == 1
assert active[0]["plan_id"] == "FPLAN-0001"
assert active[0]["subject"] == "Plan A"
assert active[0]["status"] == "open"
result = mod._extract_plans_by_branch(registry)
assert "flow" in result
assert "devpulse" in result
assert result["flow"]["statistics"]["active_count"] == 1
assert result["devpulse"]["statistics"]["active_count"] == 1
def test_extracts_closed_plans(self):
"""Returns closed plans with closed metadata."""
def test_extracts_active_and_closed(self):
"""Separates active and closed plans within a branch."""
mod = _import_mod()
flow_root = str(mod.FLOW_ROOT)
registry = {
"plans": {
"FPLAN-0001": {
"subject": "Done plan",
"subject": "Open",
"status": "open",
"created": "2026-04-20",
"file_path": "/p/FPLAN-0001.md",
"location": "/repo/src/aipass/flow",
},
"FPLAN-0002": {
"subject": "Closed",
"status": "closed",
"created": "2026-04-15",
"closed": "2026-04-20",
"closed_reason": "completed",
"file_path": "/p/FPLAN-0001.md",
"location": flow_root,
"relative_path": "FPLAN-0001.md",
"closed": "2026-04-18",
"closed_reason": "done",
"file_path": "/p/FPLAN-0002.md",
"location": "/repo/src/aipass/flow",
},
}
}
active, closed = mod._extract_flow_plans(registry)
assert len(active) == 0
assert len(closed) == 1
assert closed[0]["closed"] == "2026-04-20"
assert closed[0]["closed_reason"] == "completed"
def test_excludes_plans_from_other_locations(self):
"""Excludes plans where location does not match FLOW_ROOT."""
mod = _import_mod()
registry = {
"plans": {
"FPLAN-0001": {
"subject": "Other branch plan",
"status": "open",
"created": "2026-04-20",
"file_path": "/other/FPLAN-0001.md",
"location": "/some/other/path",
},
}
}
active, closed = mod._extract_flow_plans(registry)
assert len(active) == 0
assert len(closed) == 0
result = mod._extract_plans_by_branch(registry)
flow = result["flow"]
assert flow["statistics"]["active_count"] == 1
assert flow["statistics"]["total_closed"] == 1
assert len(flow["active_plans"]) == 1
assert len(flow["recently_closed"]) == 1
assert flow["recently_closed"][0]["closed"] == "2026-04-18"
def test_sorts_active_newest_first(self):
"""Active plans are sorted by created date, newest first."""
"""Active plans sorted by created date, newest first."""
mod = _import_mod()
flow_root = str(mod.FLOW_ROOT)
registry = {
"plans": {
"FPLAN-0001": {
@@ -394,114 +387,110 @@ class TestExtractFlowPlans:
"status": "open",
"created": "2026-04-01",
"file_path": "/p/FPLAN-0001.md",
"location": flow_root,
"location": "/repo/src/aipass/flow",
},
"FPLAN-0002": {
"subject": "Newest",
"status": "open",
"created": "2026-04-25",
"file_path": "/p/FPLAN-0002.md",
"location": flow_root,
},
"FPLAN-0003": {
"subject": "Middle",
"status": "open",
"created": "2026-04-15",
"file_path": "/p/FPLAN-0003.md",
"location": flow_root,
"location": "/repo/src/aipass/flow",
},
}
}
active, _ = mod._extract_flow_plans(registry)
result = mod._extract_plans_by_branch(registry)
active = result["flow"]["active_plans"]
assert active[0]["subject"] == "Newest"
assert active[1]["subject"] == "Middle"
assert active[2]["subject"] == "Oldest"
assert active[1]["subject"] == "Oldest"
def test_closed_plans_limited_to_5(self):
"""Recently closed plans are limited to 5."""
def test_closed_limited_to_5(self):
"""Recently closed plans limited to 5 per branch."""
mod = _import_mod()
flow_root = str(mod.FLOW_ROOT)
plans = {}
for i in range(1, 9):
plans[f"FPLAN-{str(i).zfill(4)}"] = {
"subject": f"Closed plan {i}",
"subject": f"Closed {i}",
"status": "closed",
"created": "2026-04-01",
"closed": f"2026-04-{str(i + 10).zfill(2)}",
"closed_reason": "done",
"file_path": f"/p/FPLAN-{str(i).zfill(4)}.md",
"location": flow_root,
"location": "/repo/src/aipass/flow",
}
registry = {"plans": plans}
_, closed = mod._extract_flow_plans(registry)
assert len(closed) == 5
result = mod._extract_plans_by_branch({"plans": plans})
assert len(result["flow"]["recently_closed"]) == 5
def test_closed_sorted_newest_first(self):
"""Closed plans are sorted by closed date, newest first."""
def test_empty_registry(self):
"""Returns empty dict for empty registry."""
mod = _import_mod()
result = mod._extract_plans_by_branch({"plans": {}})
assert result == {}
def test_missing_plans_key(self):
"""Returns empty dict when registry has no plans key."""
mod = _import_mod()
result = mod._extract_plans_by_branch({})
assert result == {}
def test_skips_plans_without_location(self):
"""Plans with empty location are skipped."""
mod = _import_mod()
flow_root = str(mod.FLOW_ROOT)
registry = {
"plans": {
"FPLAN-0001": {
"subject": "Old close",
"status": "closed",
"created": "2026-03-01",
"closed": "2026-03-10",
"subject": "No location",
"status": "open",
"created": "2026-04-20",
"file_path": "/p/FPLAN-0001.md",
"location": flow_root,
},
"FPLAN-0002": {
"subject": "New close",
"status": "closed",
"created": "2026-04-01",
"closed": "2026-04-20",
"file_path": "/p/FPLAN-0002.md",
"location": flow_root,
"location": "",
},
}
}
_, closed = mod._extract_flow_plans(registry)
assert closed[0]["subject"] == "New close"
assert closed[1]["subject"] == "Old close"
result = mod._extract_plans_by_branch(registry)
assert result == {}
def test_empty_registry(self):
"""Returns empty lists for empty registry."""
def test_branch_section_structure(self):
"""Each branch section has required keys."""
mod = _import_mod()
active, closed = mod._extract_flow_plans({"plans": {}})
assert active == []
assert closed == []
def test_missing_plans_key(self):
"""Returns empty lists when registry has no plans key."""
mod = _import_mod()
active, closed = mod._extract_flow_plans({})
assert active == []
assert closed == []
def test_plan_entry_structure(self):
"""Plan entries have all required keys."""
mod = _import_mod()
flow_root = str(mod.FLOW_ROOT)
registry = {
"plans": {
"FPLAN-0001": {
"subject": "Structured plan",
"subject": "Structured",
"status": "open",
"created": "2026-04-20",
"file_path": "/p/FPLAN-0001.md",
"relative_path": "FPLAN-0001.md",
"location": flow_root,
"location": "/repo/src/aipass/flow",
},
}
}
active, _ = mod._extract_flow_plans(registry)
entry = active[0]
result = mod._extract_plans_by_branch(registry)
section = result["flow"]
assert section["branch_name"] == "FLOW"
assert section["branch_path"] == "/repo/src/aipass/flow"
assert "active_plans" in section
assert "recently_closed" in section
assert "statistics" in section
entry = section["active_plans"][0]
assert "plan_id" in entry
assert "subject" in entry
assert "status" in entry
assert "created" in entry
assert "file_path" in entry
assert "relative_path" in entry
assert "branch" in entry
def test_plan_entries_have_branch_field(self):
"""Each plan entry includes the branch field."""
mod = _import_mod()
registry = {
"plans": {
"DPLAN-0001": {
"subject": "Test",
"status": "open",
"created": "2026-04-20",
"file_path": "/p/DPLAN-0001.md",
"location": "/repo/src/aipass/devpulse",
},
}
}
result = mod._extract_plans_by_branch(registry)
assert result["devpulse"]["active_plans"][0]["branch"] == "devpulse"
# =============================================
@@ -651,8 +640,15 @@ class TestPushToPlansCentral:
ai_central = tmp_path / ".ai_central"
mock_registry = {"plans": {}, "next_number": 1}
mock_active = [{"plan_id": "FPLAN-0001", "subject": "Test", "status": "open"}]
mock_closed: list = []
mock_branches = {
"flow": {
"branch_name": "FLOW",
"branch_path": str(mod.FLOW_ROOT),
"active_plans": [{"plan_id": "FPLAN-0001", "subject": "Test", "status": "open"}],
"recently_closed": [],
"statistics": {"active_count": 1, "total_closed": 0, "recently_closed_included": 0},
}
}
mock_central = {
"generated_at": "",
"branches": {},
@@ -663,7 +659,7 @@ class TestPushToPlansCentral:
patch.object(mod, "AI_CENTRAL_DIR", ai_central),
patch.object(mod, "CENTRAL_FILE", central_file),
patch.object(mod, "_load_registry", return_value=mock_registry),
patch.object(mod, "_extract_flow_plans", return_value=(mock_active, mock_closed)),
patch.object(mod, "_extract_plans_by_branch", return_value=mock_branches),
patch.object(mod, "_load_central", return_value=mock_central),
patch.object(mod, "aggregate_central_impl") as mock_agg,
):
@@ -703,30 +699,44 @@ class TestPushToPlansCentral:
assert central_file.exists()
written = json.loads(central_file.read_text(encoding="utf-8"))
assert "branches" in written
assert "flow" in written["branches"]
assert "global_statistics" in written
assert "generated_at" in written
assert written["generated_at"] != ""
def test_preserves_other_branches(self, tmp_path):
"""Preserves other branch sections when updating flow."""
def test_writes_multiple_branches(self, tmp_path):
"""Writes per-branch sections from registry data."""
mod = _import_mod()
central_file = tmp_path / "PLANS.central.json"
ai_central = tmp_path / ".ai_central"
mock_registry = {"plans": {}, "next_number": 1}
mock_central = {
"generated_at": "2026-04-01T00:00:00Z",
"branches": {
"drone": {"branch_name": "DRONE", "statistics": {"active_count": 3, "total_closed": 1}},
mock_branches = {
"flow": {
"branch_name": "FLOW",
"branch_path": "/repo/src/aipass/flow",
"active_plans": [],
"recently_closed": [],
"statistics": {"active_count": 0, "total_closed": 0, "recently_closed_included": 0},
},
"global_statistics": {"total_active": 3, "total_closed": 1, "branches_reporting": 1},
"devpulse": {
"branch_name": "DEVPULSE",
"branch_path": "/repo/src/aipass/devpulse",
"active_plans": [{"plan_id": "DPLAN-0001"}],
"recently_closed": [],
"statistics": {"active_count": 1, "total_closed": 0, "recently_closed_included": 0},
},
}
mock_central = {
"generated_at": "",
"branches": {},
"global_statistics": {"total_active": 0, "total_closed": 0, "branches_reporting": 0},
}
with (
patch.object(mod, "AI_CENTRAL_DIR", ai_central),
patch.object(mod, "CENTRAL_FILE", central_file),
patch.object(mod, "_load_registry", return_value=mock_registry),
patch.object(mod, "_extract_plans_by_branch", return_value=mock_branches),
patch.object(mod, "_load_central", return_value=mock_central),
patch.object(mod, "aggregate_central_impl"),
):
@@ -734,8 +744,8 @@ class TestPushToPlansCentral:
assert result is True
written = json.loads(central_file.read_text(encoding="utf-8"))
assert "drone" in written["branches"]
assert "flow" in written["branches"]
assert "devpulse" in written["branches"]
def test_creates_ai_central_dir(self, tmp_path):
"""Creates .ai_central directory if it does not exist."""
@@ -773,26 +783,25 @@ class TestPushToPlansCentral:
assert result is False
def test_flow_section_has_correct_statistics(self, tmp_path):
"""Flow section statistics reflect actual plan counts."""
def test_branch_section_has_correct_statistics(self, tmp_path):
"""Branch section statistics reflect actual plan counts."""
mod = _import_mod()
central_file = tmp_path / "PLANS.central.json"
ai_central = tmp_path / ".ai_central"
flow_root_str = str(mod.FLOW_ROOT)
mock_registry = {
"plans": {
"FPLAN-0001": {"status": "open", "location": flow_root_str},
"FPLAN-0002": {"status": "open", "location": flow_root_str},
"FPLAN-0003": {"status": "closed", "location": flow_root_str},
},
"next_number": 4,
mock_registry = {"plans": {}, "next_number": 4}
mock_branches = {
"flow": {
"branch_name": "FLOW",
"branch_path": str(mod.FLOW_ROOT),
"active_plans": [
{"plan_id": "FPLAN-0001", "status": "open"},
{"plan_id": "FPLAN-0002", "status": "open"},
],
"recently_closed": [{"plan_id": "FPLAN-0003", "status": "closed"}],
"statistics": {"active_count": 2, "total_closed": 1, "recently_closed_included": 1},
}
}
mock_active = [
{"plan_id": "FPLAN-0001", "status": "open"},
{"plan_id": "FPLAN-0002", "status": "open"},
]
mock_closed = [{"plan_id": "FPLAN-0003", "status": "closed"}]
mock_central = {
"generated_at": "",
"branches": {},
@@ -803,7 +812,7 @@ class TestPushToPlansCentral:
patch.object(mod, "AI_CENTRAL_DIR", ai_central),
patch.object(mod, "CENTRAL_FILE", central_file),
patch.object(mod, "_load_registry", return_value=mock_registry),
patch.object(mod, "_extract_flow_plans", return_value=(mock_active, mock_closed)),
patch.object(mod, "_extract_plans_by_branch", return_value=mock_branches),
patch.object(mod, "_load_central", return_value=mock_central),
patch.object(mod, "aggregate_central_impl"),
):
@@ -817,18 +826,117 @@ class TestPushToPlansCentral:
assert flow["branch_name"] == "FLOW"
def test_global_statistics_updated(self, tmp_path):
"""Global statistics are recalculated after flow section update."""
"""Global statistics are recalculated from all branch sections."""
mod = _import_mod()
central_file = tmp_path / "PLANS.central.json"
ai_central = tmp_path / ".ai_central"
mock_registry = {"plans": {}, "next_number": 1}
mock_branches = {
"flow": {
"branch_name": "FLOW",
"branch_path": str(mod.FLOW_ROOT),
"active_plans": [],
"recently_closed": [],
"statistics": {"active_count": 0, "total_closed": 0, "recently_closed_included": 0},
},
"devpulse": {
"branch_name": "DEVPULSE",
"branch_path": "/repo/src/aipass/devpulse",
"active_plans": [{"plan_id": "DPLAN-0001"}],
"recently_closed": [],
"statistics": {"active_count": 5, "total_closed": 3, "recently_closed_included": 0},
},
}
mock_central = {
"generated_at": "",
"branches": {
"drone": {"statistics": {"active_count": 5, "total_closed": 3}},
"branches": {},
"global_statistics": {"total_active": 0, "total_closed": 0, "branches_reporting": 0},
}
with (
patch.object(mod, "AI_CENTRAL_DIR", ai_central),
patch.object(mod, "CENTRAL_FILE", central_file),
patch.object(mod, "_load_registry", return_value=mock_registry),
patch.object(mod, "_extract_plans_by_branch", return_value=mock_branches),
patch.object(mod, "_load_central", return_value=mock_central),
patch.object(mod, "aggregate_central_impl"),
):
result = mod.push_to_plans_central()
assert result is True
written = json.loads(central_file.read_text(encoding="utf-8"))
stats = written["global_statistics"]
assert stats["total_active"] == 5
assert stats["total_closed"] == 3
assert stats["branches_reporting"] == 2
def test_log_operation_contains_expected_fields(self, tmp_path, mock_json_handler):
"""Log operation includes active_plans and branches_reporting."""
mod = _import_mod()
central_file = tmp_path / "PLANS.central.json"
ai_central = tmp_path / ".ai_central"
mock_registry = {"plans": {}, "next_number": 1}
mock_branches = {
"flow": {
"branch_name": "FLOW",
"branch_path": str(mod.FLOW_ROOT),
"active_plans": [{"plan_id": "FPLAN-0001"}],
"recently_closed": [],
"statistics": {"active_count": 1, "total_closed": 0, "recently_closed_included": 0},
},
"global_statistics": {"total_active": 5, "total_closed": 3, "branches_reporting": 1},
}
mock_central = {
"generated_at": "",
"branches": {},
"global_statistics": {"total_active": 0, "total_closed": 0, "branches_reporting": 0},
}
with (
patch.object(mod, "AI_CENTRAL_DIR", ai_central),
patch.object(mod, "CENTRAL_FILE", central_file),
patch.object(mod, "_load_registry", return_value=mock_registry),
patch.object(mod, "_extract_plans_by_branch", return_value=mock_branches),
patch.object(mod, "_load_central", return_value=mock_central),
patch.object(mod, "aggregate_central_impl"),
):
mod.push_to_plans_central()
call_args = mock_json_handler.call_args
log_data = call_args[0][1]
assert log_data["active_plans"] == 1
assert "branches_reporting" in log_data
def test_non_flow_branch_plans_in_central(self, tmp_path):
"""Regression: non-flow branch plans must appear in PLANS.central.json."""
mod = _import_mod()
central_file = tmp_path / "PLANS.central.json"
ai_central = tmp_path / ".ai_central"
mock_registry = {
"plans": {
"DPLAN-0181": {
"subject": "Devpulse plan",
"status": "open",
"created": "2026-05-01",
"file_path": "/repo/src/aipass/devpulse/DPLAN-0181.md",
"location": "/repo/src/aipass/devpulse",
},
"FPLAN-0001": {
"subject": "Flow plan",
"status": "open",
"created": "2026-05-02",
"file_path": "/repo/src/aipass/flow/FPLAN-0001.md",
"location": str(mod.FLOW_ROOT),
},
},
"next_number": 1,
}
mock_central = {
"generated_at": "",
"branches": {},
"global_statistics": {"total_active": 0, "total_closed": 0, "branches_reporting": 0},
}
with (
@@ -842,39 +950,9 @@ class TestPushToPlansCentral:
assert result is True
written = json.loads(central_file.read_text(encoding="utf-8"))
stats = written["global_statistics"]
# drone(5 active, 3 closed) + flow(0 active, 0 closed)
assert stats["total_active"] == 5
assert stats["total_closed"] == 3
assert stats["branches_reporting"] == 2
def test_log_operation_contains_expected_fields(self, tmp_path, mock_json_handler):
"""Log operation includes active_plans, recently_closed, branches_reporting."""
mod = _import_mod()
central_file = tmp_path / "PLANS.central.json"
ai_central = tmp_path / ".ai_central"
mock_registry = {"plans": {}, "next_number": 1}
mock_active = [{"plan_id": "FPLAN-0001"}]
mock_closed = [{"plan_id": "FPLAN-0002"}, {"plan_id": "FPLAN-0003"}]
mock_central = {
"generated_at": "",
"branches": {},
"global_statistics": {"total_active": 0, "total_closed": 0, "branches_reporting": 0},
}
with (
patch.object(mod, "AI_CENTRAL_DIR", ai_central),
patch.object(mod, "CENTRAL_FILE", central_file),
patch.object(mod, "_load_registry", return_value=mock_registry),
patch.object(mod, "_extract_flow_plans", return_value=(mock_active, mock_closed)),
patch.object(mod, "_load_central", return_value=mock_central),
patch.object(mod, "aggregate_central_impl"),
):
mod.push_to_plans_central()
call_args = mock_json_handler.call_args
log_data = call_args[0][1]
assert log_data["active_plans"] == 1
assert log_data["recently_closed"] == 2
assert "branches_reporting" in log_data
assert "devpulse" in written["branches"]
devpulse = written["branches"]["devpulse"]
assert devpulse["statistics"]["active_count"] == 1
assert len(devpulse["active_plans"]) == 1
assert devpulse["active_plans"][0]["plan_id"] == "DPLAN-0181"
assert devpulse["active_plans"][0]["branch"] == "devpulse"