From a7bf276b2243992c2be75bb48036db7dae80971c Mon Sep 17 00:00:00 2001 From: ibrog Date: Thu, 17 Sep 2026 13:53:56 +0300 Subject: [PATCH] =?UTF-8?q?fix(pipeline):=20=D0=BF=D1=80=D0=BE=D1=8F=D1=81?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20=D0=BE=D0=B6=D0=B8=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20GitHub=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated-with: Codex --- promptpilot/pipeline_insights.py | 24 ++++++++++++--- promptpilot/static/index.html | 3 +- tests/test_github_budget.py | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/promptpilot/pipeline_insights.py b/promptpilot/pipeline_insights.py index 58043c0..d422dda 100644 --- a/promptpilot/pipeline_insights.py +++ b/promptpilot/pipeline_insights.py @@ -415,6 +415,21 @@ def _projected_post_reservation( return projected +def _budget_blocked_summary(item: dict, active_reservations: int) -> str: + """Explain signed admission arithmetic without implying a GitHub value.""" + actual_remaining = item.get( + "remaining", item.get("reported_remaining", "—")) + return ( + f"{item['resource']}: прогноз после резервов и оценки запуска " + f"{item['effective_after']} < безопасный остаток " + f"{item['minimum_remaining']} (фактический остаток GitHub " + f"{actual_remaining}; " + f"активных резервов {int(active_reservations)}; другими задачами " + f"зарезервировано {item['reserved_other']}; оценка этого запуска " + f"{item['requested_cost']})" + ) + + def _budget_denied(policy: dict, *, state: str, reason: str, now: float, limits: dict | None = None, defer_at: float | None = None, @@ -550,7 +565,7 @@ def _evaluate_github_budget(policy: dict, limits: dict | None, *, state = "budget_in_flight" reason_prefix = "GitHub API-бюджет временно занят выполняемой задачей" summary = ", ".join( - f"{item['resource']} {item['effective_after']} < {item['minimum_remaining']}" + _budget_blocked_summary(item, active_reservations) for item in blocked) return _budget_denied( policy, state=state, reason=f"{reason_prefix}: {summary}", @@ -729,6 +744,7 @@ def _reservation_denied(policy: dict, limits: dict | None, result: dict, *, status_revision: int | None) -> dict: now = time.time() blocked = result.get("blocked_resources") or [] + active_reservations = int(result.get("active_reservations") or 0) if blocked: live_blocked = [item for item in blocked if item.get("blocked_by") == "live"] @@ -744,8 +760,8 @@ def _reservation_denied(policy: dict, limits: dict | None, result: dict, *, state = "budget_in_flight" reason_prefix = "GitHub API-бюджет временно занят выполняемой задачей" summary = ", ".join( - f"{item['resource']} {item['effective_after']} < " - f"{item['minimum_remaining']}" for item in blocked) + _budget_blocked_summary(item, active_reservations) + for item in blocked) reason = f"{reason_prefix}: {summary}" else: defer_at = now + policy["unavailable_retry_seconds"] @@ -758,7 +774,7 @@ def _reservation_denied(policy: dict, limits: dict | None, result: dict, *, status_revision=status_revision, reserved_other=result.get("reserved_other"), effective_after=result.get("effective_after"), - active_reservations=int(result.get("active_reservations") or 0)) + active_reservations=active_reservations) def _reserve_execution_admission( diff --git a/promptpilot/static/index.html b/promptpilot/static/index.html index 56276a7..318f894 100644 --- a/promptpilot/static/index.html +++ b/promptpilot/static/index.html @@ -1043,6 +1043,7 @@

Провайдеры diff --git a/tests/test_github_budget.py b/tests/test_github_budget.py index 1877eea..df76556 100644 --- a/tests/test_github_budget.py +++ b/tests/test_github_budget.py @@ -130,6 +130,45 @@ def test_projected_budget_clamps_display_but_keeps_signed_admission_value(): } +def test_budget_denial_reason_explains_signed_projection_in_both_paths(): + profile = _profile_with_costs(core=600) + profile["github_budget"]["minimum_remaining"] = { + "core": 250, "search": 0, "graphql": 0, + } + policy = pipeline_insights._budget_policy_for_route( + pipeline_insights._github_budget_policy(profile), "tool_preflight") + limits = _limits(core=1437) + reserved_other = {"core": 1400, "search": 0, "graphql": 0} + expected = ( + "GitHub API-бюджет временно занят выполняемой задачей: " + "core: прогноз после резервов и оценки запуска -563 < безопасный " + "остаток 250 (фактический остаток GitHub 1437; активных резервов 1; " + "другими задачами зарезервировано 1400; оценка этого запуска 600)" + ) + + evaluated = pipeline_insights._evaluate_github_budget( + policy, limits, now=1000, reserved_other=reserved_other, + active_reservations=1) + reserved = pipeline_insights._reservation_denied( + policy, limits, { + "blocked_resources": [{ + "resource": "core", "reported_remaining": 1437, + "reserved_other": 1400, "requested_cost": 600, + "effective_after": -563, "minimum_remaining": 250, + "reset": limits["core"]["reset"], + "blocked_by": "reservation", + }], + "reserved_other": reserved_other, + "effective_after": { + "core": -563, "search": 30, "graphql": 5000, + }, + "active_reservations": 1, + }, status_revision=1) + + assert evaluated["reason"] == expected + assert reserved["reason"] == expected + + def test_cost_schema_requires_every_known_route_and_exact_integer_vectors(): profile = _profile_with_costs() @@ -686,6 +725,18 @@ def test_web_dashboard_labels_projection_as_non_actual_github_remaining(): assert "ledgerKnown ? budgetNumber(reservedBudget, 'core') : '—'" in html +def test_web_task_detail_labels_only_pending_github_wait_as_wait_reason(): + html = (Path(__file__).parents[1] / "promptpilot" / "static" / + "index.html").read_text(encoding="utf-8") + + assert "const taskErrorLabel = githubApiWait(t) ? " \ + "'Причина ожидания' : 'Error';" in html + assert "if (t.status !== 'pending'" in html + assert '
${taskErrorLabel}
' in html + assert '
${esc(t.error)}
' in html + assert 'data-copy="${escAttr(t.error)}"' in html + + def test_success_completion_uses_local_successor_graph_without_github_scan( isolated_db, monkeypatch): profile = _profile_with_costs()