Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/extensions/git/test_git_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ def test_no_git_graceful_degradation(self, tmp_path: Path):
)
assert result.returncode == 0, result.stderr
# pwsh may prefix warnings to stdout; find the JSON line
json_line = [l for l in result.stdout.splitlines() if l.strip().startswith("{")]
json_line = [ln for ln in result.stdout.splitlines() if ln.strip().startswith("{")]
assert json_line, f"No JSON in output: {result.stdout}"
data = json.loads(json_line[-1])
assert "BRANCH_NAME" in data
Expand Down
2 changes: 1 addition & 1 deletion tests/integrations/test_integration_agy.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,5 @@ def test_hook_note_preserves_indentation(self):
)
result = AgyIntegration._inject_hook_command_note(content)
lines = result.splitlines()
note_line = [l for l in lines if "replace dots" in l][0]
note_line = [ln for ln in lines if "replace dots" in ln][0]
assert note_line.startswith(" "), "Note should preserve indentation"
6 changes: 3 additions & 3 deletions tests/integrations/test_integration_base_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,10 @@ def _expected_files(self, script_variant: str) -> list[str]:
files.append(f"{cmd_dir}/speckit.{stem}.md")

# Framework files
files.append(f".specify/integration.json")
files.append(f".specify/init-options.json")
files.append(".specify/integration.json")
files.append(".specify/init-options.json")
files.append(f".specify/integrations/{self.KEY}.manifest.json")
files.append(f".specify/integrations/speckit.manifest.json")
files.append(".specify/integrations/speckit.manifest.json")

if script_variant == "sh":
for name in ["check-prerequisites.sh", "common.sh", "create-new-feature.sh",
Expand Down
4 changes: 2 additions & 2 deletions tests/integrations/test_integration_base_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_yaml_is_valid(self, tmp_path):
content = f.read_text(encoding="utf-8")
# Strip trailing source comment before parsing
lines = content.split("\n")
yaml_lines = [l for l in lines if not l.startswith("# Source:")]
yaml_lines = [ln for ln in lines if not ln.startswith("# Source:")]
try:
parsed = yaml.safe_load("\n".join(yaml_lines))
except Exception as exc:
Expand Down Expand Up @@ -183,7 +183,7 @@ def test_yaml_prompt_excludes_frontmatter(self, tmp_path, monkeypatch):
content = cmd_files[0].read_text(encoding="utf-8")
# Strip source comment for parsing
lines = content.split("\n")
yaml_lines = [l for l in lines if not l.startswith("# Source:")]
yaml_lines = [ln for ln in lines if not ln.startswith("# Source:")]
parsed = yaml.safe_load("\n".join(yaml_lines))

assert "description:" not in parsed["prompt"]
Expand Down
1 change: 0 additions & 1 deletion tests/integrations/test_integration_subcommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import json
import os

import pytest
from typer.testing import CliRunner

from specify_cli import app
Expand Down
33 changes: 23 additions & 10 deletions tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,9 @@ def test_open_url_attaches_auth_for_matching_host(self, monkeypatch):
mock_opener = MagicMock()
def fake_open(req, timeout=None):
captured["req"] = req
resp = MagicMock(); resp.__enter__ = lambda s: s; resp.__exit__ = MagicMock(return_value=False)
resp = MagicMock()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
mock_opener.open.side_effect = fake_open
with patch("specify_cli.authentication.http.urllib.request.build_opener", return_value=mock_opener):
Expand All @@ -588,7 +590,9 @@ def test_open_url_no_auth_for_non_matching_host(self, monkeypatch):
captured = {}
def fake_urlopen(req, timeout=None):
captured["req"] = req
resp = MagicMock(); resp.__enter__ = lambda s: s; resp.__exit__ = MagicMock(return_value=False)
resp = MagicMock()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
with patch("specify_cli.authentication.http.urllib.request.urlopen", side_effect=fake_urlopen):
open_url("https://example.com/file.json")
Expand All @@ -601,7 +605,9 @@ def test_open_url_no_auth_when_no_config(self, monkeypatch):
captured = {}
def fake_urlopen(req, timeout=None):
captured["req"] = req
resp = MagicMock(); resp.__enter__ = lambda s: s; resp.__exit__ = MagicMock(return_value=False)
resp = MagicMock()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
with patch("specify_cli.authentication.http.urllib.request.urlopen", side_effect=fake_urlopen):
open_url("https://github.com/org/repo")
Expand All @@ -615,12 +621,16 @@ def test_open_url_falls_through_on_401(self, monkeypatch):
self._set_config(monkeypatch, [_github_entry()])
call_count = 0
def fake_side_effect(req, timeout=None):
nonlocal call_count; call_count += 1
nonlocal call_count
call_count += 1
if call_count == 1:
raise urllib.error.HTTPError("url", 401, "Unauthorized", {}, None)
resp = MagicMock(); resp.__enter__ = lambda s: s; resp.__exit__ = MagicMock(return_value=False)
resp = MagicMock()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
mock_opener = MagicMock(); mock_opener.open.side_effect = fake_side_effect
mock_opener = MagicMock()
mock_opener.open.side_effect = fake_side_effect
with patch("specify_cli.authentication.http.urllib.request.build_opener", return_value=mock_opener), \
patch("specify_cli.authentication.http.urllib.request.urlopen", side_effect=fake_side_effect):
open_url("https://github.com/org/repo")
Expand Down Expand Up @@ -692,7 +702,6 @@ def test_config_cached_after_first_load(self, monkeypatch):
"""_load_config() should call load_auth_config only once per process."""
from unittest.mock import patch
from specify_cli.authentication import http as _mod
from specify_cli.authentication.config import AuthConfigEntry
# Allow the real load path (no override)
monkeypatch.setattr(_mod, "_config_override", None)
monkeypatch.setattr(_mod, "_config_cache", None)
Expand Down Expand Up @@ -825,8 +834,11 @@ def _capture_request(self):
def side_effect(req, timeout=None):
captured["request"] = req
body = _json.dumps({"tag_name": "v9.9.9"}).encode()
resp = MagicMock(); resp.read.return_value = body
cm = MagicMock(); cm.__enter__.return_value = resp; cm.__exit__.return_value = False
resp = MagicMock()
resp.read.return_value = body
cm = MagicMock()
cm.__enter__.return_value = resp
cm.__exit__.return_value = False
return cm
return captured, side_effect

Expand All @@ -836,7 +848,8 @@ def test_gh_token_forwarded_when_configured(self, monkeypatch):
monkeypatch.setenv("GH_TOKEN", "forwarded-sentinel")
self._set_config(monkeypatch, [_github_entry()])
captured, side_effect = self._capture_request()
mock_opener = MagicMock(); mock_opener.open.side_effect = side_effect
mock_opener = MagicMock()
mock_opener.open.side_effect = side_effect
with patch("specify_cli.authentication.http.urllib.request.build_opener", return_value=mock_opener):
_fetch_latest_release_tag()
assert captured["request"].get_header("Authorization") == "Bearer forwarded-sentinel"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_commands_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_agent_config_importable():


def test_agent_config_re_exported_from_init():
from specify_cli import AGENT_CONFIG, AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP, SCRIPT_TYPE_CHOICES
from specify_cli import AGENT_CONFIG, SCRIPT_TYPE_CHOICES
assert isinstance(AGENT_CONFIG, dict)
assert "sh" in SCRIPT_TYPE_CHOICES

Expand Down
5 changes: 0 additions & 5 deletions tests/test_console_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@
from specify_cli import (
console,
StepTracker,
get_key,
select_with_arrows,
BannerGroup,
show_banner,
BANNER,
TAGLINE,
)


Expand Down
13 changes: 6 additions & 7 deletions tests/test_extension_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from specify_cli.extensions import (
ExtensionManifest,
ExtensionManager,
ExtensionError,
)


Expand Down Expand Up @@ -241,7 +240,7 @@ def test_skills_created_when_ai_skills_active(self, skills_project, extension_di
"""Skills should be created when ai_skills is enabled."""
project_dir, skills_dir = skills_project
manager = ExtensionManager(project_dir)
manifest = manager.install_from_directory(
manager.install_from_directory(
extension_dir, "0.1.0", register_commands=False
)

Expand Down Expand Up @@ -784,7 +783,7 @@ def test_command_without_frontmatter(self, skills_project, temp_dir):
)

manager = ExtensionManager(project_dir)
manifest = manager.install_from_directory(
manager.install_from_directory(
ext_dir, "0.1.0", register_commands=False
)

Expand All @@ -803,7 +802,7 @@ def test_gemini_agent_skills(self, project_dir, temp_dir):
ext_dir = _create_extension_dir(temp_dir, ext_id="test-ext")

manager = ExtensionManager(project_dir)
manifest = manager.install_from_directory(
manager.install_from_directory(
ext_dir, "0.1.0", register_commands=False
)

Expand All @@ -819,10 +818,10 @@ def test_multiple_extensions_independent_skills(self, skills_project, temp_dir):
ext_dir_b = _create_extension_dir(temp_dir, ext_id="ext-b")

manager = ExtensionManager(project_dir)
manifest_a = manager.install_from_directory(
manager.install_from_directory(
ext_dir_a, "0.1.0", register_commands=False
)
manifest_b = manager.install_from_directory(
manager.install_from_directory(
ext_dir_b, "0.1.0", register_commands=False
)

Expand Down Expand Up @@ -880,7 +879,7 @@ def test_malformed_frontmatter_handled(self, skills_project, temp_dir):

manager = ExtensionManager(project_dir)
# Should not raise
manifest = manager.install_from_directory(
manager.install_from_directory(
ext_dir, "0.1.0", register_commands=False
)

Expand Down
18 changes: 9 additions & 9 deletions tests/test_setup_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def test_setup_tasks_bash_core_template_resolved(tasks_repo: Path) -> None:
setup-tasks.sh --json should exit 0 and return an absolute, existing
TASKS_TEMPLATE path pointing to the core template.
"""
feat = _minimal_feature(tasks_repo)
_minimal_feature(tasks_repo)
script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh"

result = subprocess.run(
Expand All @@ -150,7 +150,7 @@ def test_setup_tasks_bash_override_wins(tasks_repo: Path) -> None:
When an override exists at .specify/templates/overrides/tasks-template.md,
setup-tasks.sh --json must return the override path, not the core path.
"""
feat = _minimal_feature(tasks_repo)
_minimal_feature(tasks_repo)

# Create the override
overrides_dir = tasks_repo / ".specify" / "templates" / "overrides"
Expand Down Expand Up @@ -187,7 +187,7 @@ def test_setup_tasks_bash_extension_wins_over_core(tasks_repo: Path) -> None:
When an extension template exists, setup-tasks.sh --json must resolve
tasks-template.md from the extension before falling back to the core path.
"""
feat = _minimal_feature(tasks_repo)
_minimal_feature(tasks_repo)

# FIX: real extension layout is .specify/extensions/<id>/templates/<name>.md
extension_dir = (
Expand Down Expand Up @@ -225,7 +225,7 @@ def test_setup_tasks_bash_preset_wins_over_extension(tasks_repo: Path) -> None:
When both preset and extension templates exist, setup-tasks.sh --json must
resolve the preset path because presets outrank extensions.
"""
feat = _minimal_feature(tasks_repo)
_minimal_feature(tasks_repo)

# FIX: real extension layout is .specify/extensions/<id>/templates/<name>.md
extension_dir = (
Expand Down Expand Up @@ -269,7 +269,7 @@ def test_setup_tasks_bash_preset_priority_order(tasks_repo: Path) -> None:
When two presets both provide tasks-template.md, the one listed first in
.specify/presets/.registry wins.
"""
feat = _minimal_feature(tasks_repo)
_minimal_feature(tasks_repo)

# resolve_template reads .specify/presets/.registry as a JSON object with a
# "presets" map where each entry has a numeric "priority" (lower = higher
Expand Down Expand Up @@ -329,7 +329,7 @@ def test_setup_tasks_bash_missing_template_errors(tasks_repo: Path) -> None:
When tasks-template.md is absent from all locations, setup-tasks.sh must
exit non-zero and print a helpful ERROR message to stderr.
"""
feat = _minimal_feature(tasks_repo)
_minimal_feature(tasks_repo)

# Remove the core template so no template exists anywhere
core = tasks_repo / ".specify" / "templates" / "tasks-template.md"
Expand Down Expand Up @@ -429,7 +429,7 @@ def test_setup_tasks_ps_core_template_resolved(tasks_repo: Path) -> None:
setup-tasks.ps1 -Json should exit 0 and return an absolute, existing
TASKS_TEMPLATE path.
"""
feat = _minimal_feature(tasks_repo)
_minimal_feature(tasks_repo)
script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1"
exe = "pwsh" if HAS_PWSH else _POWERSHELL

Expand Down Expand Up @@ -457,7 +457,7 @@ def test_setup_tasks_ps_override_wins(tasks_repo: Path) -> None:
When an override exists at .specify/templates/overrides/tasks-template.md,
setup-tasks.ps1 -Json must return the override path, not the core path.
"""
feat = _minimal_feature(tasks_repo)
_minimal_feature(tasks_repo)

overrides_dir = tasks_repo / ".specify" / "templates" / "overrides"
overrides_dir.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -493,7 +493,7 @@ def test_setup_tasks_ps_missing_template_errors(tasks_repo: Path) -> None:
When tasks-template.md is absent from all locations, setup-tasks.ps1 must
exit non-zero and write a helpful error to stderr.
"""
feat = _minimal_feature(tasks_repo)
_minimal_feature(tasks_repo)

core = tasks_repo / ".specify" / "templates" / "tasks-template.md"
core.unlink()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_timestamp_branches.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ def test_dry_run_with_timestamp(self, git_repo: Path):
assert re.match(r"^\d{8}-\d{6}-ts-feat$", branch), f"unexpected: {branch}"
# Verify no side effects
branches = subprocess.run(
["git", "branch", "--list", f"*ts-feat*"],
["git", "branch", "--list", "*ts-feat*"],
cwd=git_repo,
capture_output=True,
text=True,
Expand Down
3 changes: 1 addition & 2 deletions tests/test_utils_assets_imports.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Regression guard: utility and asset symbols importable from specify_cli."""
from specify_cli import (
run_command, check_tool, is_git_repo, init_git_repo,
handle_vscode_settings, merge_json_files,
check_tool, is_git_repo, merge_json_files,
get_speckit_version,
CLAUDE_LOCAL_PATH, CLAUDE_NPM_LOCAL_PATH,
)
Expand Down
1 change: 0 additions & 1 deletion tests/test_version_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ def test_version_symbols_available_from_star_import():

def test_version_module_symbols_directly_importable():
from specify_cli._version import (
GITHUB_API_LATEST,
_fetch_latest_release_tag,
_get_installed_version,
_is_newer,
Expand Down
Loading