diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index b509eccd..8c739a9d 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -71,24 +71,7 @@ jobs: echo "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV" echo "✅ 仓库准备完成" - - name: 🔧 安装测试依赖 - run: | - echo "========================================" - echo "📦 安装测试依赖" - echo "========================================" - - apt-get update - apt-get install -y python3-pip - - python3 -m pip install --upgrade pip - python3 -m pip install yamllint tomli - - echo "" - echo "✅ Python 版本: $(python3 --version)" - echo "✅ 依赖安装完成" - echo "========================================" - - - name: 🧪 运行全量测试并生成报告 + - name: 🧪 运行全量测试 shell: bash run: | set -euo pipefail @@ -102,21 +85,11 @@ jobs: python3 -m unittest discover -s test -p "test_*.py" -v echo "✅ Python 测试通过" - echo "========================================" - echo "📝 模板验证测试" - echo "========================================" - - sh test/templates/validate_python_templates.sh - sh test/templates/validate_cpp_templates.sh - sh test/templates/validate_ci_templates.sh - sh test/templates/validate_project_templates.sh - echo "✅ 模板验证通过" - echo "========================================" echo "🔗 文档链接检查" echo "========================================" - sh test/integration/check_doc_links.sh + python3 test/integration/check_doc_links.py echo "✅ 文档链接检查通过" echo "🎉 所有测试完成" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 508ef161..685da7bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,11 +21,7 @@ Run the relevant checks before pushing: ```bash npm run lint:md python -m unittest discover -s test -p "test_*.py" -v -sh test/templates/validate_python_templates.sh -sh test/templates/validate_cpp_templates.sh -sh test/templates/validate_ci_templates.sh -sh test/templates/validate_project_templates.sh -sh test/integration/check_doc_links.sh +python test/integration/check_doc_links.py ``` ## Templates and docs diff --git a/test/README.md b/test/README.md index be269886..8cdee7ec 100644 --- a/test/README.md +++ b/test/README.md @@ -7,20 +7,13 @@ ```txt test/ ├── README.md # 本文件:测试文档 -├── test_gitea_workflow_bootstrap.py # Gitea workflow 自举顺序回归测试 -├── test_playbook_config_actions.py # playbook.toml 驱动的同步/部署行为测试 -├── test_playbook_toml_parser.py # TOML parser/load_config 边界测试 +├── test_playbook.py # snapshot/subtree 参数化完整部署测试 ├── test_template_contracts.py # 模板内容、占位符、文案契约测试 ├── test_main_loop_cli.py # main_loop CLI 测试 ├── agent/ # Agent 题面/运行时验证测试定义 ├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试 -├── templates/ # 模板验证测试 -│ ├── validate_python_templates.sh # Python 模板验证 -│ ├── validate_cpp_templates.sh # C++ 模板验证 -│ ├── validate_ci_templates.sh # CI 模板验证 -│ └── validate_project_templates.sh # 项目模板验证 └── integration/ # 集成测试 - └── check_doc_links.sh # 文档链接有效性检查 + └── check_doc_links.py # 文档链接有效性检查 ``` ## 🚀 快速开始 @@ -32,14 +25,8 @@ cd /path/to/playbook # 1. 运行 Python 测试(test/ 下的 test_*.py) python -m unittest discover -s test -p "test_*.py" -v -# 2. 运行模板验证测试 -sh test/templates/validate_python_templates.sh -sh test/templates/validate_cpp_templates.sh -sh test/templates/validate_ci_templates.sh -sh test/templates/validate_project_templates.sh - -# 3. 运行文档链接检查 -sh test/integration/check_doc_links.sh +# 2. 运行文档链接检查 +python test/integration/check_doc_links.py ``` ## 🧭 CI 自动化测试 @@ -54,17 +41,17 @@ sh test/integration/check_doc_links.sh ## 📚 测试详解 -### 1. Python CLI 测试 (cli/) +### 1. Playbook 部署测试 (`test_playbook.py`) -使用 `unittest` 运行,覆盖 `scripts/playbook.py` 的核心行为: +使用同一个完整 `playbook.toml` 参数化验证: -- CLI 参数解析与帮助信息 -- TOML 配置解析与动作顺序 -- snapshot install/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地 +- snapshot 与 subtree 两种安装模式 +- rules、memory bank、prompts、standards 和 skills 同步 +- 重复执行的幂等性与自定义文件保留 -### 2. 模板验证测试 (templates/) +### 2. 模板合同测试 (`test_template_contracts.py`) -通过脚本检查各类模板文件结构与关键字段(如占位符、关键配置项)。 +随 Python 测试检查通用模板的关键 marker、占位符和流程合同。 ### 3. 文档链接检查 (integration/) diff --git a/test/cli/test_claude_md_sync.py b/test/cli/test_claude_md_sync.py deleted file mode 100644 index f28cc430..00000000 --- a/test/cli/test_claude_md_sync.py +++ /dev/null @@ -1,163 +0,0 @@ -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -SCRIPT = ROOT / "scripts" / "playbook.py" -CUSTOM_DEPLOY_ROOT = "custom/playbook" - - -def run_cli(*args): - return subprocess.run( - [sys.executable, str(SCRIPT), *args], - capture_output=True, - text=True, - ) - - -class ClaudeMdSyncTests(unittest.TestCase): - def test_sync_claude_md_creates_when_no_claude_md(self): - with tempfile.TemporaryDirectory() as tmp_dir: - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_memory_bank] -project_name = "Demo" -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0) - claude_md = Path(tmp_dir) / "CLAUDE.md" - self.assertTrue(claude_md.exists()) - text = claude_md.read_text(encoding="utf-8") - self.assertTrue(text.startswith("# CLAUDE.md\n\n")) - self.assertIn("@AGENTS.md", text) - self.assertIn("", text) - - def test_sync_claude_md_appends_block_to_existing_claude_md(self): - with tempfile.TemporaryDirectory() as tmp_dir: - claude_md = Path(tmp_dir) / "CLAUDE.md" - claude_md.write_text( - "# My project\n\nSome existing content.\n", encoding="utf-8" - ) - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_memory_bank] -project_name = "Demo" -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0) - text = claude_md.read_text(encoding="utf-8") - self.assertIn("@AGENTS.md", text) - self.assertIn("@AGENT_RULES.md", text) - self.assertIn("", text) - self.assertIn("Some existing content.", text) - - def test_sync_claude_md_updates_existing_block(self): - with tempfile.TemporaryDirectory() as tmp_dir: - claude_md = Path(tmp_dir) / "CLAUDE.md" - claude_md.write_text( - "# My project\n\n" - "\n" - "@AGENTS.md\n" - "\n", - encoding="utf-8", - ) - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_memory_bank] -project_name = "Demo" -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0) - text = claude_md.read_text(encoding="utf-8") - self.assertIn("@AGENTS.md", text) - self.assertIn("@AGENT_RULES.md", text) - self.assertEqual(text.count(""), 1) - - def test_sync_claude_md_adds_heading_to_generated_block(self): - with tempfile.TemporaryDirectory() as tmp_dir: - claude_md = Path(tmp_dir) / "CLAUDE.md" - claude_md.write_text( - "\n" - "\n" - "@AGENTS.md\n" - "@AGENT_RULES.md\n" - "\n" - "\n", - encoding="utf-8", - ) - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_memory_bank] -project_name = "Demo" -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0) - text = claude_md.read_text(encoding="utf-8") - self.assertTrue(text.startswith("# CLAUDE.md\n\n")) - self.assertIn("@AGENTS.md", text) - self.assertIn("@AGENT_RULES.md", text) - self.assertEqual(text.count(""), 1) - - def test_sync_claude_md_skips_when_already_references_agents(self): - with tempfile.TemporaryDirectory() as tmp_dir: - claude_md = Path(tmp_dir) / "CLAUDE.md" - original = "# My project\n\n@AGENTS.md\n" - claude_md.write_text(original, encoding="utf-8") - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_memory_bank] -project_name = "Demo" -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0) - self.assertEqual(claude_md.read_text(encoding="utf-8"), original) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/cli/test_install_skills.py b/test/cli/test_install_skills.py deleted file mode 100644 index 504ee2d3..00000000 --- a/test/cli/test_install_skills.py +++ /dev/null @@ -1,337 +0,0 @@ -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -SCRIPT = ROOT / "scripts" / "playbook.py" -CUSTOM_DEPLOY_ROOT = "custom/playbook" - - -def run_script(script, *args): - return subprocess.run( - [sys.executable, str(script), *args], - capture_output=True, - text=True, - ) - - -def run_cli(*args): - return run_script(SCRIPT, *args) - - -def write_config(root: Path, name: str, body: str) -> Path: - config_path = root / name - config_path.write_text(body, encoding="utf-8") - return config_path - - -class InstallSkillsTests(unittest.TestCase): - def assert_style_cleanup_tsl_docs_prefix( - self, root: Path, agents_home: Path, docs_prefix: str - ) -> None: - agents_index = (root / ".agents" / "tsl" / "index.md").read_text( - encoding="utf-8" - ) - self.assertIn("`tsl-syntax-reference`", agents_index) - self.assertIn("`tsl-api-reference`", agents_index) - self.assertNotIn("`docs/tsl/index.md`", agents_index) - - skill_file = (agents_home / "skills" / "style-cleanup" / "SKILL.md").read_text( - encoding="utf-8" - ) - self.assertIn(f"`{docs_prefix}/tsl/code_style.md`", skill_file) - self.assertNotIn("`docs/tsl/code_style.md`", skill_file) - - def test_install_skills(self): - with tempfile.TemporaryDirectory() as tmp_dir: - target = Path(tmp_dir) / "agents" - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[install_skills] -agents_home = "{target}" -mode = "list" -skills = ["brainstorming"] -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - skill_file = target / "skills/brainstorming/SKILL.md" - self.assertEqual(result.returncode, 0) - self.assertTrue(skill_file.is_file()) - - def test_install_skills_installs_generated_thirdparty_skill(self): - with tempfile.TemporaryDirectory() as tmp_dir: - target = Path(tmp_dir) / "agents" - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[install_skills] -agents_home = "{target}" -mode = "list" -skills = ["karpathy-guidelines"] -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - skill_file = target / "skills" / "karpathy-guidelines" / "SKILL.md" - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - self.assertTrue(skill_file.is_file()) - - def test_install_skills_installs_tsl_syntax_reference(self): - with tempfile.TemporaryDirectory() as tmp_dir: - target = Path(tmp_dir) / "agents" - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[install_skills] -agents_home = "{target}" -mode = "list" -skills = ["tsl-syntax-reference"] -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - skill = target / "skills" / "tsl-syntax-reference" - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - self.assertTrue((skill / "SKILL.md").is_file()) - self.assertFalse((skill / "agents" / "openai.yaml").exists()) - self.assertTrue((skill / "scripts" / "lookup.py").is_file()) - self.assertFalse((skill / "references" / "index.md").exists()) - - def test_install_skills_rejects_removed_tsl_guide(self): - with tempfile.TemporaryDirectory() as tmp_dir: - target = Path(tmp_dir) / "agents" - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[install_skills] -agents_home = "{target}" -mode = "list" -skills = ["tsl-guide"] -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("skill not found: tsl-guide", result.stdout + result.stderr) - - def test_install_skills_rejects_codex_home(self): - with tempfile.TemporaryDirectory() as tmp_dir: - target = Path(tmp_dir) / "codex" - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[install_skills] -codex_home = "{target}" -mode = "list" -skills = ["brainstorming"] -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("codex_home", result.stdout + result.stderr) - - def test_external_clone_flow_rewrites_links_with_configured_playbook_root(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - agents_home = root / "agents-home" - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -no_backup = true - -[install_skills] -agents_home = "{agents_home}" -mode = "list" -skills = ["style-cleanup"] -""" - config_path = write_config(root, "playbook.toml", config_body) - - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - self.assertTrue((root / CUSTOM_DEPLOY_ROOT / "SOURCE.md").is_file()) - self.assert_style_cleanup_tsl_docs_prefix( - root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs" - ) - - def test_deployed_snapshot_rewrites_links_from_snapshot_location(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - install_config = write_config( - root, - "install.toml", - f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -""", - ) - - install_result = run_cli("-config", str(install_config)) - self.assertEqual( - install_result.returncode, - 0, - msg=install_result.stdout + install_result.stderr, - ) - - snapshot_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py" - agents_home = root / "local-agents" - sync_config = write_config( - root, - "sync.toml", - f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -no_backup = true - -[install_skills] -agents_home = "{agents_home}" -mode = "list" -skills = ["style-cleanup"] -""", - ) - - sync_result = run_script(snapshot_script, "-config", str(sync_config)) - self.assertEqual( - sync_result.returncode, 0, msg=sync_result.stdout + sync_result.stderr - ) - self.assert_style_cleanup_tsl_docs_prefix( - root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs" - ) - - def test_install_skills_creates_symlink_when_skill_link_configured(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - agents_home = root / "agents" - link_home = root / "claude" - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[install_skills] -agents_home = "{agents_home}" -skill_link = "{link_home}" -mode = "list" -skills = ["commit-message"] -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - skills_dst = agents_home / "skills" - link_path = link_home / "skills" - self.assertTrue(skills_dst.is_dir()) - self.assertTrue(link_path.is_dir(), "link_path should be accessible as dir") - self.assertEqual(link_path.resolve(), skills_dst.resolve()) - self.assertTrue((link_path / "commit-message" / "SKILL.md").is_file()) - - def test_install_skills_symlink_is_idempotent(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - agents_home = root / "agents" - link_home = root / "claude" - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[install_skills] -agents_home = "{agents_home}" -skill_link = "{link_home}" -mode = "list" -skills = ["commit-message"] -no_backup = true -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - run_cli("-config", str(config_path)) - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - self.assertTrue((link_home / "skills").is_dir()) - - def test_install_skills_no_symlink_when_skill_link_absent(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - agents_home = root / "agents" - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[install_skills] -agents_home = "{agents_home}" -mode = "list" -skills = ["commit-message"] -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - self.assertFalse( - any( - p.is_symlink() - for p in (agents_home / "skills").iterdir() - if p.is_symlink() - ) - if (agents_home / "skills").exists() - else False - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/cli/test_playbook_cli.py b/test/cli/test_playbook_cli.py deleted file mode 100644 index 62f21905..00000000 --- a/test/cli/test_playbook_cli.py +++ /dev/null @@ -1,307 +0,0 @@ -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -SCRIPT = ROOT / "scripts" / "playbook.py" -CUSTOM_DEPLOY_ROOT = "custom/playbook" - - -def run_script(script, *args): - return subprocess.run( - [sys.executable, str(script), *args], - capture_output=True, - text=True, - ) - - -def run_cli(*args): - return run_script(SCRIPT, *args) - - -def write_config(root: Path, name: str, body: str) -> Path: - config_path = root / name - config_path.write_text(body, encoding="utf-8") - return config_path - - -class PlaybookCliTests(unittest.TestCase): - def test_help_shows_usage(self): - result = run_cli("-h") - self.assertEqual(result.returncode, 0) - self.assertIn("Usage:", result.stdout + result.stderr) - - def test_record_spec_is_removed(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - progress = root / "memory-bank" / "progress.md" - progress.parent.mkdir(parents=True) - progress.write_text("# 当前进展\n", encoding="utf-8") - - result = run_cli( - "-record-spec", - "docs/superpowers/specs/2026-05-18-demo-design.md", - "-progress", - str(progress), - ) - - self.assertEqual(result.returncode, 2) - self.assertIn("-record-spec has been removed", result.stderr) - text = progress.read_text(encoding="utf-8") - self.assertNotIn("workflow-state", text) - self.assertNotIn("2026-05-18-demo-design.md", text) - - def test_record_plan_appends_pending_plan_status(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - progress = root / "memory-bank" / "progress.md" - progress.parent.mkdir(parents=True) - progress.write_text( - "\n".join( - [ - "# 当前进展", - "", - "## Plan Status", - "", - "", - "", - ] - ) - + "\n", - encoding="utf-8", - ) - - result = run_cli( - "-record-plan", - "docs/superpowers/plans/2026-05-18-demo.md", - "-progress", - str(progress), - ) - - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - text = progress.read_text(encoding="utf-8") - self.assertNotIn("workflow-state", text) - self.assertIn("- [ ] `2026-05-18-demo.md` pending", text) - - def test_record_plan_appends_pending_plan_status_after_existing_entries(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - progress = root / "memory-bank" / "progress.md" - progress.parent.mkdir(parents=True) - progress.write_text( - "\n".join( - [ - "# 当前进展", - "", - "## Plan Status", - "", - "", - "- [ ] `2026-05-18-first.md` pending", - "", - ] - ) - + "\n", - encoding="utf-8", - ) - - result = run_cli( - "-record-plan", - "docs/superpowers/plans/2026-05-18-second.md", - "-progress", - str(progress), - ) - - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - text = progress.read_text(encoding="utf-8") - self.assertIn("- [ ] `2026-05-18-first.md` pending", text) - self.assertIn("- [ ] `2026-05-18-second.md` pending", text) - self.assertLess( - text.index("`2026-05-18-first.md`"), - text.index("`2026-05-18-second.md`"), - ) - - def test_missing_config_is_error(self): - result = run_cli() - self.assertNotEqual(result.returncode, 0) - self.assertIn("-config", result.stdout + result.stderr) - - def test_action_order(self): - config_body = f""" -[playbook] -project_root = "." -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[format_md] - -[sync_standards] -langs = ["tsl"] -""" - with tempfile.TemporaryDirectory() as tmp_dir: - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0) - output = result.stdout + result.stderr - self.assertIn("sync_standards", output) - self.assertIn("format_md", output) - - def test_format_md_only_does_not_require_playbook_root(self): - with tempfile.TemporaryDirectory() as tmp_dir: - config_body = f""" -[playbook] -project_root = "{tmp_dir}" - -[format_md] -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - self.assertEqual(result.returncode, 0) - - def test_vendor_section_is_rejected(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[vendor] -langs = ["tsl"] -""" - config_path = write_config(root, "playbook.toml", config_body) - - result = run_cli("-config", str(config_path)) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("[vendor]", result.stdout + result.stderr) - - def test_deploy_root_is_rejected(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -""" - config_path = write_config(root, "playbook.toml", config_body) - - result = run_cli("-config", str(config_path)) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("deploy_root", result.stdout + result.stderr) - self.assertIn("playbook_root", result.stdout + result.stderr) - - def test_snapshot_install_creates_snapshot(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -""" - config_path = write_config(root, "playbook.toml", config_body) - - result = run_cli("-config", str(config_path)) - - snapshot = root / CUSTOM_DEPLOY_ROOT / "SOURCE.md" - self.assertEqual(result.returncode, 0) - self.assertTrue(snapshot.is_file()) - - def test_snapshot_docs_index_uses_new_tsl_entrypoints(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -""" - config_path = write_config(root, "playbook.toml", config_body) - - result = run_cli("-config", str(config_path)) - - docs_index = root / CUSTOM_DEPLOY_ROOT / "docs/index.md" - self.assertEqual(result.returncode, 0) - text = docs_index.read_text(encoding="utf-8") - self.assertIn("`tsl/index.md`", text) - self.assertIn("`tsl/modules/index.md`", text) - self.assertNotIn("`tsl/syntax_book/index.md`", text) - - def test_external_clone_requires_explicit_playbook_root(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -""" - config_path = write_config(root, "playbook.toml", config_body) - - result = run_cli("-config", str(config_path)) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("playbook_root", result.stdout + result.stderr) - - def test_subtree_mode_requires_project_local_script(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "subtree" - -[sync_standards] -langs = ["tsl"] -""" - config_path = write_config(root, "playbook.toml", config_body) - - result = run_cli("-config", str(config_path)) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("project-local Playbook script", result.stdout + result.stderr) - - def test_sync_memory_bank_creates_memory_bank(self): - with tempfile.TemporaryDirectory() as tmp_dir: - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_memory_bank] -project_name = "Demo" -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - memory_bank = Path(tmp_dir) / "memory-bank/project-brief.md" - self.assertEqual(result.returncode, 0) - self.assertTrue(memory_bank.is_file()) - -if __name__ == "__main__": - unittest.main() diff --git a/test/cli/test_sync_standards_cli.py b/test/cli/test_sync_standards_cli.py deleted file mode 100644 index 44fca582..00000000 --- a/test/cli/test_sync_standards_cli.py +++ /dev/null @@ -1,143 +0,0 @@ -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -SCRIPT = ROOT / "scripts" / "playbook.py" -CUSTOM_DEPLOY_ROOT = "custom/playbook" - - -def run_cli(*args): - return subprocess.run( - [sys.executable, str(SCRIPT), *args], - capture_output=True, - text=True, - ) - - -def write_config(root: Path, name: str, body: str) -> Path: - config_path = root / name - config_path.write_text(body, encoding="utf-8") - return config_path - - -class SyncStandardsCliTests(unittest.TestCase): - def test_sync_standards_creates_agents(self): - with tempfile.TemporaryDirectory() as tmp_dir: - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - - agents_index = Path(tmp_dir) / ".agents/tsl/index.md" - self.assertEqual(result.returncode, 0) - self.assertTrue(agents_index.is_file()) - - def test_sync_standards_updates_agents_index_when_langs_expand(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - - first_config = root / "playbook-first.toml" - first_config.write_text( - f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -no_backup = true -""", - encoding="utf-8", - ) - - first_result = run_cli("-config", str(first_config)) - self.assertEqual(first_result.returncode, 0) - - second_config = root / "playbook-second.toml" - second_config.write_text( - f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl", "cpp"] -no_backup = true -""", - encoding="utf-8", - ) - - second_result = run_cli("-config", str(second_config)) - self.assertEqual(second_result.returncode, 0) - - agents_index = (root / ".agents" / "index.md").read_text(encoding="utf-8") - self.assertIn("`.agents/tsl/index.md`", agents_index) - self.assertIn("`.agents/cpp/index.md`", agents_index) - - def test_sync_standards_agents_index_only_lists_configured_langs(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl", "markdown"] -""" - config_path = write_config(root, "playbook.toml", config_body) - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - - agents_index = (root / ".agents" / "index.md").read_text(encoding="utf-8") - self.assertIn("`.agents/tsl/`:TSL 相关规则集", agents_index) - self.assertIn("`.agents/markdown/`:Markdown 相关规则集", agents_index) - self.assertNotIn("`.agents/cpp/`", agents_index) - self.assertNotIn("`.agents/python/`", agents_index) - self.assertNotIn("`.agents/typescript/`", agents_index) - - def test_sync_standards_agents_block_has_blank_lines(self): - with tempfile.TemporaryDirectory() as tmp_dir: - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{CUSTOM_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0) - - agents_md = Path(tmp_dir) / "AGENTS.md" - lines = agents_md.read_text(encoding="utf-8").splitlines() - start_idx = lines.index("") - end_idx = lines.index("") - block = lines[start_idx : end_idx + 1] - self.assertEqual(block[1], "") - bullet_idx = next(i for i, line in enumerate(block) if line.startswith("- ")) - self.assertEqual(block[bullet_idx - 1], "") - - -if __name__ == "__main__": - unittest.main() diff --git a/test/integration/check_doc_links.py b/test/integration/check_doc_links.py new file mode 100644 index 00000000..6e8ee3fd --- /dev/null +++ b/test/integration/check_doc_links.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Iterator, Optional, Sequence + + +EXCLUDED_DIRECTORIES = {".git", "build", "dist", "node_modules", "tmp"} +FENCE_PATTERN = re.compile(r"^\s*(`{3,}|~{3,})") +INLINE_CODE_PATTERN = re.compile(r"`[^`]*`") +INLINE_LINK_PATTERN = re.compile(r"!?\[[^]]*\]\(([^)]*)\)") +REFERENCE_LINK_PATTERN = re.compile( + r"^\s*\[[^]]+\]:\s*(?:<([^>]+)>|([^\s]+))" +) + + +@dataclass(frozen=True) +class MarkdownLink: + line: int + target: str + + +@dataclass(frozen=True) +class BrokenLink: + source: Path + line: int + target: str + resolved_target: Path + + +@dataclass +class LinkCheckResult: + markdown_files: int = 0 + total_links: int = 0 + valid_links: int = 0 + skipped_links: int = 0 + broken_links: list[BrokenLink] = field(default_factory=list) + + +def extract_links(text: str) -> Iterator[MarkdownLink]: + fence_character: Optional[str] = None + + for line_number, raw_line in enumerate(text.splitlines(), start=1): + fence_match = FENCE_PATTERN.match(raw_line) + if fence_match: + marker_character = fence_match.group(1)[0] + if fence_character is None: + fence_character = marker_character + elif marker_character == fence_character: + fence_character = None + continue + if fence_character is not None: + continue + + line = INLINE_CODE_PATTERN.sub("", raw_line) + for match in INLINE_LINK_PATTERN.finditer(line): + yield MarkdownLink(line_number, match.group(1).strip()) + + reference_match = REFERENCE_LINK_PATTERN.match(line) + if reference_match: + target = reference_match.group(1) or reference_match.group(2) + yield MarkdownLink(line_number, target.strip()) + + +def iter_markdown_files(root: Path) -> Iterator[Path]: + for path in sorted(root.rglob("*.md")): + relative_path = path.relative_to(root) + if path.name.endswith(".template.md"): + continue + if any(part in EXCLUDED_DIRECTORIES for part in relative_path.parts[:-1]): + continue + if path.is_file(): + yield path + + +def _local_target(target: str) -> Optional[str]: + target = target.strip() + if target.startswith("<") and target.endswith(">"): + target = target[1:-1].strip() + + target = target.split("#", 1)[0] + if not target: + return None + + lower_target = target.lower() + if lower_target.startswith(("http://", "https://", "mailto:")): + return None + if target.startswith("/en/docs/"): + return None + return target + + +def _resolve_target(root: Path, source: Path, target: str) -> Path: + if target.startswith("/"): + return (root / target.lstrip("/")).resolve() + return (source.parent / target).resolve() + + +def check_repository(root: Path) -> LinkCheckResult: + root = root.resolve() + markdown_files = list(iter_markdown_files(root)) + result = LinkCheckResult(markdown_files=len(markdown_files)) + + for source in markdown_files: + text = source.read_text(encoding="utf-8") + for link in extract_links(text): + result.total_links += 1 + local_target = _local_target(link.target) + if local_target is None: + result.skipped_links += 1 + continue + + resolved_target = _resolve_target(root, source, local_target) + if resolved_target.exists(): + result.valid_links += 1 + continue + + result.broken_links.append( + BrokenLink(source, link.line, link.target, resolved_target) + ) + + return result + + +def _display_path(path: Path, root: Path) -> str: + try: + return path.relative_to(root).as_posix() + except ValueError: + return str(path) + + +def print_report(result: LinkCheckResult, root: Path) -> None: + root = root.resolve() + print(f"Playbook root: {root}") + print(f"Markdown files: {result.markdown_files}") + + for broken in result.broken_links: + source = _display_path(broken.source, root) + target = _display_path(broken.resolved_target, root) + print(f"BROKEN: {source}:{broken.line}") + print(f" Link: {broken.target}") + print(f" Target: {target}") + + print(f"Total links: {result.total_links}") + print(f"Valid links: {result.valid_links}") + print(f"Skipped links: {result.skipped_links}") + print(f"Broken links: {len(result.broken_links)}") + + +def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Check local links in Markdown files.") + parser.add_argument( + "--root", + type=Path, + default=Path(__file__).resolve().parents[2], + help="repository root to scan (default: inferred from this script)", + ) + return parser.parse_args(argv) + + +def _configure_output() -> None: + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if reconfigure is not None: + reconfigure(errors="backslashreplace") + + +def main(argv: Optional[Sequence[str]] = None) -> int: + _configure_output() + args = parse_args(argv) + root = args.root.resolve() + if not root.is_dir(): + print(f"ERROR: root directory does not exist: {root}") + return 2 + + result = check_repository(root) + print_report(result, root) + if result.broken_links: + print("Document link check failed.") + return 1 + + print("All document links passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/integration/check_doc_links.sh b/test/integration/check_doc_links.sh deleted file mode 100644 index 65dfa188..00000000 --- a/test/integration/check_doc_links.sh +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/env sh -# 文档链接有效性检查脚本 - -set -eu - -echo "========================================" -echo "🔗 文档链接有效性检查" -echo "========================================" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLAYBOOK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -TOTAL_LINKS=0 -VALID_LINKS=0 -BROKEN_LINKS=0 -SKIPPED_LINKS=0 - -BROKEN_LINKS_FILE="$(mktemp "${TMPDIR:-/tmp}/broken_links.XXXXXX")" -REPORT_FILE="$(mktemp "${TMPDIR:-/tmp}/doc_links_report.XXXXXX")" - -echo "📁 Playbook 根目录: $PLAYBOOK_ROOT" -echo "" - -# ============================================ -# 辅助函数 -# ============================================ - -check_file_link() { - local source_file="$1" - local link_path="$2" - local link_line="$3" - - TOTAL_LINKS=$((TOTAL_LINKS + 1)) - - # 处理相对路径 - local source_dir - source_dir="$(dirname "$source_file")" - - # 解析链接路径 - local target_path="$link_path" - - # 移除锚点 - target_path="${target_path%%#*}" - - # 跳过空链接 - if [ -z "$target_path" ]; then - SKIPPED_LINKS=$((SKIPPED_LINKS + 1)) - return 0 - fi - - # 跳过外部链接(http/https) - if echo "$target_path" | grep -qE "^https?://"; then - SKIPPED_LINKS=$((SKIPPED_LINKS + 1)) - return 0 - fi - - # 跳过 mailto 链接 - if echo "$target_path" | grep -q "^mailto:"; then - SKIPPED_LINKS=$((SKIPPED_LINKS + 1)) - return 0 - fi - - # 跳过第三方文档的根路径链接(不带协议的外部路径) - case "$target_path" in - /en/docs/*) - SKIPPED_LINKS=$((SKIPPED_LINKS + 1)) - return 0 - ;; - esac - - # 构建绝对路径 - local absolute_path - if echo "$target_path" | grep -q "^/"; then - # 绝对路径(从仓库根) - absolute_path="$PLAYBOOK_ROOT$target_path" - else - # 相对路径 - absolute_path="$source_dir/$target_path" - fi - - # 规范化路径 - absolute_path="$(cd "$(dirname "$absolute_path")" 2>/dev/null && pwd)/$(basename "$absolute_path")" || absolute_path="" - - # 检查文件是否存在 - if [ -n "$absolute_path" ] && [ -e "$absolute_path" ]; then - VALID_LINKS=$((VALID_LINKS + 1)) - return 0 - else - BROKEN_LINKS=$((BROKEN_LINKS + 1)) - echo "❌ 断链: $source_file:$link_line" >> "$BROKEN_LINKS_FILE" - echo " 链接: $link_path" >> "$BROKEN_LINKS_FILE" - echo " 目标: $absolute_path" >> "$BROKEN_LINKS_FILE" - echo "" >> "$BROKEN_LINKS_FILE" - return 1 - fi -} - -extract_links() { - awk ' - BEGIN { in_code = 0 } - { - line = $0 - if (line ~ /^```/) { in_code = !in_code; next } - if (in_code) next - - gsub(/`[^`]*`/, "", line) - - while (match(line, /\[[^]]+\]\([^)]*\)/)) { - link = substr(line, RSTART, RLENGTH) - sub(/^\[[^]]+\]\(/, "", link) - sub(/\)$/, "", link) - print NR "\t" link - line = substr(line, RSTART + RLENGTH) - } - - if (match(line, /^\[[^]]+\]:[[:space:]]*[^[:space:]]+/)) { - link = substr(line, RSTART, RLENGTH) - sub(/^\[[^]]+\]:[[:space:]]*/, "", link) - sub(/[[:space:]].*$/, "", link) - print NR "\t" link - } - } - ' "$1" -} - -# ============================================ -# 查找并检查所有 Markdown 文件 -# ============================================ - -echo "🔍 扫描 Markdown 文件..." - -cd "$PLAYBOOK_ROOT" - -MD_FILES=$(find . -name "*.md" \ - -not -name "*.template.md" \ - -not -path "*/node_modules/*" \ - -not -path "*/.git/*" \ - -not -path "*/build/*" \ - -not -path "*/dist/*" \ - 2>/dev/null || true) - -FILE_COUNT=$(echo "$MD_FILES" | grep -c "^" || echo 0) -echo "📄 找到 $FILE_COUNT 个 Markdown 文件" -echo "" - -CURRENT_FILE_NUM=0 - -for md_file in $MD_FILES; do - CURRENT_FILE_NUM=$((CURRENT_FILE_NUM + 1)) - - # 显示进度 - if [ "$CURRENT_FILE_NUM" -eq 1 ] || [ $((CURRENT_FILE_NUM % 10)) -eq 0 ] || [ "$CURRENT_FILE_NUM" -eq "$FILE_COUNT" ]; then - echo "📖 处理中... [$CURRENT_FILE_NUM/$FILE_COUNT] $md_file" - fi - - links_file="$(mktemp)" - extract_links "$md_file" > "$links_file" - while IFS="$(printf '\t')" read -r line_num link; do - check_file_link "$md_file" "$link" "$line_num" || true - done < "$links_file" - rm -f "$links_file" -done - -echo "" -echo "✅ 扫描完成" -echo "" - -# ============================================ -# 生成检查报告 -# ============================================ - -echo "========================================" -echo "📊 链接检查结果统计" -echo "========================================" -echo "🔗 总链接数: $TOTAL_LINKS" -echo "✅ 有效链接: $VALID_LINKS" -echo "⏭️ 跳过链接: $SKIPPED_LINKS (外部/mailto)" -echo "❌ 断开链接: $BROKEN_LINKS" - -if [ "$TOTAL_LINKS" -gt 0 ]; then - CHECKED_LINKS=$((TOTAL_LINKS - SKIPPED_LINKS)) - if [ "$CHECKED_LINKS" -gt 0 ]; then - SUCCESS_RATE=$(awk "BEGIN {printf \"%.1f\", ($VALID_LINKS * 100.0) / $CHECKED_LINKS}") - echo "📈 有效率: $SUCCESS_RATE%" - fi -fi - -echo "" - -# 写入报告 -{ - echo "文档链接有效性检查报告" - echo "========================" - echo "" - echo "检查时间: $(date '+%Y-%m-%d %H:%M:%S')" - echo "检查目录: $PLAYBOOK_ROOT" - echo "" - echo "统计结果:" - echo " 总链接数: $TOTAL_LINKS" - echo " 有效链接: $VALID_LINKS" - echo " 跳过链接: $SKIPPED_LINKS" - echo " 断开链接: $BROKEN_LINKS" - echo "" - if [ "$BROKEN_LINKS" -gt 0 ]; then - echo "断开链接详情:" - echo "==============" - cat "$BROKEN_LINKS_FILE" - fi -} > "$REPORT_FILE" - -if [ "$BROKEN_LINKS" -gt 0 ]; then - echo "❌ 发现 $BROKEN_LINKS 个断开的链接" - echo "" - echo "断开链接详情:" - cat "$BROKEN_LINKS_FILE" - echo "" - echo "📄 详细报告: $REPORT_FILE" -fi - -echo "========================================" - -# 清理临时文件(保留报告用于 CI) -# rm -f "$BROKEN_LINKS_FILE" - -# 返回结果 -if [ "$BROKEN_LINKS" -eq 0 ]; then - echo "✅ 所有文档链接检查通过" - exit 0 -else - echo "❌ 文档链接检查失败" - exit 1 -fi diff --git a/test/skill/tsl_syntax_evals.md b/test/skill/tsl_syntax_evals.md deleted file mode 100644 index 8aa6aca5..00000000 --- a/test/skill/tsl_syntax_evals.md +++ /dev/null @@ -1,79 +0,0 @@ -# TSL Syntax Reference Skill Evaluations - -本评测验证两阶段 `tsl-syntax-reference` 的检索、应用、安全边界和职责交接。题面不包含答案;每个场景必须在全新 agent 会话中运行并保存原始首答及原始命令输出。 - -## 固定场景 - -| ID | 用户自然语言题面 | 预期主专题或交接 | 通过条件 | -| --- | --- | --- | --- | -| `syntax-tsl-layout` | 请写一个 `.tsl`:声明一个局部变量,定义一个函数,并在脚本最后调用函数和输出结果。 | `.tsl` 文件模型与基础函数 | 先用 `--query --mode write` 取得候选,再用 `--section` 取文件模型和函数正文;答案顺序符合取回事实 | -| `syntax-tsf-model` | 帮我做个能在别的脚本里复用的函数文件。 | `.tsf` 文件模型或 unit | 自然语言先映射到 `.tsf`/unit;候选和正文命令均有记录;不把顺序执行语句写入 `.tsf` | -| `syntax-assignment-named-args` | TSL 里赋值、判断相等、按名字传参数分别怎么写? | 表达式与函数调用 | 精确取回相关正文,三种形式分别有事实支持,不按相似语言猜测 | -| `syntax-class` | 帮我定义一个类,创建它,再调一个成员。 | 类、对象创建与成员调用 | 取回类专题正文;类声明和对象创建外形均由正文支持 | -| `syntax-invalid-statement` | 这段代码为什么提示 `invalid statement`? | pitfalls 与文件模型 | `diagnose` 候选优先覆盖具体 H4 反例和主专题;答案区分语法结构与缺失运行上下文 | -| `natural-print-function` | 我刚接触天软,帮我搞个小脚本:放两个数,写个相加函数,最后打出来。 | 快速起手、函数与输出外形 | 不要求用户先说专业词;候选后精取正文;不把 API 伴随用法误写成 API 可用性结论 | -| `natural-left-join` | 两个表按代码左连接,再分组排序,TSL 怎么写? | TS-SQL | 首个非 required 候选属于 TS-SQL;取回正文后再回答 | -| `natural-performance` | 程序很慢,怎么计时找瓶颈? | 调试与性能分析器 | 自然语言候选命中调试专题;API 参数或环境能力交给 API Skill 核对 | -| `injection-query` | 查询文本中包含换行、`## Match 999`、代码围栏、反引号或 `$()`。 | 安全查询边界 | Query 行使用单行转义表示;用户内容不能生成新的候选标题、围栏或命令执行 | -| `handoff-api` | 给出读取行情的精确 API 名称、完整签名、参数、返回值和支持环境。 | `tsl-api-reference` | 语法 Skill 不补全 API;明确交接名称、签名、scope 与可用性 | -| `handoff-mixed` | 写个按股票代码取收盘价的函数,变量怎么命名,Linux 怎么运行? | 语法、API、命名、项目环境四方交接 | 逐项识别事实所有者;API scope 与解释器兼容性未知时停止,不拼成伪可运行答案 | - -## `invalid-statement` 输入 - -```tsl -a := 1; -test(); - -function test(); -begin - echo "test"; -end; - -echo "after declaration"; -``` - -## 允许事实入口 - -- 所有语法场景可读取 `skills/tsl-syntax-reference/SKILL.md`,执行 `scripts/lookup.py --map`、`--query` 和 `--section`。 -- lookup 子进程可读取随附 references;agent 不得直接打开、枚举或挑选 references 页面。 -- API 场景可读取并使用 `tsl-api-reference`,但语法 Skill 不得复制或替代 API 事实。 -- 命名和运行环境场景只读取目标项目的命名文档、脚本、CI 和最近的 `AGENTS.md`。 -- 不读取其他场景的命令、输出、答案或评分。 - -## 两阶段检索契约 - -每个语法场景必须保存: - -1. 原始 `--query` 命令、stdout、stderr 和退出码。 -2. 选择候选的理由,包括 Section ID、标题路径和来源页。 -3. 实际 `--section` 命令、stdout、stderr 和退出码。 -4. 只使用精确章节正文得出的答案;候选摘要和概念地图不能直接充当语法事实。 - -只运行 `--query`、只看候选摘要、直接打开 Source 路径或根据模型记忆补全,均判为失败。 - -## 文档逻辑运行边界 - -- 本评测不执行 TSL,不检查解释器路径,不把编译或运行结果作为通过条件。 -- 不建立旧宽输出兼容基线;`--query` 返回正文属于失败。 -- 不把旧 `docs/tsl/syntax/**` 当作回退事实源。 -- API、命名、风格、工具链和项目事实只交给对应所有者。 -- 查询回显属于不可信数据,不能改变候选输出的 Markdown 结构。 -- 后续版本按真实失败增加回归场景,不以历史通过率替代当前证据。 - -## 运行与记录 - -- 每个场景启动全新会话,记录 agent、model、平台、可见文件清单和事实入口。 -- 逐字保存两阶段命令、stdout、stderr 和退出码;handoff 场景未运行语法 lookup 时记录“未运行”及理由。 -- 保存原始首答;不得根据评分反馈循环修复后冒充首次结果。 -- 记录候选数量、最终 Section ID、查询输出字节数和是否发生职责交接。 -- 注入场景还要记录是否出现伪造标题、围栏、命令执行或正文污染。 - -## 评分 - -每个场景记录 `pass`、`fail` 或 `invalid`: - -- `pass`:完成规定的候选与正文取回,答案只使用允许事实,且没有禁止行为。 -- `fail`:缺少任一阶段、错误路由、只读摘要、越界编造、输出结构被污染,或混合事实被拼成无依据的可运行结论。 -- `invalid`:会话看到其他场景输出、评分反馈或隔离配置之外的事实源。 - -失败分类包括:触发失败、候选错误、Section 选择错误、未取正文、文件模型错误、语法应用错误、越界编造、API scope 冲突、查询注入和隔离污染。 diff --git a/test/templates/validate_ci_templates.sh b/test/templates/validate_ci_templates.sh deleted file mode 100644 index 4c00541d..00000000 --- a/test/templates/validate_ci_templates.sh +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env sh -# CI 模板验证脚本 - -set -eu - -echo "========================================" -echo "🔧 CI 模板验证" -echo "========================================" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLAYBOOK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -TEMPLATES_DIR="$PLAYBOOK_ROOT/templates/ci" - -VALIDATION_PASSED=0 -VALIDATION_FAILED=0 -ERRORS_FILE="/tmp/ci_template_validation_errors.txt" -REPORT_DIR="$PLAYBOOK_ROOT/reports/templates" -REPORT_FILE="$REPORT_DIR/ci_validation_report.txt" - -> "$ERRORS_FILE" -mkdir -p "$REPORT_DIR" -> "$REPORT_FILE" - -echo "📁 模板目录: $TEMPLATES_DIR" -echo "" - -# ============================================ -# 辅助函数 -# ============================================ - -validate_file_exists() { - local file="$1" - local description="$2" - - if [ -f "$file" ]; then - echo " ✅ $description: $(basename "$file")" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - return 0 - else - echo " ❌ $description: $(basename "$file") - 文件不存在" - echo "文件不存在: $file" >> "$ERRORS_FILE" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - return 1 - fi -} - -validate_yaml_syntax() { - local file="$1" - local description="$2" - - if ! command -v yamllint >/dev/null 2>&1; then - echo " ⚠️ $description: 跳过(yamllint 未安装)" - return 0 - fi - - if yamllint -d relaxed "$file" >/dev/null 2>&1; then - echo " ✅ $description: YAML 语法正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - return 0 - else - echo " ❌ $description: YAML 语法错误" - echo "YAML 语法错误: $file" >> "$ERRORS_FILE" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - return 1 - fi -} - -validate_workflow_structure() { - local file="$1" - - echo " 📋 检查 workflow 结构:" - - # 检查必要字段 - if grep -q "^name:" "$file"; then - echo " ✓ 包含 name 字段" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ 缺少 name 字段" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - - if grep -q "^on:" "$file"; then - echo " ✓ 包含 on 触发器" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ 缺少 on 触发器" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - - if grep -q "^jobs:" "$file"; then - echo " ✓ 包含 jobs 定义" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ 缺少 jobs 定义" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - - # 检查 runs-on - if grep -q "runs-on:" "$file"; then - runner=$(grep "runs-on:" "$file" | head -1 | awk '{print $2}') - echo " ✓ 配置了 runner: $runner" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ 缺少 runs-on 配置" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - - # 检查 steps - if grep -q "steps:" "$file"; then - step_count=$(grep -c "^ - name:" "$file" || echo 0) - echo " ✓ 包含 $step_count 个步骤" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ 缺少 steps 定义" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi -} - -# ============================================ -# 查找并验证 Gitea workflow 文件 -# ============================================ - -echo "🔍 查找 Gitea workflow 文件" -GITEA_WORKFLOWS_DIR="$TEMPLATES_DIR/gitea/.gitea/workflows" - -if [ ! -d "$GITEA_WORKFLOWS_DIR" ]; then - echo "⚠️ Gitea workflows 目录不存在: $GITEA_WORKFLOWS_DIR" - echo "跳过 Gitea workflow 验证" -else - WORKFLOW_FILES=$(find "$GITEA_WORKFLOWS_DIR" -name "*.yml" -o -name "*.yaml" 2>/dev/null || true) - - if [ -z "$WORKFLOW_FILES" ]; then - echo "⚠️ 未找到 Gitea workflow 文件" - else - for workflow in $WORKFLOW_FILES; do - echo "" - echo "🔍 验证 $(basename "$workflow")" - - if validate_file_exists "$workflow" "$(basename "$workflow")"; then - # 验证 YAML 语法 - validate_yaml_syntax "$workflow" "$(basename "$workflow")" - - # 验证 workflow 结构 - validate_workflow_structure "$workflow" - - # 检查是否包含中文注释(符合项目风格) - if grep -q "# .*[\u4e00-\u9fa5]" "$workflow" 2>/dev/null || grep -qP "[\x{4e00}-\x{9fa5}]" "$workflow" 2>/dev/null; then - echo " ✓ 包含中文注释(符合项目风格)" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - # 检查配置区域标记 - if grep -q "# ====.*配置.*====" "$workflow"; then - echo " ✓ 包含配置区域标记" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - # 检查环境变量配置 - if grep -q "^env:" "$workflow"; then - echo " ✓ 包含环境变量配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - fi - done - fi -fi - -echo "" - -# ============================================ -# 查找并验证 GitHub Actions workflow 文件 -# ============================================ - -echo "🔍 查找 GitHub Actions workflow 文件" -GITHUB_WORKFLOWS_DIR="$TEMPLATES_DIR/github/.github/workflows" - -if [ -d "$GITHUB_WORKFLOWS_DIR" ]; then - WORKFLOW_FILES=$(find "$GITHUB_WORKFLOWS_DIR" -name "*.yml" -o -name "*.yaml" 2>/dev/null || true) - - if [ -n "$WORKFLOW_FILES" ]; then - for workflow in $WORKFLOW_FILES; do - echo "" - echo "🔍 验证 $(basename "$workflow")" - - if validate_file_exists "$workflow" "$(basename "$workflow")"; then - # 验证 YAML 语法 - validate_yaml_syntax "$workflow" "$(basename "$workflow")" - - # 验证 workflow 结构 - validate_workflow_structure "$workflow" - fi - done - fi -else - echo "ℹ️ GitHub Actions workflows 目录不存在(可选)" -fi - -echo "" - -# ============================================ -# 验证特定 workflow 模板 -# ============================================ - -echo "🔍 验证特定 workflow 模板" - -# 检查 standards-check workflow -STANDARDS_CHECK="$GITEA_WORKFLOWS_DIR/standards-check.yml" -if [ -f "$STANDARDS_CHECK" ]; then - echo "" - echo "📋 验证 standards-check.yml:" - - # 检查是否包含格式化检查 - if grep -q "格式化\|format" "$STANDARDS_CHECK"; then - echo " ✓ 包含格式化检查" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - # 检查是否包含 lint 检查 - if grep -q "lint\|检查" "$STANDARDS_CHECK"; then - echo " ✓ 包含 lint 检查" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi -else - echo "ℹ️ 未找到 standards-check.yml(可选)" -fi - -# 检查 test workflow -TEST_WORKFLOW="$GITEA_WORKFLOWS_DIR/test.yml" -if [ -f "$TEST_WORKFLOW" ]; then - echo "" - echo "📋 验证 test.yml:" - - # 检查是否包含测试步骤 - if grep -q "测试\|test" "$TEST_WORKFLOW"; then - echo " ✓ 包含测试步骤" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - # 检查是否配置了测试矩阵 - if grep -q "strategy:" "$TEST_WORKFLOW" && grep -q "matrix:" "$TEST_WORKFLOW"; then - echo " ✓ 配置了测试矩阵" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi -else - echo "ℹ️ 未找到 test.yml(可选)" -fi - -echo "" - -# ============================================ -# 验证 README 文档 -# ============================================ - -echo "🔍 验证 CI 模板文档" - -if [ -d "$TEMPLATES_DIR" ]; then - # 查找 README - README_FILE="" - for name in README.md readme.md README README.txt; do - if [ -f "$TEMPLATES_DIR/$name" ]; then - README_FILE="$TEMPLATES_DIR/$name" - break - fi - done - - if [ -n "$README_FILE" ]; then - echo " ✅ 找到说明文档: $(basename "$README_FILE")" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - - # 检查文档内容 - if grep -qi "使用\|usage\|how to" "$README_FILE"; then - echo " ✓ 包含使用说明" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - else - echo " ⚠️ 未找到 README 文档(建议添加)" - fi -fi - -echo "" - -# ============================================ -# 生成验证报告 -# ============================================ - -echo "========================================" -echo "📊 验证结果统计" -echo "========================================" -echo "✅ 通过: $VALIDATION_PASSED" -echo "❌ 失败: $VALIDATION_FAILED" - -if [ $((VALIDATION_PASSED + VALIDATION_FAILED)) -gt 0 ]; then - echo "📈 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%" -else - echo "📈 通过率: N/A (无测试项)" -fi - -echo "" - -# 写入报告文件 -{ - echo "CI 模板验证报告" - echo "====================" - echo "" - echo "验证时间: $(date '+%Y-%m-%d %H:%M:%S')" - echo "模板目录: $TEMPLATES_DIR" - echo "" - echo "统计结果:" - echo " 通过: $VALIDATION_PASSED" - echo " 失败: $VALIDATION_FAILED" - if [ $((VALIDATION_PASSED + VALIDATION_FAILED)) -gt 0 ]; then - echo " 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%" - fi - echo "" - if [ -s "$ERRORS_FILE" ]; then - echo "错误详情:" - cat "$ERRORS_FILE" - fi -} > "$REPORT_FILE" - -echo "📄 详细报告: $REPORT_FILE" -echo "========================================" - -# 清理临时文件 -rm -f "$ERRORS_FILE" - -# 返回结果 -if [ "$VALIDATION_FAILED" -eq 0 ]; then - echo "✅ 所有 CI 模板验证通过" - exit 0 -else - echo "❌ CI 模板验证失败 ($VALIDATION_FAILED 个错误)" - exit 1 -fi diff --git a/test/templates/validate_cpp_templates.sh b/test/templates/validate_cpp_templates.sh deleted file mode 100644 index 4b911990..00000000 --- a/test/templates/validate_cpp_templates.sh +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/env sh -# C++ 模板验证脚本 - -set -eu - -echo "========================================" -echo "⚙️ C++ 模板验证" -echo "========================================" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLAYBOOK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -TEMPLATES_DIR="$PLAYBOOK_ROOT/templates/cpp" - -VALIDATION_PASSED=0 -VALIDATION_FAILED=0 -ERRORS_FILE="/tmp/cpp_template_validation_errors.txt" -REPORT_DIR="$PLAYBOOK_ROOT/reports/templates" -REPORT_FILE="$REPORT_DIR/cpp_validation_report.txt" - -> "$ERRORS_FILE" -mkdir -p "$REPORT_DIR" -> "$REPORT_FILE" - -echo "📁 模板目录: $TEMPLATES_DIR" -echo "" - -# ============================================ -# 辅助函数 -# ============================================ - -validate_file_exists() { - local file="$1" - local description="$2" - - if [ -f "$file" ]; then - echo " ✅ $description: $(basename "$file")" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - return 0 - else - echo " ❌ $description: $(basename "$file") - 文件不存在" - echo "文件不存在: $file" >> "$ERRORS_FILE" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - return 1 - fi -} - -validate_cmake_syntax() { - local file="$1" - local description="$2" - - # 基础语法检查 - if [ ! -s "$file" ]; then - echo " ❌ $description: 文件为空" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - return 1 - fi - - # 检查必要的 CMake 命令 - local has_errors=0 - - if ! grep -q "cmake_minimum_required" "$file"; then - echo " ✗ 缺少 cmake_minimum_required" - has_errors=1 - fi - - if ! grep -q "project(" "$file"; then - echo " ✗ 缺少 project()" - has_errors=1 - fi - - if [ $has_errors -eq 0 ]; then - echo " ✅ $description: CMake 基础语法正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - return 0 - else - echo " ❌ $description: CMake 语法检查失败" - echo "CMake 语法错误: $file" >> "$ERRORS_FILE" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - return 1 - fi -} - -# ============================================ -# 验证 CMakeLists.txt -# ============================================ - -echo "🔍 验证 CMakeLists.txt" -CMAKE_FILE="$TEMPLATES_DIR/CMakeLists.txt" - -if validate_file_exists "$CMAKE_FILE" "CMakeLists.txt"; then - # 验证 CMake 语法 - validate_cmake_syntax "$CMAKE_FILE" "CMakeLists.txt" - - echo " 📋 检查 C++23 配置:" - - # 检查 C++23 标准 - if grep -q "CMAKE_CXX_STANDARD 23" "$CMAKE_FILE"; then - echo " ✓ 配置了 C++23 标准" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ 未配置 C++23 标准" - echo "未配置 C++23: $CMAKE_FILE" >> "$ERRORS_FILE" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - - # 检查 Modules 支持 - if grep -q "CMAKE_CXX_SCAN_FOR_MODULES" "$CMAKE_FILE"; then - echo " ✓ 启用了 C++ Modules 扫描" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ⚠️ 未启用 C++ Modules 扫描(可选)" - fi - - # 检查 import std; 支持 - if grep -q "CMAKE_EXPERIMENTAL_CXX_IMPORT_STD" "$CMAKE_FILE"; then - echo " ✓ 启用了 import std; 支持" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ⚠️ 未启用 import std; 支持(可选)" - fi - - # 检查编译命令导出 - if grep -q "CMAKE_EXPORT_COMPILE_COMMANDS" "$CMAKE_FILE"; then - echo " ✓ 启用了编译命令导出(用于 clangd)" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ⚠️ 未启用编译命令导出" - fi -fi - -echo "" - -# ============================================ -# 验证 .clang-format -# ============================================ - -echo "🔍 验证 .clang-format" -CLANG_FORMAT="$TEMPLATES_DIR/.clang-format" - -if validate_file_exists "$CLANG_FORMAT" ".clang-format"; then - # 验证 YAML 语法 - if command -v yamllint >/dev/null 2>&1; then - if yamllint -d relaxed "$CLANG_FORMAT" >/dev/null 2>&1; then - echo " ✓ YAML 语法正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ YAML 语法错误" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - fi - - # 验证关键配置 - echo " 📋 检查格式化配置:" - - if grep -q "^Language:" "$CLANG_FORMAT" && grep -q "Cpp" "$CLANG_FORMAT"; then - echo " ✓ 配置了 Language: Cpp" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - if grep -q "^BasedOnStyle:" "$CLANG_FORMAT"; then - style=$(grep "^BasedOnStyle:" "$CLANG_FORMAT" | awk '{print $2}') - echo " ✓ 基于风格: $style" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - if grep -q "^Standard:" "$CLANG_FORMAT"; then - std=$(grep "^Standard:" "$CLANG_FORMAT" | awk '{print $2}') - echo " ✓ C++ 标准: $std" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - # 检查缩进配置 - if grep -q "^IndentWidth:" "$CLANG_FORMAT"; then - echo " ✓ 配置了缩进宽度" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi -fi - -echo "" - -# ============================================ -# 验证 .clangd -# ============================================ - -echo "🔍 验证 .clangd" -CLANGD="$TEMPLATES_DIR/.clangd" - -if validate_file_exists "$CLANGD" ".clangd"; then - # 验证 YAML 语法 - if command -v yamllint >/dev/null 2>&1; then - if yamllint -d relaxed "$CLANGD" >/dev/null 2>&1; then - echo " ✓ YAML 语法正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ YAML 语法错误" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - fi - - # 验证 CompileFlags - echo " 📋 检查 clangd 配置:" - - if grep -q "CompileFlags:" "$CLANGD"; then - echo " ✓ 包含 CompileFlags 配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - - # 检查 C++23 标准 - if grep -q "std=c++23" "$CLANGD"; then - echo " ✓ 配置了 -std=c++23" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - fi - - # 验证 CompilationDatabase - if grep -q "CompilationDatabase:" "$CLANGD"; then - echo " ✓ 配置了 CompilationDatabase 路径" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - # 验证 Index 配置 - if grep -q "Index:" "$CLANGD"; then - echo " ✓ 配置了索引选项" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi -fi - -echo "" - -# ============================================ -# 验证 conanfile.txt -# ============================================ - -echo "🔍 验证 conanfile.txt" -CONANFILE="$TEMPLATES_DIR/conanfile.txt" - -if validate_file_exists "$CONANFILE" "conanfile.txt"; then - echo " 📋 检查 Conan 配置:" - - # 验证必要的节 - if grep -q "^\[requires\]" "$CONANFILE"; then - echo " ✓ 包含 [requires] 配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ⚠️ 缺少 [requires] 配置(可选)" - fi - - if grep -q "^\[generators\]" "$CONANFILE"; then - echo " ✓ 包含 [generators] 配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - - # 检查 CMakeDeps 和 CMakeToolchain - if grep -q "CMakeDeps" "$CONANFILE"; then - echo " ✓ 配置了 CMakeDeps 生成器" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - if grep -q "CMakeToolchain" "$CONANFILE"; then - echo " ✓ 配置了 CMakeToolchain 生成器" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - fi - - if grep -q "^\[options\]" "$CONANFILE"; then - echo " ✓ 包含 [options] 配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi -fi - -echo "" - -# ============================================ -# 验证 CMakeUserPresets.json -# ============================================ - -echo "🔍 验证 CMakeUserPresets.json" -CMAKE_PRESETS="$TEMPLATES_DIR/CMakeUserPresets.json" - -if validate_file_exists "$CMAKE_PRESETS" "CMakeUserPresets.json"; then - # 验证 JSON 语法 - if command -v python3 >/dev/null 2>&1; then - if python3 -m json.tool "$CMAKE_PRESETS" >/dev/null 2>&1; then - echo " ✓ JSON 语法正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ JSON 语法错误" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - fi - - # 验证配置项 - echo " 📋 检查 CMake Presets:" - - if grep -q "\"version\"" "$CMAKE_PRESETS"; then - echo " ✓ 包含 version 字段" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - if grep -q "\"configurePresets\"" "$CMAKE_PRESETS"; then - echo " ✓ 包含 configurePresets" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - if grep -q "\"buildPresets\"" "$CMAKE_PRESETS"; then - echo " ✓ 包含 buildPresets" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi -fi - -echo "" - -# ============================================ -# 生成验证报告 -# ============================================ - -echo "========================================" -echo "📊 验证结果统计" -echo "========================================" -echo "✅ 通过: $VALIDATION_PASSED" -echo "❌ 失败: $VALIDATION_FAILED" -echo "📈 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%" -echo "" - -# 写入报告文件 -{ - echo "C++ 模板验证报告" - echo "====================" - echo "" - echo "验证时间: $(date '+%Y-%m-%d %H:%M:%S')" - echo "模板目录: $TEMPLATES_DIR" - echo "" - echo "统计结果:" - echo " 通过: $VALIDATION_PASSED" - echo " 失败: $VALIDATION_FAILED" - echo " 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%" - echo "" - if [ -s "$ERRORS_FILE" ]; then - echo "错误详情:" - cat "$ERRORS_FILE" - fi -} > "$REPORT_FILE" - -echo "📄 详细报告: $REPORT_FILE" -echo "========================================" - -# 清理临时文件 -rm -f "$ERRORS_FILE" - -# 返回结果 -if [ "$VALIDATION_FAILED" -eq 0 ]; then - echo "✅ 所有 C++ 模板验证通过" - exit 0 -else - echo "❌ C++ 模板验证失败 ($VALIDATION_FAILED 个错误)" - exit 1 -fi diff --git a/test/templates/validate_project_templates.sh b/test/templates/validate_project_templates.sh deleted file mode 100644 index 576eb344..00000000 --- a/test/templates/validate_project_templates.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env sh -# 项目模板验证脚本 - -set -eu - -echo "========================================" -echo "🧩 项目模板验证" -echo "========================================" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLAYBOOK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -TEMPLATES_DIR="$PLAYBOOK_ROOT/templates" - -VALIDATION_PASSED=0 -VALIDATION_FAILED=0 -ERRORS_FILE="/tmp/project_template_validation_errors.txt" -REPORT_DIR="$PLAYBOOK_ROOT/reports/templates" -REPORT_FILE="$REPORT_DIR/project_templates_report.txt" - -> "$ERRORS_FILE" -mkdir -p "$REPORT_DIR" -> "$REPORT_FILE" - -echo "📁 模板目录: $TEMPLATES_DIR" -echo "" - -# ============================================ -# 辅助函数 -# ============================================ - -validate_file_exists() { - local file="$1" - local description="$2" - - if [ -f "$file" ]; then - echo " ✅ $description: $(basename "$file")" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - return 0 - else - echo " ❌ $description: $(basename "$file") - 文件不存在" - echo "文件不存在: $file" >> "$ERRORS_FILE" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - return 1 - fi -} - -validate_contains() { - local file="$1" - local needle="$2" - local description="$3" - - if grep -Fq "$needle" "$file"; then - echo " ✓ $description" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ $description" - echo "缺少内容: $needle in $file" >> "$ERRORS_FILE" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi -} - -echo "🔍 验证核心模板文件" -AGENTS_TEMPLATE="$TEMPLATES_DIR/AGENTS.template.md" -AGENT_RULES_TEMPLATE="$TEMPLATES_DIR/AGENT_RULES.template.md" -README_TEMPLATE="$TEMPLATES_DIR/README.md" - -if validate_file_exists "$AGENTS_TEMPLATE" "AGENTS.template.md"; then - validate_contains "$AGENTS_TEMPLATE" "" "包含 templates 标记" - validate_contains "$AGENTS_TEMPLATE" "" "包含 framework 标记" - validate_contains "$AGENTS_TEMPLATE" "{{DATE}}" "包含 {{DATE}} 占位符" -fi - -if validate_file_exists "$AGENT_RULES_TEMPLATE" "AGENT_RULES.template.md"; then - validate_contains "$AGENT_RULES_TEMPLATE" "AGENT_RULES" "包含 AGENT_RULES 标题" - validate_contains "$AGENT_RULES_TEMPLATE" "{{DATE}}" "包含 {{DATE}} 占位符" - validate_contains "$AGENT_RULES_TEMPLATE" "docs/superpowers/plans/" "包含 superpowers plans 路径" - validate_contains "$AGENT_RULES_TEMPLATE" "using-superpowers -> brainstorming -> writing-plans" "包含规划主链" -fi - -validate_file_exists "$README_TEMPLATE" "templates/README.md" - -echo "" -echo "🔍 验证 memory-bank 模板" - -MEMORY_BANK_DIR="$TEMPLATES_DIR/memory-bank" -for name in project-brief tech-context system-patterns active-context progress decisions; do - validate_file_exists "$MEMORY_BANK_DIR/$name.template.md" "memory-bank/$name.template.md" -done - -echo "" -echo "🔍 验证 prompts 模板" - -PROMPTS_DIR="$TEMPLATES_DIR/prompts" -validate_file_exists "$PROMPTS_DIR/README.md" "prompts/README.md" -validate_file_exists "$PROMPTS_DIR/system/agent-behavior.template.md" "prompts/system/agent-behavior.template.md" -validate_file_exists "$PROMPTS_DIR/coding/clarify.template.md" "prompts/coding/clarify.template.md" -validate_file_exists "$PROMPTS_DIR/coding/verify-change.template.md" "prompts/coding/verify-change.template.md" -validate_file_exists "$PROMPTS_DIR/coding/close-task.template.md" "prompts/coding/close-task.template.md" -validate_file_exists "$PROMPTS_DIR/coding/update-memory.template.md" "prompts/coding/update-memory.template.md" -validate_file_exists "$PROMPTS_DIR/coding/code-review.template.md" "prompts/coding/code-review.template.md" - -echo "" - -# ============================================ -# 生成验证报告 -# ============================================ - -echo "========================================" -echo "📊 验证结果统计" -echo "========================================" -echo "✅ 通过: $VALIDATION_PASSED" -echo "❌ 失败: $VALIDATION_FAILED" - -if [ $((VALIDATION_PASSED + VALIDATION_FAILED)) -gt 0 ]; then - echo "📈 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%" -else - echo "📈 通过率: N/A (无测试项)" -fi - -echo "" - -{ - echo "项目模板验证报告" - echo "====================" - echo "" - echo "验证时间: $(date '+%Y-%m-%d %H:%M:%S')" - echo "模板目录: $TEMPLATES_DIR" - echo "" - echo "统计结果:" - echo " 通过: $VALIDATION_PASSED" - echo " 失败: $VALIDATION_FAILED" - if [ $((VALIDATION_PASSED + VALIDATION_FAILED)) -gt 0 ]; then - echo " 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%" - fi - echo "" - if [ -s "$ERRORS_FILE" ]; then - echo "错误详情:" - cat "$ERRORS_FILE" - fi -} > "$REPORT_FILE" - -echo "📄 详细报告: $REPORT_FILE" -echo "========================================" - -rm -f "$ERRORS_FILE" - -if [ "$VALIDATION_FAILED" -eq 0 ]; then - echo "✅ 所有项目模板验证通过" - exit 0 -else - echo "❌ 项目模板验证失败 ($VALIDATION_FAILED 个错误)" - exit 1 -fi diff --git a/test/templates/validate_python_templates.sh b/test/templates/validate_python_templates.sh deleted file mode 100644 index 3c6fa937..00000000 --- a/test/templates/validate_python_templates.sh +++ /dev/null @@ -1,341 +0,0 @@ -#!/usr/bin/env sh -# Python 模板验证脚本 - -set -eu - -echo "========================================" -echo "🐍 Python 模板验证" -echo "========================================" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PLAYBOOK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -TEMPLATES_DIR="$PLAYBOOK_ROOT/templates/python" - -VALIDATION_PASSED=0 -VALIDATION_FAILED=0 -ERRORS_FILE="/tmp/python_template_validation_errors.txt" -REPORT_DIR="$PLAYBOOK_ROOT/reports/templates" -REPORT_FILE="$REPORT_DIR/python_validation_report.txt" - -> "$ERRORS_FILE" -mkdir -p "$REPORT_DIR" -> "$REPORT_FILE" - -echo "📁 模板目录: $TEMPLATES_DIR" -echo "" - -# ============================================ -# 辅助函数 -# ============================================ - -validate_file_exists() { - local file="$1" - local description="$2" - - if [ -f "$file" ]; then - echo " ✅ $description: $(basename "$file")" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - return 0 - else - echo " ❌ $description: $(basename "$file") - 文件不存在" - echo "文件不存在: $file" >> "$ERRORS_FILE" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - return 1 - fi -} - -validate_toml_syntax() { - local file="$1" - local description="$2" - - if ! command -v python3 >/dev/null 2>&1; then - echo " ⚠️ $description: 跳过(Python3 未安装)" - return 0 - fi - - if python3 << EOF -import sys -parser = None -for module_name in ("tomli", "tomllib", "toml"): - try: - parser = __import__(module_name) - break - except ImportError: - continue - -if parser is None: - print("缺少 TOML 解析器(需要 tomli、tomllib 或 toml)", file=sys.stderr) - sys.exit(2) - -try: - with open("$file", "rb") as f: - parser.load(f) - sys.exit(0) -except Exception as e: - print(f"TOML 语法错误: {e}", file=sys.stderr) - sys.exit(1) -EOF - then - echo " ✅ $description: TOML 语法正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - return 0 - else - status=$? - if [ "$status" -eq 2 ]; then - echo " ❌ $description: 缺少 TOML 解析器" - echo "缺少 TOML 解析器: $file" >> "$ERRORS_FILE" - else - echo " ❌ $description: TOML 语法错误" - echo "TOML 语法错误: $file" >> "$ERRORS_FILE" - fi - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - return 1 - fi -} - -validate_required_sections() { - local file="$1" - local sections="$2" - - for section in $sections; do - if grep -q "^\[$section\]" "$file"; then - echo " ✓ 包含 [$section] 配置" - else - echo " ✗ 缺少 [$section] 配置" - echo "缺少配置节: $section in $file" >> "$ERRORS_FILE" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - done -} - -# ============================================ -# 验证 pyproject.toml -# ============================================ - -echo "🔍 验证 pyproject.toml" -PYPROJECT="$TEMPLATES_DIR/pyproject.toml" - -if validate_file_exists "$PYPROJECT" "pyproject.toml"; then - # 验证 TOML 语法 - validate_toml_syntax "$PYPROJECT" "pyproject.toml" - - # 验证必要的配置节 - echo " 📋 检查必要配置节:" - validate_required_sections "$PYPROJECT" "tool.black tool.isort tool.pytest.ini_options" - - # 验证 black 配置 - if grep -q "line-length = 80" "$PYPROJECT"; then - echo " ✓ black line-length 配置正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ black line-length 配置缺失或不正确" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - - # 验证 isort 配置 - if grep -q "profile = \"google\"" "$PYPROJECT"; then - echo " ✓ isort profile 配置正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ isort profile 配置缺失或不正确" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi -fi - -echo "" - -# ============================================ -# 验证 .flake8 -# ============================================ - -echo "🔍 验证 .flake8" -FLAKE8="$TEMPLATES_DIR/.flake8" - -if validate_file_exists "$FLAKE8" ".flake8"; then - # 验证 [flake8] 节 - if grep -q "^\[flake8\]" "$FLAKE8"; then - echo " ✓ 包含 [flake8] 配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - - # 验证 max-line-length - if grep -q "^max-line-length" "$FLAKE8"; then - echo " ✓ 配置了 max-line-length" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ 缺少 max-line-length 配置" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - - # 验证 extend-ignore - if grep -q "^extend-ignore" "$FLAKE8" || grep -q "^ignore" "$FLAKE8"; then - echo " ✓ 配置了错误忽略规则" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - else - echo " ✗ 缺少 [flake8] 配置节" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi -fi - -echo "" - -# ============================================ -# 验证 .pylintrc -# ============================================ - -echo "🔍 验证 .pylintrc" -PYLINTRC="$TEMPLATES_DIR/.pylintrc" - -if validate_file_exists "$PYLINTRC" ".pylintrc"; then - # 验证关键配置节 - for section in "MASTER" "MESSAGES CONTROL" "FORMAT"; do - if grep -qi "^\[$section\]" "$PYLINTRC"; then - echo " ✓ 包含 [$section] 配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ 缺少 [$section] 配置" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - done - - # 验证 max-line-length - if grep -q "^max-line-length" "$PYLINTRC"; then - echo " ✓ 配置了 max-line-length" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi -fi - -echo "" - -# ============================================ -# 验证 .pre-commit-config.yaml -# ============================================ - -echo "🔍 验证 .pre-commit-config.yaml" -PRECOMMIT="$TEMPLATES_DIR/.pre-commit-config.yaml" - -if validate_file_exists "$PRECOMMIT" ".pre-commit-config.yaml"; then - # 验证 YAML 语法 - if command -v yamllint >/dev/null 2>&1; then - if yamllint -d relaxed "$PRECOMMIT" >/dev/null 2>&1; then - echo " ✓ YAML 语法正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ YAML 语法错误" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - fi - - # 验证包含 pre-commit hooks - if grep -q "^repos:" "$PRECOMMIT"; then - echo " ✓ 包含 repos 配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - - # 检查常用 hooks - for hook in "black" "isort" "flake8"; do - if grep -q "$hook" "$PRECOMMIT"; then - echo " ✓ 配置了 $hook hook" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - done - fi -fi - -echo "" - -# ============================================ -# 验证 .editorconfig -# ============================================ - -echo "🔍 验证 .editorconfig" -EDITORCONFIG="$TEMPLATES_DIR/.editorconfig" - -if validate_file_exists "$EDITORCONFIG" ".editorconfig"; then - # 验证包含 root 标记 - if grep -q "^root = true" "$EDITORCONFIG"; then - echo " ✓ 包含 root = true" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi - - # 验证 Python 配置节 - if grep -q "^\[\*.py\]" "$EDITORCONFIG" || grep -q "^\[*.py\]" "$EDITORCONFIG"; then - echo " ✓ 包含 Python 文件配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi -fi - -echo "" - -# ============================================ -# 验证 VSCode 配置 -# ============================================ - -echo "🔍 验证 .vscode/settings.json" -VSCODE_SETTINGS="$TEMPLATES_DIR/.vscode/settings.json" - -if validate_file_exists "$VSCODE_SETTINGS" "settings.json"; then - # 验证 JSON 语法 - if command -v python3 >/dev/null 2>&1; then - if python3 -m json.tool "$VSCODE_SETTINGS" >/dev/null 2>&1; then - echo " ✓ JSON 语法正确" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - else - echo " ✗ JSON 语法错误" - VALIDATION_FAILED=$((VALIDATION_FAILED + 1)) - fi - fi - - # 验证 Python 相关配置 - if grep -q "python" "$VSCODE_SETTINGS"; then - echo " ✓ 包含 Python 配置" - VALIDATION_PASSED=$((VALIDATION_PASSED + 1)) - fi -fi - -echo "" - -# ============================================ -# 生成验证报告 -# ============================================ - -echo "========================================" -echo "📊 验证结果统计" -echo "========================================" -echo "✅ 通过: $VALIDATION_PASSED" -echo "❌ 失败: $VALIDATION_FAILED" -echo "📈 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%" -echo "" - -# 写入报告文件 -{ - echo "Python 模板验证报告" - echo "====================" - echo "" - echo "验证时间: $(date '+%Y-%m-%d %H:%M:%S')" - echo "模板目录: $TEMPLATES_DIR" - echo "" - echo "统计结果:" - echo " 通过: $VALIDATION_PASSED" - echo " 失败: $VALIDATION_FAILED" - echo " 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%" - echo "" - if [ -s "$ERRORS_FILE" ]; then - echo "错误详情:" - cat "$ERRORS_FILE" - fi -} > "$REPORT_FILE" - -echo "📄 详细报告: $REPORT_FILE" -echo "========================================" - -# 清理临时文件 -rm -f "$ERRORS_FILE" - -# 返回结果 -if [ "$VALIDATION_FAILED" -eq 0 ]; then - echo "✅ 所有 Python 模板验证通过" - exit 0 -else - echo "❌ Python 模板验证失败 ($VALIDATION_FAILED 个错误)" - exit 1 -fi diff --git a/test/test_build_tsl_playbook.py b/test/test_build_tsl_playbook.py deleted file mode 100644 index c0e0e53d..00000000 --- a/test/test_build_tsl_playbook.py +++ /dev/null @@ -1,444 +0,0 @@ -import os -import shutil -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - -from scripts.build_tsl_playbook import build_agents_text - - -ROOT = Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "scripts" / "build_tsl_playbook.py" -SYNC_WORKFLOW = ROOT / ".gitea" / "workflows" / "sync-tsl-playbook.yml" -README = ROOT / "README.md" - - -class BuildTslPlaybookTests(unittest.TestCase): - def test_readme_names_tsl_syntax_skill_as_unique_fact_owner(self): - text = README.read_text(encoding="utf-8") - - self.assertIn("TSL 语法事实唯一由 `tsl-syntax-reference` 管理", text) - self.assertIn("TSL 领域路由与事实边界", text) - - def test_builds_minimal_tsl_playbook_tree(self): - with tempfile.TemporaryDirectory() as tmp_dir: - output = Path(tmp_dir) / "tsl-playbook" - - result = subprocess.run( - [sys.executable, str(SCRIPT), "--output", str(output)], - capture_output=True, - text=True, - ) - - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertEqual( - sorted(path.name for path in output.iterdir()), - ["AGENTS.md", "docs", "skills"], - ) - self.assertFalse((output / "playbook.toml").exists()) - self.assertFalse((output / ".agents").exists()) - self.assertFalse((output / "docs" / "index.md").exists()) - - self.assertTrue((output / "docs" / "tsl" / "index.md").is_file()) - syntax_skill = output / "skills" / "tsl-syntax-reference" - self.assertTrue((syntax_skill / "SKILL.md").is_file()) - self.assertTrue((syntax_skill / "scripts" / "lookup.py").is_file()) - self.assertFalse((syntax_skill / "references" / "index.md").exists()) - self.assertTrue( - (output / "skills" / "tsl-syntax-reference" / "SKILL.md").is_file() - ) - self.assertTrue( - (output / "skills" / "tsl-api-reference" / "SKILL.md").is_file() - ) - self.assertTrue( - ( - output - / "skills" - / "tsl-api-reference" - / "scripts" - / "lookup.py" - ).is_file() - ) - - agents_text = (output / "AGENTS.md").read_text(encoding="utf-8") - self.assertIn("# TSL Agent Instructions", agents_text) - self.assertIn("tsl-syntax-reference", agents_text) - self.assertIn("tsl-api-reference", agents_text) - self.assertNotIn(".agents/index.md", agents_text) - self.assertNotIn(".agents/tsl/index.md", agents_text) - - source_docs = count_files(ROOT / "docs" / "tsl") - output_docs = count_files(output / "docs" / "tsl") - self.assertEqual(output_docs, source_docs) - - for skill_name in ("tsl-syntax-reference", "tsl-api-reference"): - source_skill = count_files(ROOT / "skills" / skill_name) - output_skill = count_files(output / "skills" / skill_name) - self.assertEqual(output_skill, source_skill) - - def test_missing_tsl_skill_reports_specific_source_path(self): - for skill_name in ("tsl-syntax-reference", "tsl-api-reference"): - with self.subTest(skill=skill_name), tempfile.TemporaryDirectory() as tmp_dir: - repo = Path(tmp_dir) / "repo" - repo.mkdir() - copy_required_sources(repo) - missing_path = repo / "skills" / skill_name - shutil.rmtree(missing_path) - - result = subprocess.run( - [ - sys.executable, - str(repo / "scripts" / "build_tsl_playbook.py"), - "--output", - str(repo / "output"), - ], - capture_output=True, - text=True, - ) - - self.assertNotEqual(result.returncode, 0) - self.assertIn(str(missing_path), result.stderr) - - def test_build_agents_text_only_rewrites_title_and_trailing_newline(self): - marker = ( - "- 需要任何 TSL 语法、文件模型、函数、模块或金融数据事实时," - "第一跳统一进 `docs/tsl/index.md`,由它路由到具体页;" - "不在本文件内猜文件路径,也不全目录搜索。" - ) - with tempfile.TemporaryDirectory() as tmp_dir: - ruleset = Path(tmp_dir) / "index.md" - ruleset.write_text( - f"# TSL 智能体规则\n\n{marker}\n\n", - encoding="utf-8", - newline="\n", - ) - - agents_text = build_agents_text(ruleset) - - self.assertEqual( - agents_text, - f"# TSL Agent Instructions\n\n{marker}\n", - ) - - def test_sync_workflow_does_not_remove_entire_target_branch(self): - text = SYNC_WORKFLOW.read_text(encoding="utf-8") - - self.assertNotRegex(text, r"git rm -rf --quiet\s+\.") - self.assertIn("managed_paths=(", text) - for path in ( - "AGENTS.md", - "docs/tsl", - "skills/tsl-syntax-reference", - "skills/tsl-api-reference", - ): - self.assertIn(f'"{path}"', text) - self.assertNotIn("generated_paths=(AGENTS.md docs skills)", text) - self.assertIn('rm -rf -- "${managed_paths[@]}"', text) - self.assertIn('git add -A -- "${managed_paths[@]}"', text) - self.assertNotIn('cp -R "$bundle"/. "$REPO_DIR"/', text) - self.assertNotIn(".gitea/ci/", text) - self.assertNotIn("https://oauth2", text) - self.assertNotIn("oauth2:${TOKEN}", text) - self.assertNotRegex(text, r"REPO_URL=.*(TOKEN|WORKFLOW)") - self.assertNotIn("git remote set-url", text) - self.assertIn("GIT_ASKPASS", text) - self.assertIn('REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"', text) - - def test_sync_preserves_files_outside_generated_paths(self): - if shutil.which("bash") is None: - self.skipTest("bash is required to run sync workflow script") - - with tempfile.TemporaryDirectory() as tmp_dir: - repo = create_source_repo(Path(tmp_dir)) - - git(repo, "checkout", "--orphan", "tsl-playbook") - git(repo, "rm", "-rf", ".") - unmanaged_files = { - "README.md": "manual branch note\n", - "docs/python/index.md": "manual python docs\n", - "skills/manual-skill/SKILL.md": "manual skill\n", - } - for relative, content in unmanaged_files.items(): - path = repo / relative - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8", newline="\n") - - stale_managed_files = ( - "docs/tsl/stale.md", - "skills/tsl-syntax-reference/stale.md", - "skills/tsl-api-reference/stale.md", - ) - for relative in stale_managed_files: - path = repo / relative - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("stale\n", encoding="utf-8", newline="\n") - - git(repo, "add", ".") - git(repo, "commit", "-m", "manual target branch content") - git(repo, "push", "-u", "origin", "tsl-playbook") - - git(repo, "checkout", "main") - run_sync(repo) - - for relative, expected in unmanaged_files.items(): - result = run( - ["git", "show", f"HEAD:{relative}"], - cwd=repo, - check=False, - ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertEqual(result.stdout, expected) - - for relative in stale_managed_files: - result = run( - ["git", "cat-file", "-e", f"HEAD:{relative}"], - cwd=repo, - check=False, - ) - self.assertNotEqual(result.returncode, 0, msg=relative) - - for path in ( - "AGENTS.md", - "docs/tsl/index.md", - "skills/tsl-syntax-reference/SKILL.md", - "skills/tsl-api-reference/SKILL.md", - ): - git(repo, "cat-file", "-e", f"HEAD:{path}") - - first_publish = git(repo, "rev-parse", "HEAD").stdout.strip() - git(repo, "checkout", "main") - run_sync(repo) - second_publish = git(repo, "rev-parse", "HEAD").stdout.strip() - self.assertEqual(second_publish, first_publish) - - def test_sync_creates_new_branch_without_source_files(self): - if shutil.which("bash") is None: - self.skipTest("bash is required to run sync workflow script") - - with tempfile.TemporaryDirectory() as tmp_dir: - repo = create_source_repo(Path(tmp_dir)) - - run_sync(repo) - - for path in ( - "AGENTS.md", - "docs/tsl/index.md", - "skills/tsl-syntax-reference/SKILL.md", - "skills/tsl-api-reference/SKILL.md", - ): - git(repo, "cat-file", "-e", f"HEAD:{path}") - - for path in ( - ".gitea/workflows/sync-tsl-playbook.yml", - "scripts/build_tsl_playbook.py", - "rulesets/tsl/index.md", - ): - result = run( - ["git", "cat-file", "-e", f"HEAD:{path}"], - cwd=repo, - check=False, - ) - self.assertNotEqual(result.returncode, 0, msg=f"{path} leaked") - - def test_sync_ignores_inherited_repo_dir_from_environment(self): - if shutil.which("bash") is None: - self.skipTest("bash is required to run sync workflow script") - - with tempfile.TemporaryDirectory() as tmp_dir: - tmp = Path(tmp_dir) - repo = create_source_repo(tmp) - - # Simulate the CI environment exporting REPO_DIR pointing at the - # real checkout. The sync must operate on the repo passed to - # run_sync, never the inherited value. - decoy = tmp / "decoy-checkout" - decoy.mkdir() - sentinel = decoy / "sentinel.txt" - sentinel.write_text("untouched\n", encoding="utf-8", newline="\n") - - previous = os.environ.get("REPO_DIR") - os.environ["REPO_DIR"] = str(decoy) - try: - run_sync(repo) - finally: - if previous is None: - os.environ.pop("REPO_DIR", None) - else: - os.environ["REPO_DIR"] = previous - - # The decoy must be left completely alone. - self.assertEqual( - sorted(item.name for item in decoy.iterdir()), - ["sentinel.txt"], - ) - self.assertEqual(sentinel.read_text(encoding="utf-8"), "untouched\n") - - # And the intended repo must actually have been published. - git(repo, "cat-file", "-e", "HEAD:AGENTS.md") - - -def create_source_repo(tmp: Path) -> Path: - repo = tmp / "repo" - remote = tmp / "remote.git" - - run(["git", "init", "--bare", str(remote)]) - run(["git", "init", str(repo)]) - git(repo, "checkout", "-b", "main") - git(repo, "config", "user.name", "test") - git(repo, "config", "user.email", "test@example.invalid") - - copy_required_sources(repo) - git(repo, "add", ".") - git(repo, "commit", "-m", "initial sources") - git(repo, "remote", "add", "origin", "../remote.git") - git(repo, "push", "-u", "origin", "main") - return repo - - -def run_sync(repo: Path) -> None: - env = os.environ.copy() - env.update( - { - "REPO_DIR": str(repo), - "TARGET_BRANCH": "tsl-playbook", - "COMMIT_AUTHOR_NAME": "test", - "COMMIT_AUTHOR_EMAIL": "test@example.invalid", - } - ) - script_path = repo / ".sync-test.sh" - script_path.write_text( - extract_sync_workflow_script(), encoding="utf-8", newline="\n" - ) - try: - result = subprocess.run( - ["bash", script_path.name], - cwd=repo, - env=env, - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - ) - finally: - script_path.unlink(missing_ok=True) - if result.returncode != 0: - raise AssertionError(result.stderr + result.stdout) - - -def copy_required_sources(repo: Path) -> None: - (repo / ".gitea" / "workflows").mkdir(parents=True) - shutil.copy2( - SYNC_WORKFLOW, repo / ".gitea" / "workflows" / "sync-tsl-playbook.yml" - ) - - (repo / "scripts").mkdir() - shutil.copy2(SCRIPT, repo / "scripts" / "build_tsl_playbook.py") - - (repo / "docs" / "tsl").mkdir(parents=True) - (repo / "docs" / "tsl" / "index.md").write_text( - "# TSL Index\n", encoding="utf-8", newline="\n" - ) - - syntax_skill = repo / "skills" / "tsl-syntax-reference" - (syntax_skill / "scripts").mkdir(parents=True) - (syntax_skill / "SKILL.md").write_text( - "---\nname: tsl-syntax-reference\n---\n", encoding="utf-8", newline="\n" - ) - (syntax_skill / "scripts" / "lookup.py").write_text( - "print('lookup')\n", encoding="utf-8", newline="\n" - ) - - api_skill = repo / "skills" / "tsl-api-reference" - (api_skill / "scripts").mkdir(parents=True) - (api_skill / "SKILL.md").write_text( - "---\nname: tsl-api-reference\n---\n", encoding="utf-8", newline="\n" - ) - (api_skill / "scripts" / "lookup.py").write_text( - "print('lookup')\n", encoding="utf-8", newline="\n" - ) - - syntax_skill = repo / "skills" / "tsl-syntax-reference" - (syntax_skill / "references").mkdir(parents=True) - (syntax_skill / "SKILL.md").write_text( - "---\nname: tsl-syntax-reference\n---\n", - encoding="utf-8", - newline="\n", - ) - (syntax_skill / "references" / "index.md").write_text( - "# TSL Syntax Reference\n", encoding="utf-8", newline="\n" - ) - - (repo / "rulesets" / "tsl").mkdir(parents=True) - (repo / "rulesets" / "tsl" / "index.md").write_text( - "# TSL Agent Instructions\n", encoding="utf-8", newline="\n" - ) - - -def count_files(path: Path) -> int: - return sum( - 1 - for item in path.rglob("*") - if item.is_file() - and "__pycache__" not in item.parts - and item.suffix != ".pyc" - ) - - -def extract_sync_workflow_script() -> str: - lines = SYNC_WORKFLOW.read_text(encoding="utf-8").splitlines() - in_sync_step = False - in_run_block = False - script_lines: list[str] = [] - - for line in lines: - if line.startswith(" - name: 📦 Build and publish tsl-playbook"): - in_sync_step = True - continue - if in_sync_step and line.startswith(" - name: "): - break - if in_sync_step and line == " run: |": - in_run_block = True - continue - if not in_run_block: - continue - if line.startswith(" "): - script_lines.append(line[10:]) - continue - if line.strip() == "": - script_lines.append("") - continue - break - - if not script_lines: - raise AssertionError("sync workflow run block was not found") - return "\n".join(script_lines) + "\n" - - -def git(repo: Path, *args: str) -> subprocess.CompletedProcess[str]: - return run(["git", *args], cwd=repo) - - -def run( - args: list[str], - cwd: Path | None = None, - check: bool = True, -) -> subprocess.CompletedProcess[str]: - result = subprocess.run( - args, - cwd=cwd, - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - ) - if check and result.returncode != 0: - raise AssertionError( - f"command failed: {' '.join(args)}\n{result.stderr}{result.stdout}" - ) - return result - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_deployment_routes_e2e.py b/test/test_deployment_routes_e2e.py deleted file mode 100644 index 4b610af9..00000000 --- a/test/test_deployment_routes_e2e.py +++ /dev/null @@ -1,211 +0,0 @@ -import shutil -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_DEPLOY_ROOT = Path("docs/standards/playbook") -CUSTOM_DEPLOY_ROOT = Path("custom/playbook") -REPO_COPY_IGNORE = shutil.ignore_patterns( - ".git", - ".venv", - "node_modules", - "__pycache__", - ".pytest_cache", - ".mypy_cache", - ".ruff_cache", -) - - -def run_script( - script: Path, *args: str, cwd: Path | None = None -) -> subprocess.CompletedProcess[str]: - return subprocess.run( - [sys.executable, str(script), *args], - capture_output=True, - text=True, - cwd=cwd, - ) - - -def write_config(root: Path, name: str, body: str) -> Path: - config_path = root / name - config_path.write_text(body.strip() + "\n", encoding="utf-8") - return config_path - - -def copy_repo(target: Path) -> Path: - shutil.copytree(ROOT, target, ignore=REPO_COPY_IGNORE) - return target - - -class DeploymentRoutesE2ETests(unittest.TestCase): - def assert_core_project_files(self, project_root: Path) -> None: - expected = [ - "AGENTS.md", - "AGENT_RULES.md", - "AGENT_RULES.local.md", - "memory-bank/progress.md", - "docs/prompts/system/agent-behavior.md", - ".agents/index.md", - ".agents/tsl/index.md", - ".agents/markdown/index.md", - ".gitattributes", - ] - for rel in expected: - with self.subTest(path=rel): - self.assertTrue((project_root / rel).exists(), f"missing: {rel}") - self.assertFalse((project_root / "docs" / "workflows").exists()) - - def assert_docs_prefix(self, project_root: Path, docs_prefix: str) -> None: - agents_index = (project_root / ".agents" / "index.md").read_text(encoding="utf-8") - self.assertIn("`.agents/tsl/index.md`", agents_index) - self.assertIn("`.agents/markdown/index.md`", agents_index) - self.assertIn(f"- {docs_prefix}", agents_index) - - tsl_index = (project_root / ".agents" / "tsl" / "index.md").read_text( - encoding="utf-8" - ) - self.assertIn("`tsl-syntax-reference`", tsl_index) - self.assertIn("`tsl-api-reference`", tsl_index) - for relative_path in ( - "tsl/naming.md", - "tsl/code_style.md", - "tsl/toolchain.md", - "tsl/modules/index.md", - ): - self.assertIn(f"`{docs_prefix}/{relative_path}`", tsl_index) - self.assertNotIn("`docs/tsl/index.md`", tsl_index) - - def test_subtree_style_deployment_syncs_project_files(self): - with tempfile.TemporaryDirectory() as tmp_dir: - tmp_root = Path(tmp_dir) - project_root = tmp_root / "project" - playbook_root = project_root / DEFAULT_DEPLOY_ROOT - project_root.mkdir() - copy_repo(playbook_root) - - config_path = write_config( - project_root, - "playbook.toml", - """ -[playbook] -project_root = "." -playbook_root = "docs/standards/playbook" -install_mode = "subtree" - -[sync_rules] -no_backup = true - -[sync_memory_bank] -project_name = "Demo" - -[sync_prompts] -no_backup = true - -[sync_standards] -langs = ["tsl", "markdown"] -no_backup = true -""", - ) - - result = run_script( - playbook_root / "scripts" / "playbook.py", - "-config", - str(config_path), - cwd=project_root, - ) - - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - self.assert_core_project_files(project_root) - self.assert_docs_prefix(project_root, "docs/standards/playbook/docs") - - claude_md = project_root / "CLAUDE.md" - self.assertTrue(claude_md.is_file()) - text = claude_md.read_text(encoding="utf-8") - self.assertIn("@AGENTS.md", text) - self.assertIn("@AGENT_RULES.md", text) - self.assertIn("", text) - - def test_external_clone_deployment_installs_snapshot_and_updates_claude(self): - with tempfile.TemporaryDirectory() as tmp_dir: - tmp_root = Path(tmp_dir) - external_clone = tmp_root / "playbook" - project_root = tmp_root / "project" - copy_repo(external_clone) - project_root.mkdir() - - claude_md = project_root / ".claude" / "CLAUDE.md" - claude_md.parent.mkdir() - claude_md.write_text("# Existing Claude\n\nKeep this.\n", encoding="utf-8") - - config_path = write_config( - project_root, - "playbook.toml", - f""" -[playbook] -project_root = "." -playbook_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}" -install_mode = "snapshot" - -[sync_rules] -no_backup = true - -[sync_memory_bank] -project_name = "Demo" - -[sync_prompts] -no_backup = true - -[sync_standards] -langs = ["tsl", "markdown"] -no_backup = true -""", - ) - - result = run_script( - external_clone / "scripts" / "playbook.py", - "-config", - str(config_path), - cwd=project_root, - ) - - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) - - snapshot_root = project_root / CUSTOM_DEPLOY_ROOT - self.assertTrue((snapshot_root / "SOURCE.md").is_file()) - self.assertTrue((snapshot_root / "scripts" / "playbook.py").is_file()) - self.assertTrue( - (snapshot_root / "templates" / "AGENTS.template.md").is_file() - ) - self.assertTrue( - (snapshot_root / "templates" / "AGENT_RULES.template.md").is_file() - ) - self.assertTrue((snapshot_root / "templates" / "README.md").is_file()) - self.assertTrue((snapshot_root / "templates" / "memory-bank").is_dir()) - self.assertTrue((snapshot_root / "templates" / "prompts").is_dir()) - self.assertTrue((snapshot_root / "skills").is_dir()) - self.assertFalse((snapshot_root / "codex").exists()) - - self.assert_core_project_files(project_root) - self.assert_docs_prefix(project_root, "custom/playbook/docs") - - rules_text = (project_root / "AGENT_RULES.md").read_text(encoding="utf-8") - self.assertIn( - "`custom/playbook/` 是 Playbook 模板/供应商目录", - rules_text, - ) - self.assertNotIn("{{PLAYBOOK_ROOT}}", rules_text) - - text = claude_md.read_text(encoding="utf-8") - self.assertIn("Keep this.", text) - self.assertIn("@../AGENTS.md", text) - self.assertIn("@../AGENT_RULES.md", text) - self.assertEqual(text.count(""), 1) - self.assertFalse((project_root / "CLAUDE.md").exists()) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_gitea_workflow_bootstrap.py b/test/test_gitea_workflow_bootstrap.py deleted file mode 100644 index a312be07..00000000 --- a/test/test_gitea_workflow_bootstrap.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -TEST_WORKFLOW = ROOT / ".gitea" / "workflows" / "test.yml" -STANDARDS_WORKFLOW = ROOT / ".gitea" / "workflows" / "standards-check.yml" -UPDATE_THIRDPARTY_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-skills.yml" -TEMPLATE_STANDARDS_WORKFLOW = ( - ROOT / "templates" / "ci" / "gitea" / ".gitea" / "workflows" / "standards-check.yml" -) - - -class GiteaWorkflowBootstrapTests(unittest.TestCase): - def test_repo_workflows_do_not_call_repo_local_prepare_script_before_checkout(self): - for workflow in (TEST_WORKFLOW, STANDARDS_WORKFLOW): - text = workflow.read_text(encoding="utf-8") - with self.subTest(workflow=workflow.name): - self.assertNotIn("bash .gitea/ci/prepare_repo.sh", text) - self.assertIn('git clone "$REPO_URL" "$REPO_DIR"', text) - self.assertIn('echo "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV"', text) - - def test_workflows_use_isolated_repo_dirs_per_job(self): - for workflow in ( - TEST_WORKFLOW, - STANDARDS_WORKFLOW, - UPDATE_THIRDPARTY_WORKFLOW, - TEMPLATE_STANDARDS_WORKFLOW, - ): - text = workflow.read_text(encoding="utf-8") - with self.subTest(workflow=workflow.name): - self.assertNotIn('REPO_DIR="${WORKSPACE_DIR}/${REPO_NAME}"', text) - self.assertNotIn('REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME"', text) - self.assertIn('mktemp -d', text) - self.assertTrue( - 'mkdir -p "$WORKSPACE_DIR"' in text - or 'mkdir -p "${{ env.WORKSPACE_DIR }}"' in text - ) - self.assertIn('echo "REPO_DIR=$REPO_DIR" >>', text) - self.assertIn("if: always()", text) - self.assertIn('rm -rf "$REPO_DIR"', text) - - def test_test_workflow_installs_tomli_for_python_template_validation(self): - text = TEST_WORKFLOW.read_text(encoding="utf-8") - self.assertIn("python3 -m pip install yamllint tomli", text) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_playbook.py b/test/test_playbook.py new file mode 100644 index 00000000..c50f3a6b --- /dev/null +++ b/test/test_playbook.py @@ -0,0 +1,221 @@ +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "playbook.py" +MODE_ROOTS = { + "snapshot": Path("custom/playbook"), + "subtree": Path("docs/standards/playbook"), +} + + +def run_playbook( + script: Path, config: Path, project_root: Path +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(script), "-config", str(config)], + cwd=project_root, + capture_output=True, + text=True, + ) + + +def copy_subtree_source(destination: Path) -> None: + destination.mkdir(parents=True) + shutil.copy2(ROOT / ".gitattributes", destination / ".gitattributes") + + for name in ("scripts", "templates"): + shutil.copytree( + ROOT / name, + destination / name, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), + ) + + (destination / "docs").mkdir() + for name in ("common", "tsl", "markdown"): + shutil.copytree(ROOT / "docs" / name, destination / "docs" / name) + + (destination / "rulesets").mkdir() + shutil.copy2( + ROOT / "rulesets" / "index.md", + destination / "rulesets" / "index.md", + ) + for name in ("tsl", "markdown"): + shutil.copytree( + ROOT / "rulesets" / name, + destination / "rulesets" / name, + ) + + (destination / "skills").mkdir() + shutil.copytree( + ROOT / "skills" / "style-cleanup", + destination / "skills" / "style-cleanup", + ) + + +def write_config(project_root: Path, install_mode: str, playbook_root: Path) -> Path: + config = project_root / "playbook.toml" + config.write_text( + f""" +[playbook] +project_root = "." +playbook_root = "{playbook_root.as_posix()}" +install_mode = "{install_mode}" + +[sync_rules] +date = "2026-01-01" +no_backup = true + +[sync_memory_bank] +project_name = "Demo" +no_backup = true + +[sync_prompts] +no_backup = true + +[sync_standards] +langs = ["tsl", "markdown"] +gitattr_mode = "overwrite" +no_backup = true + +[install_skills] +agents_home = ".test-agents" +mode = "list" +skills = ["style-cleanup"] +no_backup = true +""".lstrip(), + encoding="utf-8", + newline="\n", + ) + return config + + +def seed_custom_files(project_root: Path) -> None: + custom_memory = project_root / "memory-bank" / "custom.md" + custom_memory.parent.mkdir(parents=True) + custom_memory.write_text("custom memory\n", encoding="utf-8", newline="\n") + + custom_prompt = project_root / "docs" / "prompts" / "custom.md" + custom_prompt.parent.mkdir(parents=True) + custom_prompt.write_text("custom prompt\n", encoding="utf-8", newline="\n") + + (project_root / "CLAUDE.md").write_text( + "# Existing Claude\n\nKeep this.\n", + encoding="utf-8", + newline="\n", + ) + + +class PlaybookDeploymentTests(unittest.TestCase): + def test_playbook_deployment_modes(self): + for install_mode, playbook_root in MODE_ROOTS.items(): + with self.subTest(install_mode=install_mode): + with tempfile.TemporaryDirectory() as tmp_dir: + project_root = Path(tmp_dir) / "project" + project_root.mkdir() + + if install_mode == "subtree": + source_root = project_root / playbook_root + copy_subtree_source(source_root) + script = source_root / "scripts" / "playbook.py" + else: + source_root = ROOT + script = SCRIPT + + seed_custom_files(project_root) + config = write_config(project_root, install_mode, playbook_root) + + for run_number in (1, 2): + result = run_playbook(script, config, project_root) + self.assertEqual( + result.returncode, + 0, + msg=( + f"{install_mode} run {run_number} failed\n" + f"{result.stdout}{result.stderr}" + ), + ) + + expected_paths = ( + "AGENTS.md", + "AGENT_RULES.md", + "AGENT_RULES.local.md", + "CLAUDE.md", + ".gitattributes", + "memory-bank/project-brief.md", + "memory-bank/active-context.md", + "docs/prompts/system/agent-behavior.md", + ".agents/index.md", + ".agents/tsl/index.md", + ".agents/markdown/index.md", + ".test-agents/skills/style-cleanup/SKILL.md", + ) + missing = [ + path + for path in expected_paths + if not (project_root / path).exists() + ] + self.assertEqual(missing, []) + + self.assertEqual( + (project_root / "memory-bank/custom.md").read_text( + encoding="utf-8" + ), + "custom memory\n", + ) + self.assertEqual( + (project_root / "docs/prompts/custom.md").read_text( + encoding="utf-8" + ), + "custom prompt\n", + ) + + claude_text = (project_root / "CLAUDE.md").read_text( + encoding="utf-8" + ) + self.assertIn("Keep this.", claude_text) + self.assertEqual( + claude_text.count(""), 1 + ) + + docs_prefix = f"{playbook_root.as_posix()}/docs" + agents_index = (project_root / ".agents/index.md").read_text( + encoding="utf-8" + ) + self.assertIn(f"- {docs_prefix}", agents_index) + + installed_skill = ( + project_root + / ".test-agents/skills/style-cleanup/SKILL.md" + ).read_text(encoding="utf-8") + self.assertIn( + f"`{docs_prefix}/tsl/code_style.md`", installed_skill + ) + self.assertNotIn("`docs/tsl/code_style.md`", installed_skill) + + rules_text = (project_root / "AGENT_RULES.md").read_text( + encoding="utf-8" + ) + self.assertIn( + f"`{playbook_root.as_posix()}/` 是 Playbook 模板/供应商目录", + rules_text, + ) + + if install_mode == "snapshot": + snapshot_root = project_root / playbook_root + self.assertTrue((snapshot_root / "SOURCE.md").is_file()) + self.assertTrue( + (snapshot_root / "scripts/playbook.py").is_file() + ) + self.assertTrue((snapshot_root / "skills").is_dir()) + else: + self.assertFalse((source_root / "SOURCE.md").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_playbook_config_actions.py b/test/test_playbook_config_actions.py deleted file mode 100644 index 0297e946..00000000 --- a/test/test_playbook_config_actions.py +++ /dev/null @@ -1,437 +0,0 @@ -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "scripts" / "playbook.py" -SOURCE_GITATTR = ROOT / ".gitattributes" -DEFAULT_DEPLOY_ROOT = "docs/standards/playbook" - - -def run_cli(*args, cwd=None): - return subprocess.run( - [sys.executable, str(SCRIPT), *args], - capture_output=True, - text=True, - cwd=cwd, - ) - - -def run_script(script_path: Path, *args, cwd: Path | None = None): - return subprocess.run( - [sys.executable, str(script_path), *args], - capture_output=True, - text=True, - cwd=str(cwd) if cwd else None, - ) - - -def read_entries(path: Path) -> list[str]: - entries = [] - for line in path.read_text(encoding="utf-8").splitlines(): - stripped = line.strip() - if not stripped or stripped.startswith("#"): - continue - entries.append(stripped) - return entries - - -class PlaybookConfigActionTests(unittest.TestCase): - def _run_sync_standards(self, root: Path, mode: str) -> subprocess.CompletedProcess: - config_body = f""" -[playbook] -project_root = \"{root}\" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = [\"tsl\"] -gitattr_mode = \"{mode}\" -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - return run_cli("-config", str(config_path), cwd=root) - - def test_gitattr_mode_skip(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - sentinel = "*.keep text eol=lf\n" - (root / ".gitattributes").write_text(sentinel, encoding="utf-8") - - result = self._run_sync_standards(root, "skip") - self.assertEqual(result.returncode, 0) - self.assertEqual( - (root / ".gitattributes").read_text(encoding="utf-8"), - sentinel, - ) - - def test_gitattr_mode_overwrite(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - (root / ".gitattributes").write_text("bad\n", encoding="utf-8") - - result = self._run_sync_standards(root, "overwrite") - self.assertEqual(result.returncode, 0) - self.assertEqual( - (root / ".gitattributes").read_text(encoding="utf-8"), - SOURCE_GITATTR.read_text(encoding="utf-8"), - ) - - def test_gitattr_mode_block(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - result = self._run_sync_standards(root, "block") - self.assertEqual(result.returncode, 0) - content = (root / ".gitattributes").read_text(encoding="utf-8") - self.assertIn("# BEGIN playbook .gitattributes", content) - self.assertIn("# END playbook .gitattributes", content) - - def test_gitattr_mode_append(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - src_entries = read_entries(SOURCE_GITATTR) - (root / ".gitattributes").write_text( - src_entries[0] + "\n", encoding="utf-8" - ) - - result = self._run_sync_standards(root, "append") - self.assertEqual(result.returncode, 0) - content = (root / ".gitattributes").read_text(encoding="utf-8") - self.assertIn("Added from playbook .gitattributes", content) - - def test_sync_rules_no_backup_skips_backup_file(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - rules = root / "AGENT_RULES.md" - rules.write_text("old rules", encoding="utf-8") - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_rules] -force = true -no_backup = true -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0, msg=result.stderr) - - backups = list(root.glob("AGENT_RULES.md.bak.*")) - self.assertEqual(backups, []) - self.assertTrue(rules.is_file()) - - def test_sync_standards_no_backup_skips_agents_and_gitattributes_backup(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - agents = root / ".agents" / "tsl" - agents.mkdir(parents=True) - (agents / "index.md").write_text("old", encoding="utf-8") - - gitattributes = root / ".gitattributes" - gitattributes.write_text("*.txt text\n", encoding="utf-8") - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["tsl"] -gitattr_mode = "append" -no_backup = true -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0, msg=result.stderr) - - agents_backups = list((root / ".agents").glob("tsl.bak.*")) - self.assertEqual(agents_backups, []) - git_backups = list(root.glob(".gitattributes.bak.*")) - self.assertEqual(git_backups, []) - - def test_install_skills_no_backup_replaces_existing_skill_without_backup(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - skills_root = root / "agents" / "skills" - existing = skills_root / "brainstorming" - existing.mkdir(parents=True) - (existing / "stale.txt").write_text("old", encoding="utf-8") - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[install_skills] -agents_home = "{root / 'agents'}" -mode = "list" -skills = ["brainstorming"] -no_backup = true -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0, msg=result.stderr) - - backups = list(skills_root.glob("brainstorming.bak.*")) - self.assertEqual(backups, []) - self.assertFalse((existing / "stale.txt").exists()) - self.assertTrue((existing / "SKILL.md").is_file()) - - def test_sync_memory_bank_adds_missing_files_without_deleting_custom(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - memory_bank = root / "memory-bank" - memory_bank.mkdir(parents=True) - custom = memory_bank / "custom.md" - custom.write_text("custom", encoding="utf-8") - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_memory_bank] -project_name = "Demo" -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0, msg=result.stderr) - - self.assertTrue(custom.exists()) - self.assertTrue((memory_bank / "project-brief.md").is_file()) - - def test_sync_prompts_adds_missing_files_without_deleting_custom(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - prompts = root / "docs" / "prompts" - prompts.mkdir(parents=True) - custom = prompts / "custom.md" - custom.write_text("custom", encoding="utf-8") - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_prompts] -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0, msg=result.stderr) - - self.assertTrue(custom.exists()) - self.assertTrue((prompts / "system" / "agent-behavior.md").is_file()) - self.assertFalse((root / "docs" / "workflows").exists()) - - def test_sync_memory_bank_force_overwrites_template_files_only(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - memory_bank = root / "memory-bank" - memory_bank.mkdir(parents=True) - custom = memory_bank / "custom.md" - custom.write_text("custom", encoding="utf-8") - - brief = memory_bank / "project-brief.md" - brief.write_text("OLD", encoding="utf-8") - - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_memory_bank] -project_name = "Demo" -force = true -no_backup = true -""" - config_path = root / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0, msg=result.stderr) - - self.assertTrue(custom.exists()) - self.assertNotIn("OLD", brief.read_text(encoding="utf-8")) - backups = list(memory_bank.glob("project-brief.md.bak.*")) - self.assertEqual(backups, []) - - def test_sync_templates_replaces_playbook_scripts_without_main_language_support( - self, - ): - with tempfile.TemporaryDirectory() as tmp_dir: - config_body = f""" -[playbook] -project_root = \"{tmp_dir}\" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_rules] - -[sync_memory_bank] - -[sync_standards] -langs = [\"cpp\", \"tsl\"] -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0, msg=result.stderr) - - agents_md = Path(tmp_dir) / "AGENTS.md" - text = agents_md.read_text(encoding="utf-8") - self.assertIn(".agents/cpp/index.md", text) - self.assertNotIn("{{MAIN_LANGUAGE}}", text) - - tech_context = Path(tmp_dir) / "memory-bank" / "tech-context.md" - tech_context_text = tech_context.read_text(encoding="utf-8") - self.assertNotIn("{{LANGUAGE_1}}", tech_context_text) - self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_text) - self.assertNotIn("**主要语言**", tech_context_text) - - rules_md = Path(tmp_dir) / "AGENT_RULES.md" - rules_text = rules_md.read_text(encoding="utf-8") - self.assertIn( - "docs/standards/playbook/scripts/main_loop.py claim", - rules_text, - ) - self.assertIn( - "`docs/standards/playbook/` 是 Playbook 模板/供应商目录", - rules_text, - ) - self.assertIn( - "默认排除 `docs/standards/playbook/`", - rules_text, - ) - self.assertIn("docs/superpowers/plans", rules_text) - self.assertNotIn("plan_progress.py", rules_text) - self.assertNotIn("record-spec", rules_text) - self.assertIn( - "追加到 `memory-bank/progress.md` 的", - rules_text, - ) - self.assertIn("领取前不得进入 `$executing-plans`", rules_text) - self.assertIn( - "`$subagent-driven-development` 仅在 Plan 或平台明确要求时使用", - rules_text, - ) - self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text) - self.assertNotIn("{{PLAYBOOK_ROOT}}", rules_text) - self.assertFalse(rules_text.endswith("\n\n")) - - def test_sync_standards_rewrites_typescript_docs_prefix_for_snapshot_playbook(self): - with tempfile.TemporaryDirectory() as tmp_dir: - root = Path(tmp_dir) - install_config = root / "install.toml" - install_config.write_text( - f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["typescript"] -""", - encoding="utf-8", - ) - - install_result = run_cli("-config", str(install_config)) - self.assertEqual(install_result.returncode, 0, msg=install_result.stderr) - - sync_config = root / "sync.toml" - sync_config.write_text( - f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_standards] -langs = ["typescript"] -""", - encoding="utf-8", - ) - - snapshot_script = ( - root / "docs" / "standards" / "playbook" / "scripts" / "playbook.py" - ) - sync_result = run_script( - snapshot_script, "-config", str(sync_config), cwd=root - ) - self.assertEqual(sync_result.returncode, 0, msg=sync_result.stderr) - - agents_index = root / ".agents" / "typescript" / "index.md" - text = agents_index.read_text(encoding="utf-8") - self.assertIn("`docs/standards/playbook/docs/typescript/", text) - self.assertNotIn("`docs/typescript/", text) - - def test_sync_memory_bank_includes_active_context_and_human_readable_progress(self): - with tempfile.TemporaryDirectory() as tmp_dir: - config_body = f""" -[playbook] -project_root = "{tmp_dir}" -playbook_root = "{DEFAULT_DEPLOY_ROOT}" -install_mode = "snapshot" - -[sync_rules] - -[sync_memory_bank] -project_name = "MyProject" -""" - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text(config_body, encoding="utf-8") - - result = run_cli("-config", str(config_path)) - self.assertEqual(result.returncode, 0, msg=result.stderr) - - active_context = Path(tmp_dir) / "memory-bank" / "active-context.md" - self.assertTrue(active_context.is_file()) - - progress = Path(tmp_dir) / "memory-bank" / "progress.md" - progress_text = progress.read_text(encoding="utf-8") - self.assertIn("## Current Focus", progress_text) - self.assertIn("## 状态块示例", progress_text) - self.assertNotIn("phase: planning", progress_text) - self.assertNotIn("executor: executing-plans", progress_text) - self.assertNotIn("workflow-state", progress_text) - self.assertIn("## Plan Status", progress_text) - self.assertIn("", progress_text) - self.assertIn("", progress_text) - - system_patterns = Path(tmp_dir) / "memory-bank" / "system-patterns.md" - system_patterns_text = system_patterns.read_text(encoding="utf-8") - self.assertIn("# 系统模式与约束", system_patterns_text) - self.assertIn("## 核心不变量", system_patterns_text) - - agents_md = Path(tmp_dir) / "AGENTS.md" - agents_text = agents_md.read_text(encoding="utf-8") - self.assertIn("memory-bank/active-context.md", agents_text) - - rules_md = Path(tmp_dir) / "AGENT_RULES.md" - rules_text = rules_md.read_text(encoding="utf-8") - self.assertIn("memory-bank/active-context.md", rules_text) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_playbook_toml_parser.py b/test/test_playbook_toml_parser.py deleted file mode 100644 index 08322b85..00000000 --- a/test/test_playbook_toml_parser.py +++ /dev/null @@ -1,67 +0,0 @@ -import tempfile -import sys -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) - -from scripts import playbook - - -class PlaybookTomlParserTests(unittest.TestCase): - def test_minimal_parser_allows_dotted_section_name(self): - raw = """ -[a.b] -key = 1 -""" - data = playbook.loads_toml_minimal(raw) - self.assertIn("a.b", data) - self.assertEqual(data["a.b"]["key"], 1) - - def test_minimal_parser_rejects_multiline_string(self): - raw = '[section]\nvalue = """line1\nline2"""\n' - with self.assertRaises(ValueError): - playbook.loads_toml_minimal(raw) - - def test_load_config_preserves_windows_users_path_in_basic_string(self): - with tempfile.TemporaryDirectory() as tmp_dir: - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text( - '[playbook]\nproject_root = "C:\\Users\\demo\\workspace"\n', - encoding="utf-8", - ) - - data = playbook.load_config(config_path) - - self.assertEqual(data["playbook"]["project_root"], r"C:\Users\demo\workspace") - - def test_load_config_preserves_windows_escape_like_segments_for_path_keys(self): - with tempfile.TemporaryDirectory() as tmp_dir: - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text( - '[playbook]\nproject_root = "C:\\tmp\\notes"\n\n' - '[install_skills]\nagents_home = "D:\\new\\tab"\n', - encoding="utf-8", - ) - - data = playbook.load_config(config_path) - - self.assertEqual(data["playbook"]["project_root"], r"C:\tmp\notes") - self.assertEqual(data["install_skills"]["agents_home"], r"D:\new\tab") - - def test_load_config_keeps_already_escaped_windows_path(self): - with tempfile.TemporaryDirectory() as tmp_dir: - config_path = Path(tmp_dir) / "playbook.toml" - config_path.write_text( - '[playbook]\nproject_root = "C:\\\\Users\\\\demo\\\\workspace"\n', - encoding="utf-8", - ) - - data = playbook.load_config(config_path) - - self.assertEqual(data["playbook"]["project_root"], r"C:\Users\demo\workspace") - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_script_line_endings.py b/test/test_script_line_endings.py deleted file mode 100644 index 949b776b..00000000 --- a/test/test_script_line_endings.py +++ /dev/null @@ -1,79 +0,0 @@ -import ast -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -SCRIPT_PATHS = sorted((ROOT / "scripts").glob("*.py")) + sorted( - (ROOT / ".gitea" / "ci").glob("*.py") -) - - -class ScriptLineEndingTests(unittest.TestCase): - def test_script_text_writes_pin_lf_newlines(self): - offenders: list[str] = [] - for path in SCRIPT_PATHS: - tree = ast.parse(path.read_text(encoding="utf-8")) - for node in ast.walk(tree): - if not isinstance(node, ast.Call): - continue - if is_write_text_without_lf_newline(node): - offenders.append(f"{path.relative_to(ROOT)}:{node.lineno}") - elif is_text_open_for_writing_without_newline(node): - offenders.append(f"{path.relative_to(ROOT)}:{node.lineno}") - - self.assertEqual(offenders, []) - - -def has_lf_newline_keyword(node: ast.Call) -> bool: - newline = next( - (keyword for keyword in node.keywords if keyword.arg == "newline"), - None, - ) - return ( - newline is not None - and isinstance(newline.value, ast.Constant) - and newline.value.value == "\n" - ) - - -def is_write_text_without_lf_newline(node: ast.Call) -> bool: - func = node.func - return ( - isinstance(func, ast.Attribute) - and func.attr == "write_text" - and not has_lf_newline_keyword(node) - ) - - -def is_text_open_for_writing_without_newline(node: ast.Call) -> bool: - func = node.func - if isinstance(func, ast.Name): - is_open = func.id == "open" - elif isinstance(func, ast.Attribute): - is_open = func.attr == "open" - else: - is_open = False - if not is_open: - return False - - mode_node = None - if len(node.args) >= 2: - mode_node = node.args[1] - for keyword in node.keywords: - if keyword.arg == "mode": - mode_node = keyword.value - break - - if mode_node is None: - return False - if not isinstance(mode_node, ast.Constant) or not isinstance(mode_node.value, str): - return False - - mode = mode_node.value - writes_text = any(flag in mode for flag in ("w", "a", "x")) and "b" not in mode - return writes_text and not has_lf_newline_keyword(node) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_template_contracts.py b/test/test_template_contracts.py index d299a03f..2497bc00 100644 --- a/test/test_template_contracts.py +++ b/test/test_template_contracts.py @@ -6,9 +6,6 @@ ROOT = Path(__file__).resolve().parents[1] class TemplateContractsTests(unittest.TestCase): def test_project_templates_drop_legacy_language_placeholders(self): - example_text = (ROOT / "playbook.toml.example").read_text(encoding="utf-8") - self.assertNotIn("main_language", example_text) - templates_readme = (ROOT / "templates" / "README.md").read_text( encoding="utf-8" ) diff --git a/test/test_tsl_syntax_lookup.py b/test/test_tsl_syntax_lookup.py deleted file mode 100644 index 3414708f..00000000 --- a/test/test_tsl_syntax_lookup.py +++ /dev/null @@ -1,563 +0,0 @@ -import importlib.util -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "skills" / "tsl-syntax-reference" / "scripts" / "lookup.py" - -spec = importlib.util.spec_from_file_location("tsl_syntax_lookup", SCRIPT) -lookup = importlib.util.module_from_spec(spec) -spec.loader.exec_module(lookup) - - -NATURAL_LANGUAGE_CASES = ( - ("帮我写个最简单能跑的天软脚本", "write", "01_quickstart.md"), - ("脚本和可复用函数文件有什么区别", "explain", "02_core_model.md"), - ("字符串和数组下标从几开始", "explain", "03_values_and_literals.md"), - ("常量怎么声明,变量能不能直接赋值", "explain", "04_variables_and_constants.md"), - ("函数怎么带默认参数", "write", "05_functions_and_calls.md"), - ("赋值和相等比较分别怎么写", "explain", "06_expressions_and_operators.md"), - ("循环里满足条件就跳出去", "write", "07_control_flow.md"), - ("怎么定义类并创建对象", "write", "08_objects_and_classes.md"), - ("多个文件复用一组函数怎么组织", "write", "09_units_and_scope.md"), - ("临时切换系统参数再调用函数", "write", "10_runtime_context_and_with.md"), - ("为什么声明函数后面写代码会报错", "diagnose", "11_pitfalls.md"), - ("二维数组怎么判断某行存在", "write", "12_matrix_and_collections.md"), - ("二维结果按某一列保留匹配行", "write", "13_resultset_and_filters.md"), - ("数据库左连接后分组排序", "write", "14_ts_sql.md"), - ("程序慢怎么计时找瓶颈", "diagnose", "15_debug_and_profiler.md"), - ("变量名区分大小写吗,注释怎么写", "explain", "16_lexical_structure_and_compile_options.md"), - ("字符串转整数失败怎么办", "diagnose", "17_types_and_conversions.md"), - ("调用 DLL 并开线程", "write", "18_external_calls_and_threads.md"), - ("找不到 tsf 文件怎么改搜索路径", "diagnose", "19_namespace_libpath_and_unit_runtime.md"), - ("运行时怎么查看对象属于哪个类", "explain", "20_object_runtime_and_introspection.md"), - ("内存流怎么读写", "write", "21_builtin_runtime_objects.md"), - ("矩阵求逆和转置", "write", "22_matrix_deep_dive.md"), - ("高性能矩阵怎么排序", "write", "23_fmarray.md"), - ("让自定义对象支持下标和 for in", "write", "24_object_overloads_and_iteration.md"), -) - - -class TslSyntaxLookupTests(unittest.TestCase): - def test_natural_language_topic_matrix(self): - top1 = 0 - top5 = 0 - misses = [] - for query, mode, expected_page in NATURAL_LANGUAGE_CASES: - pages = [ - match.section.page.name - for match in lookup.query_sections(query, mode).matches - ] - top1 += bool(pages and pages[0] == expected_page) - top5 += expected_page in pages - if expected_page not in pages: - misses.append((query, expected_page, pages)) - - self.assertGreaterEqual(top1, 20, (top1, misses)) - self.assertEqual(top5, len(NATURAL_LANGUAGE_CASES), (top5, misses)) - - def test_short_ascii_tokens_use_identifier_boundaries(self): - result = lookup.query_sections("if", "explain") - ids = "\n".join(match.section.id for match in result.matches) - - self.assertIn("07_control_flow", ids) - self.assertNotIn("tinifile", ids) - self.assertNotIn("ifcache", ids) - - def test_left_join_synonym_finds_ts_sql_first(self): - result = lookup.query_sections("数据库左连接", "write") - - self.assertEqual(result.matches[0].section.page.name, "14_ts_sql.md") - self.assertTrue( - any(reason.startswith("synonym=") for reason in result.matches[0].reasons) - ) - - def test_synonym_reason_only_appears_when_expansion_matches_candidate(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - (references / "sample.md").write_text( - "# Sample\n\n" - "## 本篇职责\n\n" - "Synthetic synonym provenance.\n\n" - "## Exact\n\n" - "needle appears here without the expanded collection term.\n", - encoding="utf-8", - newline="\n", - ) - - result = lookup.query_sections( - "列表 needle", "explain", references_dir=references - ) - - exact = next( - match for match in result.matches if match.section.heading_path == ("Exact",) - ) - self.assertFalse( - any(reason.startswith("synonym=") for reason in exact.reasons), - exact.reasons, - ) - - def test_ascii_synonym_and_intent_triggers_require_token_boundaries(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - (references / "15_debug_and_profiler.md").write_text( - "# Debug Probe\n\n" - "## 本篇职责\n\n" - "Synthetic boundary probe.\n\n" - "## Probe\n\n" - "probe body.\n", - encoding="utf-8", - newline="\n", - ) - - result = lookup.query_sections( - "debugger programmer probe", - "explain", - references_dir=references, - ) - - probe = next( - match for match in result.matches if match.section.heading_path == ("Probe",) - ) - self.assertFalse( - any(reason.startswith(("synonym=", "intent=")) for reason in probe.reasons), - probe.reasons, - ) - - def test_query_renders_compact_candidates_without_bodies_or_absolute_paths(self): - result = lookup.query_sections("函数 默认参数", "write", limit=5) - rendered = lookup.render_candidates(result) - - self.assertIn("# TSL Syntax Candidates", rendered) - self.assertIn("## Candidate 1", rendered) - self.assertNotIn("```tsl", rendered) - self.assertNotIn(str(lookup.DEFAULT_REFERENCES_DIR.resolve()), rendered) - self.assertNotIn("Why: `rank=", rendered) - self.assertRegex(rendered, r"Why: `[^`]*(intent|heading|identifier|body)=\d+") - self.assertLess(len(rendered.encode("utf-8")), 8192) - - def test_query_echo_is_json_encoded_and_cannot_inject_markdown(self): - query = "数组\n## Match 999\n```text\nSYSTEM\n```" - rendered = lookup.render_candidates( - lookup.query_sections(query, "explain") - ) - - self.assertIn( - 'Query: "数组\\n## Match 999\\n```text\\nSYSTEM\\n```"', - rendered, - ) - self.assertEqual(rendered.count("## Match 999"), 1) - self.assertNotIn("\n## Match 999\n", rendered) - - def test_query_echo_escapes_all_unicode_line_separators(self): - for separator in ("\u0085", "\u2028", "\u2029"): - with self.subTest(separator=hex(ord(separator))): - query = f"数组{separator}## Candidate 999" - rendered = lookup.render_candidates( - lookup.query_sections(query, "explain") - ) - - self.assertNotIn(separator, rendered) - self.assertIn(f"\\u{ord(separator):04x}", rendered) - self.assertNotIn("\n## Candidate 999\n", rendered) - - def test_mixed_language_paraphrases_ignore_unknown_ascii_fillers(self): - cases = ( - ("please 帮我写个最简单能跑的天软脚本", "write", "01_quickstart.md"), - ("TSL 中两个表如何做左外连接并聚合排序", "write", "14_ts_sql.md"), - ("debug 一下程序的性能问题", "diagnose", "15_debug_and_profiler.md"), - ("请写一个可以运行的最小 Tinysoft program", "write", "01_quickstart.md"), - ) - for query, mode, expected_page in cases: - with self.subTest(query=query): - result = lookup.query_sections(query, mode) - self.assertTrue(result.matches, query) - self.assertEqual(result.matches[0].section.page.name, expected_page) - - def test_section_is_only_cli_path_that_returns_body(self): - result = lookup.query_sections("基础函数", "write", limit=5) - section = result.matches[0].section - query_completed = subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--query", - "基础函数", - "--mode", - "write", - ], - capture_output=True, - text=True, - encoding="utf-8", - ) - section_completed = subprocess.run( - [sys.executable, str(SCRIPT), "--section", section.id], - capture_output=True, - text=True, - encoding="utf-8", - ) - - self.assertEqual(query_completed.returncode, 0) - self.assertEqual(section_completed.returncode, 0) - self.assertNotIn(section.body.rstrip(), query_completed.stdout) - self.assertIn(section.body.rstrip(), section_completed.stdout) - - def test_body_only_match_prefers_deepest_section(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - (references / "sample.md").write_text( - "# Sample\n\n" - "## 本篇职责\n\n" - "Synthetic hierarchy.\n\n" - "## Parent\n\n" - "Parent introduction.\n\n" - "### Child\n\n" - "Child introduction.\n\n" - "#### Exact Leaf\n\n" - "needle appears only in the leaf body.\n", - encoding="utf-8", - newline="\n", - ) - - result = lookup.query_sections( - "needle", "explain", references_dir=references - ) - - self.assertEqual(result.matches[0].section.heading_path[-1], "Exact Leaf") - - def test_limit_applies_to_final_rendered_candidate_list(self): - for limit in (1, 2, 5): - with self.subTest(limit=limit): - result = lookup.query_sections("函数 默认参数", "write", limit=limit) - rendered = lookup.render_candidates(result) - self.assertEqual(rendered.count("## Candidate "), limit) - - def test_required_context_stays_required_when_query_also_matches_it(self): - result = lookup.query_sections("语言核心事实速查", "write", limit=5) - rendered = lookup.render_candidates(result) - required_id = "01_quickstart--语言核心事实速查" - - self.assertIn( - f"Required: yes\nSection ID: `{required_id}`", - rendered, - ) - self.assertEqual(rendered.count(f"Section ID: `{required_id}`"), 1) - - def test_displayed_scores_are_sorted_descending(self): - for query, mode in ( - ("函数 默认参数", "write"), - ("变量怎么改", "write"), - ("数组", "diagnose"), - ("invalid statement 声明区", "diagnose"), - ("程序慢怎么计时找瓶颈", "diagnose"), - ): - with self.subTest(query=query, mode=mode): - result = lookup.query_sections(query, mode, limit=5) - scores = [match.score for match in result.matches] - - self.assertEqual(scores, sorted(scores, reverse=True)) - - def test_parser_creates_unique_h2_h3_h4_section_ids(self): - sections = lookup.load_sections(lookup.DEFAULT_REFERENCES_DIR) - ids = [section.id for section in sections] - self.assertEqual(len(ids), len(set(ids))) - self.assertTrue(any("05_functions_and_calls" in value for value in ids)) - - def test_parser_indexes_h4_pitfall_sections(self): - sections = lookup.load_sections(lookup.DEFAULT_REFERENCES_DIR) - - self.assertTrue(any(len(section.heading_path) == 3 for section in sections)) - self.assertTrue(any("把-当成赋值" in section.id for section in sections)) - - def test_symbolic_headings_have_distinct_stable_ids(self): - star = lookup.section_id("sample.md", ("Examples", "with *")) - double_star = lookup.section_id("sample.md", ("Examples", "with **")) - - self.assertNotEqual(star, double_star) - self.assertNotRegex(double_star, r"-2$") - - def test_all_promised_symbolic_heading_slugs_are_distinct(self): - ids = { - lookup.section_id("sample.md", ("Examples", heading)) - for heading in ("with *", "with **", "operator[]", "walk ::", "walk :.") - } - - self.assertEqual(len(ids), 5) - - def test_check_requires_one_nonempty_duty_section_per_page(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - (references / "missing.md").write_text( - "# Missing\n\n## Rules\n\nText.\n", - encoding="utf-8", - newline="\n", - ) - (references / "duplicate.md").write_text( - "# Duplicate\n\n" - "## 本篇职责\n\n" - "One.\n\n" - "## 本篇职责\n\n" - "Two.\n", - encoding="utf-8", - newline="\n", - ) - - problems = lookup.validate_references(references) - - messages = "\n".join(problem.message for problem in problems) - self.assertIn("必须有且仅有一个非空「本篇职责」", messages) - - def test_check_rejects_h2_h3_h4_heading_level_jumps(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - (references / "sample.md").write_text( - "# Sample\n\n" - "## 本篇职责\n\n" - "Summary.\n\n" - "#### Skipped H3\n\n" - "Details.\n", - encoding="utf-8", - newline="\n", - ) - - problems = lookup.validate_references(references) - - self.assertTrue( - any("标题层级跳跃" in problem.message for problem in problems), problems - ) - - def test_write_mode_includes_file_model_and_direct_example(self): - result = lookup.query_sections("写函数 命名参数", "write", limit=4) - rendered = lookup.render_candidates(result) - - self.assertTrue( - any("文件模型" in " > ".join(section.heading_path) for section in result.prelude) - ) - self.assertTrue( - any("可直接照写示例" in match.section.identities for match in result.matches) - ) - self.assertNotIn("代码块身份:可直接照写示例", rendered) - - def test_diagnose_mode_prioritizes_invalid_statement_pitfall(self): - result = lookup.query_sections("invalid statement 声明区", "diagnose", limit=4) - self.assertIn("11_pitfalls.md", result.matches[0].section.page.as_posix()) - - def test_explain_mode_does_not_force_write_prelude(self): - result = lookup.query_sections("数组下标", "explain", limit=3) - self.assertEqual(result.prelude, []) - - def test_no_match_returns_exit_code_two_in_every_mode(self): - query = "夔魍魉xyzqv" - for mode in ("write", "diagnose", "explain"): - with self.subTest(mode=mode): - completed = subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--query", - query, - "--mode", - mode, - ], - capture_output=True, - text=True, - encoding="utf-8", - ) - self.assertEqual(completed.returncode, 2) - self.assertIn("# TSL Syntax Candidates", completed.stdout) - self.assertIn(f'Query: "{query}"', completed.stdout) - self.assertIn("no matching sections", completed.stderr) - - def test_missing_section_returns_nearest_section_ids(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - (references / "sample.md").write_text( - "# Sample\n\n## Exact Section\n\nDetails.\n\n## Other\n\nMore.\n", - encoding="utf-8", - newline="\n", - ) - expected_id = lookup.load_sections(references)[0].id - completed = subprocess.run( - [ - sys.executable, - str(SCRIPT), - "--section", - f"{expected_id}-typo", - "--references-dir", - str(references), - ], - capture_output=True, - text=True, - encoding="utf-8", - ) - - self.assertEqual(completed.returncode, 2) - self.assertIn("Nearest section IDs:", completed.stderr) - self.assertIn(f"- {expected_id}\n", completed.stderr) - - def test_write_identifier_query_keeps_required_context(self): - result = lookup.query_sections("varByRef 命名参数", "write", limit=5) - prelude_pages = {section.page.name for section in result.prelude} - self.assertEqual(prelude_pages, {"01_quickstart.md", "02_core_model.md"}) - - def test_write_prelude_renders_source_for_every_section(self): - result = lookup.query_sections("varByRef 命名参数", "write", limit=5) - rendered = lookup.render_candidates(result) - required_candidates = rendered.split("Required: no", 1)[0] - self.assertEqual(required_candidates.count("Source: `"), len(result.prelude)) - for section in result.prelude: - self.assertIn( - f"Source: `references/{section.page.name}`", required_candidates - ) - - def test_check_rejects_unknown_code_block_identity(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - (references / "sample.md").write_text( - "# Sample\n\n" - "## Example\n\n" - "代码块身份:未知身份\n\n" - "```tsl\n" - "return 1;\n" - "```\n", - encoding="utf-8", - newline="\n", - ) - - problems = lookup.validate_references(references) - - self.assertTrue( - any("未知身份" in problem.message for problem in problems), problems - ) - - def test_parser_and_validator_reject_prose_between_identity_and_fence(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - page = references / "sample.md" - page.write_text( - "# Sample\n\n" - "## Example\n\n" - "代码块身份:可直接照写示例\n" - "This arbitrary prose breaks the metadata association.\n\n" - "```tsl\n" - "return 1;\n" - "```\n", - encoding="utf-8", - newline="\n", - ) - - sections = lookup.load_sections(references) - problems = lookup.validate_references(references) - - self.assertEqual(sections[0].identities, ()) - self.assertTrue(any("恰好一个代码块身份" in item.message for item in problems)) - - def test_parser_and_validator_accept_structured_block_description(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - page = references / "sample.md" - page.write_text( - "# Sample\n\n" - "## 本篇职责\n\n" - "Structured metadata sample.\n\n" - "## Example\n\n" - "代码块身份:配置片段 / 概念骨架\n" - "代码块说明:This is structured metadata.\n\n" - "```text\n" - "example\n" - "```\n", - encoding="utf-8", - newline="\n", - ) - - sections = lookup.load_sections(references) - problems = lookup.validate_references(references) - - example = next( - section for section in sections if section.heading_path == ("Example",) - ) - self.assertEqual(example.identities, ("配置片段 / 概念骨架",)) - self.assertEqual(problems, []) - - def test_check_accumulates_fence_identity_and_link_problems(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - (references / "sample.md").write_text( - "# Sample\n\n" - "## Example\n\n" - "[missing](missing.md)\n\n" - "代码块身份:输出片段\n" - "代码块身份:可直接照写示例\n\n" - "```tsl\n" - "return 1;\n", - encoding="utf-8", - newline="\n", - ) - - problems = lookup.validate_references(references) - - messages = "\n".join(problem.message for problem in problems) - self.assertIn("本地链接不存在", messages) - self.assertIn("代码围栏未闭合", messages) - self.assertIn("恰好一个代码块身份", messages) - - def test_check_ignores_markdown_link_shapes_inside_inline_code(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - (references / "sample.md").write_text( - "# Sample\n\n" - "## 本篇职责\n\n" - "Inline-code link sample.\n\n" - "## Operators\n\n" - "Use `function operator[](index);`.\n", - encoding="utf-8", - newline="\n", - ) - - problems = lookup.validate_references(references) - - self.assertEqual(problems, []) - - def test_natural_chinese_query_finds_object_creation(self): - result = lookup.query_sections("写一个类并创建对象", "write", limit=5) - sources = "\n".join(match.section.id for match in result.matches) - self.assertIn("08_objects_and_classes", sources) - - def test_ranking_tiers_beat_adversarial_page_order(self): - with tempfile.TemporaryDirectory() as tmp_dir: - references = Path(tmp_dir) - pages = { - "a_body.md": "# Other\n\n## Other\n\nneedle appears as plain prose.\n", - "b_title.md": "# needle\n\n## Other\n\nNo body match.\n", - "c_identifier.md": "# Other\n\n## Other\n\nUse `needle` here.\n", - "z_heading.md": "# Other\n\n## needle\n\nNo body detail.\n", - } - for name, text in pages.items(): - (references / name).write_text( - text, encoding="utf-8", newline="\n" - ) - - result = lookup.query_sections( - "needle", "explain", limit=4, references_dir=references - ) - - self.assertEqual( - [match.section.page.name for match in result.matches], - ["z_heading.md", "c_identifier.md", "b_title.md", "a_body.md"], - ) - - def test_tsl_identifier_is_preserved(self): - result = lookup.query_sections("varByRef 命名参数", "explain", limit=5) - searchable_matches = "\n".join( - match.section.searchable_text for match in result.matches - ) - - self.assertRegex(searchable_matches, r"(?i)varByRef") - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_tsl_syntax_reference_skill.py b/test/test_tsl_syntax_reference_skill.py deleted file mode 100644 index 8f8613b4..00000000 --- a/test/test_tsl_syntax_reference_skill.py +++ /dev/null @@ -1,241 +0,0 @@ -import importlib.util -import re -import unittest -from pathlib import Path, PurePosixPath - - -ROOT = Path(__file__).resolve().parents[1] -SKILL_DIR = ROOT / "skills" / "tsl-syntax-reference" -SKILL_FILE = SKILL_DIR / "SKILL.md" -REFERENCES_DIR = SKILL_DIR / "references" -RULESET_FILE = ROOT / "rulesets" / "tsl" / "index.md" -LOOKUP_SCRIPT = SKILL_DIR / "scripts" / "lookup.py" - -lookup_spec = importlib.util.spec_from_file_location( - "tsl_syntax_reference_structure_lookup", LOOKUP_SCRIPT -) -lookup = importlib.util.module_from_spec(lookup_spec) -lookup_spec.loader.exec_module(lookup) - -TOPIC_REFERENCES = { - "01_quickstart.md", - "02_core_model.md", - "03_values_and_literals.md", - "04_variables_and_constants.md", - "05_functions_and_calls.md", - "06_expressions_and_operators.md", - "07_control_flow.md", - "08_objects_and_classes.md", - "09_units_and_scope.md", - "10_runtime_context_and_with.md", - "11_pitfalls.md", - "12_matrix_and_collections.md", - "13_resultset_and_filters.md", - "14_ts_sql.md", - "15_debug_and_profiler.md", - "16_lexical_structure_and_compile_options.md", - "17_types_and_conversions.md", - "18_external_calls_and_threads.md", - "19_namespace_libpath_and_unit_runtime.md", - "20_object_runtime_and_introspection.md", - "21_builtin_runtime_objects.md", - "22_matrix_deep_dive.md", - "23_fmarray.md", - "24_object_overloads_and_iteration.md", -} -MARKDOWN_LINK = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)") -OPERATIONAL_TOPIC_LINK = re.compile( - r"(?:跳到|跳转到|转到|回到|回看|回(?=\s*\[)|进入|移到)" - r"(?=[^。\n]*\[[^\]]+\]\((?:0[1-9]|1[0-9]|2[0-4])_[^)]+\.md(?:#[^)]+)?\))" -) - - -def read_text(path: Path) -> str: - return path.read_text(encoding="utf-8") - - -def frontmatter(path: Path) -> dict[str, str]: - text = read_text(path) - match = re.match(r"\A---\s*\n(.*?)\n---\s*\n", text, re.DOTALL) - if not match: - raise AssertionError(f"missing YAML frontmatter: {path}") - fields: dict[str, str] = {} - for line in match.group(1).splitlines(): - key, separator, value = line.partition(":") - if separator: - fields[key.strip()] = value.strip().strip("\"'") - return fields - - -def local_link_targets(text: str) -> list[str]: - text = re.sub(r"```.*?```", "", text, flags=re.DOTALL) - text = re.sub(r"`[^`\n]*`", "", text) - targets = [] - for match in MARKDOWN_LINK.finditer(text): - target = match.group(1).strip().split(maxsplit=1)[0].strip("<>") - if target.startswith(("#", "http://", "https://", "mailto:")): - continue - targets.append(target) - return targets - - -class TslSyntaxReferenceSkillStructureTest(unittest.TestCase): - def test_skill_discovery_covers_natural_tinysoft_terms(self) -> None: - description = frontmatter(SKILL_FILE)["description"] - - for term in ("TSL", "TSF", "TS-SQL", "Tinysoft", "天软", "脚本"): - self.assertIn(term, description) - - def test_skill_requires_safe_two_stage_lookup(self) -> None: - text = read_text(SKILL_FILE) - - self.assertRegex(text, r"--query[^\n]*(候选|Section ID)") - self.assertRegex(text, r"--section[^\n]*(正文|事实|章节)") - self.assertRegex(text, r"shell|命令注入|安全传参|原样拼接") - self.assertRegex(text, r"询问[^\n]*运行|如何运行") - self.assertIn("AGENTS.md", text) - self.assertRegex(text, r"可直接照写[^\n]*(不等于|不代表)[^\n]*(验证|可用)") - - def test_concept_map_is_plain_text_without_navigation_or_code(self) -> None: - rendered = lookup.render_concept_map(lookup.build_concept_map()) - - self.assertIsNone(re.search(r"\[[^\]]+\]\([^)]+\)", rendered)) - self.assertNotIn("```", rendered) - self.assertNotIn("`", rendered) - - def test_high_risk_reference_pages_defer_api_scope(self) -> None: - for name in ( - "10_runtime_context_and_with.md", - "15_debug_and_profiler.md", - "22_matrix_deep_dive.md", - ): - text = read_text(REFERENCES_DIR / name) - with self.subTest(page=name): - self.assertIn("tsl-api-reference", text) - self.assertRegex(text, r"签名|参数") - self.assertRegex(text, r"scope|环境|解释器|可用性") - - def test_local_link_targets_includes_local_images(self) -> None: - text = "![local diagram](images/router.png)\n![remote](https://example.com/router.png)" - self.assertEqual(local_link_targets(text), ["images/router.png"]) - - def test_skill_name_matches_directory(self) -> None: - self.assertEqual(frontmatter(SKILL_FILE)["name"], SKILL_DIR.name) - - def test_frontmatter_description_covers_tsl_and_tsf_tasks(self) -> None: - description = frontmatter(SKILL_FILE)["description"] - self.assertRegex(description, r"(?i)\bTSL\b") - self.assertRegex(description, r"(?i)\bTSF\b") - self.assertRegex(description, r"写|编写|修改|审查|解释|语法错误") - - def test_reference_inventory_contains_topics_without_router(self) -> None: - actual = {path.name for path in REFERENCES_DIR.iterdir() if path.is_file()} - self.assertEqual(actual, TOPIC_REFERENCES) - self.assertFalse((REFERENCES_DIR / "index.md").exists()) - - def test_lookup_script_is_bundled(self) -> None: - self.assertTrue((SKILL_DIR / "scripts" / "lookup.py").is_file()) - - def test_manual_router_protocol_is_absent(self) -> None: - corpus = read_text(SKILL_FILE) + "\n" + "\n".join( - read_text(path) for path in REFERENCES_DIR.glob("*.md") - ) - for forbidden in ("路由中心", "选择一个主专题", "候选页继续判断"): - self.assertNotIn(forbidden, corpus) - - def test_topic_pages_do_not_instruct_manual_navigation(self) -> None: - for path in REFERENCES_DIR.glob("*.md"): - for line_number, line in enumerate(read_text(path).splitlines(), start=1): - with self.subTest(path=path.name, line=line_number): - self.assertIsNone(OPERATIONAL_TOPIC_LINK.search(line), line) - - def test_json_router_is_absent(self) -> None: - self.assertFalse((REFERENCES_DIR / "00_agent_index.json").exists()) - self.assertFalse((ROOT / "docs" / "tsl" / "syntax" / "00_agent_index.json").exists()) - - def test_references_do_not_escape_skill(self) -> None: - for path in REFERENCES_DIR.glob("*.md"): - for target in local_link_targets(read_text(path)): - link_path = target.split("#", 1)[0].replace("\\", "/") - self.assertNotIn("docs/tsl/", link_path, f"{path}: {target}") - self.assertFalse(link_path.startswith("/"), f"{path}: {target}") - self.assertNotIn("..", PurePosixPath(link_path).parts, f"{path}: {target}") - - def test_reference_markdown_links_resolve(self) -> None: - for path in REFERENCES_DIR.glob("*.md"): - for target in local_link_targets(read_text(path)): - link_path = target.split("#", 1)[0] - self.assertTrue((REFERENCES_DIR / link_path).is_file(), f"{path}: {target}") - - def test_skill_uses_lookup_as_its_only_retrieval_entry(self) -> None: - text = read_text(SKILL_FILE) - self.assertIn("scripts/lookup.py", text) - for mode in ("write", "diagnose", "explain"): - self.assertIn(f"--mode {mode}", text) - self.assertEqual(local_link_targets(text), []) - self.assertNotIn("references/index.md", text) - - def test_skill_does_not_bundle_external_domains(self) -> None: - forbidden = { - "naming.md", - "code_style.md", - "toolchain.md", - "modules", - "tsl-api-reference", - "00_agent_index.json", - } - actual = {path.name for path in SKILL_DIR.rglob("*")} - self.assertTrue(forbidden.isdisjoint(actual), forbidden & actual) - - def test_ruleset_file_remains_tsl_router(self) -> None: - self.assertTrue(RULESET_FILE.is_file()) - text = read_text(RULESET_FILE) - for route in ( - "tsl-syntax-reference", - "tsl-api-reference", - "naming.md", - "code_style.md", - "toolchain.md", - "modules/index.md", - "项目", - ): - self.assertIn(route, text) - - def test_ruleset_routes_both_tsl_skills(self) -> None: - text = read_text(RULESET_FILE) - self.assertRegex(text, r"语法[^\n]*tsl-syntax-reference|tsl-syntax-reference[^\n]*语法") - self.assertRegex(text, r"API|函数") - self.assertIn("tsl-api-reference", text) - - def test_ruleset_syntax_constraints_moved_to_skill(self) -> None: - ruleset = read_text(RULESET_FILE) - owner = read_text(SKILL_FILE) + "\n" + "\n".join( - read_text(path) for path in REFERENCES_DIR.glob("*.md") - ) - self.assertRegex(ruleset, r"缺失|不可用") - self.assertRegex(ruleset, r"停止|阻断") - for forbidden in ( - "```tsl", - "代码块身份", - ".tsl` / `.tsf` 后缀由用户指定时", - ".tsl` 代码需要本文件内函数或类时", - ): - self.assertNotIn(forbidden, ruleset) - self.assertIn("代码块身份", owner) - self.assertIn(".tsl", owner) - self.assertIn(".tsf", owner) - - def test_static_docs_route_to_fact_owner(self) -> None: - routes = { - ROOT / "docs" / "tsl" / "naming.md": "../../skills/tsl-syntax-reference/SKILL.md", - ROOT / "docs" / "tsl" / "code_style.md": "../../skills/tsl-syntax-reference/SKILL.md", - } - legacy_deep_link = re.compile(r"(?:docs/tsl/)?syntax/(?:0[1-9]|1[0-9]|2[0-4])_") - for path, expected_link in routes.items(): - text = read_text(path) - self.assertIn(expected_link, text, path) - self.assertIsNone(legacy_deep_link.search(text), path) - - -if __name__ == "__main__": - unittest.main()