大改版,可以针对不同项目引擎进行初始化
This commit is contained in:
726
.cursor/bootstrap/tests/test_cursor_init.py
Normal file
726
.cursor/bootstrap/tests/test_cursor_init.py
Normal file
@@ -0,0 +1,726 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "tools" / "cursor_init.py"
|
||||
PROJECT_ROOT = SCRIPT_PATH.parents[3]
|
||||
sys.dont_write_bytecode = True
|
||||
SPEC = importlib.util.spec_from_file_location("cursor_init_under_test", SCRIPT_PATH)
|
||||
if SPEC is None or SPEC.loader is None:
|
||||
raise RuntimeError(f"无法加载 {SCRIPT_PATH}")
|
||||
CURSOR_INIT = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = CURSOR_INIT
|
||||
SPEC.loader.exec_module(CURSOR_INIT)
|
||||
|
||||
|
||||
class CursorInitTests(unittest.TestCase):
|
||||
def _write_text(self, root: Path, relative: str, content: str) -> None:
|
||||
path = root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8", newline="\n")
|
||||
|
||||
def _write_json(self, root: Path, relative: str, data: object) -> None:
|
||||
self._write_text(
|
||||
root,
|
||||
relative,
|
||||
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
|
||||
)
|
||||
|
||||
def _fixture(self) -> tuple[tempfile.TemporaryDirectory[str], Path]:
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
root = Path(temporary.name)
|
||||
bootstrap = ".cursor/bootstrap"
|
||||
|
||||
self._write_json(
|
||||
root,
|
||||
f"{bootstrap}/catalog.json",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"skill_version": 5,
|
||||
"common_manifest": "common.manifest.json",
|
||||
"skills_catalog": "skills.json",
|
||||
"types": [
|
||||
{
|
||||
"id": "general",
|
||||
"label": "通用",
|
||||
"status": "ready",
|
||||
"manifest": "types/general/manifest.json",
|
||||
},
|
||||
{
|
||||
"id": "godot",
|
||||
"label": "Godot",
|
||||
"status": "ready",
|
||||
"manifest": "types/godot/manifest.json",
|
||||
},
|
||||
{
|
||||
"id": "roblox",
|
||||
"label": "Roblox",
|
||||
"status": "ready",
|
||||
"manifest": "types/roblox/manifest.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
self._write_json(
|
||||
root,
|
||||
f"{bootstrap}/common.manifest.json",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "common",
|
||||
"version": 1,
|
||||
"canonical_files": [
|
||||
".cursor/rules/common/base.mdc",
|
||||
".cursor/rules/project/.gitkeep",
|
||||
".cursor/skills/epee-orchestrator/SKILL.md",
|
||||
".cursor/pitfalls/pitfalls.md",
|
||||
],
|
||||
"skills": ["epee-orchestrator"],
|
||||
"local_env_fields": [
|
||||
{
|
||||
"key": "shell",
|
||||
"example": "powershell",
|
||||
"default": "powershell",
|
||||
"required": True,
|
||||
"prompt": "shell",
|
||||
},
|
||||
{
|
||||
"key": "changelog-author",
|
||||
"example": "<handle>",
|
||||
"default": "",
|
||||
"required": True,
|
||||
"prompt": "author",
|
||||
},
|
||||
],
|
||||
"scan": {
|
||||
"included_names": [],
|
||||
"included_extensions": [".json"],
|
||||
"excluded_directories": [".git", ".cursor"],
|
||||
},
|
||||
"gitignore_fragments": ["fragments/common.gitignore"],
|
||||
"gitattributes_fragments": ["fragments/common.gitattributes"],
|
||||
"reset_files": [
|
||||
{
|
||||
"source": "templates/pitfalls.md",
|
||||
"target": ".cursor/pitfalls/pitfalls.md",
|
||||
}
|
||||
],
|
||||
"cleanup_globs": [".cursor/changelog/entries/**/*.md"],
|
||||
},
|
||||
)
|
||||
self._write_json(
|
||||
root,
|
||||
f"{bootstrap}/skills.json",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"skills": [
|
||||
{
|
||||
"id": "epee-orchestrator",
|
||||
"scope": "common",
|
||||
"type": "基础设施",
|
||||
"target": ".cursor/skills/epee-orchestrator",
|
||||
"capability": "测试 common Skill",
|
||||
"triggers": "测试",
|
||||
"output": "测试输出",
|
||||
},
|
||||
{
|
||||
"id": "godot-helper",
|
||||
"scope": "godot",
|
||||
"type": "项目级",
|
||||
"target": ".cursor/skills/godot-helper",
|
||||
"capability": "测试 Godot Skill",
|
||||
"triggers": "Godot 测试",
|
||||
"output": "Godot 测试输出",
|
||||
},
|
||||
{
|
||||
"id": "roblox-helper",
|
||||
"scope": "roblox",
|
||||
"type": "项目级",
|
||||
"target": ".cursor/skills/roblox-helper",
|
||||
"capability": "测试 Roblox Skill",
|
||||
"triggers": "Roblox 测试",
|
||||
"output": "测试输出",
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
self._write_json(
|
||||
root,
|
||||
f"{bootstrap}/types/general/manifest.json",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "general",
|
||||
"version": 1,
|
||||
"readiness": {"allow_empty_payload": True},
|
||||
"files": [],
|
||||
"trees": [],
|
||||
"skills": [],
|
||||
"local_env_fields": [],
|
||||
"scan": {
|
||||
"included_names": [],
|
||||
"included_extensions": [],
|
||||
"excluded_directories": [],
|
||||
},
|
||||
"gitignore_fragments": [],
|
||||
"gitattributes_fragments": [],
|
||||
},
|
||||
)
|
||||
self._write_json(
|
||||
root,
|
||||
f"{bootstrap}/types/godot/manifest.json",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "godot",
|
||||
"version": 1,
|
||||
"readiness": {
|
||||
"allow_empty_payload": False,
|
||||
"require_scan": True,
|
||||
"require_gitignore": True,
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"source": "payload/rules/godot.mdc",
|
||||
"target": ".cursor/rules/project/godot.mdc",
|
||||
}
|
||||
],
|
||||
"trees": [
|
||||
{
|
||||
"source": "payload/skills/godot-helper",
|
||||
"target": ".cursor/skills/godot-helper",
|
||||
}
|
||||
],
|
||||
"skills": ["godot-helper"],
|
||||
"local_env_fields": [
|
||||
{
|
||||
"key": "godot4_path",
|
||||
"example": "<godot>",
|
||||
"default": "",
|
||||
"required": False,
|
||||
"prompt": "Godot",
|
||||
}
|
||||
],
|
||||
"scan": {
|
||||
"included_names": ["project.godot"],
|
||||
"included_extensions": [".gd"],
|
||||
"excluded_directories": [".godot"],
|
||||
},
|
||||
"gitignore_fragments": ["fragments/godot.gitignore"],
|
||||
"gitattributes_fragments": ["fragments/godot.gitattributes"],
|
||||
},
|
||||
)
|
||||
self._write_json(
|
||||
root,
|
||||
f"{bootstrap}/types/roblox/manifest.json",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "roblox",
|
||||
"version": 1,
|
||||
"readiness": {
|
||||
"allow_empty_payload": False,
|
||||
"require_scan": True,
|
||||
"require_gitignore": True,
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"source": "payload/rules/roblox.mdc",
|
||||
"target": ".cursor/rules/project/roblox.mdc",
|
||||
}
|
||||
],
|
||||
"trees": [
|
||||
{
|
||||
"source": "payload/skills/roblox-helper",
|
||||
"target": ".cursor/skills/roblox-helper",
|
||||
}
|
||||
],
|
||||
"skills": ["roblox-helper"],
|
||||
"local_env_fields": [
|
||||
{
|
||||
"key": "roblox_open_cloud_api_key",
|
||||
"example": "<roblox-api-key>",
|
||||
"default": "",
|
||||
"required": False,
|
||||
"sensitive": True,
|
||||
"collect_at_init": False,
|
||||
"prompt": "Roblox API Key",
|
||||
}
|
||||
],
|
||||
"scan": {
|
||||
"included_names": ["default.project.json"],
|
||||
"included_extensions": [".luau"],
|
||||
"excluded_directories": ["Packages"],
|
||||
},
|
||||
"gitignore_fragments": ["fragments/roblox.gitignore"],
|
||||
"gitattributes_fragments": ["fragments/roblox.gitattributes"],
|
||||
},
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/fragments/common.gitignore",
|
||||
".cursor/local-env.json\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/fragments/common.gitattributes",
|
||||
"*.json text eol=lf\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/templates/pitfalls.md",
|
||||
"# Pitfall Journal\n\n---\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/types/godot/fragments/godot.gitignore",
|
||||
".godot/\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/types/godot/fragments/godot.gitattributes",
|
||||
"*.gd text eol=lf\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/types/godot/payload/rules/godot.mdc",
|
||||
"# Godot\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/types/godot/payload/skills/godot-helper/SKILL.md",
|
||||
"---\nname: godot-helper\ndescription: test\n---\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/types/roblox/fragments/roblox.gitignore",
|
||||
"Packages/\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/types/roblox/fragments/roblox.gitattributes",
|
||||
"*.luau text eol=lf\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/types/roblox/payload/rules/roblox.mdc",
|
||||
"# Roblox\n",
|
||||
)
|
||||
self._write_text(
|
||||
root,
|
||||
f"{bootstrap}/types/roblox/payload/skills/roblox-helper/SKILL.md",
|
||||
"---\nname: roblox-helper\ndescription: test\n---\n",
|
||||
)
|
||||
self._write_text(root, ".cursor/rules/common/base.mdc", "# Base\n")
|
||||
self._write_text(root, ".cursor/rules/project/.gitkeep", "")
|
||||
self._write_text(
|
||||
root,
|
||||
".cursor/skills/epee-orchestrator/SKILL.md",
|
||||
"---\nname: epee-orchestrator\ndescription: test\n---\n",
|
||||
)
|
||||
self._write_text(root, ".cursor/pitfalls/pitfalls.md", "# Old pitfalls\n")
|
||||
self._write_text(
|
||||
root,
|
||||
".cursor/changelog/entries/tester/old.md",
|
||||
"# Old changelog\n",
|
||||
)
|
||||
return temporary, root
|
||||
|
||||
def _plan(self, root: Path, project_type: str):
|
||||
context = CURSOR_INIT.load_context(root)
|
||||
plan = CURSOR_INIT.build_plan(
|
||||
context,
|
||||
project_type,
|
||||
values={"shell": "powershell", "changelog-author": "tester"},
|
||||
)
|
||||
return context, plan
|
||||
|
||||
def _apply(self, root: Path, project_type: str):
|
||||
context, plan = self._plan(root, project_type)
|
||||
self.assertEqual(plan.blockers, [])
|
||||
CURSOR_INIT.apply_plan(context, plan, plan.digest)
|
||||
return context, plan
|
||||
|
||||
def test_list_types_includes_ready_roblox(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
context = CURSOR_INIT.load_context(root)
|
||||
|
||||
self.assertEqual(
|
||||
CURSOR_INIT.list_ready_types(context),
|
||||
[
|
||||
{"id": "general", "label": "通用"},
|
||||
{"id": "godot", "label": "Godot"},
|
||||
{"id": "roblox", "label": "Roblox"},
|
||||
],
|
||||
)
|
||||
|
||||
def test_empty_roblox_bundle_cannot_be_marked_ready(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
manifest_path = root / ".cursor/bootstrap/types/roblox/manifest.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
manifest["files"] = []
|
||||
manifest["trees"] = []
|
||||
manifest["skills"] = []
|
||||
manifest["scan"] = {
|
||||
"included_names": [],
|
||||
"included_extensions": [],
|
||||
"excluded_directories": [],
|
||||
}
|
||||
manifest["gitignore_fragments"] = []
|
||||
manifest["gitattributes_fragments"] = []
|
||||
manifest_path.write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
with self.assertRaises(CURSOR_INIT.InitError):
|
||||
CURSOR_INIT.load_context(root)
|
||||
|
||||
def test_general_apply_is_idempotent(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
context, _ = self._apply(root, "general")
|
||||
|
||||
second_plan = CURSOR_INIT.build_plan(
|
||||
context,
|
||||
"general",
|
||||
values={"shell": "powershell", "changelog-author": "tester"},
|
||||
)
|
||||
|
||||
self.assertEqual(second_plan.blockers, [])
|
||||
self.assertEqual(second_plan.actions, [])
|
||||
self.assertEqual(CURSOR_INIT.check_project(context), [])
|
||||
|
||||
def test_godot_to_general_removes_type_files(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self._apply(root, "godot")
|
||||
self.assertTrue((root / ".cursor/rules/project/godot.mdc").is_file())
|
||||
self.assertTrue((root / ".cursor/skills/godot-helper/SKILL.md").is_file())
|
||||
|
||||
self._apply(root, "general")
|
||||
|
||||
self.assertFalse((root / ".cursor/rules/project/godot.mdc").exists())
|
||||
self.assertFalse((root / ".cursor/skills/godot-helper/SKILL.md").exists())
|
||||
scan = json.loads(
|
||||
(root / ".cursor/hooks/changelog-scan.json").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(scan["project_type"], "general")
|
||||
|
||||
def test_roblox_materializes_only_for_roblox_and_switch_removes_it(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
|
||||
self._apply(root, "roblox")
|
||||
|
||||
self.assertTrue((root / ".cursor/rules/project/roblox.mdc").is_file())
|
||||
self.assertTrue(
|
||||
(root / ".cursor/skills/roblox-helper/SKILL.md").is_file()
|
||||
)
|
||||
registry = (
|
||||
root / ".cursor/skills/epee-orchestrator/registry.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("roblox-helper", registry)
|
||||
example = json.loads(
|
||||
(root / ".cursor/local-env.example.json").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(
|
||||
example["roblox_open_cloud_api_key"],
|
||||
"<roblox-api-key>",
|
||||
)
|
||||
local_env = json.loads(
|
||||
(root / ".cursor/local-env.json").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(local_env["roblox_open_cloud_api_key"], "")
|
||||
|
||||
self._apply(root, "general")
|
||||
|
||||
self.assertFalse((root / ".cursor/rules/project/roblox.mdc").exists())
|
||||
self.assertFalse((root / ".cursor/skills/roblox-helper/SKILL.md").exists())
|
||||
general_example = json.loads(
|
||||
(root / ".cursor/local-env.example.json").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertNotIn("roblox_open_cloud_api_key", general_example)
|
||||
|
||||
def test_godot_and_roblox_cross_switches_remove_old_bundle(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
|
||||
self._apply(root, "godot")
|
||||
self._apply(root, "roblox")
|
||||
self.assertFalse((root / ".cursor/rules/project/godot.mdc").exists())
|
||||
self.assertFalse((root / ".cursor/skills/godot-helper/SKILL.md").exists())
|
||||
self.assertTrue((root / ".cursor/rules/project/roblox.mdc").is_file())
|
||||
|
||||
self._apply(root, "godot")
|
||||
self.assertFalse((root / ".cursor/rules/project/roblox.mdc").exists())
|
||||
self.assertFalse((root / ".cursor/skills/roblox-helper/SKILL.md").exists())
|
||||
self.assertTrue((root / ".cursor/rules/project/godot.mdc").is_file())
|
||||
|
||||
def test_sensitive_value_cannot_be_passed_with_set(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
context = CURSOR_INIT.load_context(root)
|
||||
fields = CURSOR_INIT._all_env_fields(
|
||||
context,
|
||||
context.types["roblox"],
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(CURSOR_INIT.InitError, "敏感字段"):
|
||||
CURSOR_INIT._validate_input_values(
|
||||
fields,
|
||||
{"roblox_open_cloud_api_key": "do-not-accept"},
|
||||
)
|
||||
|
||||
def test_sensitive_local_value_is_preserved_and_not_drift(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self._write_json(
|
||||
root,
|
||||
".cursor/local-env.json",
|
||||
{
|
||||
"shell": "powershell",
|
||||
"changelog-author": "tester",
|
||||
"roblox_open_cloud_api_key": "local-secret",
|
||||
},
|
||||
)
|
||||
|
||||
self._apply(root, "roblox")
|
||||
local_env_path = root / ".cursor/local-env.json"
|
||||
local_env = json.loads(local_env_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(local_env["roblox_open_cloud_api_key"], "local-secret")
|
||||
local_env["roblox_open_cloud_api_key"] = "rotated-secret"
|
||||
self._write_json(root, ".cursor/local-env.json", local_env)
|
||||
|
||||
context = CURSOR_INIT.load_context(root)
|
||||
self.assertEqual(CURSOR_INIT.check_project(context, "roblox"), [])
|
||||
|
||||
def test_sensitive_value_rotation_does_not_change_plan_digest(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self._write_json(
|
||||
root,
|
||||
".cursor/local-env.json",
|
||||
{"roblox_open_cloud_api_key": "first-secret"},
|
||||
)
|
||||
context, first_plan = self._plan(root, "roblox")
|
||||
|
||||
self._write_json(
|
||||
root,
|
||||
".cursor/local-env.json",
|
||||
{"roblox_open_cloud_api_key": "rotated-secret"},
|
||||
)
|
||||
context, rotated_plan = self._plan(root, "roblox")
|
||||
|
||||
self.assertEqual(first_plan.digest, rotated_plan.digest)
|
||||
serialized = json.dumps(CURSOR_INIT.plan_as_dict(rotated_plan))
|
||||
self.assertNotIn("first-secret", serialized)
|
||||
self.assertNotIn("rotated-secret", serialized)
|
||||
self.assertIn("<local-sensitive-content>", serialized)
|
||||
|
||||
def test_plan_digest_mismatch_is_rejected(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
context, plan = self._plan(root, "general")
|
||||
|
||||
with self.assertRaises(CURSOR_INIT.InitError):
|
||||
CURSOR_INIT.apply_plan(context, plan, "incorrect")
|
||||
|
||||
self.assertFalse((root / ".cursor/.init-done").exists())
|
||||
|
||||
def test_foreign_rule_blocks_apply(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self._write_text(root, ".cursor/rules/project/foreign.mdc", "# Foreign\n")
|
||||
context, plan = self._plan(root, "general")
|
||||
|
||||
self.assertTrue(plan.foreign)
|
||||
with self.assertRaises(CURSOR_INIT.InitError):
|
||||
CURSOR_INIT.apply_plan(context, plan, plan.digest)
|
||||
self.assertFalse((root / ".cursor/.init-done").exists())
|
||||
|
||||
def test_modified_managed_file_blocks_type_switch(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self._apply(root, "godot")
|
||||
self._write_text(
|
||||
root,
|
||||
".cursor/rules/project/godot.mdc",
|
||||
"# User modified\n",
|
||||
)
|
||||
|
||||
_, plan = self._plan(root, "general")
|
||||
|
||||
self.assertTrue(
|
||||
any("拒绝删除" in blocker for blocker in plan.blockers),
|
||||
plan.blockers,
|
||||
)
|
||||
|
||||
def test_apply_resets_state_and_removes_history(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
|
||||
self._apply(root, "general")
|
||||
|
||||
self.assertEqual(
|
||||
(root / ".cursor/pitfalls/pitfalls.md").read_text(encoding="utf-8"),
|
||||
"# Pitfall Journal\n\n---\n",
|
||||
)
|
||||
self.assertFalse(
|
||||
(root / ".cursor/changelog/entries/tester/old.md").exists()
|
||||
)
|
||||
|
||||
def test_missing_common_canonical_blocks_apply(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
(root / ".cursor/rules/common/base.mdc").unlink()
|
||||
context, plan = self._plan(root, "general")
|
||||
|
||||
self.assertTrue(
|
||||
any("缺少 common canonical" in blocker for blocker in plan.blockers),
|
||||
plan.blockers,
|
||||
)
|
||||
with self.assertRaises(CURSOR_INIT.InitError):
|
||||
CURSOR_INIT.apply_plan(context, plan, plan.digest)
|
||||
self.assertFalse((root / ".cursor/.init-done").exists())
|
||||
|
||||
def test_skip_git_files_preserves_user_files(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self._write_text(root, ".gitignore", "# user\n")
|
||||
context = CURSOR_INIT.load_context(root)
|
||||
plan = CURSOR_INIT.build_plan(
|
||||
context,
|
||||
"general",
|
||||
values={"shell": "powershell", "changelog-author": "tester"},
|
||||
skip_git_files=True,
|
||||
)
|
||||
|
||||
self.assertEqual(plan.blockers, [])
|
||||
CURSOR_INIT.apply_plan(context, plan, plan.digest)
|
||||
|
||||
self.assertEqual((root / ".gitignore").read_text(encoding="utf-8"), "# user\n")
|
||||
sentinel = (root / ".cursor/.init-done").read_text(encoding="utf-8")
|
||||
self.assertIn("gitignore_generated: false", sentinel)
|
||||
|
||||
def test_generated_text_is_utf8_without_bom_and_uses_lf(self) -> None:
|
||||
temporary, root = self._fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self._apply(root, "godot")
|
||||
|
||||
generated = [
|
||||
".cursor/.init-done",
|
||||
".cursor/.init-state.json",
|
||||
".cursor/hooks/changelog-scan.json",
|
||||
".cursor/local-env.example.json",
|
||||
".cursor/local-env.json",
|
||||
".cursor/skills/epee-orchestrator/registry.md",
|
||||
".gitignore",
|
||||
".gitattributes",
|
||||
]
|
||||
for relative in generated:
|
||||
with self.subTest(path=relative):
|
||||
content = (root / relative).read_bytes()
|
||||
self.assertFalse(content.startswith(b"\xef\xbb\xbf"))
|
||||
self.assertNotIn(b"\r\n", content)
|
||||
content.decode("utf-8")
|
||||
|
||||
def test_repository_roblox_bundle_is_ready_clean_and_complete(self) -> None:
|
||||
context = CURSOR_INIT.load_context(PROJECT_ROOT)
|
||||
roblox = context.types["roblox"]
|
||||
self.assertEqual(roblox.status, "ready")
|
||||
self.assertEqual(len(roblox.manifest["skills"]), 10)
|
||||
|
||||
specs = CURSOR_INIT._expand_manifest_files(roblox)
|
||||
targets = {spec.target for spec in specs}
|
||||
self.assertIn(
|
||||
".cursor/rules/project/roblox-luau-guardrails.mdc",
|
||||
targets,
|
||||
)
|
||||
for skill_id in roblox.manifest["skills"]:
|
||||
with self.subTest(skill=skill_id):
|
||||
self.assertIn(
|
||||
f".cursor/skills/{skill_id}/SKILL.md",
|
||||
targets,
|
||||
)
|
||||
|
||||
payload_root = (
|
||||
PROJECT_ROOT / ".cursor/bootstrap/types/roblox/payload"
|
||||
)
|
||||
forbidden = ("Codex", ".codex", "xi.chen", "SwordnWizard")
|
||||
for path in payload_root.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
with self.subTest(path=path.relative_to(PROJECT_ROOT)):
|
||||
self.assertNotEqual(path.name, "openai.yaml")
|
||||
content = path.read_bytes()
|
||||
self.assertFalse(content.startswith(b"\xef\xbb\xbf"))
|
||||
self.assertNotIn(b"\r\n", content)
|
||||
text = content.decode("utf-8")
|
||||
for marker in forbidden:
|
||||
self.assertNotIn(marker, text)
|
||||
|
||||
def test_repository_payload_applies_and_cross_switches_in_temp_target(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
shutil.copytree(PROJECT_ROOT / ".cursor", root / ".cursor")
|
||||
local_env_path = root / ".cursor/local-env.json"
|
||||
if local_env_path.exists():
|
||||
local_env_path.unlink()
|
||||
for name in (".gitignore", ".gitattributes"):
|
||||
source = PROJECT_ROOT / name
|
||||
if source.is_file():
|
||||
shutil.copy2(source, root / name)
|
||||
|
||||
for project_type in ("roblox", "general", "godot", "roblox"):
|
||||
context = CURSOR_INIT.load_context(root)
|
||||
plan = CURSOR_INIT.build_plan(
|
||||
context,
|
||||
project_type,
|
||||
values={
|
||||
"shell": "powershell",
|
||||
"changelog-author": "integration-test",
|
||||
},
|
||||
)
|
||||
self.assertEqual(plan.blockers, [], project_type)
|
||||
CURSOR_INIT.apply_plan(context, plan, plan.digest)
|
||||
checked_context = CURSOR_INIT.load_context(root)
|
||||
self.assertEqual(
|
||||
CURSOR_INIT.check_project(checked_context, project_type),
|
||||
[],
|
||||
project_type,
|
||||
)
|
||||
|
||||
roblox_skill = (
|
||||
root
|
||||
/ ".cursor/skills/roblox-project-initializer/SKILL.md"
|
||||
)
|
||||
if project_type == "roblox":
|
||||
self.assertTrue(roblox_skill.is_file())
|
||||
example = json.loads(
|
||||
(
|
||||
root / ".cursor/local-env.example.json"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(
|
||||
example["roblox_open_cloud_api_key"],
|
||||
"<your-open-cloud-api-key>",
|
||||
)
|
||||
else:
|
||||
self.assertFalse(roblox_skill.exists())
|
||||
example = json.loads(
|
||||
(
|
||||
root / ".cursor/local-env.example.json"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertNotIn("roblox_open_cloud_api_key", example)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user