sanityops-cli 0.1.6__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/PKG-INFO +2 -2
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/pyproject.toml +2 -2
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/__init__.py +1 -1
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/defect_checker/markdown_reporter.py +42 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/defect_checker/renderer.py +74 -6
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/defect_checker/test_markdown_reporter.py +226 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/defect_checker/test_renderer.py +296 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.github/dependabot.yml +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.github/scripts/pr_review_bot.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.github/workflows/ai-review.yml +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.github/workflows/ci.yml +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.github/workflows/release.yml +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/.gitignore +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/CHANGELOG.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/CODE_OF_CONDUCT.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/CONTRIBUTING.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/LICENSE +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/NOTICE +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/README.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/SECURITY.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-08-13-model-config-comments-in-init-template.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-08-14-loading-animations.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-08-14-skills-files-banner-readme.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-08-28-copyright-compliance.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-09-08-config-command.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-09-20-cross-artifact-rendering.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-09-20-help-panel-migration.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-09-20-repair-feature.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/specs/2026-08-13-model-config-comments-in-init-template-design.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/specs/2026-08-14-loading-animations-design.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/specs/2026-08-14-skills-files-banner-readme-design.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/specs/2026-08-28-copyright-compliance-design.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/specs/2026-09-08-config-command-design.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/specs/2026-09-20-cross-artifact-rendering-design.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/specs/2026-09-20-help-panel-migration-design.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/specs/2026-09-20-repair-feature-design.md +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/entry.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/release.sh +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/scripts/generate-checksums.ps1 +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/scripts/install.ps1 +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/scripts/install.sh +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/repair_agent/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/repair_agent/agent.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/repair_agent/prompts.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/repair_agent/tools/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/repair_agent/tools/store_repairs_tool.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/agent.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/hooks/progress_hook.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/models/finding.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/prompts.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/tools/grep_tool.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/tools/listfiles_tool.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/tools/readfile_tool.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/tools/storefindings_tool.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/api/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/api/client.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/commands/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/commands/config.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/commands/init.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/commands/inspect.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/constants/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/constants/config_defaults.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/constants/exit_codes.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/defect_checker/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/defect_checker/checker.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/defect_checker/llm_config.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/exceptions/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/exceptions/api_exceptions.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/exceptions/base_exceptions.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/help_panel.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/logging/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/logging/logger.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/main.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/progress/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/progress/tracker.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/renderers/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/renderers/command_renderer/inspect_command_renderer.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/templates/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/templates/inspect_config.yaml +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/utils/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/utils/artifact_packer.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/utils/config_loader.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/utils/config_resolver.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/utils/validators.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/commands/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/commands/test_config.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/commands/test_init.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/commands/test_inspect_repair.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/test_help_panel.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/repair_agent/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/repair_agent/test_agent.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/repair_agent/test_store_repairs_tool.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/models/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/models/test_finding.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/test_agent.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/test_deterministic_capture.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/test_progress_hook.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/test_prompts.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/api/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/api/test_client.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/commands/test_inspect.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/commands/test_version.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/defect_checker/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/defect_checker/test_checker.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/defect_checker/test_llm_config.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/logging/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/logging/test_logger.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/progress/__init__.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/progress/test_tracker.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/utils/test_artifact_packer.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/utils/test_config_loader.py +0 -0
- {sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/utils/test_config_resolver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sanityops-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: CLI tool for static defect inspection of AI logical artifacts
|
|
5
5
|
Project-URL: Homepage, https://sanityops.org
|
|
6
6
|
Project-URL: Documentation, https://sanityops.org
|
|
@@ -23,7 +23,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
23
23
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
24
|
Requires-Python: >=3.11
|
|
25
25
|
Requires-Dist: anyio>=4.0
|
|
26
|
-
Requires-Dist: defect-check
|
|
26
|
+
Requires-Dist: defect-check>=0.0.1
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
28
|
Requires-Dist: pyyaml>=6.0
|
|
29
29
|
Requires-Dist: rich>=13.0
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sanityops-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
description = "CLI tool for static defect inspection of AI logical artifacts"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -31,7 +31,7 @@ dependencies = [
|
|
|
31
31
|
"httpx>=0.27",
|
|
32
32
|
"tomli>=2.0",
|
|
33
33
|
"tomli-w>=1.0",
|
|
34
|
-
"defect-check",
|
|
34
|
+
"defect-check>=0.0.1",
|
|
35
35
|
"pyyaml>=6.0",
|
|
36
36
|
"sanityops-agent>=0.0.4",
|
|
37
37
|
"anyio>=4.0",
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/defect_checker/markdown_reporter.py
RENAMED
|
@@ -77,6 +77,40 @@ def _aggregate_cross_defects(cross_results: list[dict]) -> list[dict]:
|
|
|
77
77
|
return defects
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
def _format_permission_table(defects: list[dict]) -> list[str]:
|
|
81
|
+
"""Generate extended markdown table for QD-PM permission defects.
|
|
82
|
+
|
|
83
|
+
Columns: ID | Name | Severity | Description | Action | Permission Side | Duty Side | Fix
|
|
84
|
+
|
|
85
|
+
Note: Missing fields display as em dash (—) via _esc_md_cell(None).
|
|
86
|
+
This differs from terminal renderer which silently omits missing fields.
|
|
87
|
+
"""
|
|
88
|
+
lines = []
|
|
89
|
+
lines.append("| ID | Name | Severity | Description | Action | Permission Side | Duty Side | Fix |")
|
|
90
|
+
lines.append("|---|---|---|---|---|---|---|---|")
|
|
91
|
+
|
|
92
|
+
for defect in defects:
|
|
93
|
+
details = defect.get("details", {})
|
|
94
|
+
if not isinstance(details, dict):
|
|
95
|
+
details = {}
|
|
96
|
+
|
|
97
|
+
defect_id = _esc_md_cell(defect.get("id") or "unknown-permission-defect")
|
|
98
|
+
name = _esc_md_cell(defect.get("name"))
|
|
99
|
+
severity = _esc_md_cell(defect.get("severity"))
|
|
100
|
+
description = _esc_md_cell(defect.get("description"))
|
|
101
|
+
action = _esc_md_cell(details.get("action"))
|
|
102
|
+
permission_side = _esc_md_cell(details.get("permission_side"))
|
|
103
|
+
duty_side = _esc_md_cell(details.get("duty_side"))
|
|
104
|
+
fix = _esc_md_cell(defect.get("fix_suggestion"))
|
|
105
|
+
|
|
106
|
+
lines.append(
|
|
107
|
+
f"| {defect_id} | {name} | {severity} | {description} | {action} | "
|
|
108
|
+
f"{permission_side} | {duty_side} | {fix} |"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return lines
|
|
112
|
+
|
|
113
|
+
|
|
80
114
|
def _calculate_cross_score(
|
|
81
115
|
defects: list[dict],
|
|
82
116
|
check_level: str,
|
|
@@ -188,6 +222,14 @@ def _build_report(
|
|
|
188
222
|
lines.append("")
|
|
189
223
|
continue
|
|
190
224
|
|
|
225
|
+
# Use permission-specific table for QD-PM module.
|
|
226
|
+
# Renderer also detects via module (primary), category, or ID prefix for consistency.
|
|
227
|
+
if module == "QD-PM":
|
|
228
|
+
lines.extend(_format_permission_table(defects))
|
|
229
|
+
lines.append("")
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
# Standard defect table for other modules
|
|
191
233
|
lines.append("| ID | Name | Severity | Description | Location | Impact | Fix |")
|
|
192
234
|
lines.append("|---|---|---|---|---|---|---|")
|
|
193
235
|
for defect in defects:
|
|
@@ -33,6 +33,7 @@ _MODULE_LABELS: dict[str, str] = {
|
|
|
33
33
|
"QDT": "Tools",
|
|
34
34
|
"QDP": "Prompts",
|
|
35
35
|
"CROSS": "Cross",
|
|
36
|
+
"QD-PM": "Permissions",
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
#: Map module names to singular type labels for panel titles.
|
|
@@ -40,6 +41,7 @@ _MODULE_TYPE_LABELS: dict[str, str] = {
|
|
|
40
41
|
"QDS": "Skill",
|
|
41
42
|
"QDT": "Tool",
|
|
42
43
|
"QDP": "Prompt",
|
|
44
|
+
"QD-PM": "Permission",
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
|
|
@@ -147,20 +149,23 @@ class DefectRenderer:
|
|
|
147
149
|
|
|
148
150
|
def _render_artifact_groups(self, results: list[dict[str, Any]]) -> None:
|
|
149
151
|
for result in results:
|
|
152
|
+
module = result.get("module", "")
|
|
150
153
|
title = resolve_artifact_title(result)
|
|
151
154
|
if title is None:
|
|
152
155
|
continue
|
|
153
156
|
defects = result.get("defects", [])
|
|
154
157
|
if not defects:
|
|
155
158
|
continue
|
|
156
|
-
self._render_group(title, defects)
|
|
159
|
+
self._render_group(title, defects, module=module)
|
|
157
160
|
self.console.print()
|
|
158
161
|
|
|
159
|
-
def _render_group(
|
|
162
|
+
def _render_group(
|
|
163
|
+
self, label: str, defects: list[dict[str, Any]], *, module: str = ""
|
|
164
|
+
) -> None:
|
|
160
165
|
lines: list[str] = []
|
|
161
166
|
shown = defects[:MAX_DEFECTS_PER_GROUP]
|
|
162
167
|
for defect in shown:
|
|
163
|
-
lines.append(self._format_defect(defect))
|
|
168
|
+
lines.append(self._format_defect(defect, module=module))
|
|
164
169
|
|
|
165
170
|
hidden = len(defects) - len(shown)
|
|
166
171
|
if hidden > 0:
|
|
@@ -169,9 +174,27 @@ class DefectRenderer:
|
|
|
169
174
|
body = "\n".join(lines)
|
|
170
175
|
self.console.print(Panel(body, title=label, border_style="cyan"))
|
|
171
176
|
|
|
172
|
-
def _format_defect(self, defect: dict[str, Any]) -> str:
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
def _format_defect(self, defect: dict[str, Any], *, module: str = "") -> str:
|
|
178
|
+
"""Format a defect for terminal display.
|
|
179
|
+
|
|
180
|
+
Dispatches to specialized formatter for permission defects.
|
|
181
|
+
|
|
182
|
+
Detection priority: module == "QD-PM" > category == "permission" > ID prefix.
|
|
183
|
+
This is more permissive than markdown_reporter's module-only detection.
|
|
184
|
+
Rationale: terminal output is per-defect and can adapt based on defect
|
|
185
|
+
content, while markdown uses module-level table structure. The primary
|
|
186
|
+
path (module == "QD-PM") is shared; fallbacks handle edge cases where
|
|
187
|
+
defect metadata is inconsistent. This is intentional design.
|
|
188
|
+
"""
|
|
189
|
+
# Detect permission defects via module, category, or ID prefix
|
|
190
|
+
category = defect.get("category") or ""
|
|
191
|
+
raw_defect_id = defect.get("id") or ""
|
|
192
|
+
if module == "QD-PM" or category == "permission" or raw_defect_id.startswith("QD-PM"):
|
|
193
|
+
return self._format_permission_defect(defect)
|
|
194
|
+
|
|
195
|
+
# Default formatting for other defect types
|
|
196
|
+
severity = defect.get("severity") or "NONE"
|
|
197
|
+
defect_id = raw_defect_id or "defect"
|
|
175
198
|
lines = [f"[bold red]✗ [{severity}] {defect_id}[/]"]
|
|
176
199
|
if defect.get("location"):
|
|
177
200
|
lines.append(f" [dim]Location[/] : {defect['location']}")
|
|
@@ -181,6 +204,51 @@ class DefectRenderer:
|
|
|
181
204
|
lines.append(f" [dim]Fix[/] : {defect['fix_suggestion']}")
|
|
182
205
|
return "\n".join(lines)
|
|
183
206
|
|
|
207
|
+
def _format_permission_defect(self, defect: dict[str, Any]) -> str:
|
|
208
|
+
"""Format a QD-PM permission defect with permission-specific fields.
|
|
209
|
+
|
|
210
|
+
Shows: name, description, action, permission_side, duty_side from details dict.
|
|
211
|
+
|
|
212
|
+
Note: Missing fields are silently omitted (unlike markdown which uses em dash).
|
|
213
|
+
This is intentional for terminal output compactness.
|
|
214
|
+
"""
|
|
215
|
+
severity = defect.get("severity") or "NONE"
|
|
216
|
+
defect_id = defect.get("id") or "unknown-permission-defect"
|
|
217
|
+
lines = [f"[bold red]✗ [{severity}] {defect_id}[/]"]
|
|
218
|
+
|
|
219
|
+
# Name field (for consistency with markdown table)
|
|
220
|
+
if defect.get("name"):
|
|
221
|
+
lines.append(f" [dim]Name[/] : {defect['name']}")
|
|
222
|
+
|
|
223
|
+
# Description field (standard field)
|
|
224
|
+
if defect.get("description"):
|
|
225
|
+
lines.append(f" [dim]Desc[/] : {defect['description']}")
|
|
226
|
+
|
|
227
|
+
# Standard fields
|
|
228
|
+
if defect.get("location"):
|
|
229
|
+
lines.append(f" [dim]Location[/] : {defect['location']}")
|
|
230
|
+
if defect.get("impact"):
|
|
231
|
+
lines.append(f" [dim]Impact[/] : {defect['impact']}")
|
|
232
|
+
|
|
233
|
+
# Permission-specific fields from details dict
|
|
234
|
+
details = defect.get("details", {})
|
|
235
|
+
if isinstance(details, dict):
|
|
236
|
+
action = details.get("action")
|
|
237
|
+
if action:
|
|
238
|
+
lines.append(f" [dim]Action[/] : {action}")
|
|
239
|
+
permission_side = details.get("permission_side")
|
|
240
|
+
if permission_side:
|
|
241
|
+
lines.append(f" [dim]Permission[/]: {permission_side}")
|
|
242
|
+
duty_side = details.get("duty_side")
|
|
243
|
+
if duty_side:
|
|
244
|
+
lines.append(f" [dim]Duty[/] : {duty_side}")
|
|
245
|
+
|
|
246
|
+
# Fix suggestion
|
|
247
|
+
if defect.get("fix_suggestion"):
|
|
248
|
+
lines.append(f" [dim]Fix[/] : {defect['fix_suggestion']}")
|
|
249
|
+
|
|
250
|
+
return "\n".join(lines)
|
|
251
|
+
|
|
184
252
|
# ------------------------------------------------------------------
|
|
185
253
|
# Footer
|
|
186
254
|
# ------------------------------------------------------------------
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/defect_checker/test_markdown_reporter.py
RENAMED
|
@@ -319,3 +319,229 @@ class TestCrossArtifactReport:
|
|
|
319
319
|
assert content.count("## Cross (CROSS)") == 1
|
|
320
320
|
assert "QD-PS-1" in content
|
|
321
321
|
assert "QD-PT-1" in content
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class TestPermissionReport:
|
|
325
|
+
"""Tests for QD-PM permission defect markdown reporting."""
|
|
326
|
+
|
|
327
|
+
def _permission_defect(
|
|
328
|
+
self,
|
|
329
|
+
defect_id="QD-PM-1.1",
|
|
330
|
+
severity="P0",
|
|
331
|
+
name="Permission overflow",
|
|
332
|
+
action="read",
|
|
333
|
+
permission_side="skill -> tool -> params",
|
|
334
|
+
duty_side="skill -> para.3",
|
|
335
|
+
fix="Narrow scope",
|
|
336
|
+
):
|
|
337
|
+
"""Create a permission defect dict for testing."""
|
|
338
|
+
return {
|
|
339
|
+
"id": defect_id,
|
|
340
|
+
"name": name,
|
|
341
|
+
"severity": severity,
|
|
342
|
+
"category": "permission",
|
|
343
|
+
"description": "Permission exceeds duty boundary",
|
|
344
|
+
"fix_suggestion": fix,
|
|
345
|
+
"details": {
|
|
346
|
+
"action": action,
|
|
347
|
+
"permission_side": permission_side,
|
|
348
|
+
"duty_side": duty_side,
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
def test_permission_module_uses_extended_table(self, tmp_path):
|
|
353
|
+
"""QD-PM defects use extended table with permission columns."""
|
|
354
|
+
response = _response(
|
|
355
|
+
results=[
|
|
356
|
+
{
|
|
357
|
+
"module": "QD-PM",
|
|
358
|
+
"status": "completed",
|
|
359
|
+
"defects": [self._permission_defect()],
|
|
360
|
+
}
|
|
361
|
+
]
|
|
362
|
+
)
|
|
363
|
+
path = save_markdown_report(response, tmp_path)
|
|
364
|
+
content = path.read_text(encoding="utf-8")
|
|
365
|
+
assert "## Permissions (QD-PM)" in content
|
|
366
|
+
# Extended table header
|
|
367
|
+
assert "| ID | Name | Severity | Description | Action | Permission Side | Duty Side | Fix |" in content
|
|
368
|
+
# Data row with permission fields
|
|
369
|
+
assert "QD-PM-1.1" in content
|
|
370
|
+
assert "read" in content
|
|
371
|
+
# Note: `->` is HTML-escaped to `->` by _esc_md_cell
|
|
372
|
+
assert "skill -> tool -> params" in content
|
|
373
|
+
|
|
374
|
+
def test_permission_defect_without_details(self, tmp_path):
|
|
375
|
+
"""Permission defects without details field render with dashes."""
|
|
376
|
+
response = _response(
|
|
377
|
+
results=[
|
|
378
|
+
{
|
|
379
|
+
"module": "QD-PM",
|
|
380
|
+
"status": "completed",
|
|
381
|
+
"defects": [
|
|
382
|
+
{
|
|
383
|
+
"id": "QD-PM-2.1",
|
|
384
|
+
"name": "Permission issue",
|
|
385
|
+
"severity": "P1",
|
|
386
|
+
"category": "permission",
|
|
387
|
+
"fix_suggestion": "Fix it",
|
|
388
|
+
# No details dict
|
|
389
|
+
}
|
|
390
|
+
],
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
)
|
|
394
|
+
path = save_markdown_report(response, tmp_path)
|
|
395
|
+
content = path.read_text(encoding="utf-8")
|
|
396
|
+
assert "QD-PM-2.1" in content
|
|
397
|
+
# Should still have extended table header
|
|
398
|
+
assert "| Action | Permission Side | Duty Side |" in content
|
|
399
|
+
# Missing fields should show dashes
|
|
400
|
+
assert "—" in content
|
|
401
|
+
|
|
402
|
+
def test_permission_defect_with_empty_details(self, tmp_path):
|
|
403
|
+
"""Permission defects with empty details dict render gracefully."""
|
|
404
|
+
response = _response(
|
|
405
|
+
results=[
|
|
406
|
+
{
|
|
407
|
+
"module": "QD-PM",
|
|
408
|
+
"status": "completed",
|
|
409
|
+
"defects": [
|
|
410
|
+
{
|
|
411
|
+
"id": "QD-PM-3.1",
|
|
412
|
+
"name": "Permission issue",
|
|
413
|
+
"severity": "P1",
|
|
414
|
+
"category": "permission",
|
|
415
|
+
"fix_suggestion": "Fix it",
|
|
416
|
+
"details": {}, # Empty dict
|
|
417
|
+
}
|
|
418
|
+
],
|
|
419
|
+
}
|
|
420
|
+
]
|
|
421
|
+
)
|
|
422
|
+
path = save_markdown_report(response, tmp_path)
|
|
423
|
+
content = path.read_text(encoding="utf-8")
|
|
424
|
+
assert "QD-PM-3.1" in content
|
|
425
|
+
# Missing fields should show dashes
|
|
426
|
+
assert "—" in content
|
|
427
|
+
|
|
428
|
+
def test_permission_table_multiple_defects(self, tmp_path):
|
|
429
|
+
"""Permission table handles multiple defects correctly."""
|
|
430
|
+
response = _response(
|
|
431
|
+
results=[
|
|
432
|
+
{
|
|
433
|
+
"module": "QD-PM",
|
|
434
|
+
"status": "completed",
|
|
435
|
+
"defects": [
|
|
436
|
+
self._permission_defect(
|
|
437
|
+
defect_id="QD-PM-1.1",
|
|
438
|
+
action="read",
|
|
439
|
+
permission_side="skill -> tool -> params.read",
|
|
440
|
+
),
|
|
441
|
+
self._permission_defect(
|
|
442
|
+
defect_id="QD-PM-1.2",
|
|
443
|
+
action="write",
|
|
444
|
+
permission_side="skill -> tool -> params.write",
|
|
445
|
+
),
|
|
446
|
+
],
|
|
447
|
+
}
|
|
448
|
+
]
|
|
449
|
+
)
|
|
450
|
+
path = save_markdown_report(response, tmp_path)
|
|
451
|
+
content = path.read_text(encoding="utf-8")
|
|
452
|
+
assert "QD-PM-1.1" in content
|
|
453
|
+
assert "QD-PM-1.2" in content
|
|
454
|
+
assert "read" in content
|
|
455
|
+
assert "write" in content
|
|
456
|
+
|
|
457
|
+
def test_permission_alongside_other_modules(self, tmp_path):
|
|
458
|
+
"""Permission module coexists with other modules in report."""
|
|
459
|
+
response = _response(
|
|
460
|
+
results=[
|
|
461
|
+
{
|
|
462
|
+
"module": "QDS",
|
|
463
|
+
"status": "completed",
|
|
464
|
+
"defects": [_defect(defect_id="QDS-1", severity="P1")],
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"module": "QD-PM",
|
|
468
|
+
"status": "completed",
|
|
469
|
+
"defects": [self._permission_defect()],
|
|
470
|
+
},
|
|
471
|
+
]
|
|
472
|
+
)
|
|
473
|
+
path = save_markdown_report(response, tmp_path)
|
|
474
|
+
content = path.read_text(encoding="utf-8")
|
|
475
|
+
# Both modules should appear
|
|
476
|
+
assert "## Skills (QDS)" in content
|
|
477
|
+
assert "## Permissions (QD-PM)" in content
|
|
478
|
+
# QDS should use standard table
|
|
479
|
+
assert "| Description | Location | Impact |" in content
|
|
480
|
+
# QD-PM should use extended table
|
|
481
|
+
assert "| Action | Permission Side | Duty Side |" in content
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
class TestFormatPermissionTable:
|
|
485
|
+
"""Tests for _format_permission_table helper function."""
|
|
486
|
+
|
|
487
|
+
def test_formats_single_defect(self):
|
|
488
|
+
"""Formats a single permission defect correctly."""
|
|
489
|
+
from sanityops_cli.defect_checker.markdown_reporter import _format_permission_table
|
|
490
|
+
|
|
491
|
+
defects = [
|
|
492
|
+
{
|
|
493
|
+
"id": "QD-PM-1.1",
|
|
494
|
+
"name": "Permission overflow",
|
|
495
|
+
"severity": "P0",
|
|
496
|
+
"fix_suggestion": "Narrow scope",
|
|
497
|
+
"details": {
|
|
498
|
+
"action": "read",
|
|
499
|
+
"permission_side": "skill -> tool -> params",
|
|
500
|
+
"duty_side": "skill -> para.3",
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
lines = _format_permission_table(defects)
|
|
505
|
+
assert len(lines) == 3 # Header + separator + 1 data row
|
|
506
|
+
assert "QD-PM-1.1" in lines[2]
|
|
507
|
+
assert "read" in lines[2]
|
|
508
|
+
|
|
509
|
+
def test_formats_multiple_defects(self):
|
|
510
|
+
"""Formats multiple permission defects correctly."""
|
|
511
|
+
from sanityops_cli.defect_checker.markdown_reporter import _format_permission_table
|
|
512
|
+
|
|
513
|
+
defects = [
|
|
514
|
+
{
|
|
515
|
+
"id": "QD-PM-1.1",
|
|
516
|
+
"name": "First",
|
|
517
|
+
"severity": "P0",
|
|
518
|
+
"details": {"action": "read"},
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
"id": "QD-PM-1.2",
|
|
522
|
+
"name": "Second",
|
|
523
|
+
"severity": "P1",
|
|
524
|
+
"details": {"action": "write"},
|
|
525
|
+
},
|
|
526
|
+
]
|
|
527
|
+
lines = _format_permission_table(defects)
|
|
528
|
+
assert len(lines) == 4 # Header + separator + 2 data rows
|
|
529
|
+
assert "read" in lines[2]
|
|
530
|
+
assert "write" in lines[3]
|
|
531
|
+
|
|
532
|
+
def test_handles_missing_details(self):
|
|
533
|
+
"""Handles defects without details dict."""
|
|
534
|
+
from sanityops_cli.defect_checker.markdown_reporter import _format_permission_table
|
|
535
|
+
|
|
536
|
+
defects = [
|
|
537
|
+
{
|
|
538
|
+
"id": "QD-PM-1.1",
|
|
539
|
+
"name": "No details",
|
|
540
|
+
"severity": "P1",
|
|
541
|
+
"fix_suggestion": "Fix it",
|
|
542
|
+
# No details
|
|
543
|
+
}
|
|
544
|
+
]
|
|
545
|
+
lines = _format_permission_table(defects)
|
|
546
|
+
assert len(lines) == 3
|
|
547
|
+
assert "—" in lines[2] # Missing fields show dash
|
|
@@ -308,3 +308,299 @@ class TestResolveArtifactTitle:
|
|
|
308
308
|
|
|
309
309
|
result = {"artifacts": [{"id": "skill-1", "name": "morning-report"}]}
|
|
310
310
|
assert resolve_artifact_title(result) is None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class TestPermissionDefects:
|
|
314
|
+
"""Tests for QD-PM permission defect rendering."""
|
|
315
|
+
|
|
316
|
+
def _permission_defect(
|
|
317
|
+
self,
|
|
318
|
+
defect_id="QD-PM-1.1",
|
|
319
|
+
severity="P0",
|
|
320
|
+
name="Permission overflow",
|
|
321
|
+
location="skill.md:15",
|
|
322
|
+
impact="Excessive read access",
|
|
323
|
+
fix="Narrow permission scope",
|
|
324
|
+
action="read",
|
|
325
|
+
permission_side="skill -> tool -> params",
|
|
326
|
+
duty_side="skill -> para.3 -> stmt.1",
|
|
327
|
+
):
|
|
328
|
+
"""Create a permission defect dict for testing."""
|
|
329
|
+
return {
|
|
330
|
+
"id": defect_id,
|
|
331
|
+
"name": name,
|
|
332
|
+
"severity": severity,
|
|
333
|
+
"category": "permission",
|
|
334
|
+
"description": "Permission exceeds duty boundary",
|
|
335
|
+
"location": location,
|
|
336
|
+
"impact": impact,
|
|
337
|
+
"fix_suggestion": fix,
|
|
338
|
+
"details": {
|
|
339
|
+
"action": action,
|
|
340
|
+
"permission_side": permission_side,
|
|
341
|
+
"duty_side": duty_side,
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
def test_qdpm_module_rendered(self):
|
|
346
|
+
"""QD-PM module is rendered with 'Permissions' label."""
|
|
347
|
+
console = Console(record=True, width=100)
|
|
348
|
+
result = _result(
|
|
349
|
+
[
|
|
350
|
+
{
|
|
351
|
+
"module": "QD-PM",
|
|
352
|
+
"status": "completed",
|
|
353
|
+
"artifacts": [],
|
|
354
|
+
"defects": [self._permission_defect()],
|
|
355
|
+
}
|
|
356
|
+
]
|
|
357
|
+
)
|
|
358
|
+
DefectRenderer(console).render(result)
|
|
359
|
+
out = console.export_text()
|
|
360
|
+
assert "Permissions" in out
|
|
361
|
+
assert "QD-PM-1.1" in out
|
|
362
|
+
|
|
363
|
+
def test_permission_defect_shows_action_field(self):
|
|
364
|
+
"""Permission defects show action field in terminal."""
|
|
365
|
+
console = Console(record=True, width=100)
|
|
366
|
+
result = _result(
|
|
367
|
+
[
|
|
368
|
+
{
|
|
369
|
+
"module": "QD-PM",
|
|
370
|
+
"status": "completed",
|
|
371
|
+
"artifacts": [],
|
|
372
|
+
"defects": [self._permission_defect(action="write")],
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
)
|
|
376
|
+
DefectRenderer(console).render(result)
|
|
377
|
+
out = console.export_text()
|
|
378
|
+
assert "Action" in out
|
|
379
|
+
assert "write" in out
|
|
380
|
+
|
|
381
|
+
def test_permission_defect_shows_permission_side(self):
|
|
382
|
+
"""Permission defects show permission_side field."""
|
|
383
|
+
console = Console(record=True, width=120)
|
|
384
|
+
result = _result(
|
|
385
|
+
[
|
|
386
|
+
{
|
|
387
|
+
"module": "QD-PM",
|
|
388
|
+
"status": "completed",
|
|
389
|
+
"artifacts": [],
|
|
390
|
+
"defects": [self._permission_defect(permission_side="skill -> tool -> file")],
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
)
|
|
394
|
+
DefectRenderer(console).render(result)
|
|
395
|
+
out = console.export_text()
|
|
396
|
+
assert "Permission" in out and "skill -> tool -> file" in out
|
|
397
|
+
|
|
398
|
+
def test_permission_defect_shows_duty_side(self):
|
|
399
|
+
"""Permission defects show duty_side field."""
|
|
400
|
+
console = Console(record=True, width=120)
|
|
401
|
+
result = _result(
|
|
402
|
+
[
|
|
403
|
+
{
|
|
404
|
+
"module": "QD-PM",
|
|
405
|
+
"status": "completed",
|
|
406
|
+
"artifacts": [],
|
|
407
|
+
"defects": [self._permission_defect(duty_side="skill -> para.5")],
|
|
408
|
+
}
|
|
409
|
+
]
|
|
410
|
+
)
|
|
411
|
+
DefectRenderer(console).render(result)
|
|
412
|
+
out = console.export_text()
|
|
413
|
+
assert "Duty" in out and "skill -> para.5" in out
|
|
414
|
+
|
|
415
|
+
def test_permission_defect_without_details(self):
|
|
416
|
+
"""Permission defects without details field still render."""
|
|
417
|
+
console = Console(record=True, width=100)
|
|
418
|
+
defect = {
|
|
419
|
+
"id": "QD-PM-2.1",
|
|
420
|
+
"name": "Permission issue",
|
|
421
|
+
"severity": "P1",
|
|
422
|
+
"category": "permission",
|
|
423
|
+
"description": "desc",
|
|
424
|
+
"location": "skill.md:20",
|
|
425
|
+
"impact": "impact",
|
|
426
|
+
"fix_suggestion": "fix",
|
|
427
|
+
# No details dict
|
|
428
|
+
}
|
|
429
|
+
result = _result(
|
|
430
|
+
[
|
|
431
|
+
{
|
|
432
|
+
"module": "QD-PM",
|
|
433
|
+
"status": "completed",
|
|
434
|
+
"artifacts": [],
|
|
435
|
+
"defects": [defect],
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
)
|
|
439
|
+
DefectRenderer(console).render(result)
|
|
440
|
+
out = console.export_text()
|
|
441
|
+
assert "QD-PM-2.1" in out
|
|
442
|
+
assert "Location" in out
|
|
443
|
+
|
|
444
|
+
def test_permission_defect_with_empty_details(self):
|
|
445
|
+
"""Permission defects with empty details dict render gracefully."""
|
|
446
|
+
console = Console(record=True, width=100)
|
|
447
|
+
defect = {
|
|
448
|
+
"id": "QD-PM-3.1",
|
|
449
|
+
"name": "Permission issue",
|
|
450
|
+
"severity": "P1",
|
|
451
|
+
"category": "permission",
|
|
452
|
+
"description": "desc",
|
|
453
|
+
"location": "skill.md:30",
|
|
454
|
+
"details": {}, # Empty dict
|
|
455
|
+
}
|
|
456
|
+
result = _result(
|
|
457
|
+
[
|
|
458
|
+
{
|
|
459
|
+
"module": "QD-PM",
|
|
460
|
+
"status": "completed",
|
|
461
|
+
"artifacts": [],
|
|
462
|
+
"defects": [defect],
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
)
|
|
466
|
+
DefectRenderer(console).render(result)
|
|
467
|
+
out = console.export_text()
|
|
468
|
+
assert "QD-PM-3.1" in out
|
|
469
|
+
# Should not crash, should show standard fields
|
|
470
|
+
|
|
471
|
+
def test_permission_defect_detected_by_id_prefix(self):
|
|
472
|
+
"""Permission defect detected by QD-PM ID prefix even with different module.
|
|
473
|
+
|
|
474
|
+
This tests the ID prefix fallback detection path.
|
|
475
|
+
"""
|
|
476
|
+
console = Console(record=True, width=100)
|
|
477
|
+
defect = {
|
|
478
|
+
"id": "QD-PM-4.1", # ID starts with QD-PM
|
|
479
|
+
"name": "Permission issue",
|
|
480
|
+
"severity": "P1",
|
|
481
|
+
"category": "", # No category
|
|
482
|
+
"description": "desc",
|
|
483
|
+
"details": {"action": "read"},
|
|
484
|
+
}
|
|
485
|
+
result = _result(
|
|
486
|
+
[
|
|
487
|
+
{
|
|
488
|
+
"module": "QDS", # Different module - tests ID prefix detection
|
|
489
|
+
"status": "completed",
|
|
490
|
+
"artifacts": [],
|
|
491
|
+
"defects": [defect],
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
)
|
|
495
|
+
DefectRenderer(console).render(result)
|
|
496
|
+
out = console.export_text()
|
|
497
|
+
# Should use permission format due to ID prefix
|
|
498
|
+
assert "Action" in out
|
|
499
|
+
assert "read" in out
|
|
500
|
+
|
|
501
|
+
def test_permission_defect_detected_by_category(self):
|
|
502
|
+
"""Permission defect detected by category='permission' with non-QD-PM module and ID.
|
|
503
|
+
|
|
504
|
+
This tests the category-based fallback detection path.
|
|
505
|
+
"""
|
|
506
|
+
console = Console(record=True, width=100)
|
|
507
|
+
defect = {
|
|
508
|
+
"id": "MISC-1", # Not QD-PM prefix
|
|
509
|
+
"name": "Permission issue",
|
|
510
|
+
"severity": "P1",
|
|
511
|
+
"category": "permission", # Category triggers detection
|
|
512
|
+
"description": "desc",
|
|
513
|
+
"details": {"action": "write"},
|
|
514
|
+
}
|
|
515
|
+
result = _result(
|
|
516
|
+
[
|
|
517
|
+
{
|
|
518
|
+
"module": "QDT", # Different module - tests category detection
|
|
519
|
+
"status": "completed",
|
|
520
|
+
"artifacts": [],
|
|
521
|
+
"defects": [defect],
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
)
|
|
525
|
+
DefectRenderer(console).render(result)
|
|
526
|
+
out = console.export_text()
|
|
527
|
+
# Should use permission format due to category
|
|
528
|
+
assert "Action" in out
|
|
529
|
+
assert "write" in out
|
|
530
|
+
|
|
531
|
+
def test_permission_defect_with_none_id_no_crash(self):
|
|
532
|
+
"""Renderer doesn't crash when defect id is None.
|
|
533
|
+
|
|
534
|
+
This tests the fix for potential AttributeError when id key exists
|
|
535
|
+
but value is None.
|
|
536
|
+
"""
|
|
537
|
+
console = Console(record=True, width=100)
|
|
538
|
+
defect = {
|
|
539
|
+
"id": None, # Explicitly None
|
|
540
|
+
"name": "Permission issue",
|
|
541
|
+
"severity": "P1",
|
|
542
|
+
"category": "permission",
|
|
543
|
+
"details": {"action": "read"},
|
|
544
|
+
}
|
|
545
|
+
result = _result(
|
|
546
|
+
[
|
|
547
|
+
{
|
|
548
|
+
"module": "QD-PM",
|
|
549
|
+
"status": "completed",
|
|
550
|
+
"artifacts": [],
|
|
551
|
+
"defects": [defect],
|
|
552
|
+
}
|
|
553
|
+
]
|
|
554
|
+
)
|
|
555
|
+
# Should not raise AttributeError
|
|
556
|
+
DefectRenderer(console).render(result)
|
|
557
|
+
out = console.export_text()
|
|
558
|
+
assert "Action" in out
|
|
559
|
+
|
|
560
|
+
def test_permission_defect_shows_name_field(self):
|
|
561
|
+
"""Permission defects show name field for consistency with markdown."""
|
|
562
|
+
console = Console(record=True, width=100)
|
|
563
|
+
result = _result(
|
|
564
|
+
[
|
|
565
|
+
{
|
|
566
|
+
"module": "QD-PM",
|
|
567
|
+
"status": "completed",
|
|
568
|
+
"artifacts": [],
|
|
569
|
+
"defects": [self._permission_defect(name="Permission overflow")],
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
)
|
|
573
|
+
DefectRenderer(console).render(result)
|
|
574
|
+
out = console.export_text()
|
|
575
|
+
assert "Name" in out
|
|
576
|
+
assert "Permission overflow" in out
|
|
577
|
+
|
|
578
|
+
def test_permission_format_used_when_module_is_qdpm(self):
|
|
579
|
+
"""Permission format used when module is QD-PM, even with non-matching ID/category.
|
|
580
|
+
|
|
581
|
+
This tests the edge case where module == "QD-PM" but defect ID doesn't start
|
|
582
|
+
with QD-PM and category is empty - ensuring terminal and markdown stay consistent.
|
|
583
|
+
"""
|
|
584
|
+
console = Console(record=True, width=100)
|
|
585
|
+
defect = {
|
|
586
|
+
"id": "OTHER-1", # Not QD-PM prefix
|
|
587
|
+
"name": "Edge case defect",
|
|
588
|
+
"severity": "P1",
|
|
589
|
+
"category": "", # Empty category
|
|
590
|
+
"details": {"action": "read"},
|
|
591
|
+
}
|
|
592
|
+
result = _result(
|
|
593
|
+
[
|
|
594
|
+
{
|
|
595
|
+
"module": "QD-PM", # Module is QD-PM
|
|
596
|
+
"status": "completed",
|
|
597
|
+
"artifacts": [],
|
|
598
|
+
"defects": [defect],
|
|
599
|
+
}
|
|
600
|
+
]
|
|
601
|
+
)
|
|
602
|
+
DefectRenderer(console).render(result)
|
|
603
|
+
out = console.export_text()
|
|
604
|
+
# Should still use permission format due to module == "QD-PM"
|
|
605
|
+
assert "Action" in out
|
|
606
|
+
assert "read" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-08-14-loading-animations.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-09-08-config-command.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/docs/superpowers/plans/2026-09-20-repair-feature.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/repair_agent/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/repair_agent/prompts.py
RENAMED
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/repair_agent/tools/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/models/finding.py
RENAMED
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/src/sanityops_cli/agents/scanner_agent/prompts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/models/__init__.py
RENAMED
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/models/test_finding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sanityops_cli-0.1.6 → sanityops_cli-0.1.7}/tests/unit/agents/scanner_agent/test_progress_hook.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|