claude-dev-env 1.29.1 → 1.29.3

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.
@@ -208,7 +208,7 @@ def test_extract_body_reassembles_split_quoted_equals_value() -> None:
208
208
  assert extract_body_from_command(command) == "this body has multiple words"
209
209
 
210
210
 
211
- def test_read_body_file_rejects_relative_path_traversal(tmp_path) -> None:
211
+ def test_read_body_file_rejects_relative_path_traversal(tmp_path, monkeypatch) -> None:
212
212
  import importlib.util, pathlib, sys
213
213
  _HOOK_DIR = pathlib.Path(__file__).parent
214
214
  if str(_HOOK_DIR) not in sys.path:
@@ -217,14 +217,15 @@ def test_read_body_file_rejects_relative_path_traversal(tmp_path) -> None:
217
217
  m = importlib.util.module_from_spec(spec)
218
218
  spec.loader.exec_module(m)
219
219
  import os, pytest
220
- sentinel_file = tmp_path / 'secret.txt'
220
+ sentinel_directory = tmp_path / 'sentinel'
221
+ sentinel_directory.mkdir()
222
+ working_directory = tmp_path / 'workdir'
223
+ working_directory.mkdir()
224
+ sentinel_file = sentinel_directory / 'secret.txt'
221
225
  sentinel_file.write_text('secret')
222
- try:
223
- rel_path = os.path.relpath(str(sentinel_file))
224
- except ValueError:
225
- pytest.skip('tmp_path on different drive than cwd; relpath undefined on Windows')
226
- if '..' not in rel_path:
227
- pytest.skip('file is under cwd, not a traversal case')
226
+ monkeypatch.chdir(working_directory)
227
+ rel_path = os.path.relpath(str(sentinel_file))
228
+ assert '..' in rel_path, 'chdir to a sibling of the sentinel must produce a traversal relpath'
228
229
  with pytest.raises(m.PathTraversalError):
229
230
  m._read_body_file_contents(rel_path)
230
231
 
@@ -0,0 +1,9 @@
1
+ """Exclude validator fixture files from pytest collection.
2
+
3
+ Files in this directory are inputs for a skip-detection validator (see
4
+ packages/claude-dev-env/hooks/validators/), not real tests. Each file
5
+ demonstrates a skip/xfail pattern the validator must detect. Collecting
6
+ them as real tests produces spurious skipped/xfailed counts in the suite.
7
+ """
8
+
9
+ collect_ignore_glob = ["test_*.py"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.29.1",
3
+ "version": "1.29.3",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -248,6 +248,8 @@ def should_write_fixed_file(
248
248
  def is_safe_relative_path(each_path: str) -> bool:
249
249
  if os.path.isabs(each_path):
250
250
  return False
251
+ if each_path.startswith(("/", "\\")):
252
+ return False
251
253
  normalized = os.path.normpath(each_path)
252
254
  if normalized.startswith(".." + os.sep) or normalized == "..":
253
255
  return False
@@ -3,20 +3,19 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- import os
7
- import subprocess
8
6
  import sys
9
7
  from pathlib import Path
10
8
 
9
+ import pytest
10
+
11
11
  _SCRIPTS_DIR = Path(__file__).resolve().parent.parent
12
12
  if str(_SCRIPTS_DIR) not in sys.path:
13
13
  sys.path.insert(0, str(_SCRIPTS_DIR))
14
14
 
15
15
  import sync_to_cursor as mod
16
+ from sync_to_cursor.engine import run as run_sync_to_cursor
16
17
  from sync_to_cursor.rules import _read_paths_glob
17
18
 
18
- _SYNC_SCRIPT = _SCRIPTS_DIR / "sync_to_cursor.py"
19
-
20
19
 
21
20
  def _minimal_rule_files(claude_rules: Path) -> None:
22
21
  claude_rules.mkdir(parents=True, exist_ok=True)
@@ -72,66 +71,45 @@ def test_sync_canonical_docs_skips_missing_with_no_dst(tmp_path: Path) -> None:
72
71
  assert not (cursor / "docs" / "TEST_QUALITY.md").is_file()
73
72
 
74
73
 
75
- def test_check_fails_when_doc_source_changes_without_resync(tmp_path: Path) -> None:
74
+ def test_check_fails_when_doc_source_changes_without_resync(
75
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
76
+ ) -> None:
76
77
  claude = tmp_path / ".claude"
77
- cursor = tmp_path / ".cursor"
78
78
  _minimal_rule_files(claude / "rules")
79
79
  _minimal_code_rules_and_test_quality(claude / "docs")
80
- env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
81
- subprocess.run(
82
- [sys.executable, str(_SYNC_SCRIPT), "--force"],
83
- env=env,
84
- check=True,
85
- cwd=str(_SCRIPTS_DIR),
86
- )
80
+ monkeypatch.setenv("LLM_SETTINGS_ROOT", str(tmp_path))
81
+ assert run_sync_to_cursor(["--force"]) == 0
87
82
  (claude / "docs" / "CODE_RULES.md").write_bytes(b"changed\n")
88
- subprocess_result = subprocess.run(
89
- [sys.executable, str(_SYNC_SCRIPT), "--check"],
90
- env=env,
91
- cwd=str(_SCRIPTS_DIR),
92
- )
93
- assert subprocess_result.returncode != 0
83
+ assert run_sync_to_cursor(["--check"]) != 0
94
84
 
95
85
 
96
- def test_check_passes_after_resync(tmp_path: Path) -> None:
86
+ def test_check_passes_after_resync(
87
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
88
+ ) -> None:
97
89
  claude = tmp_path / ".claude"
98
- cursor = tmp_path / ".cursor"
99
90
  _minimal_rule_files(claude / "rules")
100
91
  _minimal_code_rules_and_test_quality(claude / "docs")
101
- env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
102
- subprocess.run(
103
- [sys.executable, str(_SYNC_SCRIPT), "--force"],
104
- env=env,
105
- check=True,
106
- cwd=str(_SCRIPTS_DIR),
107
- )
108
- subprocess_result = subprocess.run(
109
- [sys.executable, str(_SYNC_SCRIPT), "--check"],
110
- env=env,
111
- cwd=str(_SCRIPTS_DIR),
112
- )
113
- assert subprocess_result.returncode == 0
92
+ monkeypatch.setenv("LLM_SETTINGS_ROOT", str(tmp_path))
93
+ assert run_sync_to_cursor(["--force"]) == 0
94
+ assert run_sync_to_cursor(["--check"]) == 0
114
95
 
115
96
 
116
- def test_manifest_includes_docs_entries(tmp_path: Path) -> None:
97
+ def test_manifest_includes_docs_entries(
98
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
99
+ ) -> None:
117
100
  claude = tmp_path / ".claude"
118
101
  cursor = tmp_path / ".cursor"
119
102
  _minimal_rule_files(claude / "rules")
120
103
  _minimal_code_rules_and_test_quality(claude / "docs")
121
- env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
122
- subprocess.run(
123
- [sys.executable, str(_SYNC_SCRIPT), "--force"],
124
- env=env,
125
- check=True,
126
- cwd=str(_SCRIPTS_DIR),
127
- )
104
+ monkeypatch.setenv("LLM_SETTINGS_ROOT", str(tmp_path))
105
+ assert run_sync_to_cursor(["--force"]) == 0
128
106
  manifest = json.loads((cursor / ".sync-manifest.json").read_text(encoding="utf-8"))
129
107
  assert "docs_entries" in manifest
130
- de = manifest["docs_entries"]
131
- assert "docs/CODE_RULES.md" in de
132
- assert "docs/TEST_QUALITY.md" in de
133
- assert "sources_hash" in de["docs/CODE_RULES.md"]
134
- assert "output_hash" in de["docs/CODE_RULES.md"]
108
+ docs_entries = manifest["docs_entries"]
109
+ assert "docs/CODE_RULES.md" in docs_entries
110
+ assert "docs/TEST_QUALITY.md" in docs_entries
111
+ assert "sources_hash" in docs_entries["docs/CODE_RULES.md"]
112
+ assert "output_hash" in docs_entries["docs/CODE_RULES.md"]
135
113
 
136
114
 
137
115
  def test_merge_code_standards_with_pointer_style_code_rules(tmp_path: Path) -> None:
@@ -166,40 +144,28 @@ def test_sync_canonical_docs_deletes_stale_copy_when_source_removed(tmp_path: Pa
166
144
  assert not (cursor / "docs" / "CODE_RULES.md").is_file()
167
145
 
168
146
 
169
- def test_dry_run_does_not_create_output_directory(tmp_path: Path) -> None:
147
+ def test_dry_run_does_not_create_output_directory(
148
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
149
+ ) -> None:
170
150
  claude = tmp_path / ".claude"
171
151
  cursor = tmp_path / ".cursor"
172
152
  _minimal_rule_files(claude / "rules")
173
153
  _minimal_code_rules_and_test_quality(claude / "docs")
174
- env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
175
- subprocess.run(
176
- [sys.executable, str(_SYNC_SCRIPT), "--dry-run", "--quiet"],
177
- env=env,
178
- check=True,
179
- cwd=str(_SCRIPTS_DIR),
180
- )
154
+ monkeypatch.setenv("LLM_SETTINGS_ROOT", str(tmp_path))
155
+ assert run_sync_to_cursor(["--dry-run", "--quiet"]) == 0
181
156
  assert not (cursor / "rules").exists(), "--dry-run must not create the output directory"
182
157
 
183
158
 
184
- def test_check_skips_optional_mapping_when_source_missing(tmp_path: Path) -> None:
159
+ def test_check_skips_optional_mapping_when_source_missing(
160
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
161
+ ) -> None:
185
162
  claude = tmp_path / ".claude"
186
- cursor = tmp_path / ".cursor"
187
163
  _minimal_rule_files(claude / "rules")
188
164
  _minimal_code_rules_and_test_quality(claude / "docs")
189
- env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
190
- subprocess.run(
191
- [sys.executable, str(_SYNC_SCRIPT), "--force"],
192
- env=env,
193
- check=True,
194
- cwd=str(_SCRIPTS_DIR),
195
- )
165
+ monkeypatch.setenv("LLM_SETTINGS_ROOT", str(tmp_path))
166
+ assert run_sync_to_cursor(["--force"]) == 0
196
167
  (claude / "rules" / "tasklings-preferences.md").unlink()
197
- subprocess_result = subprocess.run(
198
- [sys.executable, str(_SYNC_SCRIPT), "--check"],
199
- env=env,
200
- cwd=str(_SCRIPTS_DIR),
201
- )
202
- assert subprocess_result.returncode == 0, "--check must pass when only optional sources are missing"
168
+ assert run_sync_to_cursor(["--check"]) == 0, "--check must pass when only optional sources are missing"
203
169
 
204
170
 
205
171
  def test_tasklings_glob_derived_from_frontmatter(tmp_path: Path) -> None:
@@ -112,10 +112,10 @@ def test_contract_should_require_files_opened_in_proof_of_absence() -> None:
112
112
  )
113
113
 
114
114
 
115
- def test_step2_spawn_should_include_model_sonnet_parameter() -> None:
115
+ def test_step2_spawn_should_include_model_opus_parameter() -> None:
116
116
  skill_text = _load_skill_text()
117
- assert 'model="sonnet"' in skill_text, (
118
- "Step 2 Agent() spawn template must include model=\"sonnet\" for the primary subagent"
117
+ assert 'model="opus"' in skill_text, (
118
+ "Step 2 Agent() spawn template must include model=\"opus\" for the primary subagent"
119
119
  )
120
120
 
121
121