Skip to content

Commit a45e9f0

Browse files
fsilvaortizclaude
andcommitted
test: add tests for create-new-feature.sh JSON output and fetch silencing
Adds 5 tests covering: - Valid JSON output from --json flag - FEATURE_NUM is always a zero-padded numeric string - No git fetch stdout leaks into JSON output - Repeated runs produce clean JSON without fetch artifacts - Regression guard: git fetch redirects both stdout and stderr Tests that require git are marked with skipif so they degrade gracefully in environments without git installed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b6c6493 commit a45e9f0

File tree

1 file changed

+134
-0
lines changed

1 file changed

+134
-0
lines changed

tests/test_create_new_feature.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""
2+
Tests for scripts/bash/create-new-feature.sh.
3+
4+
Tests cover:
5+
- JSON output validity when git fetch produces stdout noise (#1592)
6+
- Correct stdout/stderr suppression in check_existing_branches()
7+
"""
8+
9+
import json
10+
import shutil
11+
import subprocess
12+
from pathlib import Path
13+
14+
import pytest
15+
16+
SCRIPT_PATH = Path(__file__).resolve().parent.parent / "scripts" / "bash" / "create-new-feature.sh"
17+
18+
requires_git = pytest.mark.skipif(
19+
shutil.which("git") is None,
20+
reason="git is not installed",
21+
)
22+
23+
24+
@pytest.fixture
25+
def git_repo(tmp_path):
26+
"""Create a temporary git repo with a fake remote that produces stdout on fetch."""
27+
repo = tmp_path / "repo"
28+
repo.mkdir()
29+
30+
# Initialize git repo
31+
subprocess.run(["git", "init", str(repo)], capture_output=True, check=True)
32+
subprocess.run(["git", "-C", str(repo), "config", "user.email", "test@test.com"], capture_output=True, check=True)
33+
subprocess.run(["git", "-C", str(repo), "config", "user.name", "Test"], capture_output=True, check=True)
34+
35+
# Create an initial commit so HEAD exists
36+
(repo / "README.md").write_text("# Test")
37+
subprocess.run(["git", "-C", str(repo), "add", "."], capture_output=True, check=True)
38+
subprocess.run(["git", "-C", str(repo), "commit", "-m", "init"], capture_output=True, check=True)
39+
40+
# Create .specify dir to simulate an initialized project
41+
(repo / ".specify").mkdir()
42+
(repo / ".specify" / "templates").mkdir()
43+
44+
yield repo
45+
shutil.rmtree(tmp_path)
46+
47+
48+
@requires_git
49+
class TestCreateNewFeatureJsonOutput:
50+
"""Test that --json output is always valid JSON (#1592)."""
51+
52+
def test_json_output_is_valid(self, git_repo):
53+
"""Script should produce valid JSON with BRANCH_NAME, SPEC_FILE, FEATURE_NUM."""
54+
result = subprocess.run(
55+
["bash", str(SCRIPT_PATH), "--json", "Test feature description"],
56+
capture_output=True,
57+
text=True,
58+
cwd=str(git_repo),
59+
)
60+
61+
assert result.returncode == 0, f"Script failed: {result.stderr}"
62+
63+
output = result.stdout.strip()
64+
parsed = json.loads(output)
65+
66+
assert "BRANCH_NAME" in parsed
67+
assert "SPEC_FILE" in parsed
68+
assert "FEATURE_NUM" in parsed
69+
70+
def test_feature_num_is_numeric(self, git_repo):
71+
"""FEATURE_NUM should be a zero-padded numeric string."""
72+
result = subprocess.run(
73+
["bash", str(SCRIPT_PATH), "--json", "Another feature"],
74+
capture_output=True,
75+
text=True,
76+
cwd=str(git_repo),
77+
)
78+
79+
assert result.returncode == 0, f"Script failed: {result.stderr}"
80+
81+
parsed = json.loads(result.stdout.strip())
82+
feature_num = parsed["FEATURE_NUM"]
83+
84+
assert feature_num.isdigit(), f"FEATURE_NUM is not numeric: {feature_num}"
85+
assert len(feature_num) == 3, f"FEATURE_NUM is not zero-padded: {feature_num}"
86+
87+
88+
@requires_git
89+
class TestGitFetchSilencing:
90+
"""Verify that git fetch stdout does not contaminate branch number detection."""
91+
92+
def test_script_does_not_leak_git_fetch_stdout(self, git_repo):
93+
"""JSON output should contain only the JSON line, no git fetch noise."""
94+
result = subprocess.run(
95+
["bash", str(SCRIPT_PATH), "--json", "Verify no fetch noise"],
96+
capture_output=True,
97+
text=True,
98+
cwd=str(git_repo),
99+
)
100+
101+
assert result.returncode == 0, f"Script failed: {result.stderr}"
102+
103+
stdout_lines = result.stdout.strip().splitlines()
104+
# In JSON mode, stdout should contain exactly one line: the JSON object
105+
json_lines = [line for line in stdout_lines if line.startswith("{")]
106+
assert len(json_lines) == 1, f"Expected 1 JSON line, got {len(json_lines)}: {stdout_lines}"
107+
108+
def test_script_does_not_leak_git_fetch_to_json(self, git_repo):
109+
"""Repeated runs should always produce clean JSON without fetch artifacts."""
110+
for i in range(2):
111+
result = subprocess.run(
112+
["bash", str(SCRIPT_PATH), "--json", f"Run {i} verify clean"],
113+
capture_output=True,
114+
text=True,
115+
cwd=str(git_repo),
116+
)
117+
assert result.returncode == 0, f"Run {i} failed: {result.stderr}"
118+
parsed = json.loads(result.stdout.strip())
119+
assert parsed["FEATURE_NUM"].isdigit()
120+
121+
122+
class TestScriptRedirectPattern:
123+
"""Static analysis: verify the script uses correct redirect patterns."""
124+
125+
def test_git_fetch_redirect_pattern_in_script(self):
126+
"""The git fetch call should redirect both stdout and stderr to /dev/null."""
127+
script_content = SCRIPT_PATH.read_text()
128+
129+
assert "git fetch --all --prune >/dev/null 2>&1" in script_content, (
130+
"git fetch should redirect both stdout and stderr: >/dev/null 2>&1"
131+
)
132+
assert "git fetch --all --prune 2>/dev/null" not in script_content, (
133+
"git fetch should NOT redirect only stderr (old pattern)"
134+
)

0 commit comments

Comments
 (0)