Files
nisse/nisse-review/tests/test_review_stop.py
T
dongarandClaude Opus 5.5 cb638fe687
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m57s
CI / e2e (pull_request) Successful in 4m57s
Compaction: the model's notes replace the middle of a long run (#158)
Before each driver call, over compact_at (0.6) of the prompt budget, the
same model is asked for notes over the very request it was about to get,
plus one message, so the server's cache covers all but the notes. The notes,
with code's list of the calls they replace, become a compaction event that
renders in place of its range, keeping word for word what started the work
and the last few turns. Rare, large steps keep the prompt's start unchanged
between them. A later compaction folds in the earlier; one from an earlier
run's view is hidden.

Failed notes are logged and fitting takes over. Fitting now applies
compactions before it measures, records what it cuts (Trim) and logs each
cut as context.trimmed. Wired into both agents' drivers and the benchmark;
nisse-dev's helper loop is a follow-up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
2026-10-17 22:51:36 +00:00

173 lines
6.4 KiB
Python

"""A clean stop (L2): SIGTERM ends a review run at its next turn, or breaks
off its model call, and the item goes back to ready with no failure counted;
a repository Gitea won't list is logged when that changes, not every round."""
import io
import json
from collections.abc import Callable
from pathlib import Path
from typing import Any
from review_fakes import FakeGitea, Scripted, call, pr
from test_review import REPO, REVIEW, item, world
from nisse_core.models.interruptible import Interruptible, Stopped
from nisse_core.oplog import OpLog
from nisse_review.build import build
from nisse_review.config import ReviewConfig
from nisse_review.review import reply_marker
from nisse_review.state import READY, REVIEWED, pr_of
class Stopping(Scripted):
"""Scripted, but at call ``at`` (0-based) it first runs ``then``: a stop
arriving while the model works on that turn."""
def __init__(self, *turns: list[Any], at: int) -> None:
super().__init__(*turns)
self.at = at
self.then: Callable[[], None] = lambda: None
self.calls = 0
def complete(self, events, tools, model, system=None): # type: ignore[no-untyped-def]
now, self.calls = self.calls, self.calls + 1
if now == self.at:
self.then()
return super().complete(events, tools, model, system)
def events(out: io.StringIO, name: str) -> list[dict[str, Any]]:
found = (json.loads(line) for line in out.getvalue().splitlines())
return [e for e in found if e["event"] == name]
def logged(w: dict[str, Any]) -> io.StringIO:
out = io.StringIO()
w["scheduler"]._oplog = OpLog("t", out)
return out
def test_a_stop_ends_the_run_at_its_next_turn(tmp_path: Path) -> None:
model = Stopping(*REVIEW, at=1)
w = world(tmp_path, model)
model.then = w["scheduler"].stop
out = logged(w)
assert w["scheduler"].round() is True
assert model.calls == 2 # the turn in flight finished; no third
assert (item(w).position, pr_of(item(w)).attempts) == (READY, 0)
assert pr_of(item(w)).run == "" and w["gitea"].posted == []
assert len(events(out, "review.interrupted")) == 1
assert events(out, "review.failed") == []
w["scheduler"].run_forever() # stopped: returns at once
def test_a_stop_breaks_off_the_model_call_in_flight(tmp_path: Path) -> None:
model = Stopping(*REVIEW, at=1)
wrapped = Interruptible(model)
w = world(tmp_path, wrapped) # type: ignore[arg-type]
def sigterm() -> None: # what Built.stop does, in the signal handler
w["scheduler"].stop()
wrapped.interrupt()
model.then = sigterm
out = logged(w)
assert w["scheduler"].round() is True
assert (item(w).position, pr_of(item(w)).attempts) == (READY, 0)
assert w["gitea"].posted == []
[said] = events(out, "review.interrupted")
assert "broke off the model call" in said["cause"]
def test_outside_a_model_call_interrupt_does_nothing() -> None:
wrapped = Interruptible(Scripted([]))
wrapped.interrupt() # nothing in flight: no Stopped
assert wrapped.complete([], None, "m") == []
wrapped.interrupt() # the call is over: still nothing
assert wrapped.offered == [set()] # the rest reaches the client
def test_stopped_is_no_failure_handlers_would_take() -> None:
assert not issubclass(Stopped, Exception)
def test_replies_posted_before_a_stop_are_not_posted_again(tmp_path: Path) -> None:
model = Stopping(
*REVIEW,
[call("reply", point="P1", text="Tabs never occur here.")],
[call("reply", point="P2", text="It counts words, yes.")], # stopped here
[call("reply", point="P1", text="It counts words, yes.")],
[call("done")],
at=5,
)
gitea = FakeGitea(repos={REPO: [pr()]})
w = world(tmp_path, model, gitea, operators=frozenset({"don"}))
model.then = w["scheduler"].stop
assert w["scheduler"].round() is True
first = w["gitea"].say("nisse-dev", "Why flag split()?")
second = w["gitea"].say("nisse-dev", "Does it count words?")
assert w["scheduler"].round() is True # replied to P1, then stopped
assert item(w).position == READY
[(_, _, posted)] = w["gitea"].comments
assert reply_marker(f"comment-{first}") in posted
w["scheduler"]._stop.clear()
w["scheduler"]._cancel.clear()
assert w["scheduler"].round() is True # only the second is left
bodies = [body for _, _, body in w["gitea"].comments]
assert sum(reply_marker(f"comment-{first}") in b for b in bodies) == 1
assert sum(reply_marker(f"comment-{second}") in b for b in bodies) == 1
assert item(w).position == REVIEWED
# --- a repository Gitea won't list ---------------------------------------------
def test_a_repository_that_keeps_failing_is_logged_once(tmp_path: Path) -> None:
gitea = FakeGitea(repos={"a/empty": [], REPO: []}, unlisted={"a/empty"})
w = world(tmp_path, Scripted(), gitea)
out = logged(w)
for _ in range(3):
w["scheduler"].round()
assert len(events(out, "intake.repo_failed")) == 1
gitea.unlisted.clear()
w["scheduler"].round()
w["scheduler"].round()
[ok] = events(out, "intake.repo_ok")
assert ok["repo"] == "a/empty"
gitea.unlisted.add("a/empty") # failing again is news again
w["scheduler"].round()
assert len(events(out, "intake.repo_failed")) == 2
def test_build_wires_a_stop_to_every_model_call(tmp_path: Path) -> None:
config = ReviewConfig.from_tree(
{
"state_db": str(tmp_path / "state.db"),
"llm": {"base_url": "http://llm.test", "token": "t"},
"roles": {"reviewer": "m", "expert": "e"},
"gitea": {
"url": "https://gitea.test",
"token": "t",
"operators": ["don"],
},
"repositories": [REPO],
},
{},
)
built = build(
config,
gitea=FakeGitea(repos={REPO: []}), # type: ignore[arg-type]
client=Scripted(),
oplog=OpLog("t", io.StringIO()),
)
try:
driver = built.scheduler._reviewer._driver
assert driver._client is built.model
# Compaction's notes are a model call a stop breaks off too.
assert driver._compactor._client is built.model
assert built.scheduler._reviewer._expert._client is built.model
built.stop() # no call in flight: nothing raised
assert driver._cancel.requested and built.scheduler._stop.is_set()
finally:
built.close()