claude-dev-env 1.57.1 → 1.58.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/bin/install.mjs +217 -27
- package/bin/install.test.mjs +344 -1
- package/hooks/blocking/intent_only_ending_blocker.py +155 -0
- package/hooks/blocking/session_handoff_blocker.py +190 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +175 -0
- package/hooks/blocking/test_session_handoff_blocker.py +312 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/messages.py +4 -0
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +10 -0
- package/hooks/workflow/auto_formatter.py +26 -1
- package/hooks/workflow/test_auto_formatter.py +134 -0
- package/package.json +1 -1
- package/rules/conservative-action.md +1 -0
- package/rules/long-horizon-autonomy.md +43 -0
- package/skills/autoconverge/SKILL.md +56 -6
- package/skills/autoconverge/reference/closing-report.md +44 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +105 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +30 -1
- package/skills/autoconverge/workflow/converge.mjs +12 -14
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +259 -0
- package/skills/autoconverge/workflow/render_report.py +903 -0
- package/skills/autoconverge/workflow/test_render_report.py +484 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""Tests for render_report.py against the real wf_881252e6-700 fixture."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
11
|
+
|
|
12
|
+
import render_report
|
|
13
|
+
|
|
14
|
+
FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" / "wf_run"
|
|
15
|
+
FIXTURE_JOURNAL = FIXTURE_DIR / "workflows" / "wf_881252e6-700.json"
|
|
16
|
+
|
|
17
|
+
EXPECTED_TOTAL_FINDINGS = 15
|
|
18
|
+
EXPECTED_CRITICAL_COUNT = 0
|
|
19
|
+
EXPECTED_MINOR_COUNT = 15
|
|
20
|
+
EXPECTED_FIX_COMMIT_COUNT = 2
|
|
21
|
+
EXPECTED_GENERATED_DATE = "2026-06-13"
|
|
22
|
+
EXPECTED_FINDINGS_BY_ROUND = {1: 11, 2: 2, 3: 2, 4: 0}
|
|
23
|
+
EXPECTED_FINDINGS_BY_THEME = {"src/exports": 11, "src/logging": 2, "src/web": 2}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_load_run_data_aggregate_counts() -> None:
|
|
27
|
+
"""Should parse the fixture journal and transcripts into correct aggregate counts."""
|
|
28
|
+
run_data = render_report.load_run_data(FIXTURE_JOURNAL, Path("."))
|
|
29
|
+
|
|
30
|
+
assert run_data.total_finding_count == EXPECTED_TOTAL_FINDINGS
|
|
31
|
+
assert run_data.critical_finding_count == EXPECTED_CRITICAL_COUNT
|
|
32
|
+
assert run_data.minor_finding_count == EXPECTED_MINOR_COUNT
|
|
33
|
+
assert run_data.fix_commit_count == EXPECTED_FIX_COMMIT_COUNT
|
|
34
|
+
assert run_data.generated_date == EXPECTED_GENERATED_DATE
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_load_run_data_by_round_counts() -> None:
|
|
38
|
+
"""Should assign findings to rounds by workflowProgress position boundary."""
|
|
39
|
+
run_data = render_report.load_run_data(FIXTURE_JOURNAL, Path("."))
|
|
40
|
+
|
|
41
|
+
for each_round, expected_count in EXPECTED_FINDINGS_BY_ROUND.items():
|
|
42
|
+
actual_count = run_data.finding_count_by_round.get(each_round, 0)
|
|
43
|
+
assert actual_count == expected_count, (
|
|
44
|
+
f"Round {each_round}: expected {expected_count}, got {actual_count}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_load_run_data_by_theme_counts() -> None:
|
|
49
|
+
"""Should group distinct findings by the first two path segments."""
|
|
50
|
+
run_data = render_report.load_run_data(FIXTURE_JOURNAL, Path("."))
|
|
51
|
+
|
|
52
|
+
assert len(run_data.finding_count_by_theme) == len(EXPECTED_FINDINGS_BY_THEME)
|
|
53
|
+
for each_theme, expected_count in EXPECTED_FINDINGS_BY_THEME.items():
|
|
54
|
+
actual_count = run_data.finding_count_by_theme.get(each_theme, 0)
|
|
55
|
+
assert actual_count == expected_count, (
|
|
56
|
+
f"Theme {each_theme}: expected {expected_count}, got {actual_count}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_cli_end_to_end(tmp_path: Path) -> None:
|
|
61
|
+
"""Should exit 0, print the output path, and write HTML with expected substrings."""
|
|
62
|
+
out_path = tmp_path / "report.html"
|
|
63
|
+
render_script = Path(__file__).resolve().parent / "render_report.py"
|
|
64
|
+
|
|
65
|
+
completed = subprocess.run(
|
|
66
|
+
[
|
|
67
|
+
sys.executable,
|
|
68
|
+
str(render_script),
|
|
69
|
+
"--journal",
|
|
70
|
+
str(FIXTURE_JOURNAL),
|
|
71
|
+
"--out",
|
|
72
|
+
str(out_path),
|
|
73
|
+
"--pr",
|
|
74
|
+
"example-owner/example-repo#211",
|
|
75
|
+
"--final-sha",
|
|
76
|
+
"7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4",
|
|
77
|
+
"--rounds",
|
|
78
|
+
"4",
|
|
79
|
+
"--repo",
|
|
80
|
+
".",
|
|
81
|
+
],
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
87
|
+
|
|
88
|
+
printed_path = completed.stdout.strip()
|
|
89
|
+
assert printed_path == str(out_path), (
|
|
90
|
+
f"Expected stdout {out_path!r}, got {printed_path!r}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
assert out_path.exists(), "Output HTML file was not written"
|
|
94
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
expected_substrings = [
|
|
97
|
+
"PR #211 Convergence Insights",
|
|
98
|
+
"at-a-glance",
|
|
99
|
+
"Findings by severity",
|
|
100
|
+
"Findings by round",
|
|
101
|
+
"Tests added per round",
|
|
102
|
+
"Findings by theme",
|
|
103
|
+
"Banned identifier",
|
|
104
|
+
"result",
|
|
105
|
+
"in test",
|
|
106
|
+
"Converged",
|
|
107
|
+
"7c2f420c",
|
|
108
|
+
]
|
|
109
|
+
for each_substring in expected_substrings:
|
|
110
|
+
assert each_substring in html_content, (
|
|
111
|
+
f"Expected substring not found in HTML: {each_substring!r}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
minor_card_count = html_content.count('class="bug-card minor"')
|
|
115
|
+
assert minor_card_count == EXPECTED_MINOR_COUNT, (
|
|
116
|
+
f"Expected {EXPECTED_MINOR_COUNT} minor cards, found {minor_card_count}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_html_contains_no_hedging_words(tmp_path: Path) -> None:
|
|
121
|
+
"""Should produce HTML with no hedging language anywhere in the rendered text."""
|
|
122
|
+
out_path = tmp_path / "report-hedge.html"
|
|
123
|
+
render_script = Path(__file__).resolve().parent / "render_report.py"
|
|
124
|
+
|
|
125
|
+
subprocess.run(
|
|
126
|
+
[
|
|
127
|
+
sys.executable,
|
|
128
|
+
str(render_script),
|
|
129
|
+
"--journal",
|
|
130
|
+
str(FIXTURE_JOURNAL),
|
|
131
|
+
"--out",
|
|
132
|
+
str(out_path),
|
|
133
|
+
"--pr",
|
|
134
|
+
"example-owner/example-repo#211",
|
|
135
|
+
"--final-sha",
|
|
136
|
+
"7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4",
|
|
137
|
+
"--rounds",
|
|
138
|
+
"4",
|
|
139
|
+
"--repo",
|
|
140
|
+
".",
|
|
141
|
+
],
|
|
142
|
+
capture_output=True,
|
|
143
|
+
text=True,
|
|
144
|
+
check=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
148
|
+
all_hedging_words = [
|
|
149
|
+
"could",
|
|
150
|
+
"might",
|
|
151
|
+
"would",
|
|
152
|
+
"should",
|
|
153
|
+
"likely",
|
|
154
|
+
"probably",
|
|
155
|
+
"appears",
|
|
156
|
+
"seems",
|
|
157
|
+
]
|
|
158
|
+
for each_word in all_hedging_words:
|
|
159
|
+
pattern = re.compile(r"\b" + re.escape(each_word) + r"\b", re.IGNORECASE)
|
|
160
|
+
assert not pattern.search(html_content), (
|
|
161
|
+
f"Hedging word {each_word!r} found in rendered HTML"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _init_git_repo(repo_path: Path) -> None:
|
|
166
|
+
"""Initialize a git repo with a committed baseline so diffs resolve."""
|
|
167
|
+
subprocess.run(
|
|
168
|
+
["git", "-C", str(repo_path), "init"], capture_output=True, check=True
|
|
169
|
+
)
|
|
170
|
+
subprocess.run(
|
|
171
|
+
["git", "-C", str(repo_path), "config", "user.email", "test@example.com"],
|
|
172
|
+
capture_output=True,
|
|
173
|
+
check=True,
|
|
174
|
+
)
|
|
175
|
+
subprocess.run(
|
|
176
|
+
["git", "-C", str(repo_path), "config", "user.name", "Test"],
|
|
177
|
+
capture_output=True,
|
|
178
|
+
check=True,
|
|
179
|
+
)
|
|
180
|
+
(repo_path / "README.md").write_text("baseline\n", encoding="utf-8")
|
|
181
|
+
subprocess.run(
|
|
182
|
+
["git", "-C", str(repo_path), "add", "."], capture_output=True, check=True
|
|
183
|
+
)
|
|
184
|
+
subprocess.run(
|
|
185
|
+
["git", "-C", str(repo_path), "commit", "-m", "baseline"],
|
|
186
|
+
capture_output=True,
|
|
187
|
+
check=True,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _resolve_head(repo_path: Path) -> str:
|
|
192
|
+
"""Return the current HEAD sha of the repo."""
|
|
193
|
+
completed = subprocess.run(
|
|
194
|
+
["git", "-C", str(repo_path), "rev-parse", "HEAD"],
|
|
195
|
+
capture_output=True,
|
|
196
|
+
text=True,
|
|
197
|
+
check=True,
|
|
198
|
+
)
|
|
199
|
+
return completed.stdout.strip()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_count_tests_added_does_not_double_count_new_file(tmp_path: Path) -> None:
|
|
203
|
+
"""Should count a new test file with two test functions as exactly two."""
|
|
204
|
+
repo_path = tmp_path / "repo"
|
|
205
|
+
repo_path.mkdir()
|
|
206
|
+
_init_git_repo(repo_path)
|
|
207
|
+
base_sha = _resolve_head(repo_path)
|
|
208
|
+
|
|
209
|
+
new_test_file = repo_path / "test_feature.py"
|
|
210
|
+
new_test_file.write_text(
|
|
211
|
+
"def test_one() -> None:\n"
|
|
212
|
+
" assert True\n"
|
|
213
|
+
"\n"
|
|
214
|
+
"def test_two() -> None:\n"
|
|
215
|
+
" assert True\n",
|
|
216
|
+
encoding="utf-8",
|
|
217
|
+
)
|
|
218
|
+
subprocess.run(
|
|
219
|
+
["git", "-C", str(repo_path), "add", "."], capture_output=True, check=True
|
|
220
|
+
)
|
|
221
|
+
subprocess.run(
|
|
222
|
+
["git", "-C", str(repo_path), "commit", "-m", "add tests"],
|
|
223
|
+
capture_output=True,
|
|
224
|
+
check=True,
|
|
225
|
+
)
|
|
226
|
+
new_sha = _resolve_head(repo_path)
|
|
227
|
+
|
|
228
|
+
test_count = render_report._count_tests_added(base_sha, new_sha, repo_path)
|
|
229
|
+
|
|
230
|
+
assert test_count == 2, f"Expected 2 added test definitions, got {test_count}"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_count_tests_added_counts_nested_test_directory(tmp_path: Path) -> None:
|
|
234
|
+
"""Should count test functions added under a nested src/<pkg>/tests/ layout."""
|
|
235
|
+
repo_path = tmp_path / "repo"
|
|
236
|
+
repo_path.mkdir()
|
|
237
|
+
_init_git_repo(repo_path)
|
|
238
|
+
base_sha = _resolve_head(repo_path)
|
|
239
|
+
|
|
240
|
+
nested_test_file = repo_path / "src" / "exports" / "tests" / "test_feature.py"
|
|
241
|
+
nested_test_file.parent.mkdir(parents=True)
|
|
242
|
+
nested_test_file.write_text(
|
|
243
|
+
"def test_one() -> None:\n"
|
|
244
|
+
" assert True\n"
|
|
245
|
+
"\n"
|
|
246
|
+
"def test_two() -> None:\n"
|
|
247
|
+
" assert True\n",
|
|
248
|
+
encoding="utf-8",
|
|
249
|
+
)
|
|
250
|
+
subprocess.run(
|
|
251
|
+
["git", "-C", str(repo_path), "add", "."], capture_output=True, check=True
|
|
252
|
+
)
|
|
253
|
+
subprocess.run(
|
|
254
|
+
["git", "-C", str(repo_path), "commit", "-m", "add nested tests"],
|
|
255
|
+
capture_output=True,
|
|
256
|
+
check=True,
|
|
257
|
+
)
|
|
258
|
+
new_sha = _resolve_head(repo_path)
|
|
259
|
+
|
|
260
|
+
test_count = render_report._count_tests_added(base_sha, new_sha, repo_path)
|
|
261
|
+
|
|
262
|
+
assert test_count == 2, (
|
|
263
|
+
f"Expected 2 added test definitions in nested dir, got {test_count}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_count_tests_added_counts_should_functions(tmp_path: Path) -> None:
|
|
268
|
+
"""Should count pytest should_* functions, not only def test functions."""
|
|
269
|
+
repo_path = tmp_path / "repo"
|
|
270
|
+
repo_path.mkdir()
|
|
271
|
+
_init_git_repo(repo_path)
|
|
272
|
+
base_sha = _resolve_head(repo_path)
|
|
273
|
+
|
|
274
|
+
new_test_file = repo_path / "test_behavior.py"
|
|
275
|
+
new_test_file.write_text(
|
|
276
|
+
"def should_validate_order() -> None:\n"
|
|
277
|
+
" assert True\n"
|
|
278
|
+
"\n"
|
|
279
|
+
"def test_explicit() -> None:\n"
|
|
280
|
+
" assert True\n",
|
|
281
|
+
encoding="utf-8",
|
|
282
|
+
)
|
|
283
|
+
subprocess.run(
|
|
284
|
+
["git", "-C", str(repo_path), "add", "."], capture_output=True, check=True
|
|
285
|
+
)
|
|
286
|
+
subprocess.run(
|
|
287
|
+
["git", "-C", str(repo_path), "commit", "-m", "add should and test"],
|
|
288
|
+
capture_output=True,
|
|
289
|
+
check=True,
|
|
290
|
+
)
|
|
291
|
+
new_sha = _resolve_head(repo_path)
|
|
292
|
+
|
|
293
|
+
test_count = render_report._count_tests_added(base_sha, new_sha, repo_path)
|
|
294
|
+
|
|
295
|
+
assert test_count == 2, (
|
|
296
|
+
f"Expected 2 added definitions (should_ + test), got {test_count}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_extract_structured_output_returns_last_tool_input(tmp_path: Path) -> None:
|
|
301
|
+
"""Should return the input of the last StructuredOutput tool_use in the transcript."""
|
|
302
|
+
transcript_path = tmp_path / "agent-stream.jsonl"
|
|
303
|
+
earlier_line = json.dumps(
|
|
304
|
+
{
|
|
305
|
+
"message": {
|
|
306
|
+
"content": [
|
|
307
|
+
{
|
|
308
|
+
"type": "tool_use",
|
|
309
|
+
"name": render_report.STRUCTURED_OUTPUT_TOOL_NAME,
|
|
310
|
+
"input": {"newSha": "aaaa1111", "pushed": False},
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
later_line = json.dumps(
|
|
317
|
+
{
|
|
318
|
+
"message": {
|
|
319
|
+
"content": [
|
|
320
|
+
{
|
|
321
|
+
"type": "tool_use",
|
|
322
|
+
"name": render_report.STRUCTURED_OUTPUT_TOOL_NAME,
|
|
323
|
+
"input": {"newSha": "bbbb2222", "pushed": True},
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
transcript_path.write_text(earlier_line + "\n" + later_line + "\n", encoding="utf-8")
|
|
330
|
+
|
|
331
|
+
extracted = render_report._extract_structured_output(transcript_path)
|
|
332
|
+
|
|
333
|
+
assert extracted == {"newSha": "bbbb2222", "pushed": True}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_extract_structured_output_returns_none_on_missing_file(tmp_path: Path) -> None:
|
|
337
|
+
"""Should return None when the transcript file does not exist."""
|
|
338
|
+
missing_path = tmp_path / "does-not-exist.jsonl"
|
|
339
|
+
|
|
340
|
+
extracted = render_report._extract_structured_output(missing_path)
|
|
341
|
+
|
|
342
|
+
assert extracted is None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_render_fix_block_falls_back_when_sha_empty() -> None:
|
|
346
|
+
"""Should not claim a commit when the fix record has an empty new sha."""
|
|
347
|
+
finding = render_report.RawFinding(
|
|
348
|
+
file="src/exports/writer.py",
|
|
349
|
+
line=10,
|
|
350
|
+
severity="P2",
|
|
351
|
+
title="example finding",
|
|
352
|
+
detail="example detail",
|
|
353
|
+
round_number=2,
|
|
354
|
+
sha="abc",
|
|
355
|
+
)
|
|
356
|
+
fix_by_round = {
|
|
357
|
+
2: render_report.FixRecord(
|
|
358
|
+
new_sha="",
|
|
359
|
+
pushed=False,
|
|
360
|
+
resolved_without_commit=False,
|
|
361
|
+
round_number=2,
|
|
362
|
+
base_sha="base",
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fix_html = render_report._render_fix_block(finding, fix_by_round)
|
|
367
|
+
|
|
368
|
+
assert "<code></code>" not in fix_html
|
|
369
|
+
assert "fix commit" not in fix_html
|
|
370
|
+
assert "resolved during convergence" in fix_html
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _write_structured_output_transcript(
|
|
374
|
+
transcript_path: Path, tool_input: dict[str, object]
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Write a one-line agent transcript carrying a single StructuredOutput tool_use."""
|
|
377
|
+
line = json.dumps(
|
|
378
|
+
{
|
|
379
|
+
"message": {
|
|
380
|
+
"content": [
|
|
381
|
+
{
|
|
382
|
+
"type": "tool_use",
|
|
383
|
+
"name": render_report.STRUCTURED_OUTPUT_TOOL_NAME,
|
|
384
|
+
"input": tool_input,
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
transcript_path.write_text(line + "\n", encoding="utf-8")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_base_sha_resets_each_round_when_prior_fix_transcript_missing(
|
|
394
|
+
tmp_path: Path,
|
|
395
|
+
) -> None:
|
|
396
|
+
"""Should bind each round's fix base sha to that round's own head, never a prior round's."""
|
|
397
|
+
agents_dir = tmp_path / "agents"
|
|
398
|
+
agents_dir.mkdir()
|
|
399
|
+
|
|
400
|
+
round_one_head = "1111111111111111111111111111111111111111"
|
|
401
|
+
round_two_head = "2222222222222222222222222222222222222222"
|
|
402
|
+
|
|
403
|
+
round_one_gate_id = "round-one-gate"
|
|
404
|
+
round_two_gate_id = "round-two-gate"
|
|
405
|
+
round_two_fix_id = "round-two-fix"
|
|
406
|
+
|
|
407
|
+
_write_structured_output_transcript(
|
|
408
|
+
agents_dir / f"agent-{round_one_gate_id}.jsonl",
|
|
409
|
+
{"sha": round_one_head, "clean": False, "findings": []},
|
|
410
|
+
)
|
|
411
|
+
_write_structured_output_transcript(
|
|
412
|
+
agents_dir / f"agent-{round_two_gate_id}.jsonl",
|
|
413
|
+
{"sha": round_two_head, "clean": False, "findings": []},
|
|
414
|
+
)
|
|
415
|
+
_write_structured_output_transcript(
|
|
416
|
+
agents_dir / f"agent-{round_two_fix_id}.jsonl",
|
|
417
|
+
{"newSha": round_two_head, "pushed": True, "resolvedWithoutCommit": False},
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
progress_entries: list[dict] = [
|
|
421
|
+
{"label": render_report.LABEL_RESOLVE_HEAD, "agentId": "round-one-resolve"},
|
|
422
|
+
{"label": render_report.LABEL_COPILOT_GATE, "agentId": round_one_gate_id},
|
|
423
|
+
{"label": render_report.LABEL_PREFIX_FIX + "copilot", "agentId": "missing-fix"},
|
|
424
|
+
{"label": render_report.LABEL_RESOLVE_HEAD, "agentId": "round-two-resolve"},
|
|
425
|
+
{"label": render_report.LABEL_COPILOT_GATE, "agentId": round_two_gate_id},
|
|
426
|
+
{"label": render_report.LABEL_PREFIX_FIX + "copilot", "agentId": round_two_fix_id},
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
_all_findings, fix_by_round = render_report._parse_progress_entries(
|
|
430
|
+
progress_entries, agents_dir
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
assert fix_by_round[2].base_sha == round_two_head, (
|
|
434
|
+
"Round 2 fix recorded a stale base sha leaked from round 1; "
|
|
435
|
+
f"expected {round_two_head}, got {fix_by_round[2].base_sha}"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_robustness_with_missing_transcripts(tmp_path: Path) -> None:
|
|
440
|
+
"""Should exit 0 and render zero finding cards when no agent transcripts exist."""
|
|
441
|
+
run_root = tmp_path / "wf_run"
|
|
442
|
+
journal_destination = run_root / "workflows" / FIXTURE_JOURNAL.name
|
|
443
|
+
journal_destination.parent.mkdir(parents=True)
|
|
444
|
+
shutil.copy(FIXTURE_JOURNAL, journal_destination)
|
|
445
|
+
|
|
446
|
+
run_id = FIXTURE_JOURNAL.stem
|
|
447
|
+
empty_agents_dir = run_root / "subagents" / "workflows" / run_id
|
|
448
|
+
empty_agents_dir.mkdir(parents=True)
|
|
449
|
+
|
|
450
|
+
out_path = tmp_path / "report-robust.html"
|
|
451
|
+
render_script = Path(__file__).resolve().parent / "render_report.py"
|
|
452
|
+
|
|
453
|
+
completed = subprocess.run(
|
|
454
|
+
[
|
|
455
|
+
sys.executable,
|
|
456
|
+
str(render_script),
|
|
457
|
+
"--journal",
|
|
458
|
+
str(journal_destination),
|
|
459
|
+
"--out",
|
|
460
|
+
str(out_path),
|
|
461
|
+
"--pr",
|
|
462
|
+
"example-owner/example-repo#211",
|
|
463
|
+
"--final-sha",
|
|
464
|
+
"7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4",
|
|
465
|
+
"--rounds",
|
|
466
|
+
"4",
|
|
467
|
+
"--repo",
|
|
468
|
+
".",
|
|
469
|
+
],
|
|
470
|
+
capture_output=True,
|
|
471
|
+
text=True,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
assert completed.returncode == 0, (
|
|
475
|
+
f"Render failed despite missing transcripts:\n{completed.stderr}"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
479
|
+
assert "PR #211 Convergence Insights" in html_content
|
|
480
|
+
|
|
481
|
+
finding_card_count = html_content.count('class="bug-card')
|
|
482
|
+
assert finding_card_count == 0, (
|
|
483
|
+
f"Missing transcripts yielded findings: expected 0 cards, got {finding_card_count}"
|
|
484
|
+
)
|