claude-dev-env 8.36.0 → 8.36.2

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.
@@ -8,6 +8,7 @@ import {
8
8
  writeFileSync,
9
9
  symlinkSync,
10
10
  readFileSync,
11
+ readlinkSync,
11
12
  readdirSync,
12
13
  existsSync,
13
14
  copyFileSync,
@@ -16,6 +17,7 @@ import {
16
17
  import { tmpdir } from 'node:os';
17
18
  import { join, dirname } from 'node:path';
18
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
20
+ import { FAULT_PHASES, INSTALL_FAULT_ENV } from './install-transaction.mjs';
19
21
 
20
22
  import {
21
23
  collectPackageSourceConflicts,
@@ -50,6 +52,7 @@ import {
50
52
  commandNamesOwnedHookScript,
51
53
  hookCommandComparisonSpelling,
52
54
  detectPython,
55
+ pythonFileAndPrefixArguments,
53
56
  retainNewestRunBackupOnly,
54
57
  } from './install.mjs';
55
58
  import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
@@ -2594,7 +2597,12 @@ test('a stale registration keeps working because the retired hook leaves an iner
2594
2597
  'a retired path no registration names gets no stand-in',
2595
2598
  );
2596
2599
 
2597
- const standInRun = spawnSync(detectPython(), [standInPath], { encoding: 'utf8' });
2600
+ const pythonCommand = pythonFileAndPrefixArguments(detectPython());
2601
+ const standInRun = spawnSync(
2602
+ pythonCommand.file,
2603
+ [...pythonCommand.prefixArguments, standInPath],
2604
+ { encoding: 'utf8' },
2605
+ );
2598
2606
 
2599
2607
  assert.equal(standInRun.status, 0, 'the stand-in exits 0, so a stale registration allows the tool');
2600
2608
  assert.equal(standInRun.stdout, '', 'the stand-in writes nothing to stdout');
@@ -3006,6 +3014,40 @@ test('pruneRetiredHookEntriesFromSettings warns and returns 0 when the host conf
3006
3014
  const PSTACK_TEST_PACKAGE_ROOT = dirname(fileURLToPath(new URL('./install.mjs', import.meta.url)));
3007
3015
  const PSTACK_TEST_INSTALLER_PATH = fileURLToPath(new URL('./install.mjs', import.meta.url));
3008
3016
 
3017
+ test('a failed update preserves an existing skills lookup pointer', () => {
3018
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-install-pointer-'));
3019
+ try {
3020
+ writeFileSync(join(homeDirectory, '.gitconfig'), '');
3021
+ const environment = {
3022
+ ...process.env,
3023
+ HOME: homeDirectory,
3024
+ USERPROFILE: homeDirectory,
3025
+ CLAUDE_CONFIG_DIR: '',
3026
+ GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
3027
+ CDE_INSTALL_PSTACK: '0',
3028
+ CDE_INSTALL_USAGE_WRAPUP: '0',
3029
+ };
3030
+ const argumentsForCoreInstall = [PSTACK_TEST_INSTALLER_PATH, '--only', 'core'];
3031
+ const firstRun = spawnSync(process.execPath, argumentsForCoreInstall, {
3032
+ encoding: 'utf8',
3033
+ env: environment,
3034
+ });
3035
+ assert.equal(firstRun.status, 0, firstRun.stdout + firstRun.stderr);
3036
+ const pointerPath = join(homeDirectory, '.claude', 'skills');
3037
+ const targetPath = readlinkSync(pointerPath);
3038
+
3039
+ const failedRun = spawnSync(process.execPath, argumentsForCoreInstall, {
3040
+ encoding: 'utf8',
3041
+ env: { ...environment, [INSTALL_FAULT_ENV]: FAULT_PHASES.AFTER_FILE_STAGING },
3042
+ });
3043
+ assert.notEqual(failedRun.status, 0, failedRun.stdout + failedRun.stderr);
3044
+ assert.equal(lstatSync(pointerPath).isSymbolicLink(), true);
3045
+ assert.equal(readlinkSync(pointerPath), targetPath);
3046
+ } finally {
3047
+ rmSync(homeDirectory, { recursive: true, force: true });
3048
+ }
3049
+ });
3050
+
3009
3051
  function writeRecordingHostCommand(directory, name, logPath) {
3010
3052
  mkdirSync(directory, { recursive: true });
3011
3053
  if (process.platform === 'win32') {
@@ -0,0 +1,14 @@
1
+ from hooks_constants import piped_pytest_blocker_constants as constants
2
+
3
+
4
+ def test_pytest_programs_and_interpreters_cover_supported_invocations() -> None:
5
+ assert {"pytest", "pytest.exe", "py.test"} <= constants.ALL_PYTEST_PROGRAM_BASENAMES
6
+ assert constants.PYTHON_INTERPRETER_BASENAME_PATTERN.fullmatch("python3.12.exe")
7
+ assert (constants.MODULE_RUN_FLAG, constants.PYTEST_MODULE_NAME) == ("-m", "pytest")
8
+
9
+
10
+ def test_pipe_and_wrapper_tokens_preserve_command_boundaries() -> None:
11
+ assert {"|", "|&"} <= constants.ALL_PIPE_OPERATOR_TOKENS
12
+ assert ";;" in constants.ALL_SEGMENT_RESET_OPERATOR_TOKENS
13
+ assert "-u" in constants.ALL_WRAPPER_OPTION_GRAMMARS_BY_NAME["sudo"].all_value_taking_options
14
+ assert constants.ALL_WRAPPER_OPTION_GRAMMARS_BY_NAME["timeout"].leading_operand_count == 1
@@ -0,0 +1,18 @@
1
+ from hooks_constants import verify_before_acting_constants as constants
2
+
3
+
4
+ def test_mutating_tool_and_mcp_names_are_classified() -> None:
5
+ assert {"Write", "Edit"} <= constants.ALL_ALWAYS_MUTATING_TOOL_NAMES
6
+ assert {"Bash", "PowerShell"} == constants.ALL_SHELL_TOOL_NAMES
7
+ assert constants.MCP_ACTION_WORD_SPLIT_PATTERN.split("create_pull_request") == [
8
+ "create",
9
+ "pull",
10
+ "request",
11
+ ]
12
+ assert "create" in constants.ALL_MCP_MUTATING_VERBS
13
+
14
+
15
+ def test_hedge_pattern_matches_a_hedged_claim_only() -> None:
16
+ assert constants.HEDGE_PATTERN.search("Perhaps the command writes a file.")
17
+ assert constants.HEDGE_PATTERN.search("I might be mistaken.")
18
+ assert constants.HEDGE_PATTERN.search("I checked the command output.") is None
@@ -15,6 +15,8 @@ the cache directory redirected to a temporary directory.
15
15
  """
16
16
 
17
17
  import importlib.util
18
+ import io
19
+ import json
18
20
  import os
19
21
  import sys
20
22
  from pathlib import Path
@@ -53,6 +55,42 @@ def _load_validator() -> ModuleType:
53
55
  return module
54
56
 
55
57
 
58
+ def test_type_error_block_uses_hook_reply_without_notification(
59
+ tmp_path_factory: pytest.TempPathFactory,
60
+ monkeypatch: pytest.MonkeyPatch,
61
+ capsys: pytest.CaptureFixture[str],
62
+ ) -> None:
63
+ validator = _load_validator()
64
+ project_root = tmp_path_factory.mktemp("blocked_project")
65
+ target_file = project_root / "typed_module.py"
66
+ target_file.write_text(TYPE_ERROR_MODULE_SOURCE, encoding="utf-8")
67
+ all_touched_names: list[str] = []
68
+
69
+ class RecordingNotificationModule(ModuleType):
70
+ def __getattr__(self, attribute_name: str) -> object:
71
+ if not attribute_name.startswith("__"):
72
+ all_touched_names.append(attribute_name)
73
+ return lambda *_arguments, **_options: False
74
+
75
+ monkeypatch.setitem(sys.modules, "notification_utils", RecordingNotificationModule("notification_utils"))
76
+ monkeypatch.setattr(validator, "discover_project_root", lambda _target: project_root)
77
+ monkeypatch.setattr(
78
+ validator,
79
+ "run_mypy",
80
+ lambda _target, _root: (1, f"{target_file}:1: error: Incompatible types\n"),
81
+ )
82
+ monkeypatch.setattr(validator, "log_hook_block", lambda **_details: None)
83
+ monkeypatch.setattr(
84
+ sys, "stdin", io.StringIO(json.dumps({"tool_input": {"file_path": str(target_file)}}))
85
+ )
86
+
87
+ with pytest.raises(SystemExit):
88
+ validator.main()
89
+
90
+ assert json.loads(capsys.readouterr().out)["decision"] == "block"
91
+ assert all_touched_names == []
92
+
93
+
56
94
  @pytest.fixture(autouse=True)
57
95
  def isolate_cache_directory(
58
96
  tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch
@@ -182,6 +182,33 @@ def _load_auto_formatter_module() -> ModuleType:
182
182
  return auto_formatter_module
183
183
 
184
184
 
185
+ def test_successful_format_does_not_access_notifications(
186
+ monkeypatch: pytest.MonkeyPatch,
187
+ ) -> None:
188
+ formatter = _load_auto_formatter_module()
189
+ all_touched_names: list[str] = []
190
+
191
+ class RecordingNotificationModule(ModuleType):
192
+ def __getattr__(self, attribute_name: str) -> object:
193
+ if not attribute_name.startswith("__"):
194
+ all_touched_names.append(attribute_name)
195
+ return lambda *_arguments, **_options: False
196
+
197
+ monkeypatch.setitem(sys.modules, "notification_utils", RecordingNotificationModule("notification_utils"))
198
+
199
+ def report_formatter_success(
200
+ command: list[str], _file_path: str, _timeout_seconds: int
201
+ ) -> tuple[subprocess.CompletedProcess[str], bool]:
202
+ return subprocess.CompletedProcess(command, 0, "", ""), False
203
+
204
+ monkeypatch.setattr(formatter, "_run_command", report_formatter_success)
205
+
206
+ formatter.run_eligible_formatter("formatted_module.py")
207
+ formatter._run_prettier("formatted_module.js")
208
+
209
+ assert all_touched_names == []
210
+
211
+
185
212
  def test_formatter_eligibility_requires_write_tool_and_untracked_source(
186
213
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
187
214
  ) -> None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.36.0",
3
+ "version": "8.36.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {