claude-dev-env 8.36.1 → 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') {
@@ -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.1",
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": {