@iseoane/agent-depot 0.2.0 → 0.3.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/CHANGELOG.md +47 -0
- package/README.md +117 -3
- package/dist/src/app-cli.js +172 -0
- package/dist/src/app-flow.js +457 -0
- package/dist/src/app-latest.js +63 -0
- package/dist/src/app-owned-skills.js +42 -0
- package/dist/src/app-recipes.js +116 -0
- package/dist/src/app-schema.js +45 -0
- package/dist/src/app-version.js +46 -0
- package/dist/src/atomic-file.js +26 -0
- package/dist/src/cli.js +1256 -0
- package/dist/src/git-source.js +409 -0
- package/dist/src/path-safety.js +31 -0
- package/dist/src/process-runner.js +41 -0
- package/dist/src/project-installation.js +1411 -0
- package/dist/src/project-manifest.js +511 -0
- package/dist/src/skill-adoption.js +77 -0
- package/dist/src/skill-discovery.js +467 -0
- package/dist/src/skill-hosts.js +103 -0
- package/dist/src/skill-install.js +374 -0
- package/dist/src/skill-removal.js +119 -0
- package/dist/src/skill-update.js +244 -0
- package/dist/src/source-state.js +240 -0
- package/dist/src/sources.js +391 -0
- package/dist/src/tui/adoption-actions.js +144 -0
- package/dist/src/tui/app-actions.js +157 -0
- package/dist/src/tui/app.js +63 -0
- package/dist/src/tui/batch.js +17 -0
- package/dist/src/tui/bulk-actions.js +243 -0
- package/dist/src/tui/catalog-actions.js +9 -0
- package/dist/src/tui/catalog-browse.js +59 -0
- package/dist/src/tui/catalog-filter.js +20 -0
- package/dist/src/tui/catalog-flow.js +107 -0
- package/dist/src/tui/catalog-installs.js +193 -0
- package/dist/src/tui/catalog-panel.js +47 -0
- package/dist/src/tui/catalog-state.js +92 -0
- package/dist/src/tui/catalog-tree.js +35 -0
- package/dist/src/tui/catalog-view.js +89 -0
- package/dist/src/tui/environment.js +1 -0
- package/dist/src/tui/installations-browse.js +136 -0
- package/dist/src/tui/installations-mode.js +1 -0
- package/dist/src/tui/installations-panel.js +27 -0
- package/dist/src/tui/installations-rows.js +11 -0
- package/dist/src/tui/installations-tree.js +80 -0
- package/dist/src/tui/installations-view.js +154 -0
- package/dist/src/tui/installations.js +172 -0
- package/dist/src/tui/keys.js +31 -0
- package/dist/src/tui/manage-actions.js +174 -0
- package/dist/src/tui/manage-panel.js +65 -0
- package/dist/src/tui/mode-keys.js +61 -0
- package/dist/src/tui/panel-parts.js +23 -0
- package/dist/src/tui/render.js +8 -0
- package/dist/src/tui/sources-mode.js +17 -0
- package/dist/src/tui/sources-view.js +276 -0
- package/dist/src/tui/theme.js +29 -0
- package/dist/src/tui/tree-navigation.js +68 -0
- package/dist/src/tui/tree.js +53 -0
- package/dist/src/tui/unmanaged-actions.js +178 -0
- package/dist/src/tui/updates-flow.js +89 -0
- package/dist/src/tui/updates-keys.js +104 -0
- package/dist/src/tui/updates-mode.js +1 -0
- package/dist/src/tui/updates-model.js +48 -0
- package/dist/src/tui/updates-panel.js +44 -0
- package/dist/src/tui/updates-view.js +101 -0
- package/dist/src/tui/updates.js +230 -0
- package/dist/src/tui/view-state.js +42 -0
- package/dist/src/tui/window.js +31 -0
- package/dist/src/unmanaged-removal.js +60 -0
- package/dist/src/update-batch.js +376 -0
- package/dist/src/update-flow.js +135 -0
- package/dist/src/usage-error.js +7 -0
- package/dist/src/user-global-skill-inventory.js +565 -0
- package/dist/src/user-method.js +14 -0
- package/package.json +7 -4
- package/skills/agent-depot-apprecipe/SKILL.md +44 -0
- package/skills/agent-depot-apprecipe/assets/example.md +25 -0
- package/skills/doctor-md-agents/scripts/test_collect_sessions.py +0 -780
- package/skills/doctor-md-agents/scripts/test_extract_rules.py +0 -530
- package/skills/doctor-md-agents/scripts/test_render_report.py +0 -383
- package/skills/doctor-md-agents/scripts/test_scan_corrections.py +0 -343
- package/skills/doctor-md-agents/scripts/test_verify_citations.py +0 -236
- package/skills/doctor-md-agents/scripts/testdata/old_extract_rules_pre_t1.py.txt +0 -507
- package/skills/doctor-md-skill/scripts/test_collect_sessions.py +0 -593
- package/skills/doctor-md-skill/scripts/test_detect_machine_sessions.py +0 -181
- package/skills/doctor-md-skill/scripts/test_render_report.py +0 -241
- package/skills/doctor-md-skill/scripts/test_verify_citations.py +0 -196
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Tests for render_report.py."""
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import re
|
|
6
|
-
import tempfile
|
|
7
|
-
import unittest
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
import render_report as rr
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def minimal_report(**overrides):
|
|
14
|
-
report = {
|
|
15
|
-
"title": "Agent Instructions Report",
|
|
16
|
-
"generated_at": "2026-09-22T00:00:00Z",
|
|
17
|
-
"harness": "claude",
|
|
18
|
-
"scope": "reports_savesearch",
|
|
19
|
-
"windows": {"corrections_days": 90, "adherence_days": 45},
|
|
20
|
-
"stats": {
|
|
21
|
-
"files": 1, "rules": 1, "est_tokens": 100,
|
|
22
|
-
"sessions_scanned": 10, "sessions_judged": 5,
|
|
23
|
-
"corrections_found": 2, "corrections_dropped_as_machine": 0,
|
|
24
|
-
"rules_without_evidence": 1,
|
|
25
|
-
"findings_total": 0, "findings_unverified": 0,
|
|
26
|
-
},
|
|
27
|
-
"files": [
|
|
28
|
-
{"label": "project:CLAUDE.md", "path": "/repo/CLAUDE.md", "scope": "project",
|
|
29
|
-
"rules": 1, "est_tokens": 100,
|
|
30
|
-
"verdicts": {"keep": 0, "improve": 1, "drop": 0, "unknown": 0}},
|
|
31
|
-
],
|
|
32
|
-
"top_findings": ["Something notable."],
|
|
33
|
-
"rules": [
|
|
34
|
-
{"id": "project:CLAUDE.md#7", "file_label": "project:CLAUDE.md", "section": "Rules",
|
|
35
|
-
"title": "Use pnpm", "line": 7, "est_tokens": 100,
|
|
36
|
-
"applicable_sessions": 3, "judged_sessions": 5, "adherence": "mixed",
|
|
37
|
-
"classes": {"harm": 0, "non-compliance": 2, "irrelevant": 0},
|
|
38
|
-
"corrections": 2, "verdict": "improve",
|
|
39
|
-
"rationale": "Corrections on its subject despite the rule existing.",
|
|
40
|
-
"evidence": [{"session_id": "session-1", "class": "non-compliance",
|
|
41
|
-
"quote": "used npm install here", "reason": "ignored the rule"}],
|
|
42
|
-
"proposed_path": None, "diff": None},
|
|
43
|
-
],
|
|
44
|
-
"missing_rules": [],
|
|
45
|
-
"notes": ["Corrections come from sessions_scanned; adherence from sessions_judged."],
|
|
46
|
-
}
|
|
47
|
-
report.update(overrides)
|
|
48
|
-
return report
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class RenderTests(unittest.TestCase):
|
|
52
|
-
def test_renders_minimal_report_without_error(self):
|
|
53
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
54
|
-
report_path = Path(tmp) / "report.json"
|
|
55
|
-
report_path.write_text(json.dumps(minimal_report()), encoding="utf-8")
|
|
56
|
-
rr.main([str(report_path)])
|
|
57
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
58
|
-
self.assertIn("<html", out)
|
|
59
|
-
self.assertIn("Use pnpm", out)
|
|
60
|
-
|
|
61
|
-
def test_script_tag_in_rationale_and_title_is_escaped(self):
|
|
62
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
63
|
-
report = minimal_report()
|
|
64
|
-
report["rules"][0]["rationale"] = "<script>alert(1)</script>"
|
|
65
|
-
report["rules"][0]["title"] = "<script>alert(1)</script>"
|
|
66
|
-
report_path = Path(tmp) / "report.json"
|
|
67
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
68
|
-
rr.main([str(report_path)])
|
|
69
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
70
|
-
self.assertNotIn("<script>alert(1)</script>", out)
|
|
71
|
-
self.assertIn("<script>alert(1)</script>", out)
|
|
72
|
-
|
|
73
|
-
def test_missing_optional_fields_do_not_crash(self):
|
|
74
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
75
|
-
report = minimal_report()
|
|
76
|
-
report["rules"][0]["diff"] = None
|
|
77
|
-
report["rules"][0]["proposed_path"] = None
|
|
78
|
-
report["missing_rules"] = []
|
|
79
|
-
report_path = Path(tmp) / "report.json"
|
|
80
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
81
|
-
rr.main([str(report_path)])
|
|
82
|
-
out_path = Path(tmp) / "report.html"
|
|
83
|
-
self.assertTrue(out_path.exists())
|
|
84
|
-
self.assertIn("No missing-rule candidates", out_path.read_text(encoding="utf-8"))
|
|
85
|
-
|
|
86
|
-
def test_out_flag_writes_to_custom_path(self):
|
|
87
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
88
|
-
report_path = Path(tmp) / "report.json"
|
|
89
|
-
report_path.write_text(json.dumps(minimal_report()), encoding="utf-8")
|
|
90
|
-
custom_out = Path(tmp) / "nested" / "custom.html"
|
|
91
|
-
rr.main([str(report_path), "--out", str(custom_out)])
|
|
92
|
-
self.assertTrue(custom_out.exists())
|
|
93
|
-
|
|
94
|
-
def test_proposed_diff_is_rendered_with_line_classes(self):
|
|
95
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
96
|
-
report = minimal_report()
|
|
97
|
-
report["rules"][0]["diff"] = "--- a\n+++ b\n@@ -1 +1 @@\n-old line\n+new line\n"
|
|
98
|
-
report_path = Path(tmp) / "report.json"
|
|
99
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
100
|
-
rr.main([str(report_path)])
|
|
101
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
102
|
-
self.assertIn("diff-add", out)
|
|
103
|
-
self.assertIn("diff-del", out)
|
|
104
|
-
|
|
105
|
-
def test_no_evidence_block_reports_token_share(self):
|
|
106
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
107
|
-
report = minimal_report()
|
|
108
|
-
report["rules"][0]["adherence"] = "insufficient_evidence"
|
|
109
|
-
report["rules"][0]["corrections"] = 0
|
|
110
|
-
report["rules"][0]["applicable_sessions"] = 0
|
|
111
|
-
report["rules"][0]["evidence"] = []
|
|
112
|
-
report_path = Path(tmp) / "report.json"
|
|
113
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
114
|
-
rr.main([str(report_path)])
|
|
115
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
116
|
-
self.assertIn("No evidence", out)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class NewFieldsTests(unittest.TestCase):
|
|
120
|
-
def test_new_stats_fields_render(self):
|
|
121
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
122
|
-
report = minimal_report()
|
|
123
|
-
report["stats"]["corrections_dropped_as_machine"] = 686
|
|
124
|
-
report["stats"]["findings_total"] = 40
|
|
125
|
-
report["stats"]["findings_unverified"] = 3
|
|
126
|
-
report_path = Path(tmp) / "report.json"
|
|
127
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
128
|
-
rr.main([str(report_path)])
|
|
129
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
130
|
-
self.assertIn("686", out)
|
|
131
|
-
self.assertIn("37", out) # findings_verified = 40 - 3
|
|
132
|
-
self.assertIn("40", out)
|
|
133
|
-
|
|
134
|
-
def test_harm_rule_marked_differently_than_noncompliance_only_rule(self):
|
|
135
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
136
|
-
report = minimal_report()
|
|
137
|
-
harm_rule = dict(report["rules"][0])
|
|
138
|
-
harm_rule["id"] = "project:CLAUDE.md#20"
|
|
139
|
-
harm_rule["title"] = "Always run the slow linter"
|
|
140
|
-
harm_rule["classes"] = {"harm": 2, "non-compliance": 0, "irrelevant": 0}
|
|
141
|
-
harm_rule["evidence"] = [
|
|
142
|
-
{"session_id": "session-9", "class": "harm",
|
|
143
|
-
"quote": "following this rule sent the fix down the wrong path",
|
|
144
|
-
"reason": "contradicted the repo's own tooling"},
|
|
145
|
-
]
|
|
146
|
-
noncompliance_rule = dict(report["rules"][0])
|
|
147
|
-
noncompliance_rule["id"] = "project:CLAUDE.md#7"
|
|
148
|
-
report["rules"] = [harm_rule, noncompliance_rule]
|
|
149
|
-
report_path = Path(tmp) / "report.json"
|
|
150
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
151
|
-
rr.main([str(report_path)])
|
|
152
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
153
|
-
self.assertIn("has-harm", out)
|
|
154
|
-
self.assertIn('badge drop" title="harm', out)
|
|
155
|
-
self.assertIn('badge improve" title="non-compliance', out)
|
|
156
|
-
# The non-compliance-only row must NOT be marked has-harm.
|
|
157
|
-
harm_row_start = out.index("Always run the slow linter")
|
|
158
|
-
noncompliance_row_start = out.index("Use pnpm")
|
|
159
|
-
self.assertIn("has-harm", out[max(0, harm_row_start - 400):harm_row_start])
|
|
160
|
-
self.assertNotIn("has-harm", out[max(0, noncompliance_row_start - 400):noncompliance_row_start])
|
|
161
|
-
|
|
162
|
-
def test_legacy_string_array_evidence_still_renders(self):
|
|
163
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
164
|
-
report = minimal_report()
|
|
165
|
-
report["rules"][0]["evidence"] = ["session-1", "session-2"]
|
|
166
|
-
report_path = Path(tmp) / "report.json"
|
|
167
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
168
|
-
rr.main([str(report_path)])
|
|
169
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
170
|
-
self.assertIn("session-1", out)
|
|
171
|
-
self.assertIn("session-2", out)
|
|
172
|
-
|
|
173
|
-
def test_findings_unverified_surfaces(self):
|
|
174
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
175
|
-
report = minimal_report()
|
|
176
|
-
report["stats"]["findings_total"] = 10
|
|
177
|
-
report["stats"]["findings_unverified"] = 4
|
|
178
|
-
report_path = Path(tmp) / "report.json"
|
|
179
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
180
|
-
rr.main([str(report_path)])
|
|
181
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
182
|
-
self.assertIn("failed quote verification", out)
|
|
183
|
-
|
|
184
|
-
def test_evidence_quote_is_escaped(self):
|
|
185
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
186
|
-
report = minimal_report()
|
|
187
|
-
report["rules"][0]["evidence"] = [
|
|
188
|
-
{"session_id": "session-1", "class": "non-compliance",
|
|
189
|
-
"quote": "<script>alert('evidence')</script>", "reason": "an injected quote"},
|
|
190
|
-
]
|
|
191
|
-
report_path = Path(tmp) / "report.json"
|
|
192
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
193
|
-
rr.main([str(report_path)])
|
|
194
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
195
|
-
self.assertNotIn("<script>alert('evidence')</script>", out)
|
|
196
|
-
self.assertIn("<script>alert('evidence')</script>", out)
|
|
197
|
-
|
|
198
|
-
def test_unjudged_rule_with_no_evidence_is_not_marked_unverified(self):
|
|
199
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
200
|
-
report = minimal_report()
|
|
201
|
-
report["rules"][0]["adherence"] = "insufficient_evidence"
|
|
202
|
-
report["rules"][0]["applicable_sessions"] = 0
|
|
203
|
-
report["rules"][0]["evidence"] = []
|
|
204
|
-
report_path = Path(tmp) / "report.json"
|
|
205
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
206
|
-
rr.main([str(report_path)])
|
|
207
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
208
|
-
self.assertNotIn("evidence unverified", out)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
class CardLayoutTests(unittest.TestCase):
|
|
212
|
-
"""The rules table (9 columns, prose crammed into a numeric grid) was
|
|
213
|
-
replaced with a card-per-rule layout. These tests guard the specific
|
|
214
|
-
failure mode that forced the rewrite: a long unbroken token widening
|
|
215
|
-
its container, and the table markup / per-column sort UI actually
|
|
216
|
-
being gone rather than just unused."""
|
|
217
|
-
|
|
218
|
-
def test_long_unbroken_token_in_title_has_a_wrapping_css_rule_applied(self):
|
|
219
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
220
|
-
report = minimal_report()
|
|
221
|
-
long_token = "a" * 200 # one unbroken token -- no spaces to wrap on
|
|
222
|
-
report["rules"][0]["title"] = long_token
|
|
223
|
-
report_path = Path(tmp) / "report.json"
|
|
224
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
225
|
-
rr.main([str(report_path)])
|
|
226
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
227
|
-
|
|
228
|
-
# The CSS rule must exist and be scoped to .rule-title specifically
|
|
229
|
-
# (not just present somewhere on the page).
|
|
230
|
-
css_rule = re.search(r"\.rule-title\s*\{[^}]*overflow-wrap:\s*anywhere", out)
|
|
231
|
-
self.assertIsNotNone(
|
|
232
|
-
css_rule, ".rule-title must have an overflow-wrap: anywhere rule"
|
|
233
|
-
)
|
|
234
|
-
# And the rendered title element for this rule actually carries
|
|
235
|
-
# that class, so the rule is applied, not just declared.
|
|
236
|
-
self.assertIn(f'<div class="rule-title">{long_token}', out)
|
|
237
|
-
|
|
238
|
-
def test_sort_toolbar_renders_one_button_per_key(self):
|
|
239
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
240
|
-
report_path = Path(tmp) / "report.json"
|
|
241
|
-
report_path.write_text(json.dumps(minimal_report()), encoding="utf-8")
|
|
242
|
-
rr.main([str(report_path)])
|
|
243
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
244
|
-
self.assertEqual(out.count('data-key="tokens"'), 1)
|
|
245
|
-
self.assertEqual(out.count('data-key="sessions"'), 1)
|
|
246
|
-
self.assertEqual(out.count('data-key="verdict"'), 1)
|
|
247
|
-
self.assertIn(">coste<", out)
|
|
248
|
-
self.assertIn(">sesiones<", out)
|
|
249
|
-
self.assertIn(">veredicto<", out)
|
|
250
|
-
|
|
251
|
-
def test_no_table_markup_remains(self):
|
|
252
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
253
|
-
report_path = Path(tmp) / "report.json"
|
|
254
|
-
report_path.write_text(json.dumps(minimal_report()), encoding="utf-8")
|
|
255
|
-
rr.main([str(report_path)])
|
|
256
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
257
|
-
self.assertNotIn("<table", out)
|
|
258
|
-
self.assertNotIn("<tr", out)
|
|
259
|
-
self.assertNotIn("<td", out)
|
|
260
|
-
self.assertIn("rule-card", out)
|
|
261
|
-
|
|
262
|
-
def test_rule_cards_stack_in_a_single_column(self):
|
|
263
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
264
|
-
report_path = Path(tmp) / "report.json"
|
|
265
|
-
report_path.write_text(json.dumps(minimal_report()), encoding="utf-8")
|
|
266
|
-
rr.main([str(report_path)])
|
|
267
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
268
|
-
container_rule = re.search(r"\.rule-cards\s*\{([^}]*)\}", out)
|
|
269
|
-
self.assertIsNotNone(container_rule)
|
|
270
|
-
self.assertNotIn("grid-template-columns", container_rule.group(1))
|
|
271
|
-
|
|
272
|
-
def test_metrics_and_rationale_render_outside_any_table_cell(self):
|
|
273
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
274
|
-
report = minimal_report()
|
|
275
|
-
report["rules"][0]["rationale"] = "A full sentence that must read as prose, not a ribbon."
|
|
276
|
-
report_path = Path(tmp) / "report.json"
|
|
277
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
278
|
-
rr.main([str(report_path)])
|
|
279
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
280
|
-
self.assertIn(
|
|
281
|
-
'<p class="rule-rationale">A full sentence that must read as prose, not a ribbon.</p>',
|
|
282
|
-
out,
|
|
283
|
-
)
|
|
284
|
-
self.assertIn("rule-metrics", out)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
class StructureSectionTests(unittest.TestCase):
|
|
288
|
-
"""report.json's optional `structure` section (Step 5b's restructure
|
|
289
|
-
proposal): one entry per file, each with its blocks (condition -> rule
|
|
290
|
-
ids -> basis) and a per-file estimated-condition count."""
|
|
291
|
-
|
|
292
|
-
def structured_report(self):
|
|
293
|
-
report = minimal_report()
|
|
294
|
-
report["structure"] = [
|
|
295
|
-
{
|
|
296
|
-
"file_label": "project:CLAUDE.md",
|
|
297
|
-
"proposed_path": "proposed/project:CLAUDE.md/RESTRUCTURED.md",
|
|
298
|
-
"diff_path": "proposed/project:CLAUDE.md/RESTRUCTURED.diff",
|
|
299
|
-
"bare": 5,
|
|
300
|
-
"wrapped": 12,
|
|
301
|
-
"estimated_conditions": 3,
|
|
302
|
-
"blocks": [
|
|
303
|
-
{
|
|
304
|
-
"condition": "editing backend Celery workers",
|
|
305
|
-
"rule_ids": ["project:CLAUDE.md#40", "project:CLAUDE.md#41"],
|
|
306
|
-
"basis": "measured",
|
|
307
|
-
"sessions": ["session-1", "session-2"],
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
"condition": "changing the xlsx dependency",
|
|
311
|
-
"rule_ids": ["project:CLAUDE.md#88"],
|
|
312
|
-
"basis": "estimated",
|
|
313
|
-
"sessions": [],
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
|
-
}
|
|
317
|
-
]
|
|
318
|
-
return report
|
|
319
|
-
|
|
320
|
-
def test_structure_section_renders_condition_and_rule_ids(self):
|
|
321
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
322
|
-
report_path = Path(tmp) / "report.json"
|
|
323
|
-
report_path.write_text(json.dumps(self.structured_report()), encoding="utf-8")
|
|
324
|
-
rr.main([str(report_path)])
|
|
325
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
326
|
-
self.assertIn("editing backend Celery workers", out)
|
|
327
|
-
self.assertIn("project:CLAUDE.md#40", out)
|
|
328
|
-
self.assertIn("estimated", out)
|
|
329
|
-
self.assertIn("measured", out)
|
|
330
|
-
|
|
331
|
-
def test_structure_section_appears_after_rules_and_before_missing(self):
|
|
332
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
333
|
-
report_path = Path(tmp) / "report.json"
|
|
334
|
-
report_path.write_text(json.dumps(self.structured_report()), encoding="utf-8")
|
|
335
|
-
rr.main([str(report_path)])
|
|
336
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
337
|
-
rules_idx = out.index("Rules by file")
|
|
338
|
-
structure_idx = out.index("editing backend Celery workers")
|
|
339
|
-
missing_idx = out.index("Missing-rule candidates")
|
|
340
|
-
self.assertLess(rules_idx, structure_idx)
|
|
341
|
-
self.assertLess(structure_idx, missing_idx)
|
|
342
|
-
|
|
343
|
-
def test_structure_condition_is_escaped(self):
|
|
344
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
345
|
-
report = self.structured_report()
|
|
346
|
-
report["structure"][0]["blocks"][0]["condition"] = "<script>alert(1)</script>"
|
|
347
|
-
report_path = Path(tmp) / "report.json"
|
|
348
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
349
|
-
rr.main([str(report_path)])
|
|
350
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
351
|
-
self.assertNotIn("<script>alert(1)</script>", out)
|
|
352
|
-
self.assertIn("<script>alert(1)</script>", out)
|
|
353
|
-
|
|
354
|
-
def test_no_table_markup_remains_with_structure_present(self):
|
|
355
|
-
# The card-layout guard (test_no_table_markup_remains) must still
|
|
356
|
-
# hold once a real structure section is rendered alongside it.
|
|
357
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
358
|
-
report_path = Path(tmp) / "report.json"
|
|
359
|
-
report_path.write_text(json.dumps(self.structured_report()), encoding="utf-8")
|
|
360
|
-
rr.main([str(report_path)])
|
|
361
|
-
out = (Path(tmp) / "report.html").read_text(encoding="utf-8")
|
|
362
|
-
self.assertNotIn("<table", out)
|
|
363
|
-
self.assertNotIn("<tr", out)
|
|
364
|
-
self.assertNotIn("<td", out)
|
|
365
|
-
|
|
366
|
-
def test_missing_structure_key_still_renders_backward_compat(self):
|
|
367
|
-
# A report.json produced before T4 has no "structure" key at all.
|
|
368
|
-
# render_report.py must not crash and must not show the section.
|
|
369
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
370
|
-
report = minimal_report()
|
|
371
|
-
self.assertNotIn("structure", report)
|
|
372
|
-
report_path = Path(tmp) / "report.json"
|
|
373
|
-
report_path.write_text(json.dumps(report), encoding="utf-8")
|
|
374
|
-
rr.main([str(report_path)])
|
|
375
|
-
out_path = Path(tmp) / "report.html"
|
|
376
|
-
self.assertTrue(out_path.exists())
|
|
377
|
-
out = out_path.read_text(encoding="utf-8")
|
|
378
|
-
self.assertIn("<html", out)
|
|
379
|
-
self.assertNotIn("Proposed restructure", out)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if __name__ == "__main__":
|
|
383
|
-
unittest.main()
|