claude-dev-env 1.59.0 → 1.61.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/CLAUDE.md +4 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
- package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/code_rules_annotations_length.py +189 -10
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +152 -0
- package/hooks/blocking/code_rules_enforcer.py +38 -15
- package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +118 -0
- package/hooks/blocking/destructive_command_blocker.py +483 -61
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_destructive_command_blocker.py +213 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +490 -0
- package/hooks/blocking/test_verified_commit_gate.py +495 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +193 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +686 -0
- package/hooks/blocking/verified_commit_gate.py +535 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +221 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +43 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
- package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/validation/mypy_validator.py +59 -7
- package/hooks/validation/test_mypy_validator.py +94 -0
- package/package.json +1 -1
- package/rules/file-global-constants.md +7 -1
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/orphan-css-class.md +23 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +54 -17
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +192 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +395 -206
- package/skills/autoconverge/workflow/converge.mjs +520 -57
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +518 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/rebase/SKILL.md +2 -4
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -15,59 +15,20 @@ FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" / "wf_run"
|
|
|
15
15
|
FIXTURE_JOURNAL = FIXTURE_DIR / "workflows" / "wf_881252e6-700.json"
|
|
16
16
|
|
|
17
17
|
EXPECTED_TOTAL_FINDINGS = 15
|
|
18
|
-
EXPECTED_CRITICAL_COUNT = 0
|
|
19
|
-
EXPECTED_MINOR_COUNT = 15
|
|
20
18
|
EXPECTED_FIX_COMMIT_COUNT = 2
|
|
21
19
|
EXPECTED_GENERATED_DATE = "2026-06-13"
|
|
22
|
-
|
|
23
|
-
EXPECTED_FINDINGS_BY_THEME = {"src/exports": 11, "src/logging": 2, "src/web": 2}
|
|
20
|
+
EXPECTED_ROUND_COUNT = 4
|
|
24
21
|
|
|
25
22
|
|
|
26
|
-
def
|
|
27
|
-
"""
|
|
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"
|
|
23
|
+
def _render_cli(journal_path: Path, out_path: Path) -> subprocess.CompletedProcess[str]:
|
|
24
|
+
"""Run the render_report CLI against a journal and return the completed process."""
|
|
63
25
|
render_script = Path(__file__).resolve().parent / "render_report.py"
|
|
64
|
-
|
|
65
|
-
completed = subprocess.run(
|
|
26
|
+
return subprocess.run(
|
|
66
27
|
[
|
|
67
28
|
sys.executable,
|
|
68
29
|
str(render_script),
|
|
69
30
|
"--journal",
|
|
70
|
-
str(
|
|
31
|
+
str(journal_path),
|
|
71
32
|
"--out",
|
|
72
33
|
str(out_path),
|
|
73
34
|
"--pr",
|
|
@@ -76,58 +37,327 @@ def test_cli_end_to_end(tmp_path: Path) -> None:
|
|
|
76
37
|
"7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4",
|
|
77
38
|
"--rounds",
|
|
78
39
|
"4",
|
|
79
|
-
"--repo",
|
|
80
|
-
".",
|
|
81
40
|
],
|
|
82
41
|
capture_output=True,
|
|
83
42
|
text=True,
|
|
84
43
|
)
|
|
85
44
|
|
|
86
|
-
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
87
45
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
46
|
+
def _copy_run_tree_without_summary_entry(destination_root: Path) -> Path:
|
|
47
|
+
"""Copy the fixture run tree, dropping the convergence-summary workflowProgress entry.
|
|
48
|
+
|
|
49
|
+
Returns the path to the copied journal whose summarizer entry has been removed.
|
|
50
|
+
"""
|
|
51
|
+
shutil.copytree(FIXTURE_DIR, destination_root)
|
|
52
|
+
journal_destination = destination_root / "workflows" / FIXTURE_JOURNAL.name
|
|
53
|
+
journal = json.loads(journal_destination.read_text(encoding="utf-8"))
|
|
54
|
+
journal["workflowProgress"] = [
|
|
55
|
+
each_entry
|
|
56
|
+
for each_entry in journal["workflowProgress"]
|
|
57
|
+
if each_entry.get("label") != render_report.LABEL_CONVERGENCE_SUMMARY
|
|
58
|
+
]
|
|
59
|
+
journal_destination.write_text(json.dumps(journal, indent=2), encoding="utf-8")
|
|
60
|
+
return journal_destination
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_load_run_data_aggregate_counts() -> None:
|
|
64
|
+
"""Should parse the fixture journal and transcripts into correct aggregate counts."""
|
|
65
|
+
run_data = render_report.load_run_data(FIXTURE_JOURNAL)
|
|
66
|
+
|
|
67
|
+
assert run_data.total_finding_count == EXPECTED_TOTAL_FINDINGS
|
|
68
|
+
assert run_data.fix_commit_count == EXPECTED_FIX_COMMIT_COUNT
|
|
69
|
+
assert run_data.generated_date == EXPECTED_GENERATED_DATE
|
|
70
|
+
assert len(run_data.all_distinct_findings) == EXPECTED_TOTAL_FINDINGS
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_load_run_data_parses_convergence_summary() -> None:
|
|
74
|
+
"""Should locate the convergence-summary entry and parse its StructuredOutput."""
|
|
75
|
+
run_data = render_report.load_run_data(FIXTURE_JOURNAL)
|
|
76
|
+
|
|
77
|
+
assert run_data.convergence_summary is not None
|
|
78
|
+
verdict_line = run_data.convergence_summary["verdictLine"]
|
|
79
|
+
issue_classes = run_data.convergence_summary["issueClasses"]
|
|
80
|
+
assert isinstance(verdict_line, str) and verdict_line
|
|
81
|
+
assert isinstance(issue_classes, list) and len(issue_classes) == 3
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_load_run_data_carries_category_on_findings() -> None:
|
|
85
|
+
"""Should default each finding's category to 'bug' when the raw dict omits it."""
|
|
86
|
+
run_data = render_report.load_run_data(FIXTURE_JOURNAL)
|
|
87
|
+
|
|
88
|
+
assert all(
|
|
89
|
+
each_finding.category == render_report.CATEGORY_BUG
|
|
90
|
+
for each_finding in run_data.all_distinct_findings
|
|
91
91
|
)
|
|
92
92
|
|
|
93
|
+
|
|
94
|
+
def test_cli_renders_verdict_banner_with_python_computed_vsub(tmp_path: Path) -> None:
|
|
95
|
+
"""Should render the verdict banner with verdictLine and a Python-computed vsub."""
|
|
96
|
+
out_path = tmp_path / "report.html"
|
|
97
|
+
|
|
98
|
+
completed = _render_cli(FIXTURE_JOURNAL, out_path)
|
|
99
|
+
|
|
100
|
+
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
101
|
+
assert completed.stdout.strip() == str(out_path)
|
|
93
102
|
assert out_path.exists(), "Output HTML file was not written"
|
|
103
|
+
|
|
94
104
|
html_content = out_path.read_text(encoding="utf-8")
|
|
105
|
+
assert "PR #211 Convergence Summary" in html_content
|
|
106
|
+
assert 'class="verdict"' in html_content
|
|
107
|
+
assert 'class="vtext"' in html_content
|
|
108
|
+
assert "Converged in 4 rounds; 3 distinct issue classes were caught and fixed." in (
|
|
109
|
+
html_content
|
|
110
|
+
)
|
|
111
|
+
assert 'class="vsub"' in html_content
|
|
112
|
+
assert "2 fix commits" in html_content
|
|
113
|
+
assert "final commit 7c2f420c" in html_content
|
|
95
114
|
|
|
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
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
116
|
+
def test_cli_renders_problem_and_fix_scene_cards(tmp_path: Path) -> None:
|
|
117
|
+
"""Should draw problem and fix scene cards with trigger, result, and caption."""
|
|
118
|
+
out_path = tmp_path / "report-scenes.html"
|
|
118
119
|
|
|
120
|
+
completed = _render_cli(FIXTURE_JOURNAL, out_path)
|
|
121
|
+
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
""
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
124
|
+
assert 'class="pf-grid"' in html_content
|
|
125
|
+
assert 'class="pf problem"' in html_content
|
|
126
|
+
assert 'class="pf fix"' in html_content
|
|
127
|
+
assert "export stops at batch 90 of 100" in html_content
|
|
128
|
+
assert "starts again at batch 1" in html_content
|
|
129
|
+
assert "continues at batch 91" in html_content
|
|
130
|
+
assert 'class="res-bad"' in html_content
|
|
131
|
+
assert 'class="res-good"' in html_content
|
|
132
|
+
assert "began again" in html_content
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_render_issue_class_panels_for_each_medium() -> None:
|
|
136
|
+
"""Should draw before/after panels: a code panel and a terminal panel per medium."""
|
|
137
|
+
convergence_summary = {
|
|
138
|
+
"verdictLine": "Converged.",
|
|
139
|
+
"problemScenes": [],
|
|
140
|
+
"fixScenes": [],
|
|
141
|
+
"issueClasses": [
|
|
142
|
+
{
|
|
143
|
+
"plainName": "A missing return type",
|
|
144
|
+
"count": 3,
|
|
145
|
+
"severity": "P2",
|
|
146
|
+
"category": "code-standard",
|
|
147
|
+
"status": "fixed",
|
|
148
|
+
"cause": "Tests did not declare their return type.",
|
|
149
|
+
"medium": "code",
|
|
150
|
+
"beforeLines": ["def test_x():"],
|
|
151
|
+
"afterLines": ["def test_x() -> None:"],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"plainName": "An install that did nothing",
|
|
155
|
+
"count": 1,
|
|
156
|
+
"severity": "P1",
|
|
157
|
+
"category": "bug",
|
|
158
|
+
"status": "fixed",
|
|
159
|
+
"cause": "The command skipped the install.",
|
|
160
|
+
"medium": "terminal",
|
|
161
|
+
"beforeLines": ["~ $ install", "(no output)"],
|
|
162
|
+
"afterLines": ["~ $ install", "Installed."],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
panels_html = render_report._render_issue_class_panels(convergence_summary)
|
|
168
|
+
|
|
169
|
+
assert 'class="code-panel"' in panels_html
|
|
170
|
+
assert "def test_x() -> None:" in panels_html
|
|
171
|
+
assert 'class="terminal"' in panels_html
|
|
172
|
+
assert 'class="term-bar"' in panels_html
|
|
173
|
+
assert "Installed." in panels_html
|
|
174
|
+
assert 'class="term-grid"' in panels_html
|
|
175
|
+
assert 'class="bug-head"' in panels_html
|
|
176
|
+
assert "A missing return type" in panels_html
|
|
177
|
+
assert "An install that did nothing" in panels_html
|
|
178
|
+
assert "3 findings" in panels_html
|
|
179
|
+
assert "1 finding" in panels_html
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_render_issue_class_panels_draws_text_panel_for_text_medium() -> None:
|
|
183
|
+
"""Should draw a text panel holding the supplied lines when the medium is text."""
|
|
184
|
+
convergence_summary = {
|
|
185
|
+
"verdictLine": "Converged.",
|
|
186
|
+
"problemScenes": [],
|
|
187
|
+
"fixScenes": [],
|
|
188
|
+
"issueClasses": [
|
|
189
|
+
{
|
|
190
|
+
"plainName": "A plain-text symptom",
|
|
191
|
+
"count": 1,
|
|
192
|
+
"severity": "P2",
|
|
193
|
+
"category": "bug",
|
|
194
|
+
"status": "fixed",
|
|
195
|
+
"cause": "A grounded cause sentence.",
|
|
196
|
+
"medium": "text",
|
|
197
|
+
"beforeLines": ["pages reloaded every visit"],
|
|
198
|
+
"afterLines": ["pages reuse the saved copy"],
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
panels_html = render_report._render_issue_class_panels(convergence_summary)
|
|
124
204
|
|
|
125
|
-
|
|
205
|
+
assert 'class="text-panel"' in panels_html
|
|
206
|
+
assert "pages reloaded every visit" in panels_html
|
|
207
|
+
assert "pages reuse the saved copy" in panels_html
|
|
208
|
+
assert 'class="terminal"' not in panels_html
|
|
209
|
+
assert 'class="code-panel"' not in panels_html
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_cli_renders_cause_line_with_severity_parenthetical(tmp_path: Path) -> None:
|
|
213
|
+
"""Should render a cause line carrying the plain cause and a muted parenthetical."""
|
|
214
|
+
out_path = tmp_path / "report-cause.html"
|
|
215
|
+
|
|
216
|
+
completed = _render_cli(FIXTURE_JOURNAL, out_path)
|
|
217
|
+
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
218
|
+
|
|
219
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
220
|
+
assert 'class="cause"' in html_content
|
|
221
|
+
assert "which the project's type checker wants" in html_content
|
|
222
|
+
assert "P2" in html_content
|
|
223
|
+
assert "code standard" in html_content
|
|
224
|
+
assert "×7" in html_content
|
|
225
|
+
assert "fixed" in html_content
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_render_issue_class_panels_omitted_when_lines_empty() -> None:
|
|
229
|
+
"""Should draw only the cause line when both before and after lines are empty."""
|
|
230
|
+
convergence_summary = {
|
|
231
|
+
"verdictLine": "Converged.",
|
|
232
|
+
"problemScenes": [],
|
|
233
|
+
"fixScenes": [],
|
|
234
|
+
"issueClasses": [
|
|
235
|
+
{
|
|
236
|
+
"plainName": "A cause-only class",
|
|
237
|
+
"count": 1,
|
|
238
|
+
"severity": "P2",
|
|
239
|
+
"category": "code-standard",
|
|
240
|
+
"status": "fixed",
|
|
241
|
+
"cause": "Nothing visual to show.",
|
|
242
|
+
"medium": "text",
|
|
243
|
+
"beforeLines": [],
|
|
244
|
+
"afterLines": [],
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
panels_html = render_report._render_issue_class_panels(convergence_summary)
|
|
250
|
+
|
|
251
|
+
assert 'class="term-grid"' not in panels_html
|
|
252
|
+
assert 'class="bug-head"' in panels_html
|
|
253
|
+
assert "A cause-only class" in panels_html
|
|
254
|
+
assert 'class="cause"' in panels_html
|
|
255
|
+
assert "Nothing visual to show." in panels_html
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_render_issue_class_panels_clean_state_when_no_classes() -> None:
|
|
259
|
+
"""Should render a clean-state line, not an empty section, when no classes exist."""
|
|
260
|
+
convergence_summary = {
|
|
261
|
+
"verdictLine": "Converged with no issues caught.",
|
|
262
|
+
"problemScenes": [],
|
|
263
|
+
"fixScenes": [],
|
|
264
|
+
"issueClasses": [],
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
panels_html = render_report._render_issue_class_panels(convergence_summary)
|
|
268
|
+
|
|
269
|
+
assert 'class="term-grid"' not in panels_html
|
|
270
|
+
assert "No issues were caught" in panels_html
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_cli_merges_run_stats_lead_into_caught_section(tmp_path: Path) -> None:
|
|
274
|
+
"""Should lead the caught section with run stats and omit any timeline section."""
|
|
275
|
+
out_path = tmp_path / "report-caught-lead.html"
|
|
276
|
+
|
|
277
|
+
completed = _render_cli(FIXTURE_JOURNAL, out_path)
|
|
278
|
+
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
279
|
+
|
|
280
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
281
|
+
assert "What was caught" in html_content
|
|
282
|
+
assert "3 bug classes" in html_content
|
|
283
|
+
assert "15 findings in all" in html_content
|
|
284
|
+
assert "caught and fixed across 4 rounds" in html_content
|
|
285
|
+
assert "2 fix commits" in html_content
|
|
286
|
+
assert "How it converged" not in html_content
|
|
287
|
+
assert 'class="timeline"' not in html_content
|
|
288
|
+
assert 'class="tstep' not in html_content
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_cli_includes_collapsed_appendix(tmp_path: Path) -> None:
|
|
292
|
+
"""Should include a collapsed details appendix listing every distinct finding."""
|
|
293
|
+
out_path = tmp_path / "report-appendix.html"
|
|
294
|
+
|
|
295
|
+
completed = _render_cli(FIXTURE_JOURNAL, out_path)
|
|
296
|
+
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
297
|
+
|
|
298
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
299
|
+
assert '<details class="appendix"' in html_content
|
|
300
|
+
assert f"Raw findings ({EXPECTED_TOTAL_FINDINGS})" in html_content
|
|
301
|
+
assert "src/exports/tests/test_resume_skip_export.py:35" in html_content
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_cli_degraded_layout_when_summary_entry_absent(tmp_path: Path) -> None:
|
|
305
|
+
"""Should render the timeline and appendix but no scene, table, or rollup markup."""
|
|
306
|
+
run_root = tmp_path / "wf_run_no_summary"
|
|
307
|
+
journal_destination = _copy_run_tree_without_summary_entry(run_root)
|
|
308
|
+
|
|
309
|
+
out_path = tmp_path / "report-degraded.html"
|
|
310
|
+
completed = _render_cli(journal_destination, out_path)
|
|
311
|
+
|
|
312
|
+
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
313
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
314
|
+
|
|
315
|
+
assert "PR #211 Convergence Summary" in html_content
|
|
316
|
+
assert 'class="timeline"' not in html_content
|
|
317
|
+
assert "distinct findings across 4 rounds" in html_content
|
|
318
|
+
assert '<details class="appendix"' in html_content
|
|
319
|
+
assert 'class="pf-grid"' not in html_content
|
|
320
|
+
assert 'class="issue-table"' not in html_content
|
|
321
|
+
assert 'class="rollup"' not in html_content
|
|
322
|
+
assert 'class="pr-summary"' not in html_content
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_cli_injects_summary_from_file_bypassing_transcripts(tmp_path: Path) -> None:
|
|
326
|
+
"""Should render the full summary body from --summary-file when no summary transcript exists."""
|
|
327
|
+
run_root = tmp_path / "wf_run_inject"
|
|
328
|
+
journal_destination = _copy_run_tree_without_summary_entry(run_root)
|
|
329
|
+
|
|
330
|
+
summary = {
|
|
331
|
+
"prProblem": "PhotoSync stopped backing up photos after an account switch.",
|
|
332
|
+
"prFix": "It re-checks the account on each backup, so a switch never halts backups.",
|
|
333
|
+
"problemScenes": [],
|
|
334
|
+
"fixScenes": [],
|
|
335
|
+
"verdictLine": "Converged in 4 rounds; every class is fixed.",
|
|
336
|
+
"issueClasses": [
|
|
337
|
+
{
|
|
338
|
+
"plainName": "An injected class the transcript never carried",
|
|
339
|
+
"count": 2,
|
|
340
|
+
"severity": "P1",
|
|
341
|
+
"category": "bug",
|
|
342
|
+
"status": "fixed",
|
|
343
|
+
"cause": "A concrete grounded cause sentence.",
|
|
344
|
+
"medium": "text",
|
|
345
|
+
"beforeLines": [],
|
|
346
|
+
"afterLines": [],
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
}
|
|
350
|
+
summary_path = tmp_path / "summary.json"
|
|
351
|
+
summary_path.write_text(json.dumps(summary), encoding="utf-8")
|
|
352
|
+
|
|
353
|
+
out_path = tmp_path / "report-injected.html"
|
|
354
|
+
render_script = Path(__file__).resolve().parent / "render_report.py"
|
|
355
|
+
completed = subprocess.run(
|
|
126
356
|
[
|
|
127
357
|
sys.executable,
|
|
128
358
|
str(render_script),
|
|
129
359
|
"--journal",
|
|
130
|
-
str(
|
|
360
|
+
str(journal_destination),
|
|
131
361
|
"--out",
|
|
132
362
|
str(out_path),
|
|
133
363
|
"--pr",
|
|
@@ -136,14 +366,29 @@ def test_html_contains_no_hedging_words(tmp_path: Path) -> None:
|
|
|
136
366
|
"7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4",
|
|
137
367
|
"--rounds",
|
|
138
368
|
"4",
|
|
139
|
-
"--
|
|
140
|
-
|
|
369
|
+
"--summary-file",
|
|
370
|
+
str(summary_path),
|
|
141
371
|
],
|
|
142
372
|
capture_output=True,
|
|
143
373
|
text=True,
|
|
144
|
-
check=True,
|
|
145
374
|
)
|
|
146
375
|
|
|
376
|
+
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
377
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
378
|
+
assert 'class="verdict"' in html_content
|
|
379
|
+
assert "Converged in 4 rounds; every class is fixed." in html_content
|
|
380
|
+
assert "An injected class the transcript never carried" in html_content
|
|
381
|
+
assert 'class="pf-grid"' in html_content
|
|
382
|
+
assert f"Raw findings ({EXPECTED_TOTAL_FINDINGS})" in html_content
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_html_contains_no_hedging_words(tmp_path: Path) -> None:
|
|
386
|
+
"""Should produce HTML with no hedging language anywhere in the rendered narrative."""
|
|
387
|
+
out_path = tmp_path / "report-hedge.html"
|
|
388
|
+
|
|
389
|
+
completed = _render_cli(FIXTURE_JOURNAL, out_path)
|
|
390
|
+
assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
|
|
391
|
+
|
|
147
392
|
html_content = out_path.read_text(encoding="utf-8")
|
|
148
393
|
all_hedging_words = [
|
|
149
394
|
"could",
|
|
@@ -162,141 +407,6 @@ def test_html_contains_no_hedging_words(tmp_path: Path) -> None:
|
|
|
162
407
|
)
|
|
163
408
|
|
|
164
409
|
|
|
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
410
|
def test_extract_structured_output_returns_last_tool_input(tmp_path: Path) -> None:
|
|
301
411
|
"""Should return the input of the last StructuredOutput tool_use in the transcript."""
|
|
302
412
|
transcript_path = tmp_path / "agent-stream.jsonl"
|
|
@@ -326,7 +436,9 @@ def test_extract_structured_output_returns_last_tool_input(tmp_path: Path) -> No
|
|
|
326
436
|
}
|
|
327
437
|
}
|
|
328
438
|
)
|
|
329
|
-
transcript_path.write_text(
|
|
439
|
+
transcript_path.write_text(
|
|
440
|
+
earlier_line + "\n" + later_line + "\n", encoding="utf-8"
|
|
441
|
+
)
|
|
330
442
|
|
|
331
443
|
extracted = render_report._extract_structured_output(transcript_path)
|
|
332
444
|
|
|
@@ -342,32 +454,14 @@ def test_extract_structured_output_returns_none_on_missing_file(tmp_path: Path)
|
|
|
342
454
|
assert extracted is None
|
|
343
455
|
|
|
344
456
|
|
|
345
|
-
def
|
|
346
|
-
"""Should
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
severity="P2",
|
|
351
|
-
title="example finding",
|
|
352
|
-
detail="example detail",
|
|
353
|
-
round_number=2,
|
|
354
|
-
sha="abc",
|
|
457
|
+
def test_fix_record_carries_summary_text() -> None:
|
|
458
|
+
"""Should read the fix agent's summary field into the FixRecord."""
|
|
459
|
+
fix_record = render_report._parse_fix_record(
|
|
460
|
+
{"newSha": "abcd1234", "pushed": True, "summary": "renamed and annotated"},
|
|
461
|
+
base_sha="base",
|
|
355
462
|
)
|
|
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
463
|
|
|
366
|
-
|
|
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
|
|
464
|
+
assert fix_record.summary == "renamed and annotated"
|
|
371
465
|
|
|
372
466
|
|
|
373
467
|
def _write_structured_output_transcript(
|
|
@@ -423,7 +517,10 @@ def test_base_sha_resets_each_round_when_prior_fix_transcript_missing(
|
|
|
423
517
|
{"label": render_report.LABEL_PREFIX_FIX + "copilot", "agentId": "missing-fix"},
|
|
424
518
|
{"label": render_report.LABEL_RESOLVE_HEAD, "agentId": "round-two-resolve"},
|
|
425
519
|
{"label": render_report.LABEL_COPILOT_GATE, "agentId": round_two_gate_id},
|
|
426
|
-
{
|
|
520
|
+
{
|
|
521
|
+
"label": render_report.LABEL_PREFIX_FIX + "copilot",
|
|
522
|
+
"agentId": round_two_fix_id,
|
|
523
|
+
},
|
|
427
524
|
]
|
|
428
525
|
|
|
429
526
|
_all_findings, fix_by_round = render_report._parse_progress_entries(
|
|
@@ -436,26 +533,173 @@ def test_base_sha_resets_each_round_when_prior_fix_transcript_missing(
|
|
|
436
533
|
)
|
|
437
534
|
|
|
438
535
|
|
|
439
|
-
def
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
536
|
+
def _render_cli_with_summary_file(
|
|
537
|
+
journal_path: Path, out_path: Path, summary_path: Path
|
|
538
|
+
) -> subprocess.CompletedProcess[str]:
|
|
539
|
+
"""Run the render CLI with an injected --summary-file and return the process."""
|
|
540
|
+
render_script = Path(__file__).resolve().parent / "render_report.py"
|
|
541
|
+
return subprocess.run(
|
|
542
|
+
[
|
|
543
|
+
sys.executable,
|
|
544
|
+
str(render_script),
|
|
545
|
+
"--journal",
|
|
546
|
+
str(journal_path),
|
|
547
|
+
"--out",
|
|
548
|
+
str(out_path),
|
|
549
|
+
"--pr",
|
|
550
|
+
"example-owner/example-repo#211",
|
|
551
|
+
"--final-sha",
|
|
552
|
+
"7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4",
|
|
553
|
+
"--rounds",
|
|
554
|
+
"4",
|
|
555
|
+
"--summary-file",
|
|
556
|
+
str(summary_path),
|
|
557
|
+
],
|
|
558
|
+
capture_output=True,
|
|
559
|
+
text=True,
|
|
560
|
+
)
|
|
445
561
|
|
|
446
|
-
run_id = FIXTURE_JOURNAL.stem
|
|
447
|
-
empty_agents_dir = run_root / "subagents" / "workflows" / run_id
|
|
448
|
-
empty_agents_dir.mkdir(parents=True)
|
|
449
562
|
|
|
450
|
-
|
|
451
|
-
|
|
563
|
+
def test_cli_renders_when_issue_class_count_is_null(tmp_path: Path) -> None:
|
|
564
|
+
"""Should exit 0 and show a zero count when an issue class carries count: null."""
|
|
565
|
+
run_root = tmp_path / "wf_run_null_count"
|
|
566
|
+
journal_destination = _copy_run_tree_without_summary_entry(run_root)
|
|
567
|
+
|
|
568
|
+
summary = {
|
|
569
|
+
"prProblem": "A problem.",
|
|
570
|
+
"prFix": "A fix.",
|
|
571
|
+
"problemScenes": [],
|
|
572
|
+
"fixScenes": [],
|
|
573
|
+
"verdictLine": "Converged.",
|
|
574
|
+
"issueClasses": [
|
|
575
|
+
{
|
|
576
|
+
"plainName": "A class with a null count",
|
|
577
|
+
"count": None,
|
|
578
|
+
"severity": "P2",
|
|
579
|
+
"category": "bug",
|
|
580
|
+
"status": "fixed",
|
|
581
|
+
"cause": "A grounded cause.",
|
|
582
|
+
"medium": "text",
|
|
583
|
+
"beforeLines": [],
|
|
584
|
+
"afterLines": [],
|
|
585
|
+
}
|
|
586
|
+
],
|
|
587
|
+
}
|
|
588
|
+
summary_path = tmp_path / "summary-null-count.json"
|
|
589
|
+
summary_path.write_text(json.dumps(summary), encoding="utf-8")
|
|
590
|
+
|
|
591
|
+
out_path = tmp_path / "report-null-count.html"
|
|
592
|
+
completed = _render_cli_with_summary_file(
|
|
593
|
+
journal_destination, out_path, summary_path
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
assert completed.returncode == 0, f"CLI crashed on null count:\n{completed.stderr}"
|
|
597
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
598
|
+
assert "A class with a null count" in html_content
|
|
599
|
+
assert "0 findings" in html_content
|
|
600
|
+
assert "×0" in html_content
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def test_cli_renders_when_issue_class_count_is_non_numeric(tmp_path: Path) -> None:
|
|
604
|
+
"""Should exit 0 and show a zero count when an issue class count is a bad string."""
|
|
605
|
+
run_root = tmp_path / "wf_run_bad_count"
|
|
606
|
+
journal_destination = _copy_run_tree_without_summary_entry(run_root)
|
|
607
|
+
|
|
608
|
+
summary = {
|
|
609
|
+
"prProblem": "A problem.",
|
|
610
|
+
"prFix": "A fix.",
|
|
611
|
+
"problemScenes": [],
|
|
612
|
+
"fixScenes": [],
|
|
613
|
+
"verdictLine": "Converged.",
|
|
614
|
+
"issueClasses": [
|
|
615
|
+
{
|
|
616
|
+
"plainName": "A class with a non-numeric count",
|
|
617
|
+
"count": "x",
|
|
618
|
+
"severity": "P2",
|
|
619
|
+
"category": "bug",
|
|
620
|
+
"status": "fixed",
|
|
621
|
+
"cause": "A grounded cause.",
|
|
622
|
+
"medium": "text",
|
|
623
|
+
"beforeLines": [],
|
|
624
|
+
"afterLines": [],
|
|
625
|
+
}
|
|
626
|
+
],
|
|
627
|
+
}
|
|
628
|
+
summary_path = tmp_path / "summary-bad-count.json"
|
|
629
|
+
summary_path.write_text(json.dumps(summary), encoding="utf-8")
|
|
630
|
+
|
|
631
|
+
out_path = tmp_path / "report-bad-count.html"
|
|
632
|
+
completed = _render_cli_with_summary_file(
|
|
633
|
+
journal_destination, out_path, summary_path
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
assert completed.returncode == 0, (
|
|
637
|
+
f"CLI crashed on non-numeric count:\n{completed.stderr}"
|
|
638
|
+
)
|
|
639
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
640
|
+
assert "A class with a non-numeric count" in html_content
|
|
641
|
+
assert "0 findings" in html_content
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def test_cli_renders_degraded_body_when_summary_is_a_list(tmp_path: Path) -> None:
|
|
645
|
+
"""Should render the degraded layout and exit 0 when --summary-file holds a list."""
|
|
646
|
+
run_root = tmp_path / "wf_run_list_summary"
|
|
647
|
+
journal_destination = _copy_run_tree_without_summary_entry(run_root)
|
|
648
|
+
|
|
649
|
+
summary_path = tmp_path / "summary-list.json"
|
|
650
|
+
summary_path.write_text(json.dumps([]), encoding="utf-8")
|
|
651
|
+
|
|
652
|
+
out_path = tmp_path / "report-list-summary.html"
|
|
653
|
+
completed = _render_cli_with_summary_file(
|
|
654
|
+
journal_destination, out_path, summary_path
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
assert completed.returncode == 0, (
|
|
658
|
+
f"CLI crashed on a list summary:\n{completed.stderr}"
|
|
659
|
+
)
|
|
660
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
661
|
+
assert "distinct findings across 4 rounds" in html_content
|
|
662
|
+
assert 'class="pf-grid"' not in html_content
|
|
663
|
+
|
|
452
664
|
|
|
665
|
+
def test_cli_renders_degraded_body_when_summary_is_a_scalar(tmp_path: Path) -> None:
|
|
666
|
+
"""Should render the degraded layout and exit 0 when --summary-file holds a scalar."""
|
|
667
|
+
run_root = tmp_path / "wf_run_scalar_summary"
|
|
668
|
+
journal_destination = _copy_run_tree_without_summary_entry(run_root)
|
|
669
|
+
|
|
670
|
+
summary_path = tmp_path / "summary-scalar.json"
|
|
671
|
+
summary_path.write_text(json.dumps(5), encoding="utf-8")
|
|
672
|
+
|
|
673
|
+
out_path = tmp_path / "report-scalar-summary.html"
|
|
674
|
+
completed = _render_cli_with_summary_file(
|
|
675
|
+
journal_destination, out_path, summary_path
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
assert completed.returncode == 0, (
|
|
679
|
+
f"CLI crashed on a scalar summary:\n{completed.stderr}"
|
|
680
|
+
)
|
|
681
|
+
html_content = out_path.read_text(encoding="utf-8")
|
|
682
|
+
assert "distinct findings across 4 rounds" in html_content
|
|
683
|
+
assert 'class="pf-grid"' not in html_content
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def test_is_summary_structurally_valid_false_for_non_dict_summary() -> None:
|
|
687
|
+
"""Should return False for a list, string, or scalar summary, never raising."""
|
|
688
|
+
assert render_report._is_summary_structurally_valid([]) is False
|
|
689
|
+
assert render_report._is_summary_structurally_valid("str") is False
|
|
690
|
+
assert render_report._is_summary_structurally_valid(5) is False
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def test_cli_rejects_orphaned_repo_argument(tmp_path: Path) -> None:
|
|
694
|
+
"""Should reject --repo with a usage error, proving the flag is no longer declared."""
|
|
695
|
+
render_script = Path(__file__).resolve().parent / "render_report.py"
|
|
696
|
+
out_path = tmp_path / "report-repo-rejected.html"
|
|
453
697
|
completed = subprocess.run(
|
|
454
698
|
[
|
|
455
699
|
sys.executable,
|
|
456
700
|
str(render_script),
|
|
457
701
|
"--journal",
|
|
458
|
-
str(
|
|
702
|
+
str(FIXTURE_JOURNAL),
|
|
459
703
|
"--out",
|
|
460
704
|
str(out_path),
|
|
461
705
|
"--pr",
|
|
@@ -471,14 +715,29 @@ def test_robustness_with_missing_transcripts(tmp_path: Path) -> None:
|
|
|
471
715
|
text=True,
|
|
472
716
|
)
|
|
473
717
|
|
|
718
|
+
assert completed.returncode != 0
|
|
719
|
+
assert "unrecognized arguments: --repo" in completed.stderr
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def test_robustness_with_missing_transcripts(tmp_path: Path) -> None:
|
|
723
|
+
"""Should exit 0 and render the timeline and appendix when no transcripts exist."""
|
|
724
|
+
run_root = tmp_path / "wf_run"
|
|
725
|
+
journal_destination = run_root / "workflows" / FIXTURE_JOURNAL.name
|
|
726
|
+
journal_destination.parent.mkdir(parents=True)
|
|
727
|
+
shutil.copy(FIXTURE_JOURNAL, journal_destination)
|
|
728
|
+
|
|
729
|
+
run_id = FIXTURE_JOURNAL.stem
|
|
730
|
+
empty_agents_dir = run_root / "subagents" / "workflows" / run_id
|
|
731
|
+
empty_agents_dir.mkdir(parents=True)
|
|
732
|
+
|
|
733
|
+
out_path = tmp_path / "report-robust.html"
|
|
734
|
+
completed = _render_cli(journal_destination, out_path)
|
|
735
|
+
|
|
474
736
|
assert completed.returncode == 0, (
|
|
475
737
|
f"Render failed despite missing transcripts:\n{completed.stderr}"
|
|
476
738
|
)
|
|
477
739
|
|
|
478
740
|
html_content = out_path.read_text(encoding="utf-8")
|
|
479
|
-
assert "PR #211 Convergence
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
assert finding_card_count == 0, (
|
|
483
|
-
f"Missing transcripts yielded findings: expected 0 cards, got {finding_card_count}"
|
|
484
|
-
)
|
|
741
|
+
assert "PR #211 Convergence Summary" in html_content
|
|
742
|
+
assert 'class="timeline"' not in html_content
|
|
743
|
+
assert 'class="pf-grid"' not in html_content
|