Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions promptpilot/pipeline_insights.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}",
Expand Down Expand Up @@ -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"]
Expand All @@ -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"]
Expand All @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion promptpilot/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,7 @@ <h2>Провайдеры <button class="btn btn-sm btn-ghost" style="margin-left
const expanded = t.id === expandedId;
const time = timeAgo(t.created_at);
const parsed = parseResult(t.result);
const taskErrorLabel = githubApiWait(t) ? 'Причина ожидания' : 'Error';

// Build result section
let resultHTML = '';
Expand Down Expand Up @@ -1116,7 +1117,7 @@ <h2>Провайдеры <button class="btn btn-sm btn-ghost" style="margin-left
${resultHTML}

${t.error ? `
<div class="detail-label">Error</div>
<div class="detail-label">${taskErrorLabel}</div>
<div class="result-block">
<pre>${esc(t.error)}</pre>
<button class="copy-btn" data-copy="${escAttr(t.error)}" onclick="copyText(event)">Copy</button>
Expand Down
51 changes: 51 additions & 0 deletions tests/test_github_budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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 '<div class="detail-label">${taskErrorLabel}</div>' in html
assert '<pre>${esc(t.error)}</pre>' 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()
Expand Down
Loading