@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.
Files changed (354) 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/index.d.ts +2 -0
  15. package/index.js +1 -0
  16. package/marketplaces/large-codebase.json +66 -0
  17. package/marketplaces/openhands-extensions.json +682 -0
  18. package/package.json +12 -10
  19. package/plugins/README.md +30 -0
  20. package/plugins/city-weather/.plugin/plugin.json +13 -0
  21. package/plugins/city-weather/README.md +145 -0
  22. package/plugins/city-weather/commands/now.md +56 -0
  23. package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
  24. package/plugins/cobol-modernization/README.md +201 -0
  25. package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
  26. package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
  27. package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
  28. package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
  29. package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
  30. package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
  31. package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
  32. package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
  33. package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
  34. package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
  35. package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
  36. package/plugins/issue-duplicate-checker/README.md +51 -0
  37. package/plugins/issue-duplicate-checker/action.yml +349 -0
  38. package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
  39. package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
  40. package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
  41. package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
  42. package/plugins/magic-test/.plugin/plugin.json +13 -0
  43. package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
  44. package/plugins/migration-scoring/.plugin/plugin.json +19 -0
  45. package/plugins/migration-scoring/README.md +244 -0
  46. package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
  47. package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
  48. package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
  49. package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
  50. package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
  51. package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
  52. package/plugins/onboarding/.plugin/plugin.json +20 -0
  53. package/plugins/onboarding/README.md +30 -0
  54. package/plugins/onboarding/references/criteria.md +144 -0
  55. package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
  56. package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
  57. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
  58. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
  59. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
  60. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
  61. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
  62. package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
  63. package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
  64. package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
  65. package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
  66. package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
  67. package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
  68. package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
  69. package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
  70. package/plugins/openhands/.plugin/plugin.json +13 -0
  71. package/plugins/openhands/README.md +52 -0
  72. package/plugins/openhands/SKILL.md +61 -0
  73. package/plugins/openhands/commands/create.md +55 -0
  74. package/plugins/openhands/commands/openhands-cloud.md +8 -0
  75. package/plugins/openhands/scripts/run.sh +69 -0
  76. package/plugins/pr-review/.plugin/plugin.json +13 -0
  77. package/plugins/pr-review/README.md +393 -0
  78. package/plugins/pr-review/action.yml +298 -0
  79. package/plugins/pr-review/scripts/agent_script.py +1282 -0
  80. package/plugins/pr-review/scripts/evaluate_review.py +655 -0
  81. package/plugins/pr-review/scripts/prompt.py +260 -0
  82. package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
  83. package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
  84. package/plugins/qa-changes/.plugin/plugin.json +11 -0
  85. package/plugins/qa-changes/README.md +185 -0
  86. package/plugins/qa-changes/action.yml +181 -0
  87. package/plugins/qa-changes/scripts/agent_script.py +406 -0
  88. package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
  89. package/plugins/qa-changes/scripts/prompt.py +174 -0
  90. package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
  91. package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
  92. package/plugins/release-notes/.plugin/plugin.json +19 -0
  93. package/plugins/release-notes/README.md +283 -0
  94. package/plugins/release-notes/SKILL.md +83 -0
  95. package/plugins/release-notes/action.yml +117 -0
  96. package/plugins/release-notes/commands/release-notes.md +8 -0
  97. package/plugins/release-notes/scripts/agent_script.py +292 -0
  98. package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
  99. package/plugins/release-notes/scripts/prompt.py +90 -0
  100. package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
  101. package/plugins/release-notes/workflows/release-notes.yml +76 -0
  102. package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
  103. package/plugins/vulnerability-remediation/README.md +217 -0
  104. package/plugins/vulnerability-remediation/action.yml +187 -0
  105. package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
  106. package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
  107. package/pyproject.toml +12 -0
  108. package/release-please-config.json +16 -0
  109. package/scripts/build-skills-catalog.mjs +100 -0
  110. package/scripts/sync_extensions.py +494 -0
  111. package/scripts/sync_openhands_sdk_skill.py +264 -0
  112. package/skills/README.md +159 -0
  113. package/skills/add-javadoc/.plugin/plugin.json +18 -0
  114. package/skills/add-javadoc/README.md +40 -0
  115. package/skills/add-javadoc/SKILL.md +35 -0
  116. package/skills/add-javadoc/references/example.md +32 -0
  117. package/skills/add-skill/.plugin/plugin.json +18 -0
  118. package/skills/add-skill/README.md +67 -0
  119. package/skills/add-skill/SKILL.md +47 -0
  120. package/skills/add-skill/scripts/fetch_skill.py +259 -0
  121. package/skills/agent-creator/.plugin/plugin.json +20 -0
  122. package/skills/agent-creator/README.md +104 -0
  123. package/skills/agent-creator/SKILL.md +190 -0
  124. package/skills/agent-creator/commands/agent-creator.md +8 -0
  125. package/skills/agent-creator/references/fallback.md +117 -0
  126. package/skills/agent-memory/.plugin/plugin.json +18 -0
  127. package/skills/agent-memory/README.md +35 -0
  128. package/skills/agent-memory/SKILL.md +30 -0
  129. package/skills/agent-memory/commands/remember.md +8 -0
  130. package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
  131. package/skills/agent-sdk-builder/README.md +40 -0
  132. package/skills/agent-sdk-builder/SKILL.md +37 -0
  133. package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
  134. package/skills/azure-devops/.plugin/plugin.json +18 -0
  135. package/skills/azure-devops/README.md +55 -0
  136. package/skills/azure-devops/SKILL.md +50 -0
  137. package/skills/bitbucket/.plugin/plugin.json +17 -0
  138. package/skills/bitbucket/README.md +50 -0
  139. package/skills/bitbucket/SKILL.md +45 -0
  140. package/skills/code-review/.plugin/plugin.json +19 -0
  141. package/skills/code-review/README.md +18 -0
  142. package/skills/code-review/SKILL.md +208 -0
  143. package/skills/code-review/commands/codereview-roasted.md +8 -0
  144. package/skills/code-review/commands/codereview.md +8 -0
  145. package/skills/code-review/references/risk-evaluation.md +41 -0
  146. package/skills/code-review/references/supply-chain-security.md +31 -0
  147. package/skills/code-simplifier/.plugin/plugin.json +21 -0
  148. package/skills/code-simplifier/README.md +30 -0
  149. package/skills/code-simplifier/SKILL.md +91 -0
  150. package/skills/code-simplifier/commands/simplify.md +8 -0
  151. package/skills/code-simplifier/references/code-quality-review.md +86 -0
  152. package/skills/code-simplifier/references/code-reuse-review.md +63 -0
  153. package/skills/code-simplifier/references/efficiency-review.md +81 -0
  154. package/skills/datadog/.plugin/plugin.json +19 -0
  155. package/skills/datadog/README.md +100 -0
  156. package/skills/datadog/SKILL.md +95 -0
  157. package/skills/deno/.plugin/plugin.json +18 -0
  158. package/skills/deno/README.md +5 -0
  159. package/skills/deno/SKILL.md +99 -0
  160. package/skills/deno/references/README.md +6 -0
  161. package/skills/discord/.plugin/plugin.json +18 -0
  162. package/skills/discord/README.md +31 -0
  163. package/skills/discord/SKILL.md +109 -0
  164. package/skills/discord/__init__.py +0 -0
  165. package/skills/discord/references/REFERENCE.md +78 -0
  166. package/skills/discord/scripts/__init__.py +0 -0
  167. package/skills/discord/scripts/_http.py +127 -0
  168. package/skills/discord/scripts/post_webhook.py +106 -0
  169. package/skills/discord/scripts/send_message.py +102 -0
  170. package/skills/docker/.plugin/plugin.json +17 -0
  171. package/skills/docker/README.md +34 -0
  172. package/skills/docker/SKILL.md +29 -0
  173. package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
  174. package/skills/evidence-based-citations/README.md +31 -0
  175. package/skills/evidence-based-citations/SKILL.md +59 -0
  176. package/skills/flarglebargle/.plugin/plugin.json +16 -0
  177. package/skills/flarglebargle/README.md +14 -0
  178. package/skills/flarglebargle/SKILL.md +9 -0
  179. package/skills/frontend-design/.plugin/plugin.json +21 -0
  180. package/skills/frontend-design/LICENSE.txt +177 -0
  181. package/skills/frontend-design/README.md +42 -0
  182. package/skills/frontend-design/SKILL.md +42 -0
  183. package/skills/github/.plugin/plugin.json +19 -0
  184. package/skills/github/README.md +42 -0
  185. package/skills/github/SKILL.md +106 -0
  186. package/skills/github-pr-review/.plugin/plugin.json +18 -0
  187. package/skills/github-pr-review/README.md +145 -0
  188. package/skills/github-pr-review/SKILL.md +186 -0
  189. package/skills/github-pr-review/commands/github-pr-review.md +8 -0
  190. package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
  191. package/skills/github-pr-reviewer/README.md +34 -0
  192. package/skills/github-pr-reviewer/SKILL.md +89 -0
  193. package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
  194. package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
  195. package/skills/github-repo-monitor/README.md +70 -0
  196. package/skills/github-repo-monitor/SKILL.md +316 -0
  197. package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
  198. package/skills/github-repo-monitor/references/github-api.md +241 -0
  199. package/skills/github-repo-monitor/references/state-schema.md +160 -0
  200. package/skills/github-repo-monitor/scripts/main.py +915 -0
  201. package/skills/github-repo-monitor/tests/test_main.py +400 -0
  202. package/skills/gitlab/.plugin/plugin.json +17 -0
  203. package/skills/gitlab/README.md +37 -0
  204. package/skills/gitlab/SKILL.md +32 -0
  205. package/skills/incident-retrospective/.plugin/plugin.json +21 -0
  206. package/skills/incident-retrospective/README.md +34 -0
  207. package/skills/incident-retrospective/SKILL.md +98 -0
  208. package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
  209. package/skills/index.d.ts +11 -0
  210. package/skills/index.js +465 -0
  211. package/skills/iterate/.plugin/plugin.json +13 -0
  212. package/skills/iterate/README.md +25 -0
  213. package/skills/iterate/SKILL.md +399 -0
  214. package/skills/iterate/commands/babysit.md +8 -0
  215. package/skills/iterate/commands/iterate.md +8 -0
  216. package/skills/iterate/commands/verify.md +8 -0
  217. package/skills/iterate/references/heuristics.md +58 -0
  218. package/skills/iterate/references/verification.md +96 -0
  219. package/skills/jupyter/.plugin/plugin.json +18 -0
  220. package/skills/jupyter/README.md +55 -0
  221. package/skills/jupyter/SKILL.md +50 -0
  222. package/skills/kubernetes/.plugin/plugin.json +18 -0
  223. package/skills/kubernetes/README.md +53 -0
  224. package/skills/kubernetes/SKILL.md +48 -0
  225. package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
  226. package/skills/learn-from-code-review/README.md +64 -0
  227. package/skills/learn-from-code-review/SKILL.md +186 -0
  228. package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
  229. package/skills/linear/.plugin/plugin.json +19 -0
  230. package/skills/linear/README.md +58 -0
  231. package/skills/linear/SKILL.md +213 -0
  232. package/skills/linear-triage/.plugin/plugin.json +21 -0
  233. package/skills/linear-triage/README.md +34 -0
  234. package/skills/linear-triage/SKILL.md +91 -0
  235. package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
  236. package/skills/notion/.plugin/plugin.json +17 -0
  237. package/skills/notion/README.md +114 -0
  238. package/skills/notion/SKILL.md +109 -0
  239. package/skills/npm/.plugin/plugin.json +17 -0
  240. package/skills/npm/README.md +14 -0
  241. package/skills/npm/SKILL.md +9 -0
  242. package/skills/openhands-api/.plugin/plugin.json +22 -0
  243. package/skills/openhands-api/README.md +48 -0
  244. package/skills/openhands-api/SKILL.md +399 -0
  245. package/skills/openhands-api/references/README.md +33 -0
  246. package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  247. package/skills/openhands-api/references/example_prompt.md +12 -0
  248. package/skills/openhands-api/scripts/openhands_api.py +606 -0
  249. package/skills/openhands-api/scripts/openhands_api.ts +252 -0
  250. package/skills/openhands-automation/.plugin/plugin.json +19 -0
  251. package/skills/openhands-automation/README.md +89 -0
  252. package/skills/openhands-automation/SKILL.md +875 -0
  253. package/skills/openhands-automation/commands/automation:create.md +8 -0
  254. package/skills/openhands-automation/references/ab-testing.md +185 -0
  255. package/skills/openhands-automation/references/custom-automation.md +644 -0
  256. package/skills/openhands-sdk/.plugin/plugin.json +20 -0
  257. package/skills/openhands-sdk/README.md +22 -0
  258. package/skills/openhands-sdk/SKILL.md +230 -0
  259. package/skills/openhands-sdk/commands/sdk.md +8 -0
  260. package/skills/pdflatex/.plugin/plugin.json +18 -0
  261. package/skills/pdflatex/README.md +39 -0
  262. package/skills/pdflatex/SKILL.md +34 -0
  263. package/skills/prd/.plugin/plugin.json +19 -0
  264. package/skills/prd/README.md +28 -0
  265. package/skills/prd/SKILL.md +237 -0
  266. package/skills/prd/commands/prd.md +8 -0
  267. package/skills/qa-changes/README.md +18 -0
  268. package/skills/qa-changes/SKILL.md +229 -0
  269. package/skills/qa-changes/commands/qa-changes.md +8 -0
  270. package/skills/release-notes/README.md +24 -0
  271. package/skills/release-notes/SKILL.md +19 -0
  272. package/skills/release-notes/commands/release-notes.md +8 -0
  273. package/skills/research-brief/.plugin/plugin.json +20 -0
  274. package/skills/research-brief/README.md +34 -0
  275. package/skills/research-brief/SKILL.md +99 -0
  276. package/skills/research-brief/commands/research-brief:setup.md +8 -0
  277. package/skills/security/.plugin/plugin.json +18 -0
  278. package/skills/security/README.md +38 -0
  279. package/skills/security/SKILL.md +33 -0
  280. package/skills/skill-creator/.plugin/plugin.json +17 -0
  281. package/skills/skill-creator/LICENSE.txt +202 -0
  282. package/skills/skill-creator/README.md +182 -0
  283. package/skills/skill-creator/SKILL.md +545 -0
  284. package/skills/skill-creator/references/output-patterns.md +82 -0
  285. package/skills/skill-creator/references/workflows.md +28 -0
  286. package/skills/skill-creator/scripts/init_skill.py +303 -0
  287. package/skills/skill-creator/scripts/quick_validate.py +95 -0
  288. package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
  289. package/skills/slack-channel-monitor/README.md +91 -0
  290. package/skills/slack-channel-monitor/SKILL.md +276 -0
  291. package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
  292. package/skills/slack-channel-monitor/references/slack-api.md +207 -0
  293. package/skills/slack-channel-monitor/references/state-schema.md +180 -0
  294. package/skills/slack-channel-monitor/scripts/main.py +962 -0
  295. package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
  296. package/skills/slack-standup-digest/README.md +34 -0
  297. package/skills/slack-standup-digest/SKILL.md +92 -0
  298. package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
  299. package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
  300. package/skills/spark-version-upgrade/README.md +54 -0
  301. package/skills/spark-version-upgrade/SKILL.md +233 -0
  302. package/skills/ssh/.plugin/plugin.json +18 -0
  303. package/skills/ssh/README.md +140 -0
  304. package/skills/ssh/SKILL.md +135 -0
  305. package/skills/swift-linux/.plugin/plugin.json +17 -0
  306. package/skills/swift-linux/README.md +86 -0
  307. package/skills/swift-linux/SKILL.md +81 -0
  308. package/skills/theme-factory/.plugin/plugin.json +19 -0
  309. package/skills/theme-factory/LICENSE.txt +202 -0
  310. package/skills/theme-factory/README.md +58 -0
  311. package/skills/theme-factory/SKILL.md +59 -0
  312. package/skills/theme-factory/theme-showcase.pdf +0 -0
  313. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  314. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  315. package/skills/theme-factory/themes/desert-rose.md +19 -0
  316. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  317. package/skills/theme-factory/themes/golden-hour.md +19 -0
  318. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  319. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  320. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  321. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  322. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  323. package/skills/uv/.plugin/plugin.json +18 -0
  324. package/skills/uv/README.md +5 -0
  325. package/skills/uv/SKILL.md +95 -0
  326. package/skills/uv/references/README.md +5 -0
  327. package/skills/vercel/.plugin/plugin.json +18 -0
  328. package/skills/vercel/README.md +108 -0
  329. package/skills/vercel/SKILL.md +103 -0
  330. package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
  331. package/tests/test_catalogs.py +109 -0
  332. package/tests/test_code_review_risk_evaluation.py +94 -0
  333. package/tests/test_github_pr_review_skill.py +114 -0
  334. package/tests/test_issue_duplicate_checker.py +240 -0
  335. package/tests/test_openhands_api_python.py +152 -0
  336. package/tests/test_plugin_manifest.py +83 -0
  337. package/tests/test_pr_review_diff_payload.py +202 -0
  338. package/tests/test_pr_review_feedback.py +263 -0
  339. package/tests/test_pr_review_prompt.py +152 -0
  340. package/tests/test_pr_review_review_context.py +253 -0
  341. package/tests/test_qa_changes.py +232 -0
  342. package/tests/test_qa_changes_evaluation.py +259 -0
  343. package/tests/test_release_notes_generator.py +990 -0
  344. package/tests/test_sdk_loading.py +150 -0
  345. package/tests/test_skill_plugin_loading.py +149 -0
  346. package/tests/test_skills_catalog.py +336 -0
  347. package/tests/test_skills_have_readme.py +66 -0
  348. package/tests/test_sync_extensions.py +292 -0
  349. package/tests/test_workflow_sync.py +46 -0
  350. package/utils/analysis/README.md +7 -0
  351. package/utils/analysis/laminar_signals/README.md +211 -0
  352. package/utils/analysis/laminar_signals/analyze.py +780 -0
  353. package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
  354. 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