Files
github-spec-kit/tests/test_post_process.py
Roland Huss d4e7d2b888 feat(integrations): generalize post-processing to all format types (#3311)
* feat(integrations): add post_process_command_content() hook for all format types

Add post_process_command_content(self, content: str) -> str to IntegrationBase
with a no-op default. Wire it into register_commands() for non-skills format
types (Markdown, TOML, YAML) after format rendering, before writing to disk.
Also applies to aliases rendered via the inject_name path (cline, forge).

Skills-format agents are excluded to preserve the existing
post_process_skill_content() path and avoid double-processing.

This gives extension authors a clean per-agent content transformation seam
for all 21 non-skills integrations that previously had no post-processing hook.

Ref: #3303

Assisted-By: 🤖 Claude Code

* fix: initialize _integration before conditional branch

Prevents potential UnboundLocalError if the non-skills guard is
refactored without updating the alias path reference.

Assisted-By: 🤖 Claude Code
2026-07-07 11:35:43 -05:00

246 lines
7.5 KiB
Python

"""Tests for post_process_command_content() hook on IntegrationBase.
Verifies that the generalized post-processing hook:
- Runs for non-skills format types (Markdown, TOML, YAML)
- Does NOT run for skills-format agents
- Default no-op returns content unchanged
- Exceptions propagate to caller
"""
from __future__ import annotations
from unittest.mock import patch
import pytest
from specify_cli.agents import CommandRegistrar
from specify_cli.integrations.base import IntegrationBase
@pytest.fixture
def registrar():
return CommandRegistrar()
@pytest.fixture
def ext_dir(tmp_path):
"""Create a mock extension with a simple command template."""
ext = tmp_path / "extension"
ext.mkdir()
cmd_dir = ext / "commands"
cmd_dir.mkdir()
return ext, cmd_dir
def _write_cmd(cmd_dir, name="review.md", body="Review the code.\n"):
cmd_file = cmd_dir / name
cmd_file.write_text(
f"---\ndescription: Test command\n---\n\n{body}",
encoding="utf-8",
)
return cmd_file
class TestDefaultNoOp:
def test_returns_content_unchanged(self):
base = IntegrationBase()
content = "Some command content\nwith multiple lines."
assert base.post_process_command_content(content) == content
def test_empty_string(self):
base = IntegrationBase()
assert base.post_process_command_content("") == ""
class TestMarkdownAgentPostProcess:
def test_opencode_post_process_applied(
self, tmp_path, registrar, ext_dir, monkeypatch
):
ext, cmd_dir = ext_dir
_write_cmd(cmd_dir)
from specify_cli.integrations import get_integration
opencode = get_integration("opencode")
marker = "<!-- POST_PROCESSED -->"
def _inject_marker(self, content):
return content + marker
monkeypatch.setattr(
opencode.__class__, "post_process_command_content", _inject_marker
)
commands = [{"name": "speckit.test.review", "file": "commands/review.md"}]
registrar.register_commands(
"opencode", commands, "test-ext", ext, tmp_path
)
cmd_output = tmp_path / ".opencode" / "commands" / "speckit.test.review.md"
assert cmd_output.exists()
content = cmd_output.read_text(encoding="utf-8")
assert marker in content
class TestTomlAgentPostProcess:
def test_gemini_post_process_applied(
self, tmp_path, registrar, ext_dir, monkeypatch
):
ext, cmd_dir = ext_dir
_write_cmd(cmd_dir)
from specify_cli.integrations import get_integration
gemini = get_integration("gemini")
marker = "# POST_PROCESSED"
def _inject_marker(self, content):
return content + f"\n{marker}\n"
monkeypatch.setattr(
gemini.__class__, "post_process_command_content", _inject_marker
)
commands = [{"name": "speckit.test.review", "file": "commands/review.md"}]
registrar.register_commands(
"gemini", commands, "test-ext", ext, tmp_path
)
cmd_output = tmp_path / ".gemini" / "commands" / "speckit.test.review.toml"
assert cmd_output.exists()
content = cmd_output.read_text(encoding="utf-8")
assert marker in content
class TestYamlAgentPostProcess:
def test_goose_post_process_applied(
self, tmp_path, registrar, ext_dir, monkeypatch
):
ext, cmd_dir = ext_dir
_write_cmd(cmd_dir)
from specify_cli.integrations import get_integration
goose = get_integration("goose")
marker = "# POST_PROCESSED"
def _inject_marker(self, content):
return content + f"\n{marker}\n"
monkeypatch.setattr(
goose.__class__, "post_process_command_content", _inject_marker
)
commands = [{"name": "speckit.test.review", "file": "commands/review.md"}]
registrar.register_commands(
"goose", commands, "test-ext", ext, tmp_path
)
cmd_output = tmp_path / ".goose" / "recipes" / "speckit.test.review.yaml"
assert cmd_output.exists()
content = cmd_output.read_text(encoding="utf-8")
assert marker in content
class TestSkillsAgentExcluded:
def test_claude_post_process_not_called(
self, tmp_path, registrar, ext_dir, monkeypatch
):
ext, cmd_dir = ext_dir
_write_cmd(cmd_dir)
from specify_cli.integrations import get_integration
claude = get_integration("claude")
marker = "<!-- SHOULD_NOT_APPEAR -->"
def _inject_marker(self, content):
return content + marker
monkeypatch.setattr(
claude.__class__, "post_process_command_content", _inject_marker
)
commands = [{"name": "speckit.test.review", "file": "commands/review.md"}]
registrar.register_commands(
"claude", commands, "test-ext", ext, tmp_path
)
skill_file = (
tmp_path / ".claude" / "skills" / "speckit-test-review" / "SKILL.md"
)
assert skill_file.exists()
content = skill_file.read_text(encoding="utf-8")
assert marker not in content
def test_skills_agent_method_never_called(
self, tmp_path, registrar, ext_dir
):
ext, cmd_dir = ext_dir
_write_cmd(cmd_dir)
from specify_cli.integrations import get_integration
claude = get_integration("claude")
commands = [{"name": "speckit.test.review", "file": "commands/review.md"}]
with patch.object(
claude.__class__, "post_process_command_content", wraps=claude.post_process_command_content
) as mock_method:
registrar.register_commands(
"claude", commands, "test-ext", ext, tmp_path
)
mock_method.assert_not_called()
class TestExceptionPropagation:
def test_hook_exception_propagates(
self, tmp_path, registrar, ext_dir, monkeypatch
):
ext, cmd_dir = ext_dir
_write_cmd(cmd_dir)
from specify_cli.integrations import get_integration
opencode = get_integration("opencode")
def _raise(self, content):
raise RuntimeError("Hook failed")
monkeypatch.setattr(
opencode.__class__, "post_process_command_content", _raise
)
commands = [{"name": "speckit.test.review", "file": "commands/review.md"}]
with pytest.raises(RuntimeError, match="Hook failed"):
registrar.register_commands(
"opencode", commands, "test-ext", ext, tmp_path
)
class TestRegressionPlainTemplate:
@pytest.mark.parametrize(
"agent,path_pattern",
[
("claude", ".claude/skills/speckit-test-plain/SKILL.md"),
("opencode", ".opencode/commands/speckit.test.plain.md"),
],
ids=["skills", "markdown"],
)
def test_plain_template_unchanged(
self, tmp_path, registrar, ext_dir, agent, path_pattern
):
ext, cmd_dir = ext_dir
body_text = "This is a plain command with no special content.\n"
_write_cmd(cmd_dir, name="plain.md", body=body_text)
commands = [{"name": "speckit.test.plain", "file": "commands/plain.md"}]
registrar.register_commands(
agent, commands, "test-ext", ext, tmp_path
)
output_file = tmp_path / path_pattern
assert output_file.exists(), f"Output file missing for {agent}"
content = output_file.read_text(encoding="utf-8")
assert body_text.strip() in content, f"Body text missing in {agent} output"