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:
co-authored by
Claude Opus 4.8
parent
95894e4ca7
commit
ed58eb7aa6
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user