Files
cleveragents-core/tools/controller/master/__init__.py
T
drew ae940f4564 feat(controller): Phase 1d-3c-4 — HTTP adapter wiring callbacks to _claim_runtime
The production glue between the controller's callback protocols
(discovery / forgejo_writes / merging) and the existing Forgejo HTTP
client in _claim_runtime. Single build_callbacks(cfg) factory; tests
use the same module with a fake runtime stub.

tools/controller/master/forgejo_http.py:

- ForgejoCallbacks dataclass bundling every callback the controller
  needs: list_prs, list_issues, list_comments, post_comment,
  get_labels, add_label, remove_label, merge_pr.

- build_callbacks(cfg, runtime=None): wires each callback as a thin
  closure over runtime.get/post/delete. Production omits ``runtime``
  to use the real _claim_runtime module; tests inject a fake.

- Forgejo path conventions match the API:
    GET /repos/{o}/{r}/pulls?state=open
    GET /repos/{o}/{r}/issues?state=open&type=issues  (excludes PRs)
    GET/POST /repos/{o}/{r}/issues/{n}/comments
    GET/POST /repos/{o}/{r}/issues/{n}/labels
    DELETE   /repos/{o}/{r}/issues/{n}/labels/{name}  (URL-encoded)
    POST     /repos/{o}/{r}/pulls/{n}/merge  (body: {"Do": "merge"})

- Robust response handling:
  - list endpoints: non-200 → empty list; non-list body → empty;
    non-dict items filtered out.
  - post_comment: 200/201 ok; other → RuntimeError.
  - add_label / remove_label: 200/201/204 → True; remove-404 → True
    (label already gone = goal achieved); else False.
  - merge_pr: returns normalized MergeResponse. On 404, fetches the
    PR's actual state (merged=True → pr_state='merged'; state='closed'
    → 'closed'; PR fetch failure or non-200 → leave pr_state=None so
    the merging handler defaults to ABANDONED conservatively).
  - Any callback exception → synthetic 503 so the merging handler's
    retry logic kicks in cleanly.

23 new tests in test_master_forgejo_http.py:
- list_prs (path format, non-200 → empty, non-list body → empty,
  filters non-dict items)
- list_issues (type=issues filter)
- list_comments (path format)
- post_comment (201, 200, non-2xx raises, non-dict body)
- labels (get / add 201/500 / remove 204/404/URL-encoded)
- merge (200, 409, 500, callback-raises-as-503, 404+merged,
  404+closed, 404+pull-fetch-failure)

Plus a bug fix surfaced by the test_404_with_pull_fetch_failure test:
the PR-state-fetch branch was returning 'open' on a 500 response;
now correctly checks status==200 before inspecting the body.

Total: 366 controller tests; full auto_agents suite 2728 pass.
2026-05-18 14:00:11 -04:00

123 lines
3.0 KiB
Python

"""Master controller — singleton orchestrator.
Per plan v9: the master process is a singleton per (owner, repo). It:
1. Runs discovery (Forgejo poll) — Phase 1d-3
2. Drives the state machine: for each non-terminal workflow, advance
one step based on the outcome of its latest completed attempt
3. Runs the reaper + pickup guard each tick
4. Performs Forgejo writes (status comments, labels, merges) —
Phase 1d-3+
5. Runs periodic reconciliation (DB↔Forgejo) — Phase 1d-3+
This package ships incrementally. Phase 1d-1 (already shipped) had
the state machine + reaper + pickup guard. This Phase 1d-2 adds:
- ``outcomes.py``: maps worker output_payload outcomes → state machine
events (e.g., implementer outcome='resolved''implementer_pushed';
outcome='rebase-failed''implementer_rebase_failed').
- ``tick.py``: one tick handler — scans recently-completed attempts,
applies events to their workflows, emits controller_events.
"""
from .outcomes import (
EventMapResult,
map_outcome_to_event,
)
from .tick import (
TickReport,
run_tick,
)
from .scheduler import (
MAX_TIER,
PrefetchCallback,
ScheduledAttempt,
SchedulerReport,
schedule_next_attempts,
)
from .discovery import (
DiscoveredEntity,
DiscoveryReport,
ListIssuesCallback,
ListPRsCallback,
run_discovery,
)
from .forgejo_writes import (
AddLabelCallback,
GetLabelsCallback,
LabelAdjustResult,
ListCommentsCallback,
PostCommentCallback,
RemoveLabelCallback,
StatusCommentResult,
adjust_labels,
build_marker,
comment_has_fingerprint,
compute_fingerprint,
post_status_comment,
)
from .merging import (
MAX_BACKOFF_S,
MAX_MERGE_RETRIES,
MergeCallback,
MergeResponse,
MergingHandlerReport,
run_merging_tick,
)
from .forgejo_http import (
ForgejoCallbacks,
build_callbacks,
)
from .loop import (
MasterConfig,
MasterTickReport,
master_main_loop,
run_master_iteration,
)
__all__ = [
# Discovery
"DiscoveredEntity",
"DiscoveryReport",
"ListIssuesCallback",
"ListPRsCallback",
"run_discovery",
# Outcome mapping
"EventMapResult",
"map_outcome_to_event",
# Scheduler
"MAX_TIER",
"PrefetchCallback",
"ScheduledAttempt",
"SchedulerReport",
"schedule_next_attempts",
# Tick
"TickReport",
"run_tick",
# Main loop
"MasterConfig",
"MasterTickReport",
"master_main_loop",
"run_master_iteration",
# Forgejo writes
"AddLabelCallback",
"GetLabelsCallback",
"LabelAdjustResult",
"ListCommentsCallback",
"PostCommentCallback",
"RemoveLabelCallback",
"StatusCommentResult",
"adjust_labels",
"build_marker",
"comment_has_fingerprint",
"compute_fingerprint",
"post_status_comment",
# MERGING handler
"MAX_BACKOFF_S",
"MAX_MERGE_RETRIES",
"MergeCallback",
"MergeResponse",
"MergingHandlerReport",
"run_merging_tick",
# HTTP adapter (Forgejo wiring)
"ForgejoCallbacks",
"build_callbacks",
]