@shipfox/api-workflows 10.2.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/CHANGELOG.md +117 -0
  3. package/README.md +75 -0
  4. package/dist/core/agent-defaults.d.ts +2 -1
  5. package/dist/core/agent-defaults.d.ts.map +1 -1
  6. package/dist/core/agent-defaults.js.map +1 -1
  7. package/dist/core/checkout.d.ts +7 -21
  8. package/dist/core/checkout.d.ts.map +1 -1
  9. package/dist/core/checkout.js +122 -41
  10. package/dist/core/checkout.js.map +1 -1
  11. package/dist/core/entities/job-execution.d.ts +8 -0
  12. package/dist/core/entities/job-execution.d.ts.map +1 -1
  13. package/dist/core/entities/job-execution.js +13 -1
  14. package/dist/core/entities/job-execution.js.map +1 -1
  15. package/dist/core/entities/job-listener-event.d.ts +2 -0
  16. package/dist/core/entities/job-listener-event.d.ts.map +1 -1
  17. package/dist/core/entities/job-listener-event.js.map +1 -1
  18. package/dist/core/entities/step.d.ts +13 -3
  19. package/dist/core/entities/step.d.ts.map +1 -1
  20. package/dist/core/entities/step.js +6 -0
  21. package/dist/core/entities/step.js.map +1 -1
  22. package/dist/core/entities/workflow-run-attempt.d.ts +1 -0
  23. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
  24. package/dist/core/entities/workflow-run-attempt.js.map +1 -1
  25. package/dist/core/entities/workflow-run.d.ts +17 -0
  26. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  27. package/dist/core/entities/workflow-run.js.map +1 -1
  28. package/dist/core/errors.d.ts +9 -2
  29. package/dist/core/errors.d.ts.map +1 -1
  30. package/dist/core/errors.js +10 -5
  31. package/dist/core/errors.js.map +1 -1
  32. package/dist/core/job-execution.d.ts.map +1 -1
  33. package/dist/core/job-execution.js +14 -8
  34. package/dist/core/job-execution.js.map +1 -1
  35. package/dist/core/job-transition/decide-job-activation.d.ts +1 -0
  36. package/dist/core/job-transition/decide-job-activation.d.ts.map +1 -1
  37. package/dist/core/job-transition/decide-job-activation.js +1 -0
  38. package/dist/core/job-transition/decide-job-activation.js.map +1 -1
  39. package/dist/core/job-transition/derive-job-success.d.ts +1 -0
  40. package/dist/core/job-transition/derive-job-success.d.ts.map +1 -1
  41. package/dist/core/job-transition/derive-job-success.js +5 -8
  42. package/dist/core/job-transition/derive-job-success.js.map +1 -1
  43. package/dist/core/listener-execution-materialization.d.ts +6 -3
  44. package/dist/core/listener-execution-materialization.d.ts.map +1 -1
  45. package/dist/core/listener-execution-materialization.js +44 -6
  46. package/dist/core/listener-execution-materialization.js.map +1 -1
  47. package/dist/core/resolve-trigger-reference.d.ts +11 -0
  48. package/dist/core/resolve-trigger-reference.d.ts.map +1 -0
  49. package/dist/core/resolve-trigger-reference.js +39 -0
  50. package/dist/core/resolve-trigger-reference.js.map +1 -0
  51. package/dist/core/run-workflow.d.ts +1 -0
  52. package/dist/core/run-workflow.d.ts.map +1 -1
  53. package/dist/core/run-workflow.js +1 -0
  54. package/dist/core/run-workflow.js.map +1 -1
  55. package/dist/core/step-config/agent.d.ts +1 -2
  56. package/dist/core/step-config/agent.d.ts.map +1 -1
  57. package/dist/core/step-config/agent.js +22 -7
  58. package/dist/core/step-config/agent.js.map +1 -1
  59. package/dist/core/step-config/assemble-run-context.d.ts +19 -8
  60. package/dist/core/step-config/assemble-run-context.d.ts.map +1 -1
  61. package/dist/core/step-config/assemble-run-context.js +51 -19
  62. package/dist/core/step-config/assemble-run-context.js.map +1 -1
  63. package/dist/core/step-config/complete-step-dispatch-config.d.ts.map +1 -1
  64. package/dist/core/step-config/complete-step-dispatch-config.js +72 -4
  65. package/dist/core/step-config/complete-step-dispatch-config.js.map +1 -1
  66. package/dist/core/step-config/index.d.ts +1 -0
  67. package/dist/core/step-config/index.d.ts.map +1 -1
  68. package/dist/core/step-config/index.js +1 -0
  69. package/dist/core/step-config/index.js.map +1 -1
  70. package/dist/core/step-config/materialize-job-execution-steps.d.ts +1 -1
  71. package/dist/core/step-config/materialize-job-execution-steps.d.ts.map +1 -1
  72. package/dist/core/step-config/materialize-job-execution-steps.js +49 -8
  73. package/dist/core/step-config/materialize-job-execution-steps.js.map +1 -1
  74. package/dist/core/step-config/materialize-workflow-model.d.ts +3 -3
  75. package/dist/core/step-config/materialize-workflow-model.d.ts.map +1 -1
  76. package/dist/core/step-config/materialize-workflow-model.js +9 -5
  77. package/dist/core/step-config/materialize-workflow-model.js.map +1 -1
  78. package/dist/core/step-config/resolve-job-execution-name.d.ts +3 -3
  79. package/dist/core/step-config/resolve-job-execution-name.d.ts.map +1 -1
  80. package/dist/core/step-config/resolve-job-execution-name.js +24 -13
  81. package/dist/core/step-config/resolve-job-execution-name.js.map +1 -1
  82. package/dist/core/step-config/resolve-step-config.d.ts.map +1 -1
  83. package/dist/core/step-config/resolve-step-config.js +174 -9
  84. package/dist/core/step-config/resolve-step-config.js.map +1 -1
  85. package/dist/core/step-config/resolve-workflow-run-name.d.ts +19 -0
  86. package/dist/core/step-config/resolve-workflow-run-name.d.ts.map +1 -0
  87. package/dist/core/step-config/resolve-workflow-run-name.js +73 -0
  88. package/dist/core/step-config/resolve-workflow-run-name.js.map +1 -0
  89. package/dist/core/step-transition/decide-step-transition.js.map +1 -1
  90. package/dist/core/step-transition/evaluate-gate.d.ts +2 -1
  91. package/dist/core/step-transition/evaluate-gate.d.ts.map +1 -1
  92. package/dist/core/step-transition/evaluate-gate.js +5 -3
  93. package/dist/core/step-transition/evaluate-gate.js.map +1 -1
  94. package/dist/core/workflow-run-creation.d.ts +3 -0
  95. package/dist/core/workflow-run-creation.d.ts.map +1 -1
  96. package/dist/core/workflow-run-creation.js +22 -7
  97. package/dist/core/workflow-run-creation.js.map +1 -1
  98. package/dist/db/db.d.ts +278 -10
  99. package/dist/db/db.d.ts.map +1 -1
  100. package/dist/db/db.js +2 -0
  101. package/dist/db/db.js.map +1 -1
  102. package/dist/db/index.d.ts +3 -2
  103. package/dist/db/index.d.ts.map +1 -1
  104. package/dist/db/index.js +2 -1
  105. package/dist/db/index.js.map +1 -1
  106. package/dist/db/job-listener-events.d.ts +2 -0
  107. package/dist/db/job-listener-events.d.ts.map +1 -1
  108. package/dist/db/job-listener-events.js +1 -0
  109. package/dist/db/job-listener-events.js.map +1 -1
  110. package/dist/db/job-listeners.d.ts +2 -0
  111. package/dist/db/job-listeners.d.ts.map +1 -1
  112. package/dist/db/job-listeners.js +81 -21
  113. package/dist/db/job-listeners.js.map +1 -1
  114. package/dist/db/schema/job-executions.d.ts +3 -3
  115. package/dist/db/schema/job-executions.d.ts.map +1 -1
  116. package/dist/db/schema/job-executions.js +8 -4
  117. package/dist/db/schema/job-executions.js.map +1 -1
  118. package/dist/db/schema/job-listener-events.d.ts +20 -0
  119. package/dist/db/schema/job-listener-events.d.ts.map +1 -1
  120. package/dist/db/schema/job-listener-events.js +2 -0
  121. package/dist/db/schema/job-listener-events.js.map +1 -1
  122. package/dist/db/schema/step-attempts.js +2 -2
  123. package/dist/db/schema/step-attempts.js.map +1 -1
  124. package/dist/db/schema/steps.d.ts +4 -2
  125. package/dist/db/schema/steps.d.ts.map +1 -1
  126. package/dist/db/schema/steps.js +1 -1
  127. package/dist/db/schema/steps.js.map +1 -1
  128. package/dist/db/schema/workflow-run-attempts.d.ts +21 -2
  129. package/dist/db/schema/workflow-run-attempts.d.ts.map +1 -1
  130. package/dist/db/schema/workflow-run-attempts.js +2 -0
  131. package/dist/db/schema/workflow-run-attempts.js.map +1 -1
  132. package/dist/db/schema/workflow-run-counters.d.ts +43 -0
  133. package/dist/db/schema/workflow-run-counters.d.ts.map +1 -0
  134. package/dist/db/schema/workflow-run-counters.js +10 -0
  135. package/dist/db/schema/workflow-run-counters.js.map +1 -0
  136. package/dist/db/schema/workflow-runs.d.ts +54 -1
  137. package/dist/db/schema/workflow-runs.d.ts.map +1 -1
  138. package/dist/db/schema/workflow-runs.js +10 -2
  139. package/dist/db/schema/workflow-runs.js.map +1 -1
  140. package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
  141. package/dist/db/workflow-runs/job-executions.js +35 -14
  142. package/dist/db/workflow-runs/job-executions.js.map +1 -1
  143. package/dist/db/workflow-runs/jobs.d.ts +2 -0
  144. package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
  145. package/dist/db/workflow-runs/jobs.js +9 -5
  146. package/dist/db/workflow-runs/jobs.js.map +1 -1
  147. package/dist/db/workflow-runs/queries.d.ts +51 -0
  148. package/dist/db/workflow-runs/queries.d.ts.map +1 -1
  149. package/dist/db/workflow-runs/queries.js +82 -2
  150. package/dist/db/workflow-runs/queries.js.map +1 -1
  151. package/dist/db/workflow-runs/run-create.d.ts +1 -0
  152. package/dist/db/workflow-runs/run-create.d.ts.map +1 -1
  153. package/dist/db/workflow-runs/run-create.js +159 -32
  154. package/dist/db/workflow-runs/run-create.js.map +1 -1
  155. package/dist/db/workflow-runs/run-rerun.d.ts.map +1 -1
  156. package/dist/db/workflow-runs/run-rerun.js +13 -5
  157. package/dist/db/workflow-runs/run-rerun.js.map +1 -1
  158. package/dist/db/workflow-runs/shared.d.ts +1 -0
  159. package/dist/db/workflow-runs/shared.d.ts.map +1 -1
  160. package/dist/db/workflow-runs/shared.js +2 -1
  161. package/dist/db/workflow-runs/shared.js.map +1 -1
  162. package/dist/db/workflow-runs/steps.js +3 -3
  163. package/dist/db/workflow-runs/steps.js.map +1 -1
  164. package/dist/db/workflow-runs.d.ts +4 -3
  165. package/dist/db/workflow-runs.d.ts.map +1 -1
  166. package/dist/db/workflow-runs.js +3 -2
  167. package/dist/db/workflow-runs.js.map +1 -1
  168. package/dist/metrics/instance.d.ts +1 -0
  169. package/dist/metrics/instance.d.ts.map +1 -1
  170. package/dist/metrics/instance.js +9 -0
  171. package/dist/metrics/instance.js.map +1 -1
  172. package/dist/presentation/dto/checkout-token.d.ts +1 -0
  173. package/dist/presentation/dto/checkout-token.d.ts.map +1 -1
  174. package/dist/presentation/dto/checkout-token.js +1 -0
  175. package/dist/presentation/dto/checkout-token.js.map +1 -1
  176. package/dist/presentation/dto/index.d.ts +1 -1
  177. package/dist/presentation/dto/index.d.ts.map +1 -1
  178. package/dist/presentation/dto/index.js +1 -1
  179. package/dist/presentation/dto/index.js.map +1 -1
  180. package/dist/presentation/dto/step.d.ts.map +1 -1
  181. package/dist/presentation/dto/step.js +2 -2
  182. package/dist/presentation/dto/step.js.map +1 -1
  183. package/dist/presentation/dto/workflow-run.d.ts +3 -1
  184. package/dist/presentation/dto/workflow-run.d.ts.map +1 -1
  185. package/dist/presentation/dto/workflow-run.js +34 -0
  186. package/dist/presentation/dto/workflow-run.js.map +1 -1
  187. package/dist/presentation/inter-module.d.ts.map +1 -1
  188. package/dist/presentation/inter-module.js +31 -3
  189. package/dist/presentation/inter-module.js.map +1 -1
  190. package/dist/presentation/routes/checkout-token.d.ts +1 -1
  191. package/dist/presentation/routes/checkout-token.d.ts.map +1 -1
  192. package/dist/presentation/routes/checkout-token.js +32 -20
  193. package/dist/presentation/routes/checkout-token.js.map +1 -1
  194. package/dist/presentation/routes/leased-step.d.ts +2 -0
  195. package/dist/presentation/routes/leased-step.d.ts.map +1 -1
  196. package/dist/presentation/routes/leased-step.js +2 -1
  197. package/dist/presentation/routes/leased-step.js.map +1 -1
  198. package/dist/presentation/routes/list-runs.d.ts.map +1 -1
  199. package/dist/presentation/routes/list-runs.js +9 -3
  200. package/dist/presentation/routes/list-runs.js.map +1 -1
  201. package/dist/presentation/routes/project-access.d.ts +1 -0
  202. package/dist/presentation/routes/project-access.d.ts.map +1 -1
  203. package/dist/presentation/routes/report-step.d.ts.map +1 -1
  204. package/dist/presentation/routes/report-step.js +9 -2
  205. package/dist/presentation/routes/report-step.js.map +1 -1
  206. package/dist/presentation/routes/require-accessible-run.d.ts.map +1 -1
  207. package/dist/presentation/routes/require-accessible-run.js +1 -1
  208. package/dist/presentation/routes/require-accessible-run.js.map +1 -1
  209. package/dist/presentation/subscribers/on-job-steps-settled.js +1 -1
  210. package/dist/presentation/subscribers/on-job-steps-settled.js.map +1 -1
  211. package/dist/presentation/subscribers/on-runner-job-claimed.d.ts.map +1 -1
  212. package/dist/presentation/subscribers/on-runner-job-claimed.js +21 -0
  213. package/dist/presentation/subscribers/on-runner-job-claimed.js.map +1 -1
  214. package/dist/temporal/activities/index.d.ts.map +1 -1
  215. package/dist/temporal/activities/index.js +2 -1
  216. package/dist/temporal/activities/index.js.map +1 -1
  217. package/dist/temporal/activities/orchestration-activities.d.ts +1 -0
  218. package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
  219. package/dist/temporal/activities/orchestration-activities.js.map +1 -1
  220. package/dist/temporal/constants.d.ts +1 -0
  221. package/dist/temporal/constants.d.ts.map +1 -1
  222. package/dist/temporal/constants.js +1 -0
  223. package/dist/temporal/constants.js.map +1 -1
  224. package/dist/temporal/index.d.ts +1 -1
  225. package/dist/temporal/index.d.ts.map +1 -1
  226. package/dist/temporal/index.js +1 -1
  227. package/dist/temporal/index.js.map +1 -1
  228. package/dist/temporal/workflows/index.bundle.js +82 -43
  229. package/dist/temporal/workflows/index.d.ts +1 -1
  230. package/dist/temporal/workflows/index.d.ts.map +1 -1
  231. package/dist/temporal/workflows/index.js +1 -1
  232. package/dist/temporal/workflows/index.js.map +1 -1
  233. package/dist/temporal/workflows/job-execution-orchestration.d.ts +5 -0
  234. package/dist/temporal/workflows/job-execution-orchestration.d.ts.map +1 -1
  235. package/dist/temporal/workflows/job-execution-orchestration.js +77 -43
  236. package/dist/temporal/workflows/job-execution-orchestration.js.map +1 -1
  237. package/dist/temporal/workflows/test-env.d.ts +5 -1
  238. package/dist/temporal/workflows/test-env.d.ts.map +1 -1
  239. package/dist/temporal/workflows/test-env.js +32 -11
  240. package/dist/temporal/workflows/test-env.js.map +1 -1
  241. package/dist/tsconfig.test.tsbuildinfo +1 -1
  242. package/drizzle/0000_initial.sql +14 -2
  243. package/drizzle/0001_windy_sunfire.sql +1 -0
  244. package/drizzle/meta/0000_snapshot.json +93 -1
  245. package/drizzle/meta/0001_snapshot.json +1711 -0
  246. package/drizzle/meta/_journal.json +2 -2
  247. package/package.json +21 -21
  248. package/src/core/agent-defaults.ts +2 -1
  249. package/src/core/checkout.test.ts +378 -67
  250. package/src/core/checkout.ts +142 -59
  251. package/src/core/entities/job-execution.test.ts +21 -0
  252. package/src/core/entities/job-execution.ts +24 -0
  253. package/src/core/entities/job-listener-event.ts +3 -0
  254. package/src/core/entities/step.ts +15 -3
  255. package/src/core/entities/workflow-run-attempt.ts +1 -0
  256. package/src/core/entities/workflow-run.ts +16 -0
  257. package/src/core/errors.ts +21 -7
  258. package/src/core/job-execution.ts +27 -4
  259. package/src/core/job-transition/decide-job-activation.test.ts +4 -0
  260. package/src/core/job-transition/decide-job-activation.ts +2 -0
  261. package/src/core/job-transition/derive-job-success.test.ts +12 -0
  262. package/src/core/job-transition/derive-job-success.ts +6 -9
  263. package/src/core/listener-execution-materialization.test.ts +2 -0
  264. package/src/core/listener-execution-materialization.ts +57 -11
  265. package/src/core/resolve-trigger-reference.test.ts +160 -0
  266. package/src/core/resolve-trigger-reference.ts +53 -0
  267. package/src/core/run-workflow.ts +2 -0
  268. package/src/core/step-config/agent.ts +29 -12
  269. package/src/core/step-config/assemble-run-context.test.ts +149 -6
  270. package/src/core/step-config/assemble-run-context.ts +87 -25
  271. package/src/core/step-config/complete-step-dispatch-config.test.ts +125 -2
  272. package/src/core/step-config/complete-step-dispatch-config.ts +83 -1
  273. package/src/core/step-config/index.ts +7 -0
  274. package/src/core/step-config/materialize-job-execution-steps.test.ts +250 -1
  275. package/src/core/step-config/materialize-job-execution-steps.ts +57 -10
  276. package/src/core/step-config/materialize-workflow-model.test.ts +212 -4
  277. package/src/core/step-config/materialize-workflow-model.ts +33 -28
  278. package/src/core/step-config/resolve-job-execution-name.test.ts +37 -15
  279. package/src/core/step-config/resolve-job-execution-name.ts +30 -15
  280. package/src/core/step-config/resolve-step-config.ts +175 -10
  281. package/src/core/step-config/resolve-workflow-run-name.test.ts +115 -0
  282. package/src/core/step-config/resolve-workflow-run-name.ts +108 -0
  283. package/src/core/step-transition/decide-step-transition.ts +2 -2
  284. package/src/core/step-transition/evaluate-gate.test.ts +8 -0
  285. package/src/core/step-transition/evaluate-gate.ts +8 -1
  286. package/src/core/workflow-run-creation.test.ts +7 -4
  287. package/src/core/workflow-run-creation.ts +13 -7
  288. package/src/db/db.ts +2 -0
  289. package/src/db/index.ts +6 -0
  290. package/src/db/job-listener-events.test.ts +19 -0
  291. package/src/db/job-listener-events.ts +3 -0
  292. package/src/db/job-listeners.test.ts +187 -5
  293. package/src/db/job-listeners.ts +122 -30
  294. package/src/db/schema/job-executions.ts +12 -5
  295. package/src/db/schema/job-listener-events.ts +3 -0
  296. package/src/db/schema/step-attempts.ts +2 -2
  297. package/src/db/schema/steps.ts +2 -1
  298. package/src/db/schema/workflow-run-attempts.ts +2 -0
  299. package/src/db/schema/workflow-run-counters.ts +13 -0
  300. package/src/db/schema/workflow-runs.ts +11 -2
  301. package/src/db/workflow-runs/job-executions.ts +37 -15
  302. package/src/db/workflow-runs/jobs.ts +17 -4
  303. package/src/db/workflow-runs/queries.test.ts +206 -0
  304. package/src/db/workflow-runs/queries.ts +146 -2
  305. package/src/db/workflow-runs/run-create.test.ts +606 -8
  306. package/src/db/workflow-runs/run-create.ts +185 -40
  307. package/src/db/workflow-runs/run-rerun.test.ts +61 -2
  308. package/src/db/workflow-runs/run-rerun.ts +15 -5
  309. package/src/db/workflow-runs/shared.ts +2 -0
  310. package/src/db/workflow-runs/steps.ts +3 -3
  311. package/src/db/workflow-runs.ts +7 -0
  312. package/src/metrics/instance.ts +14 -0
  313. package/src/presentation/dto/checkout-token.test.ts +19 -9
  314. package/src/presentation/dto/checkout-token.ts +2 -1
  315. package/src/presentation/dto/index.ts +1 -1
  316. package/src/presentation/dto/job.test.ts +9 -0
  317. package/src/presentation/dto/step.test.ts +12 -0
  318. package/src/presentation/dto/step.ts +5 -2
  319. package/src/presentation/dto/workflow-run.ts +44 -2
  320. package/src/presentation/inter-module.test.ts +165 -0
  321. package/src/presentation/inter-module.ts +38 -4
  322. package/src/presentation/routes/cancel-run.test.ts +1 -1
  323. package/src/presentation/routes/checkout-token.test.ts +357 -142
  324. package/src/presentation/routes/checkout-token.ts +56 -26
  325. package/src/presentation/routes/get-run-aggregates.test.ts +1 -1
  326. package/src/presentation/routes/get-run.test.ts +29 -1
  327. package/src/presentation/routes/get-step-secrets.test.ts +7 -2
  328. package/src/presentation/routes/leased-step.ts +9 -1
  329. package/src/presentation/routes/list-run-attempts.test.ts +1 -1
  330. package/src/presentation/routes/list-runs.test.ts +58 -1
  331. package/src/presentation/routes/list-runs.ts +8 -3
  332. package/src/presentation/routes/report-step.test.ts +25 -0
  333. package/src/presentation/routes/report-step.ts +10 -2
  334. package/src/presentation/routes/require-accessible-run.ts +4 -1
  335. package/src/presentation/routes/rerun-run.test.ts +1 -1
  336. package/src/presentation/subscribers/on-job-steps-settled.ts +1 -1
  337. package/src/presentation/subscribers/on-runner-job-claimed.test.ts +49 -0
  338. package/src/presentation/subscribers/on-runner-job-claimed.ts +22 -0
  339. package/src/temporal/activities/index.ts +1 -0
  340. package/src/temporal/activities/orchestration-activities.ts +1 -0
  341. package/src/temporal/constants.ts +1 -0
  342. package/src/temporal/index.ts +1 -0
  343. package/src/temporal/workflows/index.ts +1 -0
  344. package/src/temporal/workflows/job-execution-orchestration.test.ts +142 -6
  345. package/src/temporal/workflows/job-execution-orchestration.ts +99 -50
  346. package/src/temporal/workflows/run-orchestration.test.ts +1 -1
  347. package/src/temporal/workflows/test-env.ts +50 -13
  348. package/test/factories/project.ts +2 -0
  349. package/test/factories/workflow-model.ts +80 -26
  350. package/test/factories/workflow-run.ts +4 -1
  351. package/test/fixtures/agent-inter-module.ts +2 -1
  352. package/test/fixtures/job-with-steps.ts +1 -1
  353. package/tsconfig.build.tsbuildinfo +1 -1
  354. package/drizzle/0001_agent_tool_materialization.sql +0 -1
@@ -67,6 +67,21 @@ function finalStatusesFor(jobId: string): string[] {
67
67
  .map((c) => c.params.status);
68
68
  }
69
69
 
70
+ async function waitForExecutionStatus(jobExecutionId: string, status: string): Promise<void> {
71
+ for (let attempt = 0; attempt < 40; attempt += 1) {
72
+ if (
73
+ setExecutionStatusCalls().some(
74
+ (call) => call.params.jobExecutionId === jobExecutionId && call.params.status === status,
75
+ )
76
+ ) {
77
+ return;
78
+ }
79
+ await new Promise((resolve) => setTimeout(resolve, 50));
80
+ }
81
+
82
+ throw new Error(`Timed out waiting for ${jobExecutionId} to reach ${status}`);
83
+ }
84
+
70
85
  function terminalSetJobCall(jobId: string) {
71
86
  return setExecutionStatusCalls().find(
72
87
  (call) => call.params.jobExecutionId === jobId && call.params.status !== 'running',
@@ -92,7 +107,7 @@ async function expectEmptyRequiredLabelsFailure(input: typeof defaultJobInput):
92
107
  }
93
108
 
94
109
  describe('jobExecutionOrchestration', () => {
95
- test('finished signal (succeeded) flips status and releases the lease', async () => {
110
+ test('runner claim flips status to running, then finished releases the lease', async () => {
96
111
  setCfg({
97
112
  dag: makeDag([dagJob('job-1', 'build')]),
98
113
  jobResults: new Map([['job-1', 'succeeded']]),
@@ -107,6 +122,109 @@ describe('jobExecutionOrchestration', () => {
107
122
  expect(callsNamed('bulkSetStepStatuses')).toHaveLength(0);
108
123
  });
109
124
 
125
+ test('keeps an execution pending until the current runner claim', async () => {
126
+ setCfg({dag: makeDag([]), jobResults: new Map(), skipSignal: true});
127
+
128
+ const handle = await testEnv.client.workflow.start('jobExecutionOrchestration', {
129
+ taskQueue: TASK_QUEUE,
130
+ workflowId: 'job:job-pending-before-claim',
131
+ args: [
132
+ {
133
+ ...defaultJobInput,
134
+ jobId: 'job-pending-before-claim',
135
+ jobExecutionId: 'job-pending-before-claim',
136
+ },
137
+ ],
138
+ });
139
+
140
+ await new Promise((resolve) => setTimeout(resolve, 100));
141
+ expect(setExecutionStatusCalls()).toHaveLength(0);
142
+
143
+ await handle.signal('job-claimed', {
144
+ jobExecutionId: 'job-pending-before-claim',
145
+ claimedAt: '2026-06-22T10:05:00.000Z',
146
+ });
147
+ await waitForExecutionStatus('job-pending-before-claim', 'running');
148
+ await handle.signal('job-finished', {
149
+ jobExecutionId: 'job-pending-before-claim',
150
+ status: 'succeeded',
151
+ });
152
+
153
+ const result = await handle.result();
154
+ expect(result.status).toBe('succeeded');
155
+ expect(finalStatusesFor('job-pending-before-claim')).toEqual(['running', 'succeeded']);
156
+ });
157
+
158
+ test('ignores stale and duplicate claims and transitions exactly once', async () => {
159
+ setCfg({dag: makeDag([]), jobResults: new Map(), skipSignal: true});
160
+
161
+ const handle = await testEnv.client.workflow.start('jobExecutionOrchestration', {
162
+ taskQueue: TASK_QUEUE,
163
+ workflowId: 'job:job-claim-idempotent',
164
+ args: [
165
+ {
166
+ ...defaultJobInput,
167
+ jobId: 'job-claim-idempotent',
168
+ jobExecutionId: 'execution-current',
169
+ },
170
+ ],
171
+ });
172
+
173
+ await handle.signal('job-claimed', {
174
+ jobExecutionId: 'execution-stale',
175
+ claimedAt: '2026-06-22T10:04:00.000Z',
176
+ });
177
+ await new Promise((resolve) => setTimeout(resolve, 100));
178
+ expect(setExecutionStatusCalls()).toHaveLength(0);
179
+
180
+ const claim = {
181
+ jobExecutionId: 'execution-current',
182
+ claimedAt: '2026-06-22T10:05:00.000Z',
183
+ };
184
+ await handle.signal('job-claimed', claim);
185
+ await handle.signal('job-claimed', {...claim, claimedAt: '2026-06-22T10:06:00.000Z'});
186
+ await new Promise((resolve) => setTimeout(resolve, 100));
187
+ await handle.signal('job-finished', {
188
+ jobExecutionId: 'execution-current',
189
+ status: 'succeeded',
190
+ });
191
+
192
+ const result = await handle.result();
193
+ expect(result.status).toBe('succeeded');
194
+ expect(finalStatusesFor('execution-current')).toEqual(['running', 'succeeded']);
195
+ });
196
+
197
+ test('a finish before a delayed claim remains terminal', async () => {
198
+ setCfg({dag: makeDag([]), jobResults: new Map(), skipSignal: true});
199
+
200
+ const handle = await testEnv.client.workflow.start('jobExecutionOrchestration', {
201
+ taskQueue: TASK_QUEUE,
202
+ workflowId: 'job:job-finish-before-claim',
203
+ args: [
204
+ {
205
+ ...defaultJobInput,
206
+ jobId: 'job-finish-before-claim',
207
+ jobExecutionId: 'job-finish-before-claim',
208
+ },
209
+ ],
210
+ });
211
+
212
+ await handle.signal('job-finished', {
213
+ jobExecutionId: 'job-finish-before-claim',
214
+ status: 'succeeded',
215
+ });
216
+ const result = await handle.result();
217
+
218
+ expect(result.status).toBe('succeeded');
219
+ expect(finalStatusesFor('job-finish-before-claim')).toEqual(['succeeded']);
220
+ await expect(
221
+ handle.signal('job-claimed', {
222
+ jobExecutionId: 'job-finish-before-claim',
223
+ claimedAt: '2026-06-22T10:05:00.000Z',
224
+ }),
225
+ ).rejects.toThrow();
226
+ });
227
+
110
228
  test('empty required labels fail before the job is marked running', async () => {
111
229
  setCfg({
112
230
  dag: makeDag([dagJob('job-empty-labels', 'build')]),
@@ -272,7 +390,7 @@ describe('jobExecutionOrchestration', () => {
272
390
  ]);
273
391
  }, 15_000);
274
392
 
275
- test('already-terminal job is not enqueued', async () => {
393
+ test('already-terminal job is not reopened after a claim', async () => {
276
394
  setCfg({
277
395
  dag: makeDag([]),
278
396
  jobResults: new Map(),
@@ -282,7 +400,7 @@ describe('jobExecutionOrchestration', () => {
282
400
  const result = await executeJob({...defaultJobInput, jobId: 'job-cancelled'});
283
401
 
284
402
  expect(result).toMatchObject({status: 'cancelled'});
285
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(0);
403
+ expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(1);
286
404
  expect(callsNamed('releaseLeaseActivity')).toHaveLength(0);
287
405
  });
288
406
 
@@ -314,7 +432,7 @@ describe('jobExecutionOrchestration', () => {
314
432
 
315
433
  expect(result.status).toBe('failed');
316
434
  expect(callsNamed('failJobExecutionAsTimedOutActivity')).toHaveLength(1);
317
- expect(finalStatusesFor('job-timeout')).toEqual(['running']);
435
+ expect(finalStatusesFor('job-timeout')).toEqual([]);
318
436
  expect(callsNamed('bulkSetStepStatuses')).toContainEqual({
319
437
  name: 'bulkSetStepStatuses',
320
438
  params: {jobExecutionId: 'job-timeout', status: 'failed'},
@@ -323,6 +441,24 @@ describe('jobExecutionOrchestration', () => {
323
441
  expect(callsNamed('resolveLeaseExpiredJobExecutionActivity')).toHaveLength(0);
324
442
  });
325
443
 
444
+ test('does not reset the execution deadline after claim', async () => {
445
+ setCfg({
446
+ dag: makeDag([dagJob('job-claim-timeout', 'build')]),
447
+ jobResults: new Map(),
448
+ skipOutcomeSignal: true,
449
+ });
450
+
451
+ const result = await executeJob({
452
+ ...defaultJobInput,
453
+ jobId: 'job-claim-timeout',
454
+ executionTimeoutMs: 250,
455
+ });
456
+
457
+ expect(result.status).toBe('failed');
458
+ expect(finalStatusesFor('job-claim-timeout')).toEqual(['running']);
459
+ expect(callsNamed('failJobExecutionAsTimedOutActivity')).toHaveLength(1);
460
+ });
461
+
326
462
  test('surfaces a timeout activity failure', async () => {
327
463
  setCfg({
328
464
  dag: makeDag([dagJob('job-timeout-error', 'build')]),
@@ -339,10 +475,10 @@ describe('jobExecutionOrchestration', () => {
339
475
  }),
340
476
  ).rejects.toThrow();
341
477
 
342
- expect(finalStatusesFor('job-timeout-error')).toEqual(['running']);
478
+ expect(finalStatusesFor('job-timeout-error')).toEqual([]);
343
479
  });
344
480
 
345
- test('no signal workflow stays blocked indefinitely', async () => {
481
+ test('no signal: workflow stays blocked indefinitely', async () => {
346
482
  setCfg({dag: makeDag([]), jobResults: new Map(), skipSignal: true});
347
483
 
348
484
  const handle = await testEnv.client.workflow.start('jobExecutionOrchestration', {
@@ -9,25 +9,25 @@ import {
9
9
  import type {RuntimeCompletionStatus} from '#core/workflow-scheduling/runtime-dag.js';
10
10
 
11
11
  import type {createOrchestrationActivities} from '../activities/index.js';
12
- import {JOB_FINISHED_SIGNAL, JOB_LEASE_EXPIRED_SIGNAL} from '../constants.js';
12
+ import {JOB_CLAIMED_SIGNAL, JOB_FINISHED_SIGNAL, JOB_LEASE_EXPIRED_SIGNAL} from '../constants.js';
13
+ import {remainingMs} from './deadline.js';
13
14
 
14
15
  /**
15
- * Two signals, one precedence ladder, then best-effort lease release:
16
+ * Enqueue, wait for the runner claim, then wait for one of the terminal facts:
16
17
  *
17
- * job-finished (recordStepResult exhausted the steps) ─┐
18
- * job-lease-expired (runner heartbeat went stale) ─────┤
19
- *
20
- * condition(finished ∥ leaseExpired, executionTimeout)
21
- * ├─ finished → setJobExecutionStatus(status) + resolve job status ─┐
22
- * ├─ leaseExpired → resolveLeaseExpiredJobExecutionActivity ─┤→ releaseLease
23
- * └─ neither (6h) → failJobExecutionAsTimedOutActivity + sweep steps (NO release; the
24
- * TIMED_OUT event drives cooperative cancel, the detector reaps)
18
+ * enqueue ──> PENDING ── job-claimed ──> RUNNING
19
+ * │ │
20
+ * │ timeout ├─ job-finished
21
+ * │ ├─ job-lease-expired
22
+ * ▼ └─ timeout
23
+ * TERMINAL
25
24
  *
26
- * Both signals can be delivered before condition() resumes (independent outboxes,
27
- * no ordering), so finished is evaluated FIRST: a genuinely-finished job is never
28
- * flipped to failed by a late lease-expiry. releaseLease is best-effort a runners
29
- * DB outage must never block the child workflow from returning the job outcome to
30
- * run-orchestration; a leftover lease row is reaped by the stuck detector.
25
+ * The claim signal is the runner-owned lifecycle boundary. The workflow keeps the
26
+ * execution pending while it is queued, then performs the versioned pending → running
27
+ * transition after the current claim. One deadline spans both waits; claim never resets
28
+ * it. Signals can arrive in any order, so the precedence is finished > lease expired >
29
+ * claimed > timeout. Lease cleanup remains best-effort: a runners DB outage must never
30
+ * block the child workflow from returning the job outcome to run-orchestration.
31
31
  */
32
32
 
33
33
  const {
@@ -64,6 +64,11 @@ export const jobFinishedSignal =
64
64
  );
65
65
  export const jobLeaseExpiredSignal =
66
66
  defineSignal<[{jobExecutionId?: string | undefined}]>(JOB_LEASE_EXPIRED_SIGNAL);
67
+ export interface JobClaimedSignalPayload {
68
+ jobExecutionId: string;
69
+ claimedAt: string;
70
+ }
71
+ export const jobClaimedSignal = defineSignal<[JobClaimedSignalPayload]>(JOB_CLAIMED_SIGNAL);
67
72
 
68
73
  export interface JobExecutionOrchestrationInput {
69
74
  workspaceId: string;
@@ -116,12 +121,7 @@ async function resolveJobStatusOrFailClosed(
116
121
  }
117
122
  }
118
123
 
119
- async function markJobExecutionRunningAndEnqueue(
120
- input: JobExecutionOrchestrationInput,
121
- ): Promise<
122
- | {kind: 'running'; runningVersion: number}
123
- | {kind: 'terminal'; result: JobExecutionOrchestrationResult}
124
- > {
124
+ async function enqueueJobExecution(input: JobExecutionOrchestrationInput): Promise<void> {
125
125
  if (hasNoRequiredRunnerLabels(input.requiredLabels)) {
126
126
  throw ApplicationFailure.nonRetryable(
127
127
  `Job ${input.jobId} has no required runner labels`,
@@ -129,15 +129,6 @@ async function markJobExecutionRunningAndEnqueue(
129
129
  );
130
130
  }
131
131
 
132
- const {newVersion: runningVersion, status} = await setJobExecutionStatus({
133
- jobExecutionId: input.jobExecutionId,
134
- status: 'running',
135
- version: input.executionVersion,
136
- });
137
-
138
- const start = jobExecutionStartOutcome({newVersion: runningVersion, status});
139
- if (start.kind === 'terminal') return start;
140
-
141
132
  await enqueueJobExecutionForRunner({
142
133
  workspaceId: input.workspaceId,
143
134
  workflowRunId: input.workflowRunId,
@@ -147,30 +138,59 @@ async function markJobExecutionRunningAndEnqueue(
147
138
  projectId: input.projectId,
148
139
  requiredLabels: input.requiredLabels,
149
140
  });
141
+ }
142
+
143
+ async function markJobExecutionRunning(
144
+ input: JobExecutionOrchestrationInput,
145
+ ): Promise<
146
+ | {kind: 'running'; runningVersion: number}
147
+ | {kind: 'terminal'; result: JobExecutionOrchestrationResult}
148
+ > {
149
+ const {newVersion: runningVersion, status} = await setJobExecutionStatus({
150
+ jobExecutionId: input.jobExecutionId,
151
+ status: 'running',
152
+ version: input.executionVersion,
153
+ });
150
154
 
155
+ const start = jobExecutionStartOutcome({newVersion: runningVersion, status});
156
+ if (start.kind === 'terminal') return start;
151
157
  return {kind: 'running', runningVersion};
152
158
  }
153
159
 
154
- // Both signals can arrive before condition() resumes (independent outboxes, no
155
- // ordering), so callers must evaluate `finished` before `leaseExpired`.
156
- async function awaitJobOutcome(
160
+ interface JobExecutionSignals extends JobExecutionOutcomeSignals {
161
+ claimed: JobClaimedSignalPayload | undefined;
162
+ }
163
+
164
+ function registerJobExecutionSignalHandlers(
157
165
  jobExecutionId: string,
158
- timeoutMs: number,
159
- ): Promise<JobExecutionOutcomeSignals> {
160
- let finished: {status: RuntimeCompletionStatus; jobExecutionId?: string | undefined} | undefined;
161
- let leaseExpired = false;
166
+ signals: JobExecutionSignals,
167
+ ): void {
162
168
  setHandler(jobFinishedSignal, (payload) => {
163
169
  if (payload.jobExecutionId !== undefined && payload.jobExecutionId !== jobExecutionId) return;
164
- finished ??= payload;
170
+ signals.finished ??= payload;
165
171
  });
166
172
  setHandler(jobLeaseExpiredSignal, (payload = {}) => {
167
173
  if (payload.jobExecutionId !== undefined && payload.jobExecutionId !== jobExecutionId) return;
168
- leaseExpired = true;
174
+ signals.leaseExpired = true;
169
175
  });
176
+ setHandler(jobClaimedSignal, (payload) => {
177
+ if (payload.jobExecutionId !== jobExecutionId) return;
178
+ signals.claimed ??= payload;
179
+ });
180
+ }
170
181
 
171
- await condition(() => finished !== undefined || leaseExpired, timeoutMs);
172
-
173
- return {finished, leaseExpired};
182
+ async function waitForJobExecutionSignal(
183
+ signals: JobExecutionSignals,
184
+ deadline: number,
185
+ includeClaim: boolean,
186
+ ): Promise<void> {
187
+ await condition(
188
+ () =>
189
+ signals.finished !== undefined ||
190
+ signals.leaseExpired ||
191
+ (includeClaim && signals.claimed !== undefined),
192
+ remainingMs(deadline) ?? 0,
193
+ );
174
194
  }
175
195
 
176
196
  interface JobExecutionResolution {
@@ -286,7 +306,41 @@ async function resolveTimedOutJobExecution({
286
306
  export async function jobExecutionOrchestration(
287
307
  input: JobExecutionOrchestrationInput,
288
308
  ): Promise<JobExecutionOrchestrationResult> {
289
- const running = await markJobExecutionRunningAndEnqueue(input);
309
+ const signals: JobExecutionSignals = {
310
+ finished: undefined,
311
+ leaseExpired: false,
312
+ claimed: undefined,
313
+ };
314
+ // Register every signal before enqueue can block or publish a claim/outcome event. The
315
+ // handlers retain signals that arrive while the enqueue activity is in flight.
316
+ registerJobExecutionSignalHandlers(input.jobExecutionId, signals);
317
+ await enqueueJobExecution(input);
318
+
319
+ const timeoutMs = input.executionTimeoutMs ?? DEFAULT_EXECUTION_MAX_DURATION_MS;
320
+ const deadline = Date.now() + timeoutMs;
321
+ await waitForJobExecutionSignal(signals, deadline, true);
322
+
323
+ // A terminal fact wins over claim even when both signals arrive before the condition
324
+ // resumes. This prevents a late claim from reopening an execution that already finished.
325
+ let resolution = resolveJobExecutionOutcomeSignal(signals);
326
+ if (resolution === 'finished') {
327
+ const {finished} = signals;
328
+ if (finished === undefined) throw new Error('Missing finished signal for finished resolution');
329
+
330
+ return resolveFinishedJobExecution({
331
+ input,
332
+ runningVersion: input.executionVersion,
333
+ status: finished.status,
334
+ });
335
+ }
336
+ if (resolution === 'lease-expired') {
337
+ return resolveLeaseExpiredJobExecution({input, runningVersion: input.executionVersion});
338
+ }
339
+ if (!signals.claimed) {
340
+ return resolveTimedOutJobExecution({input, runningVersion: input.executionVersion});
341
+ }
342
+
343
+ const running = await markJobExecutionRunning(input);
290
344
  if (running.kind === 'terminal') {
291
345
  if (input.resolveJobStatus === false) {
292
346
  return {status: running.result.status, jobVersion: input.jobVersion};
@@ -295,16 +349,11 @@ export async function jobExecutionOrchestration(
295
349
  }
296
350
  const {runningVersion} = running;
297
351
 
298
- const timeoutMs = input.executionTimeoutMs ?? DEFAULT_EXECUTION_MAX_DURATION_MS;
299
- const signals = await awaitJobOutcome(input.jobExecutionId, timeoutMs);
300
-
301
- // Precedence ladder: a genuinely-finished job is never flipped to failed by a
302
- // late lease-expiry, so `finished` wins over `leaseExpired`.
303
- const resolution = resolveJobExecutionOutcomeSignal(signals);
352
+ await waitForJobExecutionSignal(signals, deadline, false);
353
+ resolution = resolveJobExecutionOutcomeSignal(signals);
304
354
  if (resolution === 'finished') {
305
355
  const {finished} = signals;
306
356
  if (finished === undefined) throw new Error('Missing finished signal for finished resolution');
307
-
308
357
  return resolveFinishedJobExecution({input, runningVersion, status: finished.status});
309
358
  }
310
359
  if (resolution === 'lease-expired') {
@@ -382,7 +382,7 @@ describe('runOrchestration', () => {
382
382
  const runAttemptStatuses = setRunAttemptStatusCalls().map((c) => c.params.status);
383
383
  expect(runAttemptStatuses).toEqual(['running', 'failed']);
384
384
 
385
- // A, B, C enqueued D skipped because B failed
385
+ // A, B, C enqueued: D skipped because B failed
386
386
  expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(3);
387
387
  const jobStatuses = setJobStatusCalls().map((c) => ({
388
388
  id: c.params.jobId,
@@ -9,7 +9,7 @@ import type {
9
9
  ListenerBufferPeek,
10
10
  } from '#db/job-listeners.js';
11
11
  import type {RunDag} from '../activities/orchestration-activities.js';
12
- import {JOB_FINISHED_SIGNAL, JOB_LEASE_EXPIRED_SIGNAL} from '../constants.js';
12
+ import {JOB_CLAIMED_SIGNAL, JOB_FINISHED_SIGNAL, JOB_LEASE_EXPIRED_SIGNAL} from '../constants.js';
13
13
 
14
14
  const TASK_QUEUE = 'test-orchestration';
15
15
  const WORKFLOWS_PATH = resolve(import.meta.dirname, '../../../dist/temporal/workflows/index.js');
@@ -17,7 +17,7 @@ const WORKFLOWS_PATH = resolve(import.meta.dirname, '../../../dist/temporal/work
17
17
  export {TASK_QUEUE};
18
18
 
19
19
  // ---------------------------------------------------------------------------
20
- // Shared mutable test config each test sets this before starting a workflow.
20
+ // Shared mutable test config: each test sets this before starting a workflow.
21
21
  // Activities read from it via closure. Tests run serially within a describe.
22
22
  // ---------------------------------------------------------------------------
23
23
 
@@ -29,8 +29,12 @@ export interface TestConfig {
29
29
  enqueueError?: string;
30
30
  /** If true, enqueueJobExecutionForRunner sends two job-finished signals (for dedup testing) */
31
31
  duplicateSignal?: boolean;
32
- /** If true, enqueueJobExecutionForRunner does nothing (no signal for timeout testing) */
32
+ /** If true, enqueueJobExecutionForRunner does nothing without a signal for timeout testing */
33
33
  skipSignal?: boolean;
34
+ /** If true, emit the claim but hold back the terminal outcome (for deadline testing) */
35
+ skipOutcomeSignal?: boolean;
36
+ /** Runner-owned claim timestamp used by the mock claim event */
37
+ claimedAt?: string;
34
38
  /** If true, signal job-lease-expired instead of job-finished */
35
39
  signalLeaseExpired?: boolean;
36
40
  /** If true, signal BOTH job-finished and job-lease-expired (precedence testing) */
@@ -73,6 +77,14 @@ export interface ActivityCall {
73
77
 
74
78
  export let calls: ActivityCall[];
75
79
  let versionSeq: number;
80
+ const pendingJobOutcomes = new Map<
81
+ string,
82
+ {
83
+ handle: ReturnType<typeof testEnv.client.workflow.getHandle>;
84
+ status: RuntimeCompletionStatus;
85
+ duplicateSignal: boolean;
86
+ }
87
+ >();
76
88
 
77
89
  function nextVersion(): number {
78
90
  return ++versionSeq;
@@ -81,6 +93,7 @@ function nextVersion(): number {
81
93
  export function resetCalls(): void {
82
94
  calls = [];
83
95
  versionSeq = 0;
96
+ pendingJobOutcomes.clear();
84
97
  }
85
98
 
86
99
  export function callsNamed(name: string): ActivityCall[] {
@@ -192,7 +205,7 @@ export async function setupEnv(): Promise<void> {
192
205
  activities: createMockActivities(),
193
206
  });
194
207
 
195
- // Run worker in background it processes tasks until shutdown is called
208
+ // Run worker in background: it processes tasks until shutdown is called
196
209
  workerRunPromise = worker.run();
197
210
  workerRunPromise.catch(() => {
198
211
  // Suppress unhandled rejection on shutdown
@@ -242,7 +255,7 @@ function createMockActivities() {
242
255
  });
243
256
  },
244
257
 
245
- setJobExecutionStatus: (params: {
258
+ setJobExecutionStatus: async (params: {
246
259
  jobExecutionId: string;
247
260
  status: string;
248
261
  version: number;
@@ -251,6 +264,24 @@ function createMockActivities() {
251
264
  calls.push({name: 'setJobExecutionStatus', params});
252
265
  const status =
253
266
  params.status === 'running' && cfg.runningJobStatus ? cfg.runningJobStatus : params.status;
267
+
268
+ if (params.status === 'running') {
269
+ const outcome = pendingJobOutcomes.get(params.jobExecutionId);
270
+ pendingJobOutcomes.delete(params.jobExecutionId);
271
+ if (outcome && status === 'running') {
272
+ await outcome.handle.signal(JOB_FINISHED_SIGNAL, {
273
+ status: outcome.status,
274
+ jobExecutionId: params.jobExecutionId,
275
+ });
276
+ if (outcome.duplicateSignal) {
277
+ await outcome.handle.signal(JOB_FINISHED_SIGNAL, {
278
+ status: 'failed',
279
+ jobExecutionId: params.jobExecutionId,
280
+ });
281
+ }
282
+ }
283
+ }
284
+
254
285
  return {newVersion: nextVersion(), status};
255
286
  },
256
287
 
@@ -285,6 +316,12 @@ function createMockActivities() {
285
316
 
286
317
  const status = cfg.jobResults.get(params.jobId) ?? 'succeeded';
287
318
  const handle = testEnv.client.workflow.getHandle(`job:${params.jobId}`);
319
+ await handle.signal(JOB_CLAIMED_SIGNAL, {
320
+ jobExecutionId: params.jobExecutionId,
321
+ claimedAt: cfg.claimedAt ?? '2026-06-22T10:05:00.000Z',
322
+ });
323
+
324
+ if (cfg.skipOutcomeSignal) return;
288
325
 
289
326
  if (cfg.signalLeaseExpired) {
290
327
  await handle.signal(JOB_LEASE_EXPIRED_SIGNAL, {jobExecutionId: params.jobExecutionId});
@@ -297,14 +334,14 @@ function createMockActivities() {
297
334
  return;
298
335
  }
299
336
 
300
- await handle.signal(JOB_FINISHED_SIGNAL, {status, jobExecutionId: params.jobExecutionId});
301
-
302
- if (cfg.duplicateSignal) {
303
- await handle.signal(JOB_FINISHED_SIGNAL, {
304
- status: 'failed',
305
- jobExecutionId: params.jobExecutionId,
306
- });
307
- }
337
+ // The terminal outbox is independent from the claim, but the mock delivers it from the
338
+ // pending → running activity. This gives the workflow a deterministic ordering boundary
339
+ // without leaving a polling promise alive after the test finishes.
340
+ pendingJobOutcomes.set(params.jobExecutionId, {
341
+ handle,
342
+ status,
343
+ duplicateSignal: cfg.duplicateSignal ?? false,
344
+ });
308
345
  },
309
346
 
310
347
  resolveLeaseExpiredJobExecutionActivity: (params: {
@@ -5,6 +5,7 @@ interface Project {
5
5
  workspaceId: string;
6
6
  sourceConnectionId: string;
7
7
  sourceExternalRepositoryId: string;
8
+ sourceRepositoryOwner: string | null;
8
9
  name: string;
9
10
  createdAt: Date;
10
11
  updatedAt: Date;
@@ -15,6 +16,7 @@ export const projectFactory = Factory.define<Project>(({sequence}) => ({
15
16
  workspaceId: crypto.randomUUID(),
16
17
  sourceConnectionId: crypto.randomUUID(),
17
18
  sourceExternalRepositoryId: `acme/repo-${sequence}`,
19
+ sourceRepositoryOwner: 'acme',
18
20
  name: `Project ${sequence}`,
19
21
  createdAt: new Date(),
20
22
  updatedAt: new Date(),