"""The crowd's order of speaking, as arithmetic. `crowd.next_turn` is a pure function so that the interesting half of this feature — every way a round refuses to continue — can be tested without an endpoint, a session or a clock. The order the owner asked for is one sequence, and getting it wrong in either direction is a feature that looks like it works: a backward pass that starts on the speaker who has just spoken asks it whether it disagrees with itself, and one that runs to the main model twice gives it two closing turns. """ from __future__ import annotations from datetime import UTC, datetime, timedelta from lembas.services import crowd def _first(speakers: int) -> crowd.Turn: turn = crowd.next_turn(speakers=speakers, state=None, turn_id="u1") assert turn is not None return turn def _walk(speakers: int, *, again_at: set[int] = frozenset(), max_rounds: int = 2) -> list[str]: """The whole sequence as `phase/index` strings, for one readable assertion.""" state = None seen: list[str] = [] for _ in range(60): again = state is not None and state.round in again_at and state.phase == crowd.PHASE_CLOSE turn = crowd.next_turn( speakers=speakers, state=state, turn_id="u1", again=again, max_rounds=max_rounds, ) if turn is None or turn.stopped: if turn is not None and turn.stopped: seen.append(f"stopped:{turn.stopped}") break seen.append(f"{turn.phase}/{turn.index}") state = turn return seen # --- The order ---------------------------------------------------------------- def test_one_model_is_not_a_crowd(): """The chat's own model with nobody else answers exactly as it always did.""" assert crowd.next_turn(speakers=1, state=None, turn_id="u1") is None def test_two_speakers_go_out_and_straight_back_to_the_main_model(): """With one member there is nobody to ask on the way back, so the round is main, member, main — and the backward pass is empty rather than asking the member about its own answer.""" assert _walk(2) == ["out/1", "close/0"] def test_three_speakers_come_back_through_the_middle(): assert _walk(3) == ["out/1", "out/2", "back/1", "close/0"] def test_five_speakers_walk_out_and_back_in_order(): assert _walk(5) == [ "out/1", "out/2", "out/3", "out/4", "back/3", "back/2", "back/1", "close/0", ] def test_the_way_back_never_asks_the_last_speaker_about_itself(): """It starts one short of the speaker that has just finished.""" for speakers in range(2, 7): sequence = _walk(speakers) out = [s for s in sequence if s.startswith("out/")] back = [s for s in sequence if s.startswith("back/")] if back: assert back[0] != out[-1].replace("out/", "back/") def test_the_main_model_gets_exactly_one_closing_turn(): for speakers in range(2, 7): assert _walk(speakers).count("close/0") == 1 def test_the_first_reply_is_not_scheduled_by_this(): """The composer starts it, as it always has. A round *begins* at the second speaker, which is why `state=None` returns index 1.""" assert _first(4).index == 1 assert _first(4).phase == crowd.PHASE_OUT assert _first(4).round == 1 def test_the_size_of_the_round_is_recorded_on_every_turn(): """`of` is what the chip in the transcript counts against.""" turn = _first(4) assert turn.of == 4 # --- Going round again --------------------------------------------------------- def test_without_being_asked_the_round_ends_at_the_main_model(): assert _walk(3, again_at=set()) == ["out/1", "out/2", "back/1", "close/0"] def test_asked_for_another_round_it_starts_again_at_the_second_speaker(): """The main model has just spoken as the closer, so round two begins with the others rather than with it.""" sequence = _walk(3, again_at={1}, max_rounds=2) assert sequence == [ "out/1", "out/2", "back/1", "close/0", "out/1", "out/2", "back/1", "close/0", ] def test_the_round_cap_stops_it_and_says_why(): """Reached rather than never: the cap is a ceiling on ordinary work here, unlike a runaway backstop, so somebody has to be able to see it was hit.""" sequence = _walk(3, again_at={1, 2, 3}, max_rounds=2) assert sequence[-1] == f"stopped:{crowd.STOPPED_ROUNDS}" assert sequence.count("close/0") == 2 def test_one_round_means_one_round(): sequence = _walk(3, again_at={1, 2}, max_rounds=1) assert sequence.count("close/0") == 1 assert sequence[-1] == f"stopped:{crowd.STOPPED_ROUNDS}" # --- Running out of time ------------------------------------------------------- def _stale(seconds: int) -> crowd.Turn: began = datetime.now(UTC) - timedelta(seconds=seconds) return crowd.Turn( turn="u1", round=1, phase=crowd.PHASE_OUT, index=1, of=4, started_at=began.isoformat(), ) def test_a_round_that_has_run_long_enough_is_stopped(): stopped = crowd.next_turn(speakers=4, state=_stale(1000), turn_id="u1", wall_seconds=900) assert stopped is not None assert stopped.stopped == crowd.STOPPED_TIME def test_a_round_inside_its_time_carries_on(): turn = crowd.next_turn(speakers=4, state=_stale(10), turn_id="u1", wall_seconds=900) assert turn is not None assert not turn.stopped assert turn.index == 2 def test_the_clock_covers_the_whole_turn_not_one_speaker(): """`started_at` is carried from the round's first turn, never refreshed, so a crowd of slow members cannot outrun the limit one speaker at a time.""" first = _first(4) second = crowd.next_turn(speakers=4, state=first, turn_id="u1") assert second is not None assert second.started_at == first.started_at def test_an_unreadable_stamp_reads_as_no_time_passed(): """A round abandoned because of a bad timestamp would be a feature failing for a reason nobody could see.""" broken = crowd.Turn( turn="u1", round=1, phase=crowd.PHASE_OUT, index=1, of=4, started_at="not a date" ) turn = crowd.next_turn(speakers=4, state=broken, turn_id="u1", wall_seconds=1) assert turn is not None assert not turn.stopped # --- Errors ------------------------------------------------------------------- def test_one_speaker_failing_is_skipped_rather_than_ending_the_round(): """The commonest failure is a small member's window overflowing on a transcript several models have written into. Ending the round there would kill every crowd at whichever member is smallest.""" turn = crowd.next_turn(speakers=5, state=_first(5), turn_id="u1", errored=True) assert turn is not None assert not turn.stopped assert turn.index == 2 assert turn.errors == 1 def test_two_failures_in_a_row_end_the_round(): """Which is `_drain`'s protection kept: the endpoint has actually gone, and feeding it the next prompt produces a second failure and spends the words to do it.""" first = crowd.next_turn(speakers=5, state=_first(5), turn_id="u1", errored=True) second = crowd.next_turn(speakers=5, state=first, turn_id="u1", errored=True) assert second is not None assert second.stopped == crowd.STOPPED_ERRORS def test_the_count_is_of_consecutive_failures(): """One failure, then a success, then a failure is not a dead endpoint.""" state = crowd.next_turn(speakers=6, state=_first(6), turn_id="u1", errored=True) assert state.errors == 1 state = crowd.next_turn(speakers=6, state=state, turn_id="u1", errored=False) assert state.errors == 0 state = crowd.next_turn(speakers=6, state=state, turn_id="u1", errored=True) assert state is not None assert not state.stopped # --- What is stored ----------------------------------------------------------- def test_the_state_survives_a_round_trip_through_the_row(): """It is read back off a message after a restart, so the two halves have to agree exactly.""" class Row: crowd_json = None turn = _first(4) Row.crowd_json = turn.as_json() assert crowd.state_of(Row) == turn def test_a_message_with_no_state_is_not_part_of_a_round(): class Row: crowd_json = None assert crowd.state_of(Row) is None assert crowd.state_of(None) is None def test_nonsense_on_the_row_reads_as_no_round(): """A hand-edited database must not raise inside the generation loop.""" class Row: crowd_json = {"round": "third", "index": None} assert crowd.state_of(Row) is None