@openhands/extensions 0.1.0 → 0.2.0
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.
- package/.agents/skills/custom-codereview-guide.md +25 -0
- package/.github/pull_request_template.md +38 -0
- package/.github/release.yml +14 -0
- package/.github/workflows/check-extensions.yml +72 -0
- package/.github/workflows/npm-publish.yml +89 -0
- package/.github/workflows/pr.yml +30 -0
- package/.github/workflows/release.yml +24 -0
- package/.github/workflows/tests.yml +25 -0
- package/.github/workflows/vulnerability-scan.yml +87 -0
- package/.release-please-manifest.json +3 -0
- package/AGENTS.md +132 -0
- package/README.md +10 -0
- package/analysis_results.md +162 -0
- package/marketplaces/large-codebase.json +66 -0
- package/marketplaces/openhands-extensions.json +682 -0
- package/package.json +4 -10
- package/plugins/README.md +30 -0
- package/plugins/city-weather/.plugin/plugin.json +13 -0
- package/plugins/city-weather/README.md +145 -0
- package/plugins/city-weather/commands/now.md +56 -0
- package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
- package/plugins/cobol-modernization/README.md +201 -0
- package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
- package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
- package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
- package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
- package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
- package/plugins/issue-duplicate-checker/README.md +51 -0
- package/plugins/issue-duplicate-checker/action.yml +349 -0
- package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
- package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
- package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
- package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
- package/plugins/magic-test/.plugin/plugin.json +13 -0
- package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
- package/plugins/migration-scoring/.plugin/plugin.json +19 -0
- package/plugins/migration-scoring/README.md +244 -0
- package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
- package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
- package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
- package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
- package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
- package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
- package/plugins/onboarding/.plugin/plugin.json +20 -0
- package/plugins/onboarding/README.md +30 -0
- package/plugins/onboarding/references/criteria.md +144 -0
- package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
- package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
- package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
- package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
- package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
- package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
- package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
- package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
- package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
- package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
- package/plugins/openhands/.plugin/plugin.json +13 -0
- package/plugins/openhands/README.md +52 -0
- package/plugins/openhands/SKILL.md +61 -0
- package/plugins/openhands/commands/create.md +55 -0
- package/plugins/openhands/commands/openhands-cloud.md +8 -0
- package/plugins/openhands/scripts/run.sh +69 -0
- package/plugins/pr-review/.plugin/plugin.json +13 -0
- package/plugins/pr-review/README.md +393 -0
- package/plugins/pr-review/action.yml +298 -0
- package/plugins/pr-review/scripts/agent_script.py +1282 -0
- package/plugins/pr-review/scripts/evaluate_review.py +655 -0
- package/plugins/pr-review/scripts/prompt.py +260 -0
- package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
- package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
- package/plugins/qa-changes/.plugin/plugin.json +11 -0
- package/plugins/qa-changes/README.md +185 -0
- package/plugins/qa-changes/action.yml +181 -0
- package/plugins/qa-changes/scripts/agent_script.py +406 -0
- package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
- package/plugins/qa-changes/scripts/prompt.py +174 -0
- package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
- package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
- package/plugins/release-notes/.plugin/plugin.json +19 -0
- package/plugins/release-notes/README.md +283 -0
- package/plugins/release-notes/SKILL.md +83 -0
- package/plugins/release-notes/action.yml +117 -0
- package/plugins/release-notes/commands/release-notes.md +8 -0
- package/plugins/release-notes/scripts/agent_script.py +292 -0
- package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
- package/plugins/release-notes/scripts/prompt.py +90 -0
- package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
- package/plugins/release-notes/workflows/release-notes.yml +76 -0
- package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/README.md +217 -0
- package/plugins/vulnerability-remediation/action.yml +187 -0
- package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
- package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
- package/pyproject.toml +12 -0
- package/release-please-config.json +16 -0
- package/scripts/sync_extensions.py +494 -0
- package/scripts/sync_openhands_sdk_skill.py +264 -0
- package/skills/README.md +159 -0
- package/skills/add-javadoc/.plugin/plugin.json +18 -0
- package/skills/add-javadoc/README.md +40 -0
- package/skills/add-javadoc/SKILL.md +35 -0
- package/skills/add-javadoc/references/example.md +32 -0
- package/skills/add-skill/.plugin/plugin.json +18 -0
- package/skills/add-skill/README.md +67 -0
- package/skills/add-skill/SKILL.md +47 -0
- package/skills/add-skill/scripts/fetch_skill.py +259 -0
- package/skills/agent-creator/.plugin/plugin.json +20 -0
- package/skills/agent-creator/README.md +104 -0
- package/skills/agent-creator/SKILL.md +190 -0
- package/skills/agent-creator/commands/agent-creator.md +8 -0
- package/skills/agent-creator/references/fallback.md +117 -0
- package/skills/agent-memory/.plugin/plugin.json +18 -0
- package/skills/agent-memory/README.md +35 -0
- package/skills/agent-memory/SKILL.md +30 -0
- package/skills/agent-memory/commands/remember.md +8 -0
- package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
- package/skills/agent-sdk-builder/README.md +40 -0
- package/skills/agent-sdk-builder/SKILL.md +37 -0
- package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
- package/skills/azure-devops/.plugin/plugin.json +18 -0
- package/skills/azure-devops/README.md +55 -0
- package/skills/azure-devops/SKILL.md +50 -0
- package/skills/bitbucket/.plugin/plugin.json +17 -0
- package/skills/bitbucket/README.md +50 -0
- package/skills/bitbucket/SKILL.md +45 -0
- package/skills/code-review/.plugin/plugin.json +19 -0
- package/skills/code-review/README.md +18 -0
- package/skills/code-review/SKILL.md +208 -0
- package/skills/code-review/commands/codereview-roasted.md +8 -0
- package/skills/code-review/commands/codereview.md +8 -0
- package/skills/code-review/references/risk-evaluation.md +41 -0
- package/skills/code-review/references/supply-chain-security.md +31 -0
- package/skills/code-simplifier/.plugin/plugin.json +21 -0
- package/skills/code-simplifier/README.md +30 -0
- package/skills/code-simplifier/SKILL.md +91 -0
- package/skills/code-simplifier/commands/simplify.md +8 -0
- package/skills/code-simplifier/references/code-quality-review.md +86 -0
- package/skills/code-simplifier/references/code-reuse-review.md +63 -0
- package/skills/code-simplifier/references/efficiency-review.md +81 -0
- package/skills/datadog/.plugin/plugin.json +19 -0
- package/skills/datadog/README.md +100 -0
- package/skills/datadog/SKILL.md +95 -0
- package/skills/deno/.plugin/plugin.json +18 -0
- package/skills/deno/README.md +5 -0
- package/skills/deno/SKILL.md +99 -0
- package/skills/deno/references/README.md +6 -0
- package/skills/discord/.plugin/plugin.json +18 -0
- package/skills/discord/README.md +31 -0
- package/skills/discord/SKILL.md +109 -0
- package/skills/discord/__init__.py +0 -0
- package/skills/discord/references/REFERENCE.md +78 -0
- package/skills/discord/scripts/__init__.py +0 -0
- package/skills/discord/scripts/_http.py +127 -0
- package/skills/discord/scripts/post_webhook.py +106 -0
- package/skills/discord/scripts/send_message.py +102 -0
- package/skills/docker/.plugin/plugin.json +17 -0
- package/skills/docker/README.md +34 -0
- package/skills/docker/SKILL.md +29 -0
- package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
- package/skills/evidence-based-citations/README.md +31 -0
- package/skills/evidence-based-citations/SKILL.md +59 -0
- package/skills/flarglebargle/.plugin/plugin.json +16 -0
- package/skills/flarglebargle/README.md +14 -0
- package/skills/flarglebargle/SKILL.md +9 -0
- package/skills/frontend-design/.plugin/plugin.json +21 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/README.md +42 -0
- package/skills/frontend-design/SKILL.md +42 -0
- package/skills/github/.plugin/plugin.json +19 -0
- package/skills/github/README.md +42 -0
- package/skills/github/SKILL.md +106 -0
- package/skills/github-pr-review/.plugin/plugin.json +18 -0
- package/skills/github-pr-review/README.md +145 -0
- package/skills/github-pr-review/SKILL.md +148 -0
- package/skills/github-pr-review/commands/github-pr-review.md +8 -0
- package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
- package/skills/github-pr-reviewer/README.md +34 -0
- package/skills/github-pr-reviewer/SKILL.md +89 -0
- package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
- package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
- package/skills/github-repo-monitor/README.md +70 -0
- package/skills/github-repo-monitor/SKILL.md +316 -0
- package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
- package/skills/github-repo-monitor/references/github-api.md +241 -0
- package/skills/github-repo-monitor/references/state-schema.md +160 -0
- package/skills/github-repo-monitor/scripts/main.py +915 -0
- package/skills/github-repo-monitor/tests/test_main.py +400 -0
- package/skills/gitlab/.plugin/plugin.json +17 -0
- package/skills/gitlab/README.md +37 -0
- package/skills/gitlab/SKILL.md +32 -0
- package/skills/incident-retrospective/.plugin/plugin.json +21 -0
- package/skills/incident-retrospective/README.md +34 -0
- package/skills/incident-retrospective/SKILL.md +98 -0
- package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
- package/skills/iterate/.plugin/plugin.json +13 -0
- package/skills/iterate/README.md +25 -0
- package/skills/iterate/SKILL.md +399 -0
- package/skills/iterate/commands/babysit.md +8 -0
- package/skills/iterate/commands/iterate.md +8 -0
- package/skills/iterate/commands/verify.md +8 -0
- package/skills/iterate/references/heuristics.md +58 -0
- package/skills/iterate/references/verification.md +96 -0
- package/skills/jupyter/.plugin/plugin.json +18 -0
- package/skills/jupyter/README.md +55 -0
- package/skills/jupyter/SKILL.md +50 -0
- package/skills/kubernetes/.plugin/plugin.json +18 -0
- package/skills/kubernetes/README.md +53 -0
- package/skills/kubernetes/SKILL.md +48 -0
- package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
- package/skills/learn-from-code-review/README.md +64 -0
- package/skills/learn-from-code-review/SKILL.md +186 -0
- package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
- package/skills/linear/.plugin/plugin.json +19 -0
- package/skills/linear/README.md +58 -0
- package/skills/linear/SKILL.md +213 -0
- package/skills/linear-triage/.plugin/plugin.json +21 -0
- package/skills/linear-triage/README.md +34 -0
- package/skills/linear-triage/SKILL.md +91 -0
- package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
- package/skills/notion/.plugin/plugin.json +17 -0
- package/skills/notion/README.md +114 -0
- package/skills/notion/SKILL.md +109 -0
- package/skills/npm/.plugin/plugin.json +17 -0
- package/skills/npm/README.md +14 -0
- package/skills/npm/SKILL.md +9 -0
- package/skills/openhands-api/.plugin/plugin.json +22 -0
- package/skills/openhands-api/README.md +48 -0
- package/skills/openhands-api/SKILL.md +399 -0
- package/skills/openhands-api/references/README.md +33 -0
- package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
- package/skills/openhands-api/references/example_prompt.md +12 -0
- package/skills/openhands-api/scripts/openhands_api.py +606 -0
- package/skills/openhands-api/scripts/openhands_api.ts +252 -0
- package/skills/openhands-automation/.plugin/plugin.json +19 -0
- package/skills/openhands-automation/README.md +89 -0
- package/skills/openhands-automation/SKILL.md +875 -0
- package/skills/openhands-automation/commands/automation:create.md +8 -0
- package/skills/openhands-automation/references/ab-testing.md +185 -0
- package/skills/openhands-automation/references/custom-automation.md +644 -0
- package/skills/openhands-sdk/.plugin/plugin.json +20 -0
- package/skills/openhands-sdk/README.md +22 -0
- package/skills/openhands-sdk/SKILL.md +229 -0
- package/skills/openhands-sdk/commands/sdk.md +8 -0
- package/skills/pdflatex/.plugin/plugin.json +18 -0
- package/skills/pdflatex/README.md +39 -0
- package/skills/pdflatex/SKILL.md +34 -0
- package/skills/prd/.plugin/plugin.json +19 -0
- package/skills/prd/README.md +28 -0
- package/skills/prd/SKILL.md +237 -0
- package/skills/prd/commands/prd.md +8 -0
- package/skills/qa-changes/README.md +18 -0
- package/skills/qa-changes/SKILL.md +229 -0
- package/skills/qa-changes/commands/qa-changes.md +8 -0
- package/skills/release-notes/README.md +24 -0
- package/skills/release-notes/SKILL.md +19 -0
- package/skills/release-notes/commands/release-notes.md +8 -0
- package/skills/research-brief/.plugin/plugin.json +20 -0
- package/skills/research-brief/README.md +34 -0
- package/skills/research-brief/SKILL.md +99 -0
- package/skills/research-brief/commands/research-brief:setup.md +8 -0
- package/skills/security/.plugin/plugin.json +18 -0
- package/skills/security/README.md +38 -0
- package/skills/security/SKILL.md +33 -0
- package/skills/skill-creator/.plugin/plugin.json +17 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/README.md +182 -0
- package/skills/skill-creator/SKILL.md +545 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/init_skill.py +303 -0
- package/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
- package/skills/slack-channel-monitor/README.md +91 -0
- package/skills/slack-channel-monitor/SKILL.md +276 -0
- package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
- package/skills/slack-channel-monitor/references/slack-api.md +207 -0
- package/skills/slack-channel-monitor/references/state-schema.md +180 -0
- package/skills/slack-channel-monitor/scripts/main.py +962 -0
- package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
- package/skills/slack-standup-digest/README.md +34 -0
- package/skills/slack-standup-digest/SKILL.md +92 -0
- package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
- package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
- package/skills/spark-version-upgrade/README.md +54 -0
- package/skills/spark-version-upgrade/SKILL.md +233 -0
- package/skills/ssh/.plugin/plugin.json +18 -0
- package/skills/ssh/README.md +140 -0
- package/skills/ssh/SKILL.md +135 -0
- package/skills/swift-linux/.plugin/plugin.json +17 -0
- package/skills/swift-linux/README.md +86 -0
- package/skills/swift-linux/SKILL.md +81 -0
- package/skills/theme-factory/.plugin/plugin.json +19 -0
- package/skills/theme-factory/LICENSE.txt +202 -0
- package/skills/theme-factory/README.md +58 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/uv/.plugin/plugin.json +18 -0
- package/skills/uv/README.md +5 -0
- package/skills/uv/SKILL.md +95 -0
- package/skills/uv/references/README.md +5 -0
- package/skills/vercel/.plugin/plugin.json +18 -0
- package/skills/vercel/README.md +108 -0
- package/skills/vercel/SKILL.md +103 -0
- package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
- package/tests/test_catalogs.py +109 -0
- package/tests/test_code_review_risk_evaluation.py +94 -0
- package/tests/test_issue_duplicate_checker.py +240 -0
- package/tests/test_openhands_api_python.py +152 -0
- package/tests/test_plugin_manifest.py +83 -0
- package/tests/test_pr_review_diff_payload.py +202 -0
- package/tests/test_pr_review_feedback.py +263 -0
- package/tests/test_pr_review_prompt.py +152 -0
- package/tests/test_pr_review_review_context.py +253 -0
- package/tests/test_qa_changes.py +232 -0
- package/tests/test_qa_changes_evaluation.py +259 -0
- package/tests/test_release_notes_generator.py +990 -0
- package/tests/test_sdk_loading.py +150 -0
- package/tests/test_skill_plugin_loading.py +149 -0
- package/tests/test_skills_have_readme.py +66 -0
- package/tests/test_sync_extensions.py +292 -0
- package/tests/test_workflow_sync.py +46 -0
- package/utils/analysis/README.md +7 -0
- package/utils/analysis/laminar_signals/README.md +211 -0
- package/utils/analysis/laminar_signals/analyze.py +780 -0
- package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
- package/utils/analysis/laminar_signals/templates/pr_review.j2 +61 -0
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
"""Tests for the release notes generator plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
# Add the plugin scripts directory to the path
|
|
12
|
+
plugin_dir = Path(__file__).parent.parent / "plugins" / "release-notes" / "scripts"
|
|
13
|
+
sys.path.insert(0, str(plugin_dir))
|
|
14
|
+
|
|
15
|
+
from generate_release_notes import (
|
|
16
|
+
CATEGORIES,
|
|
17
|
+
Change,
|
|
18
|
+
Contributor,
|
|
19
|
+
ReleaseNotes,
|
|
20
|
+
_dedupe_changes,
|
|
21
|
+
_process_commit,
|
|
22
|
+
_process_contributors,
|
|
23
|
+
categorize_change,
|
|
24
|
+
get_commits_between_tags,
|
|
25
|
+
is_new_contributor,
|
|
26
|
+
)
|
|
27
|
+
from prompt import format_prompt
|
|
28
|
+
from validate_release_notes import (
|
|
29
|
+
ReleaseNotesValidationError,
|
|
30
|
+
append_reference_coverage_appendix,
|
|
31
|
+
format_coverage_summary,
|
|
32
|
+
missing_references,
|
|
33
|
+
validate_release_notes_markdown,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestChange:
|
|
38
|
+
"""Tests for the Change dataclass."""
|
|
39
|
+
|
|
40
|
+
def test_to_markdown_basic(self):
|
|
41
|
+
"""Test basic markdown formatting."""
|
|
42
|
+
change = Change(
|
|
43
|
+
message="Add new feature",
|
|
44
|
+
sha="abc123",
|
|
45
|
+
author="testuser",
|
|
46
|
+
)
|
|
47
|
+
result = change.to_markdown("owner/repo")
|
|
48
|
+
assert result == "- Add new feature @testuser"
|
|
49
|
+
|
|
50
|
+
def test_to_markdown_with_pr(self):
|
|
51
|
+
"""Test markdown formatting with PR number."""
|
|
52
|
+
change = Change(
|
|
53
|
+
message="Resolve memory leak",
|
|
54
|
+
sha="abc123",
|
|
55
|
+
author="testuser",
|
|
56
|
+
pr_number=42,
|
|
57
|
+
)
|
|
58
|
+
result = change.to_markdown("owner/repo")
|
|
59
|
+
assert result == "- Resolve memory leak (#42) @testuser"
|
|
60
|
+
|
|
61
|
+
def test_to_markdown_strips_conventional_commit_prefix(self):
|
|
62
|
+
"""Test that conventional commit prefixes are stripped."""
|
|
63
|
+
change = Change(
|
|
64
|
+
message="feat: Add new feature",
|
|
65
|
+
sha="abc123",
|
|
66
|
+
author="testuser",
|
|
67
|
+
pr_number=42,
|
|
68
|
+
)
|
|
69
|
+
result = change.to_markdown("owner/repo")
|
|
70
|
+
assert "feat:" not in result
|
|
71
|
+
assert "Add new feature" in result
|
|
72
|
+
|
|
73
|
+
def test_to_markdown_strips_scoped_prefix(self):
|
|
74
|
+
"""Test that scoped conventional commit prefixes are stripped."""
|
|
75
|
+
change = Change(
|
|
76
|
+
message="fix(api): Resolve timeout issue",
|
|
77
|
+
sha="abc123",
|
|
78
|
+
author="testuser",
|
|
79
|
+
)
|
|
80
|
+
result = change.to_markdown("owner/repo")
|
|
81
|
+
assert "fix(api):" not in result
|
|
82
|
+
assert "Resolve timeout issue" in result
|
|
83
|
+
|
|
84
|
+
def test_to_markdown_capitalizes_first_letter(self):
|
|
85
|
+
"""Test that the first letter is capitalized."""
|
|
86
|
+
change = Change(
|
|
87
|
+
message="fix: lower case message",
|
|
88
|
+
sha="abc123",
|
|
89
|
+
author="testuser",
|
|
90
|
+
)
|
|
91
|
+
result = change.to_markdown("owner/repo")
|
|
92
|
+
assert "Lower case message" in result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestCategorizeChange:
|
|
96
|
+
"""Tests for the categorize_change function."""
|
|
97
|
+
|
|
98
|
+
def test_categorize_feat_prefix(self):
|
|
99
|
+
"""Test categorization of feat: prefix."""
|
|
100
|
+
change = Change(message="feat: Add new API", sha="abc", author="user")
|
|
101
|
+
assert categorize_change(change) == "features"
|
|
102
|
+
|
|
103
|
+
def test_categorize_feature_prefix(self):
|
|
104
|
+
"""Test categorization of feature: prefix."""
|
|
105
|
+
change = Change(message="feature: Add new API", sha="abc", author="user")
|
|
106
|
+
assert categorize_change(change) == "features"
|
|
107
|
+
|
|
108
|
+
def test_categorize_fix_prefix(self):
|
|
109
|
+
"""Test categorization of fix: prefix."""
|
|
110
|
+
change = Change(message="fix: Resolve crash", sha="abc", author="user")
|
|
111
|
+
assert categorize_change(change) == "fixes"
|
|
112
|
+
|
|
113
|
+
def test_categorize_docs_prefix(self):
|
|
114
|
+
"""Test categorization of docs: prefix."""
|
|
115
|
+
change = Change(message="docs: Update README", sha="abc", author="user")
|
|
116
|
+
assert categorize_change(change) == "docs"
|
|
117
|
+
|
|
118
|
+
def test_categorize_chore_prefix(self):
|
|
119
|
+
"""Test categorization of chore: prefix."""
|
|
120
|
+
change = Change(message="chore: Update dependencies", sha="abc", author="user")
|
|
121
|
+
assert categorize_change(change) == "internal"
|
|
122
|
+
|
|
123
|
+
def test_categorize_ci_prefix(self):
|
|
124
|
+
"""Test categorization of ci: prefix."""
|
|
125
|
+
change = Change(message="ci: Add GitHub Actions", sha="abc", author="user")
|
|
126
|
+
assert categorize_change(change) == "internal"
|
|
127
|
+
|
|
128
|
+
def test_categorize_breaking_prefix(self):
|
|
129
|
+
"""Test categorization of BREAKING: prefix."""
|
|
130
|
+
change = Change(message="BREAKING: Remove deprecated API", sha="abc", author="user")
|
|
131
|
+
assert categorize_change(change) == "breaking"
|
|
132
|
+
|
|
133
|
+
def test_categorize_by_label_enhancement(self):
|
|
134
|
+
"""Test categorization by enhancement label."""
|
|
135
|
+
change = Change(
|
|
136
|
+
message="Add feature",
|
|
137
|
+
sha="abc",
|
|
138
|
+
author="user",
|
|
139
|
+
pr_labels=["enhancement"],
|
|
140
|
+
)
|
|
141
|
+
assert categorize_change(change) == "features"
|
|
142
|
+
|
|
143
|
+
def test_categorize_by_label_bug(self):
|
|
144
|
+
"""Test categorization by bug label."""
|
|
145
|
+
change = Change(
|
|
146
|
+
message="Fix something",
|
|
147
|
+
sha="abc",
|
|
148
|
+
author="user",
|
|
149
|
+
pr_labels=["bug"],
|
|
150
|
+
)
|
|
151
|
+
assert categorize_change(change) == "fixes"
|
|
152
|
+
|
|
153
|
+
def test_categorize_by_label_breaking_change(self):
|
|
154
|
+
"""Test categorization by breaking-change label."""
|
|
155
|
+
change = Change(
|
|
156
|
+
message="Change API",
|
|
157
|
+
sha="abc",
|
|
158
|
+
author="user",
|
|
159
|
+
pr_labels=["breaking-change"],
|
|
160
|
+
)
|
|
161
|
+
assert categorize_change(change) == "breaking"
|
|
162
|
+
|
|
163
|
+
def test_categorize_uncategorized(self):
|
|
164
|
+
"""Test uncategorized changes fall to other."""
|
|
165
|
+
change = Change(message="Random change", sha="abc", author="user")
|
|
166
|
+
assert categorize_change(change) == "other"
|
|
167
|
+
|
|
168
|
+
def test_commit_prefix_takes_precedence_over_label(self):
|
|
169
|
+
"""Test that commit prefix categorization takes precedence."""
|
|
170
|
+
change = Change(
|
|
171
|
+
message="feat: Add feature",
|
|
172
|
+
sha="abc",
|
|
173
|
+
author="user",
|
|
174
|
+
pr_labels=["bug"], # Conflicting label
|
|
175
|
+
)
|
|
176
|
+
# Should be categorized as feature based on prefix
|
|
177
|
+
assert categorize_change(change) == "features"
|
|
178
|
+
|
|
179
|
+
def test_keyword_categorizes_docs(self):
|
|
180
|
+
"""Test docs keyword fallback categorization."""
|
|
181
|
+
change = Change(
|
|
182
|
+
message="Update documentation with new examples",
|
|
183
|
+
sha="abc",
|
|
184
|
+
author="user",
|
|
185
|
+
)
|
|
186
|
+
assert categorize_change(change) == "docs"
|
|
187
|
+
|
|
188
|
+
def test_keyword_categorizes_internal_before_feature(self):
|
|
189
|
+
"""Test internal keyword fallback takes precedence over generic feature verbs."""
|
|
190
|
+
change = Change(
|
|
191
|
+
message="Add extensive typing to controller directory",
|
|
192
|
+
sha="abc",
|
|
193
|
+
author="user",
|
|
194
|
+
)
|
|
195
|
+
assert categorize_change(change) == "internal"
|
|
196
|
+
|
|
197
|
+
def test_keyword_categorizes_fixes(self):
|
|
198
|
+
"""Test fix keyword fallback categorization."""
|
|
199
|
+
change = Change(
|
|
200
|
+
message="Resolve timeout error in session reconnect",
|
|
201
|
+
sha="abc",
|
|
202
|
+
author="user",
|
|
203
|
+
)
|
|
204
|
+
assert categorize_change(change) == "fixes"
|
|
205
|
+
|
|
206
|
+
def test_keyword_categorizes_features(self):
|
|
207
|
+
"""Test feature keyword fallback categorization."""
|
|
208
|
+
change = Change(
|
|
209
|
+
message="Add VS Code tab alongside the terminal",
|
|
210
|
+
sha="abc",
|
|
211
|
+
author="user",
|
|
212
|
+
)
|
|
213
|
+
assert categorize_change(change) == "features"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TestReleaseNotes:
|
|
217
|
+
"""Tests for the ReleaseNotes dataclass."""
|
|
218
|
+
|
|
219
|
+
def test_to_markdown_basic(self):
|
|
220
|
+
"""Test basic release notes generation."""
|
|
221
|
+
notes = ReleaseNotes(
|
|
222
|
+
tag="v1.0.0",
|
|
223
|
+
previous_tag="v0.9.0",
|
|
224
|
+
date="2026-03-06",
|
|
225
|
+
repo_name="owner/repo",
|
|
226
|
+
changes={
|
|
227
|
+
"features": [
|
|
228
|
+
Change(message="Add feature", sha="abc", author="user1", pr_number=1)
|
|
229
|
+
],
|
|
230
|
+
"fixes": [
|
|
231
|
+
Change(message="Fix bug", sha="def", author="user2", pr_number=2)
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
markdown = notes.to_markdown()
|
|
236
|
+
|
|
237
|
+
assert "## [v1.0.0] - 2026-03-06" in markdown
|
|
238
|
+
assert "### ✨ New Features" in markdown
|
|
239
|
+
assert "### 🐛 Bug Fixes" in markdown
|
|
240
|
+
assert "Add feature (#1) @user1" in markdown
|
|
241
|
+
assert "(#2) @user2" in markdown # Fix bug becomes Bug due to prefix stripping
|
|
242
|
+
assert "compare/v0.9.0...v1.0.0" in markdown
|
|
243
|
+
|
|
244
|
+
def test_to_markdown_with_breaking_changes(self):
|
|
245
|
+
"""Test release notes with breaking changes."""
|
|
246
|
+
notes = ReleaseNotes(
|
|
247
|
+
tag="v2.0.0",
|
|
248
|
+
previous_tag="v1.0.0",
|
|
249
|
+
date="2026-03-06",
|
|
250
|
+
repo_name="owner/repo",
|
|
251
|
+
changes={
|
|
252
|
+
"breaking": [
|
|
253
|
+
Change(message="Remove API", sha="abc", author="user", pr_number=1)
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
markdown = notes.to_markdown()
|
|
258
|
+
|
|
259
|
+
assert "### ⚠️ Breaking Changes" in markdown
|
|
260
|
+
assert "Remove API" in markdown
|
|
261
|
+
|
|
262
|
+
def test_to_markdown_with_new_contributors(self):
|
|
263
|
+
"""Test release notes with new contributors."""
|
|
264
|
+
notes = ReleaseNotes(
|
|
265
|
+
tag="v1.0.0",
|
|
266
|
+
previous_tag="v0.9.0",
|
|
267
|
+
date="2026-03-06",
|
|
268
|
+
repo_name="owner/repo",
|
|
269
|
+
changes={},
|
|
270
|
+
new_contributors=[
|
|
271
|
+
Contributor(username="newuser", first_pr=42, is_new=True),
|
|
272
|
+
],
|
|
273
|
+
)
|
|
274
|
+
markdown = notes.to_markdown()
|
|
275
|
+
|
|
276
|
+
assert "### 👥 New Contributors" in markdown
|
|
277
|
+
assert "@newuser made their first contribution in #42" in markdown
|
|
278
|
+
|
|
279
|
+
def test_to_markdown_internal_excluded_by_default(self):
|
|
280
|
+
"""Test that internal changes are excluded by default."""
|
|
281
|
+
notes = ReleaseNotes(
|
|
282
|
+
tag="v1.0.0",
|
|
283
|
+
previous_tag="v0.9.0",
|
|
284
|
+
date="2026-03-06",
|
|
285
|
+
repo_name="owner/repo",
|
|
286
|
+
changes={
|
|
287
|
+
"internal": [
|
|
288
|
+
Change(message="Update CI", sha="abc", author="user", pr_number=1)
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
markdown = notes.to_markdown(include_internal=False)
|
|
293
|
+
|
|
294
|
+
assert "Internal" not in markdown
|
|
295
|
+
|
|
296
|
+
def test_to_markdown_internal_included_when_requested(self):
|
|
297
|
+
"""Test that internal changes are included when requested."""
|
|
298
|
+
notes = ReleaseNotes(
|
|
299
|
+
tag="v1.0.0",
|
|
300
|
+
previous_tag="v0.9.0",
|
|
301
|
+
date="2026-03-06",
|
|
302
|
+
repo_name="owner/repo",
|
|
303
|
+
changes={
|
|
304
|
+
"internal": [
|
|
305
|
+
Change(message="Update CI", sha="abc", author="user", pr_number=1)
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
markdown = notes.to_markdown(include_internal=True)
|
|
310
|
+
|
|
311
|
+
assert "### 🏗️ Internal/Infrastructure" in markdown
|
|
312
|
+
assert "Update CI" in markdown
|
|
313
|
+
|
|
314
|
+
def test_to_markdown_omits_other_changes(self):
|
|
315
|
+
"""Test that uncategorized changes are omitted for a more concise summary."""
|
|
316
|
+
notes = ReleaseNotes(
|
|
317
|
+
tag="v1.0.0",
|
|
318
|
+
previous_tag="v0.9.0",
|
|
319
|
+
date="2026-03-06",
|
|
320
|
+
repo_name="owner/repo",
|
|
321
|
+
changes={
|
|
322
|
+
"other": [
|
|
323
|
+
Change(message="Random internal cleanup", sha="abc", author="user")
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
markdown = notes.to_markdown()
|
|
328
|
+
|
|
329
|
+
assert "Other Changes" not in markdown
|
|
330
|
+
assert "Random internal cleanup" not in markdown
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class TestProcessingHelpers:
|
|
334
|
+
"""Tests for processing helpers."""
|
|
335
|
+
|
|
336
|
+
def test_dedupe_changes_collapses_multiple_commits_from_same_pr(self):
|
|
337
|
+
"""Test that only one entry is kept per PR."""
|
|
338
|
+
changes = [
|
|
339
|
+
Change(message="First commit", sha="abc", author="user", pr_number=10),
|
|
340
|
+
Change(message="Second commit", sha="def", author="user", pr_number=10),
|
|
341
|
+
Change(message="Standalone commit", sha="ghi", author="user"),
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
deduped = _dedupe_changes(changes)
|
|
345
|
+
|
|
346
|
+
assert [change.pr_number for change in deduped] == [10, None]
|
|
347
|
+
assert [change.sha for change in deduped] == ["abc", "ghi"]
|
|
348
|
+
|
|
349
|
+
@patch("generate_release_notes.get_pr_for_commit")
|
|
350
|
+
def test_process_commit_prefers_pr_title_and_author(self, mock_get_pr_for_commit):
|
|
351
|
+
"""Test that PR metadata is preferred for user-facing release note entries."""
|
|
352
|
+
mock_get_pr_for_commit.return_value = {
|
|
353
|
+
"number": 42,
|
|
354
|
+
"title": "Add settings page",
|
|
355
|
+
"body": "Adds a new settings page for managing preferences.",
|
|
356
|
+
"html_url": "https://github.com/owner/repo/pull/42",
|
|
357
|
+
"labels": [{"name": "enhancement"}],
|
|
358
|
+
"user": {"login": "pr-author", "type": "User"},
|
|
359
|
+
"created_at": "2026-03-05T12:00:00Z",
|
|
360
|
+
"merged_at": "2026-03-06T09:30:00Z",
|
|
361
|
+
}
|
|
362
|
+
commit = {
|
|
363
|
+
"sha": "abc123",
|
|
364
|
+
"commit": {"message": "feat: Low-level implementation detail\n\nMore text"},
|
|
365
|
+
"author": {"login": "commit-author"},
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
change = _process_commit(commit, "owner/repo", "token")
|
|
369
|
+
|
|
370
|
+
assert change is not None
|
|
371
|
+
assert change.message == "Add settings page"
|
|
372
|
+
assert change.author == "pr-author"
|
|
373
|
+
assert change.pr_number == 42
|
|
374
|
+
assert change.pr_labels == ["enhancement"]
|
|
375
|
+
assert change.body == "Adds a new settings page for managing preferences."
|
|
376
|
+
assert change.url == "https://github.com/owner/repo/pull/42"
|
|
377
|
+
assert change.author_type == "User"
|
|
378
|
+
assert change.pr_created_at == "2026-03-05T12:00:00Z"
|
|
379
|
+
assert change.pr_merged_at == "2026-03-06T09:30:00Z"
|
|
380
|
+
|
|
381
|
+
@patch("generate_release_notes.github_api_request")
|
|
382
|
+
def test_get_commits_between_tags_warns_on_truncation(self, mock_github_api_request, capsys):
|
|
383
|
+
"""Test that compare API truncation is surfaced to users."""
|
|
384
|
+
mock_github_api_request.return_value = {
|
|
385
|
+
"total_commits": 300,
|
|
386
|
+
"commits": [{"sha": "abc"}],
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
commits = get_commits_between_tags("owner/repo", "v1.0.0", "v1.1.0", "token")
|
|
390
|
+
|
|
391
|
+
assert commits == [{"sha": "abc"}]
|
|
392
|
+
assert "truncated the commit list" in capsys.readouterr().err
|
|
393
|
+
|
|
394
|
+
@patch("generate_release_notes._search_merged_pull_requests_by_author")
|
|
395
|
+
def test_is_new_contributor_rejects_prior_merged_pr(self, mock_search):
|
|
396
|
+
"""A contributor is not new if they already merged a PR earlier."""
|
|
397
|
+
mock_search.return_value = [
|
|
398
|
+
{"number": 41, "closed_at": "2026-03-01T10:00:00Z"},
|
|
399
|
+
{"number": 42, "closed_at": "2026-03-06T09:30:00Z"},
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
assert not is_new_contributor(
|
|
403
|
+
"pr-author",
|
|
404
|
+
"owner/repo",
|
|
405
|
+
"2026-03-06T09:30:00Z",
|
|
406
|
+
"token",
|
|
407
|
+
current_pr_number=42,
|
|
408
|
+
author_type="User",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
@patch("generate_release_notes._search_merged_pull_requests_by_author")
|
|
412
|
+
def test_is_new_contributor_accepts_first_merged_pr(self, mock_search):
|
|
413
|
+
"""A contributor is new when the only merged PR found is the current one."""
|
|
414
|
+
mock_search.return_value = [{"number": 42, "closed_at": "2026-03-06T09:30:00Z"}]
|
|
415
|
+
|
|
416
|
+
assert is_new_contributor(
|
|
417
|
+
"pr-author",
|
|
418
|
+
"owner/repo",
|
|
419
|
+
"2026-03-06T09:30:00Z",
|
|
420
|
+
"token",
|
|
421
|
+
current_pr_number=42,
|
|
422
|
+
author_type="User",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
@patch("generate_release_notes._search_merged_pull_requests_by_author")
|
|
426
|
+
def test_is_new_contributor_ignores_bot_accounts(self, mock_search):
|
|
427
|
+
"""Bot-authored PRs should never appear in new contributors."""
|
|
428
|
+
assert not is_new_contributor(
|
|
429
|
+
"dependabot[bot]",
|
|
430
|
+
"owner/repo",
|
|
431
|
+
"2026-03-06T09:30:00Z",
|
|
432
|
+
"token",
|
|
433
|
+
current_pr_number=42,
|
|
434
|
+
author_type="Bot",
|
|
435
|
+
)
|
|
436
|
+
mock_search.assert_not_called()
|
|
437
|
+
|
|
438
|
+
@patch("generate_release_notes.is_new_contributor")
|
|
439
|
+
def test_process_contributors_uses_earliest_release_pr_per_author(self, mock_is_new):
|
|
440
|
+
"""Contributor detection should use the author's earliest PR in the release."""
|
|
441
|
+
mock_is_new.side_effect = lambda author, *_args, **_kwargs: author == "alice"
|
|
442
|
+
changes = [
|
|
443
|
+
Change(
|
|
444
|
+
message="Later PR",
|
|
445
|
+
sha="bbb2222",
|
|
446
|
+
author="alice",
|
|
447
|
+
pr_number=43,
|
|
448
|
+
author_type="User",
|
|
449
|
+
pr_created_at="2026-03-10T12:00:00Z",
|
|
450
|
+
pr_merged_at="2026-03-11T12:00:00Z",
|
|
451
|
+
),
|
|
452
|
+
Change(
|
|
453
|
+
message="Earlier PR",
|
|
454
|
+
sha="aaa1111",
|
|
455
|
+
author="alice",
|
|
456
|
+
pr_number=42,
|
|
457
|
+
author_type="User",
|
|
458
|
+
pr_created_at="2026-03-05T12:00:00Z",
|
|
459
|
+
pr_merged_at="2026-03-06T09:30:00Z",
|
|
460
|
+
),
|
|
461
|
+
Change(
|
|
462
|
+
message="Bot PR",
|
|
463
|
+
sha="ccc3333",
|
|
464
|
+
author="dependabot[bot]",
|
|
465
|
+
pr_number=44,
|
|
466
|
+
author_type="Bot",
|
|
467
|
+
pr_created_at="2026-03-07T12:00:00Z",
|
|
468
|
+
pr_merged_at="2026-03-08T12:00:00Z",
|
|
469
|
+
),
|
|
470
|
+
]
|
|
471
|
+
|
|
472
|
+
contributors, new_contributors = _process_contributors(changes, "owner/repo", "token")
|
|
473
|
+
|
|
474
|
+
assert [contributor.username for contributor in contributors] == ["alice", "dependabot[bot]"]
|
|
475
|
+
assert [contributor.first_pr for contributor in contributors] == [42, 44]
|
|
476
|
+
assert [contributor.username for contributor in new_contributors] == ["alice"]
|
|
477
|
+
mock_is_new.assert_any_call(
|
|
478
|
+
"alice",
|
|
479
|
+
"owner/repo",
|
|
480
|
+
"2026-03-06T09:30:00Z",
|
|
481
|
+
"token",
|
|
482
|
+
current_pr_number=42,
|
|
483
|
+
author_type="User",
|
|
484
|
+
)
|
|
485
|
+
mock_is_new.assert_any_call(
|
|
486
|
+
"dependabot[bot]",
|
|
487
|
+
"owner/repo",
|
|
488
|
+
"2026-03-08T12:00:00Z",
|
|
489
|
+
"token",
|
|
490
|
+
current_pr_number=44,
|
|
491
|
+
author_type="Bot",
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class TestPrompt:
|
|
496
|
+
"""Tests for the release-notes agent prompt."""
|
|
497
|
+
|
|
498
|
+
def test_format_prompt_includes_editorial_instructions(self):
|
|
499
|
+
"""Test that the prompt tells the agent to make editorial judgments."""
|
|
500
|
+
prompt = format_prompt(
|
|
501
|
+
repo_name="owner/repo",
|
|
502
|
+
tag="v1.2.0",
|
|
503
|
+
previous_tag="v1.1.0",
|
|
504
|
+
date="2026-03-07",
|
|
505
|
+
commit_count=12,
|
|
506
|
+
include_internal=False,
|
|
507
|
+
output_format="release",
|
|
508
|
+
full_changelog_url="https://github.com/owner/repo/compare/v1.1.0...v1.2.0",
|
|
509
|
+
change_candidates="- Ref: #42\n Title: Add dark mode",
|
|
510
|
+
new_contributors="- @new-user made their first contribution in #42",
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
assert "Write official release notes for `owner/repo` tag `v1.2.0`." in prompt
|
|
514
|
+
assert "decide which PRs are important enough to mention" in prompt
|
|
515
|
+
assert "group related PRs into a single bullet" in prompt
|
|
516
|
+
assert "aggressively compress the notes into a shorter set of higher-signal bullets" in prompt
|
|
517
|
+
assert "if a section would have more than 5 bullets" in prompt
|
|
518
|
+
assert "omit trivial, repetitive, or low-signal changes" in prompt
|
|
519
|
+
assert "prefer end-user impact over implementation detail" in prompt
|
|
520
|
+
assert "prioritize public APIs, user-visible capabilities, security fixes" in prompt
|
|
521
|
+
assert "treat toolkit-maintainer or contributor-facing changes as secondary" in prompt
|
|
522
|
+
assert "should stay in the small/internal appendix unless they are unusually significant" in prompt
|
|
523
|
+
assert "public API additions still belong in `### ✨ New Features`" in prompt
|
|
524
|
+
assert "omit prompt wording, benchmark plumbing, workflow maintenance" in prompt
|
|
525
|
+
assert "start with a short, conversational 1-2 sentence overview" in prompt
|
|
526
|
+
assert "optional top-level highlight bullets (maximum 3)" in prompt
|
|
527
|
+
assert "every change bullet must end with explicit references" in prompt
|
|
528
|
+
assert "format PR references as `(#123) @username`" in prompt
|
|
529
|
+
assert "include every new contributor listed below" in prompt
|
|
530
|
+
assert "Current tag: v1.2.0" in prompt
|
|
531
|
+
assert "Full changelog URL:" in prompt
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class TestValidation:
|
|
535
|
+
"""Tests for release notes attribution validation."""
|
|
536
|
+
|
|
537
|
+
def test_validate_release_notes_accepts_grouped_prs_with_authors(self):
|
|
538
|
+
"""Grouped bullets are allowed when every PR and author is listed."""
|
|
539
|
+
notes = ReleaseNotes(
|
|
540
|
+
tag="v1.2.0",
|
|
541
|
+
previous_tag="v1.1.0",
|
|
542
|
+
date="2026-03-07",
|
|
543
|
+
repo_name="owner/repo",
|
|
544
|
+
changes={
|
|
545
|
+
"features": [
|
|
546
|
+
Change(message="Add dark mode", sha="abc1234", author="alice", pr_number=42),
|
|
547
|
+
Change(message="Add theme presets", sha="def5678", author="bob", pr_number=43),
|
|
548
|
+
]
|
|
549
|
+
},
|
|
550
|
+
)
|
|
551
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
552
|
+
|
|
553
|
+
### ✨ New Features
|
|
554
|
+
- Add appearance improvements (#42) @alice, (#43) @bob
|
|
555
|
+
|
|
556
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
summary = validate_release_notes_markdown(markdown, notes)
|
|
560
|
+
|
|
561
|
+
assert summary.bullet_count == 1
|
|
562
|
+
assert summary.referenced_prs == [42, 43]
|
|
563
|
+
assert summary.referenced_authors == ["alice", "bob"]
|
|
564
|
+
|
|
565
|
+
def test_validate_release_notes_rejects_missing_author(self):
|
|
566
|
+
"""Each referenced PR must include the matching author handle."""
|
|
567
|
+
notes = ReleaseNotes(
|
|
568
|
+
tag="v1.2.0",
|
|
569
|
+
previous_tag="v1.1.0",
|
|
570
|
+
date="2026-03-07",
|
|
571
|
+
repo_name="owner/repo",
|
|
572
|
+
changes={
|
|
573
|
+
"fixes": [
|
|
574
|
+
Change(message="Fix reconnect bug", sha="abc1234", author="alice", pr_number=42),
|
|
575
|
+
]
|
|
576
|
+
},
|
|
577
|
+
)
|
|
578
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
579
|
+
|
|
580
|
+
### 🐛 Bug Fixes
|
|
581
|
+
- Fix reconnect bug (#42)
|
|
582
|
+
|
|
583
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
with pytest.raises(ReleaseNotesValidationError, match=r"missing @alice"):
|
|
587
|
+
validate_release_notes_markdown(markdown, notes)
|
|
588
|
+
|
|
589
|
+
def test_validate_release_notes_rejects_missing_refs(self):
|
|
590
|
+
"""Each change bullet must contain explicit references."""
|
|
591
|
+
notes = ReleaseNotes(
|
|
592
|
+
tag="v1.2.0",
|
|
593
|
+
previous_tag="v1.1.0",
|
|
594
|
+
date="2026-03-07",
|
|
595
|
+
repo_name="owner/repo",
|
|
596
|
+
changes={
|
|
597
|
+
"features": [
|
|
598
|
+
Change(message="Add dark mode", sha="abc1234", author="alice", pr_number=42),
|
|
599
|
+
]
|
|
600
|
+
},
|
|
601
|
+
)
|
|
602
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
603
|
+
|
|
604
|
+
### ✨ New Features
|
|
605
|
+
- Add dark mode @alice
|
|
606
|
+
|
|
607
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
with pytest.raises(
|
|
611
|
+
ReleaseNotesValidationError,
|
|
612
|
+
match=r"Bullet missing explicit PR/commit references",
|
|
613
|
+
):
|
|
614
|
+
validate_release_notes_markdown(markdown, notes)
|
|
615
|
+
|
|
616
|
+
def test_validate_release_notes_accepts_standalone_commit_refs(self):
|
|
617
|
+
"""Standalone commits can be referenced by short SHA plus author."""
|
|
618
|
+
notes = ReleaseNotes(
|
|
619
|
+
tag="v1.2.0",
|
|
620
|
+
previous_tag="v1.1.0",
|
|
621
|
+
date="2026-03-07",
|
|
622
|
+
repo_name="owner/repo",
|
|
623
|
+
changes={
|
|
624
|
+
"docs": [
|
|
625
|
+
Change(message="Update docs", sha="abc1234fedcba", author="alice"),
|
|
626
|
+
]
|
|
627
|
+
},
|
|
628
|
+
)
|
|
629
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
630
|
+
|
|
631
|
+
### 📚 Documentation
|
|
632
|
+
- Update docs (abc1234) @alice
|
|
633
|
+
|
|
634
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
summary = validate_release_notes_markdown(markdown, notes)
|
|
638
|
+
|
|
639
|
+
assert summary.referenced_commits == ["abc1234"]
|
|
640
|
+
assert summary.referenced_authors == ["alice"]
|
|
641
|
+
|
|
642
|
+
def test_validate_release_notes_accepts_accurate_new_contributor_section(self):
|
|
643
|
+
"""The validator should allow the exact expected new contributor entry."""
|
|
644
|
+
notes = ReleaseNotes(
|
|
645
|
+
tag="v1.2.0",
|
|
646
|
+
previous_tag="v1.1.0",
|
|
647
|
+
date="2026-03-07",
|
|
648
|
+
repo_name="owner/repo",
|
|
649
|
+
changes={
|
|
650
|
+
"fixes": [
|
|
651
|
+
Change(message="Fix reconnect bug", sha="abc1234", author="alice", pr_number=42),
|
|
652
|
+
]
|
|
653
|
+
},
|
|
654
|
+
new_contributors=[Contributor(username="alice", first_pr=42, is_new=True)],
|
|
655
|
+
)
|
|
656
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
657
|
+
|
|
658
|
+
### 🐛 Bug Fixes
|
|
659
|
+
- Fix reconnect bug (#42) @alice
|
|
660
|
+
|
|
661
|
+
### 👥 New Contributors
|
|
662
|
+
- @alice made their first contribution in #42
|
|
663
|
+
|
|
664
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
summary = validate_release_notes_markdown(markdown, notes)
|
|
668
|
+
|
|
669
|
+
assert summary.referenced_prs == [42]
|
|
670
|
+
assert summary.referenced_authors == ["alice"]
|
|
671
|
+
|
|
672
|
+
def test_validate_release_notes_rejects_inaccurate_new_contributor_entry(self):
|
|
673
|
+
"""The validator should reject incorrect or hallucinated first-contribution bullets."""
|
|
674
|
+
notes = ReleaseNotes(
|
|
675
|
+
tag="v1.2.0",
|
|
676
|
+
previous_tag="v1.1.0",
|
|
677
|
+
date="2026-03-07",
|
|
678
|
+
repo_name="owner/repo",
|
|
679
|
+
changes={
|
|
680
|
+
"fixes": [
|
|
681
|
+
Change(message="Fix reconnect bug", sha="abc1234", author="alice", pr_number=42),
|
|
682
|
+
]
|
|
683
|
+
},
|
|
684
|
+
new_contributors=[Contributor(username="alice", first_pr=42, is_new=True)],
|
|
685
|
+
)
|
|
686
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
687
|
+
|
|
688
|
+
### 🐛 Bug Fixes
|
|
689
|
+
- Fix reconnect bug (#42) @alice
|
|
690
|
+
|
|
691
|
+
### 👥 New Contributors
|
|
692
|
+
- @alice made their first contribution in #99
|
|
693
|
+
|
|
694
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
695
|
+
"""
|
|
696
|
+
|
|
697
|
+
with pytest.raises(
|
|
698
|
+
ReleaseNotesValidationError,
|
|
699
|
+
match=r"must reference #42",
|
|
700
|
+
):
|
|
701
|
+
validate_release_notes_markdown(markdown, notes)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def test_validate_release_notes_rejects_missing_new_contributor_section(self):
|
|
705
|
+
"""Expected new contributors must appear in the dedicated section."""
|
|
706
|
+
notes = ReleaseNotes(
|
|
707
|
+
tag="v1.2.0",
|
|
708
|
+
previous_tag="v1.1.0",
|
|
709
|
+
date="2026-03-07",
|
|
710
|
+
repo_name="owner/repo",
|
|
711
|
+
changes={
|
|
712
|
+
"fixes": [
|
|
713
|
+
Change(message="Fix reconnect bug", sha="abc1234", author="alice", pr_number=42),
|
|
714
|
+
]
|
|
715
|
+
},
|
|
716
|
+
new_contributors=[Contributor(username="alice", first_pr=42, is_new=True)],
|
|
717
|
+
)
|
|
718
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
719
|
+
|
|
720
|
+
### 🐛 Bug Fixes
|
|
721
|
+
- Fix reconnect bug (#42) @alice
|
|
722
|
+
|
|
723
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
724
|
+
"""
|
|
725
|
+
|
|
726
|
+
with pytest.raises(
|
|
727
|
+
ReleaseNotesValidationError,
|
|
728
|
+
match=r"missing new contributor coverage for: @alice",
|
|
729
|
+
):
|
|
730
|
+
validate_release_notes_markdown(markdown, notes)
|
|
731
|
+
|
|
732
|
+
def test_format_coverage_summary_lists_prs_and_authors(self):
|
|
733
|
+
"""Coverage summary output is suitable for logs and evidence."""
|
|
734
|
+
summary = format_coverage_summary(
|
|
735
|
+
validate_release_notes_markdown(
|
|
736
|
+
"""## [v1.2.0] - 2026-03-07
|
|
737
|
+
|
|
738
|
+
### ✨ New Features
|
|
739
|
+
- Add dark mode (#42) @alice, (#43) @bob
|
|
740
|
+
|
|
741
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
742
|
+
""",
|
|
743
|
+
ReleaseNotes(
|
|
744
|
+
tag="v1.2.0",
|
|
745
|
+
previous_tag="v1.1.0",
|
|
746
|
+
date="2026-03-07",
|
|
747
|
+
repo_name="owner/repo",
|
|
748
|
+
changes={
|
|
749
|
+
"features": [
|
|
750
|
+
Change(
|
|
751
|
+
message="Add dark mode",
|
|
752
|
+
sha="abc1234",
|
|
753
|
+
author="alice",
|
|
754
|
+
pr_number=42,
|
|
755
|
+
),
|
|
756
|
+
Change(
|
|
757
|
+
message="Add theme presets",
|
|
758
|
+
sha="def5678",
|
|
759
|
+
author="bob",
|
|
760
|
+
pr_number=43,
|
|
761
|
+
),
|
|
762
|
+
]
|
|
763
|
+
},
|
|
764
|
+
),
|
|
765
|
+
)
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
assert "PRs referenced: #42, #43" in summary
|
|
769
|
+
assert "Authors referenced: @alice, @bob" in summary
|
|
770
|
+
|
|
771
|
+
def test_validate_release_notes_allows_agent_to_recategorize_other_candidates(self):
|
|
772
|
+
"""The validator should allow refs for candidates the agent re-categorizes."""
|
|
773
|
+
notes = ReleaseNotes(
|
|
774
|
+
tag="v1.2.0",
|
|
775
|
+
previous_tag="v1.1.0",
|
|
776
|
+
date="2026-03-07",
|
|
777
|
+
repo_name="owner/repo",
|
|
778
|
+
changes={
|
|
779
|
+
"other": [
|
|
780
|
+
Change(message="Export public API", sha="abc1234", author="alice", pr_number=42),
|
|
781
|
+
]
|
|
782
|
+
},
|
|
783
|
+
)
|
|
784
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
785
|
+
|
|
786
|
+
### ✨ New Features
|
|
787
|
+
- Export public API (#42) @alice
|
|
788
|
+
|
|
789
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
790
|
+
"""
|
|
791
|
+
|
|
792
|
+
summary = validate_release_notes_markdown(markdown, notes)
|
|
793
|
+
|
|
794
|
+
assert summary.referenced_prs == [42]
|
|
795
|
+
assert summary.referenced_authors == ["alice"]
|
|
796
|
+
|
|
797
|
+
def test_validate_release_notes_rejects_missing_pr_coverage(self):
|
|
798
|
+
"""Validation fails if any release-range PR is omitted entirely."""
|
|
799
|
+
notes = ReleaseNotes(
|
|
800
|
+
tag="v1.2.0",
|
|
801
|
+
previous_tag="v1.1.0",
|
|
802
|
+
date="2026-03-07",
|
|
803
|
+
repo_name="owner/repo",
|
|
804
|
+
changes={
|
|
805
|
+
"features": [
|
|
806
|
+
Change(message="Add dark mode", sha="abc1234", author="alice", pr_number=42),
|
|
807
|
+
Change(message="Add theme presets", sha="def5678", author="bob", pr_number=43),
|
|
808
|
+
]
|
|
809
|
+
},
|
|
810
|
+
)
|
|
811
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
812
|
+
|
|
813
|
+
### ✨ New Features
|
|
814
|
+
- Add dark mode (#42) @alice
|
|
815
|
+
|
|
816
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
817
|
+
"""
|
|
818
|
+
|
|
819
|
+
with pytest.raises(
|
|
820
|
+
ReleaseNotesValidationError,
|
|
821
|
+
match=r"missing PR/commit coverage for: #43",
|
|
822
|
+
):
|
|
823
|
+
validate_release_notes_markdown(markdown, notes)
|
|
824
|
+
|
|
825
|
+
assert missing_references(markdown, notes) == ["#43"]
|
|
826
|
+
|
|
827
|
+
def test_append_reference_coverage_appendix_adds_missing_refs(self):
|
|
828
|
+
"""A deterministic appendix covers PRs the agent chose not to mention."""
|
|
829
|
+
notes = ReleaseNotes(
|
|
830
|
+
tag="v1.2.0",
|
|
831
|
+
previous_tag="v1.1.0",
|
|
832
|
+
date="2026-03-07",
|
|
833
|
+
repo_name="owner/repo",
|
|
834
|
+
changes={
|
|
835
|
+
"features": [
|
|
836
|
+
Change(message="Add dark mode", sha="abc1234", author="alice", pr_number=42),
|
|
837
|
+
],
|
|
838
|
+
"internal": [
|
|
839
|
+
Change(message="Update CI", sha="def5678", author="bob", pr_number=43),
|
|
840
|
+
Change(message="Refactor workflow", sha="fedcba9", author="bob", pr_number=44),
|
|
841
|
+
],
|
|
842
|
+
},
|
|
843
|
+
)
|
|
844
|
+
markdown = """## [v1.2.0] - 2026-03-07
|
|
845
|
+
|
|
846
|
+
This release focuses on polish and delivery improvements.
|
|
847
|
+
|
|
848
|
+
### ✨ New Features
|
|
849
|
+
- Add dark mode (#42) @alice
|
|
850
|
+
|
|
851
|
+
**Full Changelog**: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
augmented = append_reference_coverage_appendix(markdown, notes)
|
|
855
|
+
|
|
856
|
+
assert "### 🔎 Small Fixes/Internal Changes" in augmented
|
|
857
|
+
assert "- @bob: #43, #44" in augmented
|
|
858
|
+
assert augmented.count("@bob") == 1
|
|
859
|
+
summary = validate_release_notes_markdown(augmented, notes)
|
|
860
|
+
assert summary.referenced_prs == [42, 43, 44]
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
class TestCategories:
|
|
864
|
+
"""Tests for the CATEGORIES constant."""
|
|
865
|
+
|
|
866
|
+
def test_all_categories_have_required_fields(self):
|
|
867
|
+
"""Test that all categories have the required fields."""
|
|
868
|
+
required_fields = ["emoji", "title", "commit_patterns", "labels"]
|
|
869
|
+
for category, info in CATEGORIES.items():
|
|
870
|
+
for field in required_fields:
|
|
871
|
+
assert field in info, f"Category {category} missing {field}"
|
|
872
|
+
|
|
873
|
+
def test_breaking_category_exists(self):
|
|
874
|
+
"""Test that the breaking category exists."""
|
|
875
|
+
assert "breaking" in CATEGORIES
|
|
876
|
+
assert CATEGORIES["breaking"]["emoji"] == "⚠️"
|
|
877
|
+
|
|
878
|
+
def test_features_category_exists(self):
|
|
879
|
+
"""Test that the features category exists."""
|
|
880
|
+
assert "features" in CATEGORIES
|
|
881
|
+
assert CATEGORIES["features"]["emoji"] == "✨"
|
|
882
|
+
|
|
883
|
+
def test_fixes_category_exists(self):
|
|
884
|
+
"""Test that the fixes category exists."""
|
|
885
|
+
assert "fixes" in CATEGORIES
|
|
886
|
+
assert CATEGORIES["fixes"]["emoji"] == "🐛"
|
|
887
|
+
|
|
888
|
+
def test_docs_category_exists(self):
|
|
889
|
+
"""Test that the docs category exists."""
|
|
890
|
+
assert "docs" in CATEGORIES
|
|
891
|
+
assert CATEGORIES["docs"]["emoji"] == "📚"
|
|
892
|
+
|
|
893
|
+
def test_internal_category_exists(self):
|
|
894
|
+
"""Test that the internal category exists."""
|
|
895
|
+
assert "internal" in CATEGORIES
|
|
896
|
+
assert CATEGORIES["internal"]["emoji"] == "🏗️"
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
class TestPluginStructure:
|
|
900
|
+
"""Tests for the plugin directory structure."""
|
|
901
|
+
|
|
902
|
+
def test_plugin_directory_exists(self):
|
|
903
|
+
"""Test that the plugin directory exists."""
|
|
904
|
+
plugin_dir = Path(__file__).parent.parent / "plugins" / "release-notes"
|
|
905
|
+
assert plugin_dir.is_dir()
|
|
906
|
+
|
|
907
|
+
def test_skill_md_exists(self):
|
|
908
|
+
"""Test that SKILL.md exists."""
|
|
909
|
+
skill_md = Path(__file__).parent.parent / "plugins" / "release-notes" / "SKILL.md"
|
|
910
|
+
assert skill_md.is_file()
|
|
911
|
+
|
|
912
|
+
def test_readme_exists(self):
|
|
913
|
+
"""Test that README.md exists."""
|
|
914
|
+
readme = Path(__file__).parent.parent / "plugins" / "release-notes" / "README.md"
|
|
915
|
+
assert readme.is_file()
|
|
916
|
+
|
|
917
|
+
def test_action_yml_exists(self):
|
|
918
|
+
"""Test that action.yml exists."""
|
|
919
|
+
action = Path(__file__).parent.parent / "plugins" / "release-notes" / "action.yml"
|
|
920
|
+
assert action.is_file()
|
|
921
|
+
|
|
922
|
+
def test_script_exists(self):
|
|
923
|
+
"""Test that the generator script exists."""
|
|
924
|
+
script = (
|
|
925
|
+
Path(__file__).parent.parent
|
|
926
|
+
/ "plugins"
|
|
927
|
+
/ "release-notes"
|
|
928
|
+
/ "scripts"
|
|
929
|
+
/ "generate_release_notes.py"
|
|
930
|
+
)
|
|
931
|
+
assert script.is_file()
|
|
932
|
+
|
|
933
|
+
def test_workflow_exists(self):
|
|
934
|
+
"""Test that the workflow file exists."""
|
|
935
|
+
workflow = (
|
|
936
|
+
Path(__file__).parent.parent
|
|
937
|
+
/ "plugins"
|
|
938
|
+
/ "release-notes"
|
|
939
|
+
/ "workflows"
|
|
940
|
+
/ "release-notes.yml"
|
|
941
|
+
)
|
|
942
|
+
assert workflow.is_file()
|
|
943
|
+
|
|
944
|
+
def test_validator_script_exists(self):
|
|
945
|
+
"""Test that the attribution validator script exists."""
|
|
946
|
+
validator = (
|
|
947
|
+
Path(__file__).parent.parent
|
|
948
|
+
/ "plugins"
|
|
949
|
+
/ "release-notes"
|
|
950
|
+
/ "scripts"
|
|
951
|
+
/ "validate_release_notes.py"
|
|
952
|
+
)
|
|
953
|
+
assert validator.is_file()
|
|
954
|
+
|
|
955
|
+
def test_agent_script_exists(self):
|
|
956
|
+
"""Test that the agent orchestration script exists."""
|
|
957
|
+
script = (
|
|
958
|
+
Path(__file__).parent.parent
|
|
959
|
+
/ "plugins"
|
|
960
|
+
/ "release-notes"
|
|
961
|
+
/ "scripts"
|
|
962
|
+
/ "agent_script.py"
|
|
963
|
+
)
|
|
964
|
+
assert script.is_file()
|
|
965
|
+
|
|
966
|
+
def test_prompt_script_exists(self):
|
|
967
|
+
"""Test that the prompt template exists."""
|
|
968
|
+
prompt = (
|
|
969
|
+
Path(__file__).parent.parent
|
|
970
|
+
/ "plugins"
|
|
971
|
+
/ "release-notes"
|
|
972
|
+
/ "scripts"
|
|
973
|
+
/ "prompt.py"
|
|
974
|
+
)
|
|
975
|
+
assert prompt.is_file()
|
|
976
|
+
|
|
977
|
+
def test_skills_symlink_exists(self):
|
|
978
|
+
"""Test that the skills symlink exists."""
|
|
979
|
+
symlink = (
|
|
980
|
+
Path(__file__).parent.parent
|
|
981
|
+
/ "plugins"
|
|
982
|
+
/ "release-notes"
|
|
983
|
+
/ "skills"
|
|
984
|
+
/ "release-notes"
|
|
985
|
+
)
|
|
986
|
+
assert symlink.exists()
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
if __name__ == "__main__":
|
|
990
|
+
pytest.main([__file__, "-v"])
|