Skip to content
Open
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
19 changes: 14 additions & 5 deletions src/skillspector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,20 @@ def _scan_state(
return state


def _result_body(result: dict) -> str:
report_body = result.get("report_body") or ""
if not report_body and result.get("sarif_report") is not None:
report_body = json.dumps(result["sarif_report"], indent=2)
return report_body


def _write_result(
result: dict[str, object],
output: Path | None,
format: FormatChoice,
) -> None:
"""Write report_body to file or stdout. Uses sarif_report if report_body missing."""
report_body = result.get("report_body") or ""
if not report_body and result.get("sarif_report") is not None:
report_body = json.dumps(result["sarif_report"], indent=2)
report_body = _result_body(result)
if output:
Path(output).write_text(report_body, encoding="utf-8")
if format == FormatChoice.terminal:
Expand Down Expand Up @@ -432,9 +437,13 @@ def _scan_multi_skill(
Path(output).write_text(json.dumps(combined, indent=2), encoding="utf-8")
console.print(f"[green]Combined report saved to:[/green] {output}")
elif output:
for _skill, result in zip(skills, results, strict=True):
# concatenated non-JSON output: not merged SARIF
sections = []
for skill, result in zip(skills, results, strict=True):
if "error" not in result:
_write_result(result, None, format)
sections.append(f"--- {skill.relative_path} ---\n\n{_result_body(result)}")
Path(output).write_text("\n\n".join(sections), encoding="utf-8")
console.print(f"[green]Combined report saved to:[/green] {output}")

if max_score > 50:
raise typer.Exit(code=1)
Expand Down
78 changes: 77 additions & 1 deletion tests/unit/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@

import json
from pathlib import Path
from unittest.mock import patch

import pytest
from typer.testing import CliRunner

from skillspector.cli import app
from skillspector.cli import FormatChoice, _scan_multi_skill, app
from skillspector.multi_skill import MultiSkillDetectionResult, SkillDirectory

runner = CliRunner()

Expand Down Expand Up @@ -113,3 +116,76 @@ def test_cli_baseline_generate_then_scan_round_trip(tmp_path: Path) -> None:
data = json.loads(scan.output)
assert data["issues"] == []
assert data["risk_assessment"]["score"] == 0


def test_scan_multi_skill_markdown_output_to_file(
tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""Non-JSON recursive scan writes concatenated report to file, not stdout."""
s1 = SkillDirectory(path=tmp_path / "skill1", name="skill1", relative_path="skill1")
s2 = SkillDirectory(path=tmp_path / "skill2", name="skill2", relative_path="skill2")
detection = MultiSkillDetectionResult(
is_multi_skill=True, skills=[s1, s2], has_root_skill=False
)

result1 = {
"report_body": "# Report ALPHA for skill1",
"risk_score": 10,
"risk_severity": "LOW",
"findings": [],
}
result2 = {
"report_body": "# Report BETA for skill2",
"risk_score": 10,
"risk_severity": "LOW",
"findings": [],
}
out = tmp_path / "report.md"

with patch("skillspector.cli.graph.invoke", side_effect=[result1, result2]):
_scan_multi_skill(
detection, FormatChoice.markdown, out, no_llm=True, yara_rules_dir=None, verbose=False
)

assert out.exists()
text = out.read_text()
assert "ALPHA" in text
assert "BETA" in text
assert "---" in text

captured = capsys.readouterr()
assert "ALPHA" not in captured.out
assert "BETA" not in captured.out


def test_scan_multi_skill_json_output_unchanged(tmp_path: Path) -> None:
"""JSON recursive scan still produces a valid combined JSON file."""
s1 = SkillDirectory(path=tmp_path / "skill1", name="skill1", relative_path="skill1")
s2 = SkillDirectory(path=tmp_path / "skill2", name="skill2", relative_path="skill2")
detection = MultiSkillDetectionResult(
is_multi_skill=True, skills=[s1, s2], has_root_skill=False
)

result1 = {
"report_body": "# Report ALPHA for skill1",
"risk_score": 10,
"risk_severity": "LOW",
"findings": [],
}
result2 = {
"report_body": "# Report BETA for skill2",
"risk_score": 10,
"risk_severity": "LOW",
"findings": [],
}
out = tmp_path / "combined.json"

with patch("skillspector.cli.graph.invoke", side_effect=[result1, result2]):
_scan_multi_skill(
detection, FormatChoice.json, out, no_llm=True, yara_rules_dir=None, verbose=False
)

assert out.exists()
data = json.loads(out.read_text())
assert data["multi_skill"] is True
assert "skills" in data