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.
- package/bin/install.test.mjs +43 -1
- package/hooks/hooks_constants/test_piped_pytest_blocker_constants.py +14 -0
- package/hooks/hooks_constants/test_verify_before_acting_constants.py +18 -0
- package/hooks/validation/test_mypy_validator.py +38 -0
- package/hooks/workflow/test_auto_formatter.py +27 -0
- package/package.json +1 -1
package/bin/install.test.mjs
CHANGED
|
@@ -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
|
|
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:
|