"""Runs going in circles (L2, X6), judged from the turns alone.""" from nisse_core.loop.circles import Turn, apologies_in_a_row, find_circles NUDGE = "Your last reply was prose. Every reply must be a tool call." def comment(n: int) -> Turn: return Turn( text="", calls=(("post_comment", f'{{"body": "Duplicate, please disregard. ({n})"}}'),), ) def test_near_identical_replies_in_a_row_are_a_circle() -> None: # The Flash-Next review loop of 2026-10-24: the same comment posted over # and over, differing only in a counter. turns = [Turn(calls=(("read_file", '{"path": "a.py"}'),))] turns += [comment(n) for n in range(6)] turns += [Turn(calls=(("finish", "{}"),))] [circle] = find_circles(turns) assert circle.kind == "repeated reply" assert circle.turns == (1, 2, 3, 4, 5, 6) def test_work_that_moves_on_is_no_circle() -> None: turns = [ Turn(calls=(("read_file", '{"path": "a.py"}'),)), Turn(calls=(("run_command", '{"command": "pytest -q"}'),)), Turn(calls=(("edit_file", '{"path": "a.py", "old": "x", "new": "y"}'),)), Turn(calls=(("run_command", '{"command": "pytest -q"}'),)), Turn(text="Done.", calls=(("finish", "{}"),)), ] assert find_circles(turns) == [] def test_two_alike_is_not_yet_a_circle() -> None: assert find_circles([comment(1), comment(2), Turn(text="done")]) == [] def test_one_correction_given_again_and_again_is_a_circle() -> None: # Invented tools met with the prose reminder turn after turn, the replies # themselves all different. replies = [ "I'll run the linter first: lint_fix", "read_file src/review.py", "Installing the dependencies with install(requirements.txt) now.", "Let me use the skill tool to summarise the diff for the review.", ] turns = [Turn(text=r, told=(NUDGE,) if i else ()) for i, r in enumerate(replies)] [circle] = find_circles(turns) assert circle.kind == "repeated correction" assert circle.turns == (1, 2, 3) assert NUDGE[:40] in circle.detail def test_apologies_over_and_over_are_a_circle() -> None: turns = [ Turn(text="Sorry, I made an error reading the file."), Turn(calls=(("read_file", '{"path": "b.py"}'),)), Turn(text="My apologies, that was the wrong path."), Turn(calls=(("list_files", "{}"),)), Turn(text="Please disregard my previous comment."), ] [circle] = find_circles(turns) assert circle.kind == "repeated apology" assert circle.turns == (0, 2, 4) def test_the_limits_are_tuning() -> None: turns = [comment(1), comment(2)] assert [c.kind for c in find_circles(turns, repeats=2)] == ["repeated reply"] assert find_circles([comment(1), comment(22), comment(333)], similar=1.0) == [] def test_alike_replies_that_got_different_results_are_no_circle() -> None: read = [("read_file", '{"path": "src/a.py"}')] turns = [Turn(calls=tuple(read), results=(f"file {n}",)) for n in range(4)] assert find_circles(turns) == [] same = [Turn(calls=tuple(read), results=("posted",)) for _ in range(4)] assert [c.kind for c in find_circles(same)] == ["repeated reply"] # The page gives no results: alike replies alone are a circle there. bare = [Turn(calls=tuple(read)) for _ in range(3)] assert [c.kind for c in find_circles(bare)] == ["repeated reply"] def test_the_driver_counts_only_exactly_repeated_calls() -> None: adds = [ Turn(calls=(("run_command", f'{{"command": "git add {f}.py"}}'),)) for f in "abc" ] assert [c.kind for c in find_circles(adds)] == ["repeated reply"] # flagged assert find_circles(adds, exact_calls=True) == [] # never ended for it same = [Turn(calls=(("read_file", '{"path": "a"}'),))] * 3 assert len(find_circles(same, exact_calls=True)) == 1 def test_apologies_end_a_run_only_in_a_row() -> None: sorry, work = Turn(text="Sorry, my mistake."), Turn(text="Reading the file.") assert apologies_in_a_row([work, sorry, sorry, sorry]) is not None assert apologies_in_a_row([sorry, work, sorry, sorry]) is None assert apologies_in_a_row([sorry, sorry]) is None # In what it posted, too: an apology comment is an apology. posted = Turn(calls=(("post_comment", '{"body": "Please disregard (1)."}'),)) assert apologies_in_a_row([posted] * 3) is not None def test_the_same_call_answered_with_a_count_is_still_a_circle() -> None: note = (("comment", '{"line": 2, "text": "Same point."}'),) turns = [Turn(calls=note, results=(f"noted ({n} in the draft)",)) for n in range(3)] assert len(find_circles(turns, exact_calls=True)) == 1 files = [ Turn(calls=(("read_file", f'{{"path": "{f}"}}'),), results=(f"{f}: x",)) for f in "abc" ] assert find_circles(files, exact_calls=True) == []