"""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) == []