Seats review as themselves: TIME_SEAT_<SEAT> token makes the verdict a real Gitea review #116
No files matched your search
@@ -1,5 +1,5 @@
|
||||
"""Offline tests for time.py: fake Gitea + fake Cursor, whole card lifecycle. Run: python3 .github/time/test_time.py"""
|
||||
import importlib.util, json, os, sys, datetime as dt
|
||||
import importlib.util, json, os, re, sys, datetime as dt
|
||||
import concurrent.futures as cf
|
||||
|
||||
# The suite assumes a clean environment: a worker's shell inherits the job's TIME_* settings and seat keys, so drop them
|
||||
@@ -112,6 +112,9 @@ class World:
|
||||
if method == "POST" and path.endswith("/merge"):
|
||||
assert body["Do"] == "squash"; self.gh_pr[int(path.split("/")[-2])]["pr"]["merged_at"] = iso(); return {}
|
||||
if method == "GET" and path.endswith(".diff"): return {}
|
||||
if method == "POST" and re.search(r"/pulls/\d+/reviews$", path):
|
||||
if getattr(self, "refuse_reviews", False): raise RuntimeError(f"POST {path} -> 422: approve your own pull is not allowed")
|
||||
self.reviews_posted = getattr(self, "reviews_posted", []) + [(int(path.split("/")[-2]), token, body)]; return {}
|
||||
if "/pulls/" in path:
|
||||
n = int(path.split("/")[5]); d = self.gh_pr[n]
|
||||
if path.endswith("/reviews?limit=100"): return d.get("reviews", [])
|
||||
@@ -215,6 +218,26 @@ tm.pr_diff = lambda n, limit=0: "diff --git a b"
|
||||
w = run(World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [pr]), {"TIME_MODE": "live", "CURSOR_API_KEY": "ck", "TIME_REVIEW": "venice:kimi"})
|
||||
assert w.posted[0][0] == 7 and w.posted[0][1].startswith("**Approve** (kimi)") and "`kimi` reviewed" in w.posted[1][1]
|
||||
|
||||
# 9a a seat with a TIME_SEAT_<SEAT> token reviews as its own user; refused or tokenless, Time comments
|
||||
def seat_run(text, refuse=False, token="kimi-tok"):
|
||||
tm.think = lambda seat, model, effort, system, user: text
|
||||
w = World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [pr]); w.refuse_reviews = refuse
|
||||
if token: os.environ["TIME_SEAT_KIMI"] = token
|
||||
try: return run(w, {"TIME_MODE": "live", "CURSOR_API_KEY": "ck", "TIME_REVIEW": "venice:kimi"})
|
||||
finally: os.environ.pop("TIME_SEAT_KIMI", None)
|
||||
w = seat_run("**Approve** looks right")
|
||||
n, tok, b = w.reviews_posted[0]
|
||||
assert (n, tok, b["event"]) == (7, "kimi-tok", "APPROVED") and b["body"].startswith("**Approve**") and "review by kimi" in b["body"] and "via Time</sub>" in b["body"]
|
||||
assert not [p_ for p_ in w.posted if p_[0] == 7] and any("`kimi` reviewed" in t for _, t in w.posted)
|
||||
assert seat_run("Object: breaks x").reviews_posted[0][2]["event"] == "REQUEST_CHANGES"
|
||||
assert seat_run("Some thoughts, no verdict").reviews_posted[0][2]["event"] == "COMMENT"
|
||||
w = seat_run("**Approve** fine", refuse=True)
|
||||
assert not getattr(w, "reviews_posted", []) and w.posted[0][0] == 7 and w.posted[0][1].startswith("**Approve** fine") and "review by kimi" in w.posted[0][1]
|
||||
w = seat_run("**Approve** fine", token=None)
|
||||
assert not getattr(w, "reviews_posted", []) and w.posted[0][0] == 7
|
||||
os.environ.pop("TIME_SEAT_HAIKU", None); assert tm.seat_token("haiku") is None
|
||||
os.environ["TIME_SEAT_HAIKU"] = "h-tok"; assert tm.seat_token("haiku") == "h-tok"; os.environ.pop("TIME_SEAT_HAIKU")
|
||||
|
||||
# 9b the reviewer's input carries the head commit's statuses under # Checks, before the diff; a failed lookup says so
|
||||
seen = {}
|
||||
tm.think = lambda seat, model, effort, system, user: (seen.update(user=user, system=system), "**Approve** ok")[1]
|
||||
|
||||
+25
-3
@@ -1007,6 +1007,29 @@ def check_lines(sha: str | None) -> str:
|
||||
return "(checks unavailable)"
|
||||
|
||||
|
||||
def seat_token(seat: str) -> str | None:
|
||||
"""The Gitea token of the user this seat posts as (`TIME_SEAT_<SEAT>`), or None."""
|
||||
return os.environ.get(f"TIME_SEAT_{seat.upper()}") or None
|
||||
|
||||
|
||||
def post_review(pr_number: int, seat: str, model: str | None, text: str) -> str:
|
||||
"""Post a reviewer's verdict. With the seat's token it is a real review by the seat's own user (Approve ->
|
||||
APPROVED, Object -> REQUEST_CHANGES, else COMMENT); without one, or if Gitea refuses (a seat on its own PR),
|
||||
it is Time's comment as before. Returns "seat" or "time"."""
|
||||
body = f"{text}\n\n<sub>review by {seat} · {model or SEATS[seat]['default']} · via Time</sub>"
|
||||
token = seat_token(seat)
|
||||
if token:
|
||||
m = VERDICT.match(text or "")
|
||||
event = {"approve": "APPROVED", "object": "REQUEST_CHANGES"}.get(m.group(1).lower() if m else "", "COMMENT")
|
||||
try:
|
||||
http("POST", f"{GH}/repos/{REPO}/pulls/{pr_number}/reviews", token, {"body": body, "event": event})
|
||||
return "seat"
|
||||
except RuntimeError as e:
|
||||
print(f"review as {seat} refused, commenting as Time: {e}")
|
||||
gh("POST", f"/repos/{REPO}/issues/{pr_number}/comments", {"body": body})
|
||||
return "time"
|
||||
|
||||
|
||||
def review(card: dict, pr: dict, done: str) -> dict:
|
||||
"""TIME_REVIEW: off | venice:<seat> | claude:<seat> [model] [effort] | cursor[:<model id>]"""
|
||||
spec = (os.environ.get("TIME_REVIEW") or "off").strip()
|
||||
@@ -1031,9 +1054,8 @@ def review(card: dict, pr: dict, done: str) -> dict:
|
||||
+ "\nThe checks listed under # Checks are the head commit's real statuses: evidence you can rely on "
|
||||
"(a success there means that context reported success). A pending check is not yet evidence either way.")
|
||||
text = think(seat, model, effort, system, user)
|
||||
gh("POST", f"/repos/{REPO}/issues/{pr['number']}/comments",
|
||||
{"body": f"{text}\n\n<sub>review by {seat} · {model or SEATS[seat]['default']} · via Time</sub>"})
|
||||
return {"review": "venice", "seat": seat}
|
||||
who = post_review(pr["number"], seat, model, text)
|
||||
return {"review": "venice", "seat": seat, "as": who}
|
||||
return {"review": f"unknown spec {spec!r}"}
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
- The second fix round escalates to Kimi's code model (`venice:kimi code`) instead of grok 4.7: still a different model from the first round, at a fraction of the cost. `TIME_VENTURE_ESCALATE` overrides it.
|
||||
|
||||
### Added
|
||||
- Seats review as themselves: with a `TIME_SEAT_<SEAT>` token (one Gitea user per seat, e.g. `TIME_SEAT_KIMI`) the reviewer's verdict is a real Gitea review by that user (Approve → approved, Object → changes requested, prose → comment), so reviews carry an identity and can count. Without a token, or if Gitea refuses (a seat on its own PR), it is Time's comment as before.
|
||||
- `TIME_PROTECTED` now governs every protected path except the workflow directories (`.github/workflows/`, `.gitea/workflows/`), which stay a human's merge whatever it says. The default still protects all of `.github/`; a repo that trusts its reviewer can set `TIME_PROTECTED=.github/workflows/,.gitea/workflows/,deploy,infra/,.env,secret` so the engine under `.github/time/` merges itself on Approve + green like any other code.
|
||||
- Objections go back to the worker. `/time retry` on a card whose PR is open hands the worker the reviewer's latest comments (marked as the reviewer's, strangers' left out) on the same branch; `open_pr` on an open PR updates its description instead of failing; the card goes back to `dispatched`, so the next beat reviews again and an objection older than the push no longer counts.
|
||||
- A batch's children start in the tick that spawned them (GitHub lists a card made a moment ago only later, so they used to wait a tick).
|
||||
|
||||
Reference in new issue
Block a user