claude-dev-env 8.102.1 → 8.103.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.
- package/.agents/skills/pull-request/reference/body-sections.md +2 -1
- package/bin/install.prompt-context.test.mjs +2 -1
- package/docs/rule-guides/proof-before-pull-request.md +3 -3
- package/hooks/blocking/pull_request_proof.py +64 -6
- package/hooks/blocking/test_pull_request_proof.py +87 -0
- package/hooks/blocking/test_turn_review_findings.py +25 -0
- package/hooks/blocking/test_turn_review_gate.py +462 -0
- package/hooks/blocking/test_turn_review_snapshot.py +107 -0
- package/hooks/blocking/turn_review_findings.py +102 -0
- package/hooks/blocking/turn_review_gate.py +362 -0
- package/hooks/blocking/turn_review_snapshot.py +116 -0
- package/hooks/features/conduct-gates.md +5 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/pull_request_proof_constants.py +14 -1
- package/hooks/hooks_constants/turn_review_gate_constants.py +161 -0
- package/package.json +1 -1
- package/scripts/policy_lint/adapter_configuration.py +6 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/tests/test_adapter_configuration.py +8 -0
|
@@ -7,7 +7,8 @@ A reviewer reads the body in place of the code. The body says what the change do
|
|
|
7
7
|
| `## What this does` | Two or three plain sentences: the behavior before, the behavior after, and who notices. Written for a reviewer who has not read the worker's conversation. |
|
|
8
8
|
| `## What changed` | One bullet per area of the diff, in plain words. |
|
|
9
9
|
| `## Existing work` | What already did part of the job, with a link or file and line, and what this change adds. Or the places searched. |
|
|
10
|
-
| `## Proof in practice` | Each command in backticks, the output without the change and with it
|
|
10
|
+
| `## Proof in practice` | Each command in backticks, and the output without the change and with it. The run on the target the change exists to fix is here before the pull request opens. |
|
|
11
|
+
| `## Not covered` | Optional. A condition the change does not target and the runs left out, such as an older data shape. |
|
|
11
12
|
| `## Eval` | Only for a `feat` title. The eval's cases and grader, the command, and its result. |
|
|
12
13
|
| `## Risk` | What could break, for whom, and how anyone would notice. `Low` and one reason for a doc or test change. |
|
|
13
14
|
| `## How to revert` | The exact undo step, and any data or state a plain revert leaves behind. |
|
|
@@ -35,13 +35,14 @@ test('reinstall drops the retired style reminder and keeps one skill reminder ho
|
|
|
35
35
|
mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/example/.claude', 'python');
|
|
36
36
|
|
|
37
37
|
const userPromptHooks = settings.hooks.UserPromptSubmit.flatMap(eachGroup => eachGroup.hooks);
|
|
38
|
-
assert.equal(userPromptHooks.length,
|
|
38
|
+
assert.equal(userPromptHooks.length, 7);
|
|
39
39
|
assert.equal(userPromptHooks[0].command, userCommand);
|
|
40
40
|
assert.match(userPromptHooks[1].command, /session\/skill_loaded_reminder\.py$/);
|
|
41
41
|
assert.match(userPromptHooks[2].command, /lifecycle\/nested_project_hooks\.py$/);
|
|
42
42
|
assert.match(userPromptHooks[3].command, /blocking\/chat_reply_gate\.py$/);
|
|
43
43
|
assert.match(userPromptHooks[4].command, /session\/tracking_nudge\.py$/);
|
|
44
44
|
assert.match(userPromptHooks[5].command, /session\/plugin_change_warning\.py$/);
|
|
45
|
+
assert.match(userPromptHooks[6].command, /blocking\/turn_review_gate\.py$/);
|
|
45
46
|
assert.ok(userPromptHooks.every(eachHook => !eachHook.command.includes('style_reminder_prompt')));
|
|
46
47
|
|
|
47
48
|
const firstMerge = JSON.stringify(settings);
|
|
@@ -30,14 +30,14 @@ For a change to Claude Code behavior, such as a hook, skill, rule, setting, prom
|
|
|
30
30
|
- Read the broker report for the account it chose. Read the stream for the hook events and their output, `permission_denials`, the final result, and the token usage on the result event.
|
|
31
31
|
- When the broker exits 3 with action "wait", no account has room. Treat that as a blocked proof and report its reason and `resets_at`. Spawn Claude only through the broker.
|
|
32
32
|
|
|
33
|
-
Report it in the pull request body under "Proof in practice". List each command you ran, quote the output lines that show the effect, state the difference between the runs with and without the change,
|
|
33
|
+
Report it in the pull request body under "Proof in practice". List each command you ran, quote the output lines that show the effect, and state the difference between the runs with and without the change. The run on the target the change exists to fix belongs in this section before the pull request opens. List a condition the change does not target, such as an older data shape, under a "Not covered" heading at the same level.
|
|
34
34
|
|
|
35
35
|
When you cannot prove it, leave the pull request unopened. Tell the owner in three short sentences what you could not run and which tool would make it possible. A blocked proof is an acceptable result, because it shows where tooling is missing.
|
|
36
36
|
|
|
37
37
|
## Enforcement
|
|
38
38
|
|
|
39
|
-
`hooks/blocking/pull_request_proof.py` reads the body of each new pull request before it opens. `pr_lifecycle_skill_gate.py` runs it on `gh pr create`, the `pull_request.py create` script, and each `mcp__*__create_pull_request` tool. The gate denies the call when the body has no "Proof in practice" heading, or when the text under that heading names no command in backticks. The gate also denies a section whose only commands run a test runner, such as `pytest`, `python -m unittest`, `uv run pytest`, `npm test`, `npx vitest`, `jest`, `go test` or `cargo test`. Quotes around a runner word, interpreter flags before `-m`, and `coverage run -m unittest` are still test runs. A command is an inline backticked span or a `$ ` line inside a fenced block. A span that starts with a path and line, such as `gate.py:12`, is not a command. A span that holds one path or file name, such as `src/x.py`, is not a command unless that span runs a test runner, such as `py.test` or `pytest.exe`. A test run piped into `tee`, `tail`, `head`, `grep` or `cat` is still a test run. The deny quotes the line "Unit tests are welcome as extra evidence. The live run is the proof." and asks for a run of the changed thing the way its user runs it, without the change and with it. A section with one command that is not a test run passes. A `gh pr create` with no `--body` or `--body-file` is denied with a request for the body. Edits, merges, and pushes pass.
|
|
39
|
+
`hooks/blocking/pull_request_proof.py` reads the body of each new pull request before it opens. `pr_lifecycle_skill_gate.py` runs it on `gh pr create`, the `pull_request.py create` script, and each `mcp__*__create_pull_request` tool. The gate denies the call when the body has no "Proof in practice" heading, or when the text under that heading names no command in backticks. The gate also denies a section whose only commands run a test runner, such as `pytest`, `python -m unittest`, `uv run pytest`, `npm test`, `npx vitest`, `jest`, `go test` or `cargo test`. Quotes around a runner word, interpreter flags before `-m`, and `coverage run -m unittest` are still test runs. A command is an inline backticked span or a `$ ` line inside a fenced block. A span that starts with a path and line, such as `gate.py:12`, is not a command. A span that holds one path or file name, such as `src/x.py`, is not a command unless that span runs a test runner, such as `py.test` or `pytest.exe`. A test run piped into `tee`, `tail`, `head`, `grep` or `cat` is still a test run. The deny quotes the line "Unit tests are welcome as extra evidence. The live run is the proof." and asks for a run of the changed thing the way its user runs it, without the change and with it. The gate also denies a section whose prose says part of the change is not proven, not tested or not verified, with "yet", "been" or "fully" allowed after the "not", or says unproven, untested or unverified. The deny quotes that line, and asks for the run before the pull request opens, or for the line under a "Not covered" heading when the change does not target it. Words inside backticks or a fenced block are quoted output and do not count. A section with one command that is not a test run, and no such line, passes. A `gh pr create` with no `--body` or `--body-file` is denied with a request for the body. Edits, merges, and pushes pass.
|
|
40
40
|
|
|
41
41
|
When the checked-out branch changes a file people see (`.html`, `.htm`, `.css`, `.scss`, `.tsx`, `.jsx`, `.vue`, `.svelte`), the gate also denies a body whose proof section holds no picture: a markdown image, an image URL, or an Artifact page link that shows the screenshots. Render the change, read the screenshot, and put it in the section, so the reader sees what you looked at. When git in the session directory cannot list the branch's changed files, the gate asks for the picture as well.
|
|
42
42
|
|
|
43
|
-
The gate checks that the section is there
|
|
43
|
+
The gate checks that the section is there, names a command, and admits no unproven part. Whether the run proves the change stays a judgment for the author and the reviewer.
|
|
@@ -56,7 +56,12 @@ from hooks_constants.pull_request_proof_constants import (
|
|
|
56
56
|
FENCED_COMMAND_PROMPT,
|
|
57
57
|
INLINE_COMMAND_PATTERN,
|
|
58
58
|
TEST_ONLY_PROOF_REASON,
|
|
59
|
+
ALL_CODE_FENCE_MARKERS,
|
|
60
|
+
BLOCKQUOTE_MARKERS,
|
|
61
|
+
INLINE_CODE_SPAN_PATTERN,
|
|
59
62
|
UNITTEST_MODULE_NAME,
|
|
63
|
+
UNPROVEN_ADMISSION_PATTERN,
|
|
64
|
+
UNPROVEN_PROOF_REASON,
|
|
60
65
|
ALL_PASSTHROUGH_PROGRAM_NAMES,
|
|
61
66
|
PATH_WITH_LINE_PATTERN,
|
|
62
67
|
SINGLE_FILE_NAME_PATTERN,
|
|
@@ -332,14 +337,31 @@ def _line_commands(line: str, is_inside_fence: bool) -> list[str]:
|
|
|
332
337
|
return []
|
|
333
338
|
|
|
334
339
|
|
|
335
|
-
def
|
|
336
|
-
|
|
337
|
-
|
|
340
|
+
def _fence_marker(line: str) -> str | None:
|
|
341
|
+
fence_text = line.strip().lstrip(BLOCKQUOTE_MARKERS).strip()
|
|
342
|
+
for each_marker in ALL_CODE_FENCE_MARKERS:
|
|
343
|
+
if fence_text.startswith(each_marker) and each_marker not in fence_text[len(each_marker) :]:
|
|
344
|
+
return each_marker
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _lines_with_fence_state(section: str) -> Iterator[tuple[str, bool]]:
|
|
349
|
+
open_marker: str | None = None
|
|
338
350
|
for each_line in section.splitlines():
|
|
339
|
-
|
|
340
|
-
|
|
351
|
+
line_marker = _fence_marker(each_line)
|
|
352
|
+
if line_marker is not None and open_marker is None:
|
|
353
|
+
open_marker = line_marker
|
|
341
354
|
continue
|
|
342
|
-
|
|
355
|
+
if line_marker is not None and line_marker == open_marker:
|
|
356
|
+
open_marker = None
|
|
357
|
+
continue
|
|
358
|
+
yield each_line, open_marker is not None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _proof_commands(section: str) -> list[str]:
|
|
362
|
+
all_commands: list[str] = []
|
|
363
|
+
for each_line, each_is_inside_fence in _lines_with_fence_state(section):
|
|
364
|
+
all_commands.extend(_line_commands(each_line, each_is_inside_fence))
|
|
343
365
|
return [each_command for each_command in all_commands if _reads_as_command(each_command)]
|
|
344
366
|
|
|
345
367
|
|
|
@@ -401,6 +423,35 @@ def _proof_runs_only_tests(section: str) -> bool:
|
|
|
401
423
|
return bool(all_commands) and all(_command_runs_tests(each_command) for each_command in all_commands)
|
|
402
424
|
|
|
403
425
|
|
|
426
|
+
def _prose_lines(section: str) -> Iterator[tuple[str, str]]:
|
|
427
|
+
for each_line, each_is_inside_fence in _lines_with_fence_state(section):
|
|
428
|
+
if not each_is_inside_fence:
|
|
429
|
+
yield each_line, re.sub(INLINE_CODE_SPAN_PATTERN, "", each_line.replace(CODE_FENCE_MARKER, "`"))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def unproven_admission(section: str) -> str | None:
|
|
433
|
+
"""Return the first prose line of a proof section that says something is not proven, or None.
|
|
434
|
+
|
|
435
|
+
::
|
|
436
|
+
|
|
437
|
+
"Not proven here: a fresh Convert run." -> "Not proven here: a fresh Convert run."
|
|
438
|
+
"The live run is untested." -> "The live run is untested."
|
|
439
|
+
"It has not been tested on the target." -> "It has not been tested on the target."
|
|
440
|
+
"`not proven` printed by the probe" -> None
|
|
441
|
+
"> Not proven here: the live run." -> "> Not proven here: the live run."
|
|
442
|
+
"Ran `python run.py`, quoted below." -> None
|
|
443
|
+
|
|
444
|
+
The markers are not proven, not tested and not verified, each with any
|
|
445
|
+
run of yet, been or fully after the not, plus unproven, untested and
|
|
446
|
+
unverified, matched without regard to case. Inline code spans and fenced
|
|
447
|
+
blocks are skipped, so output quoted there never counts.
|
|
448
|
+
"""
|
|
449
|
+
for each_line, each_prose in _prose_lines(section):
|
|
450
|
+
if re.search(UNPROVEN_ADMISSION_PATTERN, each_prose, re.IGNORECASE):
|
|
451
|
+
return each_line.strip()
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
|
|
404
455
|
def missing_proof_reason(all_payload_fields: Mapping[str, object]) -> str | None:
|
|
405
456
|
"""Return a deny reason when a new pull request's body has no proof, else None.
|
|
406
457
|
|
|
@@ -412,6 +463,7 @@ def missing_proof_reason(all_payload_fields: Mapping[str, object]) -> str | None
|
|
|
412
463
|
proof whose only command is `py.test` -> TEST_ONLY_PROOF_REASON
|
|
413
464
|
proof whose only command is `python -q -m unittest` -> TEST_ONLY_PROOF_REASON
|
|
414
465
|
proof with `pytest` and `python run.py` -> None
|
|
466
|
+
proof whose prose says "Not proven here: the run" -> UNPROVEN_PROOF_REASON
|
|
415
467
|
gh pr create --fill -> MISSING_BODY_REASON
|
|
416
468
|
gh pr edit, git push, any other tool -> None
|
|
417
469
|
|
|
@@ -425,6 +477,9 @@ def missing_proof_reason(all_payload_fields: Mapping[str, object]) -> str | None
|
|
|
425
477
|
the section is denied. Quotes around a runner word still count. Interpreter
|
|
426
478
|
flags before -m unittest still count. coverage run -m unittest counts.
|
|
427
479
|
A pipe into tee, tail, head, grep or cat keeps a test run a test run.
|
|
480
|
+
A section whose prose says part of the change is not proven, untested or
|
|
481
|
+
unverified is denied, with the line quoted; unproven_admission lists the
|
|
482
|
+
markers.
|
|
428
483
|
|
|
429
484
|
Args:
|
|
430
485
|
all_payload_fields: The parsed PreToolUse input.
|
|
@@ -437,6 +492,9 @@ def missing_proof_reason(all_payload_fields: Mapping[str, object]) -> str | None
|
|
|
437
492
|
return MISSING_PROOF_REASON
|
|
438
493
|
if _proof_runs_only_tests(section):
|
|
439
494
|
return TEST_ONLY_PROOF_REASON
|
|
495
|
+
admission = unproven_admission(section)
|
|
496
|
+
if admission is not None:
|
|
497
|
+
return UNPROVEN_PROOF_REASON.format(admission=admission)
|
|
440
498
|
return None
|
|
441
499
|
|
|
442
500
|
|
|
@@ -18,6 +18,7 @@ from hooks_constants.pull_request_proof_constants import (
|
|
|
18
18
|
MISSING_EXISTING_WORK_REASON,
|
|
19
19
|
MISSING_PROOF_REASON,
|
|
20
20
|
TEST_ONLY_PROOF_REASON,
|
|
21
|
+
UNPROVEN_PROOF_REASON,
|
|
21
22
|
UNREADABLE_CHANGES_REASON,
|
|
22
23
|
)
|
|
23
24
|
|
|
@@ -111,6 +112,92 @@ def test_proof_with_a_test_run_and_file_references_should_be_denied() -> None:
|
|
|
111
112
|
assert gate.missing_proof_reason(_mcp_create(body)) == TEST_ONLY_PROOF_REASON
|
|
112
113
|
|
|
113
114
|
|
|
115
|
+
def test_proof_that_says_its_target_run_is_not_proven_should_be_denied() -> None:
|
|
116
|
+
body = (
|
|
117
|
+
"## Proof in practice\nRan `python verify.py theme`.\n> PASS\n\n"
|
|
118
|
+
"Not proven here: a fresh Convert run of Bear Penguin that rewrites the record.\n"
|
|
119
|
+
)
|
|
120
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
121
|
+
admission="Not proven here: a fresh Convert run of Bear Penguin that rewrites the record."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
"admission", ["unproven", "not yet tested", "Untested", "unverified", "not verified", "not been tested", "not fully verified"]
|
|
127
|
+
)
|
|
128
|
+
def test_each_unproven_marker_in_proof_prose_should_be_denied(admission: str) -> None:
|
|
129
|
+
admission_line = f"The live dialer path is {admission}."
|
|
130
|
+
body = f"## Proof in practice\nRan `python run.py`.\n{admission_line}\n"
|
|
131
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(admission=admission_line)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_unproven_line_moved_under_a_not_covered_heading_should_pass() -> None:
|
|
135
|
+
body = (
|
|
136
|
+
"## Proof in practice\nRan `python run.py`.\n> PASS\n\n"
|
|
137
|
+
"## Not covered\n\nNot proven here: a run on an older session.\n"
|
|
138
|
+
)
|
|
139
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_unproven_words_inside_quoted_output_should_pass() -> None:
|
|
143
|
+
body = (
|
|
144
|
+
"## Proof in practice\nRan `python run.py`, which printed `unverified sender denied`.\n"
|
|
145
|
+
"```\n$ python run.py\nstatus: not proven\n```\n"
|
|
146
|
+
)
|
|
147
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_unproven_admission_after_a_one_line_triple_backtick_span_should_be_denied() -> None:
|
|
151
|
+
body = (
|
|
152
|
+
"## Proof in practice\nRan `python run.py`.\n```python run.py``` -> PASS\n"
|
|
153
|
+
"Not proven here: the live run on the target.\n"
|
|
154
|
+
)
|
|
155
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
156
|
+
admission="Not proven here: the live run on the target."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_test_run_after_a_one_line_triple_backtick_span_should_be_denied() -> None:
|
|
161
|
+
body = "## Proof in practice\n```pytest``` ok\nRan `pytest tests/`.\n"
|
|
162
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == TEST_ONLY_PROOF_REASON
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_unproven_words_in_a_fence_inside_a_blockquote_should_pass() -> None:
|
|
166
|
+
body = "## Proof in practice\nRan `python run.py`.\n> ```\n> status: not proven\n> ```\n"
|
|
167
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_unproven_words_in_a_tilde_fence_should_pass() -> None:
|
|
171
|
+
body = "## Proof in practice\nRan `python run.py`.\n~~~\nstatus: not proven\n~~~\n"
|
|
172
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_tilde_line_inside_a_backtick_fence_keeps_the_fence_open() -> None:
|
|
176
|
+
body = "## Proof in practice\nRan `python run.py`.\n```\n~~~\nstatus: not proven\n```\n"
|
|
177
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_admission_after_a_backtick_fence_holding_a_tilde_line_should_be_denied() -> None:
|
|
181
|
+
body = "## Proof in practice\nRan `python run.py`.\n```\n~~~\n```\nNot proven here: the live run.\n"
|
|
182
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
183
|
+
admission="Not proven here: the live run."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_unproven_deny_quotes_the_line_as_written() -> None:
|
|
188
|
+
body = "## Proof in practice\nRan `python run.py`.\nNot proven here: the `convert.py` run on the target.\n"
|
|
189
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
190
|
+
admission="Not proven here: the `convert.py` run on the target."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_unproven_admission_inside_a_blockquote_should_be_denied() -> None:
|
|
195
|
+
body = "## Proof in practice\nRan `python run.py`.\n> Not proven here: the live dialer run.\n"
|
|
196
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
197
|
+
admission="> Not proven here: the live dialer run."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
114
201
|
def test_proof_with_a_test_run_piped_into_tee_should_be_denied() -> None:
|
|
115
202
|
body = "## Proof in practice\nRan `pytest x | tee log`.\n"
|
|
116
203
|
assert gate.missing_proof_reason(_mcp_create(body)) == TEST_ONLY_PROOF_REASON
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from turn_review_findings import finding_lines
|
|
2
|
+
|
|
3
|
+
CHANGED_PATH = "src/service.py"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_should_read_a_json_finding_on_a_changed_path() -> None:
|
|
7
|
+
review_text = (
|
|
8
|
+
'Review done.\n[{"file": "src/service.py", "line": 208, "summary": "drops the retry"},'
|
|
9
|
+
' {"file": "src/other.py", "line": 3, "summary": "unchanged file"}]'
|
|
10
|
+
)
|
|
11
|
+
assert finding_lines(review_text, [CHANGED_PATH]) == ["src/service.py:208 drops the retry"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_should_read_a_prose_finding_that_opens_with_a_changed_path() -> None:
|
|
15
|
+
review_text = (
|
|
16
|
+
"- `src/service.py:208` - drops the retry\nThe change at src/service.py:12 is fine."
|
|
17
|
+
)
|
|
18
|
+
assert finding_lines(review_text, [CHANGED_PATH]) == [
|
|
19
|
+
"- `src/service.py:208` - drops the retry"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_should_report_nothing_for_a_review_that_opens_with_none() -> None:
|
|
24
|
+
review_text = "(none)\n- `src/service.py:208` - looked at the retry, it stays"
|
|
25
|
+
assert finding_lines(review_text, [CHANGED_PATH]) == []
|