New authoring tooling and a fix for the doc-regeneration defect it exposed. Added: - src/kaizen_agentic/agent_docs.py — render + idempotent upsert of the CLAUDE.md "## Installed Agents" section (shared by installer and CLI) - `kaizen-agentic docs generate [--check]` — idempotent doc refresh / CI gate - `kaizen-agentic create-agent` — scaffold a schema-valid agent - Frontmatter schema validation in `kaizen-agentic validate` (required name/description/category, known category, valid memory/model) - tests: test_agent_docs, test_validate_schema, test_create_agent Fixed: - _update_documentation regex duplicated the Installed Agents block on every run (stopped at the first ### subheading) — now idempotent - declared frontmatter `category` is authoritative (heuristic is fallback) - list_installed_agents reads the frontmatter name, not the filename - renamed agent-project-management.md -> agent-project-assistant.md to satisfy the agent-<name>.md convention (eliminates a name/filename collision that caused install/update to write a divergent duplicate) - test_cli_error_handling no longer installs into the repo root (uses tmp) Version 1.4.0; CHANGELOG, CLI cheat sheet, agency-framework, TODO updated. Workplan KAIZEN-WP-0007 closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
3.5 KiB
Python
98 lines
3.5 KiB
Python
"""Tests for idempotent agent-docs generation (WP-0007 T01/T02)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from kaizen_agentic.agent_docs import (
|
|
SECTION_FOOTER,
|
|
SECTION_HEADING,
|
|
render_installed_agents_section,
|
|
upsert_installed_agents_section,
|
|
)
|
|
from kaizen_agentic.cli import cli
|
|
from kaizen_agentic.registry import AgentRegistry
|
|
|
|
AGENTS_DIR = Path(__file__).parent.parent / "agents"
|
|
|
|
|
|
@pytest.fixture
|
|
def runner() -> CliRunner:
|
|
return CliRunner()
|
|
|
|
|
|
def _section() -> str:
|
|
agents = AgentRegistry(AGENTS_DIR).list_agents()[:4]
|
|
return render_installed_agents_section(agents)
|
|
|
|
|
|
class TestUpsertIdempotency:
|
|
def test_upsert_is_idempotent(self):
|
|
section = _section()
|
|
base = f"# Project\n\nintro\n\n{section}\n## Keep Me\nbody\n"
|
|
once = upsert_installed_agents_section(base, section)
|
|
twice = upsert_installed_agents_section(once, section)
|
|
assert once == twice
|
|
assert once.count(SECTION_HEADING) == 1
|
|
assert once.count(SECTION_FOOTER) == 1
|
|
# A following top-level section must survive the replace
|
|
assert once.count("## Keep Me") == 1
|
|
|
|
def test_subheadings_do_not_truncate_section(self):
|
|
section = _section()
|
|
# The block contains '### Category' subheadings; the replace must not
|
|
# stop at the first one (the original regex bug).
|
|
merged = upsert_installed_agents_section("# P\n\n", section)
|
|
assert merged.count(SECTION_FOOTER) == 1
|
|
assert "### " in merged # categories rendered
|
|
|
|
def test_append_when_absent(self):
|
|
section = _section()
|
|
merged = upsert_installed_agents_section("# Project\n\nbody\n", section)
|
|
assert SECTION_HEADING in merged
|
|
assert merged.count(SECTION_FOOTER) == 1
|
|
|
|
|
|
class TestDocsGenerateCli:
|
|
def test_generate_then_check_clean(self, runner: CliRunner, tmp_path: Path):
|
|
# A project with two installed agents
|
|
proj = tmp_path / "proj"
|
|
(proj / "agents").mkdir(parents=True)
|
|
for name, cat in (("alpha", "testing"), ("beta", "code-quality")):
|
|
(proj / "agents" / f"agent-{name}.md").write_text(
|
|
f"---\nname: {name}\ndescription: d{name}\ncategory: {cat}\n---\nx\n"
|
|
)
|
|
|
|
gen = runner.invoke(cli, ["docs", "generate", "--target", str(proj)])
|
|
assert gen.exit_code == 0
|
|
claude = (proj / "CLAUDE.md").read_text()
|
|
assert claude.count(SECTION_HEADING) == 1
|
|
assert "**alpha**" in claude and "**beta**" in claude
|
|
|
|
# Second generate is a no-op
|
|
again = runner.invoke(cli, ["docs", "generate", "--target", str(proj)])
|
|
assert "already up to date" in again.output
|
|
|
|
# --check passes on a synced repo
|
|
check = runner.invoke(
|
|
cli, ["docs", "generate", "--check", "--target", str(proj)]
|
|
)
|
|
assert check.exit_code == 0
|
|
assert "up to date" in check.output
|
|
|
|
def test_check_fails_when_stale(self, runner: CliRunner, tmp_path: Path):
|
|
proj = tmp_path / "proj"
|
|
(proj / "agents").mkdir(parents=True)
|
|
(proj / "agents" / "agent-alpha.md").write_text(
|
|
"---\nname: alpha\ndescription: d\ncategory: testing\n---\nx\n"
|
|
)
|
|
(proj / "CLAUDE.md").write_text("# Proj\n\nno agents section yet\n")
|
|
check = runner.invoke(
|
|
cli, ["docs", "generate", "--check", "--target", str(proj)]
|
|
)
|
|
assert check.exit_code == 1
|
|
assert "out of date" in check.output
|