claude-dev-env 1.83.0 → 1.84.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/_shared/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +0 -4
- package/hooks/blocking/code_rules_constants_config.py +5 -0
- package/hooks/blocking/code_rules_docstrings.py +116 -6
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +11 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/hooks_constants/CLAUDE.md +0 -3
- package/hooks/hooks_constants/blocking_check_limits.py +3 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -1
- package/rules/plain-illustrative-docstrings.md +35 -1
- package/skills/CLAUDE.md +6 -1
- package/skills/autoconverge/SKILL.md +38 -57
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +70 -73
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +72 -59
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +15 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -1,613 +0,0 @@
|
|
|
1
|
-
"""Tests for md_to_html_companion hook.
|
|
2
|
-
|
|
3
|
-
This test suite validates that the md-to-html companion hook correctly
|
|
4
|
-
generates HTML from markdown input, handles edge cases, and produces
|
|
5
|
-
valid HTML output.
|
|
6
|
-
|
|
7
|
-
Sandbox parent is created lazily by a session-scoped fixture rather than at
|
|
8
|
-
module import time, so test collection has no side effect on the filesystem.
|
|
9
|
-
The sandbox is rooted in a per-session unique directory created via
|
|
10
|
-
`tempfile.mkdtemp` so the OS-temp exemption (which the companion shares with
|
|
11
|
-
the blocker) does not silently skip the hook during tests.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import functools
|
|
15
|
-
import json
|
|
16
|
-
import os
|
|
17
|
-
import shutil
|
|
18
|
-
import stat
|
|
19
|
-
import subprocess
|
|
20
|
-
import sys
|
|
21
|
-
import tempfile
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
import pytest
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "md_to_html_companion.py")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _strip_read_only_and_retry(removal_function, target_path, *_exc_info):
|
|
31
|
-
try:
|
|
32
|
-
os.chmod(target_path, stat.S_IWRITE)
|
|
33
|
-
removal_function(target_path)
|
|
34
|
-
except OSError:
|
|
35
|
-
pass
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _force_rmtree(target_path: str) -> None:
|
|
39
|
-
handler_kw = (
|
|
40
|
-
{"onexc": _strip_read_only_and_retry}
|
|
41
|
-
if sys.version_info >= (3, 12)
|
|
42
|
-
else {"onerror": _strip_read_only_and_retry}
|
|
43
|
-
)
|
|
44
|
-
try:
|
|
45
|
-
shutil.rmtree(target_path, **handler_kw)
|
|
46
|
-
except OSError:
|
|
47
|
-
pass
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@functools.lru_cache(maxsize=1)
|
|
51
|
-
def _get_sandbox_parent_directory() -> str:
|
|
52
|
-
return tempfile.mkdtemp(prefix="pytest_md_companion_", dir=str(Path.home()))
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@pytest.fixture(scope="session", autouse=True)
|
|
56
|
-
def _cleanup_sandbox_parent_directory():
|
|
57
|
-
yield
|
|
58
|
-
if _get_sandbox_parent_directory.cache_info().currsize:
|
|
59
|
-
_force_rmtree(_get_sandbox_parent_directory())
|
|
60
|
-
_get_sandbox_parent_directory.cache_clear()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _make_sandbox() -> tempfile.TemporaryDirectory:
|
|
64
|
-
"""Return a TemporaryDirectory rooted outside the OS temp directory.
|
|
65
|
-
|
|
66
|
-
The companion exempts the OS temp directory (mirroring the blocker), so
|
|
67
|
-
the default `tempfile.TemporaryDirectory()` would prevent the test hook
|
|
68
|
-
invocation generating any HTML sidecar at all.
|
|
69
|
-
"""
|
|
70
|
-
return tempfile.TemporaryDirectory(dir=_get_sandbox_parent_directory())
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class _RunHook:
|
|
74
|
-
def __call__(self, tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
|
|
75
|
-
payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
|
|
76
|
-
return subprocess.run(
|
|
77
|
-
[sys.executable, HOOK_SCRIPT_PATH],
|
|
78
|
-
input=payload,
|
|
79
|
-
capture_output=True,
|
|
80
|
-
text=True,
|
|
81
|
-
check=False,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
_run_hook = _RunHook()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def test_generates_html_companion():
|
|
89
|
-
with _make_sandbox() as tmp:
|
|
90
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
91
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
95
|
-
f.write("# Hello\n\nThis is a test.")
|
|
96
|
-
|
|
97
|
-
result = _run_hook(
|
|
98
|
-
"Write", {"file_path": md_path, "content": "# Hello\n\nThis is a test."}
|
|
99
|
-
)
|
|
100
|
-
assert result.returncode == 0
|
|
101
|
-
assert os.path.exists(html_path)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def test_html_contains_heading():
|
|
105
|
-
with _make_sandbox() as tmp:
|
|
106
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
107
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
108
|
-
f.write("# Hello World")
|
|
109
|
-
|
|
110
|
-
_run_hook("Write", {"file_path": md_path, "content": "# Hello World"})
|
|
111
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
112
|
-
with open(html_path, encoding="utf-8") as f:
|
|
113
|
-
html = f.read()
|
|
114
|
-
assert "<h1>" in html
|
|
115
|
-
assert "Hello World" in html
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def test_html_wraps_in_template():
|
|
119
|
-
with _make_sandbox() as tmp:
|
|
120
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
121
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
122
|
-
f.write("plain text")
|
|
123
|
-
|
|
124
|
-
_run_hook("Write", {"file_path": md_path, "content": "plain text"})
|
|
125
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
126
|
-
with open(html_path, encoding="utf-8") as f:
|
|
127
|
-
html = f.read()
|
|
128
|
-
assert "<!DOCTYPE html>" in html
|
|
129
|
-
assert "<style>" in html
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def test_skips_non_md_files():
|
|
133
|
-
with _make_sandbox() as tmp:
|
|
134
|
-
py_path = os.path.join(tmp, "main.py")
|
|
135
|
-
html_path = os.path.join(tmp, "main.html")
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
with open(py_path, "w", encoding="utf-8") as f:
|
|
139
|
-
f.write("x = 1")
|
|
140
|
-
|
|
141
|
-
result = _run_hook("Write", {"file_path": py_path, "content": "x = 1"})
|
|
142
|
-
assert result.returncode == 0
|
|
143
|
-
assert not os.path.exists(html_path)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def test_skips_claude_dir():
|
|
147
|
-
with _make_sandbox() as tmp:
|
|
148
|
-
claude_dir = os.path.join(tmp, ".claude")
|
|
149
|
-
md_path = os.path.join(claude_dir, "CLAUDE.md")
|
|
150
|
-
html_path = os.path.join(claude_dir, "CLAUDE.html")
|
|
151
|
-
|
|
152
|
-
os.makedirs(claude_dir, exist_ok=True)
|
|
153
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
154
|
-
f.write("# CLAUDE.md")
|
|
155
|
-
|
|
156
|
-
result = _run_hook("Write", {"file_path": md_path, "content": "# CLAUDE.md"})
|
|
157
|
-
assert result.returncode == 0
|
|
158
|
-
assert not os.path.exists(html_path)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def test_unknown_tool_passes():
|
|
162
|
-
result = _run_hook("Grep", {"pattern": "foo"})
|
|
163
|
-
assert result.returncode == 0
|
|
164
|
-
assert result.stdout == ""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def test_empty_file_path_passes():
|
|
168
|
-
result = _run_hook("Write", {"file_path": "", "content": "# Hello"})
|
|
169
|
-
assert result.returncode == 0
|
|
170
|
-
assert result.stdout == ""
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def test_nonexistent_md_passes():
|
|
174
|
-
result = _run_hook(
|
|
175
|
-
"Write",
|
|
176
|
-
{"file_path": "/nonexistent/path/guide.md", "content": "# Hello"},
|
|
177
|
-
)
|
|
178
|
-
assert result.returncode == 0
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def test_converts_code_fence():
|
|
182
|
-
with _make_sandbox() as tmp:
|
|
183
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
184
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
185
|
-
f.write("```python\nprint('hi')\n```")
|
|
186
|
-
|
|
187
|
-
_run_hook(
|
|
188
|
-
"Write", {"file_path": md_path, "content": "```python\nprint('hi')\n```"}
|
|
189
|
-
)
|
|
190
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
191
|
-
with open(html_path, encoding="utf-8") as f:
|
|
192
|
-
html = f.read()
|
|
193
|
-
assert "<pre>" in html
|
|
194
|
-
assert "<code" in html
|
|
195
|
-
assert "print('hi')" in html
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def test_converts_bold():
|
|
199
|
-
with _make_sandbox() as tmp:
|
|
200
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
201
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
202
|
-
f.write("This is **bold** text.")
|
|
203
|
-
|
|
204
|
-
_run_hook("Write", {"file_path": md_path, "content": "This is **bold** text."})
|
|
205
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
206
|
-
with open(html_path, encoding="utf-8") as f:
|
|
207
|
-
html = f.read()
|
|
208
|
-
assert "<strong>bold</strong>" in html
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def test_escapes_html_special_chars():
|
|
212
|
-
with _make_sandbox() as tmp:
|
|
213
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
214
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
215
|
-
f.write("Use <div> for layout & choose \"text\" for quotes.")
|
|
216
|
-
|
|
217
|
-
_run_hook(
|
|
218
|
-
"Write",
|
|
219
|
-
{
|
|
220
|
-
"file_path": md_path,
|
|
221
|
-
"content": "Use <div> for layout & choose \"text\" for quotes.",
|
|
222
|
-
},
|
|
223
|
-
)
|
|
224
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
225
|
-
with open(html_path, encoding="utf-8") as f:
|
|
226
|
-
html = f.read()
|
|
227
|
-
assert "<div>" in html
|
|
228
|
-
assert "&" in html
|
|
229
|
-
assert "<div>" not in html
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def test_escapes_code_block_content():
|
|
233
|
-
with _make_sandbox() as tmp:
|
|
234
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
235
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
236
|
-
f.write("```\nif x < 5 and y > 3:\n print('hello')\n```")
|
|
237
|
-
|
|
238
|
-
_run_hook(
|
|
239
|
-
"Write",
|
|
240
|
-
{
|
|
241
|
-
"file_path": md_path,
|
|
242
|
-
"content": "```\nif x < 5 and y > 3:\n print('hello')\n```",
|
|
243
|
-
},
|
|
244
|
-
)
|
|
245
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
246
|
-
with open(html_path, encoding="utf-8") as f:
|
|
247
|
-
html = f.read()
|
|
248
|
-
assert "<" in html
|
|
249
|
-
assert "if x" in html
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def test_lists_are_wrapped_in_ul():
|
|
253
|
-
with _make_sandbox() as tmp:
|
|
254
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
255
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
256
|
-
f.write("- item one\n- item two\n- item three")
|
|
257
|
-
|
|
258
|
-
_run_hook(
|
|
259
|
-
"Write",
|
|
260
|
-
{
|
|
261
|
-
"file_path": md_path,
|
|
262
|
-
"content": "- item one\n- item two\n- item three",
|
|
263
|
-
},
|
|
264
|
-
)
|
|
265
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
266
|
-
with open(html_path, encoding="utf-8") as f:
|
|
267
|
-
html = f.read()
|
|
268
|
-
assert "<ul>" in html
|
|
269
|
-
assert "</ul>" in html
|
|
270
|
-
assert html.index("<ul>") < html.index("<li>item one</li>")
|
|
271
|
-
assert html.index("</li>") < html.index("</ul>")
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def test_ordered_lists_are_wrapped_in_ol():
|
|
275
|
-
with _make_sandbox() as tmp:
|
|
276
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
277
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
278
|
-
f.write("1. first\n2. second")
|
|
279
|
-
|
|
280
|
-
_run_hook(
|
|
281
|
-
"Write",
|
|
282
|
-
{"file_path": md_path, "content": "1. first\n2. second"},
|
|
283
|
-
)
|
|
284
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
285
|
-
with open(html_path, encoding="utf-8") as f:
|
|
286
|
-
html = f.read()
|
|
287
|
-
assert "<ol>" in html
|
|
288
|
-
assert "</ol>" in html
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def test_handles_curly_braces_in_body():
|
|
292
|
-
with _make_sandbox() as tmp:
|
|
293
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
294
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
295
|
-
f.write("# JS Example\n\nUse `{ foo: 1 }` in code.")
|
|
296
|
-
|
|
297
|
-
_run_hook(
|
|
298
|
-
"Write",
|
|
299
|
-
{
|
|
300
|
-
"file_path": md_path,
|
|
301
|
-
"content": "# JS Example\n\nUse `{ foo: 1 }` in code.",
|
|
302
|
-
},
|
|
303
|
-
)
|
|
304
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
305
|
-
with open(html_path, encoding="utf-8") as f:
|
|
306
|
-
html = f.read()
|
|
307
|
-
assert "{ foo: 1 }" in html
|
|
308
|
-
assert "{{" not in html
|
|
309
|
-
assert "JS Example" in html
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
def test_escapes_title_in_html_output():
|
|
313
|
-
with _make_sandbox() as tmp:
|
|
314
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
315
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
316
|
-
f.write("# Hackers <3 Markdown & <scripts>")
|
|
317
|
-
|
|
318
|
-
_run_hook(
|
|
319
|
-
"Write",
|
|
320
|
-
{
|
|
321
|
-
"file_path": md_path,
|
|
322
|
-
"content": "# Hackers <3 Markdown & <scripts>",
|
|
323
|
-
},
|
|
324
|
-
)
|
|
325
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
326
|
-
with open(html_path, encoding="utf-8") as f:
|
|
327
|
-
html = f.read()
|
|
328
|
-
assert "<title>Hackers <3 Markdown & <scripts></title>" in html
|
|
329
|
-
assert "<script>" not in html
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def test_skips_root_readme():
|
|
333
|
-
with _make_sandbox() as tmp:
|
|
334
|
-
Path(tmp, ".git").touch()
|
|
335
|
-
original_cwd = os.getcwd()
|
|
336
|
-
try:
|
|
337
|
-
os.chdir(tmp)
|
|
338
|
-
for each_name in ("README.md", "readme.md"):
|
|
339
|
-
with open(each_name, "w", encoding="utf-8") as f:
|
|
340
|
-
f.write("# Test")
|
|
341
|
-
result = _run_hook(
|
|
342
|
-
"Write", {"file_path": each_name, "content": "# Test"}
|
|
343
|
-
)
|
|
344
|
-
assert result.returncode == 0
|
|
345
|
-
expected_html = each_name.replace(".md", ".html")
|
|
346
|
-
assert not os.path.exists(expected_html)
|
|
347
|
-
finally:
|
|
348
|
-
os.chdir(original_cwd)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def test_skips_root_changelog():
|
|
352
|
-
with _make_sandbox() as tmp:
|
|
353
|
-
Path(tmp, ".git").touch()
|
|
354
|
-
original_cwd = os.getcwd()
|
|
355
|
-
try:
|
|
356
|
-
os.chdir(tmp)
|
|
357
|
-
for each_name in ("CHANGELOG.md", "changelog.md"):
|
|
358
|
-
with open(each_name, "w", encoding="utf-8") as f:
|
|
359
|
-
f.write("# Test")
|
|
360
|
-
result = _run_hook(
|
|
361
|
-
"Write", {"file_path": each_name, "content": "# Test"}
|
|
362
|
-
)
|
|
363
|
-
assert result.returncode == 0
|
|
364
|
-
expected_html = each_name.replace(".md", ".html")
|
|
365
|
-
assert not os.path.exists(expected_html)
|
|
366
|
-
finally:
|
|
367
|
-
os.chdir(original_cwd)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def test_language_class_valid():
|
|
371
|
-
with _make_sandbox() as tmp:
|
|
372
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
373
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
374
|
-
f.write("```python\nx = 1\n```")
|
|
375
|
-
|
|
376
|
-
_run_hook("Write", {"file_path": md_path, "content": "```python\nx = 1\n```"})
|
|
377
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
378
|
-
with open(html_path, encoding="utf-8") as f:
|
|
379
|
-
html = f.read()
|
|
380
|
-
assert 'class="language-python"' in html
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def test_language_class_skips_invalid():
|
|
384
|
-
with _make_sandbox() as tmp:
|
|
385
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
386
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
387
|
-
f.write("```my lang\nx = 1\n```")
|
|
388
|
-
|
|
389
|
-
_run_hook("Write", {"file_path": md_path, "content": "```my lang\nx = 1\n```"})
|
|
390
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
391
|
-
with open(html_path, encoding="utf-8") as f:
|
|
392
|
-
html = f.read()
|
|
393
|
-
assert "<pre><code>" in html
|
|
394
|
-
assert 'class="language-' not in html
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def test_language_class_allows_valid_chars():
|
|
398
|
-
with _make_sandbox() as tmp:
|
|
399
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
400
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
401
|
-
f.write("```c++\nint x = 1;\n```")
|
|
402
|
-
|
|
403
|
-
_run_hook("Write", {"file_path": md_path, "content": "```c++\nint x = 1;\n```"})
|
|
404
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
405
|
-
with open(html_path, encoding="utf-8") as f:
|
|
406
|
-
html = f.read()
|
|
407
|
-
assert 'class="language-c++"' in html
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
def test_link_text_asterisks_remain_literal():
|
|
411
|
-
with _make_sandbox() as tmp:
|
|
412
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
413
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
414
|
-
f.write("See [text *not italic*](url).")
|
|
415
|
-
|
|
416
|
-
_run_hook(
|
|
417
|
-
"Write",
|
|
418
|
-
{"file_path": md_path, "content": "See [text *not italic*](url)."},
|
|
419
|
-
)
|
|
420
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
421
|
-
with open(html_path, encoding="utf-8") as f:
|
|
422
|
-
html = f.read()
|
|
423
|
-
assert '<a href="url">text *not italic*</a>' in html
|
|
424
|
-
assert "<em>" not in html
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
def test_handles_parentheses_in_links():
|
|
428
|
-
with _make_sandbox() as tmp:
|
|
429
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
430
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
431
|
-
f.write(
|
|
432
|
-
"See [Python]"
|
|
433
|
-
"(https://en.wikipedia.org/wiki/Python_(programming_language))."
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
_run_hook(
|
|
437
|
-
"Write",
|
|
438
|
-
{
|
|
439
|
-
"file_path": md_path,
|
|
440
|
-
"content": "See [Python]"
|
|
441
|
-
"(https://en.wikipedia.org/wiki/Python_(programming_language)).",
|
|
442
|
-
},
|
|
443
|
-
)
|
|
444
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
445
|
-
with open(html_path, encoding="utf-8") as f:
|
|
446
|
-
html = f.read()
|
|
447
|
-
assert (
|
|
448
|
-
'href="https://en.wikipedia.org/wiki/Python_(programming_language)"'
|
|
449
|
-
in html
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
def test_does_not_skip_nested_readme():
|
|
454
|
-
with _make_sandbox() as tmp:
|
|
455
|
-
nested_dir = os.path.join(tmp, "docs")
|
|
456
|
-
os.makedirs(nested_dir)
|
|
457
|
-
md_path = os.path.join(nested_dir, "README.md")
|
|
458
|
-
html_path = os.path.join(nested_dir, "README.html")
|
|
459
|
-
|
|
460
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
461
|
-
f.write("# Nested README")
|
|
462
|
-
|
|
463
|
-
result = _run_hook(
|
|
464
|
-
"Write",
|
|
465
|
-
{"file_path": md_path, "content": "# Nested README"},
|
|
466
|
-
)
|
|
467
|
-
assert result.returncode == 0
|
|
468
|
-
assert os.path.exists(html_path)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
def test_inline_code_preserves_asterisks():
|
|
472
|
-
with _make_sandbox() as tmp:
|
|
473
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
474
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
475
|
-
f.write("Type `**bold**` in a docstring.")
|
|
476
|
-
|
|
477
|
-
_run_hook(
|
|
478
|
-
"Write",
|
|
479
|
-
{
|
|
480
|
-
"file_path": md_path,
|
|
481
|
-
"content": "Type `**bold**` in a docstring.",
|
|
482
|
-
},
|
|
483
|
-
)
|
|
484
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
485
|
-
with open(html_path, encoding="utf-8") as f:
|
|
486
|
-
html = f.read()
|
|
487
|
-
assert "<code>**bold**</code>" in html
|
|
488
|
-
assert "<strong>" not in html
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def test_blocks_javascript_url_scheme():
|
|
492
|
-
with _make_sandbox() as tmp:
|
|
493
|
-
md_path = os.path.join(tmp, "guide.md")
|
|
494
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
495
|
-
f.write("[click me](javascript:alert(1))")
|
|
496
|
-
|
|
497
|
-
_run_hook(
|
|
498
|
-
"Write",
|
|
499
|
-
{
|
|
500
|
-
"file_path": md_path,
|
|
501
|
-
"content": "[click me](javascript:alert(1))",
|
|
502
|
-
},
|
|
503
|
-
)
|
|
504
|
-
html_path = os.path.join(tmp, "guide.html")
|
|
505
|
-
with open(html_path, encoding="utf-8") as f:
|
|
506
|
-
html = f.read()
|
|
507
|
-
assert "javascript:" not in html
|
|
508
|
-
assert "click me" in html
|
|
509
|
-
assert "<a" not in html
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
def test_companion_skips_home_session_log_directory(tmp_path, monkeypatch):
|
|
513
|
-
synthetic_home_directory = tmp_path / "synthetic_home"
|
|
514
|
-
synthetic_home_directory.mkdir()
|
|
515
|
-
monkeypatch.setenv("HOME", str(synthetic_home_directory))
|
|
516
|
-
monkeypatch.setenv("USERPROFILE", str(synthetic_home_directory))
|
|
517
|
-
session_log_directory = synthetic_home_directory / "SessionLog" / "decisions"
|
|
518
|
-
session_log_directory.mkdir(parents=True)
|
|
519
|
-
md_path = str(session_log_directory / "companion_exempt_test.md")
|
|
520
|
-
html_path = str(session_log_directory / "companion_exempt_test.html")
|
|
521
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
522
|
-
f.write("# Note")
|
|
523
|
-
result = _run_hook(
|
|
524
|
-
"Write",
|
|
525
|
-
{"file_path": md_path, "content": "# Note"},
|
|
526
|
-
)
|
|
527
|
-
assert result.returncode == 0
|
|
528
|
-
assert not os.path.exists(html_path)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
def test_companion_skips_skill_md_anywhere():
|
|
532
|
-
with _make_sandbox() as tmp:
|
|
533
|
-
nested_directory = os.path.join(tmp, "packages", "dev-env", "skills", "foo")
|
|
534
|
-
os.makedirs(nested_directory, exist_ok=True)
|
|
535
|
-
md_path = os.path.join(nested_directory, "SKILL.md")
|
|
536
|
-
html_path = os.path.join(nested_directory, "SKILL.html")
|
|
537
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
538
|
-
f.write("# Skill")
|
|
539
|
-
result = _run_hook(
|
|
540
|
-
"Write",
|
|
541
|
-
{"file_path": md_path, "content": "# Skill"},
|
|
542
|
-
)
|
|
543
|
-
assert result.returncode == 0
|
|
544
|
-
assert not os.path.exists(html_path)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
def test_companion_skips_agents_directory_anywhere():
|
|
548
|
-
with _make_sandbox() as tmp:
|
|
549
|
-
agents_directory = os.path.join(tmp, "packages", "dev-env", "agents")
|
|
550
|
-
os.makedirs(agents_directory, exist_ok=True)
|
|
551
|
-
md_path = os.path.join(agents_directory, "pr-description-writer.md")
|
|
552
|
-
html_path = os.path.join(
|
|
553
|
-
agents_directory, "pr-description-writer.html"
|
|
554
|
-
)
|
|
555
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
556
|
-
f.write("# Agent")
|
|
557
|
-
result = _run_hook(
|
|
558
|
-
"Write",
|
|
559
|
-
{"file_path": md_path, "content": "# Agent"},
|
|
560
|
-
)
|
|
561
|
-
assert result.returncode == 0
|
|
562
|
-
assert not os.path.exists(html_path)
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
def test_companion_skips_claude_plugin_directory():
|
|
566
|
-
with _make_sandbox() as tmp:
|
|
567
|
-
plugin_directory = os.path.join(tmp, ".claude-plugin")
|
|
568
|
-
os.makedirs(plugin_directory, exist_ok=True)
|
|
569
|
-
md_path = os.path.join(plugin_directory, "manifest.md")
|
|
570
|
-
html_path = os.path.join(plugin_directory, "manifest.html")
|
|
571
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
572
|
-
f.write("# Manifest")
|
|
573
|
-
result = _run_hook(
|
|
574
|
-
"Write",
|
|
575
|
-
{"file_path": md_path, "content": "# Manifest"},
|
|
576
|
-
)
|
|
577
|
-
assert result.returncode == 0
|
|
578
|
-
assert not os.path.exists(html_path)
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
def test_companion_still_fires_for_ordinary_docs_md_file():
|
|
582
|
-
with _make_sandbox() as tmp:
|
|
583
|
-
docs_directory = os.path.join(tmp, "docs")
|
|
584
|
-
os.makedirs(docs_directory, exist_ok=True)
|
|
585
|
-
md_path = os.path.join(docs_directory, "regular.md")
|
|
586
|
-
html_path = os.path.join(docs_directory, "regular.html")
|
|
587
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
588
|
-
f.write("# Regular")
|
|
589
|
-
result = _run_hook(
|
|
590
|
-
"Write",
|
|
591
|
-
{"file_path": md_path, "content": "# Regular"},
|
|
592
|
-
)
|
|
593
|
-
assert result.returncode == 0
|
|
594
|
-
assert os.path.exists(html_path)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def test_companion_skips_system_temp_directory():
|
|
598
|
-
temp_directory = tempfile.gettempdir()
|
|
599
|
-
md_path = os.path.join(temp_directory, "companion_temp_exempt_test.md")
|
|
600
|
-
html_path = os.path.join(temp_directory, "companion_temp_exempt_test.html")
|
|
601
|
-
try:
|
|
602
|
-
with open(md_path, "w", encoding="utf-8") as f:
|
|
603
|
-
f.write("# Scratch")
|
|
604
|
-
result = _run_hook(
|
|
605
|
-
"Write",
|
|
606
|
-
{"file_path": md_path, "content": "# Scratch"},
|
|
607
|
-
)
|
|
608
|
-
assert result.returncode == 0
|
|
609
|
-
assert not os.path.exists(html_path)
|
|
610
|
-
finally:
|
|
611
|
-
for each_path in (md_path, html_path):
|
|
612
|
-
if os.path.exists(each_path):
|
|
613
|
-
os.remove(each_path)
|