📦 deps(playbook): sync playbook snapshot
This commit is contained in:
@@ -1,51 +1,54 @@
|
||||
# 🧪 Playbook 测试套件
|
||||
|
||||
本目录包含 Playbook 项目的完整测试框架,用于验证脚本、模板和文档的正确性。
|
||||
本目录包含 Playbook 项目的测试,用于验证 CLI、模板与文档链接。
|
||||
|
||||
## 📋 目录结构
|
||||
|
||||
```txt
|
||||
tests/
|
||||
├── README.md # 本文件:测试文档
|
||||
├── scripts/ # Shell 脚本测试(bats)
|
||||
│ ├── test_sync_standards.bats # sync_standards.sh 测试
|
||||
│ ├── test_vendor_playbook.bats # vendor_playbook.sh 测试
|
||||
│ └── test_install_codex_skills.bats # install_codex_skills.sh 测试
|
||||
├── cli/ # Python CLI 测试(unittest)
|
||||
│ └── test_playbook_cli.py # playbook.py 基础功能测试
|
||||
├── test_format_md_action.py # format_md 动作测试
|
||||
├── test_gitattributes_modes.py # gitattr_mode 行为测试
|
||||
├── test_sync_directory_actions.py # sync_memory_bank/sync_prompts 行为测试
|
||||
├── test_vendor_snapshot_templates.py # vendor 快照模板完整性测试
|
||||
├── test_plan_progress_cli.py # plan_progress CLI 测试
|
||||
├── test_superpowers_list_sync.py # superpowers 列表一致性测试
|
||||
├── test_sync_templates_placeholders.py # 占位符替换测试(sync_rules/sync_standards)
|
||||
├── test_toml_edge_cases.py # TOML 解析边界测试
|
||||
├── templates/ # 模板验证测试
|
||||
│ ├── validate_python_templates.sh # Python 模板验证
|
||||
│ ├── validate_cpp_templates.sh # C++ 模板验证
|
||||
│ └── validate_ci_templates.sh # CI 模板验证
|
||||
│ ├── validate_ci_templates.sh # CI 模板验证
|
||||
│ └── validate_project_templates.sh # 项目模板验证
|
||||
└── integration/ # 集成测试
|
||||
└── check_doc_links.sh # 文档链接有效性检查
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 本地运行所有测试
|
||||
|
||||
```bash
|
||||
# 进入 playbook 根目录
|
||||
cd /path/to/playbook
|
||||
|
||||
# 1. 运行 Shell 脚本测试(需要 bats)
|
||||
sudo apt-get install bats # Ubuntu/Debian
|
||||
cd tests/scripts
|
||||
bats test_sync_standards.bats
|
||||
bats test_vendor_playbook.bats
|
||||
bats test_install_codex_skills.bats
|
||||
# 1. 运行 Python CLI 测试
|
||||
python -m unittest discover -s tests/cli -v
|
||||
|
||||
# 1.1 运行其他 Python 测试(tests/ 下的 test_*.py)
|
||||
python -m unittest discover -s tests -p "test_*.py" -v
|
||||
|
||||
# 2. 运行模板验证测试
|
||||
cd tests/templates
|
||||
sh validate_python_templates.sh
|
||||
sh validate_cpp_templates.sh
|
||||
sh validate_ci_templates.sh
|
||||
sh tests/templates/validate_python_templates.sh
|
||||
sh tests/templates/validate_cpp_templates.sh
|
||||
sh tests/templates/validate_ci_templates.sh
|
||||
sh tests/templates/validate_project_templates.sh
|
||||
|
||||
# 3. 运行集成测试
|
||||
cd tests/integration
|
||||
sh check_doc_links.sh
|
||||
# 3. 运行文档链接检查
|
||||
sh tests/integration/check_doc_links.sh
|
||||
```
|
||||
|
||||
### CI 自动化测试
|
||||
## 🧭 CI 自动化测试
|
||||
|
||||
测试套件通过 Gitea Actions 自动运行(见 `.gitea/workflows/test.yml`):
|
||||
|
||||
@@ -54,382 +57,21 @@ sh check_doc_links.sh
|
||||
- Pull Request 到 `main` 分支
|
||||
- 手动触发(workflow_dispatch)
|
||||
- **运行平台**:ubuntu-22.04
|
||||
- **并行策略**:使用 matrix 策略并行运行多个测试组
|
||||
|
||||
## 📚 测试详解
|
||||
|
||||
### 1. Shell 脚本测试 (scripts/)
|
||||
### 1. Python CLI 测试 (cli/)
|
||||
|
||||
使用 [bats-core](https://github.com/bats-core/bats-core) 框架测试 shell 脚本。
|
||||
使用 `unittest` 运行,覆盖 `scripts/playbook.py` 的核心行为:
|
||||
|
||||
#### test_sync_standards.bats
|
||||
|
||||
测试 `scripts/sync_standards.sh` 脚本的功能:
|
||||
|
||||
- **基础功能**:
|
||||
- 脚本存在且可执行
|
||||
- 无参数时默认同步 tsl(已有 `.agents/<lang>/` 则自动识别)
|
||||
- 单语言同步(tsl/cpp)
|
||||
- 多语言同步(tsl cpp)
|
||||
- **.gitattributes 同步**:
|
||||
- 默认模式追加缺失规则
|
||||
- 保留现有内容
|
||||
- 更新已存在的 managed block
|
||||
- **AGENTS.md 处理**:
|
||||
- 不存在时自动创建
|
||||
- 已存在时不覆盖
|
||||
- **备份功能**:
|
||||
- 更新前创建备份
|
||||
- **错误处理**:
|
||||
- 未找到 playbook 快照时报错
|
||||
- 无效语言参数时报错
|
||||
- **环境变量**:
|
||||
- `SYNC_GITATTR_MODE` 配置
|
||||
- **幂等性**:
|
||||
- 多次执行结果一致
|
||||
|
||||
#### test_vendor_playbook.bats
|
||||
|
||||
测试 `scripts/vendor_playbook.sh` 脚本的功能:
|
||||
|
||||
- **基础功能**:
|
||||
- 单语言 vendoring
|
||||
- 多语言 vendoring
|
||||
- **自动同步**:
|
||||
- 自动执行 sync_standards
|
||||
- **SOURCE.md 生成**:
|
||||
- 包含来源信息
|
||||
- 包含 commit hash
|
||||
- 包含时间戳
|
||||
- **裁剪功能**:
|
||||
- 仅包含指定语言
|
||||
- 始终包含 common 目录
|
||||
- 包含对应模板文件
|
||||
- **目标目录处理**:
|
||||
- 已存在时覆盖更新
|
||||
- 创建必要的父目录
|
||||
- **错误处理**:
|
||||
- 目标目录不存在时报错
|
||||
- 无效语言参数时报错
|
||||
- **完整性验证**:
|
||||
- 所有必要文件已复制
|
||||
- 脚本可执行
|
||||
- **幂等性**:
|
||||
- 多次 vendor 结果一致
|
||||
|
||||
#### test_install_codex_skills.bats
|
||||
|
||||
测试 `scripts/install_codex_skills.sh` 脚本的功能:
|
||||
|
||||
- **基础功能**:
|
||||
- 脚本存在且可执行
|
||||
- **安装功能**:
|
||||
- 创建 skills 目录
|
||||
- 复制 skill 目录(包含 `SKILL.md`)
|
||||
- 支持指定单个 skill 安装
|
||||
- 同名目录安装前创建备份
|
||||
- **错误处理**:
|
||||
- 指定不存在的 skill 报错
|
||||
- **幂等性**:
|
||||
- 多次安装结果一致
|
||||
- CLI 参数解析与帮助信息
|
||||
- TOML 配置解析与动作顺序
|
||||
- vendor/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地
|
||||
|
||||
### 2. 模板验证测试 (templates/)
|
||||
|
||||
验证项目模板文件的正确性和完整性。
|
||||
通过脚本检查各类模板文件结构与关键字段(如占位符、关键配置项)。
|
||||
|
||||
#### validate_python_templates.sh
|
||||
### 3. 文档链接检查 (integration/)
|
||||
|
||||
验证 `templates/python/` 目录下的 Python 模板:
|
||||
|
||||
- **pyproject.toml**:
|
||||
- 文件存在
|
||||
- TOML 语法正确
|
||||
- 包含必要配置节(tool.black, tool.isort, tool.pytest.ini_options)
|
||||
- black line-length 配置正确
|
||||
- isort profile 配置正确
|
||||
- **.flake8**:
|
||||
- 文件存在
|
||||
- 包含 [flake8] 配置
|
||||
- 配置了 max-line-length
|
||||
- 配置了错误忽略规则
|
||||
- **.pylintrc**:
|
||||
- 文件存在
|
||||
- 包含必要配置节(MASTER, MESSAGES CONTROL, FORMAT)
|
||||
- 配置了 max-line-length
|
||||
- **.pre-commit-config.yaml**:
|
||||
- 文件存在
|
||||
- YAML 语法正确
|
||||
- 包含 repos 配置
|
||||
- 配置了常用 hooks(black, isort, flake8)
|
||||
- **.editorconfig**:
|
||||
- 文件存在
|
||||
- 包含 root = true
|
||||
- 包含 Python 文件配置
|
||||
- **.vscode/settings.json**:
|
||||
- 文件存在
|
||||
- JSON 语法正确
|
||||
- 包含 Python 配置
|
||||
|
||||
#### validate_cpp_templates.sh
|
||||
|
||||
验证 `templates/cpp/` 目录下的 C++ 模板:
|
||||
|
||||
- **CMakeLists.txt**:
|
||||
- 文件存在
|
||||
- CMake 基础语法正确
|
||||
- 配置了 C++23 标准
|
||||
- 启用了 C++ Modules 扫描(可选)
|
||||
- 启用了 import std; 支持(可选)
|
||||
- 启用了编译命令导出(用于 clangd)
|
||||
- **.clang-format**:
|
||||
- 文件存在
|
||||
- YAML 语法正确
|
||||
- 配置了 Language: Cpp
|
||||
- 基于某个风格(BasedOnStyle)
|
||||
- 配置了 C++ 标准
|
||||
- 配置了缩进宽度
|
||||
- **.clangd**:
|
||||
- 文件存在
|
||||
- YAML 语法正确
|
||||
- 包含 CompileFlags 配置
|
||||
- 配置了 -std=c++23
|
||||
- 配置了 CompilationDatabase 路径
|
||||
- 配置了 Index 选项
|
||||
- **conanfile.txt**:
|
||||
- 文件存在
|
||||
- 包含 [requires] 配置(可选)
|
||||
- 包含 [generators] 配置
|
||||
- 配置了 CMakeDeps 生成器
|
||||
- 配置了 CMakeToolchain 生成器
|
||||
- 包含 [options] 配置
|
||||
- **CMakeUserPresets.json**:
|
||||
- 文件存在
|
||||
- JSON 语法正确
|
||||
- 包含 version 字段
|
||||
- 包含 configurePresets
|
||||
- 包含 buildPresets
|
||||
|
||||
#### validate_ci_templates.sh
|
||||
|
||||
验证 `templates/ci/` 目录下的 CI 模板:
|
||||
|
||||
- **Gitea/GitHub workflow 文件**:
|
||||
- 文件存在
|
||||
- YAML 语法正确
|
||||
- 包含必要字段(name, on, jobs, runs-on, steps)
|
||||
- 包含中文注释(符合项目风格)
|
||||
- 包含配置区域标记
|
||||
- 包含环境变量配置
|
||||
- **特定 workflow 模板**:
|
||||
- standards-check.yml:包含格式化检查和 lint 检查
|
||||
- test.yml:包含测试步骤和测试矩阵
|
||||
- **文档**:
|
||||
- 包含 README 说明文档
|
||||
- 包含使用说明
|
||||
|
||||
### 3. 集成测试 (integration/)
|
||||
|
||||
端到端测试,验证整体功能。
|
||||
|
||||
#### check_doc_links.sh
|
||||
|
||||
检查所有 Markdown 文档中的链接有效性:
|
||||
|
||||
- **扫描范围**:
|
||||
- 所有 `*.md` 文件
|
||||
- 排除 node_modules, .git, build, dist 等目录
|
||||
- **链接类型**:
|
||||
- Markdown 链接:`[text](link)`
|
||||
- 引用链接:`[text]: link`
|
||||
- **验证逻辑**:
|
||||
- 检查相对路径链接是否指向存在的文件
|
||||
- 跳过外部链接(http/https)
|
||||
- 跳过 mailto 链接
|
||||
- 跳过代码块与行内代码中的链接样式文本
|
||||
- 支持锚点(但只验证文件存在性)
|
||||
- **报告内容**:
|
||||
- 总链接数
|
||||
- 有效链接数
|
||||
- 跳过链接数(外部/mailto)
|
||||
- 断开链接详情(文件、行号、链接、目标路径)
|
||||
|
||||
## 🔧 本地开发
|
||||
|
||||
### 安装测试依赖
|
||||
|
||||
**Ubuntu/Debian**:
|
||||
|
||||
```bash
|
||||
# bats-core (Shell 脚本测试)
|
||||
sudo apt-get update
|
||||
sudo apt-get install bats
|
||||
|
||||
# Python 工具(模板验证)
|
||||
pip install toml tomli jsonschema yamllint
|
||||
|
||||
# C++ 工具(模板验证,可选)
|
||||
sudo apt-get install cmake clang-format
|
||||
|
||||
# YAML 验证
|
||||
pip install yamllint
|
||||
```
|
||||
|
||||
**macOS**:
|
||||
|
||||
```bash
|
||||
# bats-core
|
||||
brew install bats-core
|
||||
|
||||
# Python 工具
|
||||
pip3 install toml tomli jsonschema yamllint
|
||||
|
||||
# C++ 工具(可选)
|
||||
brew install cmake clang-format
|
||||
|
||||
# YAML 验证
|
||||
pip3 install yamllint
|
||||
```
|
||||
|
||||
### 运行单个测试
|
||||
|
||||
```bash
|
||||
# Shell 脚本测试
|
||||
cd tests/scripts
|
||||
bats test_sync_standards.bats --tap # TAP 格式输出
|
||||
bats test_vendor_playbook.bats --formatter junit # JUnit 格式
|
||||
|
||||
# 模板验证测试
|
||||
cd tests/templates
|
||||
sh validate_python_templates.sh
|
||||
sh validate_cpp_templates.sh
|
||||
sh validate_ci_templates.sh
|
||||
|
||||
# 集成测试
|
||||
cd tests/integration
|
||||
sh check_doc_links.sh
|
||||
```
|
||||
|
||||
### 添加新测试
|
||||
|
||||
#### 添加新的 bats 测试
|
||||
|
||||
在 `tests/scripts/` 创建新文件 `test_<script_name>.bats`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
# 测试前准备
|
||||
export TEST_DIR="$(mktemp -d)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
# 测试后清理
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
|
||||
@test "描述测试内容" {
|
||||
# 测试代码
|
||||
[ -f "some_file" ]
|
||||
}
|
||||
```
|
||||
|
||||
#### 添加新的模板验证
|
||||
|
||||
在 `tests/templates/` 创建新文件 `validate_<template_type>_templates.sh`,参考现有脚本结构。
|
||||
|
||||
#### 添加新的集成测试
|
||||
|
||||
在 `tests/integration/` 创建新脚本,确保:
|
||||
|
||||
1. 使用 `set -eu` 启用错误检测
|
||||
2. 输出清晰的测试进度
|
||||
3. 生成详细的报告文件
|
||||
4. 返回正确的退出码(0 = 成功,非 0 = 失败)
|
||||
|
||||
## 📊 测试覆盖率目标
|
||||
|
||||
- **Shell 脚本测试**:目标覆盖率 ≥ 80%
|
||||
- **模板验证测试**:目标通过率 = 100%
|
||||
- **集成测试**:目标通过率 = 100%
|
||||
- **文档链接有效性**:目标有效率 = 100%
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### bats 测试失败
|
||||
|
||||
```bash
|
||||
# 使用 --verbose 查看详细输出
|
||||
bats --verbose test_sync_standards.bats
|
||||
|
||||
# 使用 --trace 查看执行跟踪
|
||||
bats --trace test_sync_standards.bats
|
||||
```
|
||||
|
||||
### 模板验证失败
|
||||
|
||||
验证脚本会生成详细报告文件:
|
||||
|
||||
- `tests/templates/python_validation_report.txt`
|
||||
- `tests/templates/cpp_validation_report.txt`
|
||||
- `tests/templates/ci_validation_report.txt`
|
||||
|
||||
### 文档链接检查失败
|
||||
|
||||
查看详细报告:
|
||||
|
||||
```bash
|
||||
cat /tmp/doc_links_report.txt
|
||||
```
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
添加新功能时,请同步更新相应的测试:
|
||||
|
||||
1. **修改脚本**(`scripts/`)→ 更新对应的 `.bats` 测试
|
||||
2. **修改模板**(`templates/`)→ 更新对应的验证脚本
|
||||
3. **修改文档**(`docs/`, `.agents/`)→ 运行文档链接检查
|
||||
4. **修改 CI workflow**(`.gitea/workflows/`)→ 验证 YAML 语法
|
||||
|
||||
## 📖 相关文档
|
||||
|
||||
- [开发规范索引](../docs/index.md)
|
||||
- [提交信息规范](../docs/common/commit_message.md)
|
||||
- [Gitea Actions 文档](https://docs.gitea.com/usage/actions/overview)
|
||||
- [bats-core 文档](https://bats-core.readthedocs.io/)
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: 为什么测试在 CI 通过,但本地失败?
|
||||
|
||||
A: 可能原因:
|
||||
|
||||
- 环境差异(工具版本、路径)
|
||||
- 权限问题
|
||||
- Git 配置差异
|
||||
|
||||
建议使用 Docker 容器复现 CI 环境。
|
||||
|
||||
### Q: 如何跳过某些测试?
|
||||
|
||||
A: bats 支持 `skip` 命令:
|
||||
|
||||
```bash
|
||||
@test "某个测试" {
|
||||
skip "原因说明"
|
||||
# 测试代码
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 测试运行很慢,如何加速?
|
||||
|
||||
A: 建议:
|
||||
|
||||
1. 使用 bats 的 `--jobs` 参数并行运行
|
||||
2. 只运行变更相关的测试
|
||||
3. 使用 CI 的缓存机制
|
||||
|
||||
---
|
||||
|
||||
**测试套件维护者**: Playbook 团队
|
||||
**最后更新**: 2026-01-07
|
||||
扫描 `docs/` 与模板文件中的本地链接,确保引用路径有效。
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = ROOT / "scripts" / "playbook.py"
|
||||
|
||||
|
||||
def run_cli(*args):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
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_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 = """
|
||||
[playbook]
|
||||
project_root = "."
|
||||
|
||||
[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_vendor_creates_snapshot(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
|
||||
[vendor]
|
||||
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))
|
||||
|
||||
snapshot = Path(tmp_dir) / "docs/standards/playbook/SOURCE.md"
|
||||
self.assertEqual(result.returncode, 0)
|
||||
self.assertTrue(snapshot.is_file())
|
||||
|
||||
def test_sync_memory_bank_creates_memory_bank(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
|
||||
[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())
|
||||
|
||||
def test_sync_standards_creates_agents(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
|
||||
[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_agents_block_has_blank_lines(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
|
||||
[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("<!-- playbook:agents:start -->")
|
||||
end_idx = lines.index("<!-- playbook:agents:end -->")
|
||||
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], "")
|
||||
|
||||
def test_install_skills(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
target = Path(tmp_dir) / "codex"
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
|
||||
[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))
|
||||
|
||||
skill_file = target / "skills/brainstorming/SKILL.md"
|
||||
self.assertEqual(result.returncode, 0)
|
||||
self.assertTrue(skill_file.is_file())
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -63,6 +63,14 @@ check_file_link() {
|
||||
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
|
||||
@@ -127,6 +135,7 @@ echo "🔍 扫描 Markdown 文件..."
|
||||
cd "$PLAYBOOK_ROOT"
|
||||
|
||||
MD_FILES=$(find . -name "*.md" \
|
||||
-not -name "*.template.md" \
|
||||
-not -path "*/node_modules/*" \
|
||||
-not -path "*/.git/*" \
|
||||
-not -path "*/build/*" \
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env bats
|
||||
# install_codex_skills.sh 测试套件
|
||||
|
||||
setup() {
|
||||
export TEST_DIR="$(mktemp -d)"
|
||||
export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
||||
export SCRIPT_PATH="$PLAYBOOK_ROOT/scripts/install_codex_skills.sh"
|
||||
|
||||
# 创建临时 HOME 避免污染真实环境
|
||||
export ORIGINAL_HOME="$HOME"
|
||||
export HOME="$TEST_DIR/home"
|
||||
mkdir -p "$HOME"
|
||||
|
||||
export CODEX_HOME="$HOME/.codex"
|
||||
export SKILLS_DST_ROOT="$CODEX_HOME/skills"
|
||||
|
||||
cd "$TEST_DIR"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
export HOME="$ORIGINAL_HOME"
|
||||
|
||||
if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then
|
||||
chmod -R u+w "$TEST_DIR" 2>/dev/null || true
|
||||
rm -rf "$TEST_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
skip_if_no_skills() {
|
||||
if [ ! -d "$PLAYBOOK_ROOT/codex/skills" ]; then
|
||||
skip "No codex/skills directory found"
|
||||
fi
|
||||
}
|
||||
|
||||
first_skill_name() {
|
||||
ls -1 "$PLAYBOOK_ROOT/codex/skills" 2>/dev/null | head -n 1
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 基础功能测试
|
||||
# ==============================================
|
||||
|
||||
@test "install_codex_skills.sh 脚本存在且可执行" {
|
||||
[ -f "$SCRIPT_PATH" ]
|
||||
}
|
||||
|
||||
@test "安装 - 创建 skills 目录" {
|
||||
skip_if_no_skills
|
||||
|
||||
[ ! -d "$SKILLS_DST_ROOT" ]
|
||||
|
||||
sh "$SCRIPT_PATH"
|
||||
|
||||
[ -d "$SKILLS_DST_ROOT" ]
|
||||
}
|
||||
|
||||
@test "安装 - 复制 skill 目录" {
|
||||
skip_if_no_skills
|
||||
|
||||
sh "$SCRIPT_PATH"
|
||||
|
||||
SKILL_DIRS=$(find "$SKILLS_DST_ROOT" -mindepth 1 -maxdepth 1 -type d)
|
||||
[ -n "$SKILL_DIRS" ]
|
||||
|
||||
for dir in $SKILL_DIRS; do
|
||||
[ -f "$dir/SKILL.md" ]
|
||||
done
|
||||
}
|
||||
|
||||
@test "安装 - 指定单个 skill" {
|
||||
skip_if_no_skills
|
||||
|
||||
SKILL_NAME="$(first_skill_name)"
|
||||
[ -n "$SKILL_NAME" ]
|
||||
|
||||
sh "$SCRIPT_PATH" "$SKILL_NAME"
|
||||
|
||||
[ -d "$SKILLS_DST_ROOT/$SKILL_NAME" ]
|
||||
COUNT=$(find "$SKILLS_DST_ROOT" -mindepth 1 -maxdepth 1 -type d | wc -l)
|
||||
[ "$COUNT" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "备份 - 同名 skill 目录会创建备份" {
|
||||
skip_if_no_skills
|
||||
|
||||
SKILL_NAME="$(first_skill_name)"
|
||||
[ -n "$SKILL_NAME" ]
|
||||
|
||||
mkdir -p "$SKILLS_DST_ROOT/$SKILL_NAME"
|
||||
echo "marker" > "$SKILLS_DST_ROOT/$SKILL_NAME/marker.txt"
|
||||
|
||||
sh "$SCRIPT_PATH" "$SKILL_NAME"
|
||||
|
||||
BACKUP_DIRS=$(find "$SKILLS_DST_ROOT" -maxdepth 1 -type d -name "$SKILL_NAME.bak.*")
|
||||
[ -n "$BACKUP_DIRS" ]
|
||||
}
|
||||
|
||||
@test "错误处理 - 指定不存在 skill 报错" {
|
||||
skip_if_no_skills
|
||||
|
||||
run sh "$SCRIPT_PATH" nonexistent-skill
|
||||
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "幂等性 - 多次安装结果一致" {
|
||||
skip_if_no_skills
|
||||
|
||||
sh "$SCRIPT_PATH"
|
||||
CHECKSUM1=$(find "$SKILLS_DST_ROOT" -type f -name "SKILL.md" ! -path "$SKILLS_DST_ROOT"'/*.bak.*/*' -exec md5sum {} \; | sort | md5sum)
|
||||
|
||||
sh "$SCRIPT_PATH"
|
||||
CHECKSUM2=$(find "$SKILLS_DST_ROOT" -type f -name "SKILL.md" ! -path "$SKILLS_DST_ROOT"'/*.bak.*/*' -exec md5sum {} \; | sort | md5sum)
|
||||
|
||||
[ "$CHECKSUM1" = "$CHECKSUM2" ]
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
#!/usr/bin/env bats
|
||||
# sync_standards.sh 测试套件
|
||||
|
||||
# 测试辅助函数
|
||||
setup() {
|
||||
# 创建临时测试目录
|
||||
export TEST_DIR="$(mktemp -d)"
|
||||
export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
||||
# 初始化测试项目
|
||||
cd "$TEST_DIR"
|
||||
git init
|
||||
git config user.name "Test User"
|
||||
git config user.email "test@example.com"
|
||||
|
||||
# 模拟 playbook 快照目录
|
||||
mkdir -p docs/standards/playbook
|
||||
cp -r "$PLAYBOOK_ROOT"/{.agents,.gitattributes,docs,scripts} docs/standards/playbook/ 2>/dev/null || true
|
||||
|
||||
export SCRIPT_PATH="$TEST_DIR/docs/standards/playbook/scripts/sync_standards.sh"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
# 清理测试目录
|
||||
if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then
|
||||
chmod -R u+rwX "$TEST_DIR" 2>/dev/null || true
|
||||
rm -rf "$TEST_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 基础功能测试
|
||||
# ==============================================
|
||||
|
||||
@test "sync_standards.sh 脚本存在且可执行" {
|
||||
[ -f "$SCRIPT_PATH" ]
|
||||
}
|
||||
|
||||
@test "sync_standards.sh 无参数时同步 tsl 规则集" {
|
||||
cd "$TEST_DIR"
|
||||
sh "$SCRIPT_PATH"
|
||||
|
||||
# 验证输出目录
|
||||
[ -d ".agents/tsl" ]
|
||||
[ -f ".agents/tsl/index.md" ]
|
||||
}
|
||||
|
||||
@test "sync_standards.sh 无参数时自动识别已有语言" {
|
||||
cd "$TEST_DIR"
|
||||
mkdir -p .agents/cpp
|
||||
echo "# placeholder" > .agents/cpp/index.md
|
||||
|
||||
sh "$SCRIPT_PATH"
|
||||
|
||||
[ -d ".agents/cpp" ]
|
||||
[ -f ".agents/cpp/index.md" ]
|
||||
[ ! -d ".agents/tsl" ]
|
||||
}
|
||||
|
||||
@test "sync_standards.sh tsl - 同步 TSL 规则集" {
|
||||
cd "$TEST_DIR"
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
# 验证必须文件
|
||||
[ -d ".agents/tsl" ]
|
||||
[ -f ".agents/tsl/index.md" ]
|
||||
[ -f ".agents/tsl/auth.md" ]
|
||||
[ -f ".agents/tsl/code_quality.md" ]
|
||||
[ -f ".agents/tsl/performance.md" ]
|
||||
[ -f ".agents/tsl/testing.md" ]
|
||||
}
|
||||
|
||||
@test "sync_standards.sh cpp - 同步 C++ 规则集" {
|
||||
cd "$TEST_DIR"
|
||||
sh "$SCRIPT_PATH" cpp
|
||||
|
||||
# 验证必须文件
|
||||
[ -d ".agents/cpp" ]
|
||||
[ -f ".agents/cpp/index.md" ]
|
||||
[ -f ".agents/cpp/auth.md" ]
|
||||
[ -f ".agents/cpp/code_quality.md" ]
|
||||
}
|
||||
|
||||
@test "sync_standards.sh tsl cpp - 同步多个规则集" {
|
||||
cd "$TEST_DIR"
|
||||
sh "$SCRIPT_PATH" tsl cpp
|
||||
|
||||
# 验证两个规则集都存在
|
||||
[ -d ".agents/tsl" ]
|
||||
[ -d ".agents/cpp" ]
|
||||
[ -f ".agents/index.md" ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# .gitattributes 同步测试
|
||||
# ==============================================
|
||||
|
||||
@test ".gitattributes - 默认模式追加缺失规则" {
|
||||
cd "$TEST_DIR"
|
||||
[ ! -f ".gitattributes" ]
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
[ -f ".gitattributes" ]
|
||||
grep -q "Added from playbook .gitattributes" .gitattributes
|
||||
grep -q "\\*.tsl" .gitattributes
|
||||
}
|
||||
|
||||
@test ".gitattributes - 保留现有内容" {
|
||||
cd "$TEST_DIR"
|
||||
echo "# My custom rules" > .gitattributes
|
||||
echo "*.custom binary" >> .gitattributes
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
grep -q "# My custom rules" .gitattributes
|
||||
grep -q "*.custom binary" .gitattributes
|
||||
}
|
||||
|
||||
@test ".gitattributes - 更新已存在的 managed block" {
|
||||
cd "$TEST_DIR"
|
||||
cat > .gitattributes << 'EOF'
|
||||
# BEGIN playbook .gitattributes
|
||||
# Old content
|
||||
# END playbook .gitattributes
|
||||
EOF
|
||||
|
||||
export SYNC_GITATTR_MODE=block
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
# 验证 block 已更新(不再包含 "Old content")
|
||||
! grep -q "Old content" .gitattributes
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# AGENTS.md 自动生成测试
|
||||
# ==============================================
|
||||
|
||||
@test "AGENTS.md - 不存在时自动创建" {
|
||||
cd "$TEST_DIR"
|
||||
[ ! -f "AGENTS.md" ]
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
[ -f "AGENTS.md" ]
|
||||
grep -q ".agents/" AGENTS.md
|
||||
}
|
||||
|
||||
@test "AGENTS.md - 已存在时不覆盖" {
|
||||
cd "$TEST_DIR"
|
||||
echo "# My custom AGENTS.md" > AGENTS.md
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
grep -q "# My custom AGENTS.md" AGENTS.md
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 备份功能测试
|
||||
# ==============================================
|
||||
|
||||
@test "备份 - .gitattributes 更新前创建备份" {
|
||||
cd "$TEST_DIR"
|
||||
echo "# Original content" > .gitattributes
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
# 验证备份文件存在
|
||||
[ -f ".gitattributes.bak."* ] || [ -f ".gitattributes.bak" ]
|
||||
}
|
||||
|
||||
@test "备份 - .agents/ 更新前创建备份" {
|
||||
cd "$TEST_DIR"
|
||||
mkdir -p .agents/tsl
|
||||
echo "# Old index" > .agents/tsl/index.md
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
# 验证备份目录存在
|
||||
[ -d ".agents/tsl.bak."* ] || [ -d ".agents/tsl.bak" ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 多语言项目测试
|
||||
# ==============================================
|
||||
|
||||
@test "多语言 - TSL + C++ + Python 规则集共存" {
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# 复制 Python 规则集(如果存在)
|
||||
if [ -d "$PLAYBOOK_ROOT/.agents/python" ]; then
|
||||
cp -r "$PLAYBOOK_ROOT/.agents/python" docs/standards/playbook/.agents/
|
||||
fi
|
||||
|
||||
sh "$SCRIPT_PATH" tsl cpp
|
||||
|
||||
# 验证规则集不互相覆盖
|
||||
[ -d ".agents/tsl" ]
|
||||
[ -d ".agents/cpp" ]
|
||||
|
||||
# 验证索引文件正确引用
|
||||
[ -f ".agents/index.md" ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 错误处理测试
|
||||
# ==============================================
|
||||
|
||||
@test "错误处理 - 未找到 playbook 快照时报错" {
|
||||
cd "$TEST_DIR"
|
||||
rm -rf docs/standards/playbook/.agents
|
||||
|
||||
run sh "$SCRIPT_PATH" tsl
|
||||
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "错误处理 - 无效语言参数时报错" {
|
||||
cd "$TEST_DIR"
|
||||
|
||||
run sh "$SCRIPT_PATH" invalid_lang
|
||||
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 环境变量配置测试
|
||||
# ==============================================
|
||||
|
||||
@test "环境变量 - SYNC_GITATTR_MODE=skip 跳过 .gitattributes" {
|
||||
cd "$TEST_DIR"
|
||||
export SYNC_GITATTR_MODE=skip
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
[ ! -f ".gitattributes" ]
|
||||
}
|
||||
|
||||
@test "环境变量 - SYNC_GITATTR_MODE=overwrite 覆盖 .gitattributes" {
|
||||
cd "$TEST_DIR"
|
||||
echo "# Custom content" > .gitattributes
|
||||
export SYNC_GITATTR_MODE=overwrite
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
# 验证自定义内容被覆盖
|
||||
! grep -q "# Custom content" .gitattributes
|
||||
}
|
||||
|
||||
@test "环境变量 - SYNC_GITATTR_MODE=append 追加缺失规则" {
|
||||
cd "$TEST_DIR"
|
||||
echo "# Custom rules only" > .gitattributes
|
||||
export SYNC_GITATTR_MODE=append
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
grep -q "Added from playbook .gitattributes" .gitattributes
|
||||
grep -q "\\*.tsl" .gitattributes
|
||||
}
|
||||
|
||||
@test "环境变量 - SYNC_GITATTR_MODE=append 无缺失规则不追加" {
|
||||
cd "$TEST_DIR"
|
||||
cp "$PLAYBOOK_ROOT/.gitattributes" .gitattributes
|
||||
export SYNC_GITATTR_MODE=append
|
||||
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
|
||||
! grep -q "Added from playbook .gitattributes" .gitattributes
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 幂等性测试
|
||||
# ==============================================
|
||||
|
||||
@test "幂等性 - 多次执行结果一致" {
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# 第一次同步
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
CHECKSUM1=$(find .agents/tsl -type f -exec md5sum {} \; | sort | md5sum)
|
||||
|
||||
# 第二次同步
|
||||
sh "$SCRIPT_PATH" tsl
|
||||
CHECKSUM2=$(find .agents/tsl -type f -exec md5sum {} \; | sort | md5sum)
|
||||
|
||||
[ "$CHECKSUM1" = "$CHECKSUM2" ]
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
#!/usr/bin/env bats
|
||||
# vendor_playbook.sh 测试套件
|
||||
|
||||
# 测试辅助函数
|
||||
setup() {
|
||||
# 创建临时测试目录
|
||||
export TEST_DIR="$(mktemp -d)"
|
||||
export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
||||
export SCRIPT_PATH="$PLAYBOOK_ROOT/scripts/vendor_playbook.sh"
|
||||
|
||||
# 创建目标项目目录
|
||||
export TARGET_DIR="$(mktemp -d)"
|
||||
cd "$TARGET_DIR"
|
||||
git init
|
||||
git config user.name "Test User"
|
||||
git config user.email "test@example.com"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
# 清理测试目录
|
||||
if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then
|
||||
chmod -R u+w "$TEST_DIR" 2>/dev/null || true
|
||||
rm -rf "$TEST_DIR"
|
||||
fi
|
||||
if [ -n "$TARGET_DIR" ] && [ -d "$TARGET_DIR" ]; then
|
||||
chmod -R u+w "$TARGET_DIR" 2>/dev/null || true
|
||||
rm -rf "$TARGET_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 基础功能测试
|
||||
# ==============================================
|
||||
|
||||
@test "vendor_playbook.sh 脚本存在且可执行" {
|
||||
[ -f "$SCRIPT_PATH" ]
|
||||
}
|
||||
|
||||
@test "vendor_playbook.sh - 无参数时显示用法" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
run sh "$SCRIPT_PATH"
|
||||
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "vendor_playbook.sh - 单语言 vendoring (tsl)" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 验证快照目录结构
|
||||
[ -d "docs/standards/playbook" ]
|
||||
[ -d "docs/standards/playbook/docs/common" ]
|
||||
[ -d "docs/standards/playbook/docs/tsl" ]
|
||||
[ -d "docs/standards/playbook/.agents/tsl" ]
|
||||
[ -f "docs/standards/playbook/scripts/sync_standards.sh" ]
|
||||
}
|
||||
|
||||
@test "vendor_playbook.sh - 多语言 vendoring (tsl cpp)" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl cpp
|
||||
|
||||
# 验证包含两种语言的文档
|
||||
[ -d "docs/standards/playbook/docs/tsl" ]
|
||||
[ -d "docs/standards/playbook/docs/cpp" ]
|
||||
[ -d "docs/standards/playbook/.agents/tsl" ]
|
||||
[ -d "docs/standards/playbook/.agents/cpp" ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 自动同步测试
|
||||
# ==============================================
|
||||
|
||||
@test "vendor_playbook.sh - 自动执行 sync_standards" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 验证根目录已同步规则集
|
||||
[ -d ".agents/tsl" ]
|
||||
[ -f ".agents/tsl/index.md" ]
|
||||
[ -f ".gitattributes" ]
|
||||
}
|
||||
|
||||
@test "vendor_playbook.sh - 多语言自动同步" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl cpp
|
||||
|
||||
# 验证两个规则集都已同步
|
||||
[ -d ".agents/tsl" ]
|
||||
[ -d ".agents/cpp" ]
|
||||
[ -f ".agents/index.md" ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# SOURCE.md 生成测试
|
||||
# ==============================================
|
||||
|
||||
@test "vendor_playbook.sh - 生成 SOURCE.md" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
[ -f "docs/standards/playbook/SOURCE.md" ]
|
||||
grep -q "Source:" docs/standards/playbook/SOURCE.md
|
||||
grep -q "Commit:" docs/standards/playbook/SOURCE.md
|
||||
}
|
||||
|
||||
@test "vendor_playbook.sh - SOURCE.md 包含 commit hash" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 验证包含 commit hash(40个十六进制字符)
|
||||
grep -E "[0-9a-f]{40}" docs/standards/playbook/SOURCE.md
|
||||
}
|
||||
|
||||
@test "vendor_playbook.sh - SOURCE.md 包含时间戳" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 验证包含日期格式 YYYY-MM-DD
|
||||
grep -E "[0-9]{4}-[0-9]{2}-[0-9]{2}" docs/standards/playbook/SOURCE.md
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 裁剪功能测试
|
||||
# ==============================================
|
||||
|
||||
@test "裁剪 - 仅包含指定语言的文档" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 验证包含 TSL 文档
|
||||
[ -d "docs/standards/playbook/docs/tsl" ]
|
||||
|
||||
# 验证不包含其他语言(如果原本有 Python)
|
||||
if [ -d "$PLAYBOOK_ROOT/docs/python" ]; then
|
||||
[ ! -d "docs/standards/playbook/docs/python" ]
|
||||
fi
|
||||
}
|
||||
|
||||
@test "裁剪 - 始终包含 common 目录" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
[ -d "docs/standards/playbook/docs/common" ]
|
||||
[ -f "docs/standards/playbook/docs/common/commit_message.md" ]
|
||||
}
|
||||
|
||||
@test "裁剪 - 包含对应的模板文件" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" cpp
|
||||
|
||||
# 验证包含 C++ 模板
|
||||
[ -d "docs/standards/playbook/templates/cpp" ]
|
||||
|
||||
# 验证不包含 Python 模板(如果指定了 cpp)
|
||||
if [ -d "$PLAYBOOK_ROOT/templates/python" ]; then
|
||||
[ ! -d "docs/standards/playbook/templates/python" ]
|
||||
fi
|
||||
}
|
||||
|
||||
@test "裁剪 - 包含通用 CI 模板" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
[ -d "docs/standards/playbook/templates/ci" ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 目标目录处理测试
|
||||
# ==============================================
|
||||
|
||||
@test "目标目录 - 已存在时覆盖更新" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
# 首次 vendor
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 在快照中添加标记文件
|
||||
touch docs/standards/playbook/OLD_MARKER
|
||||
|
||||
# 再次 vendor
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 验证旧标记不存在(已被覆盖)
|
||||
[ ! -f "docs/standards/playbook/OLD_MARKER" ]
|
||||
}
|
||||
|
||||
@test "目标目录 - 创建必要的父目录" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
# 确保 docs/standards 不存在
|
||||
[ ! -d "docs/standards" ]
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
[ -d "docs/standards/playbook" ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 错误处理测试
|
||||
# ==============================================
|
||||
|
||||
@test "错误处理 - 目标目录不存在时报错" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
missing_dir="$TEST_DIR/missing-project"
|
||||
rm -rf "$missing_dir"
|
||||
|
||||
run sh "$SCRIPT_PATH" "$missing_dir" tsl
|
||||
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "错误处理 - 无效语言参数时报错" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
run sh "$SCRIPT_PATH" "$TARGET_DIR" invalid_lang
|
||||
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "错误处理 - 目标目录不是 git 仓库时警告" {
|
||||
cd "$TARGET_DIR"
|
||||
rm -rf .git
|
||||
|
||||
run sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 应该给出警告但不失败
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 完整性测试
|
||||
# ==============================================
|
||||
|
||||
@test "完整性 - 验证所有必要文件已复制" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 验证关键文件
|
||||
[ -f "docs/standards/playbook/README.md" ]
|
||||
[ -f "docs/standards/playbook/docs/index.md" ]
|
||||
[ -f "docs/standards/playbook/.gitattributes" ]
|
||||
[ -f "docs/standards/playbook/scripts/sync_standards.sh" ]
|
||||
[ -f "docs/standards/playbook/SOURCE.md" ]
|
||||
}
|
||||
|
||||
@test "完整性 - 验证脚本可执行性" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
|
||||
# 验证同步脚本可执行
|
||||
run sh docs/standards/playbook/scripts/sync_standards.sh tsl
|
||||
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
# ==============================================
|
||||
# 多次执行测试
|
||||
# ==============================================
|
||||
|
||||
@test "幂等性 - 多次 vendor 结果一致" {
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
# 第一次 vendor
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
CHECKSUM1=$(find docs/standards/playbook -type f -name "*.md" ! -name "SOURCE.md" -exec md5sum {} \; | sort | md5sum)
|
||||
|
||||
# 第二次 vendor
|
||||
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
|
||||
CHECKSUM2=$(find docs/standards/playbook -type f -name "*.md" ! -name "SOURCE.md" -exec md5sum {} \; | sort | md5sum)
|
||||
|
||||
[ "$CHECKSUM1" = "$CHECKSUM2" ]
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/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_FILE="$SCRIPT_DIR/project_templates_report.txt"
|
||||
|
||||
> "$ERRORS_FILE"
|
||||
> "$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" "<!-- playbook:templates:start -->" "包含 templates 标记"
|
||||
validate_contains "$AGENTS_TEMPLATE" "<!-- playbook:framework:start -->" "包含 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}} 占位符"
|
||||
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-stack architecture 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/review.template.md" "prompts/coding/review.template.md"
|
||||
validate_file_exists "$PROMPTS_DIR/meta/prompt-generator.template.md" "prompts/meta/prompt-generator.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
|
||||
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "playbook.py"
|
||||
|
||||
|
||||
def run_cli(*args, env=None):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
class FormatMdActionTests(unittest.TestCase):
|
||||
def test_format_md_invokes_prettier_from_path(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
(root / "README.md").write_text("# Title\n", encoding="utf-8")
|
||||
|
||||
bin_dir = root / "bin"
|
||||
bin_dir.mkdir()
|
||||
prettier = bin_dir / "prettier"
|
||||
prettier.write_text(
|
||||
"#!/usr/bin/env python3\n"
|
||||
"from pathlib import Path\n"
|
||||
"Path(\".prettier_called\").write_text(\"ok\")\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
prettier.chmod(0o755)
|
||||
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = \"{tmp_dir}\"
|
||||
|
||||
[format_md]
|
||||
# tool defaults to prettier
|
||||
# keep default globs
|
||||
"""
|
||||
config_path = root / "playbook.toml"
|
||||
config_path.write_text(config_body, encoding="utf-8")
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = f"{bin_dir}:{env.get('PATH', '')}"
|
||||
|
||||
result = run_cli("-config", str(config_path), env=env)
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertTrue((root / ".prettier_called").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,94 @@
|
||||
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"
|
||||
|
||||
|
||||
def run_cli(*args, cwd=None):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=cwd,
|
||||
)
|
||||
|
||||
|
||||
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 GitattributesModeTests(unittest.TestCase):
|
||||
def _run_sync(self, root: Path, mode: str) -> subprocess.CompletedProcess:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = \"{root}\"
|
||||
|
||||
[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(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(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(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(root, "append")
|
||||
self.assertEqual(result.returncode, 0)
|
||||
content = (root / ".gitattributes").read_text(encoding="utf-8")
|
||||
self.assertIn("Added from playbook .gitattributes", content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,76 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "playbook.py"
|
||||
|
||||
|
||||
def run_cli(*args):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
class NoBackupFlagsTests(unittest.TestCase):
|
||||
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}"
|
||||
|
||||
[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}"
|
||||
|
||||
[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, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,166 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "plan_progress.py"
|
||||
|
||||
|
||||
def run_cli(*args, cwd=None):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=cwd,
|
||||
)
|
||||
|
||||
|
||||
class PlanProgressCliTests(unittest.TestCase):
|
||||
def _current_env(self) -> str:
|
||||
system = platform.system().lower()
|
||||
mapping = {"windows": "windows", "linux": "linux", "darwin": "darwin"}
|
||||
if system not in mapping:
|
||||
self.skipTest(f"Unsupported environment: {system}")
|
||||
return mapping[system]
|
||||
|
||||
def test_select_seeds_progress_when_missing(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
plans_dir = root / "docs" / "plans"
|
||||
plans_dir.mkdir(parents=True)
|
||||
(plans_dir / "2026-01-01-old.md").write_text("old", encoding="utf-8")
|
||||
(plans_dir / "2026-01-02-new.md").write_text("new", encoding="utf-8")
|
||||
|
||||
result = run_cli(
|
||||
"select",
|
||||
"-plans",
|
||||
"docs/plans",
|
||||
"-progress",
|
||||
"memory-bank/progress.md",
|
||||
cwd=root,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), "docs/plans/2026-01-01-old.md")
|
||||
|
||||
progress = root / "memory-bank" / "progress.md"
|
||||
text = progress.read_text(encoding="utf-8")
|
||||
self.assertIn("<!-- plan-status:start -->", text)
|
||||
self.assertIn("<!-- plan-status:end -->", text)
|
||||
self.assertIn("`2026-01-01-old.md` pending", text)
|
||||
self.assertIn("`2026-01-02-new.md` pending", text)
|
||||
|
||||
def test_select_returns_first_pending_in_order(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
plans_dir = root / "docs" / "plans"
|
||||
plans_dir.mkdir(parents=True)
|
||||
(plans_dir / "2026-01-01-a.md").write_text("a", encoding="utf-8")
|
||||
(plans_dir / "2026-01-02-b.md").write_text("b", encoding="utf-8")
|
||||
|
||||
progress = root / "memory-bank" / "progress.md"
|
||||
progress.parent.mkdir(parents=True)
|
||||
progress.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"# Plan 状态",
|
||||
"",
|
||||
"<!-- plan-status:start -->",
|
||||
"- [ ] `2026-01-02-b.md` pending",
|
||||
"- [ ] `2026-01-01-a.md` pending",
|
||||
"<!-- plan-status:end -->",
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = run_cli(
|
||||
"select",
|
||||
"-plans",
|
||||
"docs/plans",
|
||||
"-progress",
|
||||
"memory-bank/progress.md",
|
||||
cwd=root,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), "docs/plans/2026-01-02-b.md")
|
||||
|
||||
def test_select_returns_env_blocked_plan_without_flag(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
plans_dir = root / "docs" / "plans"
|
||||
plans_dir.mkdir(parents=True)
|
||||
(plans_dir / "2026-01-05-env.md").write_text("env", encoding="utf-8")
|
||||
|
||||
progress = root / "memory-bank" / "progress.md"
|
||||
progress.parent.mkdir(parents=True)
|
||||
env = self._current_env()
|
||||
progress.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"# Plan 状态",
|
||||
"",
|
||||
"<!-- plan-status:start -->",
|
||||
f"- [ ] `2026-01-05-env.md` blocked: env:{env}:Task1",
|
||||
"<!-- plan-status:end -->",
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = run_cli(
|
||||
"select",
|
||||
"-plans",
|
||||
"docs/plans",
|
||||
"-progress",
|
||||
"memory-bank/progress.md",
|
||||
cwd=root,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), "docs/plans/2026-01-05-env.md")
|
||||
|
||||
def test_record_updates_line(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 状态",
|
||||
"",
|
||||
"<!-- plan-status:start -->",
|
||||
"- [ ] `2026-01-03-demo.md` pending",
|
||||
"<!-- plan-status:end -->",
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = run_cli(
|
||||
"record",
|
||||
"-plan",
|
||||
"docs/plans/2026-01-03-demo.md",
|
||||
"-status",
|
||||
"done",
|
||||
"-progress",
|
||||
"memory-bank/progress.md",
|
||||
cwd=root,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
text = progress.read_text(encoding="utf-8")
|
||||
self.assertIn("- [x] `2026-01-03-demo.md` done", text)
|
||||
self.assertEqual(text.count("2026-01-03-demo.md"), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,42 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SKILLS_MD = ROOT / "SKILLS.md"
|
||||
SOURCES_LIST = ROOT / "codex" / "skills" / ".sources" / "superpowers.list"
|
||||
|
||||
|
||||
def read_sources_list() -> list[str]:
|
||||
return [
|
||||
line.strip()
|
||||
for line in SOURCES_LIST.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip() and not line.strip().startswith("#")
|
||||
]
|
||||
|
||||
|
||||
def read_skills_md_list() -> list[str]:
|
||||
lines = SKILLS_MD.read_text(encoding="utf-8").splitlines()
|
||||
start = "<!-- superpowers:skills:start -->"
|
||||
end = "<!-- superpowers:skills:end -->"
|
||||
try:
|
||||
start_idx = lines.index(start) + 1
|
||||
end_idx = lines.index(end)
|
||||
except ValueError as exc:
|
||||
raise AssertionError("superpowers markers missing in SKILLS.md") from exc
|
||||
|
||||
items = []
|
||||
for line in lines[start_idx:end_idx]:
|
||||
stripped = line.strip()
|
||||
if not stripped.startswith("-"):
|
||||
continue
|
||||
items.append(stripped.lstrip("- ").strip())
|
||||
return items
|
||||
|
||||
|
||||
class SuperpowersListSyncTests(unittest.TestCase):
|
||||
def test_superpowers_list_matches_skills_md(self):
|
||||
self.assertEqual(read_sources_list(), read_skills_md_list())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,100 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "playbook.py"
|
||||
|
||||
|
||||
def run_cli(*args):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
class SyncDirectoryActionsTests(unittest.TestCase):
|
||||
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}"
|
||||
|
||||
[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}"
|
||||
|
||||
[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())
|
||||
|
||||
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}"
|
||||
|
||||
[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, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,49 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "playbook.py"
|
||||
|
||||
|
||||
def run_cli(*args):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
class SyncTemplatesPlaceholdersTests(unittest.TestCase):
|
||||
def test_main_language_placeholder_replaced(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = \"{tmp_dir}\"
|
||||
|
||||
[sync_rules]
|
||||
|
||||
[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)
|
||||
|
||||
rules_md = Path(tmp_dir) / "AGENT_RULES.md"
|
||||
rules_text = rules_md.read_text(encoding="utf-8")
|
||||
self.assertIn("docs/standards/playbook/scripts/plan_progress.py", rules_text)
|
||||
self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,23 @@
|
||||
import unittest
|
||||
|
||||
from scripts import playbook
|
||||
|
||||
|
||||
class TomlEdgeCaseTests(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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,44 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "playbook.py"
|
||||
|
||||
|
||||
def run_cli(*args):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
class VendorSnapshotTemplatesTests(unittest.TestCase):
|
||||
def test_vendor_includes_core_templates(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
|
||||
[vendor]
|
||||
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, msg=result.stderr)
|
||||
|
||||
snapshot = Path(tmp_dir) / "docs/standards/playbook"
|
||||
self.assertTrue((snapshot / "templates/AGENTS.template.md").is_file())
|
||||
self.assertTrue((snapshot / "templates/AGENT_RULES.template.md").is_file())
|
||||
self.assertTrue((snapshot / "templates/README.md").is_file())
|
||||
self.assertTrue((snapshot / "templates/memory-bank").is_dir())
|
||||
self.assertTrue((snapshot / "templates/prompts").is_dir())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user