mirror of
https://github.com/github/spec-kit.git
synced 2026-07-06 14:01:01 +08:00
Merge branch 'main' into feature/add-forgecode-agent-support
This commit is contained in:
0
tests/integrations/__init__.py
Normal file
0
tests/integrations/__init__.py
Normal file
23
tests/integrations/conftest.py
Normal file
23
tests/integrations/conftest.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Shared test helpers for integration tests."""
|
||||
|
||||
from specify_cli.integrations.base import MarkdownIntegration
|
||||
|
||||
|
||||
class StubIntegration(MarkdownIntegration):
|
||||
"""Minimal concrete integration for testing."""
|
||||
|
||||
key = "stub"
|
||||
config = {
|
||||
"name": "Stub Agent",
|
||||
"folder": ".stub/",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": None,
|
||||
"requires_cli": False,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".stub/commands",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "STUB.md"
|
||||
169
tests/integrations/test_base.py
Normal file
169
tests/integrations/test_base.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""Tests for IntegrationOption, IntegrationBase, MarkdownIntegration, and primitives."""
|
||||
|
||||
import pytest
|
||||
|
||||
from specify_cli.integrations.base import (
|
||||
IntegrationBase,
|
||||
IntegrationOption,
|
||||
MarkdownIntegration,
|
||||
)
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
from .conftest import StubIntegration
|
||||
|
||||
|
||||
class TestIntegrationOption:
|
||||
def test_defaults(self):
|
||||
opt = IntegrationOption(name="--flag")
|
||||
assert opt.name == "--flag"
|
||||
assert opt.is_flag is False
|
||||
assert opt.required is False
|
||||
assert opt.default is None
|
||||
assert opt.help == ""
|
||||
|
||||
def test_flag_option(self):
|
||||
opt = IntegrationOption(name="--skills", is_flag=True, default=True, help="Enable skills")
|
||||
assert opt.is_flag is True
|
||||
assert opt.default is True
|
||||
assert opt.help == "Enable skills"
|
||||
|
||||
def test_required_option(self):
|
||||
opt = IntegrationOption(name="--commands-dir", required=True, help="Dir path")
|
||||
assert opt.required is True
|
||||
|
||||
def test_frozen(self):
|
||||
opt = IntegrationOption(name="--x")
|
||||
with pytest.raises(AttributeError):
|
||||
opt.name = "--y" # type: ignore[misc]
|
||||
|
||||
|
||||
class TestIntegrationBase:
|
||||
def test_key_and_config(self):
|
||||
i = StubIntegration()
|
||||
assert i.key == "stub"
|
||||
assert i.config["name"] == "Stub Agent"
|
||||
assert i.registrar_config["format"] == "markdown"
|
||||
assert i.context_file == "STUB.md"
|
||||
|
||||
def test_options_default_empty(self):
|
||||
assert StubIntegration.options() == []
|
||||
|
||||
def test_shared_commands_dir(self):
|
||||
i = StubIntegration()
|
||||
cmd_dir = i.shared_commands_dir()
|
||||
assert cmd_dir is not None
|
||||
assert cmd_dir.is_dir()
|
||||
|
||||
def test_setup_uses_shared_templates(self, tmp_path):
|
||||
i = StubIntegration()
|
||||
manifest = IntegrationManifest("stub", tmp_path)
|
||||
created = i.setup(tmp_path, manifest)
|
||||
assert len(created) > 0
|
||||
for f in created:
|
||||
assert f.parent == tmp_path / ".stub" / "commands"
|
||||
assert f.name.startswith("speckit.")
|
||||
assert f.name.endswith(".md")
|
||||
|
||||
def test_setup_copies_templates(self, tmp_path, monkeypatch):
|
||||
tpl = tmp_path / "_templates"
|
||||
tpl.mkdir()
|
||||
(tpl / "plan.md").write_text("plan content", encoding="utf-8")
|
||||
(tpl / "specify.md").write_text("spec content", encoding="utf-8")
|
||||
|
||||
i = StubIntegration()
|
||||
monkeypatch.setattr(type(i), "list_command_templates", lambda self: sorted(tpl.glob("*.md")))
|
||||
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
created = i.setup(project, IntegrationManifest("stub", project))
|
||||
assert len(created) == 2
|
||||
assert (project / ".stub" / "commands" / "speckit.plan.md").exists()
|
||||
assert (project / ".stub" / "commands" / "speckit.specify.md").exists()
|
||||
|
||||
def test_install_delegates_to_setup(self, tmp_path):
|
||||
i = StubIntegration()
|
||||
manifest = IntegrationManifest("stub", tmp_path)
|
||||
result = i.install(tmp_path, manifest)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_uninstall_delegates_to_teardown(self, tmp_path):
|
||||
i = StubIntegration()
|
||||
manifest = IntegrationManifest("stub", tmp_path)
|
||||
removed, skipped = i.uninstall(tmp_path, manifest)
|
||||
assert removed == []
|
||||
assert skipped == []
|
||||
|
||||
|
||||
class TestMarkdownIntegration:
|
||||
def test_is_subclass_of_base(self):
|
||||
assert issubclass(MarkdownIntegration, IntegrationBase)
|
||||
|
||||
def test_stub_is_markdown(self):
|
||||
assert isinstance(StubIntegration(), MarkdownIntegration)
|
||||
|
||||
|
||||
class TestBasePrimitives:
|
||||
def test_shared_commands_dir_returns_path(self):
|
||||
i = StubIntegration()
|
||||
cmd_dir = i.shared_commands_dir()
|
||||
assert cmd_dir is not None
|
||||
assert cmd_dir.is_dir()
|
||||
|
||||
def test_shared_templates_dir_returns_path(self):
|
||||
i = StubIntegration()
|
||||
tpl_dir = i.shared_templates_dir()
|
||||
assert tpl_dir is not None
|
||||
assert tpl_dir.is_dir()
|
||||
|
||||
def test_list_command_templates_returns_md_files(self):
|
||||
i = StubIntegration()
|
||||
templates = i.list_command_templates()
|
||||
assert len(templates) > 0
|
||||
assert all(t.suffix == ".md" for t in templates)
|
||||
|
||||
def test_command_filename_default(self):
|
||||
i = StubIntegration()
|
||||
assert i.command_filename("plan") == "speckit.plan.md"
|
||||
|
||||
def test_commands_dest(self, tmp_path):
|
||||
i = StubIntegration()
|
||||
dest = i.commands_dest(tmp_path)
|
||||
assert dest == tmp_path / ".stub" / "commands"
|
||||
|
||||
def test_commands_dest_no_config_raises(self, tmp_path):
|
||||
class NoConfig(MarkdownIntegration):
|
||||
key = "noconfig"
|
||||
with pytest.raises(ValueError, match="config is not set"):
|
||||
NoConfig().commands_dest(tmp_path)
|
||||
|
||||
def test_copy_command_to_directory(self, tmp_path):
|
||||
src = tmp_path / "source.md"
|
||||
src.write_text("content", encoding="utf-8")
|
||||
dest_dir = tmp_path / "output"
|
||||
result = IntegrationBase.copy_command_to_directory(src, dest_dir, "speckit.plan.md")
|
||||
assert result == dest_dir / "speckit.plan.md"
|
||||
assert result.read_text(encoding="utf-8") == "content"
|
||||
|
||||
def test_record_file_in_manifest(self, tmp_path):
|
||||
f = tmp_path / "f.txt"
|
||||
f.write_text("hello", encoding="utf-8")
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
IntegrationBase.record_file_in_manifest(f, tmp_path, m)
|
||||
assert "f.txt" in m.files
|
||||
|
||||
def test_write_file_and_record(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
dest = tmp_path / "sub" / "f.txt"
|
||||
result = IntegrationBase.write_file_and_record("content", dest, tmp_path, m)
|
||||
assert result == dest
|
||||
assert dest.read_text(encoding="utf-8") == "content"
|
||||
assert "sub/f.txt" in m.files
|
||||
|
||||
def test_setup_copies_shared_templates(self, tmp_path):
|
||||
i = StubIntegration()
|
||||
m = IntegrationManifest("stub", tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
assert len(created) > 0
|
||||
for f in created:
|
||||
assert f.parent.name == "commands"
|
||||
assert f.name.startswith("speckit.")
|
||||
assert f.name.endswith(".md")
|
||||
122
tests/integrations/test_cli.py
Normal file
122
tests/integrations/test_cli.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Tests for --integration flag on specify init (CLI-level)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestInitIntegrationFlag:
|
||||
def test_integration_and_ai_mutually_exclusive(self):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "test-project", "--ai", "claude", "--integration", "copilot",
|
||||
])
|
||||
assert result.exit_code != 0
|
||||
assert "mutually exclusive" in result.output
|
||||
|
||||
def test_unknown_integration_rejected(self):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "test-project", "--integration", "nonexistent",
|
||||
])
|
||||
assert result.exit_code != 0
|
||||
assert "Unknown integration" in result.output
|
||||
|
||||
def test_integration_copilot_creates_files(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
runner = CliRunner()
|
||||
project = tmp_path / "int-test"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--integration", "copilot", "--script", "sh", "--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init failed: {result.output}"
|
||||
assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists()
|
||||
assert (project / ".github" / "prompts" / "speckit.plan.prompt.md").exists()
|
||||
assert (project / ".specify" / "scripts" / "bash" / "common.sh").exists()
|
||||
|
||||
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
|
||||
assert data["integration"] == "copilot"
|
||||
assert "scripts" in data
|
||||
assert "update-context" in data["scripts"]
|
||||
|
||||
opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
|
||||
assert opts["integration"] == "copilot"
|
||||
|
||||
assert (project / ".specify" / "integrations" / "copilot.manifest.json").exists()
|
||||
assert (project / ".specify" / "integrations" / "copilot" / "scripts" / "update-context.sh").exists()
|
||||
|
||||
shared_manifest = project / ".specify" / "integrations" / "speckit.manifest.json"
|
||||
assert shared_manifest.exists()
|
||||
|
||||
def test_ai_copilot_auto_promotes(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
project = tmp_path / "promote-test"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", "copilot", "--script", "sh", "--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0
|
||||
assert "--integration copilot" in result.output
|
||||
assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists()
|
||||
|
||||
def test_shared_infra_skips_existing_files(self, tmp_path):
|
||||
"""Pre-existing shared files are not overwritten by _install_shared_infra."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
project = tmp_path / "skip-test"
|
||||
project.mkdir()
|
||||
|
||||
# Pre-create a shared script with custom content
|
||||
scripts_dir = project / ".specify" / "scripts" / "bash"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
custom_content = "# user-modified common.sh\n"
|
||||
(scripts_dir / "common.sh").write_text(custom_content, encoding="utf-8")
|
||||
|
||||
# Pre-create a shared template with custom content
|
||||
templates_dir = project / ".specify" / "templates"
|
||||
templates_dir.mkdir(parents=True)
|
||||
custom_template = "# user-modified spec-template\n"
|
||||
(templates_dir / "spec-template.md").write_text(custom_template, encoding="utf-8")
|
||||
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--force",
|
||||
"--integration", "copilot",
|
||||
"--script", "sh",
|
||||
"--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
# User's files should be preserved
|
||||
assert (scripts_dir / "common.sh").read_text(encoding="utf-8") == custom_content
|
||||
assert (templates_dir / "spec-template.md").read_text(encoding="utf-8") == custom_template
|
||||
|
||||
# Other shared files should still be installed
|
||||
assert (scripts_dir / "setup-plan.sh").exists()
|
||||
assert (templates_dir / "plan-template.md").exists()
|
||||
11
tests/integrations/test_integration_amp.py
Normal file
11
tests/integrations/test_integration_amp.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for AmpIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestAmpIntegration(MarkdownIntegrationTests):
|
||||
KEY = "amp"
|
||||
FOLDER = ".agents/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".agents/commands"
|
||||
CONTEXT_FILE = "AGENTS.md"
|
||||
11
tests/integrations/test_integration_auggie.py
Normal file
11
tests/integrations/test_integration_auggie.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for AuggieIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestAuggieIntegration(MarkdownIntegrationTests):
|
||||
KEY = "auggie"
|
||||
FOLDER = ".augment/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".augment/commands"
|
||||
CONTEXT_FILE = ".augment/rules/specify-rules.md"
|
||||
296
tests/integrations/test_integration_base_markdown.py
Normal file
296
tests/integrations/test_integration_base_markdown.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""Reusable test mixin for standard MarkdownIntegration subclasses.
|
||||
|
||||
Each per-agent test file sets ``KEY``, ``FOLDER``, ``COMMANDS_SUBDIR``,
|
||||
``REGISTRAR_DIR``, and ``CONTEXT_FILE``, then inherits all verification
|
||||
logic from ``MarkdownIntegrationTests``.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from specify_cli.integrations import INTEGRATION_REGISTRY, get_integration
|
||||
from specify_cli.integrations.base import MarkdownIntegration
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
|
||||
class MarkdownIntegrationTests:
|
||||
"""Mixin — set class-level constants and inherit these tests.
|
||||
|
||||
Required class attrs on subclass::
|
||||
|
||||
KEY: str — integration registry key
|
||||
FOLDER: str — e.g. ".claude/"
|
||||
COMMANDS_SUBDIR: str — e.g. "commands"
|
||||
REGISTRAR_DIR: str — e.g. ".claude/commands"
|
||||
CONTEXT_FILE: str — e.g. "CLAUDE.md"
|
||||
"""
|
||||
|
||||
KEY: str
|
||||
FOLDER: str
|
||||
COMMANDS_SUBDIR: str
|
||||
REGISTRAR_DIR: str
|
||||
CONTEXT_FILE: str
|
||||
|
||||
# -- Registration -----------------------------------------------------
|
||||
|
||||
def test_registered(self):
|
||||
assert self.KEY in INTEGRATION_REGISTRY
|
||||
assert get_integration(self.KEY) is not None
|
||||
|
||||
def test_is_markdown_integration(self):
|
||||
assert isinstance(get_integration(self.KEY), MarkdownIntegration)
|
||||
|
||||
# -- Config -----------------------------------------------------------
|
||||
|
||||
def test_config_folder(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i.config["folder"] == self.FOLDER
|
||||
|
||||
def test_config_commands_subdir(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i.config["commands_subdir"] == self.COMMANDS_SUBDIR
|
||||
|
||||
def test_registrar_config(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i.registrar_config["dir"] == self.REGISTRAR_DIR
|
||||
assert i.registrar_config["format"] == "markdown"
|
||||
assert i.registrar_config["args"] == "$ARGUMENTS"
|
||||
assert i.registrar_config["extension"] == ".md"
|
||||
|
||||
def test_context_file(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i.context_file == self.CONTEXT_FILE
|
||||
|
||||
# -- Setup / teardown -------------------------------------------------
|
||||
|
||||
def test_setup_creates_files(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
assert len(created) > 0
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
for f in cmd_files:
|
||||
assert f.exists()
|
||||
assert f.name.startswith("speckit.")
|
||||
assert f.name.endswith(".md")
|
||||
|
||||
def test_setup_writes_to_correct_directory(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
expected_dir = i.commands_dest(tmp_path)
|
||||
assert expected_dir.exists(), f"Expected directory {expected_dir} was not created"
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
assert len(cmd_files) > 0, "No command files were created"
|
||||
for f in cmd_files:
|
||||
assert f.resolve().parent == expected_dir.resolve(), (
|
||||
f"{f} is not under {expected_dir}"
|
||||
)
|
||||
|
||||
def test_templates_are_processed(self, tmp_path):
|
||||
"""Command files must have placeholders replaced, not raw templates."""
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
assert len(cmd_files) > 0
|
||||
for f in cmd_files:
|
||||
content = f.read_text(encoding="utf-8")
|
||||
assert "{SCRIPT}" not in content, f"{f.name} has unprocessed {{SCRIPT}}"
|
||||
assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__"
|
||||
assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}"
|
||||
assert "\nscripts:\n" not in content, f"{f.name} has unstripped scripts: block"
|
||||
assert "\nagent_scripts:\n" not in content, f"{f.name} has unstripped agent_scripts: block"
|
||||
|
||||
def test_all_files_tracked_in_manifest(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
for f in created:
|
||||
rel = f.resolve().relative_to(tmp_path.resolve()).as_posix()
|
||||
assert rel in m.files, f"{rel} not tracked in manifest"
|
||||
|
||||
def test_install_uninstall_roundtrip(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.install(tmp_path, m)
|
||||
assert len(created) > 0
|
||||
m.save()
|
||||
for f in created:
|
||||
assert f.exists()
|
||||
removed, skipped = i.uninstall(tmp_path, m)
|
||||
assert len(removed) == len(created)
|
||||
assert skipped == []
|
||||
|
||||
def test_modified_file_survives_uninstall(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.install(tmp_path, m)
|
||||
m.save()
|
||||
modified_file = created[0]
|
||||
modified_file.write_text("user modified this", encoding="utf-8")
|
||||
removed, skipped = i.uninstall(tmp_path, m)
|
||||
assert modified_file.exists()
|
||||
assert modified_file in skipped
|
||||
|
||||
# -- Scripts ----------------------------------------------------------
|
||||
|
||||
def test_setup_installs_update_context_scripts(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts"
|
||||
assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}"
|
||||
assert (scripts_dir / "update-context.sh").exists()
|
||||
assert (scripts_dir / "update-context.ps1").exists()
|
||||
|
||||
def test_scripts_tracked_in_manifest(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
i.setup(tmp_path, m)
|
||||
script_rels = [k for k in m.files if "update-context" in k]
|
||||
assert len(script_rels) >= 2
|
||||
|
||||
def test_sh_script_is_executable(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
i.setup(tmp_path, m)
|
||||
sh = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" / "update-context.sh"
|
||||
assert os.access(sh, os.X_OK)
|
||||
|
||||
# -- CLI auto-promote -------------------------------------------------
|
||||
|
||||
def test_ai_flag_auto_promotes(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
project = tmp_path / f"promote-{self.KEY}"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--ai", self.KEY, "--script", "sh", "--no-git",
|
||||
"--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}"
|
||||
assert f"--integration {self.KEY}" in result.output
|
||||
|
||||
def test_integration_flag_creates_files(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
project = tmp_path / f"int-{self.KEY}"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, [
|
||||
"init", "--here", "--integration", self.KEY, "--script", "sh", "--no-git",
|
||||
"--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}"
|
||||
i = get_integration(self.KEY)
|
||||
cmd_dir = i.commands_dest(project)
|
||||
assert cmd_dir.is_dir(), f"Commands directory {cmd_dir} not created"
|
||||
commands = sorted(cmd_dir.glob("speckit.*"))
|
||||
assert len(commands) > 0, f"No command files in {cmd_dir}"
|
||||
|
||||
# -- Complete file inventory ------------------------------------------
|
||||
|
||||
COMMAND_STEMS = [
|
||||
"analyze", "checklist", "clarify", "constitution",
|
||||
"implement", "plan", "specify", "tasks", "taskstoissues",
|
||||
]
|
||||
|
||||
def _expected_files(self, script_variant: str) -> list[str]:
|
||||
"""Build the expected file list for this integration + script variant."""
|
||||
i = get_integration(self.KEY)
|
||||
cmd_dir = i.registrar_config["dir"]
|
||||
files = []
|
||||
|
||||
# Command files
|
||||
for stem in self.COMMAND_STEMS:
|
||||
files.append(f"{cmd_dir}/speckit.{stem}.md")
|
||||
|
||||
# Integration scripts
|
||||
files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1")
|
||||
files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh")
|
||||
|
||||
# Framework files
|
||||
files.append(f".specify/integration.json")
|
||||
files.append(f".specify/init-options.json")
|
||||
files.append(f".specify/integrations/{self.KEY}.manifest.json")
|
||||
files.append(f".specify/integrations/speckit.manifest.json")
|
||||
|
||||
if script_variant == "sh":
|
||||
for name in ["check-prerequisites.sh", "common.sh", "create-new-feature.sh",
|
||||
"setup-plan.sh", "update-agent-context.sh"]:
|
||||
files.append(f".specify/scripts/bash/{name}")
|
||||
else:
|
||||
for name in ["check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1",
|
||||
"setup-plan.ps1", "update-agent-context.ps1"]:
|
||||
files.append(f".specify/scripts/powershell/{name}")
|
||||
|
||||
for name in ["agent-file-template.md", "checklist-template.md",
|
||||
"constitution-template.md", "plan-template.md",
|
||||
"spec-template.md", "tasks-template.md"]:
|
||||
files.append(f".specify/templates/{name}")
|
||||
|
||||
files.append(".specify/memory/constitution.md")
|
||||
return sorted(files)
|
||||
|
||||
def test_complete_file_inventory_sh(self, tmp_path):
|
||||
"""Every file produced by specify init --integration <key> --script sh."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
project = tmp_path / f"inventory-sh-{self.KEY}"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(app, [
|
||||
"init", "--here", "--integration", self.KEY, "--script", "sh",
|
||||
"--no-git", "--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init failed: {result.output}"
|
||||
actual = sorted(p.relative_to(project).as_posix()
|
||||
for p in project.rglob("*") if p.is_file())
|
||||
expected = self._expected_files("sh")
|
||||
assert actual == expected, (
|
||||
f"Missing: {sorted(set(expected) - set(actual))}\n"
|
||||
f"Extra: {sorted(set(actual) - set(expected))}"
|
||||
)
|
||||
|
||||
def test_complete_file_inventory_ps(self, tmp_path):
|
||||
"""Every file produced by specify init --integration <key> --script ps."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
project = tmp_path / f"inventory-ps-{self.KEY}"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(app, [
|
||||
"init", "--here", "--integration", self.KEY, "--script", "ps",
|
||||
"--no-git", "--ignore-agent-tools",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0, f"init failed: {result.output}"
|
||||
actual = sorted(p.relative_to(project).as_posix()
|
||||
for p in project.rglob("*") if p.is_file())
|
||||
expected = self._expected_files("ps")
|
||||
assert actual == expected, (
|
||||
f"Missing: {sorted(set(expected) - set(actual))}\n"
|
||||
f"Extra: {sorted(set(actual) - set(expected))}"
|
||||
)
|
||||
11
tests/integrations/test_integration_bob.py
Normal file
11
tests/integrations/test_integration_bob.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for BobIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestBobIntegration(MarkdownIntegrationTests):
|
||||
KEY = "bob"
|
||||
FOLDER = ".bob/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".bob/commands"
|
||||
CONTEXT_FILE = "AGENTS.md"
|
||||
11
tests/integrations/test_integration_claude.py
Normal file
11
tests/integrations/test_integration_claude.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for ClaudeIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestClaudeIntegration(MarkdownIntegrationTests):
|
||||
KEY = "claude"
|
||||
FOLDER = ".claude/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".claude/commands"
|
||||
CONTEXT_FILE = "CLAUDE.md"
|
||||
11
tests/integrations/test_integration_codebuddy.py
Normal file
11
tests/integrations/test_integration_codebuddy.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for CodebuddyIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestCodebuddyIntegration(MarkdownIntegrationTests):
|
||||
KEY = "codebuddy"
|
||||
FOLDER = ".codebuddy/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".codebuddy/commands"
|
||||
CONTEXT_FILE = "CODEBUDDY.md"
|
||||
266
tests/integrations/test_integration_copilot.py
Normal file
266
tests/integrations/test_integration_copilot.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""Tests for CopilotIntegration."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from specify_cli.integrations import get_integration
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
|
||||
class TestCopilotIntegration:
|
||||
def test_copilot_key_and_config(self):
|
||||
copilot = get_integration("copilot")
|
||||
assert copilot is not None
|
||||
assert copilot.key == "copilot"
|
||||
assert copilot.config["folder"] == ".github/"
|
||||
assert copilot.config["commands_subdir"] == "agents"
|
||||
assert copilot.registrar_config["extension"] == ".agent.md"
|
||||
assert copilot.context_file == ".github/copilot-instructions.md"
|
||||
|
||||
def test_command_filename_agent_md(self):
|
||||
copilot = get_integration("copilot")
|
||||
assert copilot.command_filename("plan") == "speckit.plan.agent.md"
|
||||
|
||||
def test_setup_creates_agent_md_files(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
created = copilot.setup(tmp_path, m)
|
||||
assert len(created) > 0
|
||||
agent_files = [f for f in created if ".agent." in f.name]
|
||||
assert len(agent_files) > 0
|
||||
for f in agent_files:
|
||||
assert f.parent == tmp_path / ".github" / "agents"
|
||||
assert f.name.endswith(".agent.md")
|
||||
|
||||
def test_setup_creates_companion_prompts(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
created = copilot.setup(tmp_path, m)
|
||||
prompt_files = [f for f in created if f.parent.name == "prompts"]
|
||||
assert len(prompt_files) > 0
|
||||
for f in prompt_files:
|
||||
assert f.name.endswith(".prompt.md")
|
||||
content = f.read_text(encoding="utf-8")
|
||||
assert content.startswith("---\nagent: speckit.")
|
||||
|
||||
def test_agent_and_prompt_counts_match(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
created = copilot.setup(tmp_path, m)
|
||||
agents = [f for f in created if ".agent.md" in f.name]
|
||||
prompts = [f for f in created if ".prompt.md" in f.name]
|
||||
assert len(agents) == len(prompts)
|
||||
|
||||
def test_setup_creates_vscode_settings_new(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
assert copilot._vscode_settings_path() is not None
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
created = copilot.setup(tmp_path, m)
|
||||
settings = tmp_path / ".vscode" / "settings.json"
|
||||
assert settings.exists()
|
||||
assert settings in created
|
||||
assert any("settings.json" in k for k in m.files)
|
||||
|
||||
def test_setup_merges_existing_vscode_settings(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
vscode_dir = tmp_path / ".vscode"
|
||||
vscode_dir.mkdir(parents=True)
|
||||
existing = {"editor.fontSize": 14, "custom.setting": True}
|
||||
(vscode_dir / "settings.json").write_text(json.dumps(existing, indent=4), encoding="utf-8")
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
created = copilot.setup(tmp_path, m)
|
||||
settings = tmp_path / ".vscode" / "settings.json"
|
||||
data = json.loads(settings.read_text(encoding="utf-8"))
|
||||
assert data["editor.fontSize"] == 14
|
||||
assert data["custom.setting"] is True
|
||||
assert settings not in created
|
||||
assert not any("settings.json" in k for k in m.files)
|
||||
|
||||
def test_all_created_files_tracked_in_manifest(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
created = copilot.setup(tmp_path, m)
|
||||
for f in created:
|
||||
rel = f.resolve().relative_to(tmp_path.resolve()).as_posix()
|
||||
assert rel in m.files, f"Created file {rel} not tracked in manifest"
|
||||
|
||||
def test_install_uninstall_roundtrip(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
created = copilot.install(tmp_path, m)
|
||||
assert len(created) > 0
|
||||
m.save()
|
||||
for f in created:
|
||||
assert f.exists()
|
||||
removed, skipped = copilot.uninstall(tmp_path, m)
|
||||
assert len(removed) == len(created)
|
||||
assert skipped == []
|
||||
|
||||
def test_modified_file_survives_uninstall(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
created = copilot.install(tmp_path, m)
|
||||
m.save()
|
||||
modified_file = created[0]
|
||||
modified_file.write_text("user modified this", encoding="utf-8")
|
||||
removed, skipped = copilot.uninstall(tmp_path, m)
|
||||
assert modified_file.exists()
|
||||
assert modified_file in skipped
|
||||
|
||||
def test_directory_structure(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
copilot.setup(tmp_path, m)
|
||||
agents_dir = tmp_path / ".github" / "agents"
|
||||
assert agents_dir.is_dir()
|
||||
agent_files = sorted(agents_dir.glob("speckit.*.agent.md"))
|
||||
assert len(agent_files) == 9
|
||||
expected_commands = {
|
||||
"analyze", "checklist", "clarify", "constitution",
|
||||
"implement", "plan", "specify", "tasks", "taskstoissues",
|
||||
}
|
||||
actual_commands = {f.name.removeprefix("speckit.").removesuffix(".agent.md") for f in agent_files}
|
||||
assert actual_commands == expected_commands
|
||||
|
||||
def test_templates_are_processed(self, tmp_path):
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
copilot = CopilotIntegration()
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
copilot.setup(tmp_path, m)
|
||||
agents_dir = tmp_path / ".github" / "agents"
|
||||
for agent_file in agents_dir.glob("speckit.*.agent.md"):
|
||||
content = agent_file.read_text(encoding="utf-8")
|
||||
assert "{SCRIPT}" not in content, f"{agent_file.name} has unprocessed {{SCRIPT}}"
|
||||
assert "__AGENT__" not in content, f"{agent_file.name} has unprocessed __AGENT__"
|
||||
assert "{ARGS}" not in content, f"{agent_file.name} has unprocessed {{ARGS}}"
|
||||
assert "\nscripts:\n" not in content
|
||||
assert "\nagent_scripts:\n" not in content
|
||||
|
||||
def test_complete_file_inventory_sh(self, tmp_path):
|
||||
"""Every file produced by specify init --integration copilot --script sh."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
project = tmp_path / "inventory-sh"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(app, [
|
||||
"init", "--here", "--integration", "copilot", "--script", "sh", "--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0
|
||||
actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file())
|
||||
expected = sorted([
|
||||
".github/agents/speckit.analyze.agent.md",
|
||||
".github/agents/speckit.checklist.agent.md",
|
||||
".github/agents/speckit.clarify.agent.md",
|
||||
".github/agents/speckit.constitution.agent.md",
|
||||
".github/agents/speckit.implement.agent.md",
|
||||
".github/agents/speckit.plan.agent.md",
|
||||
".github/agents/speckit.specify.agent.md",
|
||||
".github/agents/speckit.tasks.agent.md",
|
||||
".github/agents/speckit.taskstoissues.agent.md",
|
||||
".github/prompts/speckit.analyze.prompt.md",
|
||||
".github/prompts/speckit.checklist.prompt.md",
|
||||
".github/prompts/speckit.clarify.prompt.md",
|
||||
".github/prompts/speckit.constitution.prompt.md",
|
||||
".github/prompts/speckit.implement.prompt.md",
|
||||
".github/prompts/speckit.plan.prompt.md",
|
||||
".github/prompts/speckit.specify.prompt.md",
|
||||
".github/prompts/speckit.tasks.prompt.md",
|
||||
".github/prompts/speckit.taskstoissues.prompt.md",
|
||||
".vscode/settings.json",
|
||||
".specify/integration.json",
|
||||
".specify/init-options.json",
|
||||
".specify/integrations/copilot.manifest.json",
|
||||
".specify/integrations/speckit.manifest.json",
|
||||
".specify/integrations/copilot/scripts/update-context.ps1",
|
||||
".specify/integrations/copilot/scripts/update-context.sh",
|
||||
".specify/scripts/bash/check-prerequisites.sh",
|
||||
".specify/scripts/bash/common.sh",
|
||||
".specify/scripts/bash/create-new-feature.sh",
|
||||
".specify/scripts/bash/setup-plan.sh",
|
||||
".specify/scripts/bash/update-agent-context.sh",
|
||||
".specify/templates/agent-file-template.md",
|
||||
".specify/templates/checklist-template.md",
|
||||
".specify/templates/constitution-template.md",
|
||||
".specify/templates/plan-template.md",
|
||||
".specify/templates/spec-template.md",
|
||||
".specify/templates/tasks-template.md",
|
||||
".specify/memory/constitution.md",
|
||||
])
|
||||
assert actual == expected, (
|
||||
f"Missing: {sorted(set(expected) - set(actual))}\n"
|
||||
f"Extra: {sorted(set(actual) - set(expected))}"
|
||||
)
|
||||
|
||||
def test_complete_file_inventory_ps(self, tmp_path):
|
||||
"""Every file produced by specify init --integration copilot --script ps."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
project = tmp_path / "inventory-ps"
|
||||
project.mkdir()
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(project)
|
||||
result = CliRunner().invoke(app, [
|
||||
"init", "--here", "--integration", "copilot", "--script", "ps", "--no-git",
|
||||
], catch_exceptions=False)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0
|
||||
actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file())
|
||||
expected = sorted([
|
||||
".github/agents/speckit.analyze.agent.md",
|
||||
".github/agents/speckit.checklist.agent.md",
|
||||
".github/agents/speckit.clarify.agent.md",
|
||||
".github/agents/speckit.constitution.agent.md",
|
||||
".github/agents/speckit.implement.agent.md",
|
||||
".github/agents/speckit.plan.agent.md",
|
||||
".github/agents/speckit.specify.agent.md",
|
||||
".github/agents/speckit.tasks.agent.md",
|
||||
".github/agents/speckit.taskstoissues.agent.md",
|
||||
".github/prompts/speckit.analyze.prompt.md",
|
||||
".github/prompts/speckit.checklist.prompt.md",
|
||||
".github/prompts/speckit.clarify.prompt.md",
|
||||
".github/prompts/speckit.constitution.prompt.md",
|
||||
".github/prompts/speckit.implement.prompt.md",
|
||||
".github/prompts/speckit.plan.prompt.md",
|
||||
".github/prompts/speckit.specify.prompt.md",
|
||||
".github/prompts/speckit.tasks.prompt.md",
|
||||
".github/prompts/speckit.taskstoissues.prompt.md",
|
||||
".vscode/settings.json",
|
||||
".specify/integration.json",
|
||||
".specify/init-options.json",
|
||||
".specify/integrations/copilot.manifest.json",
|
||||
".specify/integrations/speckit.manifest.json",
|
||||
".specify/integrations/copilot/scripts/update-context.ps1",
|
||||
".specify/integrations/copilot/scripts/update-context.sh",
|
||||
".specify/scripts/powershell/check-prerequisites.ps1",
|
||||
".specify/scripts/powershell/common.ps1",
|
||||
".specify/scripts/powershell/create-new-feature.ps1",
|
||||
".specify/scripts/powershell/setup-plan.ps1",
|
||||
".specify/scripts/powershell/update-agent-context.ps1",
|
||||
".specify/templates/agent-file-template.md",
|
||||
".specify/templates/checklist-template.md",
|
||||
".specify/templates/constitution-template.md",
|
||||
".specify/templates/plan-template.md",
|
||||
".specify/templates/spec-template.md",
|
||||
".specify/templates/tasks-template.md",
|
||||
".specify/memory/constitution.md",
|
||||
])
|
||||
assert actual == expected, (
|
||||
f"Missing: {sorted(set(expected) - set(actual))}\n"
|
||||
f"Extra: {sorted(set(actual) - set(expected))}"
|
||||
)
|
||||
11
tests/integrations/test_integration_cursor_agent.py
Normal file
11
tests/integrations/test_integration_cursor_agent.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for CursorAgentIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestCursorAgentIntegration(MarkdownIntegrationTests):
|
||||
KEY = "cursor-agent"
|
||||
FOLDER = ".cursor/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".cursor/commands"
|
||||
CONTEXT_FILE = ".cursor/rules/specify-rules.mdc"
|
||||
11
tests/integrations/test_integration_iflow.py
Normal file
11
tests/integrations/test_integration_iflow.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for IflowIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestIflowIntegration(MarkdownIntegrationTests):
|
||||
KEY = "iflow"
|
||||
FOLDER = ".iflow/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".iflow/commands"
|
||||
CONTEXT_FILE = "IFLOW.md"
|
||||
11
tests/integrations/test_integration_junie.py
Normal file
11
tests/integrations/test_integration_junie.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for JunieIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestJunieIntegration(MarkdownIntegrationTests):
|
||||
KEY = "junie"
|
||||
FOLDER = ".junie/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".junie/commands"
|
||||
CONTEXT_FILE = ".junie/AGENTS.md"
|
||||
11
tests/integrations/test_integration_kilocode.py
Normal file
11
tests/integrations/test_integration_kilocode.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for KilocodeIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestKilocodeIntegration(MarkdownIntegrationTests):
|
||||
KEY = "kilocode"
|
||||
FOLDER = ".kilocode/"
|
||||
COMMANDS_SUBDIR = "workflows"
|
||||
REGISTRAR_DIR = ".kilocode/workflows"
|
||||
CONTEXT_FILE = ".kilocode/rules/specify-rules.md"
|
||||
11
tests/integrations/test_integration_kiro_cli.py
Normal file
11
tests/integrations/test_integration_kiro_cli.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for KiroCliIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestKiroCliIntegration(MarkdownIntegrationTests):
|
||||
KEY = "kiro-cli"
|
||||
FOLDER = ".kiro/"
|
||||
COMMANDS_SUBDIR = "prompts"
|
||||
REGISTRAR_DIR = ".kiro/prompts"
|
||||
CONTEXT_FILE = "AGENTS.md"
|
||||
11
tests/integrations/test_integration_opencode.py
Normal file
11
tests/integrations/test_integration_opencode.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for OpencodeIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestOpencodeIntegration(MarkdownIntegrationTests):
|
||||
KEY = "opencode"
|
||||
FOLDER = ".opencode/"
|
||||
COMMANDS_SUBDIR = "command"
|
||||
REGISTRAR_DIR = ".opencode/command"
|
||||
CONTEXT_FILE = "AGENTS.md"
|
||||
11
tests/integrations/test_integration_pi.py
Normal file
11
tests/integrations/test_integration_pi.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for PiIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestPiIntegration(MarkdownIntegrationTests):
|
||||
KEY = "pi"
|
||||
FOLDER = ".pi/"
|
||||
COMMANDS_SUBDIR = "prompts"
|
||||
REGISTRAR_DIR = ".pi/prompts"
|
||||
CONTEXT_FILE = "AGENTS.md"
|
||||
11
tests/integrations/test_integration_qodercli.py
Normal file
11
tests/integrations/test_integration_qodercli.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for QodercliIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestQodercliIntegration(MarkdownIntegrationTests):
|
||||
KEY = "qodercli"
|
||||
FOLDER = ".qoder/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".qoder/commands"
|
||||
CONTEXT_FILE = "QODER.md"
|
||||
11
tests/integrations/test_integration_qwen.py
Normal file
11
tests/integrations/test_integration_qwen.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for QwenIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestQwenIntegration(MarkdownIntegrationTests):
|
||||
KEY = "qwen"
|
||||
FOLDER = ".qwen/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".qwen/commands"
|
||||
CONTEXT_FILE = "QWEN.md"
|
||||
11
tests/integrations/test_integration_roo.py
Normal file
11
tests/integrations/test_integration_roo.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for RooIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestRooIntegration(MarkdownIntegrationTests):
|
||||
KEY = "roo"
|
||||
FOLDER = ".roo/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".roo/commands"
|
||||
CONTEXT_FILE = ".roo/rules/specify-rules.md"
|
||||
11
tests/integrations/test_integration_shai.py
Normal file
11
tests/integrations/test_integration_shai.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for ShaiIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestShaiIntegration(MarkdownIntegrationTests):
|
||||
KEY = "shai"
|
||||
FOLDER = ".shai/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".shai/commands"
|
||||
CONTEXT_FILE = "SHAI.md"
|
||||
11
tests/integrations/test_integration_trae.py
Normal file
11
tests/integrations/test_integration_trae.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for TraeIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestTraeIntegration(MarkdownIntegrationTests):
|
||||
KEY = "trae"
|
||||
FOLDER = ".trae/"
|
||||
COMMANDS_SUBDIR = "rules"
|
||||
REGISTRAR_DIR = ".trae/rules"
|
||||
CONTEXT_FILE = ".trae/rules/AGENTS.md"
|
||||
11
tests/integrations/test_integration_vibe.py
Normal file
11
tests/integrations/test_integration_vibe.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for VibeIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestVibeIntegration(MarkdownIntegrationTests):
|
||||
KEY = "vibe"
|
||||
FOLDER = ".vibe/"
|
||||
COMMANDS_SUBDIR = "prompts"
|
||||
REGISTRAR_DIR = ".vibe/prompts"
|
||||
CONTEXT_FILE = ".vibe/agents/specify-agents.md"
|
||||
11
tests/integrations/test_integration_windsurf.py
Normal file
11
tests/integrations/test_integration_windsurf.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Tests for WindsurfIntegration."""
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestWindsurfIntegration(MarkdownIntegrationTests):
|
||||
KEY = "windsurf"
|
||||
FOLDER = ".windsurf/"
|
||||
COMMANDS_SUBDIR = "workflows"
|
||||
REGISTRAR_DIR = ".windsurf/workflows"
|
||||
CONTEXT_FILE = ".windsurf/rules/specify-rules.md"
|
||||
245
tests/integrations/test_manifest.py
Normal file
245
tests/integrations/test_manifest.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""Tests for IntegrationManifest — record, hash, save, load, uninstall, modified detection."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from specify_cli.integrations.manifest import IntegrationManifest, _sha256
|
||||
|
||||
|
||||
class TestManifestRecordFile:
|
||||
def test_record_file_writes_and_hashes(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
content = "hello world"
|
||||
abs_path = m.record_file("a/b.txt", content)
|
||||
assert abs_path == tmp_path / "a" / "b.txt"
|
||||
assert abs_path.read_text(encoding="utf-8") == content
|
||||
expected_hash = hashlib.sha256(content.encode()).hexdigest()
|
||||
assert m.files["a/b.txt"] == expected_hash
|
||||
|
||||
def test_record_file_bytes(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
data = b"\x00\x01\x02"
|
||||
abs_path = m.record_file("bin.dat", data)
|
||||
assert abs_path.read_bytes() == data
|
||||
assert m.files["bin.dat"] == hashlib.sha256(data).hexdigest()
|
||||
|
||||
def test_record_existing(self, tmp_path):
|
||||
f = tmp_path / "existing.txt"
|
||||
f.write_text("content", encoding="utf-8")
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_existing("existing.txt")
|
||||
assert m.files["existing.txt"] == _sha256(f)
|
||||
|
||||
|
||||
class TestManifestPathTraversal:
|
||||
def test_record_file_rejects_parent_traversal(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
with pytest.raises(ValueError, match="outside"):
|
||||
m.record_file("../escape.txt", "bad")
|
||||
|
||||
def test_record_file_rejects_absolute_path(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
with pytest.raises(ValueError, match="Absolute paths"):
|
||||
m.record_file("/tmp/escape.txt", "bad")
|
||||
|
||||
def test_record_existing_rejects_parent_traversal(self, tmp_path):
|
||||
escape = tmp_path.parent / "escape.txt"
|
||||
escape.write_text("evil", encoding="utf-8")
|
||||
try:
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
with pytest.raises(ValueError, match="outside"):
|
||||
m.record_existing("../escape.txt")
|
||||
finally:
|
||||
escape.unlink(missing_ok=True)
|
||||
|
||||
def test_uninstall_skips_traversal_paths(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("safe.txt", "good")
|
||||
m._files["../outside.txt"] = "fakehash"
|
||||
m.save()
|
||||
removed, skipped = m.uninstall()
|
||||
assert len(removed) == 1
|
||||
assert removed[0].name == "safe.txt"
|
||||
|
||||
|
||||
class TestManifestCheckModified:
|
||||
def test_unmodified_file(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "original")
|
||||
assert m.check_modified() == []
|
||||
|
||||
def test_modified_file(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "original")
|
||||
(tmp_path / "f.txt").write_text("changed", encoding="utf-8")
|
||||
assert m.check_modified() == ["f.txt"]
|
||||
|
||||
def test_deleted_file_not_reported(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "original")
|
||||
(tmp_path / "f.txt").unlink()
|
||||
assert m.check_modified() == []
|
||||
|
||||
def test_symlink_treated_as_modified(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "original")
|
||||
target = tmp_path / "target.txt"
|
||||
target.write_text("target", encoding="utf-8")
|
||||
(tmp_path / "f.txt").unlink()
|
||||
(tmp_path / "f.txt").symlink_to(target)
|
||||
assert m.check_modified() == ["f.txt"]
|
||||
|
||||
|
||||
class TestManifestUninstall:
|
||||
def test_removes_unmodified(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("d/f.txt", "content")
|
||||
m.save()
|
||||
removed, skipped = m.uninstall()
|
||||
assert len(removed) == 1
|
||||
assert not (tmp_path / "d" / "f.txt").exists()
|
||||
assert not (tmp_path / "d").exists()
|
||||
assert skipped == []
|
||||
|
||||
def test_skips_modified(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "original")
|
||||
m.save()
|
||||
(tmp_path / "f.txt").write_text("modified", encoding="utf-8")
|
||||
removed, skipped = m.uninstall()
|
||||
assert removed == []
|
||||
assert len(skipped) == 1
|
||||
assert (tmp_path / "f.txt").exists()
|
||||
|
||||
def test_force_removes_modified(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "original")
|
||||
m.save()
|
||||
(tmp_path / "f.txt").write_text("modified", encoding="utf-8")
|
||||
removed, skipped = m.uninstall(force=True)
|
||||
assert len(removed) == 1
|
||||
assert skipped == []
|
||||
|
||||
def test_already_deleted_file(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "content")
|
||||
m.save()
|
||||
(tmp_path / "f.txt").unlink()
|
||||
removed, skipped = m.uninstall()
|
||||
assert removed == []
|
||||
assert skipped == []
|
||||
|
||||
def test_removes_manifest_file(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path, version="1.0")
|
||||
m.record_file("f.txt", "content")
|
||||
m.save()
|
||||
assert m.manifest_path.exists()
|
||||
m.uninstall()
|
||||
assert not m.manifest_path.exists()
|
||||
|
||||
def test_cleans_empty_parent_dirs(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("a/b/c/f.txt", "content")
|
||||
m.save()
|
||||
m.uninstall()
|
||||
assert not (tmp_path / "a").exists()
|
||||
|
||||
def test_preserves_nonempty_parent_dirs(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("a/b/tracked.txt", "content")
|
||||
(tmp_path / "a" / "b" / "other.txt").write_text("keep", encoding="utf-8")
|
||||
m.save()
|
||||
m.uninstall()
|
||||
assert not (tmp_path / "a" / "b" / "tracked.txt").exists()
|
||||
assert (tmp_path / "a" / "b" / "other.txt").exists()
|
||||
|
||||
def test_symlink_skipped_without_force(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "original")
|
||||
m.save()
|
||||
target = tmp_path / "target.txt"
|
||||
target.write_text("target", encoding="utf-8")
|
||||
(tmp_path / "f.txt").unlink()
|
||||
(tmp_path / "f.txt").symlink_to(target)
|
||||
removed, skipped = m.uninstall()
|
||||
assert removed == []
|
||||
assert len(skipped) == 1
|
||||
|
||||
def test_symlink_removed_with_force(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "original")
|
||||
m.save()
|
||||
target = tmp_path / "target.txt"
|
||||
target.write_text("target", encoding="utf-8")
|
||||
(tmp_path / "f.txt").unlink()
|
||||
(tmp_path / "f.txt").symlink_to(target)
|
||||
removed, skipped = m.uninstall(force=True)
|
||||
assert len(removed) == 1
|
||||
assert target.exists()
|
||||
|
||||
|
||||
class TestManifestPersistence:
|
||||
def test_save_and_load_roundtrip(self, tmp_path):
|
||||
m = IntegrationManifest("myagent", tmp_path, version="2.0.1")
|
||||
m.record_file("dir/file.md", "# Hello")
|
||||
m.save()
|
||||
loaded = IntegrationManifest.load("myagent", tmp_path)
|
||||
assert loaded.key == "myagent"
|
||||
assert loaded.version == "2.0.1"
|
||||
assert loaded.files == m.files
|
||||
|
||||
def test_manifest_path(self, tmp_path):
|
||||
m = IntegrationManifest("copilot", tmp_path)
|
||||
assert m.manifest_path == tmp_path / ".specify" / "integrations" / "copilot.manifest.json"
|
||||
|
||||
def test_load_missing_raises(self, tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
IntegrationManifest.load("nonexistent", tmp_path)
|
||||
|
||||
def test_save_creates_directories(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "content")
|
||||
path = m.save()
|
||||
assert path.exists()
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert data["integration"] == "test"
|
||||
|
||||
def test_save_preserves_installed_at(self, tmp_path):
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_file("f.txt", "content")
|
||||
m.save()
|
||||
first_ts = m._installed_at
|
||||
m.save()
|
||||
assert m._installed_at == first_ts
|
||||
|
||||
|
||||
class TestManifestLoadValidation:
|
||||
def test_load_non_dict_raises(self, tmp_path):
|
||||
path = tmp_path / ".specify" / "integrations" / "bad.manifest.json"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text('"just a string"', encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="JSON object"):
|
||||
IntegrationManifest.load("bad", tmp_path)
|
||||
|
||||
def test_load_bad_files_type_raises(self, tmp_path):
|
||||
path = tmp_path / ".specify" / "integrations" / "bad.manifest.json"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(json.dumps({"files": ["not", "a", "dict"]}), encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="mapping"):
|
||||
IntegrationManifest.load("bad", tmp_path)
|
||||
|
||||
def test_load_bad_files_values_raises(self, tmp_path):
|
||||
path = tmp_path / ".specify" / "integrations" / "bad.manifest.json"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(json.dumps({"files": {"a.txt": 123}}), encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="mapping"):
|
||||
IntegrationManifest.load("bad", tmp_path)
|
||||
|
||||
def test_load_invalid_json_raises(self, tmp_path):
|
||||
path = tmp_path / ".specify" / "integrations" / "bad.manifest.json"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text("{not valid json", encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="invalid JSON"):
|
||||
IntegrationManifest.load("bad", tmp_path)
|
||||
76
tests/integrations/test_registry.py
Normal file
76
tests/integrations/test_registry.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Tests for INTEGRATION_REGISTRY — mechanics, completeness, and registrar alignment."""
|
||||
|
||||
import pytest
|
||||
|
||||
from specify_cli.integrations import (
|
||||
INTEGRATION_REGISTRY,
|
||||
_register,
|
||||
get_integration,
|
||||
)
|
||||
from specify_cli.integrations.base import MarkdownIntegration
|
||||
from .conftest import StubIntegration
|
||||
|
||||
|
||||
# Every integration key that must be registered (Stage 2 + Stage 3).
|
||||
ALL_INTEGRATION_KEYS = [
|
||||
"copilot",
|
||||
# Stage 3 — standard markdown integrations
|
||||
"claude", "qwen", "opencode", "junie", "kilocode", "auggie",
|
||||
"roo", "codebuddy", "qodercli", "amp", "shai", "bob", "trae",
|
||||
"pi", "iflow", "kiro-cli", "windsurf", "vibe", "cursor-agent",
|
||||
]
|
||||
|
||||
|
||||
class TestRegistry:
|
||||
def test_registry_is_dict(self):
|
||||
assert isinstance(INTEGRATION_REGISTRY, dict)
|
||||
|
||||
def test_register_and_get(self):
|
||||
stub = StubIntegration()
|
||||
_register(stub)
|
||||
try:
|
||||
assert get_integration("stub") is stub
|
||||
finally:
|
||||
INTEGRATION_REGISTRY.pop("stub", None)
|
||||
|
||||
def test_get_missing_returns_none(self):
|
||||
assert get_integration("nonexistent-xyz") is None
|
||||
|
||||
def test_register_empty_key_raises(self):
|
||||
class EmptyKey(MarkdownIntegration):
|
||||
key = ""
|
||||
with pytest.raises(ValueError, match="empty key"):
|
||||
_register(EmptyKey())
|
||||
|
||||
def test_register_duplicate_raises(self):
|
||||
stub = StubIntegration()
|
||||
_register(stub)
|
||||
try:
|
||||
with pytest.raises(KeyError, match="already registered"):
|
||||
_register(StubIntegration())
|
||||
finally:
|
||||
INTEGRATION_REGISTRY.pop("stub", None)
|
||||
|
||||
|
||||
class TestRegistryCompleteness:
|
||||
"""Every expected integration must be registered."""
|
||||
|
||||
@pytest.mark.parametrize("key", ALL_INTEGRATION_KEYS)
|
||||
def test_key_registered(self, key):
|
||||
assert key in INTEGRATION_REGISTRY, f"{key} missing from registry"
|
||||
|
||||
|
||||
class TestRegistrarKeyAlignment:
|
||||
"""Every integration key must have a matching AGENT_CONFIGS entry."""
|
||||
|
||||
@pytest.mark.parametrize("key", ALL_INTEGRATION_KEYS)
|
||||
def test_integration_key_in_registrar(self, key):
|
||||
from specify_cli.agents import CommandRegistrar
|
||||
assert key in CommandRegistrar.AGENT_CONFIGS, (
|
||||
f"Integration '{key}' is registered but has no AGENT_CONFIGS entry"
|
||||
)
|
||||
|
||||
def test_no_stale_cursor_shorthand(self):
|
||||
"""The old 'cursor' shorthand must not appear in AGENT_CONFIGS."""
|
||||
from specify_cli.agents import CommandRegistrar
|
||||
assert "cursor" not in CommandRegistrar.AGENT_CONFIGS
|
||||
@@ -1237,24 +1237,22 @@ class TestCliValidation:
|
||||
assert "agent skills" in plain.lower()
|
||||
|
||||
def test_kiro_alias_normalized_to_kiro_cli(self, tmp_path):
|
||||
"""--ai kiro should normalize to canonical kiro-cli agent key."""
|
||||
"""--ai kiro should normalize to canonical kiro-cli and auto-promote to integration path."""
|
||||
import os
|
||||
from typer.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
target = tmp_path / "kiro-alias-proj"
|
||||
target.mkdir()
|
||||
|
||||
with patch("specify_cli.download_and_extract_template") as mock_download, \
|
||||
patch("specify_cli.scaffold_from_core_pack", create=True) as mock_scaffold, \
|
||||
patch("specify_cli.ensure_executable_scripts"), \
|
||||
patch("specify_cli.ensure_constitution_from_template"), \
|
||||
patch("specify_cli.is_git_repo", return_value=False), \
|
||||
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
|
||||
mock_scaffold.return_value = True
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(target)
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
str(target),
|
||||
"--here",
|
||||
"--ai",
|
||||
"kiro",
|
||||
"--ignore-agent-tools",
|
||||
@@ -1262,17 +1260,16 @@ class TestCliValidation:
|
||||
"sh",
|
||||
"--no-git",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
|
||||
assert result.exit_code == 0
|
||||
# Without --offline, the download path should be taken.
|
||||
assert mock_download.called, (
|
||||
"Expected download_and_extract_template to be called (default non-offline path)"
|
||||
)
|
||||
assert mock_download.call_args.args[1] == "kiro-cli"
|
||||
assert not mock_scaffold.called, (
|
||||
"scaffold_from_core_pack should not be called without --offline"
|
||||
)
|
||||
# kiro alias should auto-promote to integration path with nudge
|
||||
assert "--integration kiro-cli" in result.output
|
||||
# Command files should be created via integration path
|
||||
assert (target / ".kiro" / "prompts" / "speckit.plan.md").exists()
|
||||
|
||||
def test_q_removed_from_agent_config(self):
|
||||
"""Amazon Q legacy key should not remain in AGENT_CONFIG."""
|
||||
|
||||
@@ -981,7 +981,7 @@ $ARGUMENTS
|
||||
"Run scripts/bash/setup-plan.sh\n"
|
||||
)
|
||||
|
||||
rewritten = AgentCommandRegistrar._rewrite_project_relative_paths(body)
|
||||
rewritten = AgentCommandRegistrar.rewrite_project_relative_paths(body)
|
||||
|
||||
assert ".specify/extensions/test-ext/templates/spec.md" in rewritten
|
||||
assert ".specify/scripts/bash/setup-plan.sh" in rewritten
|
||||
|
||||
Reference in New Issue
Block a user