Skip to content

Commit 2a93fd2

Browse files
committed
refactor(config): migrate to pyproject.toml, patch tests with mock
- Remove pytest-based integration tests requiring real `gh` authentication. - Add mocked tests for `test_issue_fetch_sh.py` and `test_resolve_preflight_sh.py` to simulate `gh` CLI behavior. - Ensure mocked tests preserve key behaviors like command passthrough, environment variable handling, and output validation. - Minor adjustment in `locate-scan-query.sh` fallback logic to avoid error when no matching files are found. - Update CI workflow to install `scipy` for compatibility or extended test dependencies. - Consolidated configuration by moving pytest options and ruff settings to `pyproject.toml`. - Removed `pytest.ini` and adjusted pre-commit hooks to reflect updated config. - Simplified GitHub Actions workflow by removing unnecessary `working-directory`.
1 parent c26f38b commit 2a93fd2

12 files changed

Lines changed: 103 additions & 38 deletions

File tree

.github/workflows/ci-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ jobs:
1111
tests:
1212
runs-on: ${{ matrix.os }}
1313
strategy:
14+
fail-fast: false
1415
matrix:
1516
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
1617
python-version: ["3.10", "3.12"]
@@ -24,10 +25,9 @@ jobs:
2425
python-version: ${{ matrix.python-version }}
2526

2627
- name: Install pytest
27-
run: pip install -q pytest
28+
run: pip install -q pytest pytest-cov scipy
2829

2930
- name: Run plugin's bin tests
30-
working-directory: plugins/
3131
env:
3232
RUN_INTEGRATION: "1"
33-
run: pytest .
33+
run: pytest

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,6 @@ site/
4848
__pycache__/
4949
*.py[cod]
5050
*$py.class
51+
52+
.coverage
53+
uv.lock

.pre-commit-config.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,9 @@ repos:
7070
hooks:
7171
- id: ruff
7272
name: 🦀 ruff
73-
args: ["--line-length=120", "--fix"]
73+
args: ["--fix"]
7474
- id: ruff-format
7575
name: 🦀 ruff-format
76-
args: ["--line-length=120"]
7776

7877
- repo: https://github.com/codespell-project/codespell
7978
rev: v2.4.2

plugins/codemap/bin/locate-scan-query.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ if command -v scan-query >/dev/null 2>&1; then
1717
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -x "${CLAUDE_PLUGIN_ROOT}/bin/scan-query" ]; then
1818
SQ="${CLAUDE_PLUGIN_ROOT}/bin/scan-query"
1919
else
20-
SQ=$(ls "$HOME/.claude/plugins/cache"/*/codemap/*/bin/scan-query 2>/dev/null | sort -V | tail -1)
20+
SQ=$(ls "$HOME/.claude/plugins/cache"/*/codemap/*/bin/scan-query 2>/dev/null | sort -V | tail -1 || true)
2121
fi
2222

2323
if [ -n "$SQ" ] && [ -x "$SQ" ]; then

plugins/develop/tests/test_issue_fetch_sh.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import subprocess
1212
from pathlib import Path
1313

14-
import pytest
1514

1615
SCRIPT = Path(__file__).parent.parent / "bin" / "issue-fetch.sh"
1716

@@ -89,11 +88,10 @@ def test_non_numeric_arg_rejected(tmp_path: Path):
8988
assert not (tmp_path / "gh-args.txt").exists()
9089

9190

92-
@pytest.mark.skipif(not os.getenv("RUN_INTEGRATION"), reason="requires real gh auth")
93-
def test_integration_fetches_real_issue():
94-
"""End-to-end: fetch a known public issue through real gh CLI."""
95-
# Use a low-numbered, stable issue on a well-known public repo.
96-
# Caller must set RUN_INTEGRATION=1 and have gh authenticated.
97-
result = sh("#1", cwd="/tmp")
98-
# gh returns 0 on success; output should be non-empty.
99-
assert result.returncode in (0, 1)
91+
def test_mocked_fetch_passes_through_output(tmp_path: Path):
92+
"""Mocked fetch: gh stdout is forwarded and exit 0 is preserved."""
93+
_make_fake_gh(tmp_path, exit_code=0, stdout_text="Issue title: Fix the bug")
94+
env = {"PATH": f"{tmp_path}/bin:/usr/bin:/bin"}
95+
result = sh("#1", env=env)
96+
assert result.returncode == 0
97+
assert "Fix the bug" in result.stdout

plugins/foundry/tests/test_c33_dir_resolution.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Old code: ls -d ~/.claude/plugins/cache/borda-ai-rig/ returns the root dir itself —
44
no version specificity, causing grep to scan all cached versions × all plugins.
55
6-
Fixed code: ls -td .../foundry/*/ resolves to latest version dir only.
6+
Fixed code: ls -d .../foundry/*/ | sort -V | tail -1 resolves to latest version dir only.
77
"""
88

99
import subprocess
@@ -20,10 +20,10 @@ def test_fixed_resolution_finds_latest_version(tmp_path: Path) -> None:
2020
v2 = tmp_path / "foundry" / "0.17.0"
2121
v1.mkdir(parents=True)
2222
v2.mkdir(parents=True)
23-
# Touch v2 to ensure ls -td sorts it first (newer mtime)
24-
v2.touch()
2523

26-
result = _bash(f'_C33_DIR=$(ls -td "{tmp_path}/foundry/"*/ 2>/dev/null | head -1); echo "${{_C33_DIR:-.claude/}}"')
24+
result = _bash(
25+
f'_C33_DIR=$(ls -d "{tmp_path}/foundry/"*/ 2>/dev/null | sort -V | tail -1); echo "${{_C33_DIR:-.claude/}}"'
26+
)
2727
assert result.returncode == 0
2828
resolved = result.stdout.strip()
2929
assert "0.17.0" in resolved, f"Expected version-specific path, got: {resolved}"
@@ -33,9 +33,8 @@ def test_fixed_resolution_excludes_older_versions(tmp_path: Path) -> None:
3333
"""Fixed resolution returns exactly one version, not all of them."""
3434
for ver in ["0.15.0", "0.16.0", "0.17.0"]:
3535
(tmp_path / "foundry" / ver).mkdir(parents=True)
36-
(tmp_path / "foundry" / "0.17.0").touch()
3736

38-
result = _bash(f'ls -td "{tmp_path}/foundry/"*/ 2>/dev/null | head -1')
37+
result = _bash(f'ls -d "{tmp_path}/foundry/"*/ 2>/dev/null | sort -V | tail -1')
3938
assert result.returncode == 0
4039
lines = [line for line in result.stdout.strip().splitlines() if line]
4140
assert len(lines) == 1, f"Should resolve to single version dir, got: {lines}"
@@ -60,6 +59,8 @@ def test_old_code_returns_root_not_version(tmp_path: Path) -> None:
6059

6160
def test_fallback_when_no_cache(tmp_path: Path) -> None:
6261
"""_C33_DIR falls back to .claude/ when cache absent."""
63-
result = _bash(f'_C33_DIR=$(ls -td "{tmp_path}/foundry/"*/ 2>/dev/null | head -1); echo "${{_C33_DIR:-.claude/}}"')
62+
result = _bash(
63+
f'_C33_DIR=$(ls -d "{tmp_path}/foundry/"*/ 2>/dev/null | sort -V | tail -1); echo "${{_C33_DIR:-.claude/}}"'
64+
)
6465
assert result.returncode == 0
6566
assert result.stdout.strip() == ".claude/"

plugins/oss/bin/commit_all_items.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
# $5 SUMMARIES_FILE — optional path to file with bullet-list item summaries
1212
# --codex — include OpenAI Codex co-author trailer (pass anywhere)
1313
set -euo pipefail
14+
# timeout is GNU coreutils — not available on macOS by default.
15+
command -v timeout >/dev/null 2>&1 || { timeout() { shift; "$@"; }; }
1416

1517
PR_NUMBER=""
1618
N_AS_SUGGESTED=0

plugins/oss/bin/setup_release_dir.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# CHANGELOG.md is excluded from the backup loop — it is a symlink; re-linking
1010
# on re-run is safe and intentional.
1111
set -euo pipefail
12+
# timeout is GNU coreutils — not available on macOS by default.
13+
command -v timeout >/dev/null 2>&1 || { timeout() { shift; "$@"; }; }
1214

1315
RELEASE_DIR="${1:?release_dir required}"
1416
CHANGELOG_FILE="${2:?changelog_file required}"

plugins/oss/bin/stage_item_changes.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# source-extension untracked files. Extracted from oss:resolve
55
# action-item-dispatch Phase 2 staging block (AI7).
66
set -euo pipefail
7+
# timeout is GNU coreutils — not available on macOS by default.
8+
command -v timeout >/dev/null 2>&1 || { timeout() { shift; "$@"; }; }
79

810
ITEM_ID="${1:?item_id required}"
911

plugins/oss/tests/test_resolve_preflight_sh.py

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,43 @@
99
from __future__ import annotations
1010

1111
import os
12+
import shutil
1213
import stat
1314
import subprocess
1415
from pathlib import Path
1516

16-
import pytest
17+
_SHADOW_TOOLS = [
18+
"bash",
19+
"sh",
20+
"date",
21+
"grep",
22+
"head",
23+
"xargs",
24+
"wc",
25+
"tr",
26+
"mkdir",
27+
"git",
28+
"find",
29+
"cat",
30+
"sort",
31+
"tail",
32+
]
33+
34+
35+
def _make_shadow_bin(tmp_path: Path, exclude: list[str] | None = None) -> Path:
36+
"""Create bin dir with symlinks to system tools, excluding named executables."""
37+
shadow = tmp_path / "shadow_bin"
38+
shadow.mkdir(exist_ok=True)
39+
for tool in _SHADOW_TOOLS:
40+
if exclude and tool in exclude:
41+
continue
42+
found = shutil.which(tool)
43+
if found:
44+
link = shadow / tool
45+
if not link.exists():
46+
link.symlink_to(found)
47+
return shadow
48+
1749

1850
SCRIPT = Path(__file__).parent.parent / "bin" / "resolve_preflight.sh"
1951

@@ -42,14 +74,12 @@ def _make_fake_bin(tmp_path: Path, name: str, script_content: str) -> Path:
4274

4375
def test_gh_not_found(tmp_path: Path):
4476
"""No gh on PATH → exit 1 with stderr "gh not found"."""
45-
# Provide only basic shell utilities — no gh, no claude.
46-
# Need at least bash, find, mkdir, date, cat, grep, head, xargs, git in PATH.
77+
# Shadow bin has real tools but no gh — prevents pre-installed gh on CI from being found.
78+
shadow = _make_shadow_bin(tmp_path, exclude=["gh"])
4779
env = {
4880
"HOME": str(tmp_path),
49-
"PATH": "/usr/bin:/bin", # standard tools but no gh
81+
"PATH": str(shadow),
5082
}
51-
# Run in tmp_path (not a git repo) — the script's git checks fail silently
52-
# via `|| true`, so gh-not-found is the first hard failure.
5383
result = sh(env=env, cwd=str(tmp_path))
5484
assert result.returncode == 1
5585
assert "gh not found" in result.stderr
@@ -76,10 +106,23 @@ def test_gh_present_but_unauthenticated(tmp_path: Path):
76106
assert "gh found but not authenticated" in result.stderr
77107

78108

79-
@pytest.mark.skipif(not os.getenv("RUN_INTEGRATION"), reason="requires real gh auth")
80-
def test_integration_happy_path():
81-
"""End-to-end: real gh auth + real git repo → exit 0, GH_OK=true emitted."""
82-
repo_root = Path(__file__).parents[3]
83-
result = sh(cwd=str(repo_root))
109+
def test_happy_path_mocked(tmp_path: Path):
110+
"""Mocked happy path: authenticated gh → exit 0, GH_OK=true on stdout."""
111+
_make_fake_bin(
112+
tmp_path,
113+
"gh",
114+
"#!/bin/sh\n"
115+
'if [ "$1" = "auth" ]; then\n'
116+
' echo "Logged in to github.com as testuser (oauth_token)" >&2\n'
117+
" exit 0\n"
118+
"fi\n"
119+
"exit 0\n",
120+
)
121+
_make_fake_bin(tmp_path, "claude", "#!/bin/sh\nexit 0\n")
122+
env = {
123+
"HOME": str(tmp_path),
124+
"PATH": f"{tmp_path / 'bin'}:/usr/bin:/bin",
125+
}
126+
result = sh(env=env, cwd=str(tmp_path))
84127
assert result.returncode == 0
85128
assert "GH_OK=true" in result.stdout

0 commit comments

Comments
 (0)