@jesdi/skills 0.0.30 → 0.0.31

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.
Files changed (72) hide show
  1. package/README.md +12 -6
  2. package/package.json +2 -2
  3. package/skills/implement-spec/ARCHITECTURE.md +113 -0
  4. package/skills/implement-spec/README.md +214 -0
  5. package/skills/implement-spec/SKILL.md +18 -70
  6. package/skills/implement-spec/driver.py +580 -0
  7. package/skills/implement-spec/driver_agents.py +145 -0
  8. package/skills/implement-spec/driver_answers.py +79 -0
  9. package/skills/implement-spec/driver_decisions.py +152 -0
  10. package/skills/implement-spec/driver_final.py +505 -0
  11. package/skills/implement-spec/driver_fixloop.py +170 -0
  12. package/skills/implement-spec/driver_gate.py +47 -0
  13. package/skills/implement-spec/driver_graph.py +42 -0
  14. package/skills/implement-spec/driver_merge.py +105 -0
  15. package/skills/implement-spec/driver_report.py +115 -0
  16. package/skills/implement-spec/driver_review.py +126 -0
  17. package/skills/implement-spec/driver_review_page.py +51 -0
  18. package/skills/implement-spec/driver_run.py +262 -0
  19. package/skills/implement-spec/driver_slots.py +64 -0
  20. package/skills/implement-spec/driver_state.py +105 -0
  21. package/skills/implement-spec/driver_summary.py +40 -0
  22. package/skills/implement-spec/driver_tickets.py +132 -0
  23. package/skills/implement-spec/driver_tidy.py +57 -0
  24. package/skills/implement-spec/models.json +10 -0
  25. package/skills/implement-spec/prompts/conflict.md +32 -0
  26. package/skills/implement-spec/prompts/correction.md +4 -0
  27. package/skills/implement-spec/prompts/correctness.md +34 -0
  28. package/skills/implement-spec/prompts/final-fixer.md +36 -0
  29. package/skills/implement-spec/prompts/final-reviewer.md +36 -0
  30. package/skills/implement-spec/prompts/final-scoped.md +1 -0
  31. package/skills/implement-spec/prompts/fixer.md +35 -0
  32. package/skills/implement-spec/prompts/implementer.md +33 -0
  33. package/skills/implement-spec/prompts/interrupted.md +1 -0
  34. package/skills/implement-spec/prompts/moved.md +2 -0
  35. package/skills/implement-spec/prompts/overruled.md +5 -0
  36. package/skills/implement-spec/prompts/pr-body.md +19 -0
  37. package/skills/implement-spec/prompts/rebase-conflict.md +33 -0
  38. package/skills/implement-spec/prompts/rereview.md +3 -0
  39. package/skills/implement-spec/prompts/rerun.md +2 -0
  40. package/skills/implement-spec/prompts/reviewer.md +34 -0
  41. package/skills/implement-spec/prompts/stands.md +2 -0
  42. package/skills/implement-spec/prompts/test-writer.md +34 -0
  43. package/skills/implement-spec/prompts/timeout-restored.md +2 -0
  44. package/skills/implement-spec/prompts/timeout-shared.md +2 -0
  45. package/skills/implement-spec/prompts/timeout.md +2 -0
  46. package/skills/implement-spec/run.sh +101 -0
  47. package/skills/implement-spec/tests/conftest.py +38 -0
  48. package/skills/implement-spec/tests/test_agents.py +358 -0
  49. package/skills/implement-spec/tests/test_amendments.py +117 -0
  50. package/skills/implement-spec/tests/test_cli.py +220 -0
  51. package/skills/implement-spec/tests/test_decisions.py +434 -0
  52. package/skills/implement-spec/tests/test_driver.py +767 -0
  53. package/skills/implement-spec/tests/test_final.py +442 -0
  54. package/skills/implement-spec/tests/test_fixloop.py +523 -0
  55. package/skills/implement-spec/tests/test_graph.py +32 -0
  56. package/skills/implement-spec/tests/test_landing.py +355 -0
  57. package/skills/implement-spec/tests/test_merge.py +144 -0
  58. package/skills/implement-spec/tests/test_models.py +47 -0
  59. package/skills/implement-spec/tests/test_report.py +162 -0
  60. package/skills/implement-spec/tests/test_review.py +338 -0
  61. package/skills/implement-spec/tests/test_review_page.py +67 -0
  62. package/skills/implement-spec/tests/test_slots.py +32 -0
  63. package/skills/implement-spec/tests/test_state.py +68 -0
  64. package/skills/implement-spec/tests/test_stops.py +272 -0
  65. package/skills/implement-spec/tests/test_summary.py +28 -0
  66. package/skills/implement-spec/tests/test_tickets.py +72 -0
  67. package/skills/implement-spec/tests/test_tidy.py +582 -0
  68. package/skills/implement-spec/tests/test_walk.py +457 -0
  69. package/skills/review-page/template.html +1 -1
  70. package/skills/to-tickets/SKILL.md +19 -2
  71. package/skills-manifest.json +7 -7
  72. package/skills/implement-spec/history.md +0 -52
@@ -0,0 +1,523 @@
1
+ """In-process review, fix-loop and conflict tests: the rig and the scenarios of test_review.py without the launcher,
2
+ so coverage sees the driver, plus the cases that only a direct call reaches."""
3
+ import json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+
8
+ import jsonschema
9
+ import pytest
10
+
11
+ import driver
12
+ import driver_agents
13
+ import driver_fixloop
14
+ import driver_report
15
+ import driver_run
16
+ import driver_review
17
+ import test_review
18
+ from test_cli import three
19
+ from test_review import FABLE, ONE, OPUS, RISKY, SOL, STUCK, correct, finding, fix, review
20
+ from test_walk import impl, plan, tw
21
+
22
+
23
+ class Inside(test_review.Review):
24
+ """The rig of test_review with the driver called in this process."""
25
+
26
+ def run(self, steps, *extra, parallel="2", watch=False):
27
+ (self.rig / "plan.json").write_text(json.dumps(steps))
28
+ args = ["--tickets", ".agent/tickets", "--gate", str(self.rig / "gate"), "--spec", "specs/csv/spec.md",
29
+ "--models", str(self.models), "--runner", str(self.rig / "run-agent"), "--parallel", parallel, *extra]
30
+ here = os.getcwd()
31
+ os.chdir(self.repo)
32
+ try:
33
+ self.code = driver.main(args)
34
+ finally:
35
+ os.chdir(here)
36
+ self.out = self.err = ""
37
+ return self
38
+
39
+ def prompt(self, name, nn="01"):
40
+ return (self.folder(nn) / f"{name}.prompt.md").read_text()
41
+
42
+
43
+ @pytest.fixture(autouse=True)
44
+ def inside(monkeypatch, tmp_path, capsys):
45
+ """`test_review.scoped` and `.conflicted` build this rig; a CLI is on PATH when the rig's bin has it."""
46
+ which = shutil.which
47
+ monkeypatch.setattr(test_review, "Review", Inside)
48
+ bins = lambda: os.pathsep.join(map(str, [*tmp_path.glob("bin"), *tmp_path.glob("*/bin")]))
49
+ monkeypatch.setattr(driver_agents.shutil, "which", lambda cli: which(cli, path=bins()))
50
+
51
+
52
+ def under(tmp_path, name):
53
+ """A second rig of one test gets its own directory."""
54
+ (tmp_path / name).mkdir()
55
+ return tmp_path / name
56
+
57
+
58
+ def blocked(rig, nn="01"):
59
+ (line,) = rig.led(nn, "BLOCKED")
60
+ assert rig.code == 1 and rig.state()["tickets"][nn]["phase"] == "blocked"
61
+ return line
62
+
63
+
64
+ # ---------------------------------------------------------------- who reviews
65
+
66
+ def test_a_risky_ticket_gets_both_agents_and_one_merged_findings_file(tmp_path):
67
+ rig = Inside(tmp_path, RISKY, {"correctness": [SOL, OPUS], "implementer": [OPUS]})
68
+ rig.run(plan(**{"02/reviewer": [review("02", [finding("R-MINOR", "minor")])],
69
+ "02/correctness": [correct([finding("C-IMPORTANT")])], "02/fixer": [fix("02", 1)]}))
70
+ assert rig.code == 0, rig.ledger()
71
+ assert rig.led("Ticket 02: minor finding of kind code from the reviewer at demo/export.py:99: R-MINOR.")
72
+ assert rig.led("important finding of kind code from the correctness at", "C-IMPORTANT") and rig.led("correctness ran on", SOL)
73
+ merged = json.loads((rig.folder("02") / "findings-0.json").read_text())
74
+ assert [(f["source"], f["severity"], f["title"]) for f in merged] == [
75
+ ("reviewer", "minor", "R-MINOR"), ("correctness", "important", "C-IMPORTANT")]
76
+ assert ".agent/implement/02/findings-0.json" in rig.prompt("fixer-1", "02")
77
+ # Both agents review the fix again, each scoped to it; only the reviewer's first report must prove the tests.
78
+ for role in ("reviewer", "correctness"):
79
+ again = rig.prompt(f"{role}-1", "02")
80
+ assert "This is the re-review after fix round 1." in again and "findings-0.json" in again
81
+ assert again.rstrip().endswith("one ref store.") and "re-review" not in rig.prompt(f"{role}-0", "02")
82
+ assert str(driver_review.CORRECTNESS) in rig.prompt("correctness-0", "02")
83
+ assert "correctness.md" not in rig.prompt("reviewer-0", "02") and not rig.led("folded")
84
+ assert json.loads((rig.folder("02") / "findings-1.json").read_text()) == []
85
+
86
+
87
+ def test_correctness_entry_is_the_first_of_another_provider_than_the_implementers():
88
+ models = {"implementer": ["opus@medium"], "correctness": ["opus@high", "openai/a@low", "openai/b@low"]}
89
+ assert driver_agents.correctness_entry(models) == "openai/a@low"
90
+ assert driver_agents.correctness_entry({**models, "implementer": ["openai/x@low", "opus@low"]}) == "opus@high"
91
+ assert driver_agents.correctness_entry({**models, "correctness": ["opus@high", "claude-fable-5@low"]}) == ""
92
+
93
+
94
+ def test_a_set_with_a_risk_ticket_needs_review_diff_and_a_set_without_one_does_not(tmp_path, monkeypatch, capsys):
95
+ monkeypatch.setattr(driver_review, "CORRECTNESS", tmp_path / "skills/review-diff/correctness.md")
96
+ rig = Inside(tmp_path, three()).run(plan())
97
+ assert rig.code == 3 and not rig.calls() and not (rig.repo / ".agent/implement").exists()
98
+ out = capsys.readouterr().out
99
+ assert f"{tmp_path}/skills/review-diff/correctness.md is missing: ticket 02 has a Risk line" in out
100
+ assert "install the review-diff skill next to implement-spec" in out
101
+ plain = Inside(under(tmp_path, "plain"), ONE).run(plan())
102
+ assert plain.code == 0 and plain.state()["tickets"]["01"]["phase"] == "merged"
103
+
104
+
105
+ # ---------------------------------------------------------------- the locked files
106
+
107
+ def test_a_changed_locked_file_is_fixed_before_a_fixture_finding_is_corrected(tmp_path):
108
+ rig = Inside(tmp_path)
109
+ locked = tw("01")["files"]
110
+ cheat = impl("01", files={"demo/export.py": "def fn_01():\n return '01'\n", "tests/test_01.py": "# edited\n"})
111
+ stale = finding("STALE", "minor", kind="fixture", file="tests/test_01.py")
112
+ correction = {**tw("01"), "files": {"tests/test_01.py": "# corrected\n"}, "commit": "test: 01 fixture fix"}
113
+ rig.run(plan(**{"01/implementer": [cheat], "01/test-writer": [tw("01"), correction],
114
+ "01/reviewer": [review("01", [stale]), review("01", [stale]), review("01")],
115
+ "01/fixer": [fix("01", 1, files={**locked, "demo/export.py": "x = 1\n"})]}))
116
+ assert rig.code == 0, rig.ledger()
117
+ (line,) = rig.led("locked file changed: tests/test_01.py")
118
+ assert line == ("Ticket 01: critical finding of kind code from the driver at tests/test_01.py:1: "
119
+ "locked file changed: tests/test_01.py.")
120
+ order = [call["role"] for call in rig.calls("01")]
121
+ assert order == ["test-writer", "implementer", "reviewer", "fixer", "reviewer", "test-writer", "reviewer"]
122
+ (first,) = [f for f in json.loads((rig.folder() / "findings-0.json").read_text()) if f["source"] == "driver"]
123
+ assert first["evidence"] == f"git diff {rig.sha('01', 'test-writer')}..HEAD -- tests/test_01.py"
124
+ assert f"its content at {rig.sha('01', 'test-writer')}." in rig.prompt("fixer-1")
125
+ # The correction is the new reference of the locked-file check, and it is outside the rounds.
126
+ entry = rig.state()["tickets"]["01"]
127
+ assert (entry["round"], entry["test_writer_sha"]) == (1, rig.calls("01", "test-writer")[1]["sha"])
128
+ assert entry["locked"] == ["tests/test_01.py"] and (rig.folder() / "test-writer-2.json").exists()
129
+ assert rig.led("fixture correction in tests/test_01.py, outside the fix rounds: STALE: STALE.")
130
+ assert "This call is a fixture correction" in rig.prompt("test-writer-2") and "findings-1.json" in rig.prompt("test-writer-2")
131
+
132
+
133
+ def test_a_locked_file_check_that_git_cannot_make_is_a_finding(tmp_path):
134
+ rig = Inside(tmp_path)
135
+ assert driver_review.locked_changes(str(rig.repo), None, []) == []
136
+ (found,) = driver_review.locked_changes(str(rig.repo), "0" * 40, ["tests/test_01.py"])
137
+ assert found["severity"] == "critical" and found["title"].startswith("locked file check failed: ")
138
+
139
+
140
+ def test_a_fixture_finding_that_comes_back_after_its_correction_blocks_the_ticket(tmp_path):
141
+ stale = finding("STALE", kind="fixture", file="tests/test_01.py")
142
+ correction = {**tw("01"), "files": {"tests/test_01.py": "# corrected\n"}, "commit": "test: 01 fixture fix"}
143
+ rig = Inside(tmp_path).run(plan(**{"01/test-writer": [tw("01"), correction], "01/reviewer": [review("01", [stale])] * 2}))
144
+ assert blocked(rig).endswith("BLOCKED at the review: a fixture finding is open after the test-writer correction: STALE.")
145
+ assert len(rig.calls("01", "test-writer")) == 2 and rig.state()["tickets"]["01"]["round"] == 0
146
+
147
+
148
+ def test_an_open_finding_survives_the_fixture_correction_of_its_review_and_starts_the_fix_round(tmp_path):
149
+ stale = finding("STALE", kind="fixture", file="tests/test_01.py")
150
+ correction = {**tw("01"), "files": {"tests/test_01.py": "# corrected\n"}, "commit": "test: 01 fixture fix"}
151
+ rig = Inside(tmp_path).run(plan(**{"01/test-writer": [tw("01"), correction], "01/fixer": [fix("01", 1)],
152
+ "01/reviewer": [review("01", [stale, STUCK]), review("01")]}))
153
+ assert rig.code == 0, rig.ledger()
154
+ order = [call["role"] for call in rig.calls("01")]
155
+ assert order == ["test-writer", "implementer", "reviewer", "test-writer", "fixer", "reviewer"]
156
+ assert rig.state()["tickets"]["01"]["round"] == 1 and "findings-0.json" in rig.prompt("fixer-1")
157
+ assert rig.calls("01", "fixer")[0]["head"] == rig.calls("01", "test-writer")[1]["sha"] # on the corrected tip
158
+ # No review saw the correction yet, so the review after the round is a full one.
159
+ assert "re-review" not in rig.prompt("reviewer-1")
160
+ assert [gate["commit"] for gate in rig.walk_gates()] == [rig.sha("01", "implementer"), rig.sha("01", "fixer")]
161
+
162
+
163
+ def test_a_fixture_correction_that_is_rejected_blocks_the_ticket(tmp_path):
164
+ stale = finding("STALE", kind="fixture", file="tests/test_01.py")
165
+ rig = Inside(tmp_path).run(plan(**{"01/test-writer": [tw("01"), {}, {}], "01/reviewer": [review("01", [stale])]}))
166
+ assert "BLOCKED at the fixture correction: report .agent/implement/01/test-writer-1.json is not" in blocked(rig)
167
+
168
+
169
+ # ---------------------------------------------------------------- the locked files of the merged tickets
170
+
171
+ WEAK = "def test_x():\n assert True\n"
172
+ LOCKED_01 = tw("01")["files"]["tests/test_01.py"]
173
+ REFUSED_01 = "Ticket 02: BLOCKED at the merge: locked file changed: tests/test_01.py."
174
+ CHEAT = impl("02", files={"demo/export.py": "def fn_02():\n return '02'\n", "tests/test_01.py": WEAK})
175
+
176
+
177
+ def on_pr(rig, path):
178
+ return subprocess.run(["git", "-C", str(rig.repo), "show", f"feat/csv:{path}"], capture_output=True, text=True).stdout
179
+
180
+
181
+ def test_a_later_ticket_that_weakens_a_locked_test_of_a_merged_ticket_does_not_land(tmp_path):
182
+ """Final review, correctness 3: ticket 02 replaced the locked assertion of ticket 01, merged and was pushed.
183
+ Ticket 09: the check is at the landing, on the tree that is about to land, so the review has no such finding."""
184
+ rig = Inside(tmp_path, three()).run(plan(**{"02/implementer": [CHEAT]}))
185
+ assert blocked(rig, "02") == REFUSED_01 and rig.led("locked file changed") == [REFUSED_01], rig.ledger()
186
+ assert json.loads((rig.folder("02") / "findings-0.json").read_text()) == [] and not rig.calls("02", "fixer")
187
+ assert on_pr(rig, "tests/test_01.py") == LOCKED_01 and "feat: 02 rows" not in rig.pr_subjects()
188
+ assert [rig.state()["tickets"][nn]["phase"] for nn in ("01", "03")] == ["merged", "merged"]
189
+
190
+
191
+ def extends(text, listed):
192
+ writer = tw("02")
193
+ writer["files"]["tests/test_01.py"] = text
194
+ writer["report"]["locked"] += ["tests/test_01.py"] * listed
195
+ return writer
196
+
197
+
198
+ def test_the_test_writer_of_a_later_ticket_may_extend_the_locked_file_of_a_merged_ticket_when_it_lists_it(tmp_path):
199
+ more = LOCKED_01 + "\n\ndef test_y():\n assert 02 == 'y'\n"
200
+ rig = Inside(under(tmp_path, "listed"), three()).run(plan(**{"02/test-writer": [extends(more, True)]}))
201
+ assert rig.code == 0, rig.ledger()
202
+ assert not rig.led("locked file changed") and on_pr(rig, "tests/test_01.py") == more
203
+ assert rig.state()["tickets"]["02"]["locked"] == ["tests/test_02.py", "tests/test_01.py"]
204
+ # What the test-writer of 02 wrote is locked for the implementer of 02: the check of its own review finds it.
205
+ again = Inside(under(tmp_path, "cheat"), three()).run(
206
+ plan(**{"02/test-writer": [extends(more, True)], "02/implementer": [CHEAT], "02/fixer": [{}, {}]}))
207
+ assert again.led("Ticket 02:", "locked file changed") == [
208
+ "Ticket 02: critical finding of kind code from the driver at tests/test_01.py:1: locked file changed: tests/test_01.py."]
209
+ assert on_pr(again, "tests/test_01.py") == LOCKED_01
210
+ # An unlisted change is a report violation: one rerun with the error, then BLOCKED before any implementer.
211
+ unlisted = Inside(under(tmp_path, "unlisted"), three()).run(plan(**{"02/test-writer": [extends(more, False)] * 2}))
212
+ error = ("tests/test_01.py is a locked file of a merged ticket, and `locked` does not list it: list it in "
213
+ "`locked`, or leave the file as it was")
214
+ assert blocked(unlisted, "02") == f"Ticket 02: BLOCKED at the test-writer: {error}."
215
+ first, second = unlisted.calls("02", "test-writer")
216
+ assert f"The driver rejected the earlier attempt: {error}" in second["prompt"]
217
+ assert not unlisted.calls("02", "implementer") and on_pr(unlisted, "tests/test_01.py") == LOCKED_01
218
+
219
+
220
+ def test_a_refactor_ticket_may_not_change_a_locked_test_of_a_merged_ticket(tmp_path):
221
+ """It has no test-writer and no locked file of its own: the landing check is the one that protects ticket 01."""
222
+ files = three(**{"02-quote-fields.md": test_review.ticket("02", "Quote fields", "01", seam="none — refactor", criteria=())})
223
+ rig = Inside(tmp_path, files).run(plan(**{"02/implementer": [CHEAT]}))
224
+ assert blocked(rig, "02") == REFUSED_01 and not rig.calls("02", "test-writer"), rig.ledger()
225
+ assert on_pr(rig, "tests/test_01.py") == LOCKED_01
226
+ clean = Inside(under(tmp_path, "clean"), files).run(plan())
227
+ assert clean.code == 0 and not clean.led("locked file changed"), clean.ledger()
228
+
229
+
230
+ # A correction is a test-writer: a file that it lists is its ticket's own. It must start from the locked version.
231
+ APPEND = "--- a/tests/test_01.py\n+++ b/tests/test_01.py\n@@ -1,2 +1,3 @@\n def test_x():\n assert True\n+# corrected\n"
232
+ RESTORED_01 = ("Ticket 02: tests/test_01.py was not the locked version at the correction: the driver restored it in "
233
+ "the tree, so the correction starts from the locked version.")
234
+
235
+
236
+ def appending(tmp_path, **changes):
237
+ """A correction of 02 that appends a line to tests/test_01.py as the implementer of 02 left it, and lists it."""
238
+ (tmp_path / "append.patch").write_text(APPEND)
239
+ step = {**tw("02"), "git": [["apply", str(tmp_path / "append.patch")]], "commit": "test: 02 corrected", **changes}
240
+ step["report"] = {**step["report"], "locked": ["tests/test_02.py", "tests/test_01.py"]}
241
+ return step
242
+
243
+
244
+ def proves(nn, path, name="test_x"):
245
+ """A clean review of `nn` with one more mutation proof, for the locked test file `path`."""
246
+ step, red = review(nn), f"FAILED {path}::{name} - AssertionError"
247
+ step["report"]["mutations"] += [dict(test=f"{path}::{name}", file="demo/export.py", line=1, red=red)]
248
+ return {**step, "stdout": step["stdout"] + red + "\n"}
249
+
250
+
251
+ def test_an_overrule_correction_starts_from_the_locked_file_of_the_merged_ticket(tmp_path):
252
+ """Review round 1, I1 (probe b7b): the implementer of 02 weakened the test of 01, the correction listed the file."""
253
+ contra = {"status": "BLOCKED", "commits": [], "files": [], "concerns": ["locked test contradicts spec: tests/test_02.py::test_x"], "green": []}
254
+ correction = appending(tmp_path, files={"tests/test_02.py": "def test_x():\n assert '02' == '02'\n"})
255
+ steps = plan(**{"02/implementer": [{**CHEAT, "report": contra}, impl("02", text="def fn_02():\n return 'two'\n")],
256
+ "02/test-writer": [tw("02"), correction], "02/reviewer": [proves("02", "tests/test_01.py")]})
257
+ rig = Inside(tmp_path, three()).run(steps)
258
+ assert rig.code == 2, rig.ledger()
259
+ rig.run(steps, "--answer", "02-locked-test-1=overrule-test")
260
+ assert on_pr(rig, "tests/test_01.py") == LOCKED_01, rig.ledger()
261
+ assert rig.led("Ticket 02:", "restored") == [RESTORED_01] and rig.code == 0
262
+
263
+
264
+ def test_a_fixture_correction_starts_from_the_locked_file_of_the_merged_ticket(tmp_path):
265
+ """Review round 1, I1 (probe b6b): the same through the fixture correction of the fix loop."""
266
+ stale = finding("STALE", kind="fixture", file="tests/test_02.py")
267
+ correction = appending(tmp_path, files={"tests/test_02.py": "def test_x():\n assert '02' == 'x' # corrected\n"})
268
+ rig = Inside(tmp_path, three()).run(plan(**{"02/implementer": [CHEAT], "02/test-writer": [tw("02"), correction],
269
+ "02/reviewer": [review("02", [stale]), proves("02", "tests/test_01.py")]}))
270
+ assert on_pr(rig, "tests/test_01.py") == LOCKED_01, rig.ledger()
271
+ assert rig.led("Ticket 02:", "restored") == [RESTORED_01] and rig.code == 0
272
+
273
+
274
+ ACCENT = "tests/tést.py"
275
+
276
+
277
+ def accented(tmp_path, name, **steps):
278
+ """Ticket 01 locks a test file whose name git prints quoted (core.quotePath) when nobody asks otherwise."""
279
+ writer = tw("01")
280
+ writer["files"][ACCENT] = "def test_y():\n assert 1 == 'y'\n"
281
+ writer["report"]["locked"] += [ACCENT]
282
+ proofs = [proves("01", ACCENT, "test_y")] * 2
283
+ return Inside(under(tmp_path, name), three()).run(plan(**{"01/test-writer": [writer], "01/reviewer": proofs, **steps}))
284
+
285
+
286
+ def test_a_locked_file_with_a_name_that_git_quotes_is_protected_at_review_and_at_the_landing(tmp_path):
287
+ """Review round 1, I2: the quoted form of the path matched nothing, so no check saw the file."""
288
+ own = impl("01", files={"demo/export.py": "def fn_01():\n return '01'\n", ACCENT: WEAK})
289
+ rig = accented(tmp_path, "review", **{"01/implementer": [own], "01/fixer": [{}, {}]})
290
+ assert rig.led(f"Ticket 01: critical finding of kind code from the driver at {ACCENT}:1: locked file changed: {ACCENT}."), rig.ledger()
291
+ assert "BLOCKED at fix round 1" in blocked(rig)
292
+ later = impl("02", files={"demo/export.py": "def fn_02():\n return '02'\n", ACCENT: WEAK})
293
+ rig = accented(tmp_path, "landing", **{"02/implementer": [later]})
294
+ assert rig.state()["tickets"]["01"]["locked"] == ["tests/test_01.py", ACCENT], rig.ledger()
295
+ assert blocked(rig, "02") == f"Ticket 02: BLOCKED at the merge: locked file changed: {ACCENT}."
296
+ assert "assert 1 == 'y'" in on_pr(rig, ACCENT)
297
+
298
+
299
+ # ---------------------------------------------------------------- the rounds
300
+
301
+ def test_four_rounds_then_blocked_with_the_escalation_at_round_3(tmp_path):
302
+ rig = Inside(tmp_path, three(), {"implementer": [OPUS, FABLE]})
303
+ rig.run(plan(**{"01/reviewer": [review("01", [STUCK])] * 5, "01/fixer": [fix("01", n) for n in (1, 2, 3, 4)]}))
304
+ assert rig.code == 1 and rig.state()["tickets"]["01"]["round"] == 4
305
+ assert rig.led("BLOCKED at the review: after round 4 these findings are open: STUCK (demo/export.py:99).")
306
+ assert rig.ledger().count(f"Ticket 01 round 3: escalated to {FABLE}.") == 1
307
+ assert [len(rig.led("fixer ran on", entry)) for entry in (OPUS, FABLE)] == [2, 2]
308
+ tips = [rig.sha("01", "implementer")] + [call["sha"] for call in rig.calls("01", "fixer")]
309
+ assert [gate["commit"] for gate in rig.gates() if gate["branch"] == "ticket/01"] == tips
310
+ assert rig.state()["tickets"]["01"]["tip"] == tips[-1] and not rig.calls("02")
311
+ assert rig.state()["tickets"]["03"]["phase"] == "merged"
312
+
313
+
314
+ def test_round_3_stays_on_its_entry_when_the_implementer_has_no_next_entry(tmp_path):
315
+ rig = Inside(tmp_path, ONE, {"implementer": [OPUS]})
316
+ rig.run(plan(**{"01/reviewer": [review("01", [STUCK])] * 3 + [review("01")], "01/fixer": [fix("01", n) for n in (1, 2, 3)]}))
317
+ assert rig.code == 0, rig.ledger()
318
+ assert rig.led(f"Ticket 01 round 3: the implementer has no next entry, so the round stays on {OPUS}.")
319
+ assert len(rig.led("fixer ran on", OPUS)) == 3
320
+
321
+
322
+ def test_the_escalation_takes_the_next_entry_of_the_list():
323
+ models = {"implementer": ["a@low", "openai/b@low", "c@low"]}
324
+ assert [driver_fixloop.entry(models, n) for n in (1, 2, 3, 4)] == ["a@low", "a@low", "openai/b@low", "openai/b@low"]
325
+ assert driver_fixloop.entry({"implementer": ["a@low"]}, 3) == "a@low"
326
+
327
+
328
+ @pytest.mark.parametrize("step, error", [
329
+ ({}, "BLOCKED at fix round 1: report .agent/implement/01/fixer-1.json is not"),
330
+ ({"report": {"status": "BLOCKED", "commits": [], "files": [], "concerns": ["cannot"], "green": [], "ticket": "01"}},
331
+ "BLOCKED at fix round 1: the fixer reported BLOCKED: cannot.")])
332
+ def test_a_fixer_that_does_not_finish_blocks_the_ticket_in_its_round(tmp_path, step, error):
333
+ rig = Inside(tmp_path).run(plan(**{"01/reviewer": [review("01", [STUCK])], "01/fixer": [step, step]}))
334
+ entry = rig.state()["tickets"]["01"]
335
+ assert error in blocked(rig) and entry["round"] == 1 and entry["tip"] == rig.sha("01", "implementer")
336
+
337
+
338
+ # ---------------------------------------------------------------- the reviewers' reports and the slot
339
+
340
+ def test_a_first_review_proves_each_locked_file_and_a_second_bad_report_blocks(tmp_path):
341
+ rig = Inside(tmp_path).run(plan(**{"01/reviewer": [review("01", lines=())] * 2}))
342
+ assert "BLOCKED at the review: the reviewer: mutations: no mutation proof for the locked test file tests/test_01.py." in blocked(rig)
343
+ assert "feat: 01 rows" not in rig.pr_subjects()
344
+
345
+
346
+ def test_the_locked_file_rule_is_for_the_first_review_only(tmp_path):
347
+ report = dict(status="DONE", commits=[], files=[], concerns=[], findings=[], mutations=[])
348
+ path = tmp_path / "reviewer-0.json"
349
+ path.write_text(json.dumps(report))
350
+ context = {"repo": ".", "branch": "main", "start": "HEAD", "log": str(path), "locked": ["tests/a.py", "tests/b.py"]}
351
+ assert driver_report.validate("reviewer", str(path), {**context, "round": 0}) == []
352
+ assert driver_report.validate("reviewer", str(path), {**context, "round": 1}) == [
353
+ "mutations: no mutation proof for the locked test file tests/a.py",
354
+ "mutations: no mutation proof for the locked test file tests/b.py"]
355
+ assert driver_report.validate("correctness", str(path), {**context, "round": 0}) == []
356
+
357
+
358
+ def test_a_timed_out_read_only_role_is_restored_only_when_no_other_agent_is_in_the_slot(tmp_path):
359
+ rig = Inside(tmp_path)
360
+ tip = subprocess.run(["git", "-C", str(rig.repo), "rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip()
361
+ (rig.repo / "junk.txt").write_text("x\n")
362
+ context, shared = {"repo": str(rig.repo), "branch": "feat/csv", "start": tip}, lambda: False
363
+ assert "first revert each edit of the earlier attempt" in driver_run._timeout_lines("correctness", 30, context, shared)
364
+ assert (rig.repo / "junk.txt").exists()
365
+ assert "in the tree: continue from it" in driver_run._timeout_lines("fixer", 60, context, shared) and (rig.repo / "junk.txt").exists()
366
+ # `--only reviewer`: one agent
367
+ assert f"restored the worktree to {tip}" in driver_run._timeout_lines("reviewer", 45, context, lambda: True)
368
+ assert not (rig.repo / "junk.txt").exists()
369
+
370
+
371
+ DIRTY = [({"files": {"junk.txt": "x\n", "demo/export.py": "mutated\n"}},
372
+ "the tree is not as the review found it: demo/export.py, junk.txt"),
373
+ ({"git": [["checkout", "-q", "--detach"]]}, "the slot is no longer on ticket/01 at "),
374
+ ({"commit": "sneak", "files": {"demo/export.py": "x = 2\n"}}, "the slot is no longer on ticket/01 at ")]
375
+
376
+
377
+ def slot_state(rig):
378
+ git = lambda *args: subprocess.run(["git", "-C", str(rig.repo), *args], capture_output=True, text=True).stdout.strip()
379
+ return git("status", "--porcelain"), git("rev-parse", "ticket/01")
380
+
381
+
382
+ @pytest.mark.parametrize("step, left", DIRTY)
383
+ def test_a_reviewer_that_changes_the_slot_is_run_once_more_on_the_restored_tip(tmp_path, step, left):
384
+ rig = Inside(tmp_path).run(plan(**{"01/reviewer": [{**review("01"), **step}, review("01")]}))
385
+ assert rig.code == 0, rig.ledger()
386
+ tip = rig.sha("01", "implementer")
387
+ (line,) = rig.led("Ticket 01: a read-only role broke its rule:")
388
+ assert left in line and line.endswith(f"the driver restored the slot to {tip[:7]} and runs the review again.")
389
+ first, second = rig.calls("01", "reviewer")
390
+ assert (second["branch"], second["head"], second["status"]) == ("ticket/01", tip, "")
391
+ assert rig.tip() == tip and not (rig.repo / "junk.txt").exists()
392
+
393
+
394
+ def test_a_second_review_that_changes_the_slot_blocks_the_ticket_with_the_slot_clean(tmp_path):
395
+ step, left = DIRTY[0]
396
+ rig = Inside(tmp_path).run(plan(**{"01/reviewer": [{**review("01"), **step}] * 3}))
397
+ assert f"BLOCKED at the review: {left}." in blocked(rig)
398
+ assert len(rig.calls("01", "reviewer")) == 2 and len(rig.led("a read-only role broke its rule:")) == 2
399
+ assert slot_state(rig) == ("", rig.sha("01", "implementer")) and "feat: 01 rows" not in rig.pr_subjects()
400
+
401
+
402
+ def test_a_reviewer_that_times_out_starts_again_from_the_clean_tip(tmp_path):
403
+ hang = {"files": {"demo/export.py": "mutated\n", "junk.txt": "x\n"}, "sleep": 60}
404
+ rig = Inside(tmp_path).run(plan(**{"01/reviewer": [hang, review("01")]}), "--timeout", "reviewer=0.03")
405
+ assert rig.code == 0, rig.ledger()
406
+ first, second = rig.calls("01", "reviewer")
407
+ assert second["status"] == "" and second["head"] == rig.sha("01", "implementer")
408
+ line = f"The driver restored the worktree to {second['head']}: start again from there."
409
+ assert "timed out after 0.03 minutes" in second["prompt"] and line in second["prompt"]
410
+ assert "in the tree" not in second["prompt"] and second["prompt"].rstrip().endswith("one ref store.")
411
+
412
+
413
+ # ---------------------------------------------------------------- mutation proofs
414
+
415
+ def test_a_re_review_names_only_the_proofs_the_fix_made_due(tmp_path):
416
+ first = test_review.edit(test_review.LONG, l12="v12 = 'a'", l30="v30 = 'a'")
417
+ rig = test_review.scoped(tmp_path, (12, 30), [first, test_review.edit(first, l30="v30 = 'b'")], (30,))
418
+ assert rig.code == 0, rig.ledger()
419
+ assert "no proof is due): demo/export.py:12 demo/export.py:30\n" in rig.prompt("reviewer-1")
420
+ assert "no proof is due): demo/export.py:30\n" in rig.prompt("reviewer-2")
421
+ old, new = rig.sha("01", "implementer"), rig.calls("01", "fixer")[0]["sha"]
422
+ assert f"The fix is {old}..{new}: review only the lines it changed" in rig.prompt("reviewer-1")
423
+
424
+
425
+ def test_a_proof_follows_its_line_through_inserts_and_deletes(tmp_path):
426
+ lines = test_review.LONG.splitlines(keepends=True)
427
+ moved = "".join(lines[:19] + ["i1 = 1\n", "i2 = 2\n"] + lines[19:29] + ["v30 = 'changed'\n"] + lines[30:])
428
+ rig = test_review.scoped(tmp_path, (12, 30, 35), [moved], (32,))
429
+ assert rig.code == 0, rig.ledger()
430
+ assert "no proof is due): demo/export.py:32\n" in rig.prompt("reviewer-1")
431
+ # (first old line, old count, first new line, new count): lines 3-4 deleted, 10-11 replaced by one line.
432
+ hunks = [(3, 2, 2, 0), (10, 2, 8, 1), (20, 0, 18, 3)]
433
+ assert [driver_review._follow(line, hunks) for line in (2, 3, 4, 5, 10, 11, 12, 20, 21)] == [
434
+ (2, False), (2, True), (2, True), (3, False), (8, True), (8, True), (9, False), (17, False), (21, False)]
435
+ repo, old, new = str(rig.repo), rig.sha("01", "implementer"), rig.calls("01", "fixer")[0]["sha"]
436
+ assert driver_review.follow(repo, old, new, {("demo/export.py", 30), ("demo/export.py", 35), ("gone.py", 4)}) == {
437
+ ("demo/export.py", 32): True, ("demo/export.py", 37): False, ("gone.py", 4): False}
438
+
439
+
440
+ # ---------------------------------------------------------------- conflict agent
441
+
442
+ def test_a_conflict_is_resolved_once_in_the_ticket_slot_and_a_second_one_blocks(tmp_path):
443
+ rig = test_review.conflicted(under(tmp_path, "once"))
444
+ assert rig.code == 0, rig.ledger()
445
+ assert rig.tip() in [gate["commit"] for gate in rig.gates()] and dict(rig.state()["run"]["landed"])["01"] == rig.tip()
446
+ assert rig.led("Ticket 01: the conflict agent resolved the merge conflict in demo/cli.py at ")
447
+ prompt = rig.prompt("conflict-0")
448
+ assert "Files in conflict: demo/cli.py\n" in prompt and f"at commit {rig.sha('03', 'implementer')}\n" in prompt
449
+ again = test_review.conflicted(under(tmp_path, "twice"), moves_again=True)
450
+ line = blocked(again)
451
+ assert "BLOCKED at the merge: after the conflict agent, the merge into the PR branch conflicts in demo/cli.py." in line
452
+ assert len(again.calls("01", "conflict")) == 1
453
+
454
+
455
+ def conflict(tmp_path, resolve):
456
+ """01 and 03 both add demo/cli.py and 03 merges first (as test_review.conflicted); `resolve` is 01's conflict step."""
457
+ rig = Inside(tmp_path, {k: v for k, v in three().items() if not k.startswith("02")})
458
+ one = impl("01", files={"demo/export.py": "def fn_01():\n return '01'\n", "demo/cli.py": test_review.cli("one")})
459
+ return rig.run(plan(**{"01/test-writer": [{**tw("01"), "touch": ["s1"]}], "01/conflict": [resolve, resolve],
460
+ "03/implementer": [{**impl("03", "demo/cli.py", test_review.cli("three")), "wait": ["s1"]}],
461
+ "01/implementer": [{**one, "wait_pr_moves": True}]}))
462
+
463
+
464
+ def test_a_conflict_resolution_that_changes_a_locked_file_does_not_land(tmp_path):
465
+ done = {"status": "DONE", "commits": ["$SHA"], "files": ["demo/cli.py"], "concerns": []}
466
+ rig = conflict(tmp_path, {"files": {"demo/cli.py": test_review.cli("resolved"), "tests/test_01.py": "# weaker\n"},
467
+ "commit": "merge: resolve cli", "report": done})
468
+ assert "BLOCKED at the merge: the conflict agent: locked file changed: tests/test_01.py." in blocked(rig)
469
+ assert len(rig.calls("01", "conflict")) == 1 and rig.tip() == rig.sha("03", "implementer")
470
+ assert rig.state()["tickets"]["01"]["tip"] == rig.sha("01", "implementer")
471
+
472
+
473
+ def test_a_conflict_resolution_that_changes_a_locked_file_of_a_merged_ticket_does_not_land(tmp_path):
474
+ done = {"status": "DONE", "commits": ["$SHA"], "files": ["demo/cli.py"], "concerns": []}
475
+ rig = conflict(tmp_path, {"files": {"demo/cli.py": test_review.cli("resolved"), "tests/test_03.py": WEAK},
476
+ "commit": "merge: resolve cli", "report": done})
477
+ # The resolution is a commit of the ticket; the landing check refuses the tree that it would put on the PR branch.
478
+ assert "BLOCKED at the merge: locked file changed: tests/test_03.py." in blocked(rig)
479
+ assert rig.tip() == rig.sha("03", "implementer") and on_pr(rig, "tests/test_03.py") == tw("03")["files"]["tests/test_03.py"]
480
+ assert rig.state()["tickets"]["01"]["tip"] == rig.calls("01", "conflict")[0]["sha"]
481
+
482
+
483
+ def test_a_conflict_agent_that_gives_up_leaves_no_open_merge_in_the_slot(tmp_path):
484
+ stuck = {"status": "BLOCKED", "commits": [], "files": [], "concerns": ["both sides are needed"]}
485
+ rig = conflict(tmp_path, {"files": {"demo/cli.py": "half\n"}, "report": stuck})
486
+ assert "BLOCKED at the merge: the conflict agent: the conflict reported BLOCKED: both sides are needed." in blocked(rig)
487
+ git = lambda *args: subprocess.run(["git", "-C", str(rig.repo), *args], capture_output=True, text=True)
488
+ assert git("status", "--porcelain").stdout == "" and git("rev-parse", "-q", "--verify", "MERGE_HEAD").returncode
489
+ assert git("symbolic-ref", "--short", "HEAD").stdout.strip() == "feat/csv" # slot 1 is free again
490
+
491
+
492
+ # ---------------------------------------------------------------- templates
493
+
494
+ @pytest.mark.parametrize("role", ["fixer", "conflict"])
495
+ def test_the_templates_of_the_new_roles_hold_a_valid_schema_and_end_with_the_rule(role):
496
+ jsonschema.Draft202012Validator.check_schema(driver_report.schema(role))
497
+ text = (driver_report.PROMPTS / f"{role}.md").read_text()
498
+ assert text.rstrip().endswith("one ref store.") and "ledger" not in text.lower()
499
+ assert text.rfind("{report}") < text.rfind("DONE_WITH_CONCERNS")
500
+
501
+
502
+ def test_no_template_names_the_ledger_and_the_method_of_step_6_is_in_the_reviewer_prompt():
503
+ # The final reviewer gets the ledger as a pointer (requirement 19); no other prompt names it.
504
+ for path in driver_report.PROMPTS.glob("*.md"):
505
+ assert ("ledger" in path.read_text().lower()) == (path.name == "final-reviewer.md"), path.name
506
+ reviewer = (driver_report.PROMPTS / "reviewer.md").read_text()
507
+ for sentence in ("Test each suspicion that the ticket names in its Suspicions line.",
508
+ "a later round repeats it only when that line changed.",
509
+ "Run again each mutation that the test-writer report names in `concerns`."):
510
+ assert sentence in reviewer, sentence
511
+
512
+
513
+ def test_the_correctness_prompt_keeps_the_agent_off_the_tree_the_reviewer_edits():
514
+ text = (driver_report.PROMPTS / "correctness.md").read_text()
515
+ assert "Read file content from git at {start_sha}" in text and "never from the working tree" in text
516
+ assert "run no tests" in text
517
+
518
+
519
+ def test_git_identity_is_not_needed_to_open_the_merge_for_the_conflict_agent(tmp_path):
520
+ import driver_merge
521
+ rig = Inside(tmp_path)
522
+ git = lambda *args: subprocess.run(["git", "-C", str(rig.repo), *args], capture_output=True, text=True).stdout
523
+ assert driver_merge.begin(str(rig.repo), "main") == [] and git("status", "--porcelain") == ""
@@ -0,0 +1,32 @@
1
+ """The frontier: blocking edges and Touches holds."""
2
+ from driver_graph import frontier, overlap, touches
3
+ from driver_tickets import Ticket
4
+
5
+
6
+ def make(number, touched, *blocked_by):
7
+ return Ticket(number, f"{number}-t.md", "t", None, "`f()`", touched, None, None, [], {}, list(blocked_by))
8
+
9
+
10
+ def test_touches_reads_each_backticked_entry_and_ignores_the_prose_around_it():
11
+ ticket = make("01", "`a.py` (new: frontier), `b.py: main`, tests beside each")
12
+ assert touches(ticket) == {("a.py", ""), ("b.py", "main")}
13
+ assert touches(make("02", None)) == set()
14
+
15
+
16
+ def test_overlap_names_the_shared_function_and_a_whole_file_overlaps_its_functions():
17
+ assert overlap(make("01", "`a.py: f`, `b.py: g`"), make("02", "`b.py: g`")) == "b.py: g"
18
+ assert overlap(make("01", "`a.py`"), make("02", "`a.py: f`")) == "a.py: f"
19
+ assert overlap(make("01", "`a.py`"), make("02", "`a.py`")) == "a.py"
20
+ assert overlap(make("01", "`a.py: f`"), make("02", "`a.py: g`, `c.py: f`")) == ""
21
+
22
+
23
+ def test_frontier_waits_for_blockers_and_holds_on_a_running_overlap():
24
+ tickets = [make("01", "`a.py: f`"), make("02", "`a.py: f`", "01"), make("03", "`a.py: f`"), make("04", "`b.py: g`")]
25
+ phases = dict.fromkeys(("01", "02", "03", "04"), "pending")
26
+ ready, holds = frontier(tickets, phases, set())
27
+ assert [t.number for t in ready] == ["01", "03", "04"] and holds == {}
28
+ ready, holds = frontier(tickets, phases, {"01"})
29
+ assert [t.number for t in ready] == ["04"] and holds == {"03": "held while 01 runs: both touch a.py: f"}
30
+ ready, holds = frontier(tickets, {**phases, "01": "merged", "04": "blocked"}, set())
31
+ assert [t.number for t in ready] == ["02", "03"]
32
+ assert frontier(tickets, {**phases, "01": "blocked", "03": "merged", "04": "merged"}, set()) == ([], {})