@openhands/extensions 0.21.0 → 0.22.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 (267) hide show
  1. package/.claude-plugin/marketplace.json +881 -0
  2. package/.codex-plugin/marketplace.json +881 -0
  3. package/.github/workflows/npm-publish.yml +9 -3
  4. package/.plugin/marketplace.json +881 -0
  5. package/.release-please-manifest.json +1 -1
  6. package/README.md +3 -2
  7. package/automations/bundle-index.js +16 -7
  8. package/automations/catalog/github-delivery-watchdog/manifest.json +79 -0
  9. package/automations/catalog/github-issue-to-pr/manifest.json +23 -15
  10. package/automations/catalog/github-issue-triage/manifest.json +4 -3
  11. package/automations/catalog/github-pr-reviewer/manifest.json +22 -15
  12. package/automations/catalog-index.js +6 -4
  13. package/marketplaces/openhands-extensions.json +6 -0
  14. package/package.json +1 -1
  15. package/plugins/city-weather/.claude-plugin/plugin.json +13 -0
  16. package/plugins/city-weather/.codex-plugin/plugin.json +13 -0
  17. package/plugins/cobol-modernization/.claude-plugin/plugin.json +19 -0
  18. package/plugins/cobol-modernization/.codex-plugin/plugin.json +19 -0
  19. package/plugins/issue-duplicate-checker/.claude-plugin/plugin.json +13 -0
  20. package/plugins/issue-duplicate-checker/.codex-plugin/plugin.json +13 -0
  21. package/plugins/magic-test/.claude-plugin/plugin.json +13 -0
  22. package/plugins/magic-test/.codex-plugin/plugin.json +13 -0
  23. package/plugins/migration-scoring/.claude-plugin/plugin.json +19 -0
  24. package/plugins/migration-scoring/.codex-plugin/plugin.json +19 -0
  25. package/plugins/onboarding/.claude-plugin/plugin.json +20 -0
  26. package/plugins/onboarding/.codex-plugin/plugin.json +20 -0
  27. package/plugins/onboarding/skills/agent-readiness-report/references/criteria.md +144 -0
  28. package/plugins/onboarding/skills/improve-agent-readiness/references/criteria.md +144 -0
  29. package/plugins/openhands/.claude-plugin/plugin.json +13 -0
  30. package/plugins/openhands/.codex-plugin/plugin.json +13 -0
  31. package/plugins/openhands/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  32. package/plugins/openhands/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  33. package/plugins/openhands/skills/openhands-api/.plugin/plugin.json +22 -0
  34. package/plugins/openhands/skills/openhands-api/README.md +52 -0
  35. package/plugins/openhands/skills/openhands-api/SKILL.md +469 -0
  36. package/plugins/openhands/skills/openhands-api/references/README.md +33 -0
  37. package/plugins/openhands/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  38. package/plugins/openhands/skills/openhands-api/references/example_prompt.md +12 -0
  39. package/plugins/openhands/skills/openhands-api/references/windows.md +44 -0
  40. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +606 -0
  41. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.ts +252 -0
  42. package/plugins/openhands/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  43. package/plugins/openhands/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  44. package/plugins/openhands/skills/openhands-automation/.plugin/plugin.json +19 -0
  45. package/plugins/openhands/skills/openhands-automation/README.md +89 -0
  46. package/plugins/openhands/skills/openhands-automation/SKILL.md +899 -0
  47. package/plugins/openhands/skills/openhands-automation/commands/automation-create.md +8 -0
  48. package/plugins/openhands/skills/openhands-automation/references/ab-testing.md +185 -0
  49. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +832 -0
  50. package/plugins/openhands/skills/openhands-automation/references/security.md +84 -0
  51. package/plugins/openhands/skills/openhands-automation/references/windows.md +57 -0
  52. package/plugins/openhands/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  53. package/plugins/openhands/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  54. package/plugins/openhands/skills/openhands-sdk/.plugin/plugin.json +20 -0
  55. package/plugins/openhands/skills/openhands-sdk/README.md +22 -0
  56. package/plugins/openhands/skills/openhands-sdk/SKILL.md +245 -0
  57. package/plugins/openhands/skills/openhands-sdk/commands/sdk.md +8 -0
  58. package/plugins/pr-review/.claude-plugin/plugin.json +13 -0
  59. package/plugins/pr-review/.codex-plugin/plugin.json +13 -0
  60. package/plugins/pr-review/skills/code-review/.claude-plugin/plugin.json +19 -0
  61. package/plugins/pr-review/skills/code-review/.codex-plugin/plugin.json +19 -0
  62. package/plugins/pr-review/skills/code-review/.plugin/plugin.json +19 -0
  63. package/plugins/pr-review/skills/code-review/README.md +18 -0
  64. package/plugins/pr-review/skills/code-review/SKILL.md +209 -0
  65. package/plugins/pr-review/skills/code-review/commands/codereview-roasted.md +8 -0
  66. package/plugins/pr-review/skills/code-review/commands/codereview.md +8 -0
  67. package/plugins/pr-review/skills/code-review/references/risk-evaluation.md +41 -0
  68. package/plugins/pr-review/skills/code-review/references/supply-chain-security.md +31 -0
  69. package/plugins/pr-review/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  70. package/plugins/pr-review/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  71. package/plugins/pr-review/skills/github-pr-review/.plugin/plugin.json +18 -0
  72. package/plugins/pr-review/skills/github-pr-review/README.md +145 -0
  73. package/plugins/pr-review/skills/github-pr-review/SKILL.md +187 -0
  74. package/plugins/pr-review/skills/github-pr-review/commands/github-pr-review.md +8 -0
  75. package/plugins/pr-review/skills/github-pr-review/references/windows.md +47 -0
  76. package/plugins/qa-changes/.claude-plugin/plugin.json +11 -0
  77. package/plugins/qa-changes/.codex-plugin/plugin.json +11 -0
  78. package/plugins/qa-changes/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  79. package/plugins/qa-changes/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  80. package/plugins/qa-changes/skills/github-pr-review/.plugin/plugin.json +18 -0
  81. package/plugins/qa-changes/skills/github-pr-review/README.md +145 -0
  82. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +187 -0
  83. package/plugins/qa-changes/skills/github-pr-review/commands/github-pr-review.md +8 -0
  84. package/plugins/qa-changes/skills/github-pr-review/references/windows.md +47 -0
  85. package/plugins/qa-changes/skills/qa-changes/README.md +18 -0
  86. package/plugins/qa-changes/skills/qa-changes/SKILL.md +229 -0
  87. package/plugins/qa-changes/skills/qa-changes/commands/qa-changes.md +8 -0
  88. package/plugins/release-notes/.claude-plugin/plugin.json +19 -0
  89. package/plugins/release-notes/.codex-plugin/plugin.json +19 -0
  90. package/plugins/release-notes/skills/release-notes/README.md +24 -0
  91. package/plugins/release-notes/skills/release-notes/SKILL.md +19 -0
  92. package/plugins/release-notes/skills/release-notes/commands/release-notes.md +8 -0
  93. package/plugins/vulnerability-remediation/.claude-plugin/plugin.json +19 -0
  94. package/plugins/vulnerability-remediation/.codex-plugin/plugin.json +19 -0
  95. package/plugins/vulnerability-remediation/skills/github/.claude-plugin/plugin.json +19 -0
  96. package/plugins/vulnerability-remediation/skills/github/.codex-plugin/plugin.json +19 -0
  97. package/plugins/vulnerability-remediation/skills/github/.plugin/plugin.json +19 -0
  98. package/plugins/vulnerability-remediation/skills/github/README.md +42 -0
  99. package/plugins/vulnerability-remediation/skills/github/SKILL.md +107 -0
  100. package/plugins/vulnerability-remediation/skills/github/references/windows.md +48 -0
  101. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +143 -0
  102. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +230 -0
  103. package/plugins/vulnerability-remediation/skills/security/.claude-plugin/plugin.json +18 -0
  104. package/plugins/vulnerability-remediation/skills/security/.codex-plugin/plugin.json +18 -0
  105. package/plugins/vulnerability-remediation/skills/security/.plugin/plugin.json +18 -0
  106. package/plugins/vulnerability-remediation/skills/security/README.md +38 -0
  107. package/plugins/vulnerability-remediation/skills/security/SKILL.md +33 -0
  108. package/pyproject.toml +1 -1
  109. package/python/openhands_extensions/_version.py +1 -1
  110. package/skills/add-javadoc/.claude-plugin/plugin.json +18 -0
  111. package/skills/add-javadoc/.codex-plugin/plugin.json +18 -0
  112. package/skills/add-skill/.claude-plugin/plugin.json +18 -0
  113. package/skills/add-skill/.codex-plugin/plugin.json +18 -0
  114. package/skills/agent-canvas-environment/.claude-plugin/plugin.json +19 -0
  115. package/skills/agent-canvas-environment/.codex-plugin/plugin.json +19 -0
  116. package/skills/agent-creator/.claude-plugin/plugin.json +20 -0
  117. package/skills/agent-creator/.codex-plugin/plugin.json +20 -0
  118. package/skills/agent-memory/.claude-plugin/plugin.json +18 -0
  119. package/skills/agent-memory/.codex-plugin/plugin.json +18 -0
  120. package/skills/agent-sdk-builder/.claude-plugin/plugin.json +18 -0
  121. package/skills/agent-sdk-builder/.codex-plugin/plugin.json +18 -0
  122. package/skills/azure-devops/.claude-plugin/plugin.json +18 -0
  123. package/skills/azure-devops/.codex-plugin/plugin.json +18 -0
  124. package/skills/bitbucket/.claude-plugin/plugin.json +17 -0
  125. package/skills/bitbucket/.codex-plugin/plugin.json +17 -0
  126. package/skills/bitbucket-cloud/.claude-plugin/plugin.json +18 -0
  127. package/skills/bitbucket-cloud/.codex-plugin/plugin.json +18 -0
  128. package/skills/bitbucket-data-center/.claude-plugin/plugin.json +19 -0
  129. package/skills/bitbucket-data-center/.codex-plugin/plugin.json +19 -0
  130. package/skills/code-review/.claude-plugin/plugin.json +19 -0
  131. package/skills/code-review/.codex-plugin/plugin.json +19 -0
  132. package/skills/code-simplifier/.claude-plugin/plugin.json +21 -0
  133. package/skills/code-simplifier/.codex-plugin/plugin.json +21 -0
  134. package/skills/datadog/.claude-plugin/plugin.json +19 -0
  135. package/skills/datadog/.codex-plugin/plugin.json +19 -0
  136. package/skills/deno/.claude-plugin/plugin.json +18 -0
  137. package/skills/deno/.codex-plugin/plugin.json +18 -0
  138. package/skills/discord/.claude-plugin/plugin.json +18 -0
  139. package/skills/discord/.codex-plugin/plugin.json +18 -0
  140. package/skills/docker/.claude-plugin/plugin.json +17 -0
  141. package/skills/docker/.codex-plugin/plugin.json +17 -0
  142. package/skills/evidence-based-citations/.claude-plugin/plugin.json +20 -0
  143. package/skills/evidence-based-citations/.codex-plugin/plugin.json +20 -0
  144. package/skills/flarglebargle/.claude-plugin/plugin.json +16 -0
  145. package/skills/flarglebargle/.codex-plugin/plugin.json +16 -0
  146. package/skills/frontend-design/.claude-plugin/plugin.json +21 -0
  147. package/skills/frontend-design/.codex-plugin/plugin.json +21 -0
  148. package/skills/github/.claude-plugin/plugin.json +19 -0
  149. package/skills/github/.codex-plugin/plugin.json +19 -0
  150. package/skills/github/scripts/agent_conversation.py +143 -0
  151. package/skills/github/scripts/github_client.py +31 -5
  152. package/skills/github-actions/.claude-plugin/plugin.json +19 -0
  153. package/skills/github-actions/.codex-plugin/plugin.json +19 -0
  154. package/skills/github-agents-md-maintainer/.claude-plugin/plugin.json +19 -0
  155. package/skills/github-agents-md-maintainer/.codex-plugin/plugin.json +19 -0
  156. package/skills/github-agents-md-maintainer/scripts/github_client.py +230 -0
  157. package/skills/github-delivery-watchdog/.claude-plugin/plugin.json +9 -0
  158. package/skills/github-delivery-watchdog/.codex-plugin/plugin.json +9 -0
  159. package/skills/github-delivery-watchdog/.plugin/plugin.json +9 -0
  160. package/skills/github-delivery-watchdog/README.md +10 -0
  161. package/skills/github-delivery-watchdog/SKILL.md +35 -0
  162. package/skills/github-delivery-watchdog/commands/github-delivery-watchdog.md +8 -0
  163. package/skills/github-delivery-watchdog/scripts/github_client.py +230 -0
  164. package/skills/github-delivery-watchdog/scripts/worker.py +96 -0
  165. package/skills/github-issue-to-pr/.claude-plugin/plugin.json +19 -0
  166. package/skills/github-issue-to-pr/.codex-plugin/plugin.json +19 -0
  167. package/skills/github-issue-to-pr/SKILL.md +7 -0
  168. package/skills/github-issue-to-pr/scripts/agent_conversation.py +143 -0
  169. package/skills/github-issue-to-pr/scripts/github_client.py +230 -0
  170. package/skills/github-issue-to-pr/scripts/main.py +16 -10
  171. package/skills/github-issue-to-pr/scripts/worker.py +172 -0
  172. package/skills/github-issue-triage/.claude-plugin/plugin.json +9 -0
  173. package/skills/github-issue-triage/.codex-plugin/plugin.json +9 -0
  174. package/skills/github-issue-triage/SKILL.md +15 -18
  175. package/skills/github-issue-triage/scripts/agent_conversation.py +143 -0
  176. package/skills/github-issue-triage/scripts/github_client.py +230 -0
  177. package/skills/github-issue-triage/scripts/worker.py +81 -98
  178. package/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  179. package/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  180. package/skills/github-pr-reviewer/.claude-plugin/plugin.json +20 -0
  181. package/skills/github-pr-reviewer/.codex-plugin/plugin.json +20 -0
  182. package/skills/github-pr-reviewer/SKILL.md +7 -0
  183. package/skills/github-pr-reviewer/scripts/agent_conversation.py +143 -0
  184. package/skills/github-pr-reviewer/scripts/github_client.py +230 -0
  185. package/skills/github-pr-reviewer/scripts/main.py +20 -8
  186. package/skills/github-pr-reviewer/scripts/worker.py +94 -0
  187. package/skills/github-repo-monitor/.claude-plugin/plugin.json +22 -0
  188. package/skills/github-repo-monitor/.codex-plugin/plugin.json +22 -0
  189. package/skills/github-repo-monitor/scripts/github_client.py +230 -0
  190. package/skills/gitlab/.claude-plugin/plugin.json +17 -0
  191. package/skills/gitlab/.codex-plugin/plugin.json +17 -0
  192. package/skills/gitlab-issue-to-mr/.claude-plugin/plugin.json +19 -0
  193. package/skills/gitlab-issue-to-mr/.codex-plugin/plugin.json +19 -0
  194. package/skills/incident-retrospective/.claude-plugin/plugin.json +21 -0
  195. package/skills/incident-retrospective/.codex-plugin/plugin.json +21 -0
  196. package/skills/index.js +12 -3
  197. package/skills/iterate/.claude-plugin/plugin.json +13 -0
  198. package/skills/iterate/.codex-plugin/plugin.json +13 -0
  199. package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +19 -0
  200. package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +19 -0
  201. package/skills/jupyter/.claude-plugin/plugin.json +18 -0
  202. package/skills/jupyter/.codex-plugin/plugin.json +18 -0
  203. package/skills/kubernetes/.claude-plugin/plugin.json +18 -0
  204. package/skills/kubernetes/.codex-plugin/plugin.json +18 -0
  205. package/skills/learn-from-code-review/.claude-plugin/plugin.json +19 -0
  206. package/skills/learn-from-code-review/.codex-plugin/plugin.json +19 -0
  207. package/skills/linear/.claude-plugin/plugin.json +19 -0
  208. package/skills/linear/.codex-plugin/plugin.json +19 -0
  209. package/skills/linear-triage/.claude-plugin/plugin.json +21 -0
  210. package/skills/linear-triage/.codex-plugin/plugin.json +21 -0
  211. package/skills/news-digest/.claude-plugin/plugin.json +19 -0
  212. package/skills/news-digest/.codex-plugin/plugin.json +19 -0
  213. package/skills/notion/.claude-plugin/plugin.json +17 -0
  214. package/skills/notion/.codex-plugin/plugin.json +17 -0
  215. package/skills/npm/.claude-plugin/plugin.json +17 -0
  216. package/skills/npm/.codex-plugin/plugin.json +17 -0
  217. package/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  218. package/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  219. package/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  220. package/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  221. package/skills/openhands-enterprise-troubleshooting/.claude-plugin/plugin.json +20 -0
  222. package/skills/openhands-enterprise-troubleshooting/.codex-plugin/plugin.json +20 -0
  223. package/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  224. package/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  225. package/skills/pdflatex/.claude-plugin/plugin.json +18 -0
  226. package/skills/pdflatex/.codex-plugin/plugin.json +18 -0
  227. package/skills/plain-english-content/.claude-plugin/plugin.json +15 -0
  228. package/skills/plain-english-content/.codex-plugin/plugin.json +15 -0
  229. package/skills/prd/.claude-plugin/plugin.json +19 -0
  230. package/skills/prd/.codex-plugin/plugin.json +19 -0
  231. package/skills/research-brief/.claude-plugin/plugin.json +20 -0
  232. package/skills/research-brief/.codex-plugin/plugin.json +20 -0
  233. package/skills/security/.claude-plugin/plugin.json +18 -0
  234. package/skills/security/.codex-plugin/plugin.json +18 -0
  235. package/skills/skill-creator/.claude-plugin/plugin.json +17 -0
  236. package/skills/skill-creator/.codex-plugin/plugin.json +17 -0
  237. package/skills/slack-channel-monitor/.claude-plugin/plugin.json +21 -0
  238. package/skills/slack-channel-monitor/.codex-plugin/plugin.json +21 -0
  239. package/skills/slack-standup-digest/.claude-plugin/plugin.json +21 -0
  240. package/skills/slack-standup-digest/.codex-plugin/plugin.json +21 -0
  241. package/skills/spark-version-upgrade/.claude-plugin/plugin.json +20 -0
  242. package/skills/spark-version-upgrade/.codex-plugin/plugin.json +20 -0
  243. package/skills/ssh/.claude-plugin/plugin.json +18 -0
  244. package/skills/ssh/.codex-plugin/plugin.json +18 -0
  245. package/skills/swift-linux/.claude-plugin/plugin.json +17 -0
  246. package/skills/swift-linux/.codex-plugin/plugin.json +17 -0
  247. package/skills/technical-writing/.claude-plugin/plugin.json +15 -0
  248. package/skills/technical-writing/.codex-plugin/plugin.json +15 -0
  249. package/skills/theme-factory/.claude-plugin/plugin.json +19 -0
  250. package/skills/theme-factory/.codex-plugin/plugin.json +19 -0
  251. package/skills/ticket-to-code-change/.claude-plugin/plugin.json +21 -0
  252. package/skills/ticket-to-code-change/.codex-plugin/plugin.json +21 -0
  253. package/skills/upstream-fork-sync/.claude-plugin/plugin.json +21 -0
  254. package/skills/upstream-fork-sync/.codex-plugin/plugin.json +21 -0
  255. package/skills/uv/.claude-plugin/plugin.json +18 -0
  256. package/skills/uv/.codex-plugin/plugin.json +18 -0
  257. package/skills/vercel/.claude-plugin/plugin.json +18 -0
  258. package/skills/vercel/.codex-plugin/plugin.json +18 -0
  259. package/tests/fixtures/automations/capabilities.json +6 -3
  260. package/tests/fixtures/automations/github-pr-reviewer.json +64 -43
  261. package/tests/github_automation_helpers.py +7 -1
  262. package/tests/test_agent_conversation_dispatch.py +180 -0
  263. package/tests/test_automation_setup.py +3 -3
  264. package/tests/test_github_developer_delivery.py +150 -0
  265. package/tests/test_github_reviewer_delivery.py +110 -0
  266. package/tests/test_github_triage_delivery.py +78 -41
  267. package/tests/test_github_watchdog_delivery.py +233 -0
@@ -16,7 +16,13 @@ def worker(name, tmp_path, monkeypatch):
16
16
  bundle.mkdir()
17
17
  for filename, source in manifest["setup"]["bundle"]["files"].items():
18
18
  (bundle / filename).write_text((ROOT / source).read_text())
19
- for module in ("main", "github_client", "qa_prompt", "worker"):
19
+ for module in (
20
+ "agent_conversation",
21
+ "main",
22
+ "github_client",
23
+ "qa_prompt",
24
+ "worker",
25
+ ):
20
26
  monkeypatch.delitem(sys.modules, module, raising=False)
21
27
  monkeypatch.syspath_prepend(str(bundle))
22
28
  spec = importlib.util.spec_from_file_location("worker", bundle / "worker.py")
@@ -0,0 +1,180 @@
1
+ import json
2
+ from unittest.mock import MagicMock
3
+ from uuid import NAMESPACE_URL, UUID, uuid5
4
+
5
+ import agent_conversation
6
+ import github_client
7
+ from openhands.sdk.conversation.state import ConversationExecutionStatus
8
+
9
+
10
+ def _dispatcher(monkeypatch):
11
+ monkeypatch.setenv("AGENT_SERVER_URL", "http://agent")
12
+ monkeypatch.setenv("SESSION_API_KEY", "session")
13
+ monkeypatch.setenv(
14
+ "AUTOMATION_AGENT_PROFILE_ID", "11111111-1111-4111-8111-111111111111"
15
+ )
16
+ monkeypatch.setenv(
17
+ "AUTOMATION_EVENT_PAYLOAD", json.dumps({"automation_id": "automation-1"})
18
+ )
19
+ return agent_conversation.AgentConversationDispatcher()
20
+
21
+
22
+ def _state_key(subject):
23
+ conversation_id = uuid5(NAMESPACE_URL, f"automation-1:{subject}")
24
+ return f"agent-conversation-{conversation_id}"
25
+
26
+
27
+ def _fake_kv(monkeypatch, state):
28
+ def request(key, method, value=None):
29
+ if method == "GET":
30
+ return state.get(key)
31
+ state[key] = value
32
+ return {"key": key, "value": value}
33
+
34
+ monkeypatch.setattr(agent_conversation, "_kv_request", request)
35
+
36
+
37
+ def test_github_secret_falls_back_to_agent_server(monkeypatch):
38
+ monkeypatch.delenv("REVIEW_TOKEN", raising=False)
39
+ monkeypatch.setenv("AGENT_SERVER_URL", "http://agent")
40
+ monkeypatch.setenv("SESSION_API_KEY", "session")
41
+ secret = MagicMock()
42
+ secret.get_value.return_value = "saved-token"
43
+ workspace = MagicMock()
44
+ workspace.get_secrets.return_value = {"REVIEW_TOKEN": secret}
45
+ monkeypatch.setattr(
46
+ "openhands.sdk.workspace.RemoteWorkspace", lambda **kwargs: workspace
47
+ )
48
+
49
+ assert github_client._load_secret("REVIEW_TOKEN") == "saved-token"
50
+ workspace.get_secrets.assert_called_once_with(["REVIEW_TOKEN"])
51
+ workspace.reset_client.assert_called_once_with()
52
+
53
+
54
+ def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch):
55
+ state = {}
56
+ _fake_kv(monkeypatch, state)
57
+ monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
58
+ workspace = MagicMock()
59
+ workspace.__enter__.return_value = workspace
60
+ monkeypatch.setattr(
61
+ agent_conversation, "RemoteWorkspace", lambda **kwargs: workspace
62
+ )
63
+ missing = agent_conversation.httpx.HTTPStatusError(
64
+ "missing",
65
+ request=MagicMock(),
66
+ response=MagicMock(status_code=404),
67
+ )
68
+ monkeypatch.setattr(
69
+ agent_conversation.RemoteConversation, "attach", MagicMock(side_effect=missing)
70
+ )
71
+ conversation = MagicMock()
72
+ create = MagicMock(return_value=conversation)
73
+ monkeypatch.setattr(agent_conversation.RemoteConversation, "create", create)
74
+
75
+ with _dispatcher(monkeypatch) as dispatcher:
76
+ result = dispatcher.deliver("repo:issue:7", "revision-1", "work")
77
+
78
+ request = create.call_args.args[1]
79
+ assert request.agent_profile_id == UUID("11111111-1111-4111-8111-111111111111")
80
+ assert request.initial_message.run is True
81
+ assert result["disposition"] == "created"
82
+ record = state[_state_key("repo:issue:7")]
83
+ assert record == {
84
+ "subject": "repo:issue:7",
85
+ "conversation_id": result["conversation_id"],
86
+ "delivery": "revision-1",
87
+ }
88
+
89
+
90
+ def test_known_subject_resumes_once_per_delivery(monkeypatch):
91
+ state = {
92
+ _state_key("repo:pr:9"): {
93
+ "subject": "repo:pr:9",
94
+ "conversation_id": "22222222-2222-4222-8222-222222222222",
95
+ "delivery": "head-1",
96
+ }
97
+ }
98
+ _fake_kv(monkeypatch, state)
99
+ monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
100
+ workspace = MagicMock()
101
+ workspace.__enter__.return_value = workspace
102
+ monkeypatch.setattr(
103
+ agent_conversation, "RemoteWorkspace", lambda **kwargs: workspace
104
+ )
105
+ conversation = MagicMock()
106
+ conversation.state.execution_status = ConversationExecutionStatus.FINISHED
107
+ attach = MagicMock(return_value=conversation)
108
+ monkeypatch.setattr(agent_conversation.RemoteConversation, "attach", attach)
109
+ with _dispatcher(monkeypatch) as dispatcher:
110
+ duplicate = dispatcher.deliver("repo:pr:9", "head-1", "old")
111
+ resumed = dispatcher.deliver("repo:pr:9", "head-2", "new")
112
+
113
+ assert duplicate["disposition"] == "deduplicated"
114
+ assert resumed["disposition"] == "resumed"
115
+ conversation.send_message.assert_called_once_with("new")
116
+ conversation.run.assert_called_once_with(blocking=False)
117
+
118
+
119
+ def test_same_delivery_resumes_paused_conversation(monkeypatch):
120
+ state = {
121
+ _state_key("repo:pr:9"): {
122
+ "subject": "repo:pr:9",
123
+ "conversation_id": "22222222-2222-4222-8222-222222222222",
124
+ "delivery": "head-1",
125
+ }
126
+ }
127
+ _fake_kv(monkeypatch, state)
128
+ monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
129
+ workspace = MagicMock()
130
+ workspace.__enter__.return_value = workspace
131
+ monkeypatch.setattr(
132
+ agent_conversation, "RemoteWorkspace", lambda **kwargs: workspace
133
+ )
134
+ conversation = MagicMock()
135
+ conversation.state.execution_status = ConversationExecutionStatus.PAUSED
136
+ monkeypatch.setattr(
137
+ agent_conversation.RemoteConversation,
138
+ "attach",
139
+ MagicMock(return_value=conversation),
140
+ )
141
+
142
+ with _dispatcher(monkeypatch) as dispatcher:
143
+ result = dispatcher.deliver("repo:pr:9", "head-1", "old")
144
+
145
+ assert result["disposition"] == "resumed"
146
+ conversation.send_message.assert_not_called()
147
+ conversation.run.assert_called_once_with(blocking=False)
148
+
149
+
150
+ def test_subjects_use_independent_kv_records(monkeypatch):
151
+ state = {}
152
+ _fake_kv(monkeypatch, state)
153
+ monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
154
+ workspace = MagicMock()
155
+ workspace.__enter__.return_value = workspace
156
+ monkeypatch.setattr(
157
+ agent_conversation, "RemoteWorkspace", lambda **kwargs: workspace
158
+ )
159
+ missing = agent_conversation.httpx.HTTPStatusError(
160
+ "missing",
161
+ request=MagicMock(),
162
+ response=MagicMock(status_code=404),
163
+ )
164
+ monkeypatch.setattr(
165
+ agent_conversation.RemoteConversation, "attach", MagicMock(side_effect=missing)
166
+ )
167
+ monkeypatch.setattr(
168
+ agent_conversation.RemoteConversation,
169
+ "create",
170
+ MagicMock(side_effect=[MagicMock(), MagicMock()]),
171
+ )
172
+
173
+ with _dispatcher(monkeypatch) as dispatcher:
174
+ dispatcher.deliver("repo:issue:7", "revision-1", "first")
175
+ dispatcher.deliver("repo:issue:8", "revision-1", "second")
176
+
177
+ assert set(state) == {
178
+ _state_key("repo:issue:7"),
179
+ _state_key("repo:issue:8"),
180
+ }
@@ -676,9 +676,9 @@ def test_schema_rejects_content_a_setup_block_must_never_carry() -> None:
676
676
  rejected.append(("{{env.GITHUB_TOKEN}}", with_unknown_placeholder))
677
677
 
678
678
  with_secret_value = deepcopy(entry)
679
- with_secret_value["requires"]["integrations"]["github"]["value"] = (
680
- "ghp_notarealtokenvalue00"
681
- )
679
+ with_secret_value["requires"]["integrations"]["github"] = {
680
+ "value": "ghp_notarealtokenvalue00"
681
+ }
682
682
  rejected.append(("value", with_secret_value))
683
683
 
684
684
  with_repeated_identity = deepcopy(entry)
@@ -0,0 +1,150 @@
1
+ """Contract tests for independent developer delivery."""
2
+
3
+ from unittest.mock import Mock
4
+
5
+ from github_automation_helpers import worker
6
+
7
+
8
+ def _developer(tmp_path, monkeypatch):
9
+ module = worker("github-issue-to-pr", tmp_path, monkeypatch)
10
+ run = object.__new__(module.IssueToPR)
11
+ run.config = {
12
+ "trigger_label": "ready-for-dev",
13
+ "branch_prefix": "openhands/issue",
14
+ "review_label": "openhands-review",
15
+ }
16
+ run.repository = "owner/repo"
17
+ run.token_name = "FACTORY_GITHUB_DEVELOPER_TOKEN"
18
+ run.dispatcher = Mock()
19
+ run.dependencies_complete = lambda issue: True
20
+ run.statuses = lambda sha: {}
21
+ return module, run
22
+
23
+
24
+ def test_developer_fans_out_ready_issues_by_priority(tmp_path, monkeypatch):
25
+ _module, run = _developer(tmp_path, monkeypatch)
26
+ issues = [
27
+ {
28
+ "number": 2,
29
+ "title": "Normal",
30
+ "updated_at": "u2",
31
+ "labels": [{"name": "ready-for-dev"}],
32
+ },
33
+ {
34
+ "number": 1,
35
+ "title": "High",
36
+ "updated_at": "u1",
37
+ "labels": [{"name": "ready-for-dev"}, {"name": "priority:high"}],
38
+ },
39
+ ]
40
+ run.open_issues = lambda: issues
41
+ run.gh_pages = lambda path: []
42
+ run.gh = Mock(
43
+ side_effect=[{"id": 55, "default_branch": "main"}, {"object": {"sha": "base"}}]
44
+ )
45
+ submit = Mock(
46
+ side_effect=lambda **kwargs: {
47
+ "disposition": "created",
48
+ "conversation_id": kwargs["subject"],
49
+ }
50
+ )
51
+ run.dispatcher.deliver = submit
52
+
53
+ run.run()
54
+
55
+ assert [call.kwargs["subject"] for call in submit.call_args_list] == [
56
+ "55:issue:1",
57
+ "55:issue:2",
58
+ ]
59
+ prompt = submit.call_args_list[0].kwargs["prompt"]
60
+ assert "workspace starts empty" in prompt
61
+ assert "FACTORY_GITHUB_DEVELOPER_TOKEN" in prompt
62
+ assert "gh repo clone owner/repo ." in prompt
63
+
64
+
65
+ def test_developer_submits_failed_review_as_same_subject(tmp_path, monkeypatch):
66
+ _module, run = _developer(tmp_path, monkeypatch)
67
+ issue = {"number": 4, "title": "Feature", "updated_at": "u4", "labels": []}
68
+ pr = {
69
+ "number": 8,
70
+ "head": {"ref": "openhands/issue-4", "sha": "head"},
71
+ "base": {"ref": "main"},
72
+ "labels": [],
73
+ }
74
+ run.open_issues = lambda: [issue]
75
+ run.gh_pages = lambda path: [pr]
76
+ run.statuses = lambda sha: {"software-factory/review": "failure"}
77
+ run.gh = Mock(
78
+ side_effect=[{"id": 55, "default_branch": "main"}, {"object": {"sha": "base"}}]
79
+ )
80
+ submit = Mock(
81
+ return_value={"disposition": "created", "conversation_id": "conversation"}
82
+ )
83
+ run.dispatcher.deliver = submit
84
+
85
+ run.run()
86
+
87
+ assert submit.call_args.kwargs["subject"] == "55:issue:4"
88
+ assert submit.call_args.kwargs["delivery"] == "4:head"
89
+ prompt = submit.call_args.kwargs["prompt"]
90
+ assert "Revise existing PR #8 at exact head `head`" in prompt
91
+ assert "check out the existing remote branch `openhands/issue-4`" in prompt
92
+ assert "do not open another pull request" in prompt
93
+
94
+
95
+ def test_developer_continues_after_one_submission_fails(tmp_path, monkeypatch):
96
+ _module, run = _developer(tmp_path, monkeypatch)
97
+ issues = [
98
+ {
99
+ "number": 1,
100
+ "title": "First",
101
+ "updated_at": "u1",
102
+ "labels": [{"name": "ready-for-dev"}],
103
+ },
104
+ {
105
+ "number": 2,
106
+ "title": "Second",
107
+ "updated_at": "u2",
108
+ "labels": [{"name": "ready-for-dev"}],
109
+ },
110
+ ]
111
+ run.open_issues = lambda: issues
112
+ run.gh_pages = lambda path: []
113
+ run.gh = Mock(
114
+ side_effect=[{"id": 55, "default_branch": "main"}, {"object": {"sha": "base"}}]
115
+ )
116
+ submit = Mock(
117
+ side_effect=[
118
+ RuntimeError("unavailable"),
119
+ {"disposition": "created", "conversation_id": "second"},
120
+ ]
121
+ )
122
+ run.dispatcher.deliver = submit
123
+
124
+ run.run()
125
+
126
+ assert [call.kwargs["subject"] for call in submit.call_args_list] == [
127
+ "55:issue:1",
128
+ "55:issue:2",
129
+ ]
130
+
131
+
132
+ def test_developer_skips_open_pr_awaiting_review(tmp_path, monkeypatch):
133
+ _module, run = _developer(tmp_path, monkeypatch)
134
+ issue = {"number": 4, "title": "Feature", "labels": [{"name": "ready-for-dev"}]}
135
+ pr = {
136
+ "number": 8,
137
+ "head": {"ref": "openhands/issue-4", "sha": "head"},
138
+ "labels": [{"name": "openhands-review"}],
139
+ }
140
+ run.open_issues = lambda: [issue]
141
+ run.gh_pages = lambda path: [pr]
142
+ run.gh = Mock(
143
+ side_effect=[{"id": 55, "default_branch": "main"}, {"object": {"sha": "base"}}]
144
+ )
145
+ submit = Mock()
146
+ run.dispatcher.deliver = submit
147
+
148
+ run.run()
149
+
150
+ submit.assert_not_called()
@@ -0,0 +1,110 @@
1
+ """Contract tests for delegated GitHub PR review."""
2
+
3
+ from pathlib import Path
4
+ from unittest.mock import Mock
5
+
6
+ from github_automation_helpers import worker
7
+
8
+
9
+ def _reviewer(tmp_path, monkeypatch):
10
+ module = worker("github-pr-reviewer", tmp_path, monkeypatch)
11
+ run = object.__new__(module.PullRequestReviewer)
12
+ run.config = {"trigger_label": "openhands-review"}
13
+ run.repository = "owner/repo"
14
+ run.token = "token"
15
+ run.token_name = "FACTORY_GITHUB_REVIEWER_TOKEN"
16
+ run.dispatcher = Mock()
17
+ return module, run
18
+
19
+
20
+ def test_reviewer_submits_each_labeled_exact_head(tmp_path, monkeypatch):
21
+ module, run = _reviewer(tmp_path, monkeypatch)
22
+ prs = [
23
+ {
24
+ "number": 2,
25
+ "head": {"sha": "head-2"},
26
+ "labels": [{"name": "openhands-review"}],
27
+ },
28
+ {"number": 1, "head": {"sha": "head-1"}, "labels": []},
29
+ ]
30
+ run.gh_pages = lambda path: prs
31
+ run.gh = Mock(side_effect=[{"id": 99}, prs[0]])
32
+ monkeypatch.setattr(
33
+ module.workflow,
34
+ "_latest_trigger_label_event",
35
+ lambda *args: {"id": 7, "created_at": "now"},
36
+ )
37
+ submit = Mock(
38
+ return_value={"disposition": "created", "conversation_id": "conversation"}
39
+ )
40
+ run.dispatcher.deliver = submit
41
+
42
+ run.run()
43
+
44
+ assert submit.call_args.kwargs["subject"] == "99:pr:2"
45
+ assert submit.call_args.kwargs["delivery"] == "7:head-2"
46
+ prompt = submit.call_args.kwargs["prompt"]
47
+ assert "publish the review directly to GitHub" in prompt
48
+ assert "software-factory/review" in prompt
49
+ assert "software-factory/tests" in prompt
50
+ assert "FACTORY_GITHUB_REVIEWER_TOKEN" in prompt
51
+ assert "gh auth setup-git" in prompt
52
+ assert "GIT_TERMINAL_PROMPT=0" in prompt
53
+ assert "Never paste JSON artifacts" in prompt
54
+
55
+
56
+ def test_reviewer_ignores_unlabeled_prs(tmp_path, monkeypatch):
57
+ _module, run = _reviewer(tmp_path, monkeypatch)
58
+ run.gh_pages = lambda path: [{"number": 1, "labels": [], "head": {"sha": "head"}}]
59
+ run.gh = Mock(return_value={"id": 99})
60
+ submit = Mock()
61
+ run.dispatcher.deliver = submit
62
+
63
+ run.run()
64
+
65
+ submit.assert_not_called()
66
+
67
+
68
+ def test_reviewer_continues_after_one_submission_fails(tmp_path, monkeypatch):
69
+ module, run = _reviewer(tmp_path, monkeypatch)
70
+ prs = [
71
+ {
72
+ "number": number,
73
+ "labels": [{"name": "openhands-review"}],
74
+ "head": {"sha": f"head-{number}"},
75
+ }
76
+ for number in (1, 2)
77
+ ]
78
+ run.gh_pages = lambda path: prs
79
+ run.gh = Mock(side_effect=[{"id": 99}, *prs])
80
+ monkeypatch.setattr(
81
+ module.workflow,
82
+ "_latest_trigger_label_event",
83
+ lambda *args: {"id": 7, "created_at": "now"},
84
+ )
85
+ submit = Mock(
86
+ side_effect=[
87
+ RuntimeError("temporary failure"),
88
+ {"disposition": "created", "conversation_id": "conversation"},
89
+ ]
90
+ )
91
+ run.dispatcher.deliver = submit
92
+
93
+ run.run()
94
+
95
+ assert submit.call_count == 2
96
+
97
+
98
+ def test_reviewer_scanner_has_no_conversation_or_runtime_code():
99
+ source = (
100
+ Path(__file__).resolve().parents[1]
101
+ / "skills/github-pr-reviewer/scripts/worker.py"
102
+ ).read_text()
103
+ for forbidden in (
104
+ "RemoteConversation",
105
+ "RemoteWorkspace",
106
+ "AGENT_SERVER_URL",
107
+ "SESSION_API_KEY",
108
+ "AUTOMATION_CONVERSATION_ID",
109
+ ):
110
+ assert forbidden not in source
@@ -1,53 +1,90 @@
1
1
  """Contract tests for independent triage delivery."""
2
2
 
3
- import json
4
3
  from unittest.mock import Mock
5
4
 
6
- import pytest
7
5
  from github_automation_helpers import worker
8
6
 
9
7
 
10
- @pytest.mark.parametrize(
11
- "ready,criteria",
12
- [(True, ["One observable result"]), (False, ["Clarify account ownership"])],
13
- )
14
- def test_triage_preserves_labels_and_only_readies_clear_work(
15
- tmp_path, monkeypatch, ready, criteria
16
- ):
17
- cls = worker("github-issue-triage", tmp_path, monkeypatch).IssueTriage
8
+ def _triage(tmp_path, monkeypatch, issues):
9
+ module = worker("github-issue-triage", tmp_path, monkeypatch)
10
+ cls = module.IssueTriage
18
11
  run = object.__new__(cls)
19
- issue = {
20
- "number": 1,
21
- "title": "A small feature",
22
- "body": "",
23
- "labels": [{"name": "enhancement"}],
24
- }
25
- run.open_issues = lambda: [issue]
26
- run.gh_pages = lambda path: []
12
+ run.repository = "owner/repo"
13
+ run.token_name = "GITHUB_PERSONAL_ACCESS_TOKEN"
14
+ run.dispatcher = Mock()
15
+ run.open_issues = lambda: issues
27
16
  run.dependencies_complete = lambda issue: True
28
- result = {
29
- "ready": ready,
30
- "priority": "normal",
31
- "acceptance_criteria": criteria,
32
- "rationale": "Small independent change",
33
- }
34
- run.evidence = tmp_path
35
- run.conversation = Mock()
36
- run.conversation.run.side_effect = lambda **kwargs: (
37
- tmp_path / "triage.json"
38
- ).write_text(json.dumps(result))
39
- calls = []
40
- run.gh = lambda *args: calls.append(args)
41
- run.comment = lambda *args: calls.append(("comment", *args))
17
+ run.gh = lambda method, path, body=None: {"id": 9876}
18
+ run.gh_pages = lambda path: []
19
+ return module, run
42
20
 
43
- run.run()
44
21
 
45
- updates = [call for call in calls if call[0] == "PATCH"]
46
- assert bool(updates) is ready
47
- if ready:
48
- assert set(updates[0][2]["labels"]) == {
49
- "enhancement",
50
- "priority:normal",
51
- "ready-for-dev",
22
+ def test_triage_submits_each_changed_issue_as_agent_work(tmp_path, monkeypatch):
23
+ issues = [
24
+ {"number": 2, "title": "Second", "body": "B", "labels": []},
25
+ {"number": 1, "title": "First", "body": "A", "labels": []},
26
+ ]
27
+ _module, run = _triage(tmp_path, monkeypatch, issues)
28
+ submit = Mock(
29
+ side_effect=lambda **kwargs: {
30
+ "disposition": "created",
31
+ "conversation_id": kwargs["subject"],
52
32
  }
53
- assert criteria[0] in next(call[2] for call in calls if call[0] == "comment")
33
+ )
34
+ run.dispatcher.deliver = submit
35
+
36
+ run.run()
37
+
38
+ assert [call.kwargs["subject"] for call in submit.call_args_list] == [
39
+ "9876:issue:1",
40
+ "9876:issue:2",
41
+ ]
42
+ assert "Do not implement code" in submit.call_args_list[0].kwargs["prompt"]
43
+ assert "GITHUB_PERSONAL_ACCESS_TOKEN" in submit.call_args_list[0].kwargs["prompt"]
44
+
45
+
46
+ def test_triage_continues_after_one_submission_fails(tmp_path, monkeypatch):
47
+ issues = [
48
+ {"number": 1, "title": "First", "body": "A", "labels": []},
49
+ {"number": 2, "title": "Second", "body": "B", "labels": []},
50
+ ]
51
+ _module, run = _triage(tmp_path, monkeypatch, issues)
52
+ submit = Mock(
53
+ side_effect=[
54
+ RuntimeError("unavailable"),
55
+ {"disposition": "created", "conversation_id": "second"},
56
+ ]
57
+ )
58
+ run.dispatcher.deliver = submit
59
+
60
+ run.run()
61
+
62
+ assert [call.kwargs["subject"] for call in submit.call_args_list] == [
63
+ "9876:issue:1",
64
+ "9876:issue:2",
65
+ ]
66
+
67
+
68
+ def test_triage_skips_ready_blocked_and_unchanged_issues(tmp_path, monkeypatch):
69
+ issues = [
70
+ {
71
+ "number": 1,
72
+ "title": "Ready",
73
+ "body": "",
74
+ "labels": [{"name": "ready-for-dev"}],
75
+ },
76
+ {"number": 2, "title": "Blocked", "body": "", "labels": []},
77
+ {"number": 3, "title": "Unchanged", "body": "", "labels": []},
78
+ ]
79
+ module, run = _triage(tmp_path, monkeypatch, issues)
80
+ run.dependencies_complete = lambda issue: issue["number"] != 2
81
+ marker = module.hashlib.sha256(
82
+ module.json.dumps(["Unchanged", "", []], sort_keys=True).encode()
83
+ ).hexdigest()
84
+ run.gh_pages = lambda path: [{"id": 10, "body": f"<!-- triage-source:{marker} -->"}]
85
+ submit = Mock()
86
+ run.dispatcher.deliver = submit
87
+
88
+ run.run()
89
+
90
+ submit.assert_not_called()