nisse-review: read long files in parts; a retry starts afresh
From its failed review of #156: read_file stopped at 20,000 characters with no way on, so the reviewer hunted each file's end a definition at a time and ran out of turns; its retry carried every read before it, and a model call timed out. Both drafts were lost. - read_file takes a start line and says where the next part starts. - Each run's view starts at its brief (Transcript.since); the log keeps the rest. - A run out of turns with points drafted posts them as a partial review. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
1 parent
082631eafe
commit
1a7836167d
7 files changed
+141
-12
No files matched your search
@@ -553,3 +553,67 @@ def test_the_config_names_every_problem(tmp_path: Path) -> None:
|
||||
ReviewConfig.from_tree({**tree, "matrix": {"x": 1}}, {"LLM": "k"})
|
||||
assert "matrix.x: unknown setting" in str(raised.value)
|
||||
assert "references ${GITEA}" in str(raised.value)
|
||||
|
||||
|
||||
def test_a_long_file_is_read_in_parts_to_its_end(tmp_path: Path) -> None:
|
||||
# #156: read_file stopped at 20,000 characters with no way on, so the
|
||||
# reviewer hunted the rest a line at a time and ran out of turns.
|
||||
from nisse_review.reader import MAX_FILE_CHARS, Repository
|
||||
|
||||
body = "".join(f"x{n} = {'y' * 60}\n" for n in range(1, 1001))
|
||||
gitea = FakeGitea(files={"long.py": body})
|
||||
repo = Repository(gitea, REPO, {"head": "abc"}, tmp_path) # type: ignore[arg-type]
|
||||
first = repo.dispatch("read_file", {"path": "long.py"}).result
|
||||
assert len(first) < MAX_FILE_CHARS + 200
|
||||
assert "lines 1-" in first and "of 1000" in first
|
||||
seen, start = [], 1
|
||||
while True:
|
||||
part = repo.dispatch("read_file", {"path": "long.py", "start": start})
|
||||
assert part.success
|
||||
seen += [
|
||||
int(r.split()[0])
|
||||
for r in part.result.splitlines()[1:]
|
||||
if r[:5].strip().isdigit()
|
||||
]
|
||||
if "read on with start=" not in part.result:
|
||||
break
|
||||
start = int(part.result.rsplit("start=", 1)[1].rstrip("]"))
|
||||
assert seen == list(range(1, 1001)) # every line, once
|
||||
bad = repo.dispatch("read_file", {"path": "long.py", "start": "2000"})
|
||||
assert not bad.success and "1 to 1000" in bad.result
|
||||
|
||||
|
||||
def test_a_retry_starts_afresh_from_its_brief(tmp_path: Path) -> None:
|
||||
# #156: the retry carried the failed run's reads, and a call timed out.
|
||||
prose = [AssistantText(text="Reading on...", model="m")]
|
||||
model = Scripted(prose, prose, *REVIEW) # prose twice: the first run fails
|
||||
w = world(tmp_path, model, attempts=2)
|
||||
w["scheduler"].round()
|
||||
assert pr_of(item(w)).attempts == 1
|
||||
w["scheduler"].round()
|
||||
retry = model.seen[2] # the retry's first call
|
||||
briefs = [e for e in retry if "Review o/demo#7" in str(e.event)]
|
||||
assert len(briefs) == 1 and retry[0] is briefs[0]
|
||||
assert not any("Reading on" in str(e.event) for e in retry)
|
||||
assert item(w).position == REVIEWED
|
||||
|
||||
|
||||
def test_out_of_turns_its_drafted_points_are_posted_as_partial(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
model = Scripted(*REVIEW[:3]) # drafts two comments, never posts
|
||||
w = world(tmp_path, model)
|
||||
w["scheduler"]._reviewer._max_turns = 3
|
||||
w["scheduler"].round()
|
||||
[posted] = w["gitea"].posted
|
||||
assert len(posted["comments"]) == 2 and "A partial review" in posted["body"]
|
||||
assert item(w).position == REVIEWED
|
||||
|
||||
|
||||
def test_out_of_turns_with_nothing_drafted_is_a_failed_run(tmp_path: Path) -> None:
|
||||
model = Scripted(REVIEW[0], REVIEW[0], REVIEW[0])
|
||||
w = world(tmp_path, model, attempts=2)
|
||||
w["scheduler"]._reviewer._max_turns = 3
|
||||
w["scheduler"].round()
|
||||
assert w["gitea"].posted == []
|
||||
assert (item(w).position, pr_of(item(w)).attempts) == (READY, 1)
|
||||
Reference in new issue
Block a user