claude-dev-env 8.19.1 → 8.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import sys
6
+ import urllib.parse
6
7
  from pathlib import Path
7
8
 
8
9
  import pytest
@@ -14,6 +15,7 @@ if str(_SCRIPTS_DIRECTORY) not in sys.path:
14
15
  import agent_merge_check
15
16
  from dev_env_scripts_constants.agent_merge_check_constants import (
16
17
  BEHIND_HOLD_REASON,
18
+ BEHIND_MERGE_QUEUE_HOLD_TEMPLATE,
17
19
  BLOCKED_HOLD_REASON,
18
20
  DIRTY_HOLD_REASON,
19
21
  DRAFT_HOLD_REASON,
@@ -288,6 +290,11 @@ def test_a_ready_pull_request_exits_zero(monkeypatch: pytest.MonkeyPatch) -> Non
288
290
  "read_unresolved_thread_count",
289
291
  lambda slug, number, token: 0,
290
292
  )
293
+ monkeypatch.setattr(
294
+ agent_merge_check,
295
+ "read_merge_queue_ejection",
296
+ lambda slug, number, token: False,
297
+ )
291
298
  assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 0
292
299
 
293
300
 
@@ -303,6 +310,11 @@ def test_a_held_pull_request_exits_one(monkeypatch: pytest.MonkeyPatch) -> None:
303
310
  "read_unresolved_thread_count",
304
311
  lambda slug, number, token: 0,
305
312
  )
313
+ monkeypatch.setattr(
314
+ agent_merge_check,
315
+ "read_merge_queue_ejection",
316
+ lambda slug, number, token: False,
317
+ )
306
318
  assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 1
307
319
 
308
320
 
@@ -315,3 +327,458 @@ def test_an_unreadable_pull_request_exits_two(
315
327
  monkeypatch.setenv("GH_TOKEN", "token")
316
328
  monkeypatch.setattr(agent_merge_check, "read_pull_request", _fail)
317
329
  assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 2
330
+
331
+
332
+ HEAD_COMMITTED_AT = "2026-09-26T16:00:00Z"
333
+ PULL_REQUEST_4922_HEAD_SHA = "06bc18a1a8761a16e92c1419693a42f22dd4ae7c"
334
+ REQUIRED_CHECK_APP_ID = 15368
335
+ PULL_REQUEST_4922 = {
336
+ "number": 4922,
337
+ "draft": False,
338
+ "mergeable_state": "blocked",
339
+ "base": {"ref": "main"},
340
+ "head": {"sha": PULL_REQUEST_4922_HEAD_SHA},
341
+ }
342
+ MAIN_BRANCH_RULES = [
343
+ {
344
+ "type": "required_status_checks",
345
+ "parameters": {
346
+ "strict_required_status_checks_policy": False,
347
+ "required_status_checks": [
348
+ {"context": "Ruff", "integration_id": REQUIRED_CHECK_APP_ID},
349
+ {"context": "Tip Local green", "integration_id": REQUIRED_CHECK_APP_ID},
350
+ ],
351
+ },
352
+ },
353
+ {"type": "merge_queue", "parameters": {"merge_method": "MERGE"}},
354
+ ]
355
+
356
+
357
+ def _check_run(
358
+ name: str,
359
+ conclusion: str | None,
360
+ run_id: int = 1,
361
+ app_id: int = REQUIRED_CHECK_APP_ID,
362
+ status: str = "completed",
363
+ ) -> dict[str, object]:
364
+ return {
365
+ "id": run_id,
366
+ "name": name,
367
+ "status": status,
368
+ "conclusion": conclusion,
369
+ "app": {"id": app_id},
370
+ }
371
+
372
+
373
+ def _github_answers(
374
+ all_rules: list[object],
375
+ all_check_runs: list[dict[str, object]],
376
+ behind_by: int,
377
+ all_statuses: list[object] | None = None,
378
+ ) -> object:
379
+ def _answer(url: str, token: str, all_payload_fields: object) -> object:
380
+ parsed = urllib.parse.urlparse(url)
381
+ if parsed.path.endswith("/pulls/4922"):
382
+ return PULL_REQUEST_4922
383
+ if parsed.path.endswith("/ccr/review_threads"):
384
+ return []
385
+ if "/rules/branches/" in parsed.path:
386
+ return all_rules
387
+ if parsed.path.endswith("/check-runs"):
388
+ check_name = urllib.parse.parse_qs(parsed.query)["check_name"][0]
389
+ return {
390
+ "check_runs": [
391
+ each_run
392
+ for each_run in all_check_runs
393
+ if each_run["name"] == check_name
394
+ ]
395
+ }
396
+ if parsed.path.endswith("/status"):
397
+ return {"statuses": all_statuses or []}
398
+ if "/compare/" in parsed.path:
399
+ return {"behind_by": behind_by, "ahead_by": 1}
400
+ if parsed.path.endswith("/graphql"):
401
+ return _merge_queue_document([])
402
+ raise AssertionError(url)
403
+
404
+ return _answer
405
+
406
+
407
+ def _merge_queue_document(all_removals: list[dict[str, object]]) -> object:
408
+ return {
409
+ "data": {
410
+ "repository": {
411
+ "pullRequest": {
412
+ "commits": {
413
+ "nodes": [{"commit": {"committedDate": HEAD_COMMITTED_AT}}]
414
+ },
415
+ "timelineItems": {"nodes": all_removals},
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+
422
+ def _run_main(
423
+ monkeypatch: pytest.MonkeyPatch,
424
+ capsys: pytest.CaptureFixture[str],
425
+ answer: object,
426
+ ) -> tuple[int, str]:
427
+ monkeypatch.setenv("GH_TOKEN", "token")
428
+ monkeypatch.setattr(agent_merge_check, "_request_json", answer)
429
+ exit_code = agent_merge_check.main(["jl-cmd/claude-dev-env", "4922"])
430
+ return exit_code, capsys.readouterr().out
431
+
432
+
433
+ def test_a_green_head_behind_a_merge_queue_base_reports_the_behind_reason(
434
+ monkeypatch: pytest.MonkeyPatch,
435
+ capsys: pytest.CaptureFixture[str],
436
+ ) -> None:
437
+ exit_code, line = _run_main(
438
+ monkeypatch,
439
+ capsys,
440
+ _github_answers(
441
+ MAIN_BRANCH_RULES,
442
+ [
443
+ _check_run("Ruff", "success", run_id=11),
444
+ _check_run("Tip Local green", "success", run_id=12),
445
+ ],
446
+ behind_by=225,
447
+ ),
448
+ )
449
+ assert exit_code == 1
450
+ assert BEHIND_MERGE_QUEUE_HOLD_TEMPLATE.format(count=225) in line
451
+ assert BLOCKED_HOLD_REASON not in line
452
+ assert "06bc18a" in line
453
+
454
+
455
+ def test_a_red_required_check_is_named_ahead_of_the_behind_reason(
456
+ monkeypatch: pytest.MonkeyPatch,
457
+ capsys: pytest.CaptureFixture[str],
458
+ ) -> None:
459
+ exit_code, line = _run_main(
460
+ monkeypatch,
461
+ capsys,
462
+ _github_answers(
463
+ MAIN_BRANCH_RULES,
464
+ [
465
+ _check_run("Ruff", "success", run_id=11),
466
+ _check_run("Tip Local green", "failure", run_id=12),
467
+ ],
468
+ behind_by=225,
469
+ ),
470
+ )
471
+ assert exit_code == 1
472
+ assert "Tip Local green (failure)" in line
473
+ assert "Ruff" not in line
474
+ assert "behind" not in line
475
+
476
+
477
+ def test_an_unreadable_branch_rule_keeps_the_generic_blocked_reason(
478
+ monkeypatch: pytest.MonkeyPatch,
479
+ capsys: pytest.CaptureFixture[str],
480
+ ) -> None:
481
+ routed_answer = _github_answers(MAIN_BRANCH_RULES, [], behind_by=225)
482
+
483
+ def _answer(url: str, token: str, all_payload_fields: object) -> object:
484
+ if "/rules/branches/" in url:
485
+ raise agent_merge_check.MergeCheckError("HTTP Error 403: Forbidden")
486
+ return routed_answer(url, token, all_payload_fields)
487
+
488
+ exit_code, line = _run_main(monkeypatch, capsys, _answer)
489
+ assert exit_code == 1
490
+ assert BLOCKED_HOLD_REASON in line
491
+
492
+
493
+ def _evidence(
494
+ all_unmet_checks: tuple[str, ...] = (),
495
+ behind_by: int = 0,
496
+ has_merge_queue: bool = True,
497
+ ) -> agent_merge_check.BlockedEvidence:
498
+ return agent_merge_check.BlockedEvidence(
499
+ all_unmet_checks=all_unmet_checks,
500
+ behind_by=behind_by,
501
+ has_merge_queue=has_merge_queue,
502
+ )
503
+
504
+
505
+ @pytest.mark.parametrize(
506
+ ("evidence", "expected_reason"),
507
+ [
508
+ (_evidence(behind_by=0), BLOCKED_HOLD_REASON),
509
+ (_evidence(behind_by=3, has_merge_queue=False), BLOCKED_HOLD_REASON),
510
+ (
511
+ _evidence(behind_by=3),
512
+ BEHIND_MERGE_QUEUE_HOLD_TEMPLATE.format(count=3),
513
+ ),
514
+ ],
515
+ )
516
+ def test_a_blocked_head_with_every_required_check_green_reads_its_reason(
517
+ evidence: agent_merge_check.BlockedEvidence,
518
+ expected_reason: str,
519
+ ) -> None:
520
+ assert (
521
+ agent_merge_check.hold_reason(
522
+ _pull_request(mergeable_state="blocked"), 0, evidence
523
+ )
524
+ == expected_reason
525
+ )
526
+
527
+
528
+ def test_blocked_evidence_is_ignored_for_a_draft() -> None:
529
+ assert (
530
+ agent_merge_check.hold_reason(
531
+ _pull_request(mergeable_state="blocked", draft=True),
532
+ 0,
533
+ _evidence(behind_by=3),
534
+ )
535
+ == DRAFT_HOLD_REASON
536
+ )
537
+
538
+
539
+ def test_required_contexts_merge_every_required_status_checks_rule() -> None:
540
+ all_rules = [
541
+ *MAIN_BRANCH_RULES,
542
+ {
543
+ "type": "required_status_checks",
544
+ "parameters": {
545
+ "required_status_checks": [
546
+ {"context": "Ruff", "integration_id": REQUIRED_CHECK_APP_ID},
547
+ {"context": "Semgrep"},
548
+ ]
549
+ },
550
+ },
551
+ ]
552
+ assert agent_merge_check.required_contexts(all_rules) == (
553
+ agent_merge_check.RequiredContext("Ruff", REQUIRED_CHECK_APP_ID),
554
+ agent_merge_check.RequiredContext("Tip Local green", REQUIRED_CHECK_APP_ID),
555
+ agent_merge_check.RequiredContext("Semgrep", None),
556
+ )
557
+
558
+
559
+ def test_a_rule_set_without_a_merge_queue_rule_reports_none() -> None:
560
+ assert agent_merge_check.has_merge_queue_rule(MAIN_BRANCH_RULES)
561
+ assert not agent_merge_check.has_merge_queue_rule(MAIN_BRANCH_RULES[:1])
562
+
563
+
564
+ RUFF_FROM_REQUIRED_APP = agent_merge_check.RequiredContext(
565
+ "Ruff", REQUIRED_CHECK_APP_ID
566
+ )
567
+
568
+
569
+ @pytest.mark.parametrize(
570
+ ("all_check_runs", "all_statuses", "expected_state"),
571
+ [
572
+ ([_check_run("Ruff", "success")], [], None),
573
+ ([_check_run("Ruff", "skipped")], [], None),
574
+ ([_check_run("Ruff", "failure")], [], "failure"),
575
+ ([_check_run("Ruff", None, status="in_progress")], [], "pending"),
576
+ (
577
+ [
578
+ _check_run("Ruff", "failure", run_id=1),
579
+ _check_run("Ruff", "success", run_id=2),
580
+ ],
581
+ [],
582
+ None,
583
+ ),
584
+ (
585
+ [
586
+ _check_run("Ruff", "success", run_id=2),
587
+ _check_run("Ruff", "failure", run_id=3),
588
+ ],
589
+ [],
590
+ "failure",
591
+ ),
592
+ ([_check_run("Ruff", "success", app_id=1)], [], "missing"),
593
+ ([], [{"context": "Ruff", "state": "success"}], None),
594
+ ([], [{"context": "Ruff", "state": "pending"}], "pending"),
595
+ ([], [{"context": "Ruff", "state": "error"}], "error"),
596
+ ([], [], "missing"),
597
+ ],
598
+ )
599
+ def test_unmet_check_state_reads_the_newest_report_of_the_required_check(
600
+ all_check_runs: list[object],
601
+ all_statuses: list[object],
602
+ expected_state: str | None,
603
+ ) -> None:
604
+ assert (
605
+ agent_merge_check.unmet_check_state(
606
+ RUFF_FROM_REQUIRED_APP, all_check_runs, all_statuses
607
+ )
608
+ == expected_state
609
+ )
610
+
611
+
612
+ def test_read_blocked_evidence_reads_the_pull_request_4922_shape(
613
+ monkeypatch: pytest.MonkeyPatch,
614
+ ) -> None:
615
+ monkeypatch.setattr(
616
+ agent_merge_check,
617
+ "_request_json",
618
+ _github_answers(
619
+ MAIN_BRANCH_RULES,
620
+ [
621
+ _check_run("Ruff", "success", run_id=11),
622
+ _check_run("Tip Local green", "success", run_id=12),
623
+ ],
624
+ behind_by=225,
625
+ ),
626
+ )
627
+ assert agent_merge_check.read_blocked_evidence(
628
+ "jl-cmd/claude-dev-env", PULL_REQUEST_4922, "token"
629
+ ) == _evidence(behind_by=225)
630
+
631
+
632
+ def test_blocked_hold_reason_names_every_unmet_required_check() -> None:
633
+ reason = agent_merge_check.blocked_hold_reason(
634
+ _evidence(
635
+ all_unmet_checks=("Ruff (failure)", "Tip Local green (missing)"),
636
+ behind_by=225,
637
+ )
638
+ )
639
+ assert "Ruff (failure), Tip Local green (missing)" in reason
640
+ assert "behind" not in reason
641
+
642
+
643
+ QUEUED_PULL_REQUEST = {
644
+ "number": 1442,
645
+ "draft": False,
646
+ "mergeable_state": "clean",
647
+ "base": {"ref": "main"},
648
+ "head": {"sha": "ab845eb6945650055ebe852312a9057b9f067d6a"},
649
+ }
650
+
651
+
652
+ def _removal(created_at: str, reason: str) -> dict[str, object]:
653
+ return {"createdAt": created_at, "reason": reason}
654
+
655
+
656
+ def _merge_queue_answers(
657
+ all_removals: list[dict[str, object]],
658
+ ) -> object:
659
+ def _answer(url: str, token: str, all_payload_fields: object) -> object:
660
+ if url.endswith("/pulls/1442"):
661
+ return QUEUED_PULL_REQUEST
662
+ if url.endswith("/ccr/review_threads"):
663
+ return []
664
+ if url.endswith("/graphql"):
665
+ return _merge_queue_document(all_removals)
666
+ raise AssertionError(url)
667
+
668
+ return _answer
669
+
670
+
671
+ def _run_queued_main(
672
+ monkeypatch: pytest.MonkeyPatch,
673
+ capsys: pytest.CaptureFixture[str],
674
+ all_removals: list[dict[str, object]],
675
+ ) -> tuple[int, str]:
676
+ monkeypatch.setenv("GH_TOKEN", "token")
677
+ monkeypatch.setattr(
678
+ agent_merge_check, "_request_json", _merge_queue_answers(all_removals)
679
+ )
680
+ exit_code = agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"])
681
+ return exit_code, capsys.readouterr().out
682
+
683
+
684
+ def test_a_head_ejected_from_the_merge_queue_for_failed_checks_holds(
685
+ monkeypatch: pytest.MonkeyPatch,
686
+ capsys: pytest.CaptureFixture[str],
687
+ ) -> None:
688
+ exit_code, line = _run_queued_main(
689
+ monkeypatch,
690
+ capsys,
691
+ [
692
+ _removal("2026-09-26T15:09:20Z", "failed_checks"),
693
+ _removal("2026-09-26T17:11:35Z", "failed_checks"),
694
+ ],
695
+ )
696
+ assert exit_code == 1
697
+ assert line.startswith(HOLD_VERDICT_LABEL)
698
+ assert "merge queue" in line
699
+ assert "failed checks" in line
700
+ assert "merge_group" in line
701
+ assert "gh-readonly-queue/main/pr-1442-<sha>" in line
702
+ assert "before you enqueue it again" in line
703
+
704
+
705
+ def test_a_fix_pushed_after_the_merge_queue_ejection_clears_the_hold(
706
+ monkeypatch: pytest.MonkeyPatch,
707
+ capsys: pytest.CaptureFixture[str],
708
+ ) -> None:
709
+ exit_code, line = _run_queued_main(
710
+ monkeypatch,
711
+ capsys,
712
+ [_removal("2026-09-26T15:09:20Z", "failed_checks")],
713
+ )
714
+ assert exit_code == 0
715
+ assert line.startswith(MERGE_VERDICT_LABEL)
716
+
717
+
718
+ def test_a_manual_merge_queue_removal_does_not_hold(
719
+ monkeypatch: pytest.MonkeyPatch,
720
+ capsys: pytest.CaptureFixture[str],
721
+ ) -> None:
722
+ exit_code, line = _run_queued_main(
723
+ monkeypatch,
724
+ capsys,
725
+ [
726
+ _removal("2026-09-26T15:09:20Z", "failed_checks"),
727
+ _removal("2026-09-26T17:58:17Z", "manual"),
728
+ ],
729
+ )
730
+ assert exit_code == 0
731
+ assert line.startswith(MERGE_VERDICT_LABEL)
732
+
733
+
734
+ def test_a_repository_with_no_merge_queue_removal_may_merge(
735
+ monkeypatch: pytest.MonkeyPatch,
736
+ capsys: pytest.CaptureFixture[str],
737
+ ) -> None:
738
+ exit_code, line = _run_queued_main(monkeypatch, capsys, [])
739
+ assert exit_code == 0
740
+ assert line.startswith(MERGE_VERDICT_LABEL)
741
+
742
+
743
+ def test_a_merge_queue_answer_of_another_shape_exits_two(
744
+ monkeypatch: pytest.MonkeyPatch,
745
+ capsys: pytest.CaptureFixture[str],
746
+ ) -> None:
747
+ routed_answer = _merge_queue_answers([])
748
+
749
+ def _answer(url: str, token: str, all_payload_fields: object) -> object:
750
+ if url.endswith("/graphql"):
751
+ return {"errors": [{"message": "Bad credentials"}]}
752
+ return routed_answer(url, token, all_payload_fields)
753
+
754
+ monkeypatch.setenv("GH_TOKEN", "token")
755
+ monkeypatch.setattr(agent_merge_check, "_request_json", _answer)
756
+ assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 2
757
+ assert capsys.readouterr().out == ""
758
+
759
+
760
+ @pytest.mark.parametrize(
761
+ ("all_removals", "expected_ejection"),
762
+ [
763
+ ([_removal("2026-09-26T17:11:35Z", "failed_checks")], True),
764
+ ([_removal("2026-09-26T15:09:20Z", "failed_checks")], False),
765
+ ([_removal("2026-09-26T17:58:17Z", "manual")], False),
766
+ ([], False),
767
+ ],
768
+ )
769
+ def test_read_merge_queue_ejection_compares_failed_checks_removals_to_the_head(
770
+ monkeypatch: pytest.MonkeyPatch,
771
+ all_removals: list[dict[str, object]],
772
+ expected_ejection: bool,
773
+ ) -> None:
774
+ monkeypatch.setattr(
775
+ agent_merge_check,
776
+ "_request_json",
777
+ lambda url, token, all_payload_fields: _merge_queue_document(all_removals),
778
+ )
779
+ assert (
780
+ agent_merge_check.read_merge_queue_ejection(
781
+ "jl-cmd/claude-dev-env", 1442, "token"
782
+ )
783
+ is expected_ejection
784
+ )
@@ -139,6 +139,7 @@ def should_print_the_url_of_a_waiting_top_level_comment(
139
139
  bot_summary = model.TopLevelComment(
140
140
  identifier=1,
141
141
  author_login="qodo-merge-pro[bot]",
142
+ is_bot=True,
142
143
  created_at=posted,
143
144
  updated_at=posted,
144
145
  url=summary_url,
@@ -21,6 +21,7 @@ from dev_env_scripts_constants.review_closure_constants import (
21
21
 
22
22
  DRIVING_AGENT = frozenset({"claude[bot]"})
23
23
  REVIEW_BOT = "qodo-merge-pro[bot]"
24
+ REVIEWER = "maintainer"
24
25
 
25
26
 
26
27
  def thread(
@@ -313,10 +314,12 @@ def top_level_comment(
313
314
  posted_minute: int,
314
315
  edited_minute: int | None = None,
315
316
  url: str = BOT_SUMMARY_URL,
317
+ is_bot: bool = True,
316
318
  ) -> model.TopLevelComment:
317
319
  return model.TopLevelComment(
318
320
  identifier=posted_minute,
319
321
  author_login=author_login,
322
+ is_bot=is_bot,
320
323
  created_at=at_minute(posted_minute),
321
324
  updated_at=at_minute(posted_minute if edited_minute is None else edited_minute),
322
325
  url=url,
@@ -355,9 +358,9 @@ def should_close_every_earlier_comment_with_one_driver_comment() -> None:
355
358
  assert model.top_level_findings(all_comments, DRIVING_AGENT) == ()
356
359
 
357
360
 
358
- def should_reopen_a_bot_comment_edited_after_the_driver_reply() -> None:
361
+ def should_reopen_a_person_comment_edited_after_the_driver_reply() -> None:
359
362
  all_comments = (
360
- top_level_comment(REVIEW_BOT, 1, edited_minute=5),
363
+ top_level_comment(REVIEWER, 1, edited_minute=5, is_bot=False),
361
364
  top_level_comment("claude[bot]", 2),
362
365
  )
363
366
 
@@ -366,6 +369,15 @@ def should_reopen_a_bot_comment_edited_after_the_driver_reply() -> None:
366
369
  assert [each.subject for each in all_findings] == [BOT_SUMMARY_URL]
367
370
 
368
371
 
372
+ def should_keep_a_bot_summary_closed_when_edited_after_the_driver_reply() -> None:
373
+ all_comments = (
374
+ top_level_comment(REVIEW_BOT, 1, edited_minute=5),
375
+ top_level_comment("claude[bot]", 2),
376
+ )
377
+
378
+ assert model.top_level_findings(all_comments, DRIVING_AGENT) == ()
379
+
380
+
369
381
  def should_leave_a_bot_comment_posted_after_the_driver_open() -> None:
370
382
  all_comments = (
371
383
  top_level_comment("claude[bot]", 1),
@@ -403,9 +415,28 @@ def should_close_a_bot_comment_last_edited_at_the_driver_time() -> None:
403
415
  assert model.top_level_finding(comment, DRIVING_AGENT, at_minute(4)) is None
404
416
 
405
417
 
406
- def should_open_a_bot_comment_edited_after_the_driver_time() -> None:
418
+ def should_open_a_person_comment_edited_after_the_driver_time() -> None:
419
+ comment = top_level_comment(REVIEWER, 1, edited_minute=5, is_bot=False)
420
+
421
+ assert model.top_level_finding(
422
+ comment, DRIVING_AGENT, at_minute(4)
423
+ ) == model.OpenFinding(
424
+ subject=BOT_SUMMARY_URL,
425
+ reason=TOP_LEVEL_OPEN_REASON_TEMPLATE.format(author=REVIEWER),
426
+ )
427
+
428
+
429
+ def should_close_a_bot_comment_posted_before_and_edited_after_the_driver_time() -> (
430
+ None
431
+ ):
407
432
  comment = top_level_comment(REVIEW_BOT, 1, edited_minute=5)
408
433
 
434
+ assert model.top_level_finding(comment, DRIVING_AGENT, at_minute(4)) is None
435
+
436
+
437
+ def should_open_a_bot_comment_posted_after_the_driver_time() -> None:
438
+ comment = top_level_comment(REVIEW_BOT, 5)
439
+
409
440
  assert model.top_level_finding(
410
441
  comment, DRIVING_AGENT, at_minute(4)
411
442
  ) == model.OpenFinding(
@@ -426,7 +457,7 @@ def should_read_a_top_level_comment_record() -> None:
426
457
  comment = model.parse_top_level_comment(
427
458
  {
428
459
  "id": 1,
429
- "user": {"login": REVIEW_BOT},
460
+ "user": {"login": REVIEW_BOT, "type": "Bot"},
430
461
  "created_at": "2026-09-26T12:01:00Z",
431
462
  "updated_at": "2026-09-26T12:05:00Z",
432
463
  "html_url": BOT_SUMMARY_URL,
@@ -434,3 +465,17 @@ def should_read_a_top_level_comment_record() -> None:
434
465
  )
435
466
 
436
467
  assert comment == top_level_comment(REVIEW_BOT, 1, edited_minute=5)
468
+
469
+
470
+ def should_read_a_person_top_level_comment_record() -> None:
471
+ comment = model.parse_top_level_comment(
472
+ {
473
+ "id": 1,
474
+ "user": {"login": REVIEWER, "type": "User"},
475
+ "created_at": "2026-09-26T12:01:00Z",
476
+ "updated_at": "2026-09-26T12:05:00Z",
477
+ "html_url": BOT_SUMMARY_URL,
478
+ }
479
+ )
480
+
481
+ assert comment == top_level_comment(REVIEWER, 1, edited_minute=5, is_bot=False)
package/settings.json CHANGED
@@ -3,5 +3,6 @@
3
3
  "allow": [
4
4
  "WebFetch(domain:docs.github.com)"
5
5
  ]
6
- }
6
+ },
7
+ "advisorModel": "opus"
7
8
  }