Skip to content

Commit 92ac6df

Browse files
CopilotFieldnote-Echoproject-navi-bot
authored
fix: replace <PACK> placeholders, harden path traversal checks, strengthen empty-list tests (#52)
* Initial plan * fix: replace <PACK> placeholders, add path traversal checks, and strengthen empty-list tests Agent-Logs-Url: https://github.com/Project-Navi/navi-bootstrap/sessions/604f9871-000e-4f48-b728-1c2a85fe6c4c Co-authored-by: Fieldnote-Echo <202828230+Fieldnote-Echo@users.noreply.github.com> * style: ruff-format the new-cmd validation if-chain (one-liner -> multi-line) The single-line if chain in new() (added in this PR) exceeds ruff-format's preferred wrapping. Auto-applying `uv run ruff format` to clear the `lint` (ruff format --check) and `quality-gate` failures on PR #52. No semantic change. 381 tests still pass, mypy strict clean, both ruff check and ruff format clean. * fix: address Codex P1 + Copilot findings on PR #52 1. [Codex P1 + Copilot] pack-validator.md: pack-specific pytest step was outside the per-pack `for PACK ... done` loop, so $PACK held only the LAST iteration's value and earlier packs' tests were silently skipped. A regression in earlier-changed packs would slip through the validator. Moved the test inside the same loop body and renumbered the cross-cutting tests as step 3. 2. [Copilot] cli.py render_cmd error message: previous wording ('without dot or dot-dot components') didn't match the actual checks (`startswith('.')` rejects '.foo'; `'..' in name` substring rejects 'foo..bar'). Reworded to match the rules: 'must not start with .', 'must not contain .. or path separators'. Skipped from the same review batch: - Copilot's note that PR description claims apply/diff hardening: only render and new construct Path-from-name; apply/diff take --target as a Click Path. PR description should be updated but code is correct. 381 tests still pass, mypy strict clean, ruff + format clean. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Fieldnote-Echo <202828230+Fieldnote-Echo@users.noreply.github.com> Co-authored-by: Nelson Spence <nelson@projectnavi.ai> Co-authored-by: Navi Bot <267427491+project-navi-bot@users.noreply.github.com>
1 parent 77a05f6 commit 92ac6df

5 files changed

Lines changed: 45 additions & 22 deletions

File tree

.claude/agents/pack-validator.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,27 @@ You are a pack-validation specialist for navi-bootstrap. Your job: confirm that
1414
git diff --name-only origin/main...HEAD | grep '^packs/' | cut -d'/' -f2 | sort -u
1515
```
1616

17-
2. For each changed pack, run the full validation chain:
17+
2. For each changed pack, run the full validation chain **including** its
18+
pack-specific tests inside the same loop body — otherwise `pack_snake`
19+
takes the value of the last iteration only and earlier packs' tests are
20+
silently skipped:
1821

1922
```bash
20-
uv run nboot validate --spec nboot-spec.json
21-
scratch=$(mktemp -d -t nboot-scratch-XXXX)
22-
uv run nboot new "$scratch"
23-
uv run nboot apply --spec nboot-spec.json --pack <PACK> --target "$scratch"
24-
uv run nboot diff --spec nboot-spec.json --pack <PACK> --target "$scratch"
23+
for PACK in $(git diff --name-only origin/main...HEAD | grep '^packs/' | cut -d'/' -f2 | sort -u); do
24+
uv run nboot validate --spec nboot-spec.json
25+
scratch=$(mktemp -d -t nboot-scratch-XXXX)
26+
uv run nboot new "$scratch"
27+
uv run nboot apply --spec nboot-spec.json --pack "$PACK" --target "$scratch"
28+
uv run nboot diff --spec nboot-spec.json --pack "$PACK" --target "$scratch"
29+
30+
# Pack-specific test for this iteration's pack
31+
pack_snake=$(echo "$PACK" | tr '-' '_')
32+
uv run pytest tests/test_${pack_snake}_pack.py -v || \
33+
uv run pytest tests/ -k "$pack_snake" -v
34+
done
2535
```
2636

27-
3. Run the pack-specific test:
28-
29-
```bash
30-
pack_snake=$(echo <PACK> | tr '-' '_')
31-
uv run pytest tests/test_${pack_snake}_pack.py -v 2>/dev/null || \
32-
uv run pytest tests/ -k "$pack_snake" -v
33-
```
34-
35-
4. Run cross-cutting tests that commonly break on pack changes:
37+
3. Run cross-cutting tests that commonly break on pack changes:
3638

3739
```bash
3840
uv run pytest tests/test_engine.py tests/test_manifest.py tests/test_integration.py -v

src/navi_bootstrap/cli.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,21 @@ def render_cmd(
119119

120120
if out is None:
121121
name = spec_data["name"]
122-
if not name or "/" in name or "\\" in name:
122+
stripped_name = name.strip() if isinstance(name, str) else ""
123+
if (
124+
not stripped_name
125+
or "/" in stripped_name
126+
or "\\" in stripped_name
127+
or ".." in stripped_name
128+
or stripped_name.startswith(".")
129+
):
123130
raise click.ClickException(
124131
f"Unsafe spec name {name!r} cannot be used as output directory. "
132+
"Name must be a non-empty single path segment, must not start with "
133+
"'.', and must not contain '..' or path separators. "
125134
"Use --out to specify an explicit output path."
126135
)
127-
output_dir = Path(name)
136+
output_dir = Path(stripped_name)
128137
else:
129138
output_dir = out
130139

@@ -577,12 +586,19 @@ def new(
577586
) -> None:
578587
"""Create a new Python project with operational infrastructure."""
579588
# Validate name before using as path
580-
if not name or "/" in name or "\\" in name or name.startswith("."):
589+
stripped_name = name.strip() if name else ""
590+
if (
591+
not stripped_name
592+
or "/" in stripped_name
593+
or "\\" in stripped_name
594+
or ".." in stripped_name
595+
or stripped_name.startswith(".")
596+
):
581597
raise click.ClickException(
582598
f"Unsafe project name {name!r}. "
583-
"Names must not contain path separators or start with a dot."
599+
"Names must not contain path separators, '..', or start with a dot."
584600
)
585-
output_dir = Path(name)
601+
output_dir = Path(stripped_name)
586602
if output_dir.exists():
587603
raise click.ClickException(
588604
f"Directory {name!r} already exists. nboot new is for greenfield projects only."

tests/test_hooks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ def test_reports_failures_without_stopping(self, mock_run: MagicMock, tmp_path:
2929
assert not results[0].success
3030
assert results[1].success
3131

32-
def test_empty_hooks(self, tmp_path: Path) -> None:
32+
@patch("navi_bootstrap.hooks.subprocess.run")
33+
def test_empty_hooks(self, mock_run: MagicMock, tmp_path: Path) -> None:
3334
results = run_hooks([], tmp_path)
3435
assert results == []
36+
mock_run.assert_not_called()
3537

3638
@patch("navi_bootstrap.hooks.subprocess.run")
3739
def test_captures_output(self, mock_run: MagicMock, tmp_path: Path) -> None:

tests/test_init.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ def test_detects_async_test_functions(self, tmp_path: Path) -> None:
455455
test_file = tests / "test_example.py"
456456
test_file.write_text("def test_sync(): pass\nasync def test_async(): pass\n")
457457
result = detect_test_info(tmp_path)
458+
assert result["test_framework"] == "pytest"
458459
assert result["test_count"] == 2
459460

460461

tests/test_validate.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ def test_warnings_accepted(self, mock_run: MagicMock, tmp_path: Path) -> None:
4141
assert len(results) == 1
4242
assert results[0].passed
4343

44-
def test_empty_validations(self, tmp_path: Path) -> None:
44+
@patch("navi_bootstrap.validate.subprocess.run")
45+
def test_empty_validations(self, mock_run: MagicMock, tmp_path: Path) -> None:
4546
results = run_validations([], tmp_path)
4647
assert results == []
48+
mock_run.assert_not_called()
4749

4850
@patch("navi_bootstrap.validate.subprocess.run")
4951
def test_skips_method_based_validations(self, mock_run: MagicMock, tmp_path: Path) -> None:

0 commit comments

Comments
 (0)