diff --git a/python/robomp/src/config.py b/python/robomp/src/config.py index af6abc285..765507905 100644 --- a/python/robomp/src/config.py +++ b/python/robomp/src/config.py @@ -306,8 +306,7 @@ class Settings(BaseSettings): @property def maintainer_logins(self) -> frozenset[str]: items = [ - piece.strip().lstrip("@").lower().removesuffix("[bot]") - for piece in self.maintainer_logins_raw.split(",") + piece.strip().lstrip("@").lower().removesuffix("[bot]") for piece in self.maintainer_logins_raw.split(",") ] return frozenset(item for item in items if item) diff --git a/python/robomp/src/db.py b/python/robomp/src/db.py index 73045501c..ffbbf2d2c 100644 --- a/python/robomp/src/db.py +++ b/python/robomp/src/db.py @@ -62,7 +62,7 @@ CREATE TABLE IF NOT EXISTS issues ( session_dir TEXT, pr_number INTEGER, state TEXT NOT NULL, - classification TEXT, -- bug|enhancement|question|proposal|documentation|invalid|duplicate + classification TEXT, -- bug|enhancement|question|proposal|documentation|wontfix|invalid|duplicate updated_at TEXT NOT NULL ); diff --git a/python/robomp/src/git_ops.py b/python/robomp/src/git_ops.py index d28ba2019..4781739cb 100644 --- a/python/robomp/src/git_ops.py +++ b/python/robomp/src/git_ops.py @@ -67,7 +67,6 @@ def _git_subprocess_env() -> dict[str, str]: return env - # git matches `http..*` / `credential..*` against the FULL request # URL, and the longest path-prefix wins — so a base-only override loses to an # agent-planted repo-local key like `http./info/refs.proxy=http://evil`, diff --git a/python/robomp/src/github_events.py b/python/robomp/src/github_events.py index 644e513f4..5ffa9917d 100644 --- a/python/robomp/src/github_events.py +++ b/python/robomp/src/github_events.py @@ -206,7 +206,13 @@ def _pr_review_pr(pr: Mapping[str, Any], repo: str, action: str, bot_login: str) return RouteDecision("skip", None, repo, None, "PR missing number") login, assoc = _submitter_info(pr) return RouteDecision( - "queue", "review_pr", repo, issue_key(repo, number), f"pull_request.{action}", submitter=login, association=assoc + "queue", + "review_pr", + repo, + issue_key(repo, number), + f"pull_request.{action}", + submitter=login, + association=assoc, ) diff --git a/python/robomp/src/host_tools.py b/python/robomp/src/host_tools.py index a98a662c8..3fbf84af4 100644 --- a/python/robomp/src/host_tools.py +++ b/python/robomp/src/host_tools.py @@ -1256,7 +1256,7 @@ def _build_fetch_thread(bindings: ToolBindings) -> HostTool[Any, Any]: ) -_PRIMARY_TYPES = ("bug", "enhancement", "question", "proposal", "documentation", "invalid", "duplicate") +_PRIMARY_TYPES = ("bug", "enhancement", "question", "proposal", "documentation", "wontfix", "invalid", "duplicate") _AUTO_PR_CLASSIFICATIONS = frozenset({"bug", "documentation"}) _PRIORITIES = ("prio:p0", "prio:p1", "prio:p2", "prio:p3") _FUNCTIONAL = ("agent", "tool", "tui", "cli", "prompting", "sdk", "auth", "setup", "ux", "providers") diff --git a/python/robomp/src/prompts/followup_comment.md b/python/robomp/src/prompts/followup_comment.md index 322e64f65..b87f18dda 100644 --- a/python/robomp/src/prompts/followup_comment.md +++ b/python/robomp/src/prompts/followup_comment.md @@ -17,6 +17,7 @@ Thread context: {{origin.description}}. PR state: `{{state.pr_status}}`. Decide what to do: - **New repro info?** Re-run via `repro_record`, then `gh_post_comment` with the outcome. +- **Maintainer dismissal?** A maintainer saying "intended", "not an issue", "works as designed", or similar — however terse — permanently ends the fix workflow. No further commits, pushes, or PRs, even mid-fix with work already done. Apply `wontfix` via `set_issue_labels` (when available on this thread), reply with at most one short acknowledgement, and stop. - **PR change requested?** Amend `{{workspace.branch}}` and push only for an already-open PR / authorized implementation; NEVER open a second PR, and NEVER open the first PR for an unauthorized enhancement/proposal. Reply with a short `gh_post_comment` naming what changed. - **Confirmation or unrelated question?** Reply with one `gh_post_comment`. Leave code untouched. - **Bot author or no actionable content?** No-op. diff --git a/python/robomp/src/prompts/host_tools.toml b/python/robomp/src/prompts/host_tools.toml index 1db2f22ef..bca47df2b 100644 --- a/python/robomp/src/prompts/host_tools.toml +++ b/python/robomp/src/prompts/host_tools.toml @@ -88,10 +88,11 @@ functional = "Zero or more functional labels. Unknown values dropped silently; o provider = "Only when provider-scoped; format `provider:`. Omit otherwise." platform = "Only when platform materially affects reproduction; one of `platform:linux|macos|windows|wsl`. Omit otherwise." rationale = "One sentence explaining the classification." -branch_slug = "Kebab-case slug, 1-50 chars `[a-z0-9-]`, no leading/trailing/double hyphen. Replaces the auto-generated slug in the working branch name. Provide for `bug`/`documentation`. Omit for non-PR workflows (`question`, `enhancement`, `proposal`, `invalid`, `duplicate`)." +branch_slug = "Kebab-case slug, 1-50 chars `[a-z0-9-]`, no leading/trailing/double hyphen. Replaces the auto-generated slug in the working branch name. Provide for `bug`/`documentation`. Omit for non-PR workflows (`question`, `enhancement`, `proposal`, `wontfix`, `invalid`, `duplicate`)." [classify_issue.next_steps] bug = "reproduce → diagnose → fix → PR" +wontfix = "post one gh_post_comment explaining the design rationale and what evidence would change the assessment; no repro, no PR; the maintainer decides whether to close" documentation = "fix the docs and open a PR using the four-section template" question = "answer in a single gh_post_comment; no PR, no repro" enhancement = "post one thoughtful gh_post_comment on feasibility/scope; no PR" diff --git a/python/robomp/src/prompts/kickoff_issue.md b/python/robomp/src/prompts/kickoff_issue.md index 87767775f..a5a851394 100644 --- a/python/robomp/src/prompts/kickoff_issue.md +++ b/python/robomp/src/prompts/kickoff_issue.md @@ -18,6 +18,8 @@ the classification calls for code. Drive the todo list to completion: 1. **Triage first.** Read the body and any comments via `read` / `fetch_issue_thread`, then call `classify_issue(primary=..., priority=..., functional=[...], rationale=...)`. + Apply the **merit gate** from the system prompt before picking `bug`: + broken contract + demonstrated impact + not a deliberate tradeoff. You NEVER post a comment, push, or open a PR before this step. 2. **Follow the workflow branch** the classification dictates — see the system @@ -25,6 +27,7 @@ the classification calls for code. Drive the todo list to completion: - `bug` / `documentation` → ack comment → reproduce → fix → PR. - `question` → one comment, then stop. - `enhancement` / `proposal` → one thoughtful comment, then stop. + - `wontfix` → one comment explaining the design rationale, then stop. - `invalid` / `duplicate` → one brief comment, then stop. 3. If `bug` and you cannot reproduce after a real attempt, call diff --git a/python/robomp/src/prompts/system_append.md b/python/robomp/src/prompts/system_append.md index b84ee356c..f0010dab0 100644 --- a/python/robomp/src/prompts/system_append.md +++ b/python/robomp/src/prompts/system_append.md @@ -5,7 +5,7 @@ You are **@{{bot_login}}**, an autonomous triage-and-fix bot operating on `{{rep - **`branch_slug` for `bug` / `documentation`.** Pass a short kebab-case slug (e.g. `fix-windows-env-colon-vars`) so the branch and PR read naturally. Omit for non-PR workflows. - **Host tools only.** All GitHub mutations go through `gh_*`, `classify_issue`, `set_issue_labels`. NEVER shell out to `gh` or `git push` — the worktree's remote has no credentials you can see. - **No new branches.** `{{workspace.branch}}` is checked out. Commit on it. -- **Fix the root cause.** Suppressing warnings, special-casing inputs, or relabeling the bug as expected behavior is PROHIBITED unless the reporter explicitly accepts that resolution. +- **Fix the root cause.** Once classified `bug`, suppressing warnings, special-casing inputs, or relabeling the bug as expected behavior mid-fix is PROHIBITED unless the reporter explicitly accepts that resolution. The place to argue the behavior is intentional is triage — classify `wontfix` there; NEVER bail halfway through a fix. # Classification taxonomy @@ -15,6 +15,7 @@ Pick exactly ONE primary label per issue: | Label | When | |---|---| | `bug` | Existing behavior is broken: crashes, errors, regressions, "doesn't work". Repro + fix + PR. | +| `wontfix` | Report is technically accurate but the behavior is intentional design, a documented tradeoff, or the fix costs more than the problem it solves. Explain; no PR. | | `documentation` | Docs are missing, incorrect, or outdated. Fix + PR (treat the doc as the code). | | `enhancement` | Feature request or improvement to existing behavior. Discuss; do NOT implement uninvited. | | `proposal` | Design/process proposal requiring maintainer decision. Comment with thoughts; no PR. | @@ -22,6 +23,24 @@ Pick exactly ONE primary label per issue: | `invalid` | Spam, off-topic, or not actionable. One brief explanatory comment. | | `duplicate` | Clear duplicate of another issue. Cite the original; no PR. | +## Merit gate — `bug` vs `wontfix` vs `enhancement` + +A report earns `bug` ONLY when ALL THREE hold. Address them in the `rationale`: + +1. **Broken contract.** The behavior contradicts documented behavior or what a reasonable user doing real work would expect — not merely what a spec, standard, or filesystem *permits*. "Paths may legally contain `:`, therefore the tool must parse them" is spec-lawyering, not a broken contract. +2. **Demonstrated impact.** The reporter hit this doing real work, or users plausibly will. An input constructed solely to trigger the report is not impact, and neither is a failure mode discovered by *reading source code* rather than running the tool. Elaborate analysis — tables, line-cited "Evidence" sections, N-of-N repro counts, "Acceptance criteria" — measures the reporter's effort, NEVER the problem's severity. A meticulous report about a non-problem is still a non-problem. +3. **Not a deliberate tradeoff.** Check whether the current behavior was *chosen* — docs, code comments, git history, prior issues. Prompt policies, UX decisions, and guardrails against known failure modes are design, not defects, even when a user dislikes the consequence. Behavior originating upstream (a model's RLHF quirks, a provider API, a dependency) is not this repo's bug. + +Common shapes that fail the gate: + +- **Audit reports.** Issue reads like a code review: exhaustive citations, hypothetical failure paths, "Open questions", no first-person failure. Classify by what the finding *is* (`wontfix` for by-design, `enhancement` for hardening ideas) — never `bug` on citation volume alone. +- **Niche config + trivial workaround.** Non-default option, exotic environment, and a one-line workaround exists → `wontfix`, whatever the claimed severity. +- **Design complaints dressed as bugs.** Reporter wants *different* behavior → `enhancement` / `proposal`, even when the title screams "bug". The reporter's framing NEVER binds your classification. + +Torn between `bug` + `prio:p3` and `wontfix`? Pick `wontfix`: a maintainer flips it with one comment ("@{{bot_login}} fix it anyway"), but an unwanted PR wastes review time and lands code nobody asked for. + +**Maintainer signals override everything, at any stage.** A maintainer comment like "intended", "not an issue", or "works as designed" — however terse, mention or not — ends the fix workflow immediately: stop, apply `wontfix` via `set_issue_labels`, post at most one closing acknowledgement. NEVER push a commit, open a PR, or argue after a maintainer has called it intended. + Optional additional labels (pass to `classify_issue`): - `priority`: `prio:p0` | `prio:p1` | `prio:p2` | `prio:p3` — **REQUIRED** when `primary == "bug"`. @@ -65,6 +84,17 @@ ONE `gh_post_comment` engaging with the request: - Identify open questions the maintainer MUST decide. - NEVER implement uninvited. Even if the change is small, wait for a maintainer to label it `accepted` or comment "go ahead". +## `primary == "wontfix"` + +ONE `gh_post_comment`: + +- Acknowledge what is technically accurate in the report — no strawmanning. +- Explain the design rationale or tradeoff that makes the current behavior intentional. Cite code/docs by path. +- Name what evidence WOULD change the assessment (a real failing workflow, a documented contract the behavior violates). +- Defer the final call to the maintainer; do not close the issue. + +No repro, no branch, no PR. NEVER implement the fix "since it's small" — that decision belongs to the maintainer. + ## `primary == "invalid"` or `primary == "duplicate"` ONE brief `gh_post_comment`: @@ -104,6 +134,7 @@ symbols, not vibes.> - Triage (`classify_issue`) precedes every other action on a fresh issue. +- `bug` REQUIRES a broken contract AND demonstrated impact. Design complaints and spec-lawyering are `wontfix` / `enhancement`, never `bug`. - All GitHub mutation flows through host tools. NEVER shell out. - Commit on the prepared branch; NEVER create new branches. - `skip_checks=true` ONLY for verified pre-existing breakage, documented in `## Verification`. diff --git a/python/robomp/src/proxy/server.py b/python/robomp/src/proxy/server.py index 4860aa3e0..bf6ba4cf7 100644 --- a/python/robomp/src/proxy/server.py +++ b/python/robomp/src/proxy/server.py @@ -182,6 +182,7 @@ def _require_review_comments(value: Any) -> list[dict[str, Any]]: comments.append(comment) return comments + def _pool_dir(cfg: Settings, repo: str) -> Path: _validate_repo_name(repo) return Path(cfg.workspace_root) / "_pool" / repo.replace("/", "__") diff --git a/python/robomp/src/proxy_client.py b/python/robomp/src/proxy_client.py index 92f8450d4..e65e85311 100644 --- a/python/robomp/src/proxy_client.py +++ b/python/robomp/src/proxy_client.py @@ -11,10 +11,10 @@ to short-circuit the network. from __future__ import annotations -import json import asyncio -import time +import json import logging +import time from collections.abc import Mapping from pathlib import Path from typing import Any diff --git a/python/robomp/src/server.py b/python/robomp/src/server.py index 55032547b..6a9371f96 100644 --- a/python/robomp/src/server.py +++ b/python/robomp/src/server.py @@ -656,7 +656,9 @@ def create_app(settings: Settings | None = None) -> FastAPI: if event is None: raise HTTPException(404, f"unknown delivery {delivery_id}") if event.state != "running": - raise HTTPException(409, f"delivery {delivery_id} is {event.state}; only running deliveries can be cancelled") + raise HTTPException( + 409, f"delivery {delivery_id} is {event.state}; only running deliveries can be cancelled" + ) pool: WorkerPool = bag["pool"] fired = await pool.cancel_event(delivery_id) diff --git a/python/robomp/src/worker.py b/python/robomp/src/worker.py index 73a6b5be1..335f15f0d 100644 --- a/python/robomp/src/worker.py +++ b/python/robomp/src/worker.py @@ -697,9 +697,7 @@ def _run_rpc_blocking( stop_reason = turn.assistant_message.get("stopReason") if stop_reason == "error": error_msg = turn.assistant_message.get("errorMessage") or "model returned error" - raise RuntimeError( - f"omp agent error (stopReason=error): {error_msg}" - ) + raise RuntimeError(f"omp agent error (stopReason=error): {error_msg}") log.info( "rpc_done", extra={ diff --git a/python/robomp/tests/test_config.py b/python/robomp/tests/test_config.py index 2506e85b0..a3a7796f4 100644 --- a/python/robomp/tests/test_config.py +++ b/python/robomp/tests/test_config.py @@ -113,9 +113,7 @@ def test_bot_login_normalizes_mention_case_and_app_suffix( assert cfg.bot_login == "roboomp" -def test_maintainer_logins_normalize_csv_entries( - monkeypatch: pytest.MonkeyPatch, env: dict[str, str] -) -> None: +def test_maintainer_logins_normalize_csv_entries(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None: monkeypatch.setenv("ROBOMP_MAINTAINER_LOGINS", " can1357, @ROBOOMP , @Alice[bot] ,, ") reset_settings_cache() cfg = Settings() # type: ignore[call-arg] diff --git a/python/robomp/tests/test_github_events.py b/python/robomp/tests/test_github_events.py index 53dccdd7e..861ca1b0b 100644 --- a/python/robomp/tests/test_github_events.py +++ b/python/robomp/tests/test_github_events.py @@ -2,6 +2,7 @@ from __future__ import annotations import hashlib import hmac + import pytest from robomp.github_events import ( diff --git a/python/robomp/tests/test_host_tools.py b/python/robomp/tests/test_host_tools.py index 8c3b69cd1..54f9d4c5e 100644 --- a/python/robomp/tests/test_host_tools.py +++ b/python/robomp/tests/test_host_tools.py @@ -842,6 +842,25 @@ def test_classify_issue_question_skips_repro_path(db: Database, tmp_path: Path) assert row is not None and row.classification == "question" +def test_classify_issue_wontfix_takes_comment_only_path(db: Database, tmp_path: Path) -> None: + """`wontfix` is a non-PR primary: labels land, classification persists, and the + echoed next step routes to a single explanatory comment — no repro, no PR.""" + transport = httpx.MockTransport(lambda r: httpx.Response(200, json=[{"name": "wontfix"}, {"name": "triaged"}])) + bindings, loop, t = _bindings(db, tmp_path, transport) + try: + tool = next(x for x in build(bindings) if x.name == "classify_issue") + result = tool.execute( + {"primary": "wontfix", "rationale": "intentional design tradeoff, no demonstrated impact"}, + _ctx(), + ) + finally: + _stop_loop(loop, t) + assert "wontfix" in result + assert "no PR" in result + row = db.get_issue(bindings.issue_key) + assert row is not None and row.classification == "wontfix" + + def test_classify_issue_rejects_bug_without_priority(db: Database, tmp_path: Path) -> None: bindings, loop, t = _bindings(db, tmp_path, httpx.MockTransport(lambda r: httpx.Response(500))) try: @@ -1302,7 +1321,9 @@ def test_impl_gate_allows_later_authorized_event_to_reach_repo_commands( assert calls -def test_impl_gate_ignores_skipped_authorized_event(db: Database, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_impl_gate_ignores_skipped_authorized_event( + db: Database, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: calls: list[list[str] | tuple[str, ...]] = [] def record_repo_command(_bindings: ToolBindings, cmd: list[str] | tuple[str, ...], *, timeout: float | None = None): diff --git a/python/robomp/tests/test_proxy_server.py b/python/robomp/tests/test_proxy_server.py index 36aa94631..b1fd921c5 100644 --- a/python/robomp/tests/test_proxy_server.py +++ b/python/robomp/tests/test_proxy_server.py @@ -166,7 +166,9 @@ async def _async_client(app) -> httpx.AsyncClient: ) -def test_read_remote_urls_uses_safe_directory_and_slot_identity(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_read_remote_urls_uses_safe_directory_and_slot_identity( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: from robomp.proxy import server as proxy_server captured: dict[str, object] = {} @@ -1134,7 +1136,9 @@ async def test_git_fetch_rejects_option_shaped_origin(proxy_settings: Settings, pool_dir = _stage_pool(proxy_settings, upstream_repo) config_path = pool_dir / ".git" / "config" config_text = config_path.read_text(encoding="utf-8") - config_path.write_text(config_text.replace(f"\turl = {upstream_repo}\n", "\turl = --upload-pack=env\n"), encoding="utf-8") + config_path.write_text( + config_text.replace(f"\turl = {upstream_repo}\n", "\turl = --upload-pack=env\n"), encoding="utf-8" + ) app = _build_app(proxy_settings) body = b'{"repo":"octo/widget"}' @@ -1148,8 +1152,6 @@ async def test_git_fetch_rejects_option_shaped_origin(proxy_settings: Settings, assert resp.status_code == 400, resp.text - - @pytest.mark.parametrize( "clone_url", [ @@ -1205,6 +1207,7 @@ async def test_git_push_rejects_attacker_pushurl(proxy_settings: Settings, upstr assert resp.status_code == 400, resp.text assert not _bare_has_branch(upstream_repo, branch) + # ============================================================================ # fetch_ref refuses refspec / option injection in `ref` # ============================================================================ diff --git a/python/robomp/tests/test_sandbox.py b/python/robomp/tests/test_sandbox.py index fa002127d..272f05ecb 100644 --- a/python/robomp/tests/test_sandbox.py +++ b/python/robomp/tests/test_sandbox.py @@ -1110,7 +1110,9 @@ def test_remove_workspace(tmp_path: Path, upstream_repo: Path) -> None: assert not ws.root.exists() -def test_remove_workspace_prunes_pool_after_failed_worktree_remove(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_remove_workspace_prunes_pool_after_failed_worktree_remove( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: mgr = SandboxManager(tmp_path) # Create a real repo_dir on disk so `repo_dir.exists()` is True on entry. ws_root = mgr.workspace_root("o/r", 7) @@ -1185,6 +1187,7 @@ def test_remove_workspace_prunes_when_failed_remove_already_deleted_checkout( prune_idx = next(i for i, (c, _) in enumerate(calls) if c == ["git", "worktree", "prune"]) assert calls[prune_idx][1] == pool, "prune did not run in the repo's pool dir" + def test_redact_credentials_strips_userinfo() -> None: from robomp.sandbox import redact_credentials @@ -1911,7 +1914,9 @@ def test_run_timeout_raises_git_command_error_124(monkeypatch: pytest.MonkeyPatc assert seen["timeout"] == s._DEFAULT_SANDBOX_SUBPROCESS_TIMEOUT -def test_ensure_workspace_raises_when_local_branch_probe_times_out(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_ensure_workspace_raises_when_local_branch_probe_times_out( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: mgr = SandboxManager(tmp_path) mgr.natives_cache = None mgr.transport = SimpleNamespace( @@ -1946,7 +1951,9 @@ def test_ensure_workspace_raises_when_local_branch_probe_times_out(tmp_path: Pat ) -def test_ensure_workspace_raises_when_remote_branch_probe_times_out(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_ensure_workspace_raises_when_remote_branch_probe_times_out( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: mgr = SandboxManager(tmp_path) mgr.natives_cache = None mgr.transport = SimpleNamespace( diff --git a/python/robomp/tests/test_status_contract.py b/python/robomp/tests/test_status_contract.py index 324e6d99d..a9ae836ac 100644 --- a/python/robomp/tests/test_status_contract.py +++ b/python/robomp/tests/test_status_contract.py @@ -18,7 +18,6 @@ from robomp.config import Settings, reset_settings_cache from robomp.db import get_database from robomp.server import create_app - # Runtime/timestamp fields vary every run; normalize them so the live payload # can be compared against (or regenerated into) a byte-stable committed fixture. _VOLATILE_TS_KEYS = {"received_at", "started_at", "last_tool_ts", "updated_at"} @@ -47,7 +46,7 @@ def test_status_contract(settings: Settings) -> None: with TestClient(app) as client: # Seed AFTER startup: db = get_database(settings.sqlite_path) - + # 1. A running issue with live detail: db.upsert_issue( key="octo/widget#1", @@ -169,15 +168,31 @@ def test_status_contract(settings: Settings) -> None: data = resp.json() # Assert Python-side: top-level keys exactly: - expected_keys = {"runtime", "event_counts", "issue_event_counts", "running_events", "inflight", "issues", "recent_events"} + expected_keys = { + "runtime", + "event_counts", + "issue_event_counts", + "running_events", + "inflight", + "issues", + "recent_events", + } assert set(data.keys()) == expected_keys # check running_events[0] keys: running_ev = data["running_events"] assert len(running_ev) == 1 assert set(running_ev[0].keys()) == { - "delivery_id", "event_type", "repo", "issue_key", "received_at", - "started_at", "attempts", "model", "last_tool", "last_tool_ts" + "delivery_id", + "event_type", + "repo", + "issue_key", + "received_at", + "started_at", + "attempts", + "model", + "last_tool", + "last_tool_ts", } assert running_ev[0]["model"] == "anthropic/claude-3-5-sonnet" assert running_ev[0]["last_tool"] == "edit" @@ -185,13 +200,25 @@ def test_status_contract(settings: Settings) -> None: # check issues keys / latest_event keys: for issue_row in data["issues"]: assert set(issue_row.keys()) == { - "key", "repo", "number", "branch", "pr_number", - "state", "classification", "updated_at", "latest_event", + "key", + "repo", + "number", + "branch", + "pr_number", + "state", + "classification", + "updated_at", + "latest_event", } latest = issue_row["latest_event"] if latest is not None: assert set(latest.keys()) == { - "delivery_id", "event_type", "state", "attempts", "received_at", "last_error" + "delivery_id", + "event_type", + "state", + "attempts", + "received_at", + "last_error", } # check runtime: @@ -358,4 +385,3 @@ def test_retry_state_transition(env, monkeypatch: pytest.MonkeyPatch) -> None: evt = db.get_event("failed-retry-1") assert evt is not None assert evt.state == "queued" - diff --git a/python/robomp/tests/test_tasks_directive.py b/python/robomp/tests/test_tasks_directive.py index 73f1a3fad..4e69470cd 100644 --- a/python/robomp/tests/test_tasks_directive.py +++ b/python/robomp/tests/test_tasks_directive.py @@ -5,6 +5,7 @@ from __future__ import annotations from types import SimpleNamespace import pytest + from robomp import tasks from robomp.github_client import IssueInfo, RepoInfo from robomp.tasks import _attach_thread, _directive_from_payload @@ -91,7 +92,6 @@ async def test_attach_thread_preserves_authorizes_impl(monkeypatch: pytest.Monke assert hydrated.authorizes_impl is True - def _payload_with_directive(*, issue_number: int, body: str = "@robomp-bot ship it") -> dict[str, object]: return { "repository": { @@ -264,4 +264,4 @@ async def test_handle_pr_conversation_preserves_authorizes_impl_to_run_task( "task_kind": "handle_comment", "pr_number": 7, "run_task_authorizes_impl": True, - } \ No newline at end of file + }