大改版,可以针对不同项目引擎进行初始化
This commit is contained in:
297
.cursor/hooks/tests/test_check_changelog.py
Normal file
297
.cursor/hooks/tests/test_check_changelog.py
Normal file
@@ -0,0 +1,297 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
|
||||
HOOKS_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class CheckChangelogHookTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.implementations: list[tuple[str, list[str], str]] = []
|
||||
|
||||
powershell = shutil.which("powershell") or shutil.which("pwsh")
|
||||
if powershell:
|
||||
cls.implementations.append(
|
||||
(
|
||||
"powershell",
|
||||
[
|
||||
powershell,
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
],
|
||||
"check-changelog.ps1",
|
||||
)
|
||||
)
|
||||
|
||||
bash_candidates: list[Path] = []
|
||||
bash_from_path = shutil.which("bash")
|
||||
if bash_from_path:
|
||||
bash_candidates.append(Path(bash_from_path))
|
||||
git_from_path = shutil.which("git")
|
||||
if git_from_path:
|
||||
bash_candidates.append(Path(git_from_path).resolve().parent.parent / "bin" / "bash.exe")
|
||||
|
||||
bash: Path | None = None
|
||||
for candidate in dict.fromkeys(bash_candidates):
|
||||
if not candidate.is_file():
|
||||
continue
|
||||
try:
|
||||
bash_probe = subprocess.run(
|
||||
[str(candidate), "--version"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
timeout=10,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
continue
|
||||
if bash_probe.returncode == 0:
|
||||
bash = candidate
|
||||
break
|
||||
if bash:
|
||||
cls.implementations.append(("bash", [str(bash)], "check-changelog.sh"))
|
||||
|
||||
if not cls.implementations:
|
||||
raise unittest.SkipTest("PowerShell 和 Bash 均不可用")
|
||||
|
||||
def _create_fixture(self, script_name: str) -> tuple[tempfile.TemporaryDirectory[str], Path]:
|
||||
temporary_directory = tempfile.TemporaryDirectory()
|
||||
root = Path(temporary_directory.name)
|
||||
hooks_dir = root / ".cursor" / "hooks"
|
||||
changelog_dir = root / ".cursor" / "changelog"
|
||||
hooks_dir.mkdir(parents=True)
|
||||
changelog_dir.mkdir(parents=True)
|
||||
|
||||
shutil.copy2(HOOKS_DIR / script_name, hooks_dir / script_name)
|
||||
(hooks_dir / "changelog-scan.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"project_type": "test",
|
||||
"display_name": "Test",
|
||||
"included_names": [],
|
||||
"included_extensions": [".json"],
|
||||
"excluded_directories": [".git", ".cursor"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
(changelog_dir / "changelog-headlines.md").write_text(
|
||||
"# Dev Changelog — Headlines\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
(root / ".gitignore").write_text(
|
||||
".cursor/changelog/.session-start\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
(root / "config.json").write_text('{"value": 1}\n', encoding="utf-8", newline="\n")
|
||||
(root / "player.gd").write_text("extends Node\n", encoding="utf-8", newline="\n")
|
||||
(root / "player.luau").write_text(
|
||||
"--!strict\nreturn {}\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
self._git(root, "init", "-q")
|
||||
self._git(root, "config", "user.email", "hook-test@example.invalid")
|
||||
self._git(root, "config", "user.name", "Hook Test")
|
||||
self._git(root, "add", ".")
|
||||
self._git(root, "commit", "-q", "-m", "fixture")
|
||||
return temporary_directory, root
|
||||
|
||||
def _git(self, root: Path, *arguments: str) -> None:
|
||||
subprocess.run(
|
||||
["git", "-C", str(root), *arguments],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def _run_hook(self, command_prefix: list[str], root: Path, script_name: str) -> dict[str, str]:
|
||||
completed = subprocess.run(
|
||||
[*command_prefix, str(root / ".cursor" / "hooks" / script_name)],
|
||||
input="{}",
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=30,
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
def _write_scan_config(self, root: Path, extensions: list[str]) -> None:
|
||||
path = root / ".cursor" / "hooks" / "changelog-scan.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"project_type": "test",
|
||||
"display_name": "Test",
|
||||
"included_names": [],
|
||||
"included_extensions": extensions,
|
||||
"excluded_directories": [".git", ".cursor"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
def _set_times(
|
||||
self,
|
||||
root: Path,
|
||||
*,
|
||||
source_after_session: bool,
|
||||
source_name: str = "config.json",
|
||||
) -> None:
|
||||
baseline = int(time.time()) - 20
|
||||
changelog = root / ".cursor" / "changelog" / "changelog-headlines.md"
|
||||
session_marker = root / ".cursor" / "changelog" / ".session-start"
|
||||
source = root / source_name
|
||||
|
||||
session_marker.touch()
|
||||
os.utime(changelog, (baseline, baseline))
|
||||
os.utime(session_marker, (baseline + 5, baseline + 5))
|
||||
source_time = baseline + 10 if source_after_session else baseline + 2
|
||||
os.utime(source, (source_time, source_time))
|
||||
|
||||
def test_missing_session_marker_returns_empty(self) -> None:
|
||||
for name, command_prefix, script_name in self.implementations:
|
||||
with self.subTest(implementation=name):
|
||||
temporary_directory, root = self._create_fixture(script_name)
|
||||
self.addCleanup(temporary_directory.cleanup)
|
||||
source = root / "config.json"
|
||||
future = int(time.time()) + 5
|
||||
os.utime(source, (future, future))
|
||||
|
||||
self.assertEqual(self._run_hook(command_prefix, root, script_name), {})
|
||||
|
||||
def test_clean_worktree_with_newer_mtime_returns_empty(self) -> None:
|
||||
for name, command_prefix, script_name in self.implementations:
|
||||
with self.subTest(implementation=name):
|
||||
temporary_directory, root = self._create_fixture(script_name)
|
||||
self.addCleanup(temporary_directory.cleanup)
|
||||
self._set_times(root, source_after_session=True)
|
||||
|
||||
self.assertEqual(self._run_hook(command_prefix, root, script_name), {})
|
||||
|
||||
def test_uncommitted_change_newer_than_session_returns_followup(self) -> None:
|
||||
for name, command_prefix, script_name in self.implementations:
|
||||
with self.subTest(implementation=name):
|
||||
temporary_directory, root = self._create_fixture(script_name)
|
||||
self.addCleanup(temporary_directory.cleanup)
|
||||
(root / "config.json").write_text(
|
||||
'{"value": 2}\n',
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
self._set_times(root, source_after_session=True)
|
||||
|
||||
result = self._run_hook(command_prefix, root, script_name)
|
||||
|
||||
self.assertIn("followup_message", result)
|
||||
self.assertIn("config.json", result["followup_message"])
|
||||
|
||||
def test_change_older_than_session_returns_empty(self) -> None:
|
||||
for name, command_prefix, script_name in self.implementations:
|
||||
with self.subTest(implementation=name):
|
||||
temporary_directory, root = self._create_fixture(script_name)
|
||||
self.addCleanup(temporary_directory.cleanup)
|
||||
(root / "config.json").write_text(
|
||||
'{"value": 2}\n',
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
self._set_times(root, source_after_session=False)
|
||||
|
||||
self.assertEqual(self._run_hook(command_prefix, root, script_name), {})
|
||||
|
||||
def test_empty_profile_never_returns_followup(self) -> None:
|
||||
for name, command_prefix, script_name in self.implementations:
|
||||
with self.subTest(implementation=name):
|
||||
temporary_directory, root = self._create_fixture(script_name)
|
||||
self.addCleanup(temporary_directory.cleanup)
|
||||
self._write_scan_config(root, [])
|
||||
(root / "config.json").write_text(
|
||||
'{"value": 2}\n',
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
self._set_times(root, source_after_session=True)
|
||||
|
||||
self.assertEqual(self._run_hook(command_prefix, root, script_name), {})
|
||||
|
||||
def test_godot_profile_matches_gd_but_not_unlisted_json(self) -> None:
|
||||
for name, command_prefix, script_name in self.implementations:
|
||||
with self.subTest(implementation=name):
|
||||
temporary_directory, root = self._create_fixture(script_name)
|
||||
self.addCleanup(temporary_directory.cleanup)
|
||||
self._write_scan_config(root, [".gd"])
|
||||
(root / "config.json").write_text(
|
||||
'{"value": 2}\n',
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
(root / "player.gd").write_text(
|
||||
"extends Node2D\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
self._set_times(
|
||||
root,
|
||||
source_after_session=True,
|
||||
source_name="player.gd",
|
||||
)
|
||||
|
||||
result = self._run_hook(command_prefix, root, script_name)
|
||||
|
||||
self.assertIn("followup_message", result)
|
||||
self.assertIn("player.gd", result["followup_message"])
|
||||
|
||||
def test_roblox_profile_matches_luau(self) -> None:
|
||||
for name, command_prefix, script_name in self.implementations:
|
||||
with self.subTest(implementation=name):
|
||||
temporary_directory, root = self._create_fixture(script_name)
|
||||
self.addCleanup(temporary_directory.cleanup)
|
||||
self._write_scan_config(root, [".luau"])
|
||||
(root / "player.luau").write_text(
|
||||
"--!strict\nreturn { value = 2 }\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
self._set_times(
|
||||
root,
|
||||
source_after_session=True,
|
||||
source_name="player.luau",
|
||||
)
|
||||
|
||||
result = self._run_hook(command_prefix, root, script_name)
|
||||
|
||||
self.assertIn("followup_message", result)
|
||||
self.assertIn("player.luau", result["followup_message"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user