@openhands/extensions 0.1.0 → 0.3.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/index.d.ts +2 -0
- package/index.js +1 -0
- package/marketplaces/large-codebase.json +66 -0
- package/marketplaces/openhands-extensions.json +682 -0
- package/package.json +12 -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/build-skills-catalog.mjs +100 -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 +186 -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/index.d.ts +11 -0
- package/skills/index.js +465 -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 +230 -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_github_pr_review_skill.py +114 -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_catalog.py +336 -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,109 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_catalog_entries(relative_path: str):
|
|
11
|
+
entries = []
|
|
12
|
+
for entry_path in sorted((ROOT / relative_path).glob("*.json")):
|
|
13
|
+
entry = json.loads(entry_path.read_text())
|
|
14
|
+
assert entry["id"] == entry_path.stem
|
|
15
|
+
entries.append(entry)
|
|
16
|
+
return entries
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_catalog_ids_are_unique_and_automations_reference_existing_integrations():
|
|
20
|
+
integrations = load_catalog_entries("integrations/catalog")
|
|
21
|
+
automations = load_catalog_entries("automations/catalog")
|
|
22
|
+
|
|
23
|
+
integration_ids = [entry["id"] for entry in integrations]
|
|
24
|
+
automation_ids = [entry["id"] for entry in automations]
|
|
25
|
+
|
|
26
|
+
assert len(integration_ids) == len(set(integration_ids))
|
|
27
|
+
assert len(automation_ids) == len(set(automation_ids))
|
|
28
|
+
|
|
29
|
+
known_integration_ids = set(integration_ids)
|
|
30
|
+
for automation in automations:
|
|
31
|
+
assert automation["requiredIntegrationIds"]
|
|
32
|
+
missing_ids = (
|
|
33
|
+
set(automation["requiredIntegrationIds"]) - known_integration_ids
|
|
34
|
+
)
|
|
35
|
+
assert missing_ids == set()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_catalog_entries_have_required_fields():
|
|
39
|
+
for entry in load_catalog_entries("integrations/catalog"):
|
|
40
|
+
assert entry["id"]
|
|
41
|
+
assert entry["name"]
|
|
42
|
+
assert entry["description"]
|
|
43
|
+
assert entry["kind"] in {"mcp", "http"}
|
|
44
|
+
assert entry["iconBg"]
|
|
45
|
+
assert entry["connectionOptions"]
|
|
46
|
+
assert entry["defaultConnectionOptionId"]
|
|
47
|
+
for option in entry["connectionOptions"]:
|
|
48
|
+
assert option["id"]
|
|
49
|
+
assert option["provider"] in {"mcp", "http"}
|
|
50
|
+
assert option["auth"]["strategy"] in {
|
|
51
|
+
"none",
|
|
52
|
+
"api_key",
|
|
53
|
+
"bearer",
|
|
54
|
+
"basic",
|
|
55
|
+
"oauth2",
|
|
56
|
+
}
|
|
57
|
+
if option["provider"] == "mcp":
|
|
58
|
+
assert option["transport"]["kind"] in {"stdio", "shttp", "sse"}
|
|
59
|
+
if option["transport"]["kind"] == "stdio":
|
|
60
|
+
assert option["transport"]["serverName"]
|
|
61
|
+
assert option["transport"]["command"]
|
|
62
|
+
assert isinstance(option["transport"]["args"], list)
|
|
63
|
+
else:
|
|
64
|
+
assert option["transport"]["url"].startswith("https://")
|
|
65
|
+
|
|
66
|
+
for entry in load_catalog_entries("automations/catalog"):
|
|
67
|
+
assert entry["id"]
|
|
68
|
+
assert entry["name"]
|
|
69
|
+
assert entry["prompt"]
|
|
70
|
+
assert entry["exampleImplementation"]
|
|
71
|
+
assert isinstance(entry["popularityRank"], int)
|
|
72
|
+
assert isinstance(entry["estimatedSetupMinutes"], int)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_credential_fields_have_helper_text_and_link():
|
|
76
|
+
"""All password fields must have helperText plus a link (either a helperLink field or a
|
|
77
|
+
markdown link embedded in helperText) so users know how to get credentials."""
|
|
78
|
+
markdown_link_re = re.compile(r"\[.+?\]\(https://[^)]+\)")
|
|
79
|
+
|
|
80
|
+
for entry in load_catalog_entries("integrations/catalog"):
|
|
81
|
+
for option in entry["connectionOptions"]:
|
|
82
|
+
transport = option.get("transport", {})
|
|
83
|
+
for field_group in ("envFields", "argFields"):
|
|
84
|
+
for field in transport.get(field_group, []):
|
|
85
|
+
if field.get("type") == "password":
|
|
86
|
+
field_key = field.get("key", "<unknown>")
|
|
87
|
+
assert "helperText" in field, (
|
|
88
|
+
f"{entry['id']}: password field '{field_key}' is missing helperText"
|
|
89
|
+
)
|
|
90
|
+
assert field["helperText"], (
|
|
91
|
+
f"{entry['id']}: password field '{field_key}' has empty helperText"
|
|
92
|
+
)
|
|
93
|
+
has_helper_link = field.get("helperLink", "").startswith("https://")
|
|
94
|
+
has_inline_link = bool(markdown_link_re.search(field["helperText"]))
|
|
95
|
+
assert has_helper_link or has_inline_link, (
|
|
96
|
+
f"{entry['id']}: password field '{field_key}' must have a helperLink "
|
|
97
|
+
f"or a markdown link in helperText"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_node_package_exports_catalogs():
|
|
102
|
+
script = """
|
|
103
|
+
import { INTEGRATION_CATALOG, AUTOMATION_CATALOG } from './index.js';
|
|
104
|
+
if (!Array.isArray(INTEGRATION_CATALOG) || INTEGRATION_CATALOG.length === 0) process.exit(1);
|
|
105
|
+
if (!Array.isArray(AUTOMATION_CATALOG) || AUTOMATION_CATALOG.length === 0) process.exit(1);
|
|
106
|
+
if (!INTEGRATION_CATALOG.some((entry) => entry.id === 'github')) process.exit(1);
|
|
107
|
+
if (!AUTOMATION_CATALOG.some((entry) => entry.id === 'github-pr-reviewer')) process.exit(1);
|
|
108
|
+
"""
|
|
109
|
+
subprocess.run(["node", "--input-type=module", "-e", script], cwd=ROOT, check=True)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Test that risk/safety evaluation is integrated into the unified code review skill."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_repo_root():
|
|
7
|
+
return Path(__file__).parent.parent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_code_review_skill():
|
|
11
|
+
return (get_repo_root() / "skills" / "code-review" / "SKILL.md").read_text()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_risk_evaluation_reference():
|
|
15
|
+
return (
|
|
16
|
+
get_repo_root()
|
|
17
|
+
/ "skills"
|
|
18
|
+
/ "code-review"
|
|
19
|
+
/ "references"
|
|
20
|
+
/ "risk-evaluation.md"
|
|
21
|
+
).read_text()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestRiskEvaluationReference:
|
|
25
|
+
"""Verify the risk-evaluation.md reference file is complete."""
|
|
26
|
+
|
|
27
|
+
def test_reference_file_exists(self):
|
|
28
|
+
path = (
|
|
29
|
+
get_repo_root()
|
|
30
|
+
/ "skills"
|
|
31
|
+
/ "code-review"
|
|
32
|
+
/ "references"
|
|
33
|
+
/ "risk-evaluation.md"
|
|
34
|
+
)
|
|
35
|
+
assert path.exists()
|
|
36
|
+
|
|
37
|
+
def test_has_all_risk_levels(self):
|
|
38
|
+
content = get_risk_evaluation_reference()
|
|
39
|
+
assert "Low Risk" in content
|
|
40
|
+
assert "Medium Risk" in content
|
|
41
|
+
assert "High Risk" in content
|
|
42
|
+
|
|
43
|
+
def test_has_risk_factors(self):
|
|
44
|
+
content = get_risk_evaluation_reference()
|
|
45
|
+
factors = [
|
|
46
|
+
"Pattern conformance",
|
|
47
|
+
"Security sensitivity",
|
|
48
|
+
"Infrastructure dependencies",
|
|
49
|
+
"Blast radius",
|
|
50
|
+
"Core system impact",
|
|
51
|
+
]
|
|
52
|
+
for factor in factors:
|
|
53
|
+
assert factor in content, f"Missing risk factor: {factor}"
|
|
54
|
+
|
|
55
|
+
def test_has_high_risk_escalation_guidance(self):
|
|
56
|
+
content = get_risk_evaluation_reference()
|
|
57
|
+
assert "not auto-merg" in content.lower()
|
|
58
|
+
assert "human" in content.lower()
|
|
59
|
+
|
|
60
|
+
def test_has_repo_specific_risk_rules(self):
|
|
61
|
+
content = get_risk_evaluation_reference()
|
|
62
|
+
assert "AGENTS.md" in content
|
|
63
|
+
assert "repo-specific" in content.lower() or "Repo-specific" in content
|
|
64
|
+
|
|
65
|
+
def test_has_risk_assessment_output_format(self):
|
|
66
|
+
content = get_risk_evaluation_reference()
|
|
67
|
+
assert "Risk Assessment" in content
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestCodeReviewSkillReferencesRisk:
|
|
71
|
+
"""Verify the unified code-review skill references the risk evaluation."""
|
|
72
|
+
|
|
73
|
+
def test_has_risk_evaluation_scenario(self):
|
|
74
|
+
content = get_code_review_skill()
|
|
75
|
+
assert "Risk and Safety Evaluation" in content
|
|
76
|
+
|
|
77
|
+
def test_references_risk_evaluation_file(self):
|
|
78
|
+
content = get_code_review_skill()
|
|
79
|
+
assert "references/risk-evaluation.md" in content
|
|
80
|
+
|
|
81
|
+
def test_risk_section_appears_after_dependency_section(self):
|
|
82
|
+
content = get_code_review_skill()
|
|
83
|
+
dep_pos = content.index("8. **Dependency Changes**")
|
|
84
|
+
risk_pos = content.index("9. **Risk and Safety Evaluation**")
|
|
85
|
+
assert risk_pos > dep_pos
|
|
86
|
+
|
|
87
|
+
def test_always_include_risk_instruction(self):
|
|
88
|
+
content = get_code_review_skill()
|
|
89
|
+
assert "Always include the" in content
|
|
90
|
+
assert "Risk and Safety Evaluation" in content
|
|
91
|
+
|
|
92
|
+
def test_has_risk_assessment_in_output_format(self):
|
|
93
|
+
content = get_code_review_skill()
|
|
94
|
+
assert "RISK ASSESSMENT" in content
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Tests for the github-pr-review skill content.
|
|
2
|
+
|
|
3
|
+
These tests guard the guidance the agent reads when it builds a GitHub review
|
|
4
|
+
with inline suggestions. The skill must teach the agent how GitHub
|
|
5
|
+
``suggestion`` blocks actually behave (range replacement), so accepted
|
|
6
|
+
suggestions don't silently duplicate or delete lines, and so the prose
|
|
7
|
+
description always matches the resulting change.
|
|
8
|
+
|
|
9
|
+
See: https://github.com/OpenHands/extensions/issues/292
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
SKILL_PATH = (
|
|
17
|
+
Path(__file__).parent.parent / "skills" / "github-pr-review" / "SKILL.md"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture(scope="module")
|
|
22
|
+
def skill_text() -> str:
|
|
23
|
+
return SKILL_PATH.read_text(encoding="utf-8")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_skill_file_exists():
|
|
27
|
+
assert SKILL_PATH.is_file(), f"missing skill file: {SKILL_PATH}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_skill_does_not_claim_suggestion_must_match_range_length(skill_text: str):
|
|
31
|
+
"""Regression: the old "same number of lines as the range" rule was wrong.
|
|
32
|
+
|
|
33
|
+
GitHub replaces the targeted range with the suggestion body regardless of
|
|
34
|
+
how many lines the body contains. Telling the agent the counts must match
|
|
35
|
+
pushed it to either delete lines (when shrinking a block) or duplicate
|
|
36
|
+
lines (when expanding one), depending on which side it adjusted to satisfy
|
|
37
|
+
the bogus rule. The skill must no longer state that constraint.
|
|
38
|
+
"""
|
|
39
|
+
forbidden_phrases = [
|
|
40
|
+
"suggestion must have the same number of lines as the range",
|
|
41
|
+
"must have the same number of lines as the range",
|
|
42
|
+
]
|
|
43
|
+
for phrase in forbidden_phrases:
|
|
44
|
+
assert phrase not in skill_text, (
|
|
45
|
+
f"SKILL.md still contains the misleading rule {phrase!r}; "
|
|
46
|
+
"GitHub suggestions replace the range with any number of lines."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_skill_explains_replace_semantics(skill_text: str):
|
|
51
|
+
"""The skill must say in plain language that suggestions REPLACE the range."""
|
|
52
|
+
assert "How Suggestions Actually Work" in skill_text
|
|
53
|
+
# Mention replacement explicitly so the agent does not treat the suggestion
|
|
54
|
+
# body as "extra lines added on top of the original".
|
|
55
|
+
lowered = skill_text.lower()
|
|
56
|
+
assert "replace" in lowered
|
|
57
|
+
assert "any number of lines" in lowered
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_skill_covers_add_remove_change_patterns(skill_text: str):
|
|
61
|
+
"""The skill should give explicit recipes for the three intents that
|
|
62
|
+
most commonly produced broken suggestions: changing, adding, and
|
|
63
|
+
deleting lines. Without these, the agent invents wrong patterns.
|
|
64
|
+
"""
|
|
65
|
+
for keyword in ("Change line", "Add", "Delete"):
|
|
66
|
+
assert keyword in skill_text, (
|
|
67
|
+
f"SKILL.md is missing guidance for intent {keyword!r}; "
|
|
68
|
+
"see issue #292."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_skill_warns_about_duplicated_and_deleted_lines(skill_text: str):
|
|
73
|
+
"""The skill must call out the two failure modes from issue #292:
|
|
74
|
+
accepted suggestions that duplicate or remove lines."""
|
|
75
|
+
lowered = skill_text.lower()
|
|
76
|
+
assert "duplicat" in lowered, (
|
|
77
|
+
"SKILL.md should warn about suggestions that duplicate lines."
|
|
78
|
+
)
|
|
79
|
+
# "disappear"/"disappearing" is unique to the failure-mode description in
|
|
80
|
+
# the "Common Mistakes" section. The generic terms "delete" and "remove"
|
|
81
|
+
# also appear in the recipe table for intentional deletions, so checking
|
|
82
|
+
# for those would let the failure-mode guidance be removed silently.
|
|
83
|
+
assert "disappear" in lowered, (
|
|
84
|
+
"SKILL.md should warn about suggestions that silently drop lines "
|
|
85
|
+
"(see 'Disappearing lines' under Common Mistakes)."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_skill_requires_description_to_match_suggestion(skill_text: str):
|
|
90
|
+
"""The skill must explicitly require the prose description to match the
|
|
91
|
+
actual code change in the suggestion block (the third bug from #292)."""
|
|
92
|
+
lowered = skill_text.lower()
|
|
93
|
+
assert any(phrase in lowered for phrase in ("description does not match", "prose description", "description must match"))
|
|
94
|
+
# The text should connect the description to the resulting code change.
|
|
95
|
+
assert (
|
|
96
|
+
"match" in lowered
|
|
97
|
+
), "SKILL.md should say the description must match the change."
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_skill_has_pre_post_verification_step(skill_text: str):
|
|
101
|
+
"""Before posting, the agent must verify the suggestion against the file."""
|
|
102
|
+
assert "Mandatory Verification" in skill_text or "Verification" in skill_text
|
|
103
|
+
# Should point the agent at the real file content, not just the diff.
|
|
104
|
+
assert "sed -n" in skill_text or "read the actual file" in skill_text.lower()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_skill_offers_prose_fallback(skill_text: str):
|
|
108
|
+
"""When unsure, the agent should drop the suggestion block and use prose
|
|
109
|
+
instead — a correct comment beats a broken one-click fix."""
|
|
110
|
+
lowered = skill_text.lower()
|
|
111
|
+
assert "prose" in lowered, (
|
|
112
|
+
"SKILL.md should tell the agent to fall back to a prose comment "
|
|
113
|
+
"when the suggestion cannot be verified."
|
|
114
|
+
)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import io
|
|
3
|
+
import urllib.error
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
PLUGIN_DIR = ROOT / "plugins" / "issue-duplicate-checker" / "scripts"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_script(name: str):
|
|
14
|
+
path = PLUGIN_DIR / f"{name}.py"
|
|
15
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
16
|
+
module = importlib.util.module_from_spec(spec)
|
|
17
|
+
assert spec.loader is not None
|
|
18
|
+
spec.loader.exec_module(module)
|
|
19
|
+
return module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_action_shell_blocks_do_not_interpolate_expressions_directly():
|
|
23
|
+
action_path = ROOT / "plugins" / "issue-duplicate-checker" / "action.yml"
|
|
24
|
+
lines = action_path.read_text().splitlines()
|
|
25
|
+
in_block = False
|
|
26
|
+
block_indent = 0
|
|
27
|
+
for line_number, line in enumerate(lines, start=1):
|
|
28
|
+
stripped = line.lstrip()
|
|
29
|
+
indent = len(line) - len(stripped)
|
|
30
|
+
if in_block and stripped and indent <= block_indent:
|
|
31
|
+
in_block = False
|
|
32
|
+
if stripped.startswith(("run:", "script:")):
|
|
33
|
+
block_scalar = stripped.split(":", 1)[1].strip()
|
|
34
|
+
if block_scalar.startswith(("|", ">")):
|
|
35
|
+
in_block = True
|
|
36
|
+
block_indent = indent
|
|
37
|
+
continue
|
|
38
|
+
assert not (in_block and "${{" in line), (
|
|
39
|
+
f"Move GitHub expression on line {line_number} into env before using it"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_normalize_result_preserves_model_should_comment_false():
|
|
44
|
+
script = load_script("issue_duplicate_check_openhands")
|
|
45
|
+
|
|
46
|
+
result = script.normalize_result(
|
|
47
|
+
{
|
|
48
|
+
"should_comment": False,
|
|
49
|
+
"is_duplicate": True,
|
|
50
|
+
"auto_close_candidate": True,
|
|
51
|
+
"classification": "duplicate",
|
|
52
|
+
"confidence": "high",
|
|
53
|
+
"canonical_issue_number": 123,
|
|
54
|
+
"candidate_issues": [{"number": 123}],
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert result["should_comment"] is False
|
|
59
|
+
assert result["auto_close_candidate"] is False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_github_headers_require_token(monkeypatch):
|
|
63
|
+
script = load_script("issue_duplicate_check_openhands")
|
|
64
|
+
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
|
|
65
|
+
|
|
66
|
+
with pytest.raises(
|
|
67
|
+
RuntimeError,
|
|
68
|
+
match="GITHUB_TOKEN environment variable is required",
|
|
69
|
+
):
|
|
70
|
+
script.github_headers()
|
|
71
|
+
|
|
72
|
+
monkeypatch.setenv("GITHUB_TOKEN", "token")
|
|
73
|
+
headers = script.github_headers()
|
|
74
|
+
assert headers["Authorization"] == "Bearer token"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_issue_check_request_json_raises_structured_http_error(monkeypatch):
|
|
78
|
+
script = load_script("issue_duplicate_check_openhands")
|
|
79
|
+
|
|
80
|
+
def fake_urlopen(*args, **kwargs):
|
|
81
|
+
raise urllib.error.HTTPError(
|
|
82
|
+
url="https://api.example.test/example",
|
|
83
|
+
code=403,
|
|
84
|
+
msg="Forbidden",
|
|
85
|
+
hdrs=None,
|
|
86
|
+
fp=io.BytesIO(b'{"message":"rate limited"}'),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
monkeypatch.setattr(script.urllib.request, "urlopen", fake_urlopen)
|
|
90
|
+
|
|
91
|
+
with pytest.raises(script.HTTPError) as exc_info:
|
|
92
|
+
script.request_json("https://api.example.test", "/example")
|
|
93
|
+
|
|
94
|
+
assert exc_info.value.status_code == 403
|
|
95
|
+
assert exc_info.value.url == "https://api.example.test/example"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_parse_agent_json_extracts_fenced_json():
|
|
99
|
+
script = load_script("issue_duplicate_check_openhands")
|
|
100
|
+
|
|
101
|
+
result = script.parse_agent_json(
|
|
102
|
+
'Here is the result:\n```json\n'
|
|
103
|
+
'{"classification":"duplicate","confidence":"high"}\n```'
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
assert result == {"classification": "duplicate", "confidence": "high"}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_normalize_result_guards_auto_close_and_infers_canonical_issue():
|
|
110
|
+
script = load_script("issue_duplicate_check_openhands")
|
|
111
|
+
|
|
112
|
+
overlapping = script.normalize_result(
|
|
113
|
+
{
|
|
114
|
+
"should_comment": True,
|
|
115
|
+
"is_duplicate": False,
|
|
116
|
+
"auto_close_candidate": True,
|
|
117
|
+
"classification": "overlapping-scope",
|
|
118
|
+
"confidence": "high",
|
|
119
|
+
"candidate_issues": [{"number": 10}],
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
assert overlapping["should_comment"] is True
|
|
123
|
+
assert overlapping["auto_close_candidate"] is False
|
|
124
|
+
|
|
125
|
+
low_confidence = script.normalize_result(
|
|
126
|
+
{
|
|
127
|
+
"should_comment": True,
|
|
128
|
+
"is_duplicate": True,
|
|
129
|
+
"auto_close_candidate": True,
|
|
130
|
+
"classification": "duplicate",
|
|
131
|
+
"confidence": "low",
|
|
132
|
+
"candidate_issues": [{"number": 11}],
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
assert low_confidence["should_comment"] is False
|
|
136
|
+
assert low_confidence["auto_close_candidate"] is False
|
|
137
|
+
|
|
138
|
+
inferred = script.normalize_result(
|
|
139
|
+
{
|
|
140
|
+
"should_comment": True,
|
|
141
|
+
"is_duplicate": True,
|
|
142
|
+
"auto_close_candidate": True,
|
|
143
|
+
"classification": "duplicate",
|
|
144
|
+
"confidence": "high",
|
|
145
|
+
"canonical_issue_number": None,
|
|
146
|
+
"candidate_issues": [{"number": "12"}],
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
assert inferred["auto_close_candidate"] is True
|
|
150
|
+
assert inferred["canonical_issue_number"] == 12
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_build_prompt_includes_required_schema_keys():
|
|
154
|
+
script = load_script("issue_duplicate_check_openhands")
|
|
155
|
+
|
|
156
|
+
prompt = script.build_prompt(
|
|
157
|
+
"OpenHands/extensions",
|
|
158
|
+
{
|
|
159
|
+
"number": 123,
|
|
160
|
+
"title": "Duplicate bug",
|
|
161
|
+
"body": "Looks related",
|
|
162
|
+
"html_url": "https://github.com/OpenHands/extensions/issues/123",
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
for key in [
|
|
167
|
+
"should_comment",
|
|
168
|
+
"is_duplicate",
|
|
169
|
+
"auto_close_candidate",
|
|
170
|
+
"classification",
|
|
171
|
+
"confidence",
|
|
172
|
+
"canonical_issue_number",
|
|
173
|
+
"candidate_issues",
|
|
174
|
+
]:
|
|
175
|
+
assert key in prompt
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_find_latest_auto_close_comment_uses_newest_auto_close_marker():
|
|
179
|
+
script = load_script("auto_close_duplicate_issues")
|
|
180
|
+
|
|
181
|
+
latest, canonical = script.find_latest_auto_close_comment(
|
|
182
|
+
[
|
|
183
|
+
{
|
|
184
|
+
"id": 1,
|
|
185
|
+
"created_at": "2026-01-01T00:00:00Z",
|
|
186
|
+
"body": "<!-- openhands-duplicate-check canonical=10 auto-close=true -->",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": 2,
|
|
190
|
+
"created_at": "2026-01-03T00:00:00Z",
|
|
191
|
+
"body": "<!-- openhands-duplicate-check canonical=20 auto-close=false -->",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"id": 3,
|
|
195
|
+
"created_at": None,
|
|
196
|
+
"body": "<!-- openhands-duplicate-check canonical=30 auto-close=true -->",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"id": 4,
|
|
200
|
+
"created_at": "2026-01-02T00:00:00Z",
|
|
201
|
+
"body": "<!-- openhands-duplicate-check canonical=40 auto-close=true -->",
|
|
202
|
+
},
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
assert latest["id"] == 4
|
|
207
|
+
assert canonical == 40
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_request_json_raises_structured_http_error(monkeypatch):
|
|
211
|
+
script = load_script("auto_close_duplicate_issues")
|
|
212
|
+
monkeypatch.setenv("GITHUB_TOKEN", "token")
|
|
213
|
+
|
|
214
|
+
def fake_urlopen(*args, **kwargs):
|
|
215
|
+
raise urllib.error.HTTPError(
|
|
216
|
+
url="https://api.github.com/example",
|
|
217
|
+
code=404,
|
|
218
|
+
msg="Not Found",
|
|
219
|
+
hdrs=None,
|
|
220
|
+
fp=io.BytesIO(b'{"message":"missing"}'),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
monkeypatch.setattr(script.urllib.request, "urlopen", fake_urlopen)
|
|
224
|
+
|
|
225
|
+
with pytest.raises(script.HTTPError) as exc_info:
|
|
226
|
+
script.request_json("/example")
|
|
227
|
+
|
|
228
|
+
assert exc_info.value.status_code == 404
|
|
229
|
+
assert exc_info.value.path == "/example"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_fetch_issue_returns_none_on_404(monkeypatch):
|
|
233
|
+
script = load_script("auto_close_duplicate_issues")
|
|
234
|
+
|
|
235
|
+
def fake_request_json(path):
|
|
236
|
+
raise script.HTTPError("GET", path, 404, "missing")
|
|
237
|
+
|
|
238
|
+
monkeypatch.setattr(script, "request_json", fake_request_json)
|
|
239
|
+
|
|
240
|
+
assert script.fetch_issue("OpenHands/extensions", 123) is None
|