From 9efd0a581f4913f1198f4b815baee86050741eeb Mon Sep 17 00:00:00 2001 From: csh Date: Fri, 26 Jun 2026 18:08:04 +0800 Subject: [PATCH] :wrench: chore(test): consolidate playbook config tests Group playbook.toml-driven behavior tests under a single config-actions file and split parser/template contract coverage into focused files. Remove obsolete PowerShell audit scripts and tracked validation reports; write template validation reports under reports/templates. --- scripts/check_no_legacy_refs.ps1 | 71 --- scripts/tsl_doc_audit.ps1 | 328 ------------- test/README.md | 9 +- test/templates/ci_validation_report.txt | 11 - test/templates/cpp_validation_report.txt | 11 - test/templates/project_templates_report.txt | 11 - test/templates/python_validation_report.txt | 11 - test/templates/validate_ci_templates.sh | 4 +- test/templates/validate_cpp_templates.sh | 4 +- test/templates/validate_project_templates.sh | 4 +- test/templates/validate_python_templates.sh | 4 +- test/test_format_md_action.py | 66 --- test/test_gitattributes_modes.py | 97 ---- test/test_no_backup_flags.py | 112 ----- test/test_playbook_config_actions.py | 437 ++++++++++++++++++ ..._cases.py => test_playbook_toml_parser.py} | 6 +- test/test_sync_directory_actions.py | 108 ----- ...eholders.py => test_template_contracts.py} | 181 +------- 18 files changed, 458 insertions(+), 1017 deletions(-) delete mode 100644 scripts/check_no_legacy_refs.ps1 delete mode 100644 scripts/tsl_doc_audit.ps1 delete mode 100644 test/templates/ci_validation_report.txt delete mode 100644 test/templates/cpp_validation_report.txt delete mode 100644 test/templates/project_templates_report.txt delete mode 100644 test/templates/python_validation_report.txt delete mode 100644 test/test_format_md_action.py delete mode 100644 test/test_gitattributes_modes.py delete mode 100644 test/test_no_backup_flags.py create mode 100644 test/test_playbook_config_actions.py rename test/{test_toml_edge_cases.py => test_playbook_toml_parser.py} (94%) delete mode 100644 test/test_sync_directory_actions.py rename test/{test_sync_templates_placeholders.py => test_template_contracts.py} (60%) diff --git a/scripts/check_no_legacy_refs.ps1 b/scripts/check_no_legacy_refs.ps1 deleted file mode 100644 index 5ccb8758..00000000 --- a/scripts/check_no_legacy_refs.ps1 +++ /dev/null @@ -1,71 +0,0 @@ -param( - [string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path -) - -$ErrorActionPreference = "Stop" - -$targets = @( - (Join-Path $RepoRoot "docs/tsl/index.md"), - (Join-Path $RepoRoot "docs/tsl/syntax"), - (Join-Path $RepoRoot "docs/tsl/finance"), - (Join-Path $RepoRoot "docs/tsl/reference") -) - -$excludeFragments = @( - "\docs\plans\", - "\archive\", - "\docs\tsl\legacy\", - "\docs\tsl\syntax_book\" -) - -$pattern = '(?i)\b(?:syntax_book|legacy)\b' - -function Get-RelativePath([string]$basePath, [string]$childPath) { - $baseUri = [System.Uri]::new(($basePath.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar)) - $childUri = [System.Uri]::new($childPath) - return [System.Uri]::UnescapeDataString($baseUri.MakeRelativeUri($childUri).ToString()).Replace('\', '/') -} - -$files = foreach ($target in $targets) { - if (!(Test-Path -LiteralPath $target)) { - throw "Target not found: $target" - } - - if ((Get-Item -LiteralPath $target).PSIsContainer) { - Get-ChildItem -LiteralPath $target -Recurse -File -Filter *.md - } - else { - Get-Item -LiteralPath $target - } -} - -$activeFiles = $files | - Where-Object { - $fullName = $_.FullName - foreach ($fragment in $excludeFragments) { - if ($fullName -like "*$fragment*") { - return $false - } - } - return $true - } | - Sort-Object FullName -Unique - -$hits = foreach ($file in $activeFiles) { - Select-String -Path $file.FullName -Pattern $pattern -AllMatches -} - -if ($hits.Count -eq 0) { - Write-Output "No legacy references found in active TSL docs." - exit 0 -} - -Write-Output "Legacy references found in active TSL docs:" -foreach ($hit in $hits) { - $relativePath = Get-RelativePath -basePath $RepoRoot -childPath $hit.Path - Write-Output ("{0}:{1}: {2}" -f $relativePath, $hit.LineNumber, $hit.Line.Trim()) -} - -Write-Output ("Total files scanned: {0}" -f $activeFiles.Count) -Write-Output ("Total legacy hits: {0}" -f $hits.Count) -exit 1 diff --git a/scripts/tsl_doc_audit.ps1 b/scripts/tsl_doc_audit.ps1 deleted file mode 100644 index ad88642d..00000000 --- a/scripts/tsl_doc_audit.ps1 +++ /dev/null @@ -1,328 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [string]$Path -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -function Resolve-AuditTargets { - param( - [string]$InputPath - ) - - if (-not (Test-Path -LiteralPath $InputPath)) { - throw "Path not found: $InputPath" - } - - $item = Get-Item -LiteralPath $InputPath - if ($item.PSIsContainer) { - return @(Get-ChildItem -LiteralPath $item.FullName -Recurse -File -Filter "*.md" | Sort-Object FullName) - } - - if ($item.Extension -ne ".md") { - throw "Only Markdown files are supported: $($item.FullName)" - } - - return @($item) -} - -function Get-FirstMeaningfulLine { - param( - [string[]]$Lines - ) - - foreach ($line in $Lines) { - if (-not [string]::IsNullOrWhiteSpace($line)) { - return $line.Trim() - } - } - - return "" -} - -function Get-BlockPreview { - param( - [string]$Code - ) - - $preview = "" - foreach ($line in ($Code -split "`r?`n")) { - if (-not [string]::IsNullOrWhiteSpace($line)) { - $preview = $line.Trim() - break - } - } - - if ([string]::IsNullOrWhiteSpace($preview)) { - return "" - } - - if ($preview.Length -gt 100) { - return $preview.Substring(0, 100) + "..." - } - - return $preview -} - -function Get-SkipReason { - param( - [string]$Language, - [string]$Code - ) - - if ($Language -eq "text") { - return "text block" - } - - $trimmed = $Code.Trim() - if ([string]::IsNullOrWhiteSpace($trimmed)) { - return "empty block" - } - - if ($trimmed -match '(?m)^\s*statement;\s*$') { - return "grammar placeholder" - } - - if ($trimmed -match '…+' -or $trimmed -match '(?m)^\s*(//\s*)?\.{3,}\s*$') { - return "ellipsis placeholder" - } - - if ($trimmed -match '<[^>\r\n]+>') { - return "angle-bracket placeholder" - } - - $meaningfulLines = @( - ($Code -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() }) - ) - if ($meaningfulLines.Count -gt 1 -and - $meaningfulLines[0] -eq "begin" -and - (($meaningfulLines | Select-Object -Skip 1) -match '^(?i:(function|unit|type|class|const|var|namespace|uses))')) { - return "mixed non-standalone snippet" - } - - return $null -} - -function Get-CompileKind { - param( - [string]$Code - ) - - $firstLine = Get-FirstMeaningfulLine -Lines ($Code -split "`r?`n") - if ($firstLine -match '^(?i:(function|unit|type|class|const|var|namespace|uses|\{\$))') { - return "tsf" - } - - return "tsl" -} - -function Invoke-TslCompile { - param( - [string]$Code, - [ValidateSet("tsl", "tsf")] - [string]$Kind - ) - - $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("tsl-doc-audit-" + [guid]::NewGuid().ToString("N")) - New-Item -ItemType Directory -Path $tempRoot | Out-Null - - $process = $null - - try { - $sourcePath = Join-Path $tempRoot ("snippet." + $Kind) - [System.IO.File]::WriteAllText($sourcePath, $Code, [System.Text.UTF8Encoding]::new($false)) - - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = 'tsl' - $psi.Arguments = "-COMPILE `"$sourcePath`"" - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.RedirectStandardInput = $true - $psi.UseShellExecute = $false - $psi.CreateNoWindow = $true - - $process = New-Object System.Diagnostics.Process - $process.StartInfo = $psi - $process.Start() | Out-Null - $process.StandardInput.WriteLine('exit') - $process.StandardInput.Flush() - $process.WaitForExit(30000) | Out-Null - - $stdout = $process.StandardOutput.ReadToEnd() - $stderr = $process.StandardError.ReadToEnd() - $outputText = ($stdout + "`n" + $stderr).Trim() - $lowerText = $outputText.ToLowerInvariant() - $success = $lowerText -match 'compile success' - $failure = $lowerText -match 'compile error' - - if ($success) { - return [pscustomobject]@{ Success = $true; Output = $outputText } - } - if ($failure) { - return [pscustomobject]@{ Success = $false; Output = $outputText } - } - - return [pscustomobject]@{ Success = $false; Output = (if ($outputText) { $outputText } else { "" }) } - } - finally { - if ($process -and -not $process.HasExited) { - $process.Kill() - $process.WaitForExit() - } - if (Test-Path -LiteralPath $tempRoot) { - Remove-Item -LiteralPath $tempRoot -Recurse -Force - } - } -} - -function Wrap-AsFunctionBody { - param( - [string]$Code - ) - - $body = $Code -split "`r?`n" | ForEach-Object { " $_" } - return @( - "function __doc_check__();" - "begin" - $body - "end;" - "" - ) -join "`n" -} - -function Parse-MarkdownBlocks { - param( - [string]$Content - ) - - $blocks = New-Object System.Collections.Generic.List[object] - $lines = $Content -split "`r?`n" - $inFence = $false - $fenceLang = "" - $fenceStartLine = 0 - $buffer = New-Object System.Collections.Generic.List[string] - - for ($i = 0; $i -lt $lines.Length; $i++) { - $line = $lines[$i] - - if (-not $inFence) { - if ($line -match '^\s*```([A-Za-z0-9_-]*)\s*$') { - $inFence = $true - $fenceLang = $Matches[1].ToLowerInvariant() - $fenceStartLine = $i + 1 - $buffer.Clear() - } - continue - } - - if ($line -match '^\s*```\s*$') { - $blocks.Add([pscustomobject]@{ - Language = $fenceLang - StartLine = $fenceStartLine - Code = ($buffer -join "`n") - }) - $inFence = $false - $fenceLang = "" - $fenceStartLine = 0 - $buffer.Clear() - continue - } - - $buffer.Add($line) - } - - return $blocks -} - -$targets = Resolve-AuditTargets -InputPath $Path -$grandPass = 0 -$grandSkip = 0 -$grandFail = 0 - -foreach ($target in $targets) { - $content = Get-Content -LiteralPath $target.FullName -Raw - $blocks = Parse-MarkdownBlocks -Content $content - $results = New-Object System.Collections.Generic.List[object] - - foreach ($block in $blocks) { - if ($block.Language -notin @("tsl", "text")) { - continue - } - - $skipReason = Get-SkipReason -Language $block.Language -Code $block.Code - if ($null -ne $skipReason) { - $results.Add([pscustomobject]@{ - Status = "skip" - StartLine = $block.StartLine - Preview = Get-BlockPreview -Code $block.Code - Detail = $skipReason - }) - continue - } - - $kind = Get-CompileKind -Code $block.Code - $compile = Invoke-TslCompile -Code $block.Code -Kind $kind - - if ($compile.Success) { - $results.Add([pscustomobject]@{ - Status = "pass" - StartLine = $block.StartLine - Preview = Get-BlockPreview -Code $block.Code - Detail = $kind - }) - continue - } - - if ($kind -eq "tsl") { - $wrappedCode = Wrap-AsFunctionBody -Code $block.Code - $wrappedCompile = Invoke-TslCompile -Code $wrappedCode -Kind "tsf" - if ($wrappedCompile.Success) { - $results.Add([pscustomobject]@{ - Status = "pass" - StartLine = $block.StartLine - Preview = Get-BlockPreview -Code $block.Code - Detail = "wrapped tsf" - }) - continue - } - - $compile = $wrappedCompile - } - - $results.Add([pscustomobject]@{ - Status = "fail" - StartLine = $block.StartLine - Preview = Get-BlockPreview -Code $block.Code - Detail = ($compile.Output.Trim()) - }) - } - - $passCount = @($results | Where-Object Status -eq "pass").Count - $skipCount = @($results | Where-Object Status -eq "skip").Count - $failCount = @($results | Where-Object Status -eq "fail").Count - - $grandPass += $passCount - $grandSkip += $skipCount - $grandFail += $failCount - - Write-Output "$($target.FullName): pass=$passCount skip=$skipCount fail=$failCount" - - foreach ($failure in ($results | Where-Object Status -eq "fail")) { - Write-Output " FAIL line $($failure.StartLine): $($failure.Preview)" - foreach ($detailLine in ($failure.Detail -split "`r?`n")) { - if (-not [string]::IsNullOrWhiteSpace($detailLine)) { - Write-Output " $detailLine" - } - } - } -} - -Write-Output "TOTAL: pass=$grandPass skip=$grandSkip fail=$grandFail" - -if ($grandFail -gt 0) { - exit 1 -} - -exit 0 diff --git a/test/README.md b/test/README.md index fe55669f..be269886 100644 --- a/test/README.md +++ b/test/README.md @@ -7,16 +7,13 @@ ```txt test/ ├── README.md # 本文件:测试文档 -├── test_format_md_action.py # format_md 动作测试 ├── test_gitea_workflow_bootstrap.py # Gitea workflow 自举顺序回归测试 -├── test_gitattributes_modes.py # gitattr_mode 行为测试 -├── test_no_backup_flags.py # no_backup 行为测试 -├── test_sync_directory_actions.py # sync_memory_bank/sync_prompts 行为测试 +├── test_playbook_config_actions.py # playbook.toml 驱动的同步/部署行为测试 +├── test_playbook_toml_parser.py # TOML parser/load_config 边界测试 +├── test_template_contracts.py # 模板内容、占位符、文案契约测试 ├── test_main_loop_cli.py # main_loop CLI 测试 ├── agent/ # Agent 题面/运行时验证测试定义 ├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试 -├── 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++ 模板验证 diff --git a/test/templates/ci_validation_report.txt b/test/templates/ci_validation_report.txt deleted file mode 100644 index 8d5d1820..00000000 --- a/test/templates/ci_validation_report.txt +++ /dev/null @@ -1,11 +0,0 @@ -CI 模板验证报告 -==================== - -验证时间: 2026-06-21 06:14:22 -模板目录: /home/csh/windows_share/tinysoft/playbook/templates/ci - -统计结果: - 通过: 12 - 失败: 0 - 通过率: 100.0% - diff --git a/test/templates/cpp_validation_report.txt b/test/templates/cpp_validation_report.txt deleted file mode 100644 index e02cabd4..00000000 --- a/test/templates/cpp_validation_report.txt +++ /dev/null @@ -1,11 +0,0 @@ -C++ 模板验证报告 -==================== - -验证时间: 2026-06-21 06:14:22 -模板目录: /home/csh/windows_share/tinysoft/playbook/templates/cpp - -统计结果: - 通过: 24 - 失败: 0 - 通过率: 100.0% - diff --git a/test/templates/project_templates_report.txt b/test/templates/project_templates_report.txt deleted file mode 100644 index eaa43601..00000000 --- a/test/templates/project_templates_report.txt +++ /dev/null @@ -1,11 +0,0 @@ -项目模板验证报告 -==================== - -验证时间: 2026-06-21 06:08:57 -模板目录: /home/csh/windows_share/tinysoft/playbook/templates - -统计结果: - 通过: 23 - 失败: 0 - 通过率: 100.0% - diff --git a/test/templates/python_validation_report.txt b/test/templates/python_validation_report.txt deleted file mode 100644 index 5d1a496c..00000000 --- a/test/templates/python_validation_report.txt +++ /dev/null @@ -1,11 +0,0 @@ -Python 模板验证报告 -==================== - -验证时间: 2026-06-21 06:14:22 -模板目录: /home/csh/windows_share/tinysoft/playbook/templates/python - -统计结果: - 通过: 25 - 失败: 0 - 通过率: 100.0% - diff --git a/test/templates/validate_ci_templates.sh b/test/templates/validate_ci_templates.sh index c6963534..4c00541d 100644 --- a/test/templates/validate_ci_templates.sh +++ b/test/templates/validate_ci_templates.sh @@ -14,9 +14,11 @@ TEMPLATES_DIR="$PLAYBOOK_ROOT/templates/ci" VALIDATION_PASSED=0 VALIDATION_FAILED=0 ERRORS_FILE="/tmp/ci_template_validation_errors.txt" -REPORT_FILE="$SCRIPT_DIR/ci_validation_report.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" diff --git a/test/templates/validate_cpp_templates.sh b/test/templates/validate_cpp_templates.sh index 9e1f9c4a..4b911990 100644 --- a/test/templates/validate_cpp_templates.sh +++ b/test/templates/validate_cpp_templates.sh @@ -14,9 +14,11 @@ TEMPLATES_DIR="$PLAYBOOK_ROOT/templates/cpp" VALIDATION_PASSED=0 VALIDATION_FAILED=0 ERRORS_FILE="/tmp/cpp_template_validation_errors.txt" -REPORT_FILE="$SCRIPT_DIR/cpp_validation_report.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" diff --git a/test/templates/validate_project_templates.sh b/test/templates/validate_project_templates.sh index 9a761d87..576eb344 100644 --- a/test/templates/validate_project_templates.sh +++ b/test/templates/validate_project_templates.sh @@ -14,9 +14,11 @@ 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" +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" diff --git a/test/templates/validate_python_templates.sh b/test/templates/validate_python_templates.sh index 87cd9d50..3c6fa937 100644 --- a/test/templates/validate_python_templates.sh +++ b/test/templates/validate_python_templates.sh @@ -14,9 +14,11 @@ TEMPLATES_DIR="$PLAYBOOK_ROOT/templates/python" VALIDATION_PASSED=0 VALIDATION_FAILED=0 ERRORS_FILE="/tmp/python_template_validation_errors.txt" -REPORT_FILE="$SCRIPT_DIR/python_validation_report.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" diff --git a/test/test_format_md_action.py b/test/test_format_md_action.py deleted file mode 100644 index ddf98431..00000000 --- a/test/test_format_md_action.py +++ /dev/null @@ -1,66 +0,0 @@ -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() - if os.name == "nt": - prettier = bin_dir / "prettier.cmd" - prettier.write_text( - "@echo off\r\n" - "echo ok> .prettier_called\r\n", - encoding="utf-8", - ) - else: - 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}{os.pathsep}{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() diff --git a/test/test_gitattributes_modes.py b/test/test_gitattributes_modes.py deleted file mode 100644 index 8c8421ad..00000000 --- a/test/test_gitattributes_modes.py +++ /dev/null @@ -1,97 +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 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}\" -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(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() diff --git a/test/test_no_backup_flags.py b/test/test_no_backup_flags.py deleted file mode 100644 index 3608b900..00000000 --- a/test/test_no_backup_flags.py +++ /dev/null @@ -1,112 +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" -DEFAULT_DEPLOY_ROOT = "docs/standards/playbook" - - -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}" -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()) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_playbook_config_actions.py b/test/test_playbook_config_actions.py new file mode 100644 index 00000000..0297e946 --- /dev/null +++ b/test/test_playbook_config_actions.py @@ -0,0 +1,437 @@ +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_toml_edge_cases.py b/test/test_playbook_toml_parser.py similarity index 94% rename from test/test_toml_edge_cases.py rename to test/test_playbook_toml_parser.py index d7c10471..08322b85 100644 --- a/test/test_toml_edge_cases.py +++ b/test/test_playbook_toml_parser.py @@ -1,11 +1,15 @@ 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 TomlEdgeCaseTests(unittest.TestCase): +class PlaybookTomlParserTests(unittest.TestCase): def test_minimal_parser_allows_dotted_section_name(self): raw = """ [a.b] diff --git a/test/test_sync_directory_actions.py b/test/test_sync_directory_actions.py deleted file mode 100644 index 2e07f4fc..00000000 --- a/test/test_sync_directory_actions.py +++ /dev/null @@ -1,108 +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" -DEFAULT_DEPLOY_ROOT = "docs/standards/playbook" - - -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}" -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, []) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_sync_templates_placeholders.py b/test/test_template_contracts.py similarity index 60% rename from test/test_sync_templates_placeholders.py rename to test/test_template_contracts.py index 3eed9e90..d299a03f 100644 --- a/test/test_sync_templates_placeholders.py +++ b/test/test_template_contracts.py @@ -1,32 +1,10 @@ -import subprocess -import sys -import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "scripts" / "playbook.py" -DEFAULT_DEPLOY_ROOT = "docs/standards/playbook" -def run_cli(*args): - return subprocess.run( - [sys.executable, str(SCRIPT), *args], - capture_output=True, - text=True, - ) - - -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, - ) - - -class SyncTemplatesPlaceholdersTests(unittest.TestCase): +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) @@ -219,163 +197,6 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase): self.assertIn("`progress.md` 上半部分是短期状态快照", rules_template) self.assertIn("`active-context.md` 是短期上下文快照", rules_template) - 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()