727 lines
27 KiB
Python
727 lines
27 KiB
Python
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()
|