adispatchd 0.0.0-stage → 0.1.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 (396) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/api/errors.d.ts +27 -0
  4. package/dist/api/errors.js +80 -0
  5. package/dist/api/errors.js.map +1 -0
  6. package/dist/api/index.d.ts +10 -0
  7. package/dist/api/index.js +11 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/api/logs.d.ts +30 -0
  10. package/dist/api/logs.js +88 -0
  11. package/dist/api/logs.js.map +1 -0
  12. package/dist/api/serialize.d.ts +65 -0
  13. package/dist/api/serialize.js +135 -0
  14. package/dist/api/serialize.js.map +1 -0
  15. package/dist/api/server.d.ts +89 -0
  16. package/dist/api/server.js +751 -0
  17. package/dist/api/server.js.map +1 -0
  18. package/dist/api/sse.d.ts +90 -0
  19. package/dist/api/sse.js +225 -0
  20. package/dist/api/sse.js.map +1 -0
  21. package/dist/app.d.ts +173 -0
  22. package/dist/app.js +488 -0
  23. package/dist/app.js.map +1 -0
  24. package/dist/cli/config-validate.d.ts +6 -0
  25. package/dist/cli/config-validate.js +39 -0
  26. package/dist/cli/config-validate.js.map +1 -0
  27. package/dist/cli/control.d.ts +31 -0
  28. package/dist/cli/control.js +186 -0
  29. package/dist/cli/control.js.map +1 -0
  30. package/dist/cli/http-client.d.ts +57 -0
  31. package/dist/cli/http-client.js +174 -0
  32. package/dist/cli/http-client.js.map +1 -0
  33. package/dist/cli/index.d.ts +14 -0
  34. package/dist/cli/index.js +14 -0
  35. package/dist/cli/index.js.map +1 -0
  36. package/dist/cli/init-templates.d.ts +20 -0
  37. package/dist/cli/init-templates.js +275 -0
  38. package/dist/cli/init-templates.js.map +1 -0
  39. package/dist/cli/init.d.ts +16 -0
  40. package/dist/cli/init.js +107 -0
  41. package/dist/cli/init.js.map +1 -0
  42. package/dist/cli/main.d.ts +2 -0
  43. package/dist/cli/main.js +25 -0
  44. package/dist/cli/main.js.map +1 -0
  45. package/dist/cli/program.d.ts +102 -0
  46. package/dist/cli/program.js +133 -0
  47. package/dist/cli/program.js.map +1 -0
  48. package/dist/cli/service.d.ts +27 -0
  49. package/dist/cli/service.js +252 -0
  50. package/dist/cli/service.js.map +1 -0
  51. package/dist/cli/start.d.ts +22 -0
  52. package/dist/cli/start.js +133 -0
  53. package/dist/cli/start.js.map +1 -0
  54. package/dist/cli/workflow-dry-run.d.ts +13 -0
  55. package/dist/cli/workflow-dry-run.js +144 -0
  56. package/dist/cli/workflow-dry-run.js.map +1 -0
  57. package/dist/config/field-catalog.d.ts +33 -0
  58. package/dist/config/field-catalog.js +172 -0
  59. package/dist/config/field-catalog.js.map +1 -0
  60. package/dist/config/filter-schema.d.ts +15 -0
  61. package/dist/config/filter-schema.js +110 -0
  62. package/dist/config/filter-schema.js.map +1 -0
  63. package/dist/config/index.d.ts +15 -0
  64. package/dist/config/index.js +16 -0
  65. package/dist/config/index.js.map +1 -0
  66. package/dist/config/issues.d.ts +27 -0
  67. package/dist/config/issues.js +37 -0
  68. package/dist/config/issues.js.map +1 -0
  69. package/dist/config/loader.d.ts +32 -0
  70. package/dist/config/loader.js +250 -0
  71. package/dist/config/loader.js.map +1 -0
  72. package/dist/config/manager.d.ts +153 -0
  73. package/dist/config/manager.js +276 -0
  74. package/dist/config/manager.js.map +1 -0
  75. package/dist/config/model.d.ts +334 -0
  76. package/dist/config/model.js +2 -0
  77. package/dist/config/model.js.map +1 -0
  78. package/dist/config/normalize.d.ts +20 -0
  79. package/dist/config/normalize.js +200 -0
  80. package/dist/config/normalize.js.map +1 -0
  81. package/dist/config/parameters.d.ts +37 -0
  82. package/dist/config/parameters.js +71 -0
  83. package/dist/config/parameters.js.map +1 -0
  84. package/dist/config/public.d.ts +6 -0
  85. package/dist/config/public.js +18 -0
  86. package/dist/config/public.js.map +1 -0
  87. package/dist/config/schemas.d.ts +538 -0
  88. package/dist/config/schemas.js +321 -0
  89. package/dist/config/schemas.js.map +1 -0
  90. package/dist/config/snapshot.d.ts +38 -0
  91. package/dist/config/snapshot.js +57 -0
  92. package/dist/config/snapshot.js.map +1 -0
  93. package/dist/config/validate.d.ts +33 -0
  94. package/dist/config/validate.js +346 -0
  95. package/dist/config/validate.js.map +1 -0
  96. package/dist/core/clock.d.ts +59 -0
  97. package/dist/core/clock.js +133 -0
  98. package/dist/core/clock.js.map +1 -0
  99. package/dist/core/duration.d.ts +19 -0
  100. package/dist/core/duration.js +93 -0
  101. package/dist/core/duration.js.map +1 -0
  102. package/dist/core/errors.d.ts +62 -0
  103. package/dist/core/errors.js +72 -0
  104. package/dist/core/errors.js.map +1 -0
  105. package/dist/core/ids.d.ts +57 -0
  106. package/dist/core/ids.js +88 -0
  107. package/dist/core/ids.js.map +1 -0
  108. package/dist/core/index.d.ts +13 -0
  109. package/dist/core/index.js +14 -0
  110. package/dist/core/index.js.map +1 -0
  111. package/dist/core/logger.d.ts +100 -0
  112. package/dist/core/logger.js +207 -0
  113. package/dist/core/logger.js.map +1 -0
  114. package/dist/core/paths.d.ts +84 -0
  115. package/dist/core/paths.js +138 -0
  116. package/dist/core/paths.js.map +1 -0
  117. package/dist/core/template.d.ts +136 -0
  118. package/dist/core/template.js +261 -0
  119. package/dist/core/template.js.map +1 -0
  120. package/dist/core/types.d.ts +544 -0
  121. package/dist/core/types.js +172 -0
  122. package/dist/core/types.js.map +1 -0
  123. package/dist/core/worker-protocol.d.ts +102 -0
  124. package/dist/core/worker-protocol.js +75 -0
  125. package/dist/core/worker-protocol.js.map +1 -0
  126. package/dist/executors/classify.d.ts +25 -0
  127. package/dist/executors/classify.js +54 -0
  128. package/dist/executors/classify.js.map +1 -0
  129. package/dist/executors/dispatcher.d.ts +35 -0
  130. package/dist/executors/dispatcher.js +68 -0
  131. package/dist/executors/dispatcher.js.map +1 -0
  132. package/dist/executors/env.d.ts +43 -0
  133. package/dist/executors/env.js +66 -0
  134. package/dist/executors/env.js.map +1 -0
  135. package/dist/executors/event-parser.d.ts +41 -0
  136. package/dist/executors/event-parser.js +106 -0
  137. package/dist/executors/event-parser.js.map +1 -0
  138. package/dist/executors/index.d.ts +12 -0
  139. package/dist/executors/index.js +13 -0
  140. package/dist/executors/index.js.map +1 -0
  141. package/dist/executors/log-capture.d.ts +57 -0
  142. package/dist/executors/log-capture.js +155 -0
  143. package/dist/executors/log-capture.js.map +1 -0
  144. package/dist/executors/process-identity.d.ts +124 -0
  145. package/dist/executors/process-identity.js +283 -0
  146. package/dist/executors/process-identity.js.map +1 -0
  147. package/dist/executors/shell-executor.d.ts +35 -0
  148. package/dist/executors/shell-executor.js +478 -0
  149. package/dist/executors/shell-executor.js.map +1 -0
  150. package/dist/executors/stream-redactor.d.ts +40 -0
  151. package/dist/executors/stream-redactor.js +89 -0
  152. package/dist/executors/stream-redactor.js.map +1 -0
  153. package/dist/executors/workspace.d.ts +10 -0
  154. package/dist/executors/workspace.js +47 -0
  155. package/dist/executors/workspace.js.map +1 -0
  156. package/dist/filters/evaluate.d.ts +77 -0
  157. package/dist/filters/evaluate.js +187 -0
  158. package/dist/filters/evaluate.js.map +1 -0
  159. package/dist/filters/fields.d.ts +35 -0
  160. package/dist/filters/fields.js +75 -0
  161. package/dist/filters/fields.js.map +1 -0
  162. package/dist/filters/index.d.ts +11 -0
  163. package/dist/filters/index.js +12 -0
  164. package/dist/filters/index.js.map +1 -0
  165. package/dist/filters/iso-date.d.ts +10 -0
  166. package/dist/filters/iso-date.js +42 -0
  167. package/dist/filters/iso-date.js.map +1 -0
  168. package/dist/filters/predicate.d.ts +24 -0
  169. package/dist/filters/predicate.js +178 -0
  170. package/dist/filters/predicate.js.map +1 -0
  171. package/dist/filters/priority.d.ts +51 -0
  172. package/dist/filters/priority.js +91 -0
  173. package/dist/filters/priority.js.map +1 -0
  174. package/dist/filters/pushdown.d.ts +12 -0
  175. package/dist/filters/pushdown.js +52 -0
  176. package/dist/filters/pushdown.js.map +1 -0
  177. package/dist/filters/shell-check.d.ts +110 -0
  178. package/dist/filters/shell-check.js +304 -0
  179. package/dist/filters/shell-check.js.map +1 -0
  180. package/dist/identities/github-app.d.ts +55 -0
  181. package/dist/identities/github-app.js +160 -0
  182. package/dist/identities/github-app.js.map +1 -0
  183. package/dist/identities/index.d.ts +6 -0
  184. package/dist/identities/index.js +7 -0
  185. package/dist/identities/index.js.map +1 -0
  186. package/dist/identities/jwt.d.ts +24 -0
  187. package/dist/identities/jwt.js +56 -0
  188. package/dist/identities/jwt.js.map +1 -0
  189. package/dist/lifecycle/context.d.ts +106 -0
  190. package/dist/lifecycle/context.js +170 -0
  191. package/dist/lifecycle/context.js.map +1 -0
  192. package/dist/lifecycle/engine.d.ts +185 -0
  193. package/dist/lifecycle/engine.js +551 -0
  194. package/dist/lifecycle/engine.js.map +1 -0
  195. package/dist/lifecycle/errors.d.ts +48 -0
  196. package/dist/lifecycle/errors.js +71 -0
  197. package/dist/lifecycle/errors.js.map +1 -0
  198. package/dist/lifecycle/index.d.ts +9 -0
  199. package/dist/lifecycle/index.js +9 -0
  200. package/dist/lifecycle/index.js.map +1 -0
  201. package/dist/lifecycle/performers.d.ts +66 -0
  202. package/dist/lifecycle/performers.js +179 -0
  203. package/dist/lifecycle/performers.js.map +1 -0
  204. package/dist/notifications/index.d.ts +6 -0
  205. package/dist/notifications/index.js +7 -0
  206. package/dist/notifications/index.js.map +1 -0
  207. package/dist/notifications/service.d.ts +115 -0
  208. package/dist/notifications/service.js +364 -0
  209. package/dist/notifications/service.js.map +1 -0
  210. package/dist/persistence/actions.d.ts +101 -0
  211. package/dist/persistence/actions.js +196 -0
  212. package/dist/persistence/actions.js.map +1 -0
  213. package/dist/persistence/blocks.d.ts +85 -0
  214. package/dist/persistence/blocks.js +140 -0
  215. package/dist/persistence/blocks.js.map +1 -0
  216. package/dist/persistence/cron-firings.d.ts +55 -0
  217. package/dist/persistence/cron-firings.js +70 -0
  218. package/dist/persistence/cron-firings.js.map +1 -0
  219. package/dist/persistence/database.d.ts +60 -0
  220. package/dist/persistence/database.js +124 -0
  221. package/dist/persistence/database.js.map +1 -0
  222. package/dist/persistence/errors.d.ts +39 -0
  223. package/dist/persistence/errors.js +67 -0
  224. package/dist/persistence/errors.js.map +1 -0
  225. package/dist/persistence/events.d.ts +72 -0
  226. package/dist/persistence/events.js +83 -0
  227. package/dist/persistence/events.js.map +1 -0
  228. package/dist/persistence/index.d.ts +18 -0
  229. package/dist/persistence/index.js +19 -0
  230. package/dist/persistence/index.js.map +1 -0
  231. package/dist/persistence/instance-lock.d.ts +26 -0
  232. package/dist/persistence/instance-lock.js +117 -0
  233. package/dist/persistence/instance-lock.js.map +1 -0
  234. package/dist/persistence/lock-keys.d.ts +16 -0
  235. package/dist/persistence/lock-keys.js +41 -0
  236. package/dist/persistence/lock-keys.js.map +1 -0
  237. package/dist/persistence/notifications.d.ts +113 -0
  238. package/dist/persistence/notifications.js +213 -0
  239. package/dist/persistence/notifications.js.map +1 -0
  240. package/dist/persistence/persistence.d.ts +46 -0
  241. package/dist/persistence/persistence.js +42 -0
  242. package/dist/persistence/persistence.js.map +1 -0
  243. package/dist/persistence/retention.d.ts +46 -0
  244. package/dist/persistence/retention.js +68 -0
  245. package/dist/persistence/retention.js.map +1 -0
  246. package/dist/persistence/runs.d.ts +215 -0
  247. package/dist/persistence/runs.js +417 -0
  248. package/dist/persistence/runs.js.map +1 -0
  249. package/dist/persistence/schema.d.ts +16 -0
  250. package/dist/persistence/schema.js +183 -0
  251. package/dist/persistence/schema.js.map +1 -0
  252. package/dist/persistence/sqlite.d.ts +49 -0
  253. package/dist/persistence/sqlite.js +131 -0
  254. package/dist/persistence/sqlite.js.map +1 -0
  255. package/dist/runs/controller.d.ts +307 -0
  256. package/dist/runs/controller.js +1078 -0
  257. package/dist/runs/controller.js.map +1 -0
  258. package/dist/runs/errors.d.ts +20 -0
  259. package/dist/runs/errors.js +26 -0
  260. package/dist/runs/errors.js.map +1 -0
  261. package/dist/runs/event-bus.d.ts +106 -0
  262. package/dist/runs/event-bus.js +105 -0
  263. package/dist/runs/event-bus.js.map +1 -0
  264. package/dist/runs/execution.d.ts +39 -0
  265. package/dist/runs/execution.js +146 -0
  266. package/dist/runs/execution.js.map +1 -0
  267. package/dist/runs/index.d.ts +12 -0
  268. package/dist/runs/index.js +13 -0
  269. package/dist/runs/index.js.map +1 -0
  270. package/dist/runs/retention.d.ts +64 -0
  271. package/dist/runs/retention.js +232 -0
  272. package/dist/runs/retention.js.map +1 -0
  273. package/dist/runs/workspace.d.ts +20 -0
  274. package/dist/runs/workspace.js +51 -0
  275. package/dist/runs/workspace.js.map +1 -0
  276. package/dist/scheduler/checks.d.ts +41 -0
  277. package/dist/scheduler/checks.js +111 -0
  278. package/dist/scheduler/checks.js.map +1 -0
  279. package/dist/scheduler/cron.d.ts +32 -0
  280. package/dist/scheduler/cron.js +83 -0
  281. package/dist/scheduler/cron.js.map +1 -0
  282. package/dist/scheduler/dry-run.d.ts +42 -0
  283. package/dist/scheduler/dry-run.js +167 -0
  284. package/dist/scheduler/dry-run.js.map +1 -0
  285. package/dist/scheduler/errors.d.ts +22 -0
  286. package/dist/scheduler/errors.js +22 -0
  287. package/dist/scheduler/errors.js.map +1 -0
  288. package/dist/scheduler/format.d.ts +3 -0
  289. package/dist/scheduler/format.js +73 -0
  290. package/dist/scheduler/format.js.map +1 -0
  291. package/dist/scheduler/index.d.ts +13 -0
  292. package/dist/scheduler/index.js +14 -0
  293. package/dist/scheduler/index.js.map +1 -0
  294. package/dist/scheduler/planner.d.ts +88 -0
  295. package/dist/scheduler/planner.js +255 -0
  296. package/dist/scheduler/planner.js.map +1 -0
  297. package/dist/scheduler/scheduler.d.ts +160 -0
  298. package/dist/scheduler/scheduler.js +680 -0
  299. package/dist/scheduler/scheduler.js.map +1 -0
  300. package/dist/scheduler/state.d.ts +26 -0
  301. package/dist/scheduler/state.js +25 -0
  302. package/dist/scheduler/state.js.map +1 -0
  303. package/dist/scheduler/types.d.ts +197 -0
  304. package/dist/scheduler/types.js +3 -0
  305. package/dist/scheduler/types.js.map +1 -0
  306. package/dist/service/index.d.ts +6 -0
  307. package/dist/service/index.js +7 -0
  308. package/dist/service/index.js.map +1 -0
  309. package/dist/service/install.d.ts +93 -0
  310. package/dist/service/install.js +250 -0
  311. package/dist/service/install.js.map +1 -0
  312. package/dist/service/unit.d.ts +50 -0
  313. package/dist/service/unit.js +171 -0
  314. package/dist/service/unit.js.map +1 -0
  315. package/dist/sources/github/adapter.d.ts +99 -0
  316. package/dist/sources/github/adapter.js +336 -0
  317. package/dist/sources/github/adapter.js.map +1 -0
  318. package/dist/sources/github/fields.d.ts +8 -0
  319. package/dist/sources/github/fields.js +72 -0
  320. package/dist/sources/github/fields.js.map +1 -0
  321. package/dist/sources/github/http.d.ts +100 -0
  322. package/dist/sources/github/http.js +288 -0
  323. package/dist/sources/github/http.js.map +1 -0
  324. package/dist/sources/github/index.d.ts +6 -0
  325. package/dist/sources/github/index.js +7 -0
  326. package/dist/sources/github/index.js.map +1 -0
  327. package/dist/sources/github/pushdown.d.ts +14 -0
  328. package/dist/sources/github/pushdown.js +47 -0
  329. package/dist/sources/github/pushdown.js.map +1 -0
  330. package/dist/sources/github/refs.d.ts +17 -0
  331. package/dist/sources/github/refs.js +61 -0
  332. package/dist/sources/github/refs.js.map +1 -0
  333. package/dist/sources/index.d.ts +6 -0
  334. package/dist/sources/index.js +7 -0
  335. package/dist/sources/index.js.map +1 -0
  336. package/dist/ui/index.d.ts +12 -0
  337. package/dist/ui/index.js +28 -0
  338. package/dist/ui/index.js.map +1 -0
  339. package/dist/ui/public/app.css +239 -0
  340. package/dist/ui/public/favicon.svg +1 -0
  341. package/dist/ui/public/index.html +27 -0
  342. package/dist/ui/public/js/client/api.js +68 -0
  343. package/dist/ui/public/js/client/app.js +146 -0
  344. package/dist/ui/public/js/client/dom.js +34 -0
  345. package/dist/ui/public/js/client/page.js +9 -0
  346. package/dist/ui/public/js/client/pages/actions.js +35 -0
  347. package/dist/ui/public/js/client/pages/blocked.js +58 -0
  348. package/dist/ui/public/js/client/pages/config.js +126 -0
  349. package/dist/ui/public/js/client/pages/dashboard.js +175 -0
  350. package/dist/ui/public/js/client/pages/run.js +367 -0
  351. package/dist/ui/public/js/client/pages/runs.js +72 -0
  352. package/dist/ui/public/js/client/pages/views.js +80 -0
  353. package/dist/ui/public/js/client/pages/workflows.js +291 -0
  354. package/dist/ui/public/js/client/types.js +1 -0
  355. package/dist/ui/public/js/client/ui.js +152 -0
  356. package/dist/ui/public/js/shared/dom.js +77 -0
  357. package/dist/ui/public/js/shared/events.js +88 -0
  358. package/dist/ui/public/js/shared/format.js +213 -0
  359. package/dist/ui/public/js/shared/logs.js +136 -0
  360. package/dist/ui/public/js/shared/params.js +90 -0
  361. package/dist/ui/public/js/shared/route.js +36 -0
  362. package/dist/ui/public/js/shared/sort.js +70 -0
  363. package/dist/ui/public/js/shared/sse.js +193 -0
  364. package/dist/ui/public/js/shared/throttle.js +65 -0
  365. package/dist/views/index.d.ts +6 -0
  366. package/dist/views/index.js +7 -0
  367. package/dist/views/index.js.map +1 -0
  368. package/dist/views/service.d.ts +131 -0
  369. package/dist/views/service.js +264 -0
  370. package/dist/views/service.js.map +1 -0
  371. package/docs/api.md +239 -0
  372. package/docs/configuration-reference.md +1785 -0
  373. package/docs/cron-semantics.md +96 -0
  374. package/docs/manual-tests/github-fixture/config.yaml +30 -0
  375. package/docs/manual-tests/github-fixture/helpers.sh +93 -0
  376. package/docs/manual-tests/github-fixture/identities.yaml +18 -0
  377. package/docs/manual-tests/github-fixture/scripts/test-worker.sh +100 -0
  378. package/docs/manual-tests/github-fixture/sources.yaml +13 -0
  379. package/docs/manual-tests/github-fixture/views/dependencies.yaml +17 -0
  380. package/docs/manual-tests/github-fixture/views/needs-maintainer.yaml +17 -0
  381. package/docs/manual-tests/github-fixture/workflows/coder-test.yaml +76 -0
  382. package/docs/manual-tests/github-fixture/workflows/commenter-test.yaml +63 -0
  383. package/docs/manual-tests/github-fixture/workflows/reviewer-test.yaml +60 -0
  384. package/docs/manual-tests/github.md +1308 -0
  385. package/docs/manual-tests/process-supervision.md +64 -0
  386. package/docs/manual-tests/systemd.md +68 -0
  387. package/docs/operations.md +271 -0
  388. package/docs/worker-protocol.md +145 -0
  389. package/examples/config/config.yaml +35 -0
  390. package/examples/config/identities.yaml +18 -0
  391. package/examples/config/sources.yaml +13 -0
  392. package/examples/config/views/needs-maintainer.yaml +15 -0
  393. package/examples/config/workflows/coder-project-a.yaml +86 -0
  394. package/examples/config/workflows/deploy.yaml +15 -0
  395. package/examples/config/workflows/nightly-maintenance.yaml +13 -0
  396. package/package.json +75 -4
@@ -0,0 +1,1308 @@
1
+ # Manuelle Integrationstests gegen echtes GitHub (Spec §22.1 „GitHub“, §22.2, §24.1)
2
+
3
+ Automatisierte Tests laufen nur gegen `FakeGitHub` (`src/sources/github/testing/`). Diese Anleitung prüft dieselben
4
+ Annahmen Schritt für Schritt gegen die echte GitHub-API, mit eigenen Test-Apps und Wegwerf-Repos. Sie ist so geschrieben,
5
+ dass die Befehle der Reihe nach kopiert werden können. Zeitbedarf: Einrichtung etwa 45 Minuten, Testfälle etwa 2 Stunden.
6
+ Der natürliche Token-Ablauf (T2b) braucht mindestens 60 Minuten Laufzeit am Stück. Die anderen Testfälle laufen in
7
+ dieser Zeit nebenher.
8
+
9
+ Inhalt:
10
+ [0 Annahmen und Doku-Stand](#0-annahmen-und-doku-stand) ·
11
+ [1 Voraussetzungen und Testaufbau](#1-voraussetzungen-und-testaufbau) ·
12
+ [2 Testkonfiguration](#2-testkonfiguration) ·
13
+ [3 Testdaten](#3-testdaten) ·
14
+ [4 Testfälle](#4-testfälle) ·
15
+ [5 Auswertung und Aufräumen](#5-auswertung-und-aufräumen)
16
+
17
+ ---
18
+
19
+ ## 0 Annahmen und Doku-Stand
20
+
21
+ ### 0.1 Annahmen im Code (A1–A11)
22
+
23
+ | # | Annahme (im Code umgesetzt) | Stelle | Doku-Stand 2026-10-17 | Testfall |
24
+ |---|---|---|---|---|
25
+ | A1 | `X-GitHub-Api-Version: 2026-03-10` wird akzeptiert und enthält die Dependencies-API. | `GITHUB_API_VERSION` in `src/sources/github/http.ts` | **bestätigt**: neueste unterstützte Version ([R1](#r1)) | T1, T11 |
26
+ | A2 | `GET /repos/{o}/{r}/issues/{n}/dependencies/blocked_by` liefert ein Array von Issue-Objekten mit `state` und `repository_url`, paginiert über `Link` (`per_page` ≤ 100), Status 200/301/404/410. | `resolveDependencies` in `adapter.ts` | **bestätigt** ([R2](#r2)) | T11 |
27
+ | A3 | Cross-Repo-Blocker erscheinen in der Liste, **sofern** die App Zugriff auf das Blocker-Repo hat. | dito | nicht dokumentiert | T12 |
28
+ | A4 | Blocker in Repos ohne App-Zugriff fehlen in der Liste (oder führen zu 403/404). `issue_dependencies_summary.total_blocked_by` des frisch gelesenen Issues zählt sie mit. Ist der Wert größer als die sichtbare Liste oder fehlt das Summary → `dependencies.status = unknown` (fail-closed). | `resolveDependencies` | nicht dokumentiert ([R3](#r3), [R11](#r11)) | T13 |
29
+ | A5 | `issue_dependencies_summary.blocked_by` = Anzahl **offener** Blocker, `total_blocked_by` = alle. Genutzt wird nur `total_blocked_by`. | dito | Felder dokumentiert, Bedeutung **nicht** ([R3](#r3)) | T11, T14 |
30
+ | A6 | Fehlende Rechte am Dependencies-Endpunkt → 403 (`Resource not accessible by integration`) oder 404. Beides → `unknown`. | dito | Endpunkt verlangt *Issues: read* ([R2](#r2)) | T13, T19 |
31
+ | A7 | Benötigte App-Rechte: *Issues: read* (Lesen inkl. Dependencies), *Issues: write* (Labels/Kommentare an Issues), *Pull requests: write* (Labels/Kommentare an PRs über die Issues-API), *Metadata: read*. | Doku (`README.md`, `init`-Vorlage) | **teilweise anders**: Labels/Kommentare akzeptieren *Issues: write* **oder** *Pull requests: write*; die Dependencies-Endpunkte verlangen *Issues* read/write ([R4](#r4)) | T19 |
32
+ | A8 | Primäres Rate-Limit: 403/429 mit `x-ratelimit-remaining: 0` + `x-ratelimit-reset`. Sekundäres: 403/429 mit `retry-after` bzw. Meldung „rate limit“. | `GitHubHttpClient.toError` | **bestätigt** ([R8](#r8)) | T22 |
33
+ | A9 | Installation-Tokens laufen nach 1 h ab (`expires_at`). Ein JWT mit `iat = now-60 s`, `exp = now+9 min`, `iss = App-ID` wird akzeptiert. | `src/identities/` | **bestätigt**. Hinweis: GitHub *empfiehlt* inzwischen die Client-ID als `iss`, die App-ID bleibt zulässig ([R6](#r6)) | T1, T2 |
34
+ | A10 | Transferierte Issues leiten per 301 auf das neue Repo um. `repository_url` passt dann nicht → `getItem` liefert `null`. | `fetchIssue` | **bestätigt** (301, gelöscht 410, kein Zugriff 404) ([R9](#r9)) | T23 |
35
+ | A11 | Von einer App per Installation-Token erstellte Kommentare tragen `performed_via_github_app.id = <App-ID>` und `user.type = "Bot"`. Sonst findet `findCommentByMarker` eigene Kommentare nicht (Folge: Duplikat statt Unterdrückung). | `authorPredicate` in `adapter.ts` | Feld im Schema dokumentiert, `Bot`/`[bot]` **nicht** ([R7](#r7)) | T6, T7 |
36
+
37
+ ### 0.2 Rechercheergebnisse (offizielle GitHub-Doku, abgerufen 2026-10-17)
38
+
39
+ <a id="r1"></a>**R1 – API-Version.** Unterstützt sind `2026-03-10` (Supportende „Not yet scheduled“) und `2022-11-28`
40
+ (Supportende 10.03.2028). Ohne Header gilt `2022-11-28`, eine nicht mehr unterstützte Version gibt `410`.
41
+ Breaking Changes in `2026-03-10`, die den Orchestrator betreffen könnten:
42
+ - Das Feld `assignee` (Einzahl) ist aus Issues/PRs entfernt. Der Code liest nur `assignees` und ist nicht betroffen.
43
+ - `GET /rate_limit` hat kein `rate` mehr, stattdessen `resources.core`. Der Code nutzt den Endpunkt nicht, nur T22.
44
+ - `DELETE /app/installations/{id}` gibt 202. Der Code nutzt den Endpunkt nicht.
45
+ - Labels und Issue-Kommentare sind unverändert.
46
+
47
+ Quellen: [API Versions](https://docs.github.com/en/rest/about-the-rest-api/api-versions),
48
+ [Breaking changes](https://docs.github.com/en/rest/about-the-rest-api/breaking-changes).
49
+
50
+ <a id="r2"></a>**R2 – Issue-Dependencies-API.** Quelle: [Issue dependencies](https://docs.github.com/en/rest/issues/issue-dependencies),
51
+ [Creating issue dependencies](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies).
52
+
53
+ | Operation | Methode, Pfad | Status | Recht (Fine-grained/App) |
54
+ |---|---|---|---|
55
+ | Blocker auflisten | `GET /repos/{o}/{r}/issues/{issue_number}/dependencies/blocked_by` (`per_page` ≤ 100, `page`) | 200, 301, 404, 410 | *Issues: read* |
56
+ | Blocker hinzufügen | `POST …/dependencies/blocked_by`, Body `{"issue_id": <int>}` | 201, 301, 403, 404, 410, 422 | *Issues: write* |
57
+ | Blocker entfernen | `DELETE …/dependencies/blocked_by/{issue_id}` | **200** (nicht 204), 301, 400, 401, 403, 404, 410 | *Issues: write* |
58
+ | Blockierte auflisten | `GET …/dependencies/blocking` | 200, 301, 404, 410 | *Issues: read* |
59
+
60
+ - `issue_id` ist die **Datenbank-ID** des blockierenden Issues (`.id`), **nicht** die Nummer.
61
+ - Die Antwort ist ein Array vollständiger Issue-Objekte mit `repository_url` (Pflicht) und optional `repository`.
62
+ - *Pull requests* ist bei keinem der vier Endpunkte als alternatives Recht genannt.
63
+ - `gh` kann Abhängigkeiten selbst setzen (`gh issue edit N --add-blocked-by <nr|url>`, `--remove-blocked-by`;
64
+ `gh issue create --blocked-by`). Das wurde mit gh 2.101.0 geprüft.
65
+ - Laut [Changelog 2025-08-21](https://github.blog/changelog/2025-08-21-dependencies-on-issues) sind bis zu 50 Issues je
66
+ Beziehungstyp möglich.
67
+
68
+ <a id="r3"></a>**R3 – `issue_dependencies_summary`.** Das Feld ist Teil des Issue-Schemas (Get/Create/List) mit den
69
+ Pflicht-Integern `blocked_by`, `blocking`, `total_blocked_by` und `total_blocking`. Die Bedeutung ist **nicht
70
+ dokumentiert**. Dass `blocked_by` die offenen und `total_*` alle Blocker zählt, ist eine Annahme (A5, T11/T14).
71
+ Ebenfalls nicht dokumentiert ist, ob unsichtbare Blocker mitzählen (A4, T13).
72
+
73
+ <a id="r4"></a>**R4 – Rechte je Endpunkt** (Kästen „Fine-grained access tokens“, Übersicht
74
+ [Permissions required for GitHub Apps](https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps)):
75
+ - **Nur *Issues: read*:**
76
+ - `GET /repos/{o}/{r}/issues`. Die Liste enthält auch PRs, erkennbar an `pull_request`.
77
+ - `GET …/issues/{n}`.
78
+ - ***Issues: write* oder *Pull requests: write*** (eines von beiden genügt):
79
+ - `POST …/issues/{n}/labels`
80
+ - `DELETE …/labels/{name}`
81
+ - `POST …/issues/{n}/comments`
82
+ - `POST /repos/{o}/{r}/labels`
83
+ - ***Issues: read* oder *Pull requests: read*:** `GET …/issues/{n}/comments`.
84
+ - **Metadata:** *Metadata: read* setzt GitHub automatisch.
85
+
86
+ Die Doku sagt **nicht**, welches der beiden Rechte bei einem PR greift. Deshalb behält diese Anleitung für PR-schreibende
87
+ Apps vorsichtshalber beide Rechte und prüft die Frage in T19d. Die fehlende Berechtigung nennt der Antwort-Header
88
+ `X-Accepted-GitHub-Permissions`. Quellen: [Issues](https://docs.github.com/en/rest/issues/issues),
89
+ [Labels](https://docs.github.com/en/rest/issues/labels), [Comments](https://docs.github.com/en/rest/issues/comments).
90
+
91
+ <a id="r5"></a>**R5 – App anlegen und installieren.**
92
+ - **Anlegen:** [Registering a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)
93
+ (Webhook „Active“ abwählen, „Only on this account“).
94
+ - **Schlüssel:** [Managing private keys](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps).
95
+ Der PEM ist im Format PKCS#1 (`BEGIN RSA PRIVATE KEY`), läuft nicht ab, und je App sind bis zu 25 Schlüssel möglich.
96
+ - **Installieren:** [Installing your own GitHub App](https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app),
97
+ mit „Only select repositories“.
98
+ - **Installation-ID:** laut [Generating an installation access token](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app)
99
+ über das JWT mit `GET /repos/{o}/{r}/installation`, `GET /users/{u}/installation`, `GET /orgs/{org}/installation` oder
100
+ `GET /app/installations`. Die URL `…/settings/installations/<ID>` funktioniert, ist aber nicht offiziell dokumentiert.
101
+
102
+ <a id="r6"></a>**R6 – Installation-Token und JWT.**
103
+ - **Token:** `POST /app/installations/{id}/access_tokens` → `201 {token, expires_at, permissions, repository_selection}`.
104
+ Das Token läuft nach einer Stunde ab, danach kommt `401`. Mit `repositories`/`permissions` im Body lässt sich ein Token
105
+ weiter einschränken, aber nie erweitern.
106
+ - **Neues Token-Format:** Seit dem 27.04.2026 stellt GitHub auf das zustandslose Format `ghs_<APPID>_<JWT>` um (länger als
107
+ 40 Zeichen). Der Code setzt keine Länge voraus.
108
+ - **JWT:**
109
+ - Algorithmus RS256.
110
+ - `iat` 60 s in der Vergangenheit empfohlen.
111
+ - `exp` höchstens 10 min in der Zukunft.
112
+ - `iss` darf Client-ID **oder** App-ID sein; die Client-ID wird empfohlen.
113
+
114
+ Quellen: [Create an installation access token](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app),
115
+ [Generating a JWT](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app).
116
+
117
+ <a id="r7"></a>**R7 – Kommentare einer App.** Das Kommentar-Schema enthält `performed_via_github_app` (`null` oder ein
118
+ App-Objekt mit `id`, `slug`, `client_id` …) und `user.type`. Laut Doku werden Requests einer Installation „der App
119
+ zugeschrieben“
120
+ ([Authenticating as an installation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation)).
121
+ `user.type = "Bot"` und der Login `<slug>[bot]` sind **nicht dokumentiert** und werden in T6 gemessen.
122
+
123
+ <a id="r8"></a>**R8 – Rate-Limits** ([Rate limits for the REST API](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api),
124
+ [Best practices](https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api)).
125
+ - **Primäres Limit je Installation:** mindestens 5 000/h. Mit mehr Repos/Nutzern skaliert es bis 12 500/h, unter
126
+ Enterprise Cloud sind es 15 000/h.
127
+ - **Header:** `x-ratelimit-limit|remaining|used|reset|resource`. `GET /rate_limit` zählt nicht zum primären Limit.
128
+ - **Sekundäre Limits:**
129
+ - 100 parallele Requests.
130
+ - 900 Punkte/min je Endpunkt (GET = 1 Punkt, Schreiben = 5).
131
+ - 80 content-erzeugende Requests/min bzw. 500/h.
132
+ - **Antwort bei Überschreitung:** `403` **oder** `429`. Mit `retry-after` gilt dessen Wartezeit, bei `remaining: 0` die
133
+ bis `reset`, sonst mindestens 1 min mit Backoff.
134
+ - **Empfehlung:** Schreibende Requests seriell senden, mit mindestens 1 s Abstand. Der Code drosselt das nicht, siehe
135
+ [5.2](#52-bei-abweichungen).
136
+
137
+ <a id="r9"></a>**R9 – Transfer/Löschung** ([Get an issue](https://docs.github.com/en/rest/issues/issues#get-an-issue)):
138
+ - Transferiert in ein lesbares Repo → `301`.
139
+ - Transferiert in ein nicht lesbares Repo oder dort gelöscht → `404`.
140
+ - In einem lesbaren Repo gelöscht → `410`.
141
+
142
+ <a id="r11"></a>**R11 – Cross-Repo-Abhängigkeiten.** Es gibt keine dokumentierte Einschränkung (gleicher Owner o. Ä.).
143
+ Was ein Token sieht, wenn ihm das Blocker-Repo fehlt, ist nicht dokumentiert. Das prüft T13.
144
+
145
+ **Abweichungen zwischen Code-Annahmen und Doku:**
146
+ - Es gibt **keine** falsche Konstante und keinen falschen Endpunkt oder Feldnamen. Code wurde nicht geändert.
147
+ - **A7:** Die Aussage „Pull requests: write ist für Labels/Kommentare an PRs nötig“ deckt die Doku nicht. Laut Doku genügt
148
+ eines der beiden Rechte. Prüfen in T19d und danach die Rechte-Hinweise in `README.md` und `src/cli/init-templates.ts`
149
+ anpassen.
150
+ - **A7 (Dependencies):** Die Dependencies-Endpunkte nennen jetzt explizit *Issues: read/write*. Die alte Aussage „Doku
151
+ nennt keine Rechte“ ist überholt.
152
+ - **A9:** `iss` = App-ID ist weiterhin zulässig. Empfohlen ist die Client-ID; ein Umstieg ist optional (Konfigfeld fehlt).
153
+
154
+ ---
155
+
156
+ ## 1 Voraussetzungen und Testaufbau
157
+
158
+ ### 1.1 Werkzeuge
159
+
160
+ - Repo-Checkout mit `npm ci`, Node ≥ 22.13. Alle Befehle laufen im **Repo-Root**.
161
+ - `gh` ≥ 2.80 (getestet mit 2.101.0), angemeldet als Mensch mit Admin-Rechten auf die Test-Repos (`gh auth status`).
162
+ Zum Aufräumen zusätzlich `gh auth refresh -s delete_repo`.
163
+ - `curl`, `jq`, `openssl`, eine bash- oder zsh-Shell.
164
+ - Ein **eigener** GitHub-Account oder eine Test-Org. Keine produktiven Repos und keine produktiven Apps verwenden.
165
+
166
+ ### 1.2 Repos
167
+
168
+ | Repo | Sichtbarkeit | Apps installiert | Zweck |
169
+ |---|---|---|---|
170
+ | `orch-test-a` | privat | maintainer, coder, reviewer | Source `test_a`: Coder-/Reviewer-Workflows, Dependencies |
171
+ | `orch-test-b` | privat | maintainer, coder, reviewer | Source `test_b`: Kommentar-Workflow, Cross-Repo-Blocker **mit** Zugriff |
172
+ | `orch-test-hidden` | privat | **keine** | Cross-Repo-Blocker **ohne** App-Zugriff (A4) |
173
+
174
+ ```bash
175
+ export ADISPATCH_TEST_OWNER=<dein-account-oder-org>
176
+ for r in orch-test-a orch-test-b orch-test-hidden; do
177
+ gh repo create "$ADISPATCH_TEST_OWNER/$r" --private --add-readme --description "adispatchd manual test (Wegwerf)"
178
+ done
179
+ ```
180
+
181
+ `orch-test-hidden` muss **privat** sein, sonst kann die App es über die öffentliche API trotzdem lesen.
182
+
183
+ ### 1.3 Apps
184
+
185
+ Empfohlen sind **drei** GitHub Apps, eine je Rolle wie in Spec §5.3. Nur so lässt sich prüfen, dass
186
+ - Kommentare der falschen App den Marker nicht „besitzen“ (A11, T6c),
187
+ - Action-Identitäten die Workflow-Identität überschreiben (T7),
188
+ - eine Rolle ohne Schreibrechte wirklich nicht schreiben kann (T19).
189
+
190
+ | App (Vorschlag Name) | Identity | Repository permissions (minimal) | Wofür |
191
+ |---|---|---|---|
192
+ | `orch-test-maintainer-<owner>` | `maintainer` | **Issues: Read-only**, Metadata: Read-only (automatisch) | Source-Identity: Polling, Dependencies, Views, Item-Lesen für Hooks und manuelle Starts |
193
+ | `orch-test-coder-<owner>` | `coder` | **Issues: Read and write**, **Pull requests: Read and write**, Metadata: Read-only | Labels und Kommentare in `coder-test` und `commenter-test`, `GH_TOKEN` des Workers. Liest selbst: Neu-Lesen des Items vor jedem Label-Update und die Marker-Suche nutzen die **schreibende** Identity (`applyUpdate`, `findCommentByMarker`) |
194
+ | `orch-test-reviewer-<owner>` | `reviewer` | **Issues: Read and write**, **Pull requests: Read and write**, Metadata: Read-only | Labels/Kommentare an PRs (`reviewer-test`), Start-Kommentar in `commenter-test`; liest wie coder selbst (Neu-Lesen, Marker-Suche) |
195
+
196
+ Schreibende Apps brauchen also immer auch Lesezugriff auf Issues (in „Read and write“ enthalten). *Issues* darf bei
197
+ coder/reviewer nie auf „No access“ stehen, sonst scheitert schon das Neu-Lesen vor dem Update.
198
+
199
+ Alles andere bleibt auf „No access“: Contents, Workflows, Administration, Account permissions, Organization
200
+ permissions. Ein echter Coder bräuchte zusätzlich *Contents: write* für Push und PR. Das ist hier nicht nötig, weil der
201
+ Test-Worker nichts pusht.
202
+
203
+ *Pull requests* ist bei coder und reviewer bewusst vorhanden, obwohl die Doku *Issues: write* für Labels/Kommentare an
204
+ PRs vermutlich genügen lässt ([R4](#r4)). T19d prüft genau das. Fällt der Test positiv aus, kann *Pull requests* später
205
+ entfallen.
206
+
207
+ **Minimalvariante mit zwei Apps** (weniger Aufwand, eingeschränkte Aussagekraft):
208
+ - Die App `maintainer` bleibt wie oben.
209
+ - Eine App `writer` übernimmt **coder und reviewer**: In `identities.yaml` zeigen beide Einträge auf dieselbe App-ID und
210
+ Installation-ID und dieselbe Key-Variable.
211
+ - Dann entfallen T6c (fremde App mit gleichem Marker) und der Identitätswechsel in T7. Alle anderen Fälle laufen
212
+ unverändert.
213
+
214
+ Eine einzige App für alles ist **nicht** sinnvoll: Dann lässt sich weder die Read-only-Source-Identität (T19a) noch eine
215
+ Rollentrennung prüfen.
216
+
217
+ **Anlegen** (je App, [R5](#r5)):
218
+
219
+ 1. *Settings → Developer settings → GitHub Apps → New GitHub App* (bei einer Org:
220
+ *Org Settings → Developer settings → GitHub Apps*).
221
+ 2. **GitHub App name** wie oben (≤ 34 Zeichen, global eindeutig). **Homepage URL**: z. B.
222
+ `https://github.com/<owner>/orch-test-a`.
223
+ 3. **Webhook → Active abwählen** (der Orchestrator pollt).
224
+ 4. **Permissions → Repository permissions** gemäß Tabelle setzen.
225
+ 5. **Where can this GitHub App be installed? → Only on this account.** Dann *Create GitHub App*.
226
+ 6. Auf der Seite *General*: **App ID** notieren (und die Client ID, nur zur Information).
227
+ 7. *Private keys → Generate a private key*. Der Browser lädt `<name>.<datum>.private-key.pem` herunter. Die Datei nach
228
+ `~/orch-test-keys/` verschieben, `chmod 600`. Sie **nie** ins Repo legen.
229
+ 8. *Install App → Install → Only select repositories → `orch-test-a`, `orch-test-b` → Install*.
230
+ **Nicht** `orch-test-hidden`.
231
+ 9. Installation-ID notieren: Sie steht in der URL nach der Installation (`…/settings/installations/<ID>`). Zuverlässiger
232
+ ist der Helfer aus 2.3: `installation_id <app-id> <pem> orch-test-a`.
233
+
234
+ ### 1.4 Private Keys als Umgebungsvariablen
235
+
236
+ Der Orchestrator liest den PEM aus der Variablen, die `private_key_env` nennt (`normalizePrivateKeyPem` in
237
+ `src/identities/jwt.ts`). Erlaubt sind echte Zeilenumbrüche, `\n`-Escapes, CRLF und umschließende Quotes. Beide Formen
238
+ werden getestet:
239
+
240
+ ```bash
241
+ # Form 1: echte Zeilenumbrüche (direkt in der Shell)
242
+ export ADISPATCH_TEST_MAINTAINER_KEY="$(cat ~/orch-test-keys/orch-test-maintainer-*.private-key.pem)"
243
+ export ADISPATCH_TEST_REVIEWER_KEY="$(cat ~/orch-test-keys/orch-test-reviewer-*.private-key.pem)"
244
+
245
+ # Form 2: einzeilig mit literalen \n (wie in einer systemd-EnvironmentFile oder .env)
246
+ export ADISPATCH_TEST_CODER_KEY="$(awk 'BEGIN{ORS="\\n"}1' ~/orch-test-keys/orch-test-coder-*.private-key.pem)"
247
+ printf '%s' "$ADISPATCH_TEST_CODER_KEY" | head -c 40; echo # → -----BEGIN RSA PRIVATE KEY-----\nMIIE…
248
+ ```
249
+
250
+ Variante als Datei, die nur gesourct und nie eingecheckt wird (`/tmp/` im Repo ist in `.gitignore`):
251
+
252
+ ```bash
253
+ mkdir -p tmp && umask 077
254
+ {
255
+ printf 'ADISPATCH_TEST_MAINTAINER_KEY="%s"\n' "$(awk 'BEGIN{ORS="\\n"}1' ~/orch-test-keys/orch-test-maintainer-*.pem)"
256
+ printf 'ADISPATCH_TEST_CODER_KEY="%s"\n' "$(awk 'BEGIN{ORS="\\n"}1' ~/orch-test-keys/orch-test-coder-*.pem)"
257
+ printf 'ADISPATCH_TEST_REVIEWER_KEY="%s"\n' "$(awk 'BEGIN{ORS="\\n"}1' ~/orch-test-keys/orch-test-reviewer-*.pem)"
258
+ } > tmp/orch-test.env
259
+ set -a; . tmp/orch-test.env; set +a
260
+ ```
261
+
262
+ ### 1.5 App-ID und Installation-ID prüfen
263
+
264
+ ```bash
265
+ source docs/manual-tests/github-fixture/helpers.sh
266
+ export MAINTAINER_APP_ID=<id> CODER_APP_ID=<id> REVIEWER_APP_ID=<id>
267
+ K=~/orch-test-keys
268
+ installation_id "$MAINTAINER_APP_ID" $K/orch-test-maintainer-*.pem orch-test-a
269
+ installation_id "$CODER_APP_ID" $K/orch-test-coder-*.pem orch-test-a
270
+ installation_id "$REVIEWER_APP_ID" $K/orch-test-reviewer-*.pem orch-test-a
271
+ # → {"id": <Installation-ID>, "app_id": …, "app_slug": "…", "repository_selection": "selected", "permissions": {…}}
272
+ export MAINTAINER_INSTALLATION_ID=<id> CODER_INSTALLATION_ID=<id> REVIEWER_INSTALLATION_ID=<id>
273
+ # Gegenprobe: das versteckte Repo hat KEINE Installation (erwartet: 404 "Not Found")
274
+ installation_id "$MAINTAINER_APP_ID" $K/orch-test-maintainer-*.pem orch-test-hidden
275
+ # Slugs für T6 (Bot-Login = <slug>[bot]):
276
+ app_info() { curl -sS -H "Authorization: Bearer $(app_jwt "$1" "$2")" -H 'Accept: application/vnd.github+json' \
277
+ https://api.github.com/app | jq -c '{id, slug, client_id, permissions}'; }
278
+ app_info "$MAINTAINER_APP_ID" $K/orch-test-maintainer-*.pem
279
+ app_info "$CODER_APP_ID" $K/orch-test-coder-*.pem
280
+ app_info "$REVIEWER_APP_ID" $K/orch-test-reviewer-*.pem
281
+ ```
282
+
283
+ `permissions` muss der Tabelle in 1.3 entsprechen (`issues: read` bzw. `write`, `pull_requests`, `metadata: read`).
284
+
285
+ ---
286
+
287
+ ## 2 Testkonfiguration
288
+
289
+ ### 2.1 Inhalt des Fixtures
290
+
291
+ Das komplette config-dir liegt unter [`github-fixture/`](github-fixture/). Der Test
292
+ `src/sources/github/manual-fixture.test.ts` prüft bei jedem `npm run check`:
293
+ - Das Fixture lädt mit dem echten Loader ohne Fehler und Warnungen.
294
+ - Der Test-Worker hält das Worker-Protokoll ein.
295
+ - Worker und Helfer pinnen dieselbe API-Version wie `GITHUB_API_VERSION`.
296
+
297
+ | Datei | Inhalt |
298
+ |---|---|
299
+ | `config.yaml` | Port **8791**, Polling **20 s**, global `max_concurrent: 4`, Action-Retry 3 × **20 s**, Workspaces behalten |
300
+ | `sources.yaml` | `test_a` → `orch-test-a`, `test_b` → `orch-test-b`, Lese-Identity `maintainer`; Owner-Platzhalter `ADISPATCH_TEST_OWNER` |
301
+ | `identities.yaml` | `maintainer`, `coder`, `reviewer`; Platzhalter-IDs `900000x` (App) / `910000x` (Installation); Keys aus `ADISPATCH_TEST_{MAINTAINER,CODER,REVIEWER}_KEY` |
302
+ | `workflows/coder-test.yaml` | Source `test_a`, Identity `coder`, Filter `labels contains ready-for-coding` **und** `dependencies.blocked_by_open = 0`, `max_concurrent: 2`, `max_technical_failures: 3`, Label-Hooks (`coding` → `ready-for-review` / `needs-maintainer` / zurück zu `ready-for-coding`), Kommentar bei Erfolg und `failure`, `GH_TOKEN` für den Worker |
303
+ | `workflows/reviewer-test.yaml` | Source `test_a`, Identity `reviewer`, nur **PRs** mit `ready-for-review` und ohne offene Blocker, Kommentar bei Erfolg |
304
+ | `workflows/commenter-test.yaml` | Source `test_b`, Label `ready-for-comment`. `on_start` kommentiert als **reviewer** (Action-Identity), `on_success` als **coder** mit `retry: {max_attempts: 6, delay: 20s}` (Szenario 6) |
305
+ | `views/needs-maintainer.yaml` | Szenario 10: beide Repos, Label `needs-maintainer`, letzter Run und Ergebnis, Link |
306
+ | `views/dependencies.yaml` | Beobachtung: alle Items mit Label `deps-test` und ihre `dependencies.*`-Felder (startet nichts) |
307
+ | `scripts/test-worker.sh` | Test-Worker; das Verhalten steuern Labels (siehe unten) |
308
+ | `helpers.sh` | Shell-Helfer für diese Anleitung (wird **nicht** vom Orchestrator ausgeführt) |
309
+
310
+ Labels für den Test-Worker (kombinierbar):
311
+
312
+ | Label | Verhalten | Erwarteter Run |
313
+ |---|---|---|
314
+ | (kein `test:*`) | Ergebnis `success`, Exit 0 | `completed` / `success` → `on_success` |
315
+ | `test:slow` | schläft `TEST_SLOW_SECONDS` (coder 90 s, sonst 60 s), alle 10 s ein `progress`-Event | wie ohne, nur später |
316
+ | `test:failure` | Ergebnis `failure`, Exit 0 | `completed` / `failure` → `on_failure`, kein Retry-Zähler |
317
+ | `test:crash` | schreibt `success`, dann Exit 1 | `crashed` (Datei ignoriert) → `on_crash`, zählt |
318
+ | `test:invalid-result` | ungültige Ergebnisdatei, Exit 0 | `crashed` (Protokollfehler) |
319
+ | `test:token` | prüft `GH_TOKEN` per `GET /repos/…/issues/<n>` | bei HTTP ≠ 200 `crashed` |
320
+ | `test:revoke-token` | widerruft `GH_TOKEN` per `DELETE /installation/token` | Erfolg; der Orchestrator muss das Coder-Token erneuern (T2a) |
321
+
322
+ ### 2.2 Fixture kopieren und IDs eintragen
323
+
324
+ ```bash
325
+ source docs/manual-tests/github-fixture/helpers.sh # setzt ADISPATCH_CFG=tmp/orch-cfg, ADISPATCH_DATA=tmp/orch-data, API=…:8791
326
+ rm -rf "$ADISPATCH_CFG" && mkdir -p "$ADISPATCH_CFG" "$ADISPATCH_DATA"
327
+ cp -Rp docs/manual-tests/github-fixture/. "$ADISPATCH_CFG"/
328
+ perl -pi -e "s/ADISPATCH_TEST_OWNER/$ADISPATCH_TEST_OWNER/g" "$ADISPATCH_CFG/sources.yaml"
329
+ perl -pi -e "s/9000001/$MAINTAINER_APP_ID/; s/9100001/$MAINTAINER_INSTALLATION_ID/;
330
+ s/9000002/$CODER_APP_ID/; s/9100002/$CODER_INSTALLATION_ID/;
331
+ s/9000003/$REVIEWER_APP_ID/; s/9100003/$REVIEWER_INSTALLATION_ID/" "$ADISPATCH_CFG/identities.yaml"
332
+ grep -nE 'owner|app_id|installation_id' "$ADISPATCH_CFG"/sources.yaml "$ADISPATCH_CFG"/identities.yaml
333
+ ls -l "$ADISPATCH_CFG/scripts/test-worker.sh" # muss ausführbar sein (rwx)
334
+ orch config validate --config-dir "$ADISPATCH_CFG"
335
+ # → Configuration in …/tmp/orch-cfg is valid: 3 workflow(s), 2 view(s), 2 source(s), 3 identities. Hash …
336
+ ```
337
+
338
+ Minimalvariante: In `identities.yaml` bei `reviewer` dieselben Werte wie bei `coder` eintragen und
339
+ `private_key_env: ADISPATCH_TEST_CODER_KEY` setzen.
340
+
341
+ ### 2.3 Helfer
342
+
343
+ `source docs/manual-tests/github-fixture/helpers.sh` stellt diese Helfer bereit. `probe` und `ghraw` holen sich eigene
344
+ Tokens; die Key-Variablen aus 1.4 müssen deshalb auch in der Test-Shell gesetzt sein.
345
+
346
+ | Helfer | Wirkung |
347
+ |---|---|
348
+ | `orch …` | CLI aus dem Checkout |
349
+ | `api /pfad`, `post /pfad '{json}'` | Orchestrator-API; `post` setzt den CSRF-Header |
350
+ | `runs <wf>` | Runs als Tabelle |
351
+ | `run_of <source> <nr>` | neueste Run-ID eines Items |
352
+ | `diag <wf>` | letzte Planungsdiagnose (Kandidaten + Entscheidung) |
353
+ | `actions <run>` | Action-Journal |
354
+ | `events <run>` | Run-Events |
355
+ | `view <id>` | View-Zeilen, `?` = unbekannt |
356
+ | `mkissue <repo> <titel> [label…]` | Issue anlegen, gibt die Nummer aus |
357
+ | `blockedby <repo> <nr> <blocker-repo> <nr>` | Abhängigkeit setzen (über gh) |
358
+ | `blockedby_api` / `unblock_api` | Abhängigkeit setzen bzw. entfernen über REST (`issue_id`) |
359
+ | `labels <repo> <nr>` | Labels eines Issues |
360
+ | `comments <repo> <nr>` | Kommentare als Tabelle: id, Login, `user.type`, `performed_via_github_app.id`, letzte Zeile |
361
+ | `probe …` | Adapter direkt aufrufen (2.4) |
362
+ | `ghraw <identity> <pfad> [curl-Optionen]` | Rohantwort der API **als App** (frisches Installation-Token) |
363
+ | `app_jwt <app-id> <pem>`, `installation_id <app-id> <pem> <repo>` | App-JWT bzw. Installation einer App ermitteln |
364
+
365
+ ### 2.4 Probe-Skript (Adapter direkt)
366
+
367
+ Für Rohbeobachtungen ohne laufende Instanz. Die Datei unter `tmp/probe.ts` anlegen; das Verzeichnis ist in `.gitignore`.
368
+ Sie nutzt dieselbe Verdrahtung wie `start`/`dry-run` (`SourceRegistry` aus `src/app.ts`). Jeder Aufruf holt **eigene**
369
+ Tokens und teilt keinen Cache mit der laufenden Instanz.
370
+
371
+ ```ts
372
+ // tmp/probe.ts – direkter Zugriff auf Adapter/Credentials mit der Test-Konfiguration (nicht einchecken; /tmp/ ist ignoriert).
373
+ // Aufruf: npx tsx tmp/probe.ts <befehl> ... (config-dir aus $ADISPATCH_CFG, Logs auf stderr, Ergebnis als JSON auf stdout)
374
+ import { loadConfig } from '../src/config/index.js';
375
+ import { ConsoleLogger, SecretRedactor, systemClock } from '../src/core/index.js';
376
+ import { SourceRegistry } from '../src/app.js';
377
+ import { DEPENDENCY_FIELDS } from '../src/sources/index.js';
378
+
379
+ const [cmd, ...args] = process.argv.slice(2);
380
+ const loaded = await loadConfig(process.env.ADISPATCH_CFG ?? 'tmp/orch-cfg', { dataDir: process.env.ADISPATCH_DATA ?? 'tmp/orch-data' });
381
+ if (!loaded.ok) throw new Error(`config invalid: ${JSON.stringify(loaded.errors)}`);
382
+ const redactor = new SecretRedactor();
383
+ const logger = new ConsoleLogger({ level: 'debug', redact: redactor.asRedactor });
384
+ const registry = new SourceRegistry(loaded.config, { redactor, logger });
385
+ const out = (v: unknown): void => void process.stdout.write(`${JSON.stringify(v, null, 2)}\n`);
386
+ const adapter = (id: string) => registry.get(id) ?? (() => { throw new Error(`unknown source ${id}`); })();
387
+ const item = async (source: string, ref: string) => {
388
+ const a = adapter(source);
389
+ const id = a.parseItemRef(ref);
390
+ const it = id ? await a.getItem(id) : null;
391
+ if (!it) throw new Error(`item ${ref} not found in ${source}`);
392
+ return { a, it };
393
+ };
394
+
395
+ switch (cmd) {
396
+ case 'token': { // token <identity> → nur das Token (für curl); 1 h gültig
397
+ const t = await registry.getToken(args[0]!);
398
+ process.stderr.write(`expires_at ${t.expiresAt.toISOString()}\n`);
399
+ process.stdout.write(`${t.token}\n`);
400
+ break;
401
+ }
402
+ case 'list': { // list <source> [label]
403
+ const filter = args[1] ? { field: 'labels', operator: 'contains' as const, value: args[1] } : undefined;
404
+ const items = await adapter(args[0]!).listCandidates(filter ? { filter } : {});
405
+ out(items.map((i) => ({ key: i.key, type: i.type, state: i.fields.state, labels: i.fields.labels, title: i.fields.title })));
406
+ break;
407
+ }
408
+ case 'item': { // item <source> <ref>
409
+ out((await item(args[0]!, args[1]!)).it);
410
+ break;
411
+ }
412
+ case 'deps': { // deps <source> <ref>
413
+ const { a, it } = await item(args[0]!, args[1]!);
414
+ out(await a.resolveFields(it, DEPENDENCY_FIELDS));
415
+ break;
416
+ }
417
+ case 'labels': { // labels <source> <ref> <identity> +add -remove ...
418
+ const { a, it } = await item(args[0]!, args[1]!);
419
+ const add = args.slice(3).filter((l) => l.startsWith('+')).map((l) => l.slice(1));
420
+ const remove = args.slice(3).filter((l) => l.startsWith('-')).map((l) => l.slice(1));
421
+ out(await a.applyUpdate(it, { labels: { add, remove } }, args[2]!));
422
+ break;
423
+ }
424
+ case 'comment': { // comment <source> <ref> <identity> <marker> <text>
425
+ const { a, it } = await item(args[0]!, args[1]!);
426
+ out(await a.postComment(it, args[4] ?? 'probe', args[3]!, args[2]!));
427
+ break;
428
+ }
429
+ case 'find': { // find <source> <ref> <identity> <marker>
430
+ const { a, it } = await item(args[0]!, args[1]!);
431
+ out(await a.findCommentByMarker(it, args[3]!, args[2]!));
432
+ break;
433
+ }
434
+ default:
435
+ process.stderr.write('usage: probe.ts token|list|item|deps|labels|comment|find ...\n');
436
+ process.exitCode = 2;
437
+ }
438
+ ```
439
+
440
+ Der Code wurde am 2026-10-17 gegen diesen Stand mit `tsc` geprüft. Bricht er nach Refactorings, die Importe an die
441
+ `index.ts`-Exporte anpassen.
442
+
443
+ ### 2.5 Instanz starten
444
+
445
+ In einem **eigenen Terminal**, in dem die Key-Variablen gesetzt sind:
446
+
447
+ ```bash
448
+ source docs/manual-tests/github-fixture/helpers.sh
449
+ orch start --config-dir "$ADISPATCH_CFG" --data-dir "$ADISPATCH_DATA" 2>&1 | tee -a tmp/orch.log
450
+ # → URL http://127.0.0.1:8791, Stop-Timeouts; Logs (JSON-Zeilen) auf stderr → tmp/orch.log
451
+ ```
452
+
453
+ Weboberfläche: <http://127.0.0.1:8791>. Bei Bedarf stoppen und neu starten (`Ctrl-C` = Drain, zweimal = Cancel-All).
454
+ Die Daten in `$ADISPATCH_DATA` bleiben erhalten.
455
+
456
+ **Während des Testdaten-Setups** die automatischen Starts anhalten, damit kein Run startet, bevor alle Abhängigkeiten
457
+ gesetzt sind:
458
+
459
+ ```bash
460
+ post /scheduler/pause | jq .scheduler.state # → "paused" (Views und Dry-Run funktionieren weiter)
461
+ post /scheduler/resume | jq .scheduler.state # → "running" (sofortiger Planungsschritt)
462
+ ```
463
+
464
+ ---
465
+
466
+ ## 3 Testdaten
467
+
468
+ Alle Befehle setzen `source …/helpers.sh` und `ADISPATCH_TEST_OWNER` voraus. Die Testfälle legen ihre Issues selbst an
469
+ (`mkissue`). Hier entstehen nur Labels, Blocker und der PR.
470
+
471
+ ### 3.1 Labels (in beiden Repos)
472
+
473
+ ```bash
474
+ for r in orch-test-a orch-test-b; do
475
+ for l in ready-for-coding coding ready-for-review reviewing reviewed needs-maintainer \
476
+ ready-for-comment commenting commented deps-test manual-keep 'priority:high' 'prio: high/urgent' \
477
+ test:slow test:failure test:crash test:invalid-result test:token test:revoke-token; do
478
+ gh label create "$l" -R "$(repo $r)" --color C5DEF5 --force >/dev/null && printf '.'
479
+ done; echo " $r"
480
+ done
481
+ gh label list -R "$(repo orch-test-a)" --limit 100 | wc -l # ≥ 21 (inkl. GitHub-Standardlabels)
482
+ ```
483
+
484
+ Die Hooks setzen auch selbst Labels an, die es noch nicht gibt. GitHub legt sie dann an. Das Vorab-Anlegen sorgt nur für
485
+ Ordnung.
486
+
487
+ ### 3.2 Blocker-Issues
488
+
489
+ ```bash
490
+ post /scheduler/pause >/dev/null
491
+ export A_BLOCKER=$(mkissue orch-test-a "Blocker A (same repo)")
492
+ export B_BLOCKER=$(mkissue orch-test-b "Blocker B (cross repo, App hat Zugriff)")
493
+ export H_BLOCKER=$(mkissue orch-test-hidden "Blocker H (cross repo, KEIN App-Zugriff)")
494
+ echo "A_BLOCKER=$A_BLOCKER B_BLOCKER=$B_BLOCKER H_BLOCKER=$H_BLOCKER" | tee -a tmp/testdata.env
495
+ ```
496
+
497
+ ### 3.3 Pull Request in `orch-test-a`
498
+
499
+ ```bash
500
+ R=$(repo orch-test-a)
501
+ sha=$(gh api repos/$R/git/ref/heads/main --jq .object.sha)
502
+ gh api -X POST repos/$R/git/refs -f ref=refs/heads/pr-test -f sha="$sha" >/dev/null
503
+ gh api -X PUT repos/$R/contents/pr-test.txt -f message="PR-Test" -f branch=pr-test \
504
+ -f content="$(printf 'manual test\n' | base64)" >/dev/null
505
+ export A_PR=$(gh pr create -R $R --head pr-test --base main --title "PR-Test (Reviewer)" --body "manual test" | sed 's#.*/##')
506
+ echo "A_PR=$A_PR" | tee -a tmp/testdata.env
507
+ post /scheduler/resume >/dev/null
508
+ ```
509
+
510
+ Variablen in einer neuen Shell wiederherstellen: `set -a; . tmp/testdata.env; set +a`.
511
+
512
+ ---
513
+
514
+ ## 4 Testfälle
515
+
516
+ Format je Fall: **Ziel** (Bezug), **Schritte**, **Erwartet**, **Ist**. Die Ist-Zeile wird im Protokoll ([5.1](#51-ergebnisse-festhalten))
517
+ oder direkt hier abgehakt. „Nächster Poll“ heißt: höchstens 20 s plus Dauer des Planungsschritts. Die Instanz läuft
518
+ während aller Fälle (2.5), sofern nicht anders angegeben.
519
+
520
+ Reihenfolge: T1–T2 zuerst, dann beliebig. T2b läuft nebenher. T19 und T7 verändern App-Installationen: anschließend
521
+ **immer zurücksetzen**.
522
+
523
+ ### T1 – App-Auth: JWT → Installation-Token (A1, A9)
524
+
525
+ **Ziel:** Alle drei Identities bekommen Tokens. Das JWT-Format wird akzeptiert, `expires_at` liegt etwa 1 h in der
526
+ Zukunft, und jedes Token sieht genau die installierten Repos.
527
+
528
+ **Schritte:**
529
+ ```bash
530
+ grep 'Obtained GitHub installation token' tmp/orch.log | tail -5
531
+ for i in maintainer coder reviewer; do
532
+ echo "== $i"; ghraw $i /installation/repositories | jq -r '.repositories[].full_name'
533
+ done
534
+ ghraw coder /installation/repositories -o /dev/null -D - | grep -i -E '^x-github-api-version-selected|^x-ratelimit-limit'
535
+ ```
536
+
537
+ **Erwartet:**
538
+ - Das Log hat je benutzter Identity eine Zeile `Obtained GitHub installation token` mit `identity` und `expiresAt`
539
+ (≈ jetzt + 60 min), aber **ohne** Token- oder Key-Inhalt.
540
+ - Jede App listet genau `orch-test-a` und `orch-test-b`, **nicht** `orch-test-hidden`.
541
+ - Der Header `x-github-api-version-selected: 2026-03-10` ist vorhanden. Steht eine andere Version darin, ist A1
542
+ abweichend. Fehlt der Header ganz, nur notieren.
543
+
544
+ **Ist:** ☐ OK ☐ Abweichung: ……
545
+
546
+ ### T2 – Token-Erneuerung (A9, §22.1 „Token-Rotation“)
547
+
548
+ **T2a – erzwungen (Token widerrufen).**
549
+
550
+ **Ziel:** Ein ungültiges gecachtes Token (401) führt zu `invalidate` und einem neuen Token. Die Aktion gelingt ohne
551
+ sichtbaren Fehler. Der Worker bekommt dasselbe gecachte Coder-Token (`execute.secrets`) und widerruft es.
552
+
553
+ **Schritte:**
554
+ ```bash
555
+ N=$(mkissue orch-test-a "T2a revoke token" ready-for-coding test:revoke-token)
556
+ sleep 45; RUN=$(run_of test_a $N); echo $RUN
557
+ api "/runs/$RUN/logs?stream=stdout" | jq -r .data | grep DELETE
558
+ actions $RUN
559
+ grep -c '"identity":"coder"' <(grep 'Obtained GitHub installation token' tmp/orch.log)
560
+ labels orch-test-a $N
561
+ ```
562
+
563
+ **Erwartet:**
564
+ - Das stdout des Workers enthält `DELETE /installation/token → HTTP 204`.
565
+ - Die `on_success`-Actions sind `succeeded` mit `attempts=1`. Der 401 wird im Adapter einmal wiederholt und zählt nicht
566
+ als Action-Versuch.
567
+ - Das Log enthält **eine weitere** Zeile `Obtained GitHub installation token` für `coder`, kurz nach dem Run-Ende.
568
+ - Labels am Ende: `ready-for-review,test:revoke-token`.
569
+
570
+ Abweichung: Ist die Action `failed` mit HTTP 401, wiederholt der Adapter nach 401 nicht. Bleibt die zweite Token-Zeile
571
+ aus, obwohl alles gelang, bekam der Worker ein anderes Token als der Adapter. Das ist kein Fehler, aber zu notieren.
572
+
573
+ **Ist:** ☐ OK ☐ Abweichung: ……
574
+
575
+ **T2b – natürlicher Ablauf (läuft nebenher).**
576
+
577
+ **Ziel:** Das Token wird **vor** dem Ablauf erneuert (Refresh-Marge 5 min). Es gibt keinen 401 im Log.
578
+
579
+ **Schritte:** Die Instanz ≥ 65 min ohne Neustart laufen lassen. Dann:
580
+ ```bash
581
+ grep 'Obtained GitHub installation token' tmp/orch.log | jq -c '{time, identity, expiresAt}'
582
+ grep -E 'HTTP 401|"kind":"auth"' tmp/orch.log | head
583
+ ```
584
+
585
+ **Erwartet:**
586
+ - Für `maintainer` (pollt ständig) kommt etwa alle 55 min eine neue Zeile; `expiresAt` der Vorgängerzeile minus etwa
587
+ 5 min ≈ Zeit der neuen Zeile.
588
+ - Es gibt keine 401-Zeilen, außer der aus T2a.
589
+
590
+ **Ist:** ☐ OK ☐ Abweichung: ……
591
+
592
+ ### T3 – Polling, Label-Filter, Pushdown (§6, §7)
593
+
594
+ **Ziel:** Nur offene Items mit `ready-for-coding` werden gepollt (Pushdown `labels=`, `state=open`). Items ohne Label und
595
+ geschlossene Items erscheinen nicht.
596
+
597
+ **Schritte:**
598
+ ```bash
599
+ post /scheduler/pause >/dev/null
600
+ N_OK=$(mkissue orch-test-a "T3 mit Label" ready-for-coding manual-keep); echo "N_OK=$N_OK" >> tmp/testdata.env
601
+ N_NO=$(mkissue orch-test-a "T3 ohne Label")
602
+ N_CL=$(mkissue orch-test-a "T3 geschlossen mit Label" ready-for-coding); gh issue close $N_CL -R $(repo orch-test-a)
603
+ orch workflow dry-run coder-test --config-dir "$ADISPATCH_CFG" --local --data-dir "$ADISPATCH_DATA" # lokal: Gate egal
604
+ post /scheduler/resume >/dev/null; sleep 25; runs coder-test 5 | grep "test_a:$N_OK"
605
+ ```
606
+
607
+ **Erwartet:**
608
+ - Der Dry-Run zeigt `pushdown {"field":"labels","operator":"contains","value":"ready-for-coding"}`.
609
+ - `polled` entspricht der Zahl **offener** Items mit `ready-for-coding`.
610
+ - `test_a:$N_OK` ist `would_start`; `$N_NO` und `$N_CL` fehlen ganz.
611
+ - Nach `resume` gibt es einen Run für `test_a:$N_OK` (`runs`). `diag` zeigt nur den **letzten** Planungsschritt, in dem
612
+ das gestartete Item meist schon `locked` ist oder fehlt; Starts deshalb immer über `runs`/`run_of` prüfen.
613
+ - Über HTTP (`dry-run` ohne `--local`) wäre das Item bei pausiertem Scheduler `gate_closed` statt `would_start`.
614
+
615
+ **Ist:** ☐ OK ☐ Abweichung: ……
616
+
617
+ ### T4 – Erfolgreicher Run und Label-Hooks, manuelle Labels bleiben (§10, §24.5)
618
+
619
+ **Ziel:** `on_start` und `on_success` setzen bzw. entfernen genau die konfigurierten Labels. Das manuelle Label
620
+ `manual-keep` bleibt.
621
+
622
+ **Schritte:** Fortsetzung von T3.
623
+ ```bash
624
+ RUN=$(run_of test_a $N_OK); sleep 10; api /runs/$RUN | jq '{status, outcome, businessResult, resultMessage, identity, workspacePath, lifecycleStatus}'
625
+ actions $RUN
626
+ labels orch-test-a $N_OK
627
+ gh api repos/$(repo orch-test-a)/issues/$N_OK/events --jq '.[] | select(.event|test("label")) | [.event, .label.name, .actor.login, (.performed_via_github_app.slug // "-")] | @tsv'
628
+ ```
629
+
630
+ **Erwartet:**
631
+ - Der Run ist `completed`, `outcome: success`, mit `resultMessage: "Simulierter Erfolg fuer …"` und `identity: coder`.
632
+ - `on_start-labels`, `on_success-labels` und `on_success-comment` sind `succeeded` (explizite Action-IDs im Fixture).
633
+ - Labels: `manual-keep,ready-for-review`.
634
+ - Die Label-Events stammen vom Coder-Bot (`<coder-slug>[bot]`): erst `labeled coding` / `unlabeled ready-for-coding`,
635
+ dann `labeled ready-for-review` / `unlabeled coding`.
636
+
637
+ **Ist:** ☐ OK ☐ Abweichung: ……
638
+
639
+ ### T5 – Label-Updates idempotent, Sonderzeichen, Groß/Klein (§10, `applyUpdate`)
640
+
641
+ **Schritte:**
642
+ ```bash
643
+ N=$(mkissue orch-test-a "T5 labels" manual-keep 'prio: high/urgent'); echo "N_T5=$N" >> tmp/testdata.env
644
+ probe labels test_a $N coder +coding -'prio: high/urgent' # 1. Aufruf
645
+ probe labels test_a $N coder +coding -'prio: high/urgent' # 2. Aufruf
646
+ probe labels test_a $N coder +CODING # Groß/Klein
647
+ gh issue edit $N -R $(repo orch-test-a) --add-label needs-maintainer # „manuelle“ Änderung dazwischen
648
+ probe labels test_a $N coder -coding
649
+ labels orch-test-a $N
650
+ ```
651
+
652
+ **Erwartet:**
653
+ - Der 1. Aufruf gibt `{"changed": true, "applied": {"labelsAdded": ["coding"], "labelsRemoved": ["prio: high/urgent"]}}`.
654
+ Das Label mit Leerzeichen und Slash wird entfernt (URL-Encoding).
655
+ - Der 2. Aufruf und der Groß/Klein-Aufruf geben `{"changed": false}`.
656
+ - Am Ende: `manual-keep,needs-maintainer`. Die manuell hinzugefügten Labels bleiben.
657
+
658
+ **Ist:** ☐ OK ☐ Abweichung: ……
659
+
660
+ ### T6 – Kommentar mit Marker, Autor-Erkennung (A11, §10 Idempotenz)
661
+
662
+ **Ziel:** Kommentare tragen den unsichtbaren Marker als letzte Zeile. `performed_via_github_app.id` und `user.type`
663
+ entsprechen A11. `findCommentByMarker` findet nur Kommentare der **eigenen** App.
664
+
665
+ **Schritte:**
666
+ ```bash
667
+ comments orch-test-a $N_OK # Kommentar aus T4 (on_success-comment)
668
+ MARK=$(comments orch-test-a $N_OK | tail -1 | grep -o 'adispatchd:[^ ]*')
669
+ probe find test_a $N_OK coder "$MARK" # a) eigene App
670
+ probe find test_a $N_OK reviewer "$MARK" # c) fremde App, gleicher Marker
671
+ N=$(mkissue orch-test-a "T6 marker")
672
+ gh issue comment $N -R $(repo orch-test-a) --body $'Mensch\n\n<!-- adispatchd:manual-human:1 -->'
673
+ probe find test_a $N coder adispatchd:manual-human:1 # b) Mensch, gleicher Marker
674
+ probe comment test_a $N reviewer adispatchd:manual-rev:1 'als reviewer'
675
+ probe find test_a $N coder adispatchd:manual-rev:1 # c) andere App
676
+ probe find test_a $N reviewer adispatchd:manual-rev:1 # a) eigene App
677
+ probe comment test_a $A_PR coder adispatchd:manual-pr:1 'Kommentar an PR'
678
+ gh pr view $A_PR -R $(repo orch-test-a) --json comments,reviews --jq '{comments: (.comments|length), reviews: (.reviews|length)}'
679
+ ```
680
+
681
+ **Erwartet:**
682
+ - `comments` zeigt Login `<coder-slug>[bot]`, Typ `Bot`, App-ID = `$CODER_APP_ID`. Die letzte Zeile ist
683
+ `<!-- adispatchd:run-…:<action-id> -->`. Im Web-UI ist der Marker unsichtbar.
684
+ - a) gibt `{"id": "…", "url": "…#issuecomment-…"}`. b) und c) geben `null`.
685
+ - Der PR-Kommentar landet in der Konversation (`comments` +1), nicht als Review (`reviews` unverändert).
686
+
687
+ Weicht `performed_via_github_app` ab (fehlt oder `null`), ist A11 verletzt. Folge: Retries erzeugen Duplikate.
688
+ Betroffener Code: `authorPredicate`, siehe [5.2](#52-bei-abweichungen).
689
+
690
+ **Ist:** ☐ OK ☐ Abweichung: ……
691
+
692
+ ### T7 – Retry ohne Duplikat: Marker-Abgleich nach endgültigem Fehler (§10, Szenario 6 Teil 2, A11)
693
+
694
+ **Ziel:** Eine endgültig gescheiterte Kommentar-Action stoppt die Kette sichtbar. Nach der Behebung erkennt **Retry
695
+ Action** einen bereits vorhandenen Kommentar am Marker und postet **nicht** doppelt. Das simuliert „Kommentar wurde
696
+ gepostet, Antwort ging verloren“. Zusätzlich wird der Identitätswechsel geprüft: Der Start-Kommentar stammt vom Reviewer.
697
+
698
+ **Schritte:**
699
+ 1. Issue in `orch-test-b` anlegen, der Worker läuft 60 s:
700
+ ```bash
701
+ N=$(mkissue orch-test-b "T7 retry ohne Duplikat" ready-for-comment test:slow)
702
+ sleep 30; RUN=$(run_of test_b $N); actions $RUN
703
+ ```
704
+ 2. **Während der Worker läuft:** Die Coder-App verliert den Zugriff auf `orch-test-b`. Dazu
705
+ *Settings → Applications → Installed GitHub Apps → orch-test-coder-… → Configure → Repository access* öffnen,
706
+ `orch-test-b` entfernen und *Save* klicken.
707
+ 3. Das Run-Ende abwarten (ca. 40 s), dann:
708
+ ```bash
709
+ actions $RUN; api /runs/$RUN | jq '{status, outcome, lifecycleStatus, lifecycleFailed}'
710
+ api /actions/failed | jq '.actions[] | {id, hook, name, status, attempts, err: .lastError.message}'
711
+ api '/notifications?limit=3' | jq '.notifications[] | {eventType, deliveryState}'
712
+ ```
713
+ 4. Zugriff für die Coder-App wiederherstellen (`orch-test-b` wieder auswählen). Dann den Kommentar **mit genau dem
714
+ Marker** der gescheiterten Action als Coder posten. Das ist die Simulation „gepostet, Antwort verloren“:
715
+ ```bash
716
+ AID=$(api /runs/$RUN | jq -r '.actions[] | select(.name=="done-comment") | .id')
717
+ probe comment test_b $N coder "adispatchd:$RUN:$AID" 'Kommentar-Test fertig (vorab, simuliert)'
718
+ post /actions/$AID/retry | jq .accepted
719
+ sleep 5; actions $RUN; events $RUN | grep -i reconciled
720
+ comments orch-test-b $N
721
+ ```
722
+
723
+ **Erwartet:**
724
+ - Nach Schritt 1 ist `on_start-labels` `succeeded` und `start-comment` `succeeded`. Der erste Kommentar stammt von
725
+ `<reviewer-slug>[bot]` mit App-ID `$REVIEWER_APP_ID` (Action-Identity überschreibt die Workflow-Identity).
726
+ - Schritt 3:
727
+ - Der Run ist `completed`, `outcome: success`, mit `lifecycleStatus: failed` und `lifecycleFailed: true`.
728
+ - `done-comment` ist `failed` nach **1** Versuch, mit HTTP 404 bzw. 403. Das ist nicht wiederholbar, bewusst nach
729
+ WI-07.
730
+ - Das folgende `on_success-labels` bleibt `pending`.
731
+ - Es gibt eine Notification `lifecycle_action_failed`.
732
+ - Schritt 4:
733
+ - Der Retry endet mit `done-comment` `succeeded` und `externalKey` = ID des Vorab-Kommentars. Das Event
734
+ `lifecycle.action` hat `reconciled: true`.
735
+ - Danach läuft `on_success-labels`: Labels `commented,test:slow`.
736
+ - `comments` zeigt **genau einen** Kommentar mit diesem Marker. Der Worker wurde **nicht** neu gestartet (nur ein
737
+ `run.spawn` in `events`).
738
+
739
+ Variante ohne manuelles Posten: Wird Schritt 4 ohne `probe comment` ausgeführt, postet der Retry genau einmal.
740
+
741
+ Gelingt `done-comment` in Schritt 3 trotz entferntem Zugriff, war das gecachte Token noch gültig. Die Beobachtung
742
+ notieren und den Test so wiederholen: Den Zugriff **vor** dem Anlegen des Issues entziehen und die Instanz neu starten.
743
+ `on_start` scheitert dann allerdings ebenfalls, siehe T19b.
744
+
745
+ **Ist:** ☐ OK ☐ Abweichung: ……
746
+
747
+ ### T8 – Temporärer Kommentarfehler → automatischer Action-Retry (Szenario 6)
748
+
749
+ **Ziel:** Netzfehler beim Kommentar sind `transient`. Die Action wird nach 20 s wiederholt, der Worker läuft **nicht**
750
+ erneut, und es entsteht genau ein Kommentar.
751
+
752
+ **Schritte:**
753
+ ```bash
754
+ N=$(mkissue orch-test-b "T8 Netzwerk weg" ready-for-comment test:slow)
755
+ sleep 30; RUN=$(run_of test_b $N); api "/runs/$RUN/logs?stream=stdout&tail=200" | jq -r .data # Fortschritt beobachten
756
+ ```
757
+
758
+ Kurz **vor** dem Worker-Ende (bei `working 50/60s`) das Netz kappen und nach etwa 45 s wiederherstellen:
759
+
760
+ | System | Netz aus | Netz an |
761
+ |---|---|---|
762
+ | macOS (WLAN) | `networksetup -setairportpower en0 off` | `networksetup -setairportpower en0 on` |
763
+ | Linux (NetworkManager) | `nmcli networking off` | `nmcli networking on` |
764
+ | nur GitHub-API sperren | `echo '0.0.0.0 api.github.com' \| sudo tee -a /etc/hosts` (macOS danach `sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder`) | Zeile aus `/etc/hosts` entfernen, Cache erneut leeren |
765
+
766
+ ```bash
767
+ sleep 120; actions $RUN
768
+ api /runs/$RUN | jq '.actions[] | select(.name=="done-comment") | .attemptsDetail[] | {attempt, outcome, error: .error.message}'
769
+ events $RUN | grep -c run.spawn
770
+ comments orch-test-b $N | grep -c 'adispatchd:'
771
+ ```
772
+
773
+ **Erwartet:**
774
+ - `done-comment` hat ≥ 2 Versuche. Die ersten sind `failed` mit `network error` bzw. `request timed out` (`transient`),
775
+ der letzte `succeeded`. Die Abstände betragen etwa 20 s (`retryInMs` in den `lifecycle.action`-Events).
776
+ - `run.spawn` = 1.
777
+ - Genau **2** Kommentare mit Marker: `start-comment` und `done-comment`. Es gibt **kein** Duplikat. Ab dem 2. Versuch
778
+ sucht die Engine zuerst per Marker.
779
+ - Bleibt das Netz länger als 6 × 20 s weg, wird `done-comment` `failed`. Dann gilt Retry Action wie in T7.
780
+
781
+ Hinweis: Wird auch das Token-Holen getroffen, steht die Meldung beim Token-Austausch. Das ist ebenfalls `transient` und
782
+ ebenso wiederholbar.
783
+
784
+ **Ist:** ☐ OK ☐ Abweichung: ……
785
+
786
+ ### T9 – Fachliches `failure` → `needs-maintainer` (Szenario 4)
787
+
788
+ **Schritte:**
789
+ ```bash
790
+ N_FAIL=$(mkissue orch-test-a "T9 failure" ready-for-coding test:failure); echo "N_FAIL=$N_FAIL" >> tmp/testdata.env
791
+ sleep 45; RUN=$(run_of test_a $N_FAIL)
792
+ api /runs/$RUN | jq '{status, outcome, businessResult, resultMessage, countedTechnicalFailure, block}'
793
+ labels orch-test-a $N_FAIL; comments orch-test-a $N_FAIL
794
+ sleep 25; diag coder-test | grep "test_a:$N_FAIL"; api /blocks | jq '.blocks | length'
795
+ ```
796
+
797
+ **Erwartet:**
798
+ - `completed` / `outcome: failure` / `businessResult: failure`, mit `countedTechnicalFailure: false` und `block: null`.
799
+ - Labels: `needs-maintainer,test:failure` (`coding` weg, `ready-for-coding` weg).
800
+ - Der Kommentar lautet `Needs maintainer: Simulierter fachlicher Fehler fuer …`.
801
+ - Kein neuer Start, weil `ready-for-coding` fehlt; `diag` zeigt das Item deshalb gar nicht mehr (Ausgabe leer).
802
+ Es entsteht kein Block.
803
+
804
+ **Ist:** ☐ OK ☐ Abweichung: ……
805
+
806
+ ### T10 – Issue vs. Pull Request (Reviewer-Filter, Dependencies an PRs)
807
+
808
+ **Ziel:**
809
+ - PRs erscheinen im Listing (`type: pull_request`, `is_pull_request: true`), obwohl die Maintainer-App nur *Issues: read*
810
+ hat ([R4](#r4)).
811
+ - `reviewer-test` startet nur den PR. Ein Issue mit gleichem Label wird gefiltert.
812
+ - Dependencies sind an PRs auflösbar (offen, nicht dokumentiert).
813
+
814
+ **Schritte:**
815
+ ```bash
816
+ gh pr edit $A_PR -R $(repo orch-test-a) --add-label ready-for-review
817
+ probe list test_a ready-for-review | jq '.[] | {key, type}'
818
+ probe deps test_a $A_PR
819
+ ghraw maintainer "/repos/$(repo orch-test-a)/issues/$A_PR/dependencies/blocked_by" -w '\nHTTP %{http_code}\n'
820
+ sleep 25; diag reviewer-test | grep "test_a:$N_OK" # Issue: dauerhaft filtered
821
+ sleep 70; RUN=$(run_of test_a $A_PR); echo "PR-Run: ${RUN:-keiner}"; api /runs/$RUN | jq '{status, outcome, identity}'
822
+ gh pr view $A_PR -R $(repo orch-test-a) --json labels,comments --jq '{labels: [.labels[].name], last: .comments[-1].author.login}'
823
+ ```
824
+
825
+ **Erwartet:**
826
+ - `list` enthält `$A_PR` mit `type: pull_request` und das Issue aus T4 (`$N_OK`) mit `type: issue`.
827
+ - Für `deps` und den Rohaufruf gibt es zwei mögliche Ergebnisse:
828
+ - (a) HTTP 200 `[]`, `status: ok`: Dependencies funktionieren an PRs.
829
+ - (b) 404/403, `status: unknown`.
830
+
831
+ **Ergebnis notieren.** Bei (b) startet `reviewer-test` den PR nie (`filter_unknown`). Dann muss der Dependency-Filter
832
+ für PR-Workflows dokumentiert entfallen, siehe [5.2](#52-bei-abweichungen).
833
+ - `diag reviewer-test`: Das Issue `test_a:$N_OK` ist `filtered` (`is_pull_request` = false), in jedem Planungsschritt.
834
+ - Bei (a) gibt `run_of` einen Run für den PR. `diag` eignet sich dafür nicht: Es zeigt nur den letzten Schritt, und
835
+ nach dem Start fehlt der PR dort (Label `ready-for-review` entfernt). Bei (b) gibt es keinen Run, `diag` zeigt
836
+ `filter_unknown`.
837
+ - Der Run ist `completed` mit `identity: reviewer`.
838
+ - PR-Labels: `reviewed`. Der letzte Kommentar stammt von `<reviewer-slug>[bot]`.
839
+
840
+ **Ist:** ☐ OK ☐ Abweichung: …… (PR-Dependencies: ☐ a ☐ b)
841
+
842
+ ### T11 – Dependency im selben Repo (A2, A5)
843
+
844
+ **Schritte:**
845
+ ```bash
846
+ post /scheduler/pause >/dev/null
847
+ N_SAME=$(mkissue orch-test-a "T11 blocked by same repo" deps-test); echo "N_SAME=$N_SAME" >> tmp/testdata.env
848
+ blockedby orch-test-a $N_SAME orch-test-a $A_BLOCKER
849
+ gh issue edit $N_SAME -R $(repo orch-test-a) --add-label ready-for-coding
850
+ post /scheduler/resume >/dev/null; sleep 25
851
+ view dependencies
852
+ probe deps test_a $N_SAME
853
+ ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_SAME/dependencies/blocked_by" | jq '[.[] | {number, state, repository_url}]'
854
+ ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_SAME" | jq .issue_dependencies_summary
855
+ diag coder-test | grep "test_a:$N_SAME"
856
+ ```
857
+
858
+ **Erwartet:**
859
+ - View/`deps`: `status ok`, `blocked_by_total 1`, `blocked_by_open 1`, `blocked true`.
860
+ - Die Rohliste enthält genau den Blocker mit `state: "open"` und `repository_url` `…/repos/<owner>/orch-test-a`.
861
+ - Summary: `{"blocked_by": 1, "total_blocked_by": 1, …}` (A5: zuerst nur die Gesamtzahl).
862
+ - `coder-test`: `filtered` (Trace `dependencies.blocked_by_open` = 1, erwartet 0).
863
+ - Gegenprobe A5: `gh issue close $A_BLOCKER -R $(repo orch-test-a)`. Danach ist die Summary `blocked_by: 0`,
864
+ `total_blocked_by: 1`, und der nächste Poll startet `$N_SAME`. Den Blocker **wieder öffnen**
865
+ (`gh issue reopen $A_BLOCKER -R $(repo orch-test-a)`), er wird später nicht mehr gebraucht.
866
+
867
+ **Ist:** ☐ OK ☐ Abweichung: ……
868
+
869
+ ### T12 – Cross-Repo-Dependency mit Zugriff (A3)
870
+
871
+ **Schritte:**
872
+ ```bash
873
+ post /scheduler/pause >/dev/null
874
+ N_CROSS=$(mkissue orch-test-a "T12/T14 blocked by orch-test-b" deps-test); echo "N_CROSS=$N_CROSS" >> tmp/testdata.env
875
+ blockedby_api orch-test-a $N_CROSS orch-test-b $B_BLOCKER # bewusst über REST (issue_id), prüft auch den POST
876
+ gh issue edit $N_CROSS -R $(repo orch-test-a) --add-label ready-for-coding
877
+ post /scheduler/resume >/dev/null; sleep 25
878
+ view dependencies | grep -E "Nr|$N_CROSS"
879
+ ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_CROSS/dependencies/blocked_by" | jq '[.[] | {number, state, repository_url, repo: .repository.full_name}]'
880
+ diag coder-test | grep "test_a:$N_CROSS"
881
+ ```
882
+
883
+ **Erwartet:**
884
+ - `blockedby_api` gibt `blocked_by added: #<nr>` aus (HTTP 201; `gh api` zeigt den Status nicht). Laut Doku ist die
885
+ Antwort ein Issue-Objekt: notieren, ob `<nr>` das blockierte Issue (`$N_CROSS`) oder der Blocker (`$B_BLOCKER`) ist.
886
+ - View: `ok / 1 / 1 / true`.
887
+ - Die Rohliste enthält den Blocker aus `orch-test-b` (`repository_url …/orch-test-b`).
888
+ - `coder-test`: `filtered`.
889
+
890
+ **Ist:** ☐ OK ☐ Abweichung: ……
891
+
892
+ ### T13 – Cross-Repo-Dependency **ohne** Zugriff → `unknown` (A4, A6, §20.7 fail-closed)
893
+
894
+ **Schritte:**
895
+ ```bash
896
+ post /scheduler/pause >/dev/null
897
+ N_HID=$(mkissue orch-test-a "T13 blocked by hidden repo" deps-test); echo "N_HID=$N_HID" >> tmp/testdata.env
898
+ blockedby orch-test-a $N_HID orch-test-hidden $H_BLOCKER # als Mensch, der alle Repos sieht
899
+ gh issue edit $N_HID -R $(repo orch-test-a) --add-label ready-for-coding
900
+ gh issue view $N_HID -R $(repo orch-test-a) --json blockedBy --jq '.blockedBy' # Mensch sieht den Blocker
901
+ post /scheduler/resume >/dev/null; sleep 25
902
+ view dependencies | grep -E "Nr|$N_HID"
903
+ probe deps test_a $N_HID
904
+ ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_HID/dependencies/blocked_by" -w '\nHTTP %{http_code}\n'
905
+ ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_HID" | jq .issue_dependencies_summary
906
+ diag coder-test | grep "test_a:$N_HID"
907
+ gh issue close $H_BLOCKER -R $(repo orch-test-hidden); sleep 25
908
+ view dependencies | grep -E "Nr|$N_HID"; diag coder-test | grep "test_a:$N_HID"
909
+ gh issue reopen $H_BLOCKER -R $(repo orch-test-hidden)
910
+ ```
911
+
912
+ **Erwartet:**
913
+ - Der Mensch sieht den Blocker (`blockedBy` enthält `orch-test-hidden#…`).
914
+ - App-Rohantwort, **festhalten**, eine der folgenden Varianten:
915
+ - (a) HTTP 200 `[]` und Summary `total_blocked_by: 1` (A4 wie angenommen),
916
+ - (b) 403/404,
917
+ - (c) 200 mit dem Blocker trotz fehlender Installation.
918
+ - Bei (a) und (b):
919
+ - View: Status `unknown`, die anderen Spalten `?`.
920
+ - `deps`: `blocked_by_*` mit `known: false` und Grund `… not visible to identity …` bzw. `dependencies unavailable …`.
921
+ - `coder-test`: `filter_unknown`, **kein Start**.
922
+ - Auch nach dem Schließen des versteckten Blockers bleibt der Status `unknown`. Die App kann den Zustand nicht sehen,
923
+ bleibt also fail-closed.
924
+ - Bei (c) ist A4 falsch: Das private Repo wäre über die Abhängigkeit lesbar. Unbedingt melden.
925
+
926
+ **Ist:** ☐ OK ☐ Abweichung: …… (Variante: ☐ a ☐ b ☐ c; Rohantwort ins Protokoll)
927
+
928
+ ### T14 – Blocker schließen → nächster Poll startet (Szenario 3)
929
+
930
+ **Schritte:** Fortsetzung von T12, Blocker ist offen:
931
+ ```bash
932
+ diag coder-test | grep "test_a:$N_CROSS" # filtered
933
+ gh issue close $B_BLOCKER -R $(repo orch-test-b); date -u +%T
934
+ for i in 1 2 3 4; do sleep 10; date -u +%T; echo "run: $(run_of test_a $N_CROSS)"; done # leer, bis gestartet
935
+ ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_CROSS" | jq .issue_dependencies_summary
936
+ ```
937
+
938
+ **Erwartet:**
939
+ - Bis zum nächsten Planungsschritt gibt es keinen Run (das Issue bleibt `filtered`); ein früherer Start ist nicht
940
+ möglich. Danach liefert `run_of` eine Run-ID (≤ 20 s + Schrittdauer nach dem Schließen; Zeitstempel vergleichen).
941
+ - Der Run ist `completed` / `success`.
942
+ - Summary: `blocked_by: 0`, `total_blocked_by: 1` (A5).
943
+
944
+ **Ist:** ☐ OK ☐ Abweichung: ……
945
+
946
+ ### T15 – Zwei parallele Coder mit gleicher Identität, dritter durch Limit verhindert (Szenario 1)
947
+
948
+ **Schritte:**
949
+ ```bash
950
+ post /scheduler/pause >/dev/null
951
+ P1=$(mkissue orch-test-a "T15 parallel 1" ready-for-coding test:slow)
952
+ P2=$(mkissue orch-test-a "T15 parallel 2" ready-for-coding test:slow)
953
+ P3=$(mkissue orch-test-a "T15 parallel 3" ready-for-coding test:slow)
954
+ post /scheduler/resume >/dev/null; sleep 15
955
+ api /status | jq '.usage.byWorkflow["coder-test"]'
956
+ for n in $P1 $P2 $P3; do echo "#$n run: $(run_of test_a $n)"; done
957
+ diag coder-test | grep "test_a:$P3" # solange 2 aktiv: workflow_limit
958
+ for n in $P1 $P2; do api /runs/$(run_of test_a $n) | jq -c '{id, identity, workspacePath, status}'; done
959
+ sleep 100; echo "#$P3 run: $(run_of test_a $P3)"
960
+ ```
961
+
962
+ **Erwartet:**
963
+ - `byWorkflow` ist `{"active": 2, "max": 2}`. `$P1` und `$P2` haben einen Run (älteste zuerst), `$P3` keinen.
964
+ `diag` zeigt `$P3` als `workflow_limit` (`workflow limit reached (2/2 active)`); die beiden gestarteten Items
965
+ fehlen dort ab dem nächsten Schritt (Label `coding`), deshalb Starts über `run_of` prüfen.
966
+ - Beide Runs haben `identity: coder` und **verschiedene** `workspacePath`.
967
+ - Im Log gibt es **keine** zusätzliche `Obtained … coder`-Zeile für den zweiten Run (gemeinsamer Token-Cache).
968
+ - Nach Ende eines Runs startet `$P3` beim nächsten Poll (`run_of` liefert dann eine ID).
969
+
970
+ **Ist:** ☐ OK ☐ Abweichung: ……
971
+
972
+ ### T16 – Manueller Start per URL, Lock-Konflikt (Szenario 2)
973
+
974
+ **Schritte:**
975
+ ```bash
976
+ M=$(mkissue orch-test-a "T16 manuell, ohne ready-Label" test:slow)
977
+ post /workflows/coder-test/start "{\"item\":\"$(issue_url orch-test-a $M)\"}" | jq '{run: .run.id, trigger: .run.triggerType, status: .run.status}'
978
+ post /workflows/coder-test/start "{\"item\":\"$(issue_url orch-test-a $M)\"}" | jq .
979
+ post /workflows/coder-test/start "{\"item\":\"#$M\"}" | jq .error.code
980
+ post /workflows/coder-test/start "{\"item\":\"https://github.com/$ADISPATCH_TEST_OWNER/orch-test-b/issues/1\"}" | jq .error.code
981
+ post /workflows/coder-test/start "{\"item\":\"$(issue_url orch-test-a $N_HID)\"}" | jq '{run: .run.id, error: .error.code}'
982
+ ```
983
+
984
+ **Erwartet:**
985
+ - Der 1. Start gibt `202` mit `triggerType: manual`, obwohl `ready-for-coding` fehlt (Filter wird umgangen).
986
+ - Der 2. und 3. Start geben `409`, `error.code: lock_conflict`, `details.holderRunId` = Run des 1. Starts.
987
+ - Eine fremde Repo-URL gibt `400 manual_item_invalid_ref`.
988
+ - `$N_HID` (Dependency `unknown`) startet manuell (`202`), weil Filter umgangen werden. Danach erscheinen beide Runs in
989
+ `runs coder-test` mit `manual`.
990
+ - Der manuelle Run zählt zur Auslastung: `api /status | jq .usage`.
991
+
992
+ **Ist:** ☐ OK ☐ Abweichung: ……
993
+
994
+ ### T17 – Drei Crashes → Retry-Block → Remove Block (Szenario 5)
995
+
996
+ **Schritte:**
997
+ ```bash
998
+ C=$(mkissue orch-test-a "T17 crash" ready-for-coding test:crash); echo "C=$C" >> tmp/testdata.env
999
+ sleep 90; runs coder-test 10 | grep "test_a:$C" # noch keine 3 crashed? 20 s warten, wiederholen
1000
+ api /blocks | jq '.blocks[] | {id, workflowId, itemKey, reason, failureCount, affectedRunIds}'
1001
+ labels orch-test-a $C; sleep 25; diag coder-test | grep "test_a:$C"
1002
+ api '/notifications?limit=5' | jq -r '.notifications[] | .eventType' | sort | uniq -c
1003
+ ```
1004
+
1005
+ Nach dem Block das Crash-Label entfernen und den Block aufheben:
1006
+ ```bash
1007
+ gh issue edit $C -R $(repo orch-test-a) --remove-label test:crash
1008
+ BID=$(api /blocks | jq -r ".blocks[] | select(.itemKey==\"test_a:$C\") | .id")
1009
+ post /blocks/$BID/clear '{"clearedBy":"manual-test"}' | jq '.block | {active, clearedBy}'
1010
+ sleep 15; runs coder-test 10 | grep "test_a:$C"; labels orch-test-a $C
1011
+ api "/runs?item=test_a:$C" | jq '.total'
1012
+ ```
1013
+
1014
+ **Erwartet:**
1015
+ - Drei Runs `crashed` (`finalReason` mit Exit-Code 1). Die Ergebnisdatei mit `success` wird ignoriert.
1016
+ - Nach jedem Crash setzt `on_crash` `ready-for-coding` wieder. Der nächste Poll startet erneut, bis zum 3. Crash.
1017
+ - Danach entsteht genau ein aktiver Block (`failureCount: 3`, 3 `affectedRunIds`) und eine Notification
1018
+ `retry_block_created`.
1019
+ - Das Label `ready-for-coding` bleibt stehen, und `diag` zeigt `blocked` (`retry block <id>: …`). Es gibt keinen
1020
+ 4. Start.
1021
+ - Nach `clear` folgt sofort ein Planungsschritt. Der neue Run ist `completed` / `success`, die Labels werden
1022
+ `ready-for-review`.
1023
+ - `total` = 4: Die Historie bleibt vollständig.
1024
+
1025
+ **Ist:** ☐ OK ☐ Abweichung: ……
1026
+
1027
+ ### T18 – Ungültige Ergebnisdatei und Worker-Token (Worker-Protokoll, `execute.secrets`)
1028
+
1029
+ **Schritte:**
1030
+ ```bash
1031
+ I=$(mkissue orch-test-a "T18 invalid result" ready-for-coding test:invalid-result)
1032
+ T=$(mkissue orch-test-a "T18 token" ready-for-coding test:token)
1033
+ sleep 45; for n in $I $T; do api /runs/$(run_of test_a $n) | jq -c '{status, outcome, finalReason}'; done
1034
+ api "/runs/$(run_of test_a $T)/logs?stream=stdout" | jq -r .data | grep 'HTTP'
1035
+ gh issue edit $I -R $(repo orch-test-a) --remove-label ready-for-coding # sonst Crash-Schleife bis Block
1036
+ ```
1037
+
1038
+ **Erwartet:**
1039
+ - `$I`: `crashed` mit Grund „ungültige Ergebnisdatei“/Protokollfehler, `on_crash` lief.
1040
+ - `$T`: `completed` / `success`. Das Log zeigt `GET issue with GH_TOKEN → HTTP 200`, das Token selbst taucht nicht auf.
1041
+
1042
+ **Ist:** ☐ OK ☐ Abweichung: ……
1043
+
1044
+ ### T19 – Fehlende Rechte/Zugänge → sichtbarer Fehler statt stiller Annahme (§20.7, A6, A7)
1045
+
1046
+ Nach jedem Teil den Ausgangszustand wiederherstellen. Installationsänderungen wirken auf **neue** Tokens. Damit die
1047
+ laufende Instanz sie sofort sieht, die Instanz neu starten (Drain), sonst bis zur Token-Erneuerung warten.
1048
+
1049
+ **T19a – Source-Identity verliert ein Repo.** Bei der Maintainer-App unter *Configure → Repository access* `orch-test-b`
1050
+ entfernen, dann:
1051
+ ```bash
1052
+ sleep 25; diag commenter-test | head -1
1053
+ view needs-maintainer
1054
+ orch workflow dry-run commenter-test --config-dir "$ADISPATCH_CFG" --local --data-dir "$ADISPATCH_DATA"; echo "exit=$?"
1055
+ ```
1056
+
1057
+ Erwartet:
1058
+ - `status=source_error`, `error` z. B. `GET /repos/<owner>/orch-test-b/issues failed with HTTP 404: Not Found`. Kein
1059
+ Start, und andere Workflows laufen weiter.
1060
+ - Die View zeigt die Zeilen aus `test_a` plus `ERROR test_b: …`.
1061
+ - Der Dry-Run meldet `SOURCE ERROR`, Exit 1.
1062
+ - Danach `orch-test-b` wieder hinzufügen.
1063
+
1064
+ **T19b – Schreib-Identity verliert ein Repo.** Bei der Coder-App `orch-test-a` aus dem Repository access entfernen,
1065
+ Instanz neu starten:
1066
+ ```bash
1067
+ post /scheduler/pause >/dev/null
1068
+ W=$(mkissue orch-test-a "T19b ohne Schreibzugriff" ready-for-coding)
1069
+ post /scheduler/resume >/dev/null; sleep 30; RUN=$(run_of test_a $W)
1070
+ api /runs/$RUN | jq '{status, finalReason, lifecycleFailed, countedTechnicalFailure}'; actions $RUN
1071
+ post /scheduler/pause >/dev/null; labels orch-test-a $W
1072
+ ```
1073
+
1074
+ Erwartet:
1075
+ - Der Run ist `start_error` mit `finalReason` `on_start action 'on_start-labels' failed`. Der Fehler der Action
1076
+ (`actions $RUN`, letzte Spalte) lautet `Item <owner>/orch-test-a#<nr> not found`: Das Neu-Lesen in `applyUpdate`
1077
+ mit der Coder-Identity bekommt 404 und wertet es als „nicht gefunden“ (`fetchIssue` → `null`). Bei 403 stünde dort
1078
+ `… HTTP 403 …`. Der Worker wurde **nicht**
1079
+ gestartet.
1080
+ - `on_start-labels` ist `failed`, und `on_start_error-labels` ist ebenfalls `failed` (gleiche Ursache).
1081
+ - `lifecycleFailed: true`, und eine Notification `lifecycle_action_failed` entsteht.
1082
+ - Die Labels bleiben unverändert (`ready-for-coding`). Ohne Pause würde der nächste Poll erneut starten, bis nach 3
1083
+ `start_error` ein Block entsteht. Das ist korrekt (Spec §11), aber zu beobachten.
1084
+ - Danach den Zugriff wiederherstellen, `resume`, und ggf. den Block aufheben.
1085
+
1086
+ **T19c – fehlender Key.** Instanz stoppen, `unset ADISPATCH_TEST_MAINTAINER_KEY`, dann:
1087
+ ```bash
1088
+ orch workflow dry-run coder-test --config-dir "$ADISPATCH_CFG" --local --data-dir "$ADISPATCH_DATA"
1089
+ ```
1090
+
1091
+ Erwartet: `SOURCE ERROR: … Private key for identity "maintainer" missing: environment variable ADISPATCH_TEST_MAINTAINER_KEY is not set`.
1092
+ Mit einem kaputten Key (`export ADISPATCH_TEST_MAINTAINER_KEY=kaputt`) lautet die Meldung `… is not a valid RSA PEM key`.
1093
+ Keine der Ausgaben enthält Key-Material. Danach den Key wieder setzen.
1094
+
1095
+ **T19d – Rechte-Granularität (A7, [R4](#r4)).**
1096
+ 1. Coder-App *Permissions & events*: *Pull requests* auf **No access** setzen und speichern. Die Installation übernimmt
1097
+ Rechte-**Reduktionen** in der Regel sofort; ob eine Bestätigung nötig ist, notieren.
1098
+ 2. Dann:
1099
+ ```bash
1100
+ probe labels test_a $A_PR coder +perm-test # Label an PR nur mit Issues: write
1101
+ probe comment test_a $A_PR coder adispatchd:perm-pr:1 'PR-Kommentar nur mit Issues: write'
1102
+ ```
1103
+ 3. *Pull requests* wieder auf **Read and write** und *Issues* auf **Read-only** setzen. Die Erhöhung muss in der
1104
+ Installation bestätigt werden (*Review request* bzw. *Configure → Accept new permissions*). Dann:
1105
+ ```bash
1106
+ N=$(mkissue orch-test-a "T19d")
1107
+ probe labels test_a $N coder +perm-test # Label an Issue nur mit Pull requests: write
1108
+ ghraw coder "/repos/$(repo orch-test-a)/issues/$N/labels" -X POST -d '{"labels":["perm-test"]}' -D - -o /dev/null | grep -i -E '^HTTP|x-accepted-github-permissions'
1109
+ probe labels test_a $A_PR coder +perm-test2 # Label an PR nur mit Pull requests: write
1110
+ ```
1111
+ 4. Ursprüngliche Rechte wiederherstellen (Issues und Pull requests: Read and write) und bestätigen.
1112
+
1113
+ Erwartet (laut Doku): Jede der Aktionen gelingt mit **einem** der beiden Rechte. Die Ergebnisse festhalten:
1114
+
1115
+ | Ziel | nur Issues: write | nur Pull requests: write |
1116
+ |---|---|---|
1117
+ | Label an Issue | ✔ (Basis) | ☐ ✔ ☐ ✘ |
1118
+ | Label an PR | ☐ ✔ ☐ ✘ | ☐ ✔ ☐ ✘ |
1119
+ | Kommentar an PR | ☐ ✔ ☐ ✘ | – |
1120
+
1121
+ Ein ✘ zeigt sich als `forbidden`, HTTP 403 `Resource not accessible by integration`, und
1122
+ `x-accepted-github-permissions` nennt das nötige Recht. Ergebnis → Rechte-Tabelle 1.3, `README.md` und
1123
+ `src/cli/init-templates.ts` (Kommentar in `identities.yaml`) anpassen.
1124
+
1125
+ **Ist:** a ☐ b ☐ c ☐ d ☐ OK ☐ Abweichung: ……
1126
+
1127
+ ### T20 – Dry-Run gegen echte Repos (§19)
1128
+
1129
+ **Schritte:**
1130
+ ```bash
1131
+ post /scheduler/pause >/dev/null
1132
+ D=$(mkissue orch-test-a "T20 dry-run" ready-for-coding 'priority:high')
1133
+ # frische blockierte Kandidaten (die aus T11–T13 sind inzwischen gestartet/umgelabelt); Blocker A/H sind offen
1134
+ D_SAME=$(mkissue orch-test-a "T20 blocked same repo"); blockedby orch-test-a $D_SAME orch-test-a $A_BLOCKER
1135
+ D_HID=$(mkissue orch-test-a "T20 blocked hidden"); blockedby orch-test-a $D_HID orch-test-hidden $H_BLOCKER
1136
+ for n in $D_SAME $D_HID; do gh issue edit $n -R "$(repo orch-test-a)" --add-label ready-for-coding; done
1137
+ before_labels=$(labels orch-test-a $D); before_runs=$(api /runs | jq .total)
1138
+ orch workflow dry-run coder-test --config-dir "$ADISPATCH_CFG" # über die laufende Instanz (HTTP)
1139
+ orch workflow dry-run coder-test --config-dir "$ADISPATCH_CFG" --local --data-dir "$ADISPATCH_DATA" --json | jq '.candidates[] | {itemKey, decision, itemPriority, reason}'
1140
+ orch workflow dry-run reviewer-test --config-dir "$ADISPATCH_CFG"
1141
+ [ "$(labels orch-test-a $D)" = "$before_labels" ] && [ "$(api /runs | jq .total)" = "$before_runs" ] && echo "unverändert"
1142
+ post /scheduler/resume >/dev/null
1143
+ ```
1144
+
1145
+ **Erwartet:**
1146
+ - Über HTTP: Gate `paused`, geeignete Kandidaten `gate_closed`. Lokal: `test_a:$D` ist `would_start` mit
1147
+ `itemPriority: 50` und steht vor anderen Kandidaten.
1148
+ - `$D_HID` ist `filter_unknown`, `$D_SAME` ist `filtered` (Trace `dependencies.blocked_by_open` = 1).
1149
+ Nach `resume` bleiben beide ohne Run; zum Aufräumen `ready-for-coding` wieder entfernen.
1150
+ - Blockierte Items erscheinen als `blocked`, aktive als `locked`.
1151
+ - Labels, Kommentare und Runs bleiben unverändert (`unverändert`).
1152
+ - Die Notes nennen „shell filters and conditions are never executed“.
1153
+
1154
+ **Ist:** ☐ OK ☐ Abweichung: ……
1155
+
1156
+ ### T21 – View über zwei Repos (Szenario 10)
1157
+
1158
+ **Schritte:**
1159
+ ```bash
1160
+ F=$(mkissue orch-test-b "T21 failure in repo b" ready-for-comment test:failure)
1161
+ sleep 45; view needs-maintainer
1162
+ api /views/needs-maintainer/items | jq '{cached, rows: [.rows[] | {key, url, lastRunId}], errors}'
1163
+ ```
1164
+
1165
+ Zusätzlich im Browser *Views → Needs Maintainer* öffnen.
1166
+
1167
+ **Erwartet:**
1168
+ - Es gibt Zeilen aus **beiden** Repos: `test_a:$N_FAIL` (T9) und `test_b:$F`. Die Spalten „Letzter Agent“ zeigen
1169
+ `coder-test` bzw. `commenter-test`, „Ergebnis“ die `failure`-Nachricht, „Status“ `completed`.
1170
+ - Außerdem erscheint `test_a:$N_T5` (T5): Das Label `needs-maintainer` wurde dort manuell gesetzt, das Issue hat
1171
+ **keinen** Run. Die Run-Spalten sind leer (`null`, nicht `?`), sortiert nach „Aktualisiert“. Weitere Zeilen nur für
1172
+ Items, die in anderen Fällen ebenfalls `needs-maintainer` bekamen.
1173
+ - Der Titel ist ein klickbarer Link auf das GitHub-Issue.
1174
+ - `errors: []`.
1175
+ - Ein zweiter Aufruf ohne `refresh` innerhalb von 30 s liefert `cached: true`.
1176
+
1177
+ **Ist:** ☐ OK ☐ Abweichung: ……
1178
+
1179
+ ### T22 – Rate-Limit beobachten (A8, [R8](#r8))
1180
+
1181
+ **Schritte:**
1182
+ ```bash
1183
+ for i in maintainer coder; do echo "== $i"; ghraw $i /rate_limit | jq -c '.resources.core, (has("rate"))'; done
1184
+ ghraw maintainer /rate_limit -D - -o /dev/null | grep -i '^x-ratelimit'
1185
+ u1=$(ghraw maintainer /rate_limit | jq .resources.core.used); sleep 600
1186
+ u2=$(ghraw maintainer /rate_limit | jq .resources.core.used); echo "maintainer: $((u2 - u1)) Requests in 10 min"
1187
+ grep -E '"kind":"rate_limited"|retrying' tmp/orch.log | tail
1188
+ ```
1189
+
1190
+ **Erwartet:**
1191
+ - `resources.core.limit` ≥ 5000. `has("rate")` ist `false` (API 2026-03-10).
1192
+ - Die Header `x-ratelimit-limit|remaining|used|reset|resource` sind vorhanden.
1193
+ - Verbrauch in 10 min ≈ 30 Polls × (Listing `coder-test` + Listing `reviewer-test` + Listing `commenter-test` +
1194
+ 2 × Anzahl Kandidaten mit Dependency-Filter, deren günstiger Filter passt) plus View-Aufrufe. Bei rund 3 Kandidaten
1195
+ sind das etwa 300. Der Wert muss weit unter 5000/h bleiben. Messwert festhalten.
1196
+ - Im Log stehen keine `rate_limited`-Einträge.
1197
+
1198
+ Ein echtes Rate-Limit wird **nicht** provoziert, weil das sekundäre Limit den Test-Account sperren kann. Die Behandlung
1199
+ von 403/429, `retry-after` und `x-ratelimit-reset` ist in `http.test.ts` gegen Fake-Antworten geprüft. Tritt ein Limit
1200
+ zufällig auf, Header und Logzeile festhalten.
1201
+
1202
+ **Ist:** ☐ OK ☐ Abweichung: ……
1203
+
1204
+ ### T23 – Transferiertes Issue (A10, optional)
1205
+
1206
+ **Schritte:**
1207
+ ```bash
1208
+ X=$(mkissue orch-test-a "T23 transfer")
1209
+ gh issue transfer "$(issue_url orch-test-a $X)" "$(repo orch-test-b)" # URL statt Nummer: nie das Repo im cwd
1210
+ ghraw maintainer "/repos/$(repo orch-test-a)/issues/$X" -o /dev/null -w 'HTTP %{http_code} → %{redirect_url}\n'
1211
+ probe item test_a $X; echo "exit=$?"
1212
+ ```
1213
+
1214
+ **Erwartet:** Der Rohaufruf gibt `HTTP 301` mit einer Ziel-URL im neuen Repo. `probe item` meldet `item … not found`,
1215
+ weil `getItem` = `null` (fetch folgt dem Redirect, `repository_url` passt nicht).
1216
+
1217
+ **Ist:** ☐ OK ☐ Abweichung: ……
1218
+
1219
+ ### T24 – Keine Secrets in Logs und Daten (§20.4)
1220
+
1221
+ **Schritte:** nach allen anderen Fällen.
1222
+ ```bash
1223
+ PAT='ghs_[A-Za-z0-9_]{10,}|BEGIN RSA PRIVATE KEY|BEGIN PRIVATE KEY|eyJhbGciOi'
1224
+ grep -rlaE "$PAT" tmp/orch.log "$ADISPATCH_DATA" && echo "TREFFER!" || echo "sauber" # -a: auch SQLite/WAL (binär)
1225
+ command -v sqlite3 >/dev/null && sqlite3 -readonly "$ADISPATCH_DATA/adispatchd.sqlite" .dump | grep -cE "$PAT"
1226
+ api /config/files | grep -c 'PRIVATE KEY'
1227
+ ```
1228
+
1229
+ **Erwartet:** `sauber`, der `sqlite3`-Dump ergibt `0` (falls `sqlite3` installiert) und `/config/files` ebenfalls `0`. Ein Treffer ist ein Fehler: die Datei notieren, **nicht** einchecken, den Key rotieren.
1230
+
1231
+ **Ist:** ☐ OK ☐ Abweichung: ……
1232
+
1233
+ ---
1234
+
1235
+ ## 5 Auswertung und Aufräumen
1236
+
1237
+ ### 5.1 Ergebnisse festhalten
1238
+
1239
+ Je Testlauf eine Zeile pro Fall. Bei Abweichungen gehören die Rohantworten (`ghraw …`) und die passenden Logzeilen
1240
+ (`grep <run-id> tmp/orch.log`) dazu. Tokens vorher unkenntlich machen. Bleibt dieses Dokument im Repo, landen die
1241
+ Ergebnisse in einem Issue oder PR.
1242
+
1243
+ | Datum | Commit | API-Version | gh-Version | Fall | Ergebnis | Beobachtung / Rohantwort |
1244
+ |---|---|---|---|---|---|---|
1245
+ | | | 2026-03-10 | | T1 | | |
1246
+
1247
+ Zusätzlich die offenen Fragen beantworten:
1248
+
1249
+ | Frage | Testfall | Antwort |
1250
+ |---|---|---|
1251
+ | Blocker ohne Zugriff: Variante a/b/c? Summary zählt sie mit? | T13 | |
1252
+ | `issue_dependencies_summary.blocked_by` = offene Blocker? | T11, T14 | |
1253
+ | Dependencies an PRs: 200 oder 404/403? | T10 | |
1254
+ | Labels/Kommentare an PRs nur mit *Issues: write*? | T19d | |
1255
+ | `user.type = Bot`, `performed_via_github_app.id = App-ID`? | T6 | |
1256
+ | POST `blocked_by` gibt welches Issue zurück? | T12 | |
1257
+ | Reduktion von App-Rechten sofort wirksam? | T19d | |
1258
+
1259
+ ### 5.2 Bei Abweichungen
1260
+
1261
+ Vorgehen nach `docs/conventions.md`:
1262
+ 1. `FakeGitHub` an das echte Verhalten anpassen.
1263
+ 2. Einen fehlschlagenden Test schreiben.
1264
+ 3. Den Code korrigieren und `npm run check` laufen lassen.
1265
+ 4. Diese Anleitung und die Annahmen-Tabelle aktualisieren.
1266
+
1267
+ | Abweichung | Betroffene Stellen |
1268
+ |---|---|
1269
+ | A1 API-Version abgelehnt/veraltet | `GITHUB_API_VERSION` (`src/sources/github/http.ts`). `GH_API_VERSION` in `github-fixture/helpers.sh` und `scripts/test-worker.sh` mitziehen, `manual-fixture.test.ts` erzwingt das |
1270
+ | A2/A3 andere Form der Blocker-Liste, Pagination | `resolveDependencies` (`src/sources/github/adapter.ts`), `FakeGitHub` Dependencies (`src/sources/github/testing/`) |
1271
+ | A4 Variante b (403/404) | nichts nötig, fail-closed über den `catch`. Nur Fake ergänzen (`denyDependencies`) |
1272
+ | A4 Variante c (Blocker trotz fehlendem Zugriff sichtbar) | sicherheitsrelevant: als GitHub-Verhalten dokumentieren. Der Adapter bleibt korrekt (zählt), aber Spec §20.7 und README ergänzen |
1273
+ | A4 Summary zählt unsichtbare Blocker **nicht** | **kritisch**: fail-closed greift nicht, `blocked_by_open` wäre 0 → Start trotz Blocker. Mit App-Rechten ist das nicht erkennbar. Sofort dokumentieren (README, Spec §6/§20.7), dass alle Blocker-Repos für die Source-Identity installiert sein **müssen**. Code-Entscheidung als eigenes Work-Item: z. B. Option, Cross-Repo-Abhängigkeiten nur aus konfigurierten Sources zu akzeptieren |
1274
+ | A5 Semantik vertauscht | nur relevant, falls der Code `blocked_by` nutzt (derzeit nicht) |
1275
+ | A6 anderer Status bei fehlendem Recht | `GitHubHttpClient.toError` (Klassifikation). Für Dependencies unkritisch, jeder Fehler → `unknown` |
1276
+ | A7 PR-Rechte | Doku: Tabelle 1.3, `README.md` („Rechte: …“), Kommentar in `src/cli/init-templates.ts` (`IDENTITIES_YAML`) |
1277
+ | A8 Rate-Limit-Header anders | `GitHubHttpClient.toError` / `retryAfter`, `http.test.ts` |
1278
+ | A9 Token-Laufzeit/JWT abgelehnt | `src/identities/jwt.ts` (`JWT_*`), `refreshMarginMs` in `github-app.ts`. Bei `iss`-Problemen Client-ID als neues Konfigfeld (`src/config/`, eigenes Work-Item) |
1279
+ | A10 Transfer anders | `fetchIssue` / `sameRepoUrl` in `adapter.ts` |
1280
+ | A11 Autor-Felder anders | `authorPredicate` in `adapter.ts`, `FakeGitHub.addComment` (`viaAppId`, `type`). Ohne `performed_via_github_app` ggf. `user.login === '<slug>[bot]'` über `describe()` (`IdentityDescription`, `src/core/types.ts`) |
1281
+ | PR-Dependencies 404 (T10 b) | Doku: Dependency-Filter nicht für PR-Workflows (`reviewer-test`, README, Beispiel-Configs). Optional: `resolveDependencies` für `type: pull_request` → `ok/0` (Entscheidung nötig: fail-closed vs. „PRs haben keine Blocker“) |
1282
+ | Retry erzeugt Duplikat (T7/T8) | `performSourceComment` (`src/lifecycle/performers.ts`), `findCommentByMarker` / `markerPattern` (`adapter.ts`) |
1283
+ | Sekundäres Rate-Limit bei vielen Hooks | Drosselung schreibender Requests (≥ 1 s Abstand laut [R8](#r8)) in `GitHubHttpClient` fehlt. Als Folge-Work-Item aufnehmen |
1284
+
1285
+ ### 5.3 Aufräumen
1286
+
1287
+ ```bash
1288
+ # 1. Instanz stoppen (im Start-Terminal Ctrl-C) bzw.:
1289
+ orch drain --config-dir "$ADISPATCH_CFG" --wait
1290
+ # 2. Lokale Testdaten und Probe entfernen
1291
+ rm -rf tmp/orch-cfg tmp/orch-data tmp/orch.log tmp/testdata.env tmp/orch-test.env tmp/probe.ts
1292
+ unset ADISPATCH_TEST_MAINTAINER_KEY ADISPATCH_TEST_CODER_KEY ADISPATCH_TEST_REVIEWER_KEY
1293
+ # 3. Repos löschen (braucht Scope delete_repo)
1294
+ gh auth refresh -s delete_repo
1295
+ : "${ADISPATCH_TEST_OWNER:?ADISPATCH_TEST_OWNER setzen}"; type repo >/dev/null || { echo "helpers.sh nicht geladen"; false; } &&
1296
+ for r in orch-test-a orch-test-b orch-test-hidden; do gh repo delete "$(repo $r)"; done # fragt je Repo nach (Namen prüfen)
1297
+ # 4. Private Keys lokal löschen
1298
+ rm -P ~/orch-test-keys/*.pem 2>/dev/null || shred -u ~/orch-test-keys/*.pem; rmdir ~/orch-test-keys
1299
+ ```
1300
+
1301
+ Danach in GitHub für jede App:
1302
+ 1. *Settings → Applications → Installed GitHub Apps → Configure → Uninstall*.
1303
+ 2. *Developer settings → GitHub Apps → \<App> → Advanced → Delete GitHub App*. Damit werden auch die öffentlichen
1304
+ Schlüssel ungültig.
1305
+ 3. Prüfen, dass unter *Settings → Applications* keine `orch-test-*`-Installation übrig ist.
1306
+
1307
+ Soll dasselbe Setup für spätere Läufe bleiben, die Apps nur behalten, wenn die Keys sicher liegen. Zwischen den Läufen
1308
+ reicht es, die Test-Repos neu anzulegen (Abschnitt 1.2/3) und die Installationen auf die neuen Repos zu setzen.