mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
541 lines
20 KiB
Python
541 lines
20 KiB
Python
"""Tests for ``specify integration`` subcommand (list, install, uninstall, switch)."""
|
|||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
|
||
|
|
from typer.testing import CliRunner
|
||
|
|
|
||
|
|
from specify_cli import app
|
||
|
|
|
||
|
|
|
||
|
|
runner = CliRunner()
|
||
|
|
|
||
|
|
|
||
|
|
def _init_project(tmp_path, integration="copilot"):
|
||
|
|
"""Helper: init a spec-kit project with the given integration."""
|
||
|
|
project = tmp_path / "proj"
|
||
|
|
project.mkdir()
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"init", "--here",
|
||
|
|
"--integration", integration,
|
||
|
|
"--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}"
|
||
|
|
return project
|
||
|
|
|
||
|
|
|
||
|
|
# ── list ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestIntegrationList:
|
||
|
|
def test_list_requires_speckit_project(self, tmp_path):
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(tmp_path)
|
||
|
|
result = runner.invoke(app, ["integration", "list"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "Not a spec-kit project" in result.output
|
||
|
|
|
||
|
|
def test_list_shows_installed(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path, "copilot")
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "list"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "copilot" in result.output
|
||
|
|
assert "installed" in result.output
|
||
|
|
|
||
|
|
def test_list_shows_available_integrations(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path, "copilot")
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "list"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
# Should show multiple integrations
|
||
|
|
assert "claude" in result.output
|
||
|
|
assert "gemini" in result.output
|
||
|
|
|
||
|
|
|
||
|
|
# ── install ──────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestIntegrationInstall:
|
||
|
|
def test_install_requires_speckit_project(self, tmp_path):
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(tmp_path)
|
||
|
|
result = runner.invoke(app, ["integration", "install", "claude"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "Not a spec-kit project" in result.output
|
||
|
|
|
||
|
|
def test_install_unknown_integration(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path)
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "install", "nonexistent"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "Unknown integration" in result.output
|
||
|
|
|
||
|
|
def test_install_already_installed(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path, "copilot")
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "install", "copilot"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "already installed" in result.output
|
||
|
|
assert "uninstall" in result.output
|
||
|
|
|
||
|
|
def test_install_different_when_one_exists(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path, "copilot")
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "install", "claude"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "already installed" in result.output
|
||
|
|
assert "uninstall" in result.output
|
||
|
|
|
||
|
|
def test_install_into_bare_project(self, tmp_path):
|
||
|
|
"""Install into a project with .specify/ but no integration."""
|
||
|
|
project = tmp_path / "bare"
|
||
|
|
project.mkdir()
|
||
|
|
(project / ".specify").mkdir()
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"integration", "install", "claude",
|
||
|
|
"--script", "sh",
|
||
|
|
], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0, result.output
|
||
|
|
assert "installed successfully" in result.output
|
||
|
|
|
||
|
|
# integration.json written
|
||
|
|
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
|
||
|
|
assert data["integration"] == "claude"
|
||
|
|
|
||
|
|
# Manifest created
|
||
|
|
assert (project / ".specify" / "integrations" / "claude.manifest.json").exists()
|
||
|
|
|
||
|
|
# Claude uses skills directory (not commands)
|
||
|
|
assert (project / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||
|
|
|
||
|
|
def test_install_bare_project_gets_shared_infra(self, tmp_path):
|
||
|
|
"""Installing into a bare project should create shared scripts and templates."""
|
||
|
|
project = tmp_path / "bare"
|
||
|
|
project.mkdir()
|
||
|
|
(project / ".specify").mkdir()
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"integration", "install", "claude",
|
||
|
|
"--script", "sh",
|
||
|
|
], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0, result.output
|
||
|
|
|
||
|
|
# Shared infrastructure should be present
|
||
|
|
assert (project / ".specify" / "scripts").is_dir()
|
||
|
|
assert (project / ".specify" / "templates").is_dir()
|
||
|
|
|
||
|
|
|
||
|
|
# ── uninstall ────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestIntegrationUninstall:
|
||
|
|
def test_uninstall_requires_speckit_project(self, tmp_path):
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(tmp_path)
|
||
|
|
result = runner.invoke(app, ["integration", "uninstall"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "Not a spec-kit project" in result.output
|
||
|
|
|
||
|
|
def test_uninstall_no_integration(self, tmp_path):
|
||
|
|
project = tmp_path / "proj"
|
||
|
|
project.mkdir()
|
||
|
|
(project / ".specify").mkdir()
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "uninstall"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "No integration" in result.output
|
||
|
|
|
||
|
|
def test_uninstall_removes_files(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path, "claude")
|
||
|
|
# Claude uses skills directory
|
||
|
|
assert (project / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||
|
|
assert (project / ".specify" / "integrations" / "claude.manifest.json").exists()
|
||
|
|
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "uninstall"], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "uninstalled" in result.output
|
||
|
|
|
||
|
|
# Command files removed
|
||
|
|
assert not (project / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||
|
|
|
||
|
|
# Manifest removed
|
||
|
|
assert not (project / ".specify" / "integrations" / "claude.manifest.json").exists()
|
||
|
|
|
||
|
|
# integration.json removed
|
||
|
|
assert not (project / ".specify" / "integration.json").exists()
|
||
|
|
|
||
|
|
def test_uninstall_preserves_modified_files(self, tmp_path):
|
||
|
|
"""Full lifecycle: install → modify → uninstall → modified file kept."""
|
||
|
|
project = _init_project(tmp_path, "claude")
|
||
|
|
plan_file = project / ".claude" / "skills" / "speckit-plan" / "SKILL.md"
|
||
|
|
assert plan_file.exists()
|
||
|
|
|
||
|
|
# Modify a file
|
||
|
|
plan_file.write_text("# My custom plan command\n", encoding="utf-8")
|
||
|
|
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "uninstall"], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "preserved" in result.output
|
||
|
|
|
||
|
|
# Modified file kept
|
||
|
|
assert plan_file.exists()
|
||
|
|
assert plan_file.read_text(encoding="utf-8") == "# My custom plan command\n"
|
||
|
|
|
||
|
|
def test_uninstall_wrong_key(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path, "copilot")
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "uninstall", "claude"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "not the currently installed" in result.output
|
||
|
|
|
||
|
|
def test_uninstall_preserves_shared_infra(self, tmp_path):
|
||
|
|
"""Shared scripts and templates are not removed by integration uninstall."""
|
||
|
|
project = _init_project(tmp_path, "claude")
|
||
|
|
shared_script = project / ".specify" / "scripts" / "bash" / "common.sh"
|
||
|
|
assert shared_script.exists()
|
||
|
|
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "uninstall"], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# Shared infrastructure preserved
|
||
|
|
assert shared_script.exists()
|
||
|
|
assert (project / ".specify" / "templates").is_dir()
|
||
|
|
|
||
|
|
|
||
|
|
# ── switch ───────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestIntegrationSwitch:
|
||
|
|
def test_switch_requires_speckit_project(self, tmp_path):
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(tmp_path)
|
||
|
|
result = runner.invoke(app, ["integration", "switch", "claude"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "Not a spec-kit project" in result.output
|
||
|
|
|
||
|
|
def test_switch_unknown_target(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path)
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "switch", "nonexistent"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "Unknown integration" in result.output
|
||
|
|
|
||
|
|
def test_switch_same_noop(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path, "copilot")
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "switch", "copilot"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "already installed" in result.output
|
||
|
|
|
||
|
|
def test_switch_between_integrations(self, tmp_path):
|
||
|
|
project = _init_project(tmp_path, "claude")
|
||
|
|
# Verify claude files exist (claude uses skills)
|
||
|
|
assert (project / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||
|
|
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"integration", "switch", "copilot",
|
||
|
|
"--script", "sh",
|
||
|
|
], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0, result.output
|
||
|
|
assert "Switched to" in result.output
|
||
|
|
|
||
|
|
# Old claude files removed
|
||
|
|
assert not (project / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||
|
|
|
||
|
|
# New copilot files created
|
||
|
|
assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists()
|
||
|
|
|
||
|
|
# integration.json updated
|
||
|
|
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
|
||
|
|
assert data["integration"] == "copilot"
|
||
|
|
|
||
|
|
def test_switch_preserves_shared_infra(self, tmp_path):
|
||
|
|
"""Switching preserves shared scripts, templates, and memory."""
|
||
|
|
project = _init_project(tmp_path, "claude")
|
||
|
|
shared_script = project / ".specify" / "scripts" / "bash" / "common.sh"
|
||
|
|
assert shared_script.exists()
|
||
|
|
shared_content = shared_script.read_text(encoding="utf-8")
|
||
|
|
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"integration", "switch", "copilot",
|
||
|
|
"--script", "sh",
|
||
|
|
], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# Shared infra untouched
|
||
|
|
assert shared_script.exists()
|
||
|
|
assert shared_script.read_text(encoding="utf-8") == shared_content
|
||
|
|
|
||
|
|
def test_switch_from_nothing(self, tmp_path):
|
||
|
|
"""Switch when no integration is installed should just install the target."""
|
||
|
|
project = tmp_path / "bare"
|
||
|
|
project.mkdir()
|
||
|
|
(project / ".specify").mkdir()
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"integration", "switch", "claude",
|
||
|
|
"--script", "sh",
|
||
|
|
], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "Switched to" in result.output
|
||
|
|
|
||
|
|
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
|
||
|
|
assert data["integration"] == "claude"
|
||
|
|
|
||
|
|
|
||
|
|
# ── Full lifecycle ───────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestIntegrationLifecycle:
|
||
|
|
def test_install_modify_uninstall_preserves_modified(self, tmp_path):
|
||
|
|
"""Full lifecycle: install → modify file → uninstall → verify modified file kept."""
|
||
|
|
project = tmp_path / "lifecycle"
|
||
|
|
project.mkdir()
|
||
|
|
(project / ".specify").mkdir()
|
||
|
|
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
|
||
|
|
# Install
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"integration", "install", "claude",
|
||
|
|
"--script", "sh",
|
||
|
|
], catch_exceptions=False)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "installed successfully" in result.output
|
||
|
|
|
||
|
|
# Claude uses skills directory
|
||
|
|
plan_file = project / ".claude" / "skills" / "speckit-plan" / "SKILL.md"
|
||
|
|
assert plan_file.exists()
|
||
|
|
|
||
|
|
# Modify one file
|
||
|
|
plan_file.write_text("# user customization\n", encoding="utf-8")
|
||
|
|
|
||
|
|
# Uninstall
|
||
|
|
result = runner.invoke(app, ["integration", "uninstall"], catch_exceptions=False)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "preserved" in result.output
|
||
|
|
|
||
|
|
# Modified file kept
|
||
|
|
assert plan_file.exists()
|
||
|
|
assert plan_file.read_text(encoding="utf-8") == "# user customization\n"
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
|
||
|
|
|
||
|
|
# ── Edge-case fixes ─────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestScriptTypeValidation:
|
||
|
|
def test_invalid_script_type_rejected(self, tmp_path):
|
||
|
|
"""--script with an invalid value should fail with a clear error."""
|
||
|
|
project = tmp_path / "proj"
|
||
|
|
project.mkdir()
|
||
|
|
(project / ".specify").mkdir()
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"integration", "install", "claude",
|
||
|
|
"--script", "bash",
|
||
|
|
])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "Invalid script type" in result.output
|
||
|
|
|
||
|
|
def test_valid_script_types_accepted(self, tmp_path):
|
||
|
|
"""Both 'sh' and 'ps' should be accepted."""
|
||
|
|
project = tmp_path / "proj"
|
||
|
|
project.mkdir()
|
||
|
|
(project / ".specify").mkdir()
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"integration", "install", "claude",
|
||
|
|
"--script", "sh",
|
||
|
|
], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
|
||
|
|
class TestParseIntegrationOptionsEqualsForm:
|
||
|
|
def test_equals_form_parsed(self):
|
||
|
|
"""--commands-dir=./x should be parsed the same as --commands-dir ./x."""
|
||
|
|
from specify_cli import _parse_integration_options
|
||
|
|
from specify_cli.integrations import get_integration
|
||
|
|
|
||
|
|
integration = get_integration("generic")
|
||
|
|
assert integration is not None
|
||
|
|
|
||
|
|
result_space = _parse_integration_options(integration, "--commands-dir ./mydir")
|
||
|
|
result_equals = _parse_integration_options(integration, "--commands-dir=./mydir")
|
||
|
|
assert result_space is not None
|
||
|
|
assert result_equals is not None
|
||
|
|
assert result_space["commands_dir"] == "./mydir"
|
||
|
|
assert result_equals["commands_dir"] == "./mydir"
|
||
|
|
|
||
|
|
|
||
|
|
class TestUninstallNoManifestClearsInitOptions:
|
||
|
|
def test_init_options_cleared_on_no_manifest_uninstall(self, tmp_path):
|
||
|
|
"""When no manifest exists, uninstall should still clear init-options.json."""
|
||
|
|
project = tmp_path / "proj"
|
||
|
|
project.mkdir()
|
||
|
|
(project / ".specify").mkdir()
|
||
|
|
|
||
|
|
# Write integration.json and init-options.json without a manifest
|
||
|
|
int_json = project / ".specify" / "integration.json"
|
||
|
|
int_json.write_text(json.dumps({"integration": "claude"}), encoding="utf-8")
|
||
|
|
|
||
|
|
opts_json = project / ".specify" / "init-options.json"
|
||
|
|
opts_json.write_text(json.dumps({
|
||
|
|
"integration": "claude",
|
||
|
|
"ai": "claude",
|
||
|
|
"ai_skills": True,
|
||
|
|
"script": "sh",
|
||
|
|
}), encoding="utf-8")
|
||
|
|
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
result = runner.invoke(app, ["integration", "uninstall", "claude"])
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# init-options.json should have integration keys cleared
|
||
|
|
opts = json.loads(opts_json.read_text(encoding="utf-8"))
|
||
|
|
assert "integration" not in opts
|
||
|
|
assert "ai" not in opts
|
||
|
|
assert "ai_skills" not in opts
|
||
|
|
# Non-integration keys preserved
|
||
|
|
assert opts.get("script") == "sh"
|
||
|
|
|
||
|
|
|
||
|
|
class TestSwitchClearsMetadataAfterTeardown:
|
||
|
|
def test_metadata_cleared_between_phases(self, tmp_path):
|
||
|
|
"""After a successful switch, metadata should reference the new integration."""
|
||
|
|
project = _init_project(tmp_path, "claude")
|
||
|
|
|
||
|
|
# Verify initial state
|
||
|
|
int_json = project / ".specify" / "integration.json"
|
||
|
|
assert json.loads(int_json.read_text(encoding="utf-8"))["integration"] == "claude"
|
||
|
|
|
||
|
|
old_cwd = os.getcwd()
|
||
|
|
try:
|
||
|
|
os.chdir(project)
|
||
|
|
# Switch to copilot — should succeed and update metadata
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"integration", "switch", "copilot",
|
||
|
|
"--script", "sh",
|
||
|
|
], catch_exceptions=False)
|
||
|
|
finally:
|
||
|
|
os.chdir(old_cwd)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# integration.json should reference copilot, not claude
|
||
|
|
data = json.loads(int_json.read_text(encoding="utf-8"))
|
||
|
|
assert data["integration"] == "copilot"
|
||
|
|
|
||
|
|
# init-options.json should reference copilot
|
||
|
|
opts_json = project / ".specify" / "init-options.json"
|
||
|
|
opts = json.loads(opts_json.read_text(encoding="utf-8"))
|
||
|
|
assert opts.get("ai") == "copilot"
|