From 94395056ea086d7e2c8c6b047024cd4eaf53f066 Mon Sep 17 00:00:00 2001 From: csh Date: Tue, 10 Mar 2026 10:10:28 +0800 Subject: [PATCH] :bug: fix(playbook): address reported repo issues normalize Windows path-like TOML config values, regenerate .agents/index.md on sync, keep the SKILLS.md superpowers section route-only, and ignore generated Python cache dirs. add regression coverage for load_config() and the standards sync behavior touched by these fixes. --- .gitea/ci/sync_superpowers.sh | 53 +----------------------- .gitignore | 4 ++ SKILLS.md | 26 ------------ scripts/playbook.py | 62 +++++++++++++++++++++++++++-- tests/cli/test_playbook_cli.py | 40 +++++++++++++++++++ tests/test_superpowers_list_sync.py | 31 ++++++--------- tests/test_toml_edge_cases.py | 40 +++++++++++++++++++ 7 files changed, 155 insertions(+), 101 deletions(-) diff --git a/.gitea/ci/sync_superpowers.sh b/.gitea/ci/sync_superpowers.sh index 10ed891..fd6c5c9 100644 --- a/.gitea/ci/sync_superpowers.sh +++ b/.gitea/ci/sync_superpowers.sh @@ -60,58 +60,7 @@ done printf "%s\n" "${names[@]}" | sort > "$SUPERPOWERS_LIST" -update_block() { - local file="$1" - local start="" - local end="" - local tmp - - tmp="$(mktemp)" - { - echo "### Third-party Skills (superpowers)" - echo "" - echo "$start" - while IFS= read -r name; do - [ -n "$name" ] || continue - echo "- $name" - done < "$SUPERPOWERS_LIST" - echo "$end" - } > "$tmp" - - if grep -q "$start" "$file"; then - awk -v start="$start" -v end="$end" -v block="$tmp" ' - BEGIN { - while ((getline line < block) > 0) { buf[++n] = line } - close(block) - inblock=0 - replaced=0 - } - { - if (!replaced && $0 == start) { - for (i=1; i<=n; i++) print buf[i] - inblock=1 - replaced=1 - next - } - if (inblock) { - if ($0 == end) { inblock=0 } - next - } - print - } - ' "$file" > "${file}.tmp" - mv "${file}.tmp" "$file" - else - echo "" >> "$file" - cat "$tmp" >> "$file" - fi - - rm -f "$tmp" -} - -update_block "SKILLS.md" - -git add codex/skills SKILLS.md "$SUPERPOWERS_LIST" +git add codex/skills "$SUPERPOWERS_LIST" if git diff --cached --quiet; then echo "No changes to sync." diff --git a/.gitignore b/.gitignore index 9e0016f..dc7ba62 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ tags reports/ .worktrees/ + +scripts/__pycache__ +tests/__pycache__ +tests/cli/__pycache__ diff --git a/SKILLS.md b/SKILLS.md index 8c0b5ee..62e29da 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -159,32 +159,6 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml ## 9. Third-party Skills (superpowers) 来源:`codex/skills/.sources/superpowers.list`(第三方来源清单)。 -本节仅列出 superpowers 体系 skills,与本 Playbook 原生 skills 分离。 - -### Third-party Skills (superpowers) - -### Third-party Skills (superpowers) - -### Third-party Skills (superpowers) - -### Third-party Skills (superpowers) - - -- \ -- \ -- \ -- \ -- \ -- \ -- \ -- \ -- \ -- \ -- \ -- \ -- \ -- \ - --- diff --git a/scripts/playbook.py b/scripts/playbook.py index bd0f064..8dc2cc7 100644 --- a/scripts/playbook.py +++ b/scripts/playbook.py @@ -21,6 +21,7 @@ ORDER = [ ] SCRIPT_DIR = Path(__file__).resolve().parent PLAYBOOK_ROOT = SCRIPT_DIR.parent +PATH_CONFIG_KEYS = {"project_root", "target_dir", "agents_home", "codex_home"} def usage() -> str: @@ -141,8 +142,63 @@ def loads_toml_minimal(raw: str) -> dict: return data +def normalize_path_config_strings(raw: str) -> str: + normalized_lines: list[str] = [] + for line in raw.splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in line: + normalized_lines.append(line) + continue + + key_part, value_part = line.split("=", 1) + key = key_part.strip() + if key not in PATH_CONFIG_KEYS: + normalized_lines.append(line) + continue + + value = strip_inline_comment(value_part.strip()) + if len(value) < 2 or value[0] != '"' or value[-1] != '"' or "\\" not in value[1:-1]: + normalized_lines.append(line) + continue + + inner = value[1:-1] + has_lone_backslash = False + probe_idx = 0 + while probe_idx < len(inner): + if inner[probe_idx] != "\\": + probe_idx += 1 + continue + if probe_idx + 1 < len(inner) and inner[probe_idx + 1] == "\\": + probe_idx += 2 + continue + has_lone_backslash = True + break + if not has_lone_backslash: + normalized_lines.append(line) + continue + + escaped: list[str] = [] + idx = 0 + while idx < len(inner): + ch = inner[idx] + if ch != "\\": + escaped.append(ch) + idx += 1 + continue + if idx + 1 < len(inner) and inner[idx + 1] == "\\": + escaped.extend(["\\", "\\"]) + idx += 2 + continue + escaped.extend(["\\", "\\"]) + idx += 1 + normalized_lines.append(f'{key_part}= "{"".join(escaped)}"') + + suffix = "\n" if raw.endswith("\n") else "" + return "\n".join(normalized_lines) + suffix + + def load_config(path: Path) -> dict: - raw = path.read_text(encoding="utf-8") + raw = normalize_path_config_strings(path.read_text(encoding="utf-8")) if tomllib is not None: return tomllib.loads(raw) return loads_toml_minimal(raw) @@ -830,8 +886,6 @@ def update_agents_block(agents_md: Path, block_lines: list[str]) -> None: def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str | None) -> None: agents_index = agents_root / "index.md" - if agents_index.exists(): - return lines = [ "# .agents(多语言)", "", @@ -859,7 +913,7 @@ def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str | f"- {docs_prefix or 'docs/standards/playbook/docs/'}", ] agents_index.write_text("\n".join(lines) + "\n", encoding="utf-8") - log("Created .agents/index.md") + log("Synced .agents/index.md") def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None: diff --git a/tests/cli/test_playbook_cli.py b/tests/cli/test_playbook_cli.py index bb605b0..485da82 100644 --- a/tests/cli/test_playbook_cli.py +++ b/tests/cli/test_playbook_cli.py @@ -101,6 +101,46 @@ langs = ["tsl"] self.assertEqual(result.returncode, 0) self.assertTrue(agents_index.is_file()) + def test_sync_standards_updates_agents_index_when_langs_expand(self): + with tempfile.TemporaryDirectory() as tmp_dir: + root = Path(tmp_dir) + + first_config = root / "playbook-first.toml" + first_config.write_text( + f""" +[playbook] +project_root = "{tmp_dir}" + +[sync_standards] +langs = ["tsl"] +no_backup = true +""", + encoding="utf-8", + ) + + first_result = run_cli("-config", str(first_config)) + self.assertEqual(first_result.returncode, 0) + + second_config = root / "playbook-second.toml" + second_config.write_text( + f""" +[playbook] +project_root = "{tmp_dir}" + +[sync_standards] +langs = ["tsl", "cpp"] +no_backup = true +""", + encoding="utf-8", + ) + + second_result = run_cli("-config", str(second_config)) + self.assertEqual(second_result.returncode, 0) + + agents_index = (root / ".agents" / "index.md").read_text(encoding="utf-8") + self.assertIn("`.agents/tsl/index.md`", agents_index) + self.assertIn("`.agents/cpp/index.md`", agents_index) + def test_sync_standards_agents_block_has_blank_lines(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" diff --git a/tests/test_superpowers_list_sync.py b/tests/test_superpowers_list_sync.py index 3dd723b..e178f07 100644 --- a/tests/test_superpowers_list_sync.py +++ b/tests/test_superpowers_list_sync.py @@ -4,6 +4,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SKILLS_MD = ROOT / "SKILLS.md" SOURCES_LIST = ROOT / "codex" / "skills" / ".sources" / "superpowers.list" +SOURCE_REF = "来源:`codex/skills/.sources/superpowers.list`(第三方来源清单)。" def read_sources_list() -> list[str]: @@ -14,28 +15,20 @@ def read_sources_list() -> list[str]: ] -def read_skills_md_list() -> list[str]: - lines = SKILLS_MD.read_text(encoding="utf-8").splitlines() - start = "" - 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 +def read_skills_md() -> str: + return SKILLS_MD.read_text(encoding="utf-8") class SuperpowersListSyncTests(unittest.TestCase): - def test_superpowers_list_matches_skills_md(self): - self.assertEqual(read_sources_list(), read_skills_md_list()) + def test_superpowers_section_routes_to_source_list(self): + self.assertTrue(read_sources_list()) + + text = read_skills_md() + + self.assertIn(SOURCE_REF, text) + self.assertEqual(text.count("Third-party Skills (superpowers)"), 1) + self.assertNotIn("", text) + self.assertNotIn("", text) if __name__ == "__main__": diff --git a/tests/test_toml_edge_cases.py b/tests/test_toml_edge_cases.py index a64e641..d7c1047 100644 --- a/tests/test_toml_edge_cases.py +++ b/tests/test_toml_edge_cases.py @@ -1,4 +1,6 @@ +import tempfile import unittest +from pathlib import Path from scripts import playbook @@ -18,6 +20,44 @@ key = 1 with self.assertRaises(ValueError): playbook.loads_toml_minimal(raw) + def test_load_config_preserves_windows_users_path_in_basic_string(self): + with tempfile.TemporaryDirectory() as tmp_dir: + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text( + '[playbook]\nproject_root = "C:\\Users\\demo\\workspace"\n', + encoding="utf-8", + ) + + data = playbook.load_config(config_path) + + self.assertEqual(data["playbook"]["project_root"], r"C:\Users\demo\workspace") + + def test_load_config_preserves_windows_escape_like_segments_for_path_keys(self): + with tempfile.TemporaryDirectory() as tmp_dir: + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text( + '[playbook]\nproject_root = "C:\\tmp\\notes"\n\n' + '[install_skills]\nagents_home = "D:\\new\\tab"\n', + encoding="utf-8", + ) + + data = playbook.load_config(config_path) + + self.assertEqual(data["playbook"]["project_root"], r"C:\tmp\notes") + self.assertEqual(data["install_skills"]["agents_home"], r"D:\new\tab") + + def test_load_config_keeps_already_escaped_windows_path(self): + with tempfile.TemporaryDirectory() as tmp_dir: + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text( + '[playbook]\nproject_root = "C:\\\\Users\\\\demo\\\\workspace"\n', + encoding="utf-8", + ) + + data = playbook.load_config(config_path) + + self.assertEqual(data["playbook"]["project_root"], r"C:\Users\demo\workspace") + if __name__ == "__main__": unittest.main()