@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,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
|
+
)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Tests for the per-file diff payload introduced to replace the global byte-
|
|
2
|
+
slice truncation in agent_script.py.
|
|
3
|
+
|
|
4
|
+
The legacy `truncate_text()` cut the raw diff at byte 100,000, which silently
|
|
5
|
+
dropped files whose patches lived past that point (issue #233). These tests
|
|
6
|
+
exercise the replacement: a manifest that lists every file plus per-file
|
|
7
|
+
budgeted patches that mark abbreviation/omission explicitly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# Re-use the module loader from the review-context test file so we don't
|
|
13
|
+
# duplicate ~130 lines of openhands-SDK stubbing.
|
|
14
|
+
from test_pr_review_review_context import _load_agent_script_module
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _file(
|
|
18
|
+
filename: str,
|
|
19
|
+
*,
|
|
20
|
+
status: str = "modified",
|
|
21
|
+
additions: int = 0,
|
|
22
|
+
deletions: int = 0,
|
|
23
|
+
patch: str | None = "",
|
|
24
|
+
previous_filename: str | None = None,
|
|
25
|
+
) -> dict:
|
|
26
|
+
return {
|
|
27
|
+
"filename": filename,
|
|
28
|
+
"status": status,
|
|
29
|
+
"additions": additions,
|
|
30
|
+
"deletions": deletions,
|
|
31
|
+
"patch": patch,
|
|
32
|
+
"previous_filename": previous_filename,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --- manifest -----------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_manifest_lists_every_file():
|
|
40
|
+
module = _load_agent_script_module()
|
|
41
|
+
files = [
|
|
42
|
+
_file("a.py", status="added", additions=10),
|
|
43
|
+
_file("b.py", status="modified", additions=2, deletions=3),
|
|
44
|
+
_file("c.py", status="removed", deletions=15),
|
|
45
|
+
]
|
|
46
|
+
manifest = module.format_files_manifest(files)
|
|
47
|
+
assert "`a.py`" in manifest
|
|
48
|
+
assert "`b.py`" in manifest
|
|
49
|
+
assert "`c.py`" in manifest
|
|
50
|
+
assert "[added]" in manifest
|
|
51
|
+
assert "[modified]" in manifest
|
|
52
|
+
assert "[removed]" in manifest
|
|
53
|
+
assert "+10" in manifest
|
|
54
|
+
assert "+2/-3" in manifest
|
|
55
|
+
assert "-15" in manifest
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_manifest_includes_totals_in_header():
|
|
59
|
+
module = _load_agent_script_module()
|
|
60
|
+
files = [
|
|
61
|
+
_file("a.py", additions=10, deletions=2),
|
|
62
|
+
_file("b.py", additions=5, deletions=1),
|
|
63
|
+
]
|
|
64
|
+
manifest = module.format_files_manifest(files)
|
|
65
|
+
assert "2 files" in manifest
|
|
66
|
+
assert "+15" in manifest
|
|
67
|
+
assert "-3" in manifest
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_manifest_handles_renames():
|
|
71
|
+
module = _load_agent_script_module()
|
|
72
|
+
files = [
|
|
73
|
+
_file(
|
|
74
|
+
"new.py",
|
|
75
|
+
status="renamed",
|
|
76
|
+
additions=1,
|
|
77
|
+
deletions=1,
|
|
78
|
+
previous_filename="old.py",
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
manifest = module.format_files_manifest(files)
|
|
82
|
+
assert "`new.py`" in manifest
|
|
83
|
+
assert "renamed from old.py" in manifest
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_manifest_flags_binary_files():
|
|
87
|
+
module = _load_agent_script_module()
|
|
88
|
+
files = [
|
|
89
|
+
_file("logo.png", status="modified", additions=1, deletions=1, patch=""),
|
|
90
|
+
]
|
|
91
|
+
manifest = module.format_files_manifest(files)
|
|
92
|
+
assert "`logo.png`" in manifest
|
|
93
|
+
assert "binary or unavailable" in manifest
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --- patch formatting ---------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_patches_include_header_for_every_file():
|
|
100
|
+
"""Every file gets a diff header even when only a stub fits."""
|
|
101
|
+
module = _load_agent_script_module()
|
|
102
|
+
files = [
|
|
103
|
+
_file("a.py", status="added", additions=1, patch="@@ -0,0 +1 @@\n+x\n"),
|
|
104
|
+
_file("b.py", status="added", additions=1, patch="@@ -0,0 +1 @@\n+y\n"),
|
|
105
|
+
]
|
|
106
|
+
out = module.format_patches(files)
|
|
107
|
+
assert "diff --git a/a.py b/a.py" in out
|
|
108
|
+
assert "diff --git a/b.py b/b.py" in out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_patches_below_budget_passthrough():
|
|
112
|
+
module = _load_agent_script_module()
|
|
113
|
+
patch = "@@ -1,2 +1,2 @@\n-foo\n+bar\n"
|
|
114
|
+
files = [_file("a.py", patch=patch)]
|
|
115
|
+
out = module.format_patches(files, max_total=10000, max_per_file=10000)
|
|
116
|
+
assert patch in out
|
|
117
|
+
assert "[patch abbreviated" not in out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_per_file_cap_abbreviates_huge_single_file():
|
|
121
|
+
"""A single large patch is abbreviated, not dropped."""
|
|
122
|
+
module = _load_agent_script_module()
|
|
123
|
+
big_patch = "@@ -0,0 +1,1000 @@\n" + "".join(
|
|
124
|
+
f"+line {i}\n" for i in range(1000)
|
|
125
|
+
)
|
|
126
|
+
files = [_file("big.py", status="added", patch=big_patch)]
|
|
127
|
+
out = module.format_patches(files, max_total=100000, max_per_file=500)
|
|
128
|
+
assert "diff --git a/big.py b/big.py" in out
|
|
129
|
+
# The marker is the contract — the agent knows the patch was cut.
|
|
130
|
+
assert "[patch abbreviated" in out
|
|
131
|
+
# Should reference the actual file path so the agent knows where to look.
|
|
132
|
+
assert "`big.py`" in out
|
|
133
|
+
# Must not contain the full patch text.
|
|
134
|
+
assert len(out) < len(big_patch)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_total_budget_omits_late_files_with_marker_not_silence():
|
|
138
|
+
"""When total budget is exhausted, later files become header-only stubs
|
|
139
|
+
with an explicit `[patch omitted]` marker — never silently dropped."""
|
|
140
|
+
module = _load_agent_script_module()
|
|
141
|
+
fat_patch = "@@ -0,0 +1,100 @@\n" + "".join(f"+x{i}\n" for i in range(100))
|
|
142
|
+
files = [
|
|
143
|
+
_file("first.py", status="added", patch=fat_patch),
|
|
144
|
+
_file("second.py", status="added", patch=fat_patch),
|
|
145
|
+
_file("third.py", status="added", patch=fat_patch),
|
|
146
|
+
]
|
|
147
|
+
# Budget that comfortably fits one full patch but not three.
|
|
148
|
+
out = module.format_patches(files, max_total=800, max_per_file=10000)
|
|
149
|
+
|
|
150
|
+
# All three files appear in the patch block — at minimum as headers.
|
|
151
|
+
assert "diff --git a/first.py b/first.py" in out
|
|
152
|
+
assert "diff --git a/second.py b/second.py" in out
|
|
153
|
+
assert "diff --git a/third.py b/third.py" in out
|
|
154
|
+
# At least one of the later files is marked omitted.
|
|
155
|
+
assert "[patch omitted" in out
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_no_patch_field_is_annotated_not_silent():
|
|
159
|
+
"""A file with no patch text (binary, rename, etc) gets a clear note."""
|
|
160
|
+
module = _load_agent_script_module()
|
|
161
|
+
files = [_file("logo.png", additions=1, deletions=1, patch="")]
|
|
162
|
+
out = module.format_patches(files)
|
|
163
|
+
assert "diff --git a/logo.png b/logo.png" in out
|
|
164
|
+
assert "no patch available" in out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_smoking_gun_pr_14401_shape():
|
|
168
|
+
"""Regression for issue #233 — the specific failure mode that triggered
|
|
169
|
+
the redesign. Earlier files have huge patches; the implementation file
|
|
170
|
+
of interest sits past the byte-100K mark of the original raw diff. With
|
|
171
|
+
a per-file budget the late file's *patch* may still be abbreviated, but
|
|
172
|
+
its *presence* must always be visible in both manifest and patch block."""
|
|
173
|
+
module = _load_agent_script_module()
|
|
174
|
+
|
|
175
|
+
# Two heavy files dominate the byte budget.
|
|
176
|
+
bulk_patch = "@@ -1 +1,2000 @@\n" + "".join(
|
|
177
|
+
f"+filler {i}\n" for i in range(2000)
|
|
178
|
+
)
|
|
179
|
+
# The implementation file the bot kept missing.
|
|
180
|
+
target = (
|
|
181
|
+
"@@ -0,0 +1,55 @@\n"
|
|
182
|
+
+ "".join(f"+line {i}\n" for i in range(55))
|
|
183
|
+
)
|
|
184
|
+
files = [
|
|
185
|
+
_file("enterprise/poetry.lock", patch=bulk_patch),
|
|
186
|
+
_file("openhands/long_module.py", patch=bulk_patch),
|
|
187
|
+
_file(
|
|
188
|
+
"frontend/src/utils/shell-tokenize.ts",
|
|
189
|
+
status="added",
|
|
190
|
+
additions=55,
|
|
191
|
+
patch=target,
|
|
192
|
+
),
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
manifest = module.format_files_manifest(files)
|
|
196
|
+
patches = module.format_patches(files, max_total=2000, max_per_file=600)
|
|
197
|
+
|
|
198
|
+
# Manifest always names the target file — this is the property that
|
|
199
|
+
# prevents the bot from claiming the file is missing.
|
|
200
|
+
assert "`frontend/src/utils/shell-tokenize.ts`" in manifest
|
|
201
|
+
# Patch block lists it too, even when its content is abbreviated/omitted.
|
|
202
|
+
assert "diff --git a/frontend/src/utils/shell-tokenize.ts" in patches
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Tests for PR review feedback collection."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_ROOT = Path(__file__).parent.parent
|
|
12
|
+
_PR_REVIEW_SCRIPTS = _ROOT / "plugins" / "pr-review" / "scripts"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_prompt_module():
|
|
16
|
+
path = _PR_REVIEW_SCRIPTS / "prompt.py"
|
|
17
|
+
spec = importlib.util.spec_from_file_location("pr_review_prompt_feedback", path)
|
|
18
|
+
module = importlib.util.module_from_spec(spec)
|
|
19
|
+
sys.modules[spec.name] = module
|
|
20
|
+
spec.loader.exec_module(module)
|
|
21
|
+
return module
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_eval_module():
|
|
26
|
+
"""Load evaluate_review.py with Laminar stubbed."""
|
|
27
|
+
lmnr_mod = types.ModuleType("lmnr")
|
|
28
|
+
|
|
29
|
+
class _FakeLaminar:
|
|
30
|
+
@staticmethod
|
|
31
|
+
def initialize():
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def get_trace_id():
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def get_laminar_span_context():
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def set_trace_metadata(metadata):
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def set_span_output(output):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def flush():
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def start_as_current_span(**kwargs):
|
|
56
|
+
import contextlib
|
|
57
|
+
|
|
58
|
+
return contextlib.nullcontext()
|
|
59
|
+
|
|
60
|
+
class _FakeClient:
|
|
61
|
+
class evaluators:
|
|
62
|
+
@staticmethod
|
|
63
|
+
def score(**kwargs):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
class tags:
|
|
67
|
+
@staticmethod
|
|
68
|
+
def tag(trace_id, tags):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
lmnr_mod.Laminar = _FakeLaminar
|
|
72
|
+
lmnr_mod.LaminarClient = _FakeClient
|
|
73
|
+
|
|
74
|
+
saved = sys.modules.get("lmnr")
|
|
75
|
+
sys.modules["lmnr"] = lmnr_mod
|
|
76
|
+
try:
|
|
77
|
+
path = _PR_REVIEW_SCRIPTS / "evaluate_review.py"
|
|
78
|
+
spec = importlib.util.spec_from_file_location("pr_review_evaluate", path)
|
|
79
|
+
module = importlib.util.module_from_spec(spec)
|
|
80
|
+
sys.modules[spec.name] = module
|
|
81
|
+
spec.loader.exec_module(module)
|
|
82
|
+
return module
|
|
83
|
+
finally:
|
|
84
|
+
if saved is None:
|
|
85
|
+
sys.modules.pop("lmnr", None)
|
|
86
|
+
else:
|
|
87
|
+
sys.modules["lmnr"] = saved
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _format_prompt(*, collect_feedback: bool, review_run_url: str = "") -> str:
|
|
92
|
+
module = _load_prompt_module()
|
|
93
|
+
return module.format_prompt(
|
|
94
|
+
skill_trigger="/codereview",
|
|
95
|
+
title="Add review feedback footer",
|
|
96
|
+
body="## Summary\nCapture review reactions without extra PR spam.",
|
|
97
|
+
repo_name="OpenHands/extensions",
|
|
98
|
+
base_branch="main",
|
|
99
|
+
head_branch="feature/feedback-footer",
|
|
100
|
+
pr_number="249",
|
|
101
|
+
commit_id="abc123",
|
|
102
|
+
diff="diff --git a/file b/file",
|
|
103
|
+
review_context="",
|
|
104
|
+
require_evidence=False,
|
|
105
|
+
collect_feedback=collect_feedback,
|
|
106
|
+
review_run_url=review_run_url,
|
|
107
|
+
use_sub_agents=False,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_action_collect_feedback_defaults_to_true_and_uses_agent_prompt():
|
|
113
|
+
action_yml = _ROOT / "plugins" / "pr-review" / "action.yml"
|
|
114
|
+
with open(action_yml) as f:
|
|
115
|
+
action = yaml.safe_load(f)
|
|
116
|
+
|
|
117
|
+
collect_feedback = action["inputs"]["collect-feedback"]
|
|
118
|
+
assert collect_feedback["default"] == "true"
|
|
119
|
+
|
|
120
|
+
run_step = next(
|
|
121
|
+
step for step in action["runs"]["steps"] if step["name"] == "Run PR review"
|
|
122
|
+
)
|
|
123
|
+
assert run_step["env"]["COLLECT_FEEDBACK"] == "${{ inputs.collect-feedback }}"
|
|
124
|
+
assert run_step["env"]["REVIEW_RUN_URL"] == (
|
|
125
|
+
"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
126
|
+
)
|
|
127
|
+
assert all(
|
|
128
|
+
step["name"] != "Post PR review feedback prompt"
|
|
129
|
+
for step in action["runs"]["steps"]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_prompt_omits_feedback_footer_by_default():
|
|
135
|
+
prompt = _format_prompt(collect_feedback=False)
|
|
136
|
+
|
|
137
|
+
assert "## Review Feedback Footer" not in prompt
|
|
138
|
+
assert "React with 👍 or 👎 to this review" not in prompt
|
|
139
|
+
assert "<!-- openhands-pr-review-feedback -->" not in prompt
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_prompt_includes_feedback_footer_when_enabled():
|
|
144
|
+
prompt = _format_prompt(
|
|
145
|
+
collect_feedback=True,
|
|
146
|
+
review_run_url="https://github.com/OpenHands/extensions/actions/runs/123",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
assert "## Review Feedback Footer" in prompt
|
|
150
|
+
assert "main review body, not in a separate issue comment" in prompt
|
|
151
|
+
assert "React with 👍 or 👎 to this review" in prompt
|
|
152
|
+
assert "https://github.com/OpenHands/extensions/actions/runs/123" in prompt
|
|
153
|
+
assert "<!-- openhands-pr-review-feedback -->" in prompt
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_extract_review_feedback_counts_thumbs_reactions_from_reviews():
|
|
158
|
+
module = _load_eval_module()
|
|
159
|
+
|
|
160
|
+
reviews = [
|
|
161
|
+
{
|
|
162
|
+
"id": 101,
|
|
163
|
+
"user": {"login": "openhands-agent"},
|
|
164
|
+
"body": "Automated review\n<!-- openhands-pr-review-feedback -->",
|
|
165
|
+
"submitted_at": "2026-05-19T12:00:00Z",
|
|
166
|
+
"reactions": {"+1": 3, "-1": 1, "total_count": 4},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"id": 102,
|
|
170
|
+
"user": {"login": "openhands-agent"},
|
|
171
|
+
"body": "Regular review comment",
|
|
172
|
+
"reactions": {"+1": 100, "-1": 100},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"id": 103,
|
|
176
|
+
"user": {"login": "human-dev"},
|
|
177
|
+
"body": "<!-- openhands-pr-review-feedback -->",
|
|
178
|
+
"reactions": {"+1": 5, "-1": 0},
|
|
179
|
+
},
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
assert module.extract_review_feedback([], reviews) == [
|
|
183
|
+
{
|
|
184
|
+
"comment_id": 101,
|
|
185
|
+
"created_at": "2026-05-19T12:00:00Z",
|
|
186
|
+
"thumbs_up": 3,
|
|
187
|
+
"thumbs_down": 1,
|
|
188
|
+
"total": 4,
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_extract_review_feedback_keeps_legacy_issue_comment_support():
|
|
195
|
+
module = _load_eval_module()
|
|
196
|
+
|
|
197
|
+
result = module.extract_review_feedback(
|
|
198
|
+
[
|
|
199
|
+
{
|
|
200
|
+
"id": 201,
|
|
201
|
+
"user": {"login": "all-hands-bot"},
|
|
202
|
+
"body": "<!-- openhands-pr-review-feedback -->",
|
|
203
|
+
"created_at": "2026-05-19T12:00:00Z",
|
|
204
|
+
"reactions": {"+1": 2, "-1": 0},
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
assert result == [
|
|
210
|
+
{
|
|
211
|
+
"comment_id": 201,
|
|
212
|
+
"created_at": "2026-05-19T12:00:00Z",
|
|
213
|
+
"thumbs_up": 2,
|
|
214
|
+
"thumbs_down": 0,
|
|
215
|
+
"total": 2,
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_extract_review_feedback_handles_missing_reactions():
|
|
222
|
+
module = _load_eval_module()
|
|
223
|
+
|
|
224
|
+
result = module.extract_review_feedback(
|
|
225
|
+
[],
|
|
226
|
+
[
|
|
227
|
+
{
|
|
228
|
+
"id": 301,
|
|
229
|
+
"user": {"login": "all-hands-bot"},
|
|
230
|
+
"body": "<!-- openhands-pr-review-feedback -->",
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
assert result == [
|
|
236
|
+
{
|
|
237
|
+
"comment_id": 301,
|
|
238
|
+
"created_at": None,
|
|
239
|
+
"thumbs_up": 0,
|
|
240
|
+
"thumbs_down": 0,
|
|
241
|
+
"total": 0,
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_extract_review_feedback_accepts_github_actions_bot():
|
|
248
|
+
module = _load_eval_module()
|
|
249
|
+
|
|
250
|
+
result = module.extract_review_feedback(
|
|
251
|
+
[],
|
|
252
|
+
[
|
|
253
|
+
{
|
|
254
|
+
"id": 401,
|
|
255
|
+
"user": {"login": "github-actions[bot]"},
|
|
256
|
+
"body": "<!-- openhands-pr-review-feedback -->",
|
|
257
|
+
"reactions": {"+1": 1, "-1": 2},
|
|
258
|
+
}
|
|
259
|
+
],
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
assert result[0]["thumbs_up"] == 1
|
|
263
|
+
assert result[0]["thumbs_down"] == 2
|