@openhands/extensions 0.21.0 → 0.22.1

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 +150 -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 +150 -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 +150 -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 +150 -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 +150 -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 +196 -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
@@ -17,7 +17,8 @@
17
17
  "timezone": "UTC",
18
18
  "repositories": [
19
19
  "OpenHands/agent-server-gui"
20
- ]
20
+ ],
21
+ "githubTokenSecret": "GITHUB_PERSONAL_ACCESS_TOKEN"
21
22
  },
22
23
  "upload": {
23
24
  "request": {
@@ -29,7 +30,9 @@
29
30
  "contentType": "application/gzip",
30
31
  "packs": [
31
32
  "config.json",
32
- "main.py"
33
+ "main.py",
34
+ "github_client.py",
35
+ "worker.py"
33
36
  ]
34
37
  },
35
38
  "response": {
@@ -57,17 +60,18 @@
57
60
  "timezone": "UTC"
58
61
  },
59
62
  "tarball_path": "oh-internal://uploads/00000000-0000-0000-0000-000000000000",
60
- "entrypoint": "python3 main.py",
61
- "timeout": 600,
63
+ "entrypoint": "python3 worker.py",
64
+ "timeout": 300,
62
65
  "template": {
63
66
  "id": "github-pr-reviewer",
64
- "version": "1.0.0",
67
+ "version": "1.1.1",
65
68
  "config": {
66
69
  "repos": [
67
70
  "OpenHands/agent-server-gui"
68
71
  ],
69
72
  "trigger_label": "openhands-review",
70
- "review_tone": "thorough"
73
+ "review_tone": "thorough",
74
+ "github_token_secret": "GITHUB_PERSONAL_ACCESS_TOKEN"
71
75
  }
72
76
  }
73
77
  }
@@ -93,17 +97,18 @@
93
97
  "timezone": "UTC"
94
98
  },
95
99
  "tarball_path": "oh-internal://uploads/2c1f9a70-77c4-4a1e-9a9d-6f1f2b0c1d34",
96
- "entrypoint": "python3 main.py",
97
- "timeout": 600,
100
+ "entrypoint": "python3 worker.py",
101
+ "timeout": 300,
98
102
  "template": {
99
103
  "id": "github-pr-reviewer",
100
- "version": "1.0.0",
104
+ "version": "1.1.1",
101
105
  "config": {
102
106
  "repos": [
103
107
  "OpenHands/agent-server-gui"
104
108
  ],
105
109
  "trigger_label": "openhands-review",
106
- "review_tone": "thorough"
110
+ "review_tone": "thorough",
111
+ "github_token_secret": "GITHUB_PERSONAL_ACCESS_TOKEN"
107
112
  }
108
113
  }
109
114
  }
@@ -124,8 +129,8 @@
124
129
  },
125
130
  "tarball_path": "oh-internal://uploads/8d2c1f90-7e34-4a55-b0d1-6c9e3a2f4b88",
126
131
  "setup_script_path": "setup.sh",
127
- "entrypoint": ".venv/bin/python main.py",
128
- "timeout": 600,
132
+ "entrypoint": "python3 worker.py",
133
+ "timeout": 300,
129
134
  "keep_alive": null,
130
135
  "enabled": true,
131
136
  "last_triggered_at": null,
@@ -146,7 +151,8 @@
146
151
  "repositories": [
147
152
  "OpenHands/agent-server-gui",
148
153
  "OpenHands/automation"
149
- ]
154
+ ],
155
+ "githubTokenSecret": "GITHUB_PERSONAL_ACCESS_TOKEN"
150
156
  },
151
157
  "upload": {
152
158
  "request": {
@@ -158,7 +164,9 @@
158
164
  "contentType": "application/gzip",
159
165
  "packs": [
160
166
  "config.json",
161
- "main.py"
167
+ "main.py",
168
+ "github_client.py",
169
+ "worker.py"
162
170
  ]
163
171
  },
164
172
  "response": {
@@ -186,18 +194,19 @@
186
194
  "timezone": "UTC"
187
195
  },
188
196
  "tarball_path": "oh-internal://uploads/00000000-0000-0000-0000-000000000000",
189
- "entrypoint": "python3 main.py",
190
- "timeout": 600,
197
+ "entrypoint": "python3 worker.py",
198
+ "timeout": 300,
191
199
  "template": {
192
200
  "id": "github-pr-reviewer",
193
- "version": "1.0.0",
201
+ "version": "1.1.1",
194
202
  "config": {
195
203
  "repos": [
196
204
  "OpenHands/agent-server-gui",
197
205
  "OpenHands/automation"
198
206
  ],
199
207
  "trigger_label": "openhands-review",
200
- "review_tone": "thorough"
208
+ "review_tone": "thorough",
209
+ "github_token_secret": "GITHUB_PERSONAL_ACCESS_TOKEN"
201
210
  }
202
211
  }
203
212
  }
@@ -223,18 +232,19 @@
223
232
  "timezone": "UTC"
224
233
  },
225
234
  "tarball_path": "oh-internal://uploads/2c1f9a70-77c4-4a1e-9a9d-6f1f2b0c1d34",
226
- "entrypoint": "python3 main.py",
227
- "timeout": 600,
235
+ "entrypoint": "python3 worker.py",
236
+ "timeout": 300,
228
237
  "template": {
229
238
  "id": "github-pr-reviewer",
230
- "version": "1.0.0",
239
+ "version": "1.1.1",
231
240
  "config": {
232
241
  "repos": [
233
242
  "OpenHands/agent-server-gui",
234
243
  "OpenHands/automation"
235
244
  ],
236
245
  "trigger_label": "openhands-review",
237
- "review_tone": "thorough"
246
+ "review_tone": "thorough",
247
+ "github_token_secret": "GITHUB_PERSONAL_ACCESS_TOKEN"
238
248
  }
239
249
  }
240
250
  }
@@ -255,8 +265,8 @@
255
265
  },
256
266
  "tarball_path": "oh-internal://uploads/8d2c1f90-7e34-4a55-b0d1-6c9e3a2f4b88",
257
267
  "setup_script_path": "setup.sh",
258
- "entrypoint": ".venv/bin/python main.py",
259
- "timeout": 600,
268
+ "entrypoint": "python3 worker.py",
269
+ "timeout": 300,
260
270
  "keep_alive": null,
261
271
  "enabled": true,
262
272
  "last_triggered_at": null,
@@ -265,14 +275,15 @@
265
275
  "preset_metadata": {
266
276
  "template": {
267
277
  "id": "github-pr-reviewer",
268
- "version": "1.0.0",
278
+ "version": "1.1.1",
269
279
  "config": {
270
280
  "repos": [
271
281
  "OpenHands/agent-server-gui",
272
282
  "OpenHands/automation"
273
283
  ],
274
284
  "trigger_label": "openhands-review",
275
- "review_tone": "thorough"
285
+ "review_tone": "thorough",
286
+ "github_token_secret": "GITHUB_PERSONAL_ACCESS_TOKEN"
276
287
  }
277
288
  }
278
289
  }
@@ -290,7 +301,8 @@
290
301
  "timezone": "UTC",
291
302
  "repositories": [
292
303
  "OpenHands/agent-server-gui"
293
- ]
304
+ ],
305
+ "githubTokenSecret": "GITHUB_PERSONAL_ACCESS_TOKEN"
294
306
  },
295
307
  "preflight": {
296
308
  "request": {
@@ -307,17 +319,18 @@
307
319
  "timezone": "UTC"
308
320
  },
309
321
  "tarball_path": "oh-internal://uploads/00000000-0000-0000-0000-000000000000",
310
- "entrypoint": "python3 main.py",
311
- "timeout": 600,
322
+ "entrypoint": "python3 worker.py",
323
+ "timeout": 300,
312
324
  "template": {
313
325
  "id": "github-pr-reviewer",
314
- "version": "1.0.0",
326
+ "version": "1.1.1",
315
327
  "config": {
316
328
  "repos": [
317
329
  "OpenHands/agent-server-gui"
318
330
  ],
319
331
  "trigger_label": "openhands-review",
320
- "review_tone": "concise"
332
+ "review_tone": "concise",
333
+ "github_token_secret": "GITHUB_PERSONAL_ACCESS_TOKEN"
321
334
  }
322
335
  }
323
336
  }
@@ -351,7 +364,8 @@
351
364
  "timezone": "UTC",
352
365
  "repositories": [
353
366
  "OpenHands/agent-server-gui"
354
- ]
367
+ ],
368
+ "githubTokenSecret": "GITHUB_PERSONAL_ACCESS_TOKEN"
355
369
  },
356
370
  "upload": {
357
371
  "request": {
@@ -363,7 +377,9 @@
363
377
  "contentType": "application/gzip",
364
378
  "packs": [
365
379
  "config.json",
366
- "main.py"
380
+ "main.py",
381
+ "github_client.py",
382
+ "worker.py"
367
383
  ]
368
384
  },
369
385
  "response": {
@@ -388,17 +404,18 @@
388
404
  "timezone": "UTC"
389
405
  },
390
406
  "tarball_path": "oh-internal://uploads/2c1f9a70-77c4-4a1e-9a9d-6f1f2b0c1d34",
391
- "entrypoint": "python3 main.py",
392
- "timeout": 600,
407
+ "entrypoint": "python3 worker.py",
408
+ "timeout": 300,
393
409
  "template": {
394
410
  "id": "github-pr-reviewer",
395
- "version": "1.0.0",
411
+ "version": "1.1.1",
396
412
  "config": {
397
413
  "repos": [
398
414
  "OpenHands/agent-server-gui"
399
415
  ],
400
416
  "trigger_label": "openhands-review",
401
- "review_tone": "concise"
417
+ "review_tone": "concise",
418
+ "github_token_secret": "GITHUB_PERSONAL_ACCESS_TOKEN"
402
419
  }
403
420
  }
404
421
  }
@@ -435,7 +452,8 @@
435
452
  "timezone": "UTC",
436
453
  "repositories": [
437
454
  "OpenHands/agent-server-gui"
438
- ]
455
+ ],
456
+ "githubTokenSecret": "GITHUB_PERSONAL_ACCESS_TOKEN"
439
457
  },
440
458
  "upload": {
441
459
  "request": {
@@ -447,7 +465,9 @@
447
465
  "contentType": "application/gzip",
448
466
  "packs": [
449
467
  "config.json",
450
- "main.py"
468
+ "main.py",
469
+ "github_client.py",
470
+ "worker.py"
451
471
  ]
452
472
  },
453
473
  "response": {
@@ -472,17 +492,18 @@
472
492
  "timezone": "UTC"
473
493
  },
474
494
  "tarball_path": "oh-internal://uploads/2c1f9a70-77c4-4a1e-9a9d-6f1f2b0c1d34",
475
- "entrypoint": "python3 main.py",
476
- "timeout": 600,
495
+ "entrypoint": "python3 worker.py",
496
+ "timeout": 300,
477
497
  "template": {
478
498
  "id": "github-pr-reviewer",
479
- "version": "1.0.0",
499
+ "version": "1.1.1",
480
500
  "config": {
481
501
  "repos": [
482
502
  "OpenHands/agent-server-gui"
483
503
  ],
484
504
  "trigger_label": "openhands-review",
485
- "review_tone": "concise"
505
+ "review_tone": "concise",
506
+ "github_token_secret": "GITHUB_PERSONAL_ACCESS_TOKEN"
486
507
  }
487
508
  },
488
509
  "repos": [
@@ -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,196 @@
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
+ from openhands.sdk.secret import LookupSecret
9
+
10
+
11
+ def _dispatcher(monkeypatch):
12
+ monkeypatch.setenv("AGENT_SERVER_URL", "http://agent")
13
+ monkeypatch.setenv("SESSION_API_KEY", "session")
14
+ monkeypatch.setenv(
15
+ "AUTOMATION_AGENT_PROFILE_ID", "11111111-1111-4111-8111-111111111111"
16
+ )
17
+ monkeypatch.setenv(
18
+ "AUTOMATION_EVENT_PAYLOAD", json.dumps({"automation_id": "automation-1"})
19
+ )
20
+ return agent_conversation.AgentConversationDispatcher()
21
+
22
+
23
+ def _state_key(subject):
24
+ conversation_id = uuid5(NAMESPACE_URL, f"automation-1:{subject}")
25
+ return f"agent-conversation-{conversation_id}"
26
+
27
+
28
+ def _fake_kv(monkeypatch, state):
29
+ def request(key, method, value=None):
30
+ if method == "GET":
31
+ return state.get(key)
32
+ state[key] = value
33
+ return {"key": key, "value": value}
34
+
35
+ monkeypatch.setattr(agent_conversation, "_kv_request", request)
36
+
37
+
38
+ def test_github_secret_falls_back_to_agent_server(monkeypatch):
39
+ monkeypatch.delenv("REVIEW_TOKEN", raising=False)
40
+ monkeypatch.setenv("AGENT_SERVER_URL", "http://agent")
41
+ monkeypatch.setenv("SESSION_API_KEY", "session")
42
+ secret = MagicMock()
43
+ secret.get_value.return_value = "saved-token"
44
+ workspace = MagicMock()
45
+ workspace.get_secrets.return_value = {"REVIEW_TOKEN": secret}
46
+ monkeypatch.setattr(
47
+ "openhands.sdk.workspace.RemoteWorkspace", lambda **kwargs: workspace
48
+ )
49
+
50
+ assert github_client._load_secret("REVIEW_TOKEN") == "saved-token"
51
+ workspace.get_secrets.assert_called_once_with(["REVIEW_TOKEN"])
52
+ workspace.reset_client.assert_called_once_with()
53
+
54
+
55
+ def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch):
56
+ state = {}
57
+ _fake_kv(monkeypatch, state)
58
+ monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
59
+ workspace = MagicMock()
60
+ workspace.__enter__.return_value = workspace
61
+ scoped_secrets = {
62
+ "GITHUB_TOKEN": LookupSecret(url="/api/settings/secrets/GITHUB_TOKEN")
63
+ }
64
+ workspace.get_secrets.return_value = scoped_secrets
65
+ monkeypatch.setattr(
66
+ agent_conversation, "RemoteWorkspace", lambda **kwargs: workspace
67
+ )
68
+ missing = agent_conversation.httpx.HTTPStatusError(
69
+ "missing",
70
+ request=MagicMock(),
71
+ response=MagicMock(status_code=404),
72
+ )
73
+ monkeypatch.setattr(
74
+ agent_conversation.RemoteConversation, "attach", MagicMock(side_effect=missing)
75
+ )
76
+ conversation = MagicMock()
77
+ create = MagicMock(return_value=conversation)
78
+ monkeypatch.setattr(agent_conversation.RemoteConversation, "create", create)
79
+
80
+ with _dispatcher(monkeypatch) as dispatcher:
81
+ result = dispatcher.deliver("repo:issue:7", "revision-1", "work")
82
+
83
+ request = create.call_args.args[1]
84
+ assert request.agent_profile_id == UUID("11111111-1111-4111-8111-111111111111")
85
+ assert request.secrets == scoped_secrets
86
+ workspace.get_secrets.assert_called_once_with(
87
+ agent_profile_id="11111111-1111-4111-8111-111111111111"
88
+ )
89
+ assert request.initial_message.run is True
90
+ assert result["disposition"] == "created"
91
+ record = state[_state_key("repo:issue:7")]
92
+ assert record == {
93
+ "subject": "repo:issue:7",
94
+ "conversation_id": result["conversation_id"],
95
+ "delivery": "revision-1",
96
+ }
97
+
98
+
99
+ def test_known_subject_resumes_once_per_delivery(monkeypatch):
100
+ state = {
101
+ _state_key("repo:pr:9"): {
102
+ "subject": "repo:pr:9",
103
+ "conversation_id": "22222222-2222-4222-8222-222222222222",
104
+ "delivery": "head-1",
105
+ }
106
+ }
107
+ _fake_kv(monkeypatch, state)
108
+ monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
109
+ workspace = MagicMock()
110
+ workspace.__enter__.return_value = workspace
111
+ scoped_secrets = {
112
+ "GITHUB_TOKEN": LookupSecret(url="/api/settings/secrets/GITHUB_TOKEN")
113
+ }
114
+ workspace.get_secrets.return_value = scoped_secrets
115
+ monkeypatch.setattr(
116
+ agent_conversation, "RemoteWorkspace", lambda **kwargs: workspace
117
+ )
118
+ conversation = MagicMock()
119
+ conversation.state.execution_status = ConversationExecutionStatus.FINISHED
120
+ attach = MagicMock(return_value=conversation)
121
+ monkeypatch.setattr(agent_conversation.RemoteConversation, "attach", attach)
122
+ with _dispatcher(monkeypatch) as dispatcher:
123
+ duplicate = dispatcher.deliver("repo:pr:9", "head-1", "old")
124
+ resumed = dispatcher.deliver("repo:pr:9", "head-2", "new")
125
+
126
+ assert duplicate["disposition"] == "deduplicated"
127
+ assert resumed["disposition"] == "resumed"
128
+ conversation.update_secrets.assert_called_once_with(scoped_secrets)
129
+ conversation.send_message.assert_called_once_with("new")
130
+ conversation.run.assert_called_once_with(blocking=False)
131
+
132
+
133
+ def test_same_delivery_resumes_paused_conversation(monkeypatch):
134
+ state = {
135
+ _state_key("repo:pr:9"): {
136
+ "subject": "repo:pr:9",
137
+ "conversation_id": "22222222-2222-4222-8222-222222222222",
138
+ "delivery": "head-1",
139
+ }
140
+ }
141
+ _fake_kv(monkeypatch, state)
142
+ monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
143
+ workspace = MagicMock()
144
+ workspace.__enter__.return_value = workspace
145
+ workspace.get_secrets.return_value = {}
146
+ monkeypatch.setattr(
147
+ agent_conversation, "RemoteWorkspace", lambda **kwargs: workspace
148
+ )
149
+ conversation = MagicMock()
150
+ conversation.state.execution_status = ConversationExecutionStatus.PAUSED
151
+ monkeypatch.setattr(
152
+ agent_conversation.RemoteConversation,
153
+ "attach",
154
+ MagicMock(return_value=conversation),
155
+ )
156
+
157
+ with _dispatcher(monkeypatch) as dispatcher:
158
+ result = dispatcher.deliver("repo:pr:9", "head-1", "old")
159
+
160
+ assert result["disposition"] == "resumed"
161
+ conversation.send_message.assert_not_called()
162
+ conversation.run.assert_called_once_with(blocking=False)
163
+
164
+
165
+ def test_subjects_use_independent_kv_records(monkeypatch):
166
+ state = {}
167
+ _fake_kv(monkeypatch, state)
168
+ monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
169
+ workspace = MagicMock()
170
+ workspace.__enter__.return_value = workspace
171
+ workspace.get_secrets.return_value = {}
172
+ monkeypatch.setattr(
173
+ agent_conversation, "RemoteWorkspace", lambda **kwargs: workspace
174
+ )
175
+ missing = agent_conversation.httpx.HTTPStatusError(
176
+ "missing",
177
+ request=MagicMock(),
178
+ response=MagicMock(status_code=404),
179
+ )
180
+ monkeypatch.setattr(
181
+ agent_conversation.RemoteConversation, "attach", MagicMock(side_effect=missing)
182
+ )
183
+ monkeypatch.setattr(
184
+ agent_conversation.RemoteConversation,
185
+ "create",
186
+ MagicMock(side_effect=[MagicMock(), MagicMock()]),
187
+ )
188
+
189
+ with _dispatcher(monkeypatch) as dispatcher:
190
+ dispatcher.deliver("repo:issue:7", "revision-1", "first")
191
+ dispatcher.deliver("repo:issue:8", "revision-1", "second")
192
+
193
+ assert set(state) == {
194
+ _state_key("repo:issue:7"),
195
+ _state_key("repo:issue:8"),
196
+ }
@@ -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()