@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.
Files changed (347) hide show
  1. package/.agents/skills/custom-codereview-guide.md +25 -0
  2. package/.github/pull_request_template.md +38 -0
  3. package/.github/release.yml +14 -0
  4. package/.github/workflows/check-extensions.yml +72 -0
  5. package/.github/workflows/npm-publish.yml +89 -0
  6. package/.github/workflows/pr.yml +30 -0
  7. package/.github/workflows/release.yml +24 -0
  8. package/.github/workflows/tests.yml +25 -0
  9. package/.github/workflows/vulnerability-scan.yml +87 -0
  10. package/.release-please-manifest.json +3 -0
  11. package/AGENTS.md +132 -0
  12. package/README.md +10 -0
  13. package/analysis_results.md +162 -0
  14. package/marketplaces/large-codebase.json +66 -0
  15. package/marketplaces/openhands-extensions.json +682 -0
  16. package/package.json +4 -10
  17. package/plugins/README.md +30 -0
  18. package/plugins/city-weather/.plugin/plugin.json +13 -0
  19. package/plugins/city-weather/README.md +145 -0
  20. package/plugins/city-weather/commands/now.md +56 -0
  21. package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
  22. package/plugins/cobol-modernization/README.md +201 -0
  23. package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
  24. package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
  25. package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
  26. package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
  27. package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
  28. package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
  29. package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
  30. package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
  31. package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
  32. package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
  33. package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
  34. package/plugins/issue-duplicate-checker/README.md +51 -0
  35. package/plugins/issue-duplicate-checker/action.yml +349 -0
  36. package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
  37. package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
  38. package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
  39. package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
  40. package/plugins/magic-test/.plugin/plugin.json +13 -0
  41. package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
  42. package/plugins/migration-scoring/.plugin/plugin.json +19 -0
  43. package/plugins/migration-scoring/README.md +244 -0
  44. package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
  45. package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
  46. package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
  47. package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
  48. package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
  49. package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
  50. package/plugins/onboarding/.plugin/plugin.json +20 -0
  51. package/plugins/onboarding/README.md +30 -0
  52. package/plugins/onboarding/references/criteria.md +144 -0
  53. package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
  54. package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
  55. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
  56. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
  57. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
  58. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
  59. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
  60. package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
  61. package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
  62. package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
  63. package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
  64. package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
  65. package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
  66. package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
  67. package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
  68. package/plugins/openhands/.plugin/plugin.json +13 -0
  69. package/plugins/openhands/README.md +52 -0
  70. package/plugins/openhands/SKILL.md +61 -0
  71. package/plugins/openhands/commands/create.md +55 -0
  72. package/plugins/openhands/commands/openhands-cloud.md +8 -0
  73. package/plugins/openhands/scripts/run.sh +69 -0
  74. package/plugins/pr-review/.plugin/plugin.json +13 -0
  75. package/plugins/pr-review/README.md +393 -0
  76. package/plugins/pr-review/action.yml +298 -0
  77. package/plugins/pr-review/scripts/agent_script.py +1282 -0
  78. package/plugins/pr-review/scripts/evaluate_review.py +655 -0
  79. package/plugins/pr-review/scripts/prompt.py +260 -0
  80. package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
  81. package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
  82. package/plugins/qa-changes/.plugin/plugin.json +11 -0
  83. package/plugins/qa-changes/README.md +185 -0
  84. package/plugins/qa-changes/action.yml +181 -0
  85. package/plugins/qa-changes/scripts/agent_script.py +406 -0
  86. package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
  87. package/plugins/qa-changes/scripts/prompt.py +174 -0
  88. package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
  89. package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
  90. package/plugins/release-notes/.plugin/plugin.json +19 -0
  91. package/plugins/release-notes/README.md +283 -0
  92. package/plugins/release-notes/SKILL.md +83 -0
  93. package/plugins/release-notes/action.yml +117 -0
  94. package/plugins/release-notes/commands/release-notes.md +8 -0
  95. package/plugins/release-notes/scripts/agent_script.py +292 -0
  96. package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
  97. package/plugins/release-notes/scripts/prompt.py +90 -0
  98. package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
  99. package/plugins/release-notes/workflows/release-notes.yml +76 -0
  100. package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
  101. package/plugins/vulnerability-remediation/README.md +217 -0
  102. package/plugins/vulnerability-remediation/action.yml +187 -0
  103. package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
  104. package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
  105. package/pyproject.toml +12 -0
  106. package/release-please-config.json +16 -0
  107. package/scripts/sync_extensions.py +494 -0
  108. package/scripts/sync_openhands_sdk_skill.py +264 -0
  109. package/skills/README.md +159 -0
  110. package/skills/add-javadoc/.plugin/plugin.json +18 -0
  111. package/skills/add-javadoc/README.md +40 -0
  112. package/skills/add-javadoc/SKILL.md +35 -0
  113. package/skills/add-javadoc/references/example.md +32 -0
  114. package/skills/add-skill/.plugin/plugin.json +18 -0
  115. package/skills/add-skill/README.md +67 -0
  116. package/skills/add-skill/SKILL.md +47 -0
  117. package/skills/add-skill/scripts/fetch_skill.py +259 -0
  118. package/skills/agent-creator/.plugin/plugin.json +20 -0
  119. package/skills/agent-creator/README.md +104 -0
  120. package/skills/agent-creator/SKILL.md +190 -0
  121. package/skills/agent-creator/commands/agent-creator.md +8 -0
  122. package/skills/agent-creator/references/fallback.md +117 -0
  123. package/skills/agent-memory/.plugin/plugin.json +18 -0
  124. package/skills/agent-memory/README.md +35 -0
  125. package/skills/agent-memory/SKILL.md +30 -0
  126. package/skills/agent-memory/commands/remember.md +8 -0
  127. package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
  128. package/skills/agent-sdk-builder/README.md +40 -0
  129. package/skills/agent-sdk-builder/SKILL.md +37 -0
  130. package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
  131. package/skills/azure-devops/.plugin/plugin.json +18 -0
  132. package/skills/azure-devops/README.md +55 -0
  133. package/skills/azure-devops/SKILL.md +50 -0
  134. package/skills/bitbucket/.plugin/plugin.json +17 -0
  135. package/skills/bitbucket/README.md +50 -0
  136. package/skills/bitbucket/SKILL.md +45 -0
  137. package/skills/code-review/.plugin/plugin.json +19 -0
  138. package/skills/code-review/README.md +18 -0
  139. package/skills/code-review/SKILL.md +208 -0
  140. package/skills/code-review/commands/codereview-roasted.md +8 -0
  141. package/skills/code-review/commands/codereview.md +8 -0
  142. package/skills/code-review/references/risk-evaluation.md +41 -0
  143. package/skills/code-review/references/supply-chain-security.md +31 -0
  144. package/skills/code-simplifier/.plugin/plugin.json +21 -0
  145. package/skills/code-simplifier/README.md +30 -0
  146. package/skills/code-simplifier/SKILL.md +91 -0
  147. package/skills/code-simplifier/commands/simplify.md +8 -0
  148. package/skills/code-simplifier/references/code-quality-review.md +86 -0
  149. package/skills/code-simplifier/references/code-reuse-review.md +63 -0
  150. package/skills/code-simplifier/references/efficiency-review.md +81 -0
  151. package/skills/datadog/.plugin/plugin.json +19 -0
  152. package/skills/datadog/README.md +100 -0
  153. package/skills/datadog/SKILL.md +95 -0
  154. package/skills/deno/.plugin/plugin.json +18 -0
  155. package/skills/deno/README.md +5 -0
  156. package/skills/deno/SKILL.md +99 -0
  157. package/skills/deno/references/README.md +6 -0
  158. package/skills/discord/.plugin/plugin.json +18 -0
  159. package/skills/discord/README.md +31 -0
  160. package/skills/discord/SKILL.md +109 -0
  161. package/skills/discord/__init__.py +0 -0
  162. package/skills/discord/references/REFERENCE.md +78 -0
  163. package/skills/discord/scripts/__init__.py +0 -0
  164. package/skills/discord/scripts/_http.py +127 -0
  165. package/skills/discord/scripts/post_webhook.py +106 -0
  166. package/skills/discord/scripts/send_message.py +102 -0
  167. package/skills/docker/.plugin/plugin.json +17 -0
  168. package/skills/docker/README.md +34 -0
  169. package/skills/docker/SKILL.md +29 -0
  170. package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
  171. package/skills/evidence-based-citations/README.md +31 -0
  172. package/skills/evidence-based-citations/SKILL.md +59 -0
  173. package/skills/flarglebargle/.plugin/plugin.json +16 -0
  174. package/skills/flarglebargle/README.md +14 -0
  175. package/skills/flarglebargle/SKILL.md +9 -0
  176. package/skills/frontend-design/.plugin/plugin.json +21 -0
  177. package/skills/frontend-design/LICENSE.txt +177 -0
  178. package/skills/frontend-design/README.md +42 -0
  179. package/skills/frontend-design/SKILL.md +42 -0
  180. package/skills/github/.plugin/plugin.json +19 -0
  181. package/skills/github/README.md +42 -0
  182. package/skills/github/SKILL.md +106 -0
  183. package/skills/github-pr-review/.plugin/plugin.json +18 -0
  184. package/skills/github-pr-review/README.md +145 -0
  185. package/skills/github-pr-review/SKILL.md +148 -0
  186. package/skills/github-pr-review/commands/github-pr-review.md +8 -0
  187. package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
  188. package/skills/github-pr-reviewer/README.md +34 -0
  189. package/skills/github-pr-reviewer/SKILL.md +89 -0
  190. package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
  191. package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
  192. package/skills/github-repo-monitor/README.md +70 -0
  193. package/skills/github-repo-monitor/SKILL.md +316 -0
  194. package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
  195. package/skills/github-repo-monitor/references/github-api.md +241 -0
  196. package/skills/github-repo-monitor/references/state-schema.md +160 -0
  197. package/skills/github-repo-monitor/scripts/main.py +915 -0
  198. package/skills/github-repo-monitor/tests/test_main.py +400 -0
  199. package/skills/gitlab/.plugin/plugin.json +17 -0
  200. package/skills/gitlab/README.md +37 -0
  201. package/skills/gitlab/SKILL.md +32 -0
  202. package/skills/incident-retrospective/.plugin/plugin.json +21 -0
  203. package/skills/incident-retrospective/README.md +34 -0
  204. package/skills/incident-retrospective/SKILL.md +98 -0
  205. package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
  206. package/skills/iterate/.plugin/plugin.json +13 -0
  207. package/skills/iterate/README.md +25 -0
  208. package/skills/iterate/SKILL.md +399 -0
  209. package/skills/iterate/commands/babysit.md +8 -0
  210. package/skills/iterate/commands/iterate.md +8 -0
  211. package/skills/iterate/commands/verify.md +8 -0
  212. package/skills/iterate/references/heuristics.md +58 -0
  213. package/skills/iterate/references/verification.md +96 -0
  214. package/skills/jupyter/.plugin/plugin.json +18 -0
  215. package/skills/jupyter/README.md +55 -0
  216. package/skills/jupyter/SKILL.md +50 -0
  217. package/skills/kubernetes/.plugin/plugin.json +18 -0
  218. package/skills/kubernetes/README.md +53 -0
  219. package/skills/kubernetes/SKILL.md +48 -0
  220. package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
  221. package/skills/learn-from-code-review/README.md +64 -0
  222. package/skills/learn-from-code-review/SKILL.md +186 -0
  223. package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
  224. package/skills/linear/.plugin/plugin.json +19 -0
  225. package/skills/linear/README.md +58 -0
  226. package/skills/linear/SKILL.md +213 -0
  227. package/skills/linear-triage/.plugin/plugin.json +21 -0
  228. package/skills/linear-triage/README.md +34 -0
  229. package/skills/linear-triage/SKILL.md +91 -0
  230. package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
  231. package/skills/notion/.plugin/plugin.json +17 -0
  232. package/skills/notion/README.md +114 -0
  233. package/skills/notion/SKILL.md +109 -0
  234. package/skills/npm/.plugin/plugin.json +17 -0
  235. package/skills/npm/README.md +14 -0
  236. package/skills/npm/SKILL.md +9 -0
  237. package/skills/openhands-api/.plugin/plugin.json +22 -0
  238. package/skills/openhands-api/README.md +48 -0
  239. package/skills/openhands-api/SKILL.md +399 -0
  240. package/skills/openhands-api/references/README.md +33 -0
  241. package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  242. package/skills/openhands-api/references/example_prompt.md +12 -0
  243. package/skills/openhands-api/scripts/openhands_api.py +606 -0
  244. package/skills/openhands-api/scripts/openhands_api.ts +252 -0
  245. package/skills/openhands-automation/.plugin/plugin.json +19 -0
  246. package/skills/openhands-automation/README.md +89 -0
  247. package/skills/openhands-automation/SKILL.md +875 -0
  248. package/skills/openhands-automation/commands/automation:create.md +8 -0
  249. package/skills/openhands-automation/references/ab-testing.md +185 -0
  250. package/skills/openhands-automation/references/custom-automation.md +644 -0
  251. package/skills/openhands-sdk/.plugin/plugin.json +20 -0
  252. package/skills/openhands-sdk/README.md +22 -0
  253. package/skills/openhands-sdk/SKILL.md +229 -0
  254. package/skills/openhands-sdk/commands/sdk.md +8 -0
  255. package/skills/pdflatex/.plugin/plugin.json +18 -0
  256. package/skills/pdflatex/README.md +39 -0
  257. package/skills/pdflatex/SKILL.md +34 -0
  258. package/skills/prd/.plugin/plugin.json +19 -0
  259. package/skills/prd/README.md +28 -0
  260. package/skills/prd/SKILL.md +237 -0
  261. package/skills/prd/commands/prd.md +8 -0
  262. package/skills/qa-changes/README.md +18 -0
  263. package/skills/qa-changes/SKILL.md +229 -0
  264. package/skills/qa-changes/commands/qa-changes.md +8 -0
  265. package/skills/release-notes/README.md +24 -0
  266. package/skills/release-notes/SKILL.md +19 -0
  267. package/skills/release-notes/commands/release-notes.md +8 -0
  268. package/skills/research-brief/.plugin/plugin.json +20 -0
  269. package/skills/research-brief/README.md +34 -0
  270. package/skills/research-brief/SKILL.md +99 -0
  271. package/skills/research-brief/commands/research-brief:setup.md +8 -0
  272. package/skills/security/.plugin/plugin.json +18 -0
  273. package/skills/security/README.md +38 -0
  274. package/skills/security/SKILL.md +33 -0
  275. package/skills/skill-creator/.plugin/plugin.json +17 -0
  276. package/skills/skill-creator/LICENSE.txt +202 -0
  277. package/skills/skill-creator/README.md +182 -0
  278. package/skills/skill-creator/SKILL.md +545 -0
  279. package/skills/skill-creator/references/output-patterns.md +82 -0
  280. package/skills/skill-creator/references/workflows.md +28 -0
  281. package/skills/skill-creator/scripts/init_skill.py +303 -0
  282. package/skills/skill-creator/scripts/quick_validate.py +95 -0
  283. package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
  284. package/skills/slack-channel-monitor/README.md +91 -0
  285. package/skills/slack-channel-monitor/SKILL.md +276 -0
  286. package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
  287. package/skills/slack-channel-monitor/references/slack-api.md +207 -0
  288. package/skills/slack-channel-monitor/references/state-schema.md +180 -0
  289. package/skills/slack-channel-monitor/scripts/main.py +962 -0
  290. package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
  291. package/skills/slack-standup-digest/README.md +34 -0
  292. package/skills/slack-standup-digest/SKILL.md +92 -0
  293. package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
  294. package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
  295. package/skills/spark-version-upgrade/README.md +54 -0
  296. package/skills/spark-version-upgrade/SKILL.md +233 -0
  297. package/skills/ssh/.plugin/plugin.json +18 -0
  298. package/skills/ssh/README.md +140 -0
  299. package/skills/ssh/SKILL.md +135 -0
  300. package/skills/swift-linux/.plugin/plugin.json +17 -0
  301. package/skills/swift-linux/README.md +86 -0
  302. package/skills/swift-linux/SKILL.md +81 -0
  303. package/skills/theme-factory/.plugin/plugin.json +19 -0
  304. package/skills/theme-factory/LICENSE.txt +202 -0
  305. package/skills/theme-factory/README.md +58 -0
  306. package/skills/theme-factory/SKILL.md +59 -0
  307. package/skills/theme-factory/theme-showcase.pdf +0 -0
  308. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  309. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  310. package/skills/theme-factory/themes/desert-rose.md +19 -0
  311. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  312. package/skills/theme-factory/themes/golden-hour.md +19 -0
  313. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  314. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  315. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  316. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  317. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  318. package/skills/uv/.plugin/plugin.json +18 -0
  319. package/skills/uv/README.md +5 -0
  320. package/skills/uv/SKILL.md +95 -0
  321. package/skills/uv/references/README.md +5 -0
  322. package/skills/vercel/.plugin/plugin.json +18 -0
  323. package/skills/vercel/README.md +108 -0
  324. package/skills/vercel/SKILL.md +103 -0
  325. package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
  326. package/tests/test_catalogs.py +109 -0
  327. package/tests/test_code_review_risk_evaluation.py +94 -0
  328. package/tests/test_issue_duplicate_checker.py +240 -0
  329. package/tests/test_openhands_api_python.py +152 -0
  330. package/tests/test_plugin_manifest.py +83 -0
  331. package/tests/test_pr_review_diff_payload.py +202 -0
  332. package/tests/test_pr_review_feedback.py +263 -0
  333. package/tests/test_pr_review_prompt.py +152 -0
  334. package/tests/test_pr_review_review_context.py +253 -0
  335. package/tests/test_qa_changes.py +232 -0
  336. package/tests/test_qa_changes_evaluation.py +259 -0
  337. package/tests/test_release_notes_generator.py +990 -0
  338. package/tests/test_sdk_loading.py +150 -0
  339. package/tests/test_skill_plugin_loading.py +149 -0
  340. package/tests/test_skills_have_readme.py +66 -0
  341. package/tests/test_sync_extensions.py +292 -0
  342. package/tests/test_workflow_sync.py +46 -0
  343. package/utils/analysis/README.md +7 -0
  344. package/utils/analysis/laminar_signals/README.md +211 -0
  345. package/utils/analysis/laminar_signals/analyze.py +780 -0
  346. package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
  347. package/utils/analysis/laminar_signals/templates/pr_review.j2 +61 -0
@@ -0,0 +1,253 @@
1
+ import importlib.util
2
+ import sys
3
+ import types
4
+ from pathlib import Path
5
+
6
+
7
+ def _ensure_package(name: str) -> types.ModuleType:
8
+ module = sys.modules.get(name)
9
+ if module is None:
10
+ module = types.ModuleType(name)
11
+ sys.modules[name] = module
12
+ return module
13
+
14
+
15
+ def _load_agent_script_module():
16
+ _ensure_package("openhands")
17
+ _ensure_package("openhands.sdk")
18
+ _ensure_package("openhands.sdk.context")
19
+ _ensure_package("openhands.sdk.git")
20
+ _ensure_package("openhands.tools")
21
+ _ensure_package("openhands.tools.preset")
22
+
23
+ lmnr = types.ModuleType("lmnr")
24
+
25
+ class _Laminar:
26
+ @staticmethod
27
+ def get_trace_id():
28
+ return None
29
+
30
+ @staticmethod
31
+ def get_laminar_span_context():
32
+ return None
33
+
34
+ @staticmethod
35
+ def flush():
36
+ return None
37
+
38
+ @staticmethod
39
+ def start_as_current_span(*args, **kwargs):
40
+ class _ContextManager:
41
+ def __enter__(self):
42
+ return None
43
+
44
+ def __exit__(self, exc_type, exc, tb):
45
+ return False
46
+
47
+ return _ContextManager()
48
+
49
+ @staticmethod
50
+ def set_trace_metadata(metadata):
51
+ return None
52
+
53
+ lmnr.Laminar = _Laminar
54
+ sys.modules["lmnr"] = lmnr
55
+
56
+ class _Stub:
57
+ """Generic stub that accepts any arguments."""
58
+ def __init__(self, *args, **kwargs):
59
+ for k, v in kwargs.items():
60
+ setattr(self, k, v)
61
+
62
+ def model_copy(self, update=None):
63
+ copied = _Stub(**self.__dict__)
64
+ if update:
65
+ for k, v in update.items():
66
+ setattr(copied, k, v)
67
+ return copied
68
+
69
+ sdk = types.ModuleType("openhands.sdk")
70
+ sdk.LLM = _Stub
71
+ sdk.Agent = _Stub
72
+ sdk.AgentContext = _Stub
73
+ sdk.Conversation = _Stub
74
+ sdk.Tool = _Stub
75
+
76
+ class _Logger:
77
+ def info(self, *args, **kwargs):
78
+ return None
79
+
80
+ def warning(self, *args, **kwargs):
81
+ return None
82
+
83
+ def error(self, *args, **kwargs):
84
+ return None
85
+
86
+ def debug(self, *args, **kwargs):
87
+ return None
88
+
89
+ sdk.get_logger = lambda name: _Logger()
90
+ sys.modules["openhands.sdk"] = sdk
91
+
92
+ class _Skill:
93
+ def __init__(self, **kwargs):
94
+ for k, v in kwargs.items():
95
+ setattr(self, k, v)
96
+
97
+ sdk_context = _ensure_package("openhands.sdk.context")
98
+ sdk_context.Skill = _Skill
99
+ sys.modules["openhands.sdk.context"] = sdk_context
100
+
101
+ sdk_skills = types.ModuleType("openhands.sdk.skills")
102
+ sdk_skills.load_project_skills = lambda cwd: []
103
+ sys.modules["openhands.sdk.skills"] = sdk_skills
104
+
105
+ conversation = types.ModuleType("openhands.sdk.conversation")
106
+ conversation.get_agent_final_response = lambda events: ""
107
+ sys.modules["openhands.sdk.conversation"] = conversation
108
+
109
+ git_utils = types.ModuleType("openhands.sdk.git.utils")
110
+ git_utils.run_git_command = lambda command, repo_dir: "deadbeef"
111
+ sys.modules["openhands.sdk.git.utils"] = git_utils
112
+
113
+ sdk_plugin = types.ModuleType("openhands.sdk.plugin")
114
+ sdk_plugin.PluginSource = _Stub
115
+ sys.modules["openhands.sdk.plugin"] = sdk_plugin
116
+
117
+ tools_delegate = types.ModuleType("openhands.tools.delegate")
118
+ tools_delegate.DelegationVisualizer = object
119
+ sys.modules["openhands.tools.delegate"] = tools_delegate
120
+
121
+ # register_agent lives in openhands.sdk, not openhands.tools.delegate
122
+ sdk.register_agent = lambda **kwargs: None
123
+
124
+ tools_task = types.ModuleType("openhands.tools.task")
125
+
126
+ class _TaskToolSet:
127
+ name = "TaskToolSet"
128
+
129
+ tools_task.TaskToolSet = _TaskToolSet
130
+ sys.modules["openhands.tools.task"] = tools_task
131
+
132
+ tools_preset = types.ModuleType("openhands.tools.preset.default")
133
+ tools_preset.get_default_condenser = lambda llm: None
134
+ tools_preset.get_default_tools = lambda enable_browser=False: []
135
+ sys.modules["openhands.tools.preset.default"] = tools_preset
136
+
137
+ # Clear any cached 'prompt' module so agent_script.py picks up the
138
+ # correct prompt.py from its own scripts/ directory (not the one from
139
+ # another plugin like release-notes).
140
+ sys.modules.pop("prompt", None)
141
+
142
+ script_path = (
143
+ Path(__file__).parent.parent
144
+ / "plugins"
145
+ / "pr-review"
146
+ / "scripts"
147
+ / "agent_script.py"
148
+ )
149
+ spec = importlib.util.spec_from_file_location("pr_review_agent_script", script_path)
150
+ module = importlib.util.module_from_spec(spec)
151
+ sys.modules["pr_review_agent_script"] = module
152
+ spec.loader.exec_module(module)
153
+ return module
154
+
155
+
156
+ def test_get_review_comment_body_uses_body_text_for_empty_suggestion_block():
157
+ module = _load_agent_script_module()
158
+
159
+ body = module._get_review_comment_body(
160
+ {
161
+ "body": "```suggestion\n```",
162
+ "bodyText": (
163
+ "Suggested change\n"
164
+ " \n"
165
+ "- Do **NOT** approve the PR.\n"
166
+ " \n"
167
+ "- Leave a **COMMENT** review with the exact package details."
168
+ ),
169
+ }
170
+ )
171
+
172
+ assert body == (
173
+ "Suggested change\n\n"
174
+ "- Do **NOT** approve the PR.\n\n"
175
+ "- Leave a **COMMENT** review with the exact package details."
176
+ )
177
+
178
+
179
+ def test_format_thread_includes_rendered_suggestion_text_in_review_context():
180
+ module = _load_agent_script_module()
181
+
182
+ lines = module._format_thread(
183
+ {
184
+ "path": ".agents/skills/custom-codereview-guide.md",
185
+ "line": 96,
186
+ "isOutdated": False,
187
+ "isResolved": False,
188
+ "comments": {
189
+ "nodes": [
190
+ {
191
+ "author": {"login": "enyst"},
192
+ "body": "```suggestion\n```",
193
+ "bodyText": (
194
+ "Suggested change\n"
195
+ "\n"
196
+ "- Do **NOT** approve the PR.\n"
197
+ "\n"
198
+ "- Explain that Dependabot ignores the freshness guardrail."
199
+ ),
200
+ }
201
+ ]
202
+ },
203
+ }
204
+ )
205
+
206
+ formatted = "\n".join(lines)
207
+
208
+ assert "**.agents/skills/custom-codereview-guide.md:96** - ⚠️ UNRESOLVED" in formatted
209
+ assert "Suggested change" in formatted
210
+ assert "- Do **NOT** approve the PR." in formatted
211
+ assert "Dependabot ignores the freshness guardrail" in formatted
212
+ assert "```suggestion" not in formatted
213
+
214
+
215
+ def test_register_sub_agents_completes_without_error():
216
+ """Smoke test: _register_sub_agents() runs without raising."""
217
+ module = _load_agent_script_module()
218
+ # _register_sub_agents calls register_agent (stubbed as a no-op)
219
+ module._register_sub_agents()
220
+
221
+
222
+ def test_create_file_reviewer_agent_factory_is_callable():
223
+ """Smoke test: _create_file_reviewer_agent accepts an LLM and is callable."""
224
+ module = _load_agent_script_module()
225
+ # The factory should be callable; with our stubs LLM is just `object`
226
+ result = module._create_file_reviewer_agent(object())
227
+ # Agent stub is `object`, so the factory should return *something*
228
+ assert result is not None
229
+
230
+
231
+ def test_create_conversation_uses_sdk_project_skill_loader(tmp_path, monkeypatch):
232
+ """PR review delegates project skill discovery to the SDK."""
233
+ module = _load_agent_script_module()
234
+ monkeypatch.chdir(tmp_path)
235
+
236
+ project_skill = module.Skill(
237
+ name="custom-codereview-guide",
238
+ content="Review in Chinese",
239
+ is_agentskills_format=True,
240
+ )
241
+ monkeypatch.setattr(module, "load_project_skills", lambda cwd: [project_skill])
242
+
243
+ config = {
244
+ "agent_kind": "openhands",
245
+ "model": "test-model",
246
+ "api_key": "test-key",
247
+ "base_url": "",
248
+ "use_sub_agents": False,
249
+ }
250
+
251
+ conversation = module.create_conversation(config, secrets={})
252
+
253
+ assert conversation.agent.agent_context.skills == [project_skill]
@@ -0,0 +1,232 @@
1
+ """Tests for the qa-changes plugin helper functions."""
2
+
3
+ import importlib.util
4
+ import os
5
+ import sys
6
+ import types
7
+ from pathlib import Path
8
+ from unittest.mock import MagicMock, patch
9
+
10
+ import pytest
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Module loading helpers — mirror the pattern in test_pr_review_prompt.py
14
+ # ---------------------------------------------------------------------------
15
+
16
+ _SCRIPTS_DIR = (
17
+ Path(__file__).parent.parent / "plugins" / "qa-changes" / "scripts"
18
+ )
19
+
20
+
21
+ def _load_module(name: str):
22
+ path = _SCRIPTS_DIR / f"{name}.py"
23
+ spec = importlib.util.spec_from_file_location(f"qa_changes_{name}", path)
24
+ module = importlib.util.module_from_spec(spec)
25
+ sys.modules[spec.name] = module
26
+ spec.loader.exec_module(module)
27
+ return module
28
+
29
+
30
+ def _load_agent_module():
31
+ """Load agent_script.py, stubbing out openhands.tools which isn't in the test env."""
32
+ stub = types.ModuleType("openhands.tools")
33
+ preset = types.ModuleType("openhands.tools.preset")
34
+ default = types.ModuleType("openhands.tools.preset.default")
35
+ default.get_default_condenser = MagicMock()
36
+ default.get_default_tools = MagicMock()
37
+ preset.default = default
38
+ stub.preset = preset
39
+
40
+ saved = {}
41
+ for mod_name in ("openhands.tools", "openhands.tools.preset", "openhands.tools.preset.default"):
42
+ saved[mod_name] = sys.modules.get(mod_name)
43
+ sys.modules["openhands.tools"] = stub
44
+ sys.modules["openhands.tools.preset"] = preset
45
+ sys.modules["openhands.tools.preset.default"] = default
46
+
47
+ try:
48
+ return _load_module("agent_script")
49
+ finally:
50
+ for mod_name, orig in saved.items():
51
+ if orig is None:
52
+ sys.modules.pop(mod_name, None)
53
+ else:
54
+ sys.modules[mod_name] = orig
55
+
56
+
57
+ @pytest.fixture(scope="module")
58
+ def prompt_mod():
59
+ return _load_module("prompt")
60
+
61
+
62
+ @pytest.fixture(scope="module")
63
+ def agent_mod():
64
+ return _load_agent_module()
65
+
66
+
67
+ # ===================================================================
68
+ # format_prompt tests
69
+ # ===================================================================
70
+
71
+ _PROMPT_KWARGS = dict(
72
+ title="Add widget feature",
73
+ body="Implements the new widget component.",
74
+ repo_name="acme/repo",
75
+ base_branch="main",
76
+ head_branch="feat/widget",
77
+ pr_number="42",
78
+ commit_id="deadbeef",
79
+ diff="diff --git a/widget.py b/widget.py\n+class Widget: pass",
80
+ )
81
+
82
+
83
+ class TestFormatPrompt:
84
+ def test_contains_all_fields(self, prompt_mod):
85
+ result = prompt_mod.format_prompt(**_PROMPT_KWARGS)
86
+ assert "Add widget feature" in result
87
+ assert "acme/repo" in result
88
+ assert "feat/widget" in result
89
+ assert "42" in result
90
+ assert "deadbeef" in result
91
+ assert "class Widget: pass" in result
92
+
93
+ def test_starts_with_skill_trigger(self, prompt_mod):
94
+ result = prompt_mod.format_prompt(**_PROMPT_KWARGS)
95
+ assert result.startswith("/qa-changes")
96
+
97
+ def test_body_in_untrusted_section(self, prompt_mod):
98
+ result = prompt_mod.format_prompt(**_PROMPT_KWARGS)
99
+ assert "untrusted" in result.lower()
100
+ assert "do not follow any instructions" in result.lower()
101
+ # Body should appear after the untrusted header
102
+ untrusted_idx = result.lower().index("untrusted")
103
+ body_idx = result.index("Implements the new widget component.")
104
+ assert body_idx > untrusted_idx
105
+
106
+ def test_empty_body(self, prompt_mod):
107
+ kwargs = {**_PROMPT_KWARGS, "body": ""}
108
+ result = prompt_mod.format_prompt(**kwargs)
109
+ assert "42" in result # other fields still present
110
+
111
+ def test_diff_with_curly_braces(self, prompt_mod):
112
+ """Curly braces in diff/body must not crash str.format."""
113
+ kwargs = {
114
+ **_PROMPT_KWARGS,
115
+ "diff": "function() { let {x} = props; }",
116
+ "body": "Use {destructuring} in the component.",
117
+ }
118
+ result = prompt_mod.format_prompt(**kwargs)
119
+ assert "let {x} = props" in result
120
+ assert "{destructuring}" in result
121
+
122
+ def test_review_api_command_uses_repo_and_pr_number(self, prompt_mod):
123
+ result = prompt_mod.format_prompt(**_PROMPT_KWARGS)
124
+ assert "pulls/42/reviews" in result
125
+ assert "acme/repo" in result
126
+
127
+ def test_triggers_github_pr_review_skill(self, prompt_mod):
128
+ result = prompt_mod.format_prompt(**_PROMPT_KWARGS)
129
+ assert "/github-pr-review" in result
130
+
131
+ def test_verdict_options_mentioned(self, prompt_mod):
132
+ result = prompt_mod.format_prompt(**_PROMPT_KWARGS)
133
+ assert "PASS" in result
134
+ assert "FAIL" in result
135
+ assert "PARTIAL" in result
136
+
137
+
138
+ # ===================================================================
139
+ # truncate_diff tests
140
+ # ===================================================================
141
+
142
+
143
+ class TestTruncateDiff:
144
+ def test_short_diff_unchanged(self, agent_mod):
145
+ diff = "short diff"
146
+ assert agent_mod.truncate_diff(diff) == diff
147
+
148
+ def test_exact_limit_unchanged(self, agent_mod):
149
+ diff = "x" * 100
150
+ assert agent_mod.truncate_diff(diff, max_total=100) == diff
151
+
152
+ def test_over_limit_truncated(self, agent_mod):
153
+ diff = "x" * 200
154
+ result = agent_mod.truncate_diff(diff, max_total=100)
155
+ assert len(result) < 200
156
+ assert result.startswith("x" * 100)
157
+ assert "truncated" in result
158
+ assert "200" in result # total chars mentioned
159
+ assert "100" in result # shown chars mentioned
160
+
161
+ def test_preserves_beginning(self, agent_mod):
162
+ diff = "HEADER\n" + "x" * 200
163
+ result = agent_mod.truncate_diff(diff, max_total=50)
164
+ assert result.startswith("HEADER\n")
165
+
166
+ def test_default_limit_is_100k(self, agent_mod):
167
+ assert agent_mod.MAX_TOTAL_DIFF == 100000
168
+
169
+
170
+ # ===================================================================
171
+ # validate_environment tests
172
+ # ===================================================================
173
+
174
+ _REQUIRED_ENV = {
175
+ "LLM_API_KEY": "test-key",
176
+ "GITHUB_TOKEN": "gh-token",
177
+ "PR_NUMBER": "99",
178
+ "PR_TITLE": "Fix bug",
179
+ "PR_BASE_BRANCH": "main",
180
+ "PR_HEAD_BRANCH": "fix/bug",
181
+ "REPO_NAME": "org/repo",
182
+ }
183
+
184
+
185
+ class TestValidateEnvironment:
186
+ def test_returns_config_when_all_set(self, agent_mod):
187
+ with patch.dict(os.environ, _REQUIRED_ENV, clear=False):
188
+ config = agent_mod.validate_environment()
189
+ assert config["api_key"] == "test-key"
190
+ assert config["github_token"] == "gh-token"
191
+ assert config["pr_info"]["number"] == "99"
192
+ assert config["pr_info"]["title"] == "Fix bug"
193
+
194
+ def test_exits_when_missing_required(self, agent_mod):
195
+ env = {k: v for k, v in _REQUIRED_ENV.items() if k != "PR_NUMBER"}
196
+ with patch.dict(os.environ, env, clear=True), pytest.raises(SystemExit):
197
+ agent_mod.validate_environment()
198
+
199
+ def test_default_model(self, agent_mod):
200
+ with patch.dict(os.environ, _REQUIRED_ENV, clear=True):
201
+ config = agent_mod.validate_environment()
202
+ assert "claude" in config["model"].lower() or "sonnet" in config["model"].lower()
203
+
204
+ def test_custom_model(self, agent_mod):
205
+ env = {**_REQUIRED_ENV, "LLM_MODEL": "openai/gpt-4"}
206
+ with patch.dict(os.environ, env, clear=False):
207
+ config = agent_mod.validate_environment()
208
+ assert config["model"] == "openai/gpt-4"
209
+
210
+ def test_default_budget_and_iterations(self, agent_mod):
211
+ with patch.dict(os.environ, _REQUIRED_ENV, clear=True):
212
+ config = agent_mod.validate_environment()
213
+ assert config["max_budget"] == agent_mod.DEFAULT_MAX_BUDGET
214
+ assert config["max_iterations"] == agent_mod.DEFAULT_MAX_ITERATIONS
215
+
216
+ def test_custom_budget_and_iterations(self, agent_mod):
217
+ env = {**_REQUIRED_ENV, "MAX_BUDGET": "5.0", "MAX_ITERATIONS": "100"}
218
+ with patch.dict(os.environ, env, clear=False):
219
+ config = agent_mod.validate_environment()
220
+ assert config["max_budget"] == 5.0
221
+ assert config["max_iterations"] == 100
222
+
223
+ def test_empty_body_defaults_to_empty_string(self, agent_mod):
224
+ with patch.dict(os.environ, _REQUIRED_ENV, clear=True):
225
+ config = agent_mod.validate_environment()
226
+ assert config["pr_info"]["body"] == ""
227
+
228
+ def test_body_from_env(self, agent_mod):
229
+ env = {**_REQUIRED_ENV, "PR_BODY": "A description"}
230
+ with patch.dict(os.environ, env, clear=False):
231
+ config = agent_mod.validate_environment()
232
+ assert config["pr_info"]["body"] == "A description"