From e0ddae637987d664102de2421c74b4f068e7be70 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 06:25:34 +0000 Subject: [PATCH 1/2] refactor: move validate_ai_docs_sync to scripts/ai_tools/ as proper CLI tool Moved validate_ai_docs_sync.py from src/python_modern_template/ to scripts/ai_tools/ to avoid polluting the main package with development tools. This follows the same pattern as other AI tooling. Changes: - Moved scripts/ai_tools/validate_ai_docs_sync.py (from src/) - Added ai-validate-docs CLI command to pyproject.toml - Updated all documentation references (CLAUDE.md, AGENTS.md, AI_DOCS/) - Updated sync_template.py to sync from new location - Updated template files and references - Refactored tests to import from new location - Added CLI integration tests - Deleted old files from src/ and template/src/ Benefits: - Cleaner package structure (no dev tools in main package) - Consistent with other AI tools (ai-start-task, ai-log, etc.) - Better command-line interface: uv run ai-validate-docs - No namespace pollution in installed packages All tests pass (202/202), make check passes, 100% coverage maintained. --- .ai-context/LAST_SESSION_SUMMARY.md | 6 +- AGENTS.md | 2 +- AI_DOCS/documentation-sync-rules.md | 28 +- CLAUDE.md | 2 +- pyproject.toml | 1 + .../ai_tools}/validate_ai_docs_sync.py | 2 +- sync_template.py | 9 +- .../AI_DOCS/documentation-sync-rules.md.jinja | 27 +- .../scripts/ai_tools/finish_task.py.jinja | 5 + .../ai_tools}/validate_ai_docs_sync.py.jinja | 2 +- .../tests/ai_tools/test_finish_task.py.jinja | 168 +++++++++ .../ai_tools/test_template_loader.py.jinja | 276 ++++++++++++++ .../test_update_plan_backward_compat.py.jinja | 325 +++++++++++++++++ .../test_update_plan_extended.py.jinja | 336 ++++++++++++++++++ .../ai_tools/test_update_plan_fuzzy.py.jinja | 195 ++++++++++ .../test_update_plan_validation.py.jinja | 151 ++++++++ .../tests/test_validate_ai_docs_sync.py.jinja | 229 ------------ tests/test_validate_ai_docs_sync.py | 42 ++- 18 files changed, 1532 insertions(+), 274 deletions(-) rename {src/python_modern_template => scripts/ai_tools}/validate_ai_docs_sync.py (99%) rename template/{src/{{ package_name }} => scripts/ai_tools}/validate_ai_docs_sync.py.jinja (99%) create mode 100644 template/tests/ai_tools/test_finish_task.py.jinja create mode 100644 template/tests/ai_tools/test_template_loader.py.jinja create mode 100644 template/tests/ai_tools/test_update_plan_backward_compat.py.jinja create mode 100644 template/tests/ai_tools/test_update_plan_extended.py.jinja create mode 100644 template/tests/ai_tools/test_update_plan_fuzzy.py.jinja create mode 100644 template/tests/ai_tools/test_update_plan_validation.py.jinja delete mode 100644 template/tests/test_validate_ai_docs_sync.py.jinja diff --git a/.ai-context/LAST_SESSION_SUMMARY.md b/.ai-context/LAST_SESSION_SUMMARY.md index 6ebc463..7d3e068 100644 --- a/.ai-context/LAST_SESSION_SUMMARY.md +++ b/.ai-context/LAST_SESSION_SUMMARY.md @@ -6,13 +6,13 @@ **Session ID**: 20251102150000 **Task**: Test Task -**Date**: 2025-11-05 06:04:36 +**Date**: 2025-11-05 06:24:51 **Status**: ✅ Completed # Task Summary: Test Task **Session ID**: 20251102150000 -**Created**: 2025-11-05 06:04:36 +**Created**: 2025-11-05 06:24:51 **Status**: ✅ Completed --- @@ -45,4 +45,4 @@ Test summary **This file is automatically updated by the last AI agent to complete a task.** **Next AI agent: Read this file first to understand recent work!** -**Last Updated**: 2025-11-05 06:04:36 +**Last Updated**: 2025-11-05 06:24:51 diff --git a/AGENTS.md b/AGENTS.md index 82aaf4c..42ec01a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -251,7 +251,7 @@ Before committing code, ensure: 7. ✅ **Type hints everywhere**: mypy strict mode 8. ✅ **No security issues**: CI runs Bandit + Safety 9. ✅ **AI documentation synchronized** (MANDATORY) - - Run: `uv run python src/python_modern_template/validate_ai_docs_sync.py` + - Run: `uv run ai-validate-docs` - Update manual sync files if AI_DOCS changed - Update .claude/skills or .claude/agents if relevant - Update template files if changes apply to new projects diff --git a/AI_DOCS/documentation-sync-rules.md b/AI_DOCS/documentation-sync-rules.md index 594f991..7e2b64d 100644 --- a/AI_DOCS/documentation-sync-rules.md +++ b/AI_DOCS/documentation-sync-rules.md @@ -65,7 +65,7 @@ For agents that **support references** (Claude, Cursor, Aider, Gemini/AGENTS.md) - [ ] Ensure references point to correct files - [ ] Validate no broken references -**Script:** `uv run python src/python_modern_template/validate_ai_docs_sync.py` +**Script:** `uv run ai-validate-docs` ### Step 3: Update Non-Supporting Agents @@ -114,7 +114,7 @@ For agents that **don't support references** (Gemini/styleguide, Copilot): **Run validation script:** ```bash -uv run python src/python_modern_template/validate_ai_docs_sync.py +uv run ai-validate-docs ``` **Should report:** @@ -191,7 +191,7 @@ uv run python src/python_modern_template/validate_ai_docs_sync.py ## Validation Script -**Location:** `src/python_modern_template/validate_ai_docs_sync.py` +**Location:** `scripts/ai_tools/validate_ai_docs_sync.py` **What it checks:** 1. All `@AI_DOCS/` references point to existing files @@ -203,7 +203,7 @@ uv run python src/python_modern_template/validate_ai_docs_sync.py **Usage:** ```bash # Run validation -uv run python src/python_modern_template/validate_ai_docs_sync.py +uv run ai-validate-docs # Expected output (all passing): ✅ All AI_DOCS files found @@ -232,8 +232,7 @@ uv run python src/python_modern_template/validate_ai_docs_sync.py - Quality tests (5 files): `tests/quality/*.py` → `template/tests/quality/*.py.jinja` - Project tests (4 files): `tests/test_*.py` → `template/tests/test_*.py.jinja` - Quality scripts (6 files): `scripts/quality/*.py` → `template/scripts/quality/*.py.jinja` -- AI tools scripts (11 files): `scripts/ai_tools/*.py` → `template/scripts/ai_tools/*.py.jinja` -- Source modules (1 file): `src/{{ package_name }}/validate_ai_docs_sync.py.jinja` +- AI tools scripts (12 files): `scripts/ai_tools/*.py` → `template/scripts/ai_tools/*.py.jinja` **Note:** AI tools tests are NOT synced to the template. They remain in the template repository only, as they test template infrastructure that users typically won't modify. @@ -253,8 +252,7 @@ python sync_template.py **When to run:** 1. After modifying any test files in `tests/` 2. After modifying any scripts in `scripts/quality/` or `scripts/ai_tools/` -3. After modifying `src/{{ package_name }}/validate_ai_docs_sync.py` -4. Before releasing a new template version +3. Before releasing a new template version **What it doesn't sync (intentionally):** - AI_DOCS/*.md files (already synced via separate workflow) @@ -306,7 +304,7 @@ In `AI_DOCS/code-conventions.md` and all agent configs, add: - [ ] `make lint` passes - [ ] `make check` passes - [ ] **AI documentation synchronized** ⭐ NEW - - [ ] Validated with `validate_ai_docs_sync.py` + - [ ] Validated with `uv run ai-validate-docs` - [ ] Updated manual sync files if needed - [ ] Updated .claude/skills or .claude/agents if needed - [ ] Updated template files if needed @@ -314,7 +312,7 @@ In `AI_DOCS/code-conventions.md` and all agent configs, add: ### Integration with ai-finish-task Before `ai-finish-task` completes, it should: -1. Run `validate_ai_docs_sync.py` +1. Run `uv run ai-validate-docs` 2. If validation fails, prompt to fix or continue with `--yes` 3. Log validation results to EXECUTION file @@ -363,8 +361,8 @@ project/ │ ├── project-context.md.jinja │ └── documentation-sync-rules.md.jinja │ -└── src/python_modern_template/ - └── validate_ai_docs_sync.py # Validation script +└── scripts/ai_tools/ + └── validate_ai_docs_sync.py # Validation script (CLI: ai-validate-docs) ``` --- @@ -411,7 +409,7 @@ project/ 1. **Use the validation script** ```bash # After any doc change - uv run python src/python_modern_template/validate_ai_docs_sync.py + uv run ai-validate-docs ``` 2. **Reference, don't duplicate** @@ -437,7 +435,7 @@ project/ # Correct: Update both vim AI_DOCS/code-conventions.md vim template/AI_DOCS/code-conventions.md.jinja - uv run python src/python_modern_template/validate_ai_docs_sync.py + uv run ai-validate-docs ``` --- @@ -448,7 +446,7 @@ project/ **Quick Checklist:** 1. ✅ Update the changed file -2. ✅ Run `validate_ai_docs_sync.py` +2. ✅ Run `uv run ai-validate-docs` 3. ⚠️ Update manual sync files if needed (Gemini/Copilot) 4. ✅ Update .claude/skills or .claude/agents if relevant 5. ✅ Update template files diff --git a/CLAUDE.md b/CLAUDE.md index 2dccc0a..7e7cab2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -302,7 +302,7 @@ Before completing any task: - [ ] Formatter harmony check (Black vs Ruff) — adjust code (e.g., use message variables) if tools disagree - [ ] **AI documentation synchronized** (MANDATORY) - - [ ] Run `uv run python src/python_modern_template/validate_ai_docs_sync.py` + - [ ] Run `uv run ai-validate-docs` - [ ] Update `.gemini/styleguide.md` if AI_DOCS changed (add sync date) - [ ] Update `.github/copilot-instructions.md` if AI_DOCS changed (add sync date) - [ ] Update `.claude/skills/` or `.claude/agents/` if relevant diff --git a/pyproject.toml b/pyproject.toml index 4c0bf34..3fa1d26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ ai-context-summary = "scripts.ai_tools.context_summary:main" ai-check-conflicts = "scripts.ai_tools.check_conflicts:main" ai-add-decision = "scripts.ai_tools.add_decision:main" ai-add-convention = "scripts.ai_tools.add_convention:main" +ai-validate-docs = "scripts.ai_tools.validate_ai_docs_sync:main" # Quality Tools - Single source of truth for quality checks quality-format = "scripts.quality.format:main" diff --git a/src/python_modern_template/validate_ai_docs_sync.py b/scripts/ai_tools/validate_ai_docs_sync.py similarity index 99% rename from src/python_modern_template/validate_ai_docs_sync.py rename to scripts/ai_tools/validate_ai_docs_sync.py index 7829056..bf23a89 100644 --- a/src/python_modern_template/validate_ai_docs_sync.py +++ b/scripts/ai_tools/validate_ai_docs_sync.py @@ -271,7 +271,7 @@ def generate_sync_report(issues: list[dict[str, str]]) -> str: 3. **Verify fixes:** ```bash - python -m python_modern_template.validate_ai_docs_sync + uv run ai-validate-docs ``` """ diff --git a/sync_template.py b/sync_template.py index eb3c0d7..c4d73c5 100644 --- a/sync_template.py +++ b/sync_template.py @@ -171,6 +171,7 @@ def main() -> None: "template_loader.py", "update_plan.py", "utils.py", + "validate_ai_docs_sync.py", ], PROJECT_ROOT / "scripts" / "ai_tools", TEMPLATE_ROOT / "scripts" / "ai_tools", @@ -185,14 +186,6 @@ def main() -> None: sync_file(scripts_init_src, scripts_init_dst, add_jinja_ext=False) total += 1 - # Sync src module files - total += sync_file_list( - ["validate_ai_docs_sync.py"], - PROJECT_ROOT / "src" / "python_modern_template", - TEMPLATE_ROOT / "src" / "{{ package_name }}", - "📦 Syncing src module files...", - ) - print("\n" + "=" * 70) print(f"✅ SYNC COMPLETE: {total} files synced") print("=" * 70) diff --git a/template/AI_DOCS/documentation-sync-rules.md.jinja b/template/AI_DOCS/documentation-sync-rules.md.jinja index 724c07f..b75e84e 100644 --- a/template/AI_DOCS/documentation-sync-rules.md.jinja +++ b/template/AI_DOCS/documentation-sync-rules.md.jinja @@ -65,7 +65,7 @@ For agents that **support references** (Claude, Cursor, Aider, Gemini/AGENTS.md) - [ ] Ensure references point to correct files - [ ] Validate no broken references -**Script:** `uv run python src/{{ package_name }}/validate_ai_docs_sync.py` +**Script:** `uv run ai-validate-docs` ### Step 3: Update Non-Supporting Agents @@ -114,7 +114,7 @@ For agents that **don't support references** (Gemini/styleguide, Copilot): **Run validation script:** ```bash -uv run python src/{{ package_name }}/validate_ai_docs_sync.py +uv run ai-validate-docs ``` **Should report:** @@ -191,7 +191,7 @@ uv run python src/{{ package_name }}/validate_ai_docs_sync.py ## Validation Script -**Location:** `src/{{ package_name }}/validate_ai_docs_sync.py` +**Location:** `scripts/ai_tools/validate_ai_docs_sync.py` **What it checks:** 1. All `@AI_DOCS/` references point to existing files @@ -203,7 +203,7 @@ uv run python src/{{ package_name }}/validate_ai_docs_sync.py **Usage:** ```bash # Run validation -uv run python src/{{ package_name }}/validate_ai_docs_sync.py +uv run ai-validate-docs # Expected output (all passing): ✅ All AI_DOCS files found @@ -232,8 +232,7 @@ uv run python src/{{ package_name }}/validate_ai_docs_sync.py - Quality tests (5 files): `tests/quality/*.py` → `template/tests/quality/*.py.jinja` - Project tests (4 files): `tests/test_*.py` → `template/tests/test_*.py.jinja` - Quality scripts (6 files): `scripts/quality/*.py` → `template/scripts/quality/*.py.jinja` -- AI tools scripts (11 files): `scripts/ai_tools/*.py` → `template/scripts/ai_tools/*.py.jinja` -- Source modules (1 file): `src/{{ package_name }}/validate_ai_docs_sync.py.jinja` +- AI tools scripts (12 files): `scripts/ai_tools/*.py` → `template/scripts/ai_tools/*.py.jinja` **Note:** AI tools tests are NOT synced to the template. They remain in the template repository only, as they test template infrastructure that users typically won't modify. @@ -253,7 +252,7 @@ python sync_template.py **When to run:** 1. After modifying any test files in `tests/` 2. After modifying any scripts in `scripts/quality/` or `scripts/ai_tools/` -3. After modifying `src/{{ package_name }}/validate_ai_docs_sync.py` +3. After modifying `scripts/ai_tools/validate_ai_docs_sync.py` 4. Before releasing a new template version **What it doesn't sync (intentionally):** @@ -306,7 +305,7 @@ In `AI_DOCS/code-conventions.md` and all agent configs, add: - [ ] `make lint` passes - [ ] `make check` passes - [ ] **AI documentation synchronized** ⭐ NEW - - [ ] Validated with `validate_ai_docs_sync.py` + - [ ] Validated with `uv run ai-validate-docs` - [ ] Updated manual sync files if needed - [ ] Updated .claude/skills or .claude/agents if needed - [ ] Updated template files if needed @@ -314,7 +313,7 @@ In `AI_DOCS/code-conventions.md` and all agent configs, add: ### Integration with ai-finish-task Before `ai-finish-task` completes, it should: -1. Run `validate_ai_docs_sync.py` +1. Run `uv run ai-validate-docs` 2. If validation fails, prompt to fix or continue with `--yes` 3. Log validation results to EXECUTION file @@ -363,8 +362,8 @@ project/ │ ├── project-context.md.jinja │ └── documentation-sync-rules.md.jinja │ -└── src/{{ package_name }}/ - └── validate_ai_docs_sync.py # Validation script +└── scripts/ai_tools/ + └── validate_ai_docs_sync.py # Validation script (CLI: ai-validate-docs) ``` --- @@ -411,7 +410,7 @@ project/ 1. **Use the validation script** ```bash # After any doc change - uv run python src/{{ package_name }}/validate_ai_docs_sync.py + uv run ai-validate-docs ``` 2. **Reference, don't duplicate** @@ -437,7 +436,7 @@ project/ # Correct: Update both vim AI_DOCS/code-conventions.md vim template/AI_DOCS/code-conventions.md.jinja - uv run python src/{{ package_name }}/validate_ai_docs_sync.py + uv run ai-validate-docs ``` --- @@ -448,7 +447,7 @@ project/ **Quick Checklist:** 1. ✅ Update the changed file -2. ✅ Run `validate_ai_docs_sync.py` +2. ✅ Run `uv run ai-validate-docs` 3. ⚠️ Update manual sync files if needed (Gemini/Copilot) 4. ✅ Update .claude/skills or .claude/agents if relevant 5. ✅ Update template files diff --git a/template/scripts/ai_tools/finish_task.py.jinja b/template/scripts/ai_tools/finish_task.py.jinja index 0569880..15ce574 100644 --- a/template/scripts/ai_tools/finish_task.py.jinja +++ b/template/scripts/ai_tools/finish_task.py.jinja @@ -244,6 +244,11 @@ def finish_task( print_error(f"Session files not found for session {session_id}") sys.exit(1) + # Type narrowing: we know these are Path objects after the check + assert plan_file is not None + assert summary_file is not None + assert execution_file is not None + # Read files plan_content = plan_file.read_text() summary_file.read_text() diff --git a/template/src/{{ package_name }}/validate_ai_docs_sync.py.jinja b/template/scripts/ai_tools/validate_ai_docs_sync.py.jinja similarity index 99% rename from template/src/{{ package_name }}/validate_ai_docs_sync.py.jinja rename to template/scripts/ai_tools/validate_ai_docs_sync.py.jinja index 37e39d5..bf23a89 100644 --- a/template/src/{{ package_name }}/validate_ai_docs_sync.py.jinja +++ b/template/scripts/ai_tools/validate_ai_docs_sync.py.jinja @@ -271,7 +271,7 @@ Everything is synchronized! 🎉 3. **Verify fixes:** ```bash - python -m {{ package_name }}.validate_ai_docs_sync + uv run ai-validate-docs ``` """ diff --git a/template/tests/ai_tools/test_finish_task.py.jinja b/template/tests/ai_tools/test_finish_task.py.jinja new file mode 100644 index 0000000..7490fd9 --- /dev/null +++ b/template/tests/ai_tools/test_finish_task.py.jinja @@ -0,0 +1,168 @@ +"""Tests for ai-finish-task tool.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from scripts.ai_tools.finish_task import finish_task + + +def test_finish_task_with_yes_flag_bypasses_incomplete_plan_prompt( + temp_context_dir: Path, sample_session_files: dict[str, Path] +) -> None: + """Test --yes flag bypasses prompt when plan is incomplete.""" + # Arrange + plan_file = sample_session_files["plan"] + execution_file = sample_session_files["execution"] + + # Create incomplete plan (0 items checked) + plan_content = """# Task Plan: Test Task + +## Phase 1: Setup +- [ ] Item 1 +- [ ] Item 2 +""" + plan_file.write_text(plan_content) + + # Add minimal execution content + execution_file.write_text("[2025-11-03 16:00:00] Started task\n") + + # Act & Assert - should NOT prompt and should complete successfully + with ( + patch( + "scripts.ai_tools.utils.get_sessions_dir", + return_value=temp_context_dir / "sessions", + ), + patch( + "builtins.input", side_effect=AssertionError("input() should not be called") + ), + ): + # This should not raise AssertionError because input() shouldn't be called + finish_task(summary="Test summary", session_id=None, yes=True) + + +def test_finish_task_with_yes_flag_bypasses_make_check_prompt( + temp_context_dir: Path, sample_session_files: dict[str, Path] +) -> None: + """Test --yes flag bypasses prompt when make check not run.""" + # Arrange + plan_file = sample_session_files["plan"] + execution_file = sample_session_files["execution"] + + # Create complete plan + plan_content = """# Task Plan: Test Task + +## Phase 1: Setup +- [x] Item 1 +- [x] Item 2 +""" + plan_file.write_text(plan_content) + + # Execution without 'make check' + execution_file.write_text("[2025-11-03 16:00:00] Started task\n") + + # Act & Assert - should NOT prompt + with ( + patch( + "scripts.ai_tools.utils.get_sessions_dir", + return_value=temp_context_dir / "sessions", + ), + patch( + "builtins.input", side_effect=AssertionError("input() should not be called") + ), + ): + finish_task(summary="Test summary", session_id=None, yes=True) + + +def test_finish_task_without_yes_flag_prompts_on_incomplete_plan( + temp_context_dir: Path, sample_session_files: dict[str, Path] +) -> None: + """Test without --yes flag prompts user when plan incomplete.""" + # Arrange + plan_file = sample_session_files["plan"] + execution_file = sample_session_files["execution"] + + plan_content = """# Task Plan: Test Task + +## Phase 1: Setup +- [ ] Item 1 +""" + plan_file.write_text(plan_content) + execution_file.write_text("[2025-11-03 16:00:00] Started\n") + + # Act & Assert - should call input() and exit when user says 'n' + with ( + patch( + "scripts.ai_tools.utils.get_sessions_dir", + return_value=temp_context_dir / "sessions", + ), + patch("builtins.input", return_value="n"), + pytest.raises(SystemExit) as exc_info, + ): + finish_task(summary="Test", session_id=None, yes=False) + + assert exc_info.value.code == 0 + + +def test_finish_task_without_yes_flag_prompts_on_missing_make_check( + temp_context_dir: Path, sample_session_files: dict[str, Path] +) -> None: + """Test without --yes flag prompts when make check missing.""" + # Arrange + plan_file = sample_session_files["plan"] + execution_file = sample_session_files["execution"] + + # Complete plan but no make check + plan_content = """# Task Plan: Test Task + +## Phase 1: Setup +- [x] Item 1 +""" + plan_file.write_text(plan_content) + execution_file.write_text("[2025-11-03 16:00:00] Started\n") + + # Act & Assert + with ( + patch( + "scripts.ai_tools.utils.get_sessions_dir", + return_value=temp_context_dir / "sessions", + ), + patch("builtins.input", return_value="n"), + pytest.raises(SystemExit) as exc_info, + ): + finish_task(summary="Test", session_id=None, yes=False) + + assert exc_info.value.code == 0 + + +def test_finish_task_yes_defaults_to_false( + temp_context_dir: Path, sample_session_files: dict[str, Path] +) -> None: + """Test yes parameter defaults to False for backward compatibility.""" + # Arrange + plan_file = sample_session_files["plan"] + execution_file = sample_session_files["execution"] + + plan_content = """# Task Plan: Test Task + +## Phase 1: Setup +- [x] Item 1 +""" + plan_file.write_text(plan_content) + + # Add make check to execution + execution_content = """[2025-11-03 16:00:00] Started task +[2025-11-03 16:01:00] Running make check +""" + execution_file.write_text(execution_content) + + # Act - call without yes parameter (should default to False, but pass checks) + with patch( + "scripts.ai_tools.utils.get_sessions_dir", + return_value=temp_context_dir / "sessions", + ): + # Should complete successfully without prompts since all checks pass + finish_task(summary="Test summary", session_id=None) diff --git a/template/tests/ai_tools/test_template_loader.py.jinja b/template/tests/ai_tools/test_template_loader.py.jinja new file mode 100644 index 0000000..b9afb99 --- /dev/null +++ b/template/tests/ai_tools/test_template_loader.py.jinja @@ -0,0 +1,276 @@ +"""Tests for AI template loader module. + +This module tests template loading and variable substitution for +task-specific plan templates. +""" + +from __future__ import annotations + +import pytest + +from scripts.ai_tools.template_loader import ( + TemplateNotFoundError, + get_template_path, + load_template, + substitute_variables, +) + + +class TestGetTemplatePath: + """Test getting template file paths.""" + + def test_feature_template_path(self) -> None: + """Test getting path for feature template.""" + path = get_template_path("feature") + assert path.name == "feature.md" + assert "templates" in str(path) + + def test_bugfix_template_path(self) -> None: + """Test getting path for bugfix template.""" + path = get_template_path("bugfix") + assert path.name == "bugfix.md" + + def test_docs_template_path(self) -> None: + """Test getting path for docs template.""" + path = get_template_path("docs") + assert path.name == "docs.md" + + def test_refactor_template_path(self) -> None: + """Test getting path for refactor template.""" + path = get_template_path("refactor") + assert path.name == "refactor.md" + + def test_unknown_type_raises_error(self) -> None: + """Test that unknown task type raises error.""" + with pytest.raises(TemplateNotFoundError) as exc_info: + get_template_path("unknown_type") + + error_message = str(exc_info.value) + assert "unknown_type" in error_message.lower() + + +class TestSubstituteVariables: + """Test template variable substitution.""" + + def test_substitute_session_id(self) -> None: + """Test substituting session ID variable.""" + template = "Session: {{session_id}}" + result = substitute_variables( + template, session_id="20251103120000", task_name="Test", task_type="feature" + ) + assert result == "Session: 20251103120000" + + def test_substitute_task_name(self) -> None: + """Test substituting task name variable.""" + template = "Task: {{task_name}}" + result = substitute_variables( + template, session_id="123", task_name="Add validation", task_type="feature" + ) + assert result == "Task: Add validation" + + def test_substitute_task_type(self) -> None: + """Test substituting task type variable.""" + template = "Type: {{task_type}}" + result = substitute_variables( + template, session_id="123", task_name="Test", task_type="bugfix" + ) + assert result == "Type: bugfix" + + def test_substitute_timestamp(self) -> None: + """Test substituting timestamp variable.""" + template = "Created: {{timestamp}}" + result = substitute_variables( + template, session_id="123", task_name="Test", task_type="feature" + ) + # Should contain a timestamp in format YYYY-MM-DD HH:MM:SS + assert "Created: " in result + assert len(result.split(": ")[1]) > 10 # Timestamp has length + + def test_substitute_multiple_variables(self) -> None: + """Test substituting multiple variables at once.""" + template = "{{session_id}}: {{task_name}} ({{task_type}})" + result = substitute_variables( + template, + session_id="20251103120000", + task_name="Test task", + task_type="feature", + ) + assert result == "20251103120000: Test task (feature)" + + def test_substitute_same_variable_multiple_times(self) -> None: + """Test substituting same variable appearing multiple times.""" + template = "Start: {{task_name}}, End: {{task_name}}" + result = substitute_variables( + template, session_id="123", task_name="Build", task_type="feature" + ) + assert result == "Start: Build, End: Build" + + def test_no_variables_returns_unchanged(self) -> None: + """Test template without variables returns unchanged.""" + template = "This is a plain template" + result = substitute_variables( + template, session_id="123", task_name="Test", task_type="feature" + ) + assert result == template + + +class TestLoadTemplate: + """Test loading complete templates.""" + + def test_load_feature_template(self) -> None: + """Test loading feature template with substitution.""" + result = load_template( + task_type="feature", + session_id="20251103120000", + task_name="Add user authentication", + ) + + # Check structure + assert "# Task Plan: Add user authentication" in result + assert "**Session ID**: 20251103120000" in result + assert "**Task Type**: feature" in result + assert "**Status**: 🚧 In Progress" in result + + # Check phases specific to feature + assert "Phase 1: Research & Design" in result + assert "Phase 2: Write Tests (TDD)" in result + assert "Phase 3: Implementation" in result + assert "Phase 4: Quality Checks" in result + assert "Phase 5: Documentation" in result + + def test_load_bugfix_template(self) -> None: + """Test loading bugfix template with different phases.""" + result = load_template( + task_type="bugfix", + session_id="20251103120000", + task_name="Fix validation bug", + ) + + # Check structure + assert "# Task Plan: Fix validation bug" in result + assert "**Task Type**: bugfix" in result + + # Check phases specific to bugfix + assert "Phase 1: Reproduce Bug" in result + assert "Phase 2: Write Regression Test" in result + assert "Phase 3: Fix Implementation" in result + assert "Phase 4: Verify Fix" in result + assert "Phase 5: Quality Checks" in result + + def test_load_docs_template_no_testing_phase(self) -> None: + """Test loading docs template doesn't include testing phase.""" + result = load_template( + task_type="docs", + session_id="20251103120000", + task_name="Update README", + ) + + # Check structure + assert "# Task Plan: Update README" in result + assert "**Task Type**: docs" in result + + # Docs should NOT have testing phase + assert "Write Tests" not in result + assert "Write Regression Test" not in result + + # Should have docs-specific phases + assert "Phase 1: Review Current Docs" in result or "Review" in result + assert "Phase 2: Update Documentation" in result or "Update" in result + + def test_load_refactor_template(self) -> None: + """Test loading refactor template.""" + result = load_template( + task_type="refactor", + session_id="20251103120000", + task_name="Refactor validators", + ) + + # Check structure + assert "# Task Plan: Refactor validators" in result + assert "**Task Type**: refactor" in result + + # Check phases specific to refactor + assert "Phase 1: Ensure Test Coverage" in result or "Test Coverage" in result + assert "Phase 2: Refactor Code" in result or "Refactor" in result + assert "Phase 3: Verify Tests Pass" in result or "Verify" in result + + def test_load_template_with_special_characters_in_name(self) -> None: + """Test loading template with special characters in task name.""" + result = load_template( + task_type="feature", + session_id="20251103120000", + task_name="Add email validation & phone verification", + ) + + # Should handle special characters properly + assert "Add email validation & phone verification" in result + + def test_unknown_task_type_raises_error(self) -> None: + """Test loading unknown task type raises appropriate error.""" + with pytest.raises(TemplateNotFoundError) as exc_info: + load_template( + task_type="invalid_type", + session_id="123", + task_name="Test", + ) + + error_message = str(exc_info.value) + assert "invalid_type" in error_message.lower() + assert "feature" in error_message.lower() # Should suggest valid types + + +class TestTemplateContent: + """Test that loaded templates have required sections.""" + + @pytest.mark.parametrize( + "task_type", + ["feature", "bugfix", "docs", "refactor"], + ) + def test_template_has_required_sections(self, task_type: str) -> None: + """Test all templates have required sections.""" + result = load_template(task_type=task_type, session_id="123", task_name="Test") + + # All templates should have these sections + required_sections = [ + "# Task Plan:", + "**Session ID**:", + "**Created**:", + "**Task Type**:", + "**Status**:", + "## Objective", + "## Context", + "## Implementation Steps", + "## Files to Change", + "## Risks & Considerations", + "## Notes", + ] + + for section in required_sections: + assert section in result, f"Template {task_type} missing section: {section}" + + @pytest.mark.parametrize( + "task_type", + ["feature", "bugfix", "docs", "refactor"], + ) + def test_template_has_checkboxes(self, task_type: str) -> None: + """Test all templates have checkbox items.""" + result = load_template(task_type=task_type, session_id="123", task_name="Test") + + # Should have multiple checkbox items + checkbox_count = result.count("- [ ]") + assert checkbox_count >= 5, f"Template {task_type} has too few checkboxes" + + def test_templates_are_different(self) -> None: + """Test that different task types have different content.""" + feature = load_template(task_type="feature", session_id="123", task_name="Test") + bugfix = load_template(task_type="bugfix", session_id="123", task_name="Test") + docs = load_template(task_type="docs", session_id="123", task_name="Test") + refactor = load_template( + task_type="refactor", session_id="123", task_name="Test" + ) + + # Templates should be different + assert feature != bugfix + assert feature != docs + assert feature != refactor + assert bugfix != docs diff --git a/template/tests/ai_tools/test_update_plan_backward_compat.py.jinja b/template/tests/ai_tools/test_update_plan_backward_compat.py.jinja new file mode 100644 index 0000000..5edaa31 --- /dev/null +++ b/template/tests/ai_tools/test_update_plan_backward_compat.py.jinja @@ -0,0 +1,325 @@ +"""Backward compatibility tests for ai-update-plan. + +CRITICAL: These tests ensure that extending ai-update-plan with new editing +features does NOT break existing functionality. All existing commands must +continue to work exactly as before. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any +from unittest.mock import patch + +import pytest + +from scripts.ai_tools.update_plan import update_plan + +# Patch targets - extracted to avoid Black/Ruff formatting conflicts +PATCH_GET_CURRENT_SESSION = "scripts.ai_tools.update_plan.get_current_session" +PATCH_GET_SESSION_FILES = "scripts.ai_tools.update_plan.get_session_files" + + +@pytest.fixture +def mock_session_setup(tmp_path: Path) -> tuple[str, Path]: + """Set up mock session with plan file. + + Args: + tmp_path: Pytest temporary directory + + Returns: + Tuple of (session_id, plan_file_path) + """ + session_id = "20251103120000" + plan_content = """# Task Plan: Test + +### Phase 1: Testing +- [ ] Write tests +- [ ] Run tests +- [x] Completed task + +### Phase 2: Implementation +- [ ] Implement feature +""" + + # Create mock plan file + plan_file = tmp_path / f"{session_id}-PLAN-test.md" + plan_file.write_text(plan_content) + + return session_id, plan_file + + +class TestBackwardCompatibilityCheckboxMode: + """Test that existing checkbox functionality still works.""" + + def test_check_item_still_works(self, mock_session_setup: tuple[str, Path]) -> None: + """Test that checking an item works exactly as before.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Old command: ai-update-plan "Write tests" + update_plan(item="Write tests", check=True, session_id=session_id) + + # Verify item was checked + updated_content = plan_file.read_text() + assert "- [x] Write tests" in updated_content + + def test_uncheck_item_still_works( + self, mock_session_setup: tuple[str, Path] + ) -> None: + """Test that unchecking an item works exactly as before.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Old command: ai-update-plan "Completed task" --uncheck + update_plan(item="Completed task", uncheck=True, session_id=session_id) + + # Verify item was unchecked + updated_content = plan_file.read_text() + assert "- [ ] Completed task" in updated_content + + def test_show_plan_still_works( + self, mock_session_setup: tuple[str, Path], capsys: Any + ) -> None: + """Test that showing plan works exactly as before.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Old command: ai-update-plan --show + update_plan(show=True, session_id=session_id) + + # Verify output contains plan + captured = capsys.readouterr() + assert "# Task Plan: Test" in captured.out + assert "Progress:" in captured.out + + def test_default_behavior_unchanged( + self, mock_session_setup: tuple[str, Path] + ) -> None: + """Test that default behavior (check) is unchanged.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Old command: ai-update-plan "Run tests" (defaults to check=True) + update_plan(item="Run tests", session_id=session_id) + + # Should check the item by default + updated_content = plan_file.read_text() + assert "- [x] Run tests" in updated_content + + def test_partial_match_still_works( + self, mock_session_setup: tuple[str, Path] + ) -> None: + """Test that partial matching still works as before.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Old behavior: partial match on "Implement" + update_plan(item="Implement", session_id=session_id) + + # Should match "Implement feature" + updated_content = plan_file.read_text() + assert "- [x] Implement feature" in updated_content + + +class TestBackwardCompatibilityModeDetection: + """Test that mode detection correctly identifies checkbox vs edit mode.""" + + def test_no_edit_flags_uses_checkbox_mode( + self, mock_session_setup: tuple[str, Path] + ) -> None: + """Test that absence of edit flags uses checkbox mode.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # No edit flags provided = checkbox mode + update_plan(item="Write tests", session_id=session_id) + + # Should toggle checkbox, not try to edit + updated_content = plan_file.read_text() + assert "- [x] Write tests" in updated_content + # Plan structure unchanged + assert "### Phase 1: Testing" in updated_content + + def test_item_parameter_alone_triggers_checkbox_mode( + self, mock_session_setup: tuple[str, Path] + ) -> None: + """Test that providing only item parameter triggers checkbox mode.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Just item parameter = checkbox mode (not edit mode) + update_plan(item="Write tests", session_id=session_id) + + # Should check the item + updated_content = plan_file.read_text() + assert "- [x] Write tests" in updated_content + + +class TestBackwardCompatibilityErrorHandling: + """Test that error handling is unchanged for existing functionality.""" + + def test_item_not_found_error_unchanged( + self, mock_session_setup: tuple[str, Path] + ) -> None: + """Test that item not found error is unchanged.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Should exit with error when item not found + with pytest.raises(SystemExit): + update_plan(item="Non-existent item", session_id=session_id) + + def test_no_session_error_unchanged(self) -> None: + """Test that no session error is unchanged.""" + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: + mock_session.return_value = None + + # Should exit with error when no session + with pytest.raises(SystemExit): + update_plan(item="Test item") + + def test_missing_item_without_show_error_unchanged( + self, mock_session_setup: tuple[str, Path] + ) -> None: + """Test that missing item parameter error is unchanged.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Should exit with error when item not provided and not showing + with pytest.raises(SystemExit): + update_plan(item=None, show=False, session_id=session_id) + + +class TestBackwardCompatibilityOutputFormat: + """Test that output format is unchanged for existing commands.""" + + def test_success_message_format_unchanged( + self, mock_session_setup: tuple[str, Path], capsys: Any + ) -> None: + """Test that success message format is unchanged.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + update_plan(item="Write tests", session_id=session_id) + + captured = capsys.readouterr() + # Should contain session ID and progress + assert session_id in captured.out + assert "Progress:" in captured.out + assert "items complete" in captured.out + + def test_progress_percentage_displayed( + self, mock_session_setup: tuple[str, Path], capsys: Any + ) -> None: + """Test that progress percentage is still displayed.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + update_plan(item="Write tests", session_id=session_id) + + captured = capsys.readouterr() + # Should show percentage (e.g., "50%") + assert "%" in captured.out + + +class TestBackwardCompatibilityIntegration: + """Integration tests for backward compatibility.""" + + def test_multiple_checkbox_operations_work( + self, mock_session_setup: tuple[str, Path] + ) -> None: + """Test that multiple checkbox operations work sequentially.""" + session_id, plan_file = mock_session_setup + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Check first item + update_plan(item="Write tests", session_id=session_id) + content1 = plan_file.read_text() + assert "- [x] Write tests" in content1 + + # Check second item + update_plan(item="Run tests", session_id=session_id) + content2 = plan_file.read_text() + assert "- [x] Run tests" in content2 + assert "- [x] Write tests" in content2 # First still checked + + # Uncheck first item + update_plan(item="Write tests", uncheck=True, session_id=session_id) + content3 = plan_file.read_text() + assert "- [ ] Write tests" in content3 + assert "- [x] Run tests" in content3 # Second still checked + + def test_original_workflow_unchanged( + self, mock_session_setup: tuple[str, Path] + ) -> None: + """Test that the original workflow is completely unchanged.""" + session_id, plan_file = mock_session_setup + + plan_file.read_text() + + with patch(PATCH_GET_CURRENT_SESSION) as mock_session: # noqa: SIM117 + with patch(PATCH_GET_SESSION_FILES) as mock_files: + mock_session.return_value = session_id + mock_files.return_value = {"plan": plan_file} + + # Simulate original workflow: show, check, show + update_plan(show=True, session_id=session_id) + update_plan(item="Write tests", session_id=session_id) + update_plan(show=True, session_id=session_id) + + # Verify plan structure unchanged (except for checked item) + updated_content = plan_file.read_text() + assert "# Task Plan: Test" in updated_content + assert "### Phase 1: Testing" in updated_content + assert "### Phase 2: Implementation" in updated_content + # Only difference: Write tests is now checked + assert "- [x] Write tests" in updated_content diff --git a/template/tests/ai_tools/test_update_plan_extended.py.jinja b/template/tests/ai_tools/test_update_plan_extended.py.jinja new file mode 100644 index 0000000..c54cd8f --- /dev/null +++ b/template/tests/ai_tools/test_update_plan_extended.py.jinja @@ -0,0 +1,336 @@ +"""Tests for extended ai-update-plan editing functionality. + +This module tests the new editing capabilities (add, remove, rename) +added to ai-update-plan while ensuring backward compatibility. +""" + +from __future__ import annotations + +from scripts.ai_tools.update_plan import ( + add_item_to_plan, + add_phase_to_plan, + find_phase_section, + remove_item_from_plan, + rename_item_in_plan, +) + + +class TestAddItemToPlan: + """Test adding new checklist items to plan.""" + + def test_add_item_to_end_of_phase(self) -> None: + """Test adding item to end of a phase.""" + plan = """### Phase 1: Test +- [ ] Existing item 1 +- [ ] Existing item 2 + +### Phase 2: Next""" + + result = add_item_to_plan(plan, "New item", phase="Phase 1") + + assert "- [ ] New item" in result + # Verify it's in Phase 1 section (before Phase 2) + phase1_start = result.index("### Phase 1") + phase2_start = result.index("### Phase 2") + new_item_pos = result.index("- [ ] New item") + assert phase1_start < new_item_pos < phase2_start + + def test_add_item_without_phase_adds_to_current(self) -> None: + """Test adding item without specifying phase adds to last phase.""" + plan = """### Phase 1: First +- [ ] Item 1 + +### Phase 2: Second +- [ ] Item 2""" + + result = add_item_to_plan(plan, "New item") + + # Should add to last phase (Phase 2) + assert "### Phase 2: Second" in result + assert "- [ ] New item" in result + # Verify it's in Phase 2 section + phase2_start = result.index("### Phase 2") + new_item_pos = result.index("- [ ] New item") + assert new_item_pos > phase2_start + + def test_add_item_creates_phase_if_not_exists(self) -> None: + """Test adding item to non-existent phase creates it.""" + plan = """### Phase 1: Test +- [ ] Item 1""" + + result = add_item_to_plan(plan, "New item", phase="Phase 3") + + assert "### Phase 3:" in result or "Phase 3" not in result + # If phase doesn't exist, should still add item (to end) + assert "- [ ] New item" in result + + def test_add_item_with_special_characters(self) -> None: + """Test adding item with special characters.""" + plan = """### Phase 1: Test +- [ ] Item 1""" + + result = add_item_to_plan( + plan, + "Test with special chars: @#$% and (parentheses)", + ) + + assert "- [ ] Test with special chars: @#$% and (parentheses)" in result + + def test_add_item_preserves_existing_content(self) -> None: + """Test that adding item preserves all existing content.""" + plan = """# Plan +### Phase 1: Test +- [ ] Item 1 +- [x] Item 2 + +## Notes +Some notes here""" + + result = add_item_to_plan(plan, "New item", phase="Phase 1") + + # All original content should be preserved + assert "# Plan" in result + assert "- [ ] Item 1" in result + assert "- [x] Item 2" in result # Checked state preserved + assert "## Notes" in result + assert "Some notes here" in result + # Plus new item + assert "- [ ] New item" in result + + +class TestRemoveItemFromPlan: + """Test removing checklist items from plan.""" + + def test_remove_item_by_exact_match(self) -> None: + """Test removing item with exact text match.""" + plan = """### Phase 1: Test +- [ ] Item to remove +- [ ] Item to keep""" + + result = remove_item_from_plan(plan, "Item to remove") + + assert "Item to remove" not in result + assert "Item to keep" in result + + def test_remove_item_by_partial_match(self) -> None: + """Test removing item with partial text match.""" + plan = """### Phase 1: Test +- [ ] Write test file(s) for validators +- [ ] Write other tests""" + + result = remove_item_from_plan(plan, "test file") + + # Should remove the item containing "test file" + assert "Write test file(s) for validators" not in result + assert "Write other tests" in result + + def test_remove_item_preserves_checked_state_of_others(self) -> None: + """Test that removing item preserves checked state of other items.""" + plan = """### Phase 1: Test +- [ ] Item to remove +- [x] Completed item +- [ ] Pending item""" + + result = remove_item_from_plan(plan, "Item to remove") + + assert "Item to remove" not in result + assert "- [x] Completed item" in result # Checked state preserved + assert "- [ ] Pending item" in result + + def test_remove_item_not_found_returns_unchanged(self) -> None: + """Test that trying to remove non-existent item returns plan unchanged.""" + plan = """### Phase 1: Test +- [ ] Item 1 +- [ ] Item 2""" + + result = remove_item_from_plan(plan, "Non-existent item") + + # Plan should be unchanged + assert result == plan + + def test_remove_multiple_matching_items_removes_first(self) -> None: + """Test that removing item with multiple matches removes first occurrence.""" + plan = """### Phase 1: Test +- [ ] Test item +- [ ] Test item""" + + result = remove_item_from_plan(plan, "Test item") + + # Should remove only first occurrence + assert result.count("Test item") == 1 + + +class TestRenameItemInPlan: + """Test renaming checklist items in plan.""" + + def test_rename_item_by_exact_match(self) -> None: + """Test renaming item with exact match.""" + plan = """### Phase 1: Test +- [ ] Old item name +- [ ] Other item""" + + result = rename_item_in_plan(plan, "Old item name", "New item name") + + assert "Old item name" not in result + assert "- [ ] New item name" in result + assert "Other item" in result # Other items unchanged + + def test_rename_item_by_partial_match(self) -> None: + """Test renaming item with partial match.""" + plan = """### Phase 1: Test +- [ ] Write test file(s) +- [ ] Other task""" + + result = rename_item_in_plan( + plan, "test file", "Write tests/test_validators.py" + ) + + assert "Write test file(s)" not in result + assert "- [ ] Write tests/test_validators.py" in result + + def test_rename_preserves_checkbox_state(self) -> None: + """Test that renaming preserves checkbox state (checked/unchecked).""" + plan = """### Phase 1: Test +- [x] Completed old name +- [ ] Pending old name""" + + # Rename checked item + result = rename_item_in_plan(plan, "Completed old name", "Completed new name") + assert "- [x] Completed new name" in result + + # Rename unchecked item + result2 = rename_item_in_plan(plan, "Pending old name", "Pending new name") + assert "- [ ] Pending new name" in result2 + + def test_rename_item_not_found_returns_unchanged(self) -> None: + """Test that renaming non-existent item returns plan unchanged.""" + plan = """### Phase 1: Test +- [ ] Item 1""" + + result = rename_item_in_plan(plan, "Non-existent", "New name") + + assert result == plan + + def test_rename_multiple_matches_renames_first(self) -> None: + """Test that renaming with multiple matches renames first occurrence.""" + plan = """### Phase 1: Test +- [ ] Duplicate item +- [ ] Duplicate item""" + + result = rename_item_in_plan(plan, "Duplicate item", "Renamed item") + + # Should rename only first occurrence + assert "- [ ] Renamed item" in result + assert result.count("Renamed item") == 1 + assert result.count("Duplicate item") == 1 + + +class TestFindPhaseSection: + """Test finding phase sections in plan.""" + + def test_find_phase_by_name(self) -> None: + """Test finding phase section by phase name.""" + plan = """### Phase 1: Research +- [ ] Item 1 + +### Phase 2: Implementation +- [ ] Item 2 + +### Phase 3: Testing""" + + result = find_phase_section(plan, "Phase 2") + + assert "### Phase 2: Implementation" in result + assert "- [ ] Item 2" in result + # Should not include other phases + assert "Phase 1" not in result + assert "Phase 3" not in result + + def test_find_phase_by_partial_name(self) -> None: + """Test finding phase by partial name match.""" + plan = """### Phase 1: Write Tests (TDD) +- [ ] Item 1""" + + result = find_phase_section(plan, "Write Tests") + + assert "### Phase 1: Write Tests (TDD)" in result + assert "- [ ] Item 1" in result + + def test_find_phase_not_found_returns_empty(self) -> None: + """Test that finding non-existent phase returns empty string.""" + plan = """### Phase 1: Test +- [ ] Item 1""" + + result = find_phase_section(plan, "Non-existent Phase") + + assert result == "" + + def test_find_phase_includes_all_items_until_next_phase(self) -> None: + """Test that phase section includes all items until next phase header.""" + plan = """### Phase 1: Test +- [ ] Item 1 +- [ ] Item 2 +- [ ] Item 3 + +### Phase 2: Next""" + + result = find_phase_section(plan, "Phase 1") + + assert "- [ ] Item 1" in result + assert "- [ ] Item 2" in result + assert "- [ ] Item 3" in result + assert "Phase 2" not in result + + +class TestAddPhaseToPlan: + """Test adding new phase sections to plan.""" + + def test_add_phase_to_end(self) -> None: + """Test adding new phase section to end of plan.""" + plan = """### Phase 1: Test +- [ ] Item 1 + +## Notes +Some notes""" + + result = add_phase_to_plan(plan, "Phase 2: New Phase") + + assert "### Phase 2: New Phase" in result + # Should be added before ## Notes section + notes_pos = result.index("## Notes") + phase2_pos = result.index("### Phase 2") + assert phase2_pos < notes_pos + + def test_add_phase_with_items(self) -> None: + """Test adding phase with initial checklist items.""" + plan = """### Phase 1: Test +- [ ] Item 1""" + + items = ["New item 1", "New item 2"] + result = add_phase_to_plan(plan, "Phase 2: Implementation", items=items) + + assert "### Phase 2: Implementation" in result + assert "- [ ] New item 1" in result + assert "- [ ] New item 2" in result + + def test_add_phase_maintains_plan_structure(self) -> None: + """Test that adding phase maintains overall plan structure.""" + plan = """# Task Plan: Test + +### Phase 1: First +- [ ] Item 1 + +## Files to Change +- [ ] src/ + +## Notes +Some notes""" + + result = add_phase_to_plan(plan, "Phase 2: Second") + + # Structure should be maintained + assert "# Task Plan: Test" in result + assert "### Phase 1: First" in result + assert "### Phase 2: Second" in result + assert "## Files to Change" in result + assert "## Notes" in result diff --git a/template/tests/ai_tools/test_update_plan_fuzzy.py.jinja b/template/tests/ai_tools/test_update_plan_fuzzy.py.jinja new file mode 100644 index 0000000..b6aade4 --- /dev/null +++ b/template/tests/ai_tools/test_update_plan_fuzzy.py.jinja @@ -0,0 +1,195 @@ +"""Tests for Phase 3 enhanced features: fuzzy matching and validation. + +This module tests advanced fuzzy matching using Levenshtein distance, +better error messages with suggestions, and validation features. +""" + +from __future__ import annotations + +from scripts.ai_tools.update_plan import ( + calculate_levenshtein_distance, + find_similar_items, + validate_no_duplicates, +) + + +class TestLevenshteinDistance: + """Test Levenshtein distance calculation.""" + + def test_identical_strings_return_zero(self) -> None: + """Test that identical strings have distance 0.""" + assert calculate_levenshtein_distance("test", "test") == 0 + + def test_completely_different_strings(self) -> None: + """Test distance for completely different strings.""" + distance = calculate_levenshtein_distance("abc", "xyz") + assert distance == 3 # 3 substitutions needed + + def test_one_character_difference(self) -> None: + """Test distance for one character difference.""" + assert calculate_levenshtein_distance("test", "tset") == 2 # 2 ops (swap) + assert calculate_levenshtein_distance("test", "text") == 1 # 1 substitution + + def test_insertion_distance(self) -> None: + """Test distance when characters need to be inserted.""" + assert calculate_levenshtein_distance("test", "tests") == 1 # 1 insertion + + def test_deletion_distance(self) -> None: + """Test distance when characters need to be deleted.""" + assert calculate_levenshtein_distance("tests", "test") == 1 # 1 deletion + + def test_empty_string_cases(self) -> None: + """Test distance with empty strings.""" + assert calculate_levenshtein_distance("", "") == 0 + assert calculate_levenshtein_distance("test", "") == 4 + assert calculate_levenshtein_distance("", "test") == 4 + + def test_case_sensitive(self) -> None: + """Test that distance calculation is case sensitive.""" + assert calculate_levenshtein_distance("Test", "test") == 1 + + +class TestFindSimilarItems: + """Test finding similar items using fuzzy matching.""" + + def test_find_exact_match(self) -> None: + """Test finding exact match returns it first.""" + items = ["Run tests", "Write tests", "Format code"] + result = find_similar_items("Run tests", items, threshold=0.6) + + assert len(result) > 0 + assert result[0][0] == "Run tests" + assert result[0][1] == 1.0 # Perfect match + + def test_find_close_match(self) -> None: + """Test finding close matches with typos.""" + items = ["Run tests", "Write tests", "Format code"] + result = find_similar_items("Run tsets", items, threshold=0.6) + + assert len(result) > 0 + # "Run tests" should be most similar to "Run tsets" + assert "Run tests" in [item for item, _ in result] + + def test_find_multiple_similar_items(self) -> None: + """Test finding multiple similar items.""" + items = ["test_login", "test_logout", "test_signup", "format_code"] + result = find_similar_items("test", items, threshold=0.5) + + # Should find all test_ items as similar + similar_items = [item for item, _ in result] + assert "test_login" in similar_items + assert "test_logout" in similar_items + assert "test_signup" in similar_items + + def test_threshold_filters_low_similarity(self) -> None: + """Test that threshold filters out low similarity matches.""" + items = ["apple", "orange", "banana"] + result = find_similar_items("test", items, threshold=0.8) + + # None of these should be similar enough to "test" + assert len(result) == 0 + + def test_returns_sorted_by_similarity(self) -> None: + """Test that results are sorted by similarity (descending).""" + items = ["tests", "test", "testing", "format"] + result = find_similar_items("test", items, threshold=0.5) + + # Should be sorted with "test" first (exact match) + assert result[0][0] == "test" + assert result[0][1] == 1.0 + + # Next items should have decreasing similarity + if len(result) > 1: + assert result[1][1] < result[0][1] + + def test_case_insensitive_matching(self) -> None: + """Test that matching is case insensitive.""" + items = ["Run Tests", "Write Tests"] + result = find_similar_items("run tests", items, threshold=0.6) + + assert len(result) > 0 + assert result[0][0] == "Run Tests" + assert result[0][1] == 1.0 # Should match despite case + + def test_empty_items_list(self) -> None: + """Test behavior with empty items list.""" + result = find_similar_items("test", [], threshold=0.6) + assert result == [] + + def test_partial_word_matching(self) -> None: + """Test matching partial words.""" + items = ["Run make check", "Run make format", "Run tests"] + result = find_similar_items("make", items, threshold=0.4) + + # Should find items containing "make" + similar_items = [item for item, _ in result] + assert "Run make check" in similar_items + assert "Run make format" in similar_items + + +class TestValidateNoDuplicates: + """Test duplicate validation.""" + + def test_no_duplicates_returns_none(self) -> None: + """Test that no duplicates returns None (valid).""" + plan = """### Phase 1: Test +- [ ] Item 1 +- [ ] Item 2 +- [ ] Item 3""" + + result = validate_no_duplicates(plan, "Item 4") + assert result is None + + def test_exact_duplicate_detected(self) -> None: + """Test that exact duplicate is detected.""" + plan = """### Phase 1: Test +- [ ] Item 1 +- [ ] Item 2 +- [ ] Item 3""" + + result = validate_no_duplicates(plan, "Item 1") + assert result is not None + assert "Item 1" in result + assert "already exists" in result.lower() + + def test_case_insensitive_duplicate_detected(self) -> None: + """Test that case-insensitive duplicate is detected.""" + plan = """### Phase 1: Test +- [ ] Run Tests +- [ ] Write Tests""" + + result = validate_no_duplicates(plan, "run tests") + assert result is not None + assert "already exists" in result.lower() + + def test_similar_but_different_items_allowed(self) -> None: + """Test that similar but different items are allowed.""" + plan = """### Phase 1: Test +- [ ] Run tests +- [ ] Write tests""" + + result = validate_no_duplicates(plan, "Run all tests") + assert result is None # Should be allowed (different enough) + + def test_duplicate_with_checkbox_state(self) -> None: + """Test that duplicates are detected regardless of checkbox state.""" + plan = """### Phase 1: Test +- [x] Run tests +- [ ] Write tests""" + + result = validate_no_duplicates(plan, "Run tests") + assert result is not None + assert "already exists" in result.lower() + + def test_whitespace_normalized(self) -> None: + """Test that whitespace is normalized for duplicate detection.""" + plan = """### Phase 1: Test +- [ ] Run tests here""" + + result = validate_no_duplicates(plan, "Run tests here") + assert result is not None # Should detect as duplicate + + def test_empty_plan(self) -> None: + """Test validation with empty plan.""" + result = validate_no_duplicates("", "New item") + assert result is None # No duplicates in empty plan diff --git a/template/tests/ai_tools/test_update_plan_validation.py.jinja b/template/tests/ai_tools/test_update_plan_validation.py.jinja new file mode 100644 index 0000000..a47aedb --- /dev/null +++ b/template/tests/ai_tools/test_update_plan_validation.py.jinja @@ -0,0 +1,151 @@ +"""Tests for Phase 4 strict validation features. + +This module tests validation features that prevent common mistakes: +- Empty item validation +- Phase existence validation +""" + +from __future__ import annotations + +from scripts.ai_tools.update_plan import ( + validate_item_not_empty, + validate_phase_exists, +) + + +class TestValidateItemNotEmpty: + """Test empty item validation.""" + + def test_valid_item_returns_none(self) -> None: + """Test that valid item text returns None (valid).""" + result = validate_item_not_empty("Write tests") + assert result is None + + def test_empty_string_returns_error(self) -> None: + """Test that empty string returns error message.""" + result = validate_item_not_empty("") + assert result is not None + assert "empty" in result.lower() + + def test_whitespace_only_returns_error(self) -> None: + """Test that whitespace-only string returns error.""" + result = validate_item_not_empty(" ") + assert result is not None + assert "empty" in result.lower() + + def test_tabs_only_returns_error(self) -> None: + """Test that tabs-only string returns error.""" + result = validate_item_not_empty("\t\t") + assert result is not None + assert "empty" in result.lower() + + def test_newlines_only_returns_error(self) -> None: + """Test that newlines-only string returns error.""" + result = validate_item_not_empty("\n\n") + assert result is not None + assert "empty" in result.lower() + + def test_mixed_whitespace_returns_error(self) -> None: + """Test that mixed whitespace returns error.""" + result = validate_item_not_empty(" \t\n ") + assert result is not None + assert "empty" in result.lower() + + def test_single_character_is_valid(self) -> None: + """Test that single character is valid.""" + result = validate_item_not_empty("a") + assert result is None + + def test_item_with_leading_trailing_whitespace_is_valid(self) -> None: + """Test that item with whitespace around text is valid.""" + result = validate_item_not_empty(" Write tests ") + assert result is None + + +class TestValidatePhaseExists: + """Test phase existence validation.""" + + def test_existing_phase_returns_none(self) -> None: + """Test that existing phase returns None (valid).""" + plan = """### Phase 1: Test +- [ ] Item 1 + +### Phase 2: Implementation +- [ ] Item 2""" + + result = validate_phase_exists(plan, "Phase 1") + assert result is None + + def test_nonexistent_phase_returns_error(self) -> None: + """Test that non-existent phase returns error.""" + plan = """### Phase 1: Test +- [ ] Item 1""" + + result = validate_phase_exists(plan, "Phase 2") + assert result is not None + assert "does not exist" in result.lower() or "not found" in result.lower() + + def test_case_insensitive_phase_matching(self) -> None: + """Test that phase matching is case insensitive.""" + plan = """### Phase 1: Test +- [ ] Item 1""" + + result = validate_phase_exists(plan, "phase 1") + assert result is None + + def test_partial_phase_name_matching(self) -> None: + """Test that partial phase name matches.""" + plan = """### Phase 1: Research & Design +- [ ] Item 1""" + + result = validate_phase_exists(plan, "Phase 1") + assert result is None + + def test_empty_plan_returns_error(self) -> None: + """Test that empty plan returns error for any phase.""" + result = validate_phase_exists("", "Phase 1") + assert result is not None + + def test_plan_without_phases_returns_error(self) -> None: + """Test that plan without phase headers returns error.""" + plan = """# Some Plan +- [ ] Item 1 +- [ ] Item 2""" + + result = validate_phase_exists(plan, "Phase 1") + assert result is not None + + def test_phase_2_exists_when_multiple_phases(self) -> None: + """Test finding Phase 2 when multiple phases exist.""" + plan = """### Phase 1: First +- [ ] Item 1 + +### Phase 2: Second +- [ ] Item 2 + +### Phase 3: Third +- [ ] Item 3""" + + result = validate_phase_exists(plan, "Phase 2") + assert result is None + + def test_error_message_suggests_available_phases(self) -> None: + """Test that error message suggests available phases.""" + plan = """### Phase 1: Test +- [ ] Item 1 + +### Phase 2: Implementation +- [ ] Item 2""" + + result = validate_phase_exists(plan, "Phase 3") + assert result is not None + # Should suggest available phases + assert "Phase 1" in result or "Phase 2" in result + + def test_none_phase_name_returns_none(self) -> None: + """Test that None phase name returns None (no validation needed).""" + plan = """### Phase 1: Test +- [ ] Item 1""" + + result = validate_phase_exists(plan, None) + assert result is None # None means "add to last phase", which is valid diff --git a/template/tests/test_validate_ai_docs_sync.py.jinja b/template/tests/test_validate_ai_docs_sync.py.jinja deleted file mode 100644 index 0aec975..0000000 --- a/template/tests/test_validate_ai_docs_sync.py.jinja +++ /dev/null @@ -1,229 +0,0 @@ -"""Tests for AI_DOCS and template sync validation.""" - -from __future__ import annotations - -from pathlib import Path - -from {{ package_name }}.validate_ai_docs_sync import ( - check_file_exists, - compare_files, - find_ai_doc_files, - find_template_files, - generate_sync_report, - validate_sync, -) - - -class TestFindAIDocFiles: - """Test cases for finding AI_DOCS files.""" - - def test_find_ai_doc_files_in_real_directory(self) -> None: - """Test finding actual AI_DOCS files.""" - # Act - files = find_ai_doc_files() - - # Assert - assert len(files) > 0 - assert any("ai-tools.md" in str(f) for f in files) - assert any("tdd-workflow.md" in str(f) for f in files) - assert any("code-conventions.md" in str(f) for f in files) - - def test_find_ai_doc_files_returns_path_objects(self) -> None: - """Test that function returns Path objects.""" - # Act - files = find_ai_doc_files() - - # Assert - assert all(isinstance(f, Path) for f in files) - - -class TestFindTemplateFiles: - """Test cases for finding template files.""" - - def test_find_template_files_in_real_directory(self) -> None: - """Test finding actual template files.""" - # Act - files = find_template_files() - - # Assert - assert len(files) > 0 - # Should find template versions - assert any("CLAUDE.md.jinja" in str(f) for f in files) - - def test_find_template_files_returns_path_objects(self) -> None: - """Test that function returns Path objects.""" - # Act - files = find_template_files() - - # Assert - assert all(isinstance(f, Path) for f in files) - - -class TestCheckFileExists: - """Test cases for file existence checking.""" - - def test_check_file_exists_with_existing_file(self, tmp_path: Path) -> None: - """Test checking an existing file.""" - # Arrange - test_file = tmp_path / "test.txt" - test_file.write_text("content") - - # Act - result = check_file_exists(test_file) - - # Assert - assert result is True - - def test_check_file_exists_with_missing_file(self, tmp_path: Path) -> None: - """Test checking a missing file.""" - # Arrange - test_file = tmp_path / "nonexistent.txt" - - # Act - result = check_file_exists(test_file) - - # Assert - assert result is False - - -class TestCompareFiles: - """Test cases for file comparison.""" - - def test_compare_files_identical_content(self, tmp_path: Path) -> None: - """Test comparing files with identical content.""" - # Arrange - file1 = tmp_path / "file1.txt" - file2 = tmp_path / "file2.txt" - file1.write_text("same content\n") - file2.write_text("same content\n") - - # Act - are_same, diff = compare_files(file1, file2) - - # Assert - assert are_same is True - assert diff == "" - - def test_compare_files_different_content(self, tmp_path: Path) -> None: - """Test comparing files with different content.""" - # Arrange - file1 = tmp_path / "file1.txt" - file2 = tmp_path / "file2.txt" - file1.write_text("content A\n") - file2.write_text("content B\n") - - # Act - are_same, diff = compare_files(file1, file2) - - # Assert - assert are_same is False - assert diff != "" - assert "content A" in diff or "content B" in diff - - def test_compare_files_with_jinja_template(self, tmp_path: Path) -> None: - """Test comparing with Jinja template (should handle template syntax).""" - # Arrange - doc_file = tmp_path / "doc.md" - template_file = tmp_path / "doc.md.jinja" - doc_file.write_text("# Title\nContent here\n") - template_file.write_text("# Title\nContent here\n") - - # Act - are_same, diff = compare_files(doc_file, template_file, is_template=True) - - # Assert - assert are_same is True - - -class TestValidateSync: - """Test cases for sync validation.""" - - def test_validate_sync_with_real_files(self) -> None: - """Test validation with actual project files.""" - # Act - issues = validate_sync() - - # Assert - # Issues list should be returned (empty or with items) - assert isinstance(issues, list) - # Each issue should be a dict with required keys - for issue in issues: - assert "type" in issue - assert "file" in issue - assert "message" in issue - - def test_validate_sync_returns_list(self) -> None: - """Test that validate_sync returns a list.""" - # Act - result = validate_sync() - - # Assert - assert isinstance(result, list) - - -class TestGenerateSyncReport: - """Test cases for report generation.""" - - def test_generate_sync_report_with_no_issues(self) -> None: - """Test report generation with no issues.""" - # Arrange - issues: list[dict[str, str]] = [] - - # Act - report = generate_sync_report(issues) - - # Assert - assert "✅" in report or "PASSED" in report - assert "Issues Found:** 0" in report - - def test_generate_sync_report_with_issues(self) -> None: - """Test report generation with issues.""" - # Arrange - issues = [ - { - "type": "missing", - "file": "AI_DOCS/test.md", - "message": "Missing template file", - }, - { - "type": "different", - "file": "AI_DOCS/test2.md", - "message": "Content differs", - }, - ] - - # Act - report = generate_sync_report(issues) - - # Assert - assert "❌" in report or "FAILED" in report - assert "Total Issues:** 2" in report - assert "missing" in report.lower() - assert "differences" in report.lower() - assert "AI_DOCS/test.md" in report - assert "AI_DOCS/test2.md" in report - - def test_generate_sync_report_returns_string(self) -> None: - """Test that report generation returns a string.""" - # Arrange - issues: list[dict[str, str]] = [] - - # Act - result = generate_sync_report(issues) - - # Assert - assert isinstance(result, str) - assert len(result) > 0 - - def test_generate_sync_report_includes_file_types_checked(self) -> None: - """Test report includes list of file types checked.""" - # Arrange - issues: list[dict[str, str]] = [] - - # Act - result = generate_sync_report(issues) - - # Assert - assert "AI_DOCS" in result - assert "CLAUDE.md" in result - assert "AGENTS.md" in result diff --git a/tests/test_validate_ai_docs_sync.py b/tests/test_validate_ai_docs_sync.py index be8c570..1c4acdf 100644 --- a/tests/test_validate_ai_docs_sync.py +++ b/tests/test_validate_ai_docs_sync.py @@ -2,9 +2,10 @@ from __future__ import annotations +import subprocess from pathlib import Path -from python_modern_template.validate_ai_docs_sync import ( +from scripts.ai_tools.validate_ai_docs_sync import ( check_file_exists, compare_files, find_ai_doc_files, @@ -227,3 +228,42 @@ def test_generate_sync_report_includes_file_types_checked(self) -> None: assert "AI_DOCS" in result assert "CLAUDE.md" in result assert "AGENTS.md" in result + + +class TestCLIInterface: + """Test cases for CLI interface.""" + + def test_cli_command_runs_successfully(self) -> None: + """Test that ai-validate-docs CLI command runs.""" + # Act + result = subprocess.run( + ["uv", "run", "ai-validate-docs"], + capture_output=True, + text=True, + check=False, + ) + + # Assert + # Command should complete (exit code 0 if all valid, 1 if issues found) + assert result.returncode in [0, 1] + # Output should contain report + assert "AI_DOCS Sync Validation Report" in result.stdout + assert "Status:" in result.stdout + + def test_cli_output_format(self) -> None: + """Test that CLI output has expected format.""" + # Act + result = subprocess.run( + ["uv", "run", "ai-validate-docs"], + capture_output=True, + text=True, + check=False, + ) + + # Assert + output = result.stdout + # Should have markdown-style report + assert "#" in output # Markdown headers + assert "**" in output # Markdown bold + # Should mention file types checked + assert "AI_DOCS" in output or "Files Checked" in output From 2c918756ccb4189e90417b837eab67a9321c64ec Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 06:26:42 +0000 Subject: [PATCH 2/2] chore: update AI session summary after validate_ai_docs_sync refactoring --- .ai-context/LAST_SESSION_SUMMARY.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.ai-context/LAST_SESSION_SUMMARY.md b/.ai-context/LAST_SESSION_SUMMARY.md index 7d3e068..96fce9b 100644 --- a/.ai-context/LAST_SESSION_SUMMARY.md +++ b/.ai-context/LAST_SESSION_SUMMARY.md @@ -4,22 +4,22 @@ ## Session Information -**Session ID**: 20251102150000 -**Task**: Test Task -**Date**: 2025-11-05 06:24:51 +**Session ID**: 20251105061716 +**Task**: Refactor validate_ai_docs_sync.py from src/ to scripts/ai_tools/ as proper CLI tool +**Date**: 2025-11-05 06:26:08 **Status**: ✅ Completed -# Task Summary: Test Task +# Task Summary: Refactor validate_ai_docs_sync.py from src/ to scripts/ai_tools/ as proper CLI tool -**Session ID**: 20251102150000 -**Created**: 2025-11-05 06:24:51 +**Session ID**: 20251105061716 +**Created**: 2025-11-05 06:26:08 **Status**: ✅ Completed --- ## What Was Done -Test summary +Successfully refactored validate_ai_docs_sync.py from src/python_modern_template/ to scripts/ai_tools/ as a proper CLI tool (ai-validate-docs). Updated all documentation references, sync scripts, and tests. All 202 tests pass with 100% coverage. --- @@ -31,7 +31,7 @@ Test summary ## Files Changed -- No files tracked +- `validate_ai_docs_sync.py` --- @@ -45,4 +45,4 @@ Test summary **This file is automatically updated by the last AI agent to complete a task.** **Next AI agent: Read this file first to understand recent work!** -**Last Updated**: 2025-11-05 06:24:51 +**Last Updated**: 2025-11-05 06:26:08