@openhands/extensions 0.0.1-alpha → 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,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,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
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _load_openhands_api_module():
|
|
7
|
+
skill_path = Path(__file__).parent.parent / "skills" / "openhands-api" / "scripts" / "openhands_api.py"
|
|
8
|
+
spec = importlib.util.spec_from_file_location("openhands_api", skill_path)
|
|
9
|
+
mod = importlib.util.module_from_spec(spec)
|
|
10
|
+
sys.modules["openhands_api"] = mod
|
|
11
|
+
spec.loader.exec_module(mod)
|
|
12
|
+
return mod
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_app_conversation_start_builds_v1_payload(monkeypatch):
|
|
16
|
+
mod = _load_openhands_api_module()
|
|
17
|
+
OpenHandsAPI = mod.OpenHandsAPI
|
|
18
|
+
|
|
19
|
+
captured = {}
|
|
20
|
+
|
|
21
|
+
class FakeResp:
|
|
22
|
+
def raise_for_status(self):
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
def json(self):
|
|
26
|
+
return {"id": "task-1", "status": "WORKING"}
|
|
27
|
+
|
|
28
|
+
class FakeClient:
|
|
29
|
+
def __init__(self, **kwargs):
|
|
30
|
+
self.kwargs = kwargs
|
|
31
|
+
|
|
32
|
+
def post(self, url, json=None, timeout=None):
|
|
33
|
+
captured["url"] = url
|
|
34
|
+
captured["json"] = json
|
|
35
|
+
captured["timeout"] = timeout
|
|
36
|
+
return FakeResp()
|
|
37
|
+
|
|
38
|
+
def close(self):
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
monkeypatch.setattr(mod.httpx, "Client", lambda **kwargs: FakeClient(**kwargs))
|
|
42
|
+
|
|
43
|
+
api = OpenHandsAPI(api_key="k", base_url="https://example.com/")
|
|
44
|
+
resp = api.app_conversation_start(
|
|
45
|
+
initial_message="hi",
|
|
46
|
+
selected_repository="o/r",
|
|
47
|
+
selected_branch="main",
|
|
48
|
+
title="Test title",
|
|
49
|
+
run=False,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert resp == {"id": "task-1", "status": "WORKING"}
|
|
53
|
+
assert captured["url"] == "https://example.com/api/v1/app-conversations"
|
|
54
|
+
assert captured["timeout"] == 120
|
|
55
|
+
assert captured["json"] == {
|
|
56
|
+
"initial_message": {
|
|
57
|
+
"role": "user",
|
|
58
|
+
"content": [{"type": "text", "text": "hi"}],
|
|
59
|
+
"run": False,
|
|
60
|
+
},
|
|
61
|
+
"selected_repository": "o/r",
|
|
62
|
+
"selected_branch": "main",
|
|
63
|
+
"title": "Test title",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_app_conversations_get_batch_passes_ids(monkeypatch):
|
|
68
|
+
mod = _load_openhands_api_module()
|
|
69
|
+
OpenHandsAPI = mod.OpenHandsAPI
|
|
70
|
+
|
|
71
|
+
captured = {}
|
|
72
|
+
|
|
73
|
+
class FakeResp:
|
|
74
|
+
def raise_for_status(self):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def json(self):
|
|
78
|
+
return [{"id": "conv-1"}]
|
|
79
|
+
|
|
80
|
+
class FakeClient:
|
|
81
|
+
def __init__(self, **kwargs):
|
|
82
|
+
self.kwargs = kwargs
|
|
83
|
+
|
|
84
|
+
def get(self, url, params=None):
|
|
85
|
+
captured["url"] = url
|
|
86
|
+
captured["params"] = params
|
|
87
|
+
return FakeResp()
|
|
88
|
+
|
|
89
|
+
def close(self):
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
monkeypatch.setattr(mod.httpx, "Client", lambda **kwargs: FakeClient(**kwargs))
|
|
93
|
+
|
|
94
|
+
api = OpenHandsAPI(api_key="k", base_url="https://example.com")
|
|
95
|
+
conversations = api.app_conversations_get_batch(ids=["conv-1", "conv-2"])
|
|
96
|
+
|
|
97
|
+
assert conversations == [{"id": "conv-1"}]
|
|
98
|
+
assert captured["url"] == "https://example.com/api/v1/app-conversations"
|
|
99
|
+
assert captured["params"] == {"ids": ["conv-1", "conv-2"]}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_poll_start_task_until_ready_uses_start_task_endpoint(monkeypatch):
|
|
103
|
+
mod = _load_openhands_api_module()
|
|
104
|
+
OpenHandsAPI = mod.OpenHandsAPI
|
|
105
|
+
|
|
106
|
+
states = [
|
|
107
|
+
{"id": "task-1", "status": "WORKING"},
|
|
108
|
+
{"id": "task-1", "status": "READY", "app_conversation_id": "conv-1"},
|
|
109
|
+
]
|
|
110
|
+
call_count = {"sleep": 0}
|
|
111
|
+
|
|
112
|
+
class FakeClient:
|
|
113
|
+
def __init__(self, **kwargs):
|
|
114
|
+
self.kwargs = kwargs
|
|
115
|
+
|
|
116
|
+
def get(self, url, params=None):
|
|
117
|
+
class FakeResp:
|
|
118
|
+
def raise_for_status(self_nonlocal):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def json(self_nonlocal):
|
|
122
|
+
return [states.pop(0)]
|
|
123
|
+
|
|
124
|
+
return FakeResp()
|
|
125
|
+
|
|
126
|
+
def close(self):
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
monkeypatch.setattr(mod.httpx, "Client", lambda **kwargs: FakeClient(**kwargs))
|
|
130
|
+
monkeypatch.setattr(mod.time, "sleep", lambda *_args, **_kwargs: call_count.__setitem__("sleep", call_count["sleep"] + 1))
|
|
131
|
+
|
|
132
|
+
api = OpenHandsAPI(api_key="k", base_url="https://example.com")
|
|
133
|
+
ready = api.poll_start_task_until_ready("task-1", timeout_s=10, poll_interval_s=0.01, backoff_factor=1.0)
|
|
134
|
+
|
|
135
|
+
assert ready == {"id": "task-1", "status": "READY", "app_conversation_id": "conv-1"}
|
|
136
|
+
assert call_count["sleep"] == 1
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_legacy_alias_still_exists(monkeypatch):
|
|
140
|
+
mod = _load_openhands_api_module()
|
|
141
|
+
|
|
142
|
+
class FakeClient:
|
|
143
|
+
def __init__(self, **kwargs):
|
|
144
|
+
self.kwargs = kwargs
|
|
145
|
+
|
|
146
|
+
def close(self):
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
monkeypatch.setattr(mod.httpx, "Client", lambda **kwargs: FakeClient(**kwargs))
|
|
150
|
+
|
|
151
|
+
api = mod.OpenHandsV1API(api_key="k")
|
|
152
|
+
assert isinstance(api, mod.OpenHandsAPI)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Test that all plugin.json manifests are valid and Claude Code compatible."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_plugins_directory():
|
|
10
|
+
"""Get the path to the plugins directory."""
|
|
11
|
+
test_dir = Path(__file__).parent
|
|
12
|
+
return test_dir.parent / "plugins"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_all_plugin_manifests():
|
|
16
|
+
"""Yield (plugin_name, manifest_path) for every plugin with a .plugin/plugin.json."""
|
|
17
|
+
plugins_dir = get_plugins_directory()
|
|
18
|
+
for plugin_dir in sorted(plugins_dir.iterdir()):
|
|
19
|
+
if not plugin_dir.is_dir() or plugin_dir.name.startswith("."):
|
|
20
|
+
continue
|
|
21
|
+
manifest = plugin_dir / ".plugin" / "plugin.json"
|
|
22
|
+
if manifest.exists():
|
|
23
|
+
yield plugin_dir.name, manifest
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
REQUIRED_FIELDS = ["name", "description", "author", "version"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture(params=list(get_all_plugin_manifests()), ids=lambda p: p[0])
|
|
30
|
+
def plugin_manifest(request):
|
|
31
|
+
"""Parametrized fixture returning (name, path, parsed_json) for each plugin."""
|
|
32
|
+
name, path = request.param
|
|
33
|
+
data = json.loads(path.read_text())
|
|
34
|
+
return name, path, data
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_plugin_has_required_fields(plugin_manifest):
|
|
38
|
+
name, path, data = plugin_manifest
|
|
39
|
+
for field in REQUIRED_FIELDS:
|
|
40
|
+
assert field in data, (
|
|
41
|
+
f"Plugin '{name}' is missing required field '{field}' in {path}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_plugin_author_is_object(plugin_manifest):
|
|
46
|
+
"""Claude Code requires 'author' to be an object with at least a 'name' key."""
|
|
47
|
+
name, path, data = plugin_manifest
|
|
48
|
+
author = data.get("author")
|
|
49
|
+
assert isinstance(author, dict), (
|
|
50
|
+
f"Plugin '{name}': 'author' must be an object (got {type(author).__name__}). "
|
|
51
|
+
f"Use {{\"name\": \"...\", \"email\": \"...\"}} format for Claude Code compatibility."
|
|
52
|
+
)
|
|
53
|
+
assert "name" in author, (
|
|
54
|
+
f"Plugin '{name}': 'author' object must contain a 'name' field."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_plugin_json_is_valid(plugin_manifest):
|
|
59
|
+
"""Ensure plugin.json is valid JSON with expected types."""
|
|
60
|
+
name, path, data = plugin_manifest
|
|
61
|
+
assert isinstance(data.get("name"), str), f"Plugin '{name}': 'name' must be a string"
|
|
62
|
+
assert isinstance(data.get("version"), str), f"Plugin '{name}': 'version' must be a string"
|
|
63
|
+
assert isinstance(data.get("description"), str), f"Plugin '{name}': 'description' must be a string"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_all_plugins_have_manifest():
|
|
67
|
+
"""Ensure every plugin directory has a .plugin/plugin.json manifest."""
|
|
68
|
+
plugins_dir = get_plugins_directory()
|
|
69
|
+
plugin_dirs = [
|
|
70
|
+
d.name
|
|
71
|
+
for d in plugins_dir.iterdir()
|
|
72
|
+
if d.is_dir() and not d.name.startswith(".")
|
|
73
|
+
]
|
|
74
|
+
assert len(plugin_dirs) > 0, "No plugin directories found"
|
|
75
|
+
|
|
76
|
+
missing = [
|
|
77
|
+
name
|
|
78
|
+
for name in plugin_dirs
|
|
79
|
+
if not (plugins_dir / name / ".plugin" / "plugin.json").exists()
|
|
80
|
+
]
|
|
81
|
+
assert len(missing) == 0, (
|
|
82
|
+
f"Plugins missing .plugin/plugin.json: {', '.join(missing)}"
|
|
83
|
+
)
|