@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
@@ -23,6 +23,7 @@ export const workflowRunAttempts = pgTable(
23
23
  rerunMode: workflowRunRerunModeEnum('rerun_mode'),
24
24
  rerunByUserId: uuid('rerun_by_user_id'),
25
25
  model: jsonb('model').$type<WorkflowModel | WorkflowModelSnapshot>(),
26
+ vars: jsonb('vars').$type<Record<string, string>>(),
26
27
  agentToolMaterialization: jsonb(
27
28
  'agent_tool_materialization',
28
29
  ).$type<AgentToolMaterializationSnapshot>(),
@@ -53,6 +54,7 @@ export function toWorkflowRunAttempt(row: WorkflowRunAttemptDb): WorkflowRunAtte
53
54
  rerunMode: row.rerunMode ?? null,
54
55
  rerunByUserId: row.rerunByUserId,
55
56
  model: row.model === null ? null : readPersistedWorkflowModel(row.model),
57
+ vars: row.vars ?? null,
56
58
  agentToolMaterialization: row.agentToolMaterialization ?? null,
57
59
  version: row.version,
58
60
  createdAt: row.createdAt,
@@ -0,0 +1,13 @@
1
+ import {integer, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
2
+ import {pgTable} from './common.js';
3
+
4
+ export const workflowRunCounters = pgTable(
5
+ 'workflow_run_counters',
6
+ {
7
+ definitionId: uuid('definition_id').notNull(),
8
+ nextNumber: integer('next_number').notNull(),
9
+ },
10
+ (table) => [uniqueIndex('workflows_wrrc_definition_id_unique').on(table.definitionId)],
11
+ );
12
+
13
+ export type WorkflowRunCounterDb = typeof workflowRunCounters.$inferSelect;
@@ -16,6 +16,7 @@ import {
16
16
  import type {
17
17
  TriggerPayload,
18
18
  WorkflowRun,
19
+ WorkflowRunTriggerReference,
19
20
  WorkflowSourceSnapshot,
20
21
  } from '#core/entities/workflow-run.js';
21
22
  import {pgTable} from './common.js';
@@ -37,13 +38,16 @@ export const workflowRuns = pgTable(
37
38
  workspaceId: uuid('workspace_id').notNull(),
38
39
  projectId: uuid('project_id').notNull(),
39
40
  definitionId: uuid('definition_id').notNull(),
40
- name: text('name').notNull(),
41
+ number: integer('number').notNull(),
42
+ name: text('name'),
43
+ workflowName: text('workflow_name').notNull(),
41
44
  status: workflowRunStatusEnum('status').notNull().default('pending'),
42
45
  currentAttempt: integer('current_attempt').notNull().default(1),
43
46
  triggerProvider: text('trigger_provider'),
44
47
  triggerSource: text('trigger_source').notNull(),
45
48
  triggerEvent: text('trigger_event').notNull(),
46
49
  triggerPayload: jsonb('trigger_payload').notNull().$type<TriggerPayload>(),
50
+ triggerReference: jsonb('trigger_reference').$type<WorkflowRunTriggerReference>(),
47
51
  inputs: jsonb('inputs').$type<Record<string, unknown>>(),
48
52
  sourceSnapshot: jsonb('source_snapshot').$type<WorkflowSourceSnapshot>(),
49
53
  triggerIdempotencyKey: text('trigger_idempotency_key'),
@@ -56,6 +60,7 @@ export const workflowRuns = pgTable(
56
60
  },
57
61
  (table) => [
58
62
  uniqueIndex('workflows_wr_trigger_idempotency_key_unique').on(table.triggerIdempotencyKey),
63
+ uniqueIndex('workflows_wr_definition_number_unique').on(table.definitionId, table.number),
59
64
  index('workflows_wr_project_created_id_idx').on(table.projectId, table.createdAt, table.id),
60
65
  index('workflows_wr_project_status_created_id_idx').on(
61
66
  table.projectId,
@@ -92,13 +97,17 @@ export function toWorkflowRun(row: WorkflowRunDb): WorkflowRun {
92
97
  workspaceId: row.workspaceId,
93
98
  projectId: row.projectId,
94
99
  definitionId: row.definitionId,
95
- name: row.name,
100
+ number: row.number,
101
+ name: row.name ?? row.workflowName,
102
+ workflowName: row.workflowName,
103
+ nameOverride: row.name,
96
104
  status: row.status,
97
105
  currentAttempt: row.currentAttempt,
98
106
  triggerProvider: row.triggerProvider,
99
107
  triggerSource: row.triggerSource,
100
108
  triggerEvent: row.triggerEvent,
101
109
  triggerPayload: row.triggerPayload as TriggerPayload,
110
+ triggerReference: row.triggerReference ?? null,
102
111
  inputs: row.inputs ?? null,
103
112
  sourceSnapshot: row.sourceSnapshot ?? null,
104
113
  triggerIdempotencyKey: row.triggerIdempotencyKey,
@@ -32,35 +32,48 @@ import {
32
32
  } from './shared.js';
33
33
  import {bulkUpdateStepStatuses, getStepsByJobExecutionIdForUpdate} from './steps.js';
34
34
 
35
+ async function getJobExecutionFallbackName(tx: Tx, jobExecutionId: string): Promise<string> {
36
+ const [row] = await tx
37
+ .select({job: jobs})
38
+ .from(jobExecutions)
39
+ .innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
40
+ .where(eq(jobExecutions.id, jobExecutionId))
41
+ .limit(1);
42
+ if (!row) throw new JobNotFoundError(jobExecutionId);
43
+ return row.job.name ?? row.job.key;
44
+ }
45
+
35
46
  export async function getJobExecutionById(id: string, tx?: Tx): Promise<JobExecution | undefined> {
36
47
  const rows = await (tx ?? db())
37
- .select()
48
+ .select({jobExecution: jobExecutions, job: jobs})
38
49
  .from(jobExecutions)
50
+ .innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
39
51
  .where(eq(jobExecutions.id, id))
40
52
  .limit(1);
41
53
  const row = rows[0];
42
- return row ? toJobExecution(row) : undefined;
54
+ return row ? toJobExecution(row.jobExecution, row.job.name ?? row.job.key) : undefined;
43
55
  }
44
56
 
45
57
  export async function getJobExecutionsByWorkflowRunAttemptId(
46
58
  workflowRunAttemptId: string,
47
59
  ): Promise<JobExecution[]> {
48
60
  const rows = await db()
49
- .select({jobExecution: jobExecutions})
61
+ .select({jobExecution: jobExecutions, job: jobs})
50
62
  .from(jobExecutions)
51
63
  .innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
52
64
  .where(eq(jobs.workflowRunAttemptId, workflowRunAttemptId))
53
65
  .orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
54
- return rows.map((row) => toJobExecution(row.jobExecution));
66
+ return rows.map((row) => toJobExecution(row.jobExecution, row.job.name ?? row.job.key));
55
67
  }
56
68
 
57
69
  export async function getJobExecutionsByJobId(jobId: string): Promise<JobExecution[]> {
58
70
  const rows = await db()
59
- .select()
71
+ .select({jobExecution: jobExecutions, job: jobs})
60
72
  .from(jobExecutions)
73
+ .innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
61
74
  .where(eq(jobExecutions.jobId, jobId))
62
75
  .orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
63
- return rows.map(toJobExecution);
76
+ return rows.map((row) => toJobExecution(row.jobExecution, row.job.name ?? row.job.key));
64
77
  }
65
78
 
66
79
  export async function getFirstJobExecutionByJobId(
@@ -68,13 +81,14 @@ export async function getFirstJobExecutionByJobId(
68
81
  tx?: Tx,
69
82
  ): Promise<JobExecution | undefined> {
70
83
  const rows = await (tx ?? db())
71
- .select()
84
+ .select({jobExecution: jobExecutions, job: jobs})
72
85
  .from(jobExecutions)
86
+ .innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
73
87
  .where(eq(jobExecutions.jobId, jobId))
74
88
  .orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id))
75
89
  .limit(1);
76
90
  const row = rows[0];
77
- return row ? toJobExecution(row) : undefined;
91
+ return row ? toJobExecution(row.jobExecution, row.job.name ?? row.job.key) : undefined;
78
92
  }
79
93
 
80
94
  export async function getLatestJobExecutionByJobId(
@@ -82,13 +96,14 @@ export async function getLatestJobExecutionByJobId(
82
96
  tx?: Tx,
83
97
  ): Promise<JobExecution | undefined> {
84
98
  const rows = await (tx ?? db())
85
- .select()
99
+ .select({jobExecution: jobExecutions, job: jobs})
86
100
  .from(jobExecutions)
101
+ .innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
87
102
  .where(eq(jobExecutions.jobId, jobId))
88
103
  .orderBy(desc(jobExecutions.sequence), desc(jobExecutions.id))
89
104
  .limit(1);
90
105
  const row = rows[0];
91
- return row ? toJobExecution(row) : undefined;
106
+ return row ? toJobExecution(row.jobExecution, row.job.name ?? row.job.key) : undefined;
92
107
  }
93
108
 
94
109
  export interface UpdateJobExecutionStatusAtVersionParams {
@@ -129,10 +144,14 @@ async function resolveJobExecutionOutputs(
129
144
  .from(jobExecutions)
130
145
  .where(eq(jobExecutions.jobId, target.job.id))
131
146
  .orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
147
+ const fallbackName = target.job.name ?? target.job.key;
132
148
  const executions = executionRows.map((row) =>
133
149
  row.id === target.execution.id
134
- ? toJobExecution({...row, status: params.status, statusReason: params.statusReason})
135
- : toJobExecution(row),
150
+ ? toJobExecution(
151
+ {...row, status: params.status, statusReason: params.statusReason},
152
+ fallbackName,
153
+ )
154
+ : toJobExecution(row, fallbackName),
136
155
  );
137
156
  const jobExecution = executions.find((execution) => execution.id === target.execution.id);
138
157
  if (!jobExecution) throw new JobNotFoundError(params.jobExecutionId);
@@ -217,7 +236,10 @@ async function updateJobExecutionStatusAtVersion(
217
236
 
218
237
  const row = rows[0];
219
238
  if (!row) return null;
220
- return {execution: toJobExecution(row), changed: true};
239
+ return {
240
+ execution: toJobExecution(row, await getJobExecutionFallbackName(tx, row.id)),
241
+ changed: true,
242
+ };
221
243
  }
222
244
 
223
245
  export interface UpdateJobExecutionStatusParams {
@@ -250,7 +272,7 @@ export async function updateJobExecutionStatus(
250
272
  .where(eq(jobExecutions.id, params.jobExecutionId))
251
273
  .limit(1)
252
274
  )[0];
253
- return row ? toJobExecution(row) : undefined;
275
+ return row ? toJobExecution(row, await getJobExecutionFallbackName(tx, row.id)) : undefined;
254
276
  },
255
277
  matchFn: (execution) =>
256
278
  (execution.status === params.status && execution.statusReason === statusReason) ||
@@ -317,7 +339,7 @@ export async function failJobExecutionAsTimedOut(params: {
317
339
  .where(eq(jobExecutions.id, params.jobExecutionId))
318
340
  .limit(1)
319
341
  )[0];
320
- return row ? toJobExecution(row) : undefined;
342
+ return row ? toJobExecution(row, await getJobExecutionFallbackName(tx, row.id)) : undefined;
321
343
  },
322
344
  matchFn: (execution) => (execution.timedOutAt !== null ? {execution, changed: false} : null),
323
345
  failureMessage: `Optimistic lock failure: job execution ${params.jobExecutionId} version ${params.expectedVersion}`,
@@ -4,6 +4,7 @@ import {and, asc, desc, eq, inArray, notInArray, sql} from 'drizzle-orm';
4
4
  import {isJobTerminal, type Job, type JobStatus, type JobStatusReason} from '#core/entities/job.js';
5
5
  import type {JobExecution} from '#core/entities/job-execution.js';
6
6
  import type {PersistedEvaluationTraceEntry} from '#core/entities/step.js';
7
+ import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
7
8
  import {JobNotFoundError} from '#core/errors.js';
8
9
  import {
9
10
  type DeriveJobSuccessResult,
@@ -58,11 +59,16 @@ export async function getJobById(id: string): Promise<Job | undefined> {
58
59
  export interface JobScope {
59
60
  workspaceId: string;
60
61
  projectId: string;
62
+ triggerReference: WorkflowRunTriggerReference | null;
61
63
  }
62
64
 
63
65
  export async function getJobScope(jobId: string): Promise<JobScope | undefined> {
64
66
  const rows = await db()
65
- .select({workspaceId: workflowRuns.workspaceId, projectId: workflowRuns.projectId})
67
+ .select({
68
+ workspaceId: workflowRuns.workspaceId,
69
+ projectId: workflowRuns.projectId,
70
+ triggerReference: workflowRuns.triggerReference,
71
+ })
66
72
  .from(jobs)
67
73
  .innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
68
74
  .innerJoin(workflowRuns, eq(workflowRunAttempts.workflowRunId, workflowRuns.id))
@@ -98,7 +104,8 @@ export async function getDirectDependencyJobContexts(
98
104
  context = {job: toJob(row.job), executions: []};
99
105
  contextsByJobId.set(row.job.id, context);
100
106
  }
101
- if (row.execution) context.executions.push(toJobExecution(row.execution));
107
+ if (row.execution)
108
+ context.executions.push(toJobExecution(row.execution, row.job.name ?? row.job.key));
102
109
  }
103
110
 
104
111
  return [...contextsByJobId.values()];
@@ -169,6 +176,7 @@ export async function evaluateJobActivations(
169
176
  run: toWorkflowRun(target.run),
170
177
  job,
171
178
  condition: modelJob?.if,
179
+ vars: target.attempt.vars ?? undefined,
172
180
  dependencies: job.dependencies.flatMap((key) => {
173
181
  const dependency = contextsByJobKey.get(key);
174
182
  return dependency === undefined ? [] : [dependency];
@@ -249,7 +257,8 @@ async function directDependencyContextsByJobKey(
249
257
  context = {job: toJob(row.job), executions: []};
250
258
  contextsByJobKey.set(row.job.key, context);
251
259
  }
252
- if (row.execution) context.executions.push(toJobExecution(row.execution));
260
+ if (row.execution)
261
+ context.executions.push(toJobExecution(row.execution, row.job.name ?? row.job.key));
253
262
  }
254
263
 
255
264
  return contextsByJobKey;
@@ -383,6 +392,7 @@ export async function resolveJobStatusFromJobExecutions(params: {
383
392
  .from(jobExecutions)
384
393
  .where(eq(jobExecutions.jobId, params.jobId))
385
394
  .orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
395
+ const workflowContext = await getWorkflowContextForJob(params.jobId, tx);
386
396
 
387
397
  if (jobExecutionRows.length === 0) {
388
398
  throw new Error(`Cannot resolve job ${params.jobId}: no job executions found`);
@@ -390,8 +400,11 @@ export async function resolveJobStatusFromJobExecutions(params: {
390
400
 
391
401
  const {status, statusReason, trace} = evaluateJobSuccess({
392
402
  success: jobRow.success,
393
- executions: jobExecutionRows.map(toJobExecution),
403
+ executions: jobExecutionRows.map((execution) =>
404
+ toJobExecution(execution, jobRow.name ?? jobRow.key),
405
+ ),
394
406
  jobs: await getDirectDependencyJobContexts(params.jobId, tx),
407
+ vars: workflowContext.vars ?? undefined,
395
408
  });
396
409
 
397
410
  return optimisticLockRetry({
@@ -1,3 +1,4 @@
1
+ import {WORKFLOW_RUN_JOB_PREVIEW_LIMIT} from '@shipfox/api-workflows-dto';
1
2
  import {buildModel, createTestRun} from '#test/helpers/workflow-runs.js';
2
3
  import {
3
4
  createRerunWorkflowRun,
@@ -8,8 +9,10 @@ import {
8
9
  getWorkflowJobExecutionDepth,
9
10
  getWorkflowRunById,
10
11
  listRunAttempts,
12
+ listWorkflowRunJobSummaries,
11
13
  listWorkflowRunsByProject,
12
14
  updateJobExecutionStatus,
15
+ updateJobStatus,
13
16
  updateWorkflowRunStatus,
14
17
  } from '../workflow-runs.js';
15
18
 
@@ -136,6 +139,178 @@ describe('workflow run queries', () => {
136
139
  });
137
140
  });
138
141
 
142
+ describe('listWorkflowRunJobSummaries', () => {
143
+ test('returns jobs in graph order keyed by run, for many runs at once', async () => {
144
+ const model = buildModel({
145
+ jobs: {
146
+ build: {steps: [{run: 'echo build'}]},
147
+ test: {needs: 'build', steps: [{run: 'echo test'}]},
148
+ },
149
+ });
150
+ const first = await createWorkflowRun({
151
+ workspaceId,
152
+ projectId,
153
+ definitionId,
154
+ model,
155
+ triggerPayload: manualTrigger(),
156
+ });
157
+ const second = await createWorkflowRun({
158
+ workspaceId,
159
+ projectId,
160
+ definitionId,
161
+ model,
162
+ triggerPayload: manualTrigger(),
163
+ });
164
+
165
+ const summaries = await listWorkflowRunJobSummaries([
166
+ {id: first.id, currentAttempt: first.currentAttempt},
167
+ {id: second.id, currentAttempt: second.currentAttempt},
168
+ ]);
169
+
170
+ expect(summaries.get(first.id)?.preview.map((job) => job.key)).toEqual(['build', 'test']);
171
+ expect(summaries.get(second.id)?.preview.map((job) => job.key)).toEqual(['build', 'test']);
172
+ });
173
+
174
+ test('scopes jobs to the current attempt so a re-run does not stack both attempts', async () => {
175
+ const source = await createWorkflowRun({
176
+ workspaceId,
177
+ projectId,
178
+ definitionId,
179
+ model: buildModel({jobs: {build: {steps: [{run: 'echo build'}]}}}),
180
+ triggerPayload: manualTrigger(),
181
+ });
182
+ const [firstAttemptJob] = await getJobsByWorkflowRunId(source.id);
183
+ await updateWorkflowRunStatus({
184
+ workflowRunId: source.id,
185
+ status: 'failed',
186
+ expectedVersion: 1,
187
+ });
188
+ const rerun = await createRerunWorkflowRun({
189
+ workflowRunId: source.id,
190
+ mode: 'all',
191
+ actorUserId: crypto.randomUUID(),
192
+ });
193
+
194
+ const summaries = await listWorkflowRunJobSummaries([
195
+ {id: source.id, currentAttempt: rerun.currentAttempt},
196
+ ]);
197
+
198
+ expect(rerun.currentAttempt).toBe(2);
199
+ expect(summaries.get(source.id)?.preview.map((job) => job.key)).toEqual(['build']);
200
+ expect(summaries.get(source.id)?.preview[0]?.id).not.toBe(firstAttemptJob?.id);
201
+ });
202
+
203
+ // The caller pins the attempt, so a re-run landing after its read still yields the jobs
204
+ // that belong to the metadata it is about to render, not whichever attempt is newest.
205
+ test('returns the caller requested attempt, not whichever is current now', async () => {
206
+ const source = await createWorkflowRun({
207
+ workspaceId,
208
+ projectId,
209
+ definitionId,
210
+ model: buildModel({jobs: {build: {steps: [{run: 'echo build'}]}}}),
211
+ triggerPayload: manualTrigger(),
212
+ });
213
+ const [firstAttemptJob] = await getJobsByWorkflowRunId(source.id);
214
+ await updateWorkflowRunStatus({
215
+ workflowRunId: source.id,
216
+ status: 'failed',
217
+ expectedVersion: 1,
218
+ });
219
+ await createRerunWorkflowRun({
220
+ workflowRunId: source.id,
221
+ mode: 'all',
222
+ actorUserId: crypto.randomUUID(),
223
+ });
224
+
225
+ const summaries = await listWorkflowRunJobSummaries([{id: source.id, currentAttempt: 1}]);
226
+
227
+ expect(summaries.get(source.id)?.preview[0]?.id).toBe(firstAttemptJob?.id);
228
+ });
229
+
230
+ // The row draws a bounded strip, so the query must not hand back one row per job of a
231
+ // workflow that has no job limit; everything past the preview is described by counts.
232
+ test('caps the preview and still counts every job past it', async () => {
233
+ const jobCount = WORKFLOW_RUN_JOB_PREVIEW_LIMIT + 9;
234
+ const run = await createWorkflowRun({
235
+ workspaceId,
236
+ projectId,
237
+ definitionId,
238
+ model: buildModel({
239
+ jobs: Object.fromEntries(
240
+ Array.from({length: jobCount}, (_, index) => [
241
+ `job-${String(index).padStart(2, '0')}`,
242
+ {steps: [{run: `echo ${index}`}]},
243
+ ]),
244
+ ),
245
+ }),
246
+ triggerPayload: manualTrigger(),
247
+ });
248
+
249
+ const summary = await listWorkflowRunJobSummaries([
250
+ {id: run.id, currentAttempt: run.currentAttempt},
251
+ ]);
252
+
253
+ expect(summary.get(run.id)?.preview).toHaveLength(WORKFLOW_RUN_JOB_PREVIEW_LIMIT);
254
+ expect(totalOf(summary.get(run.id))).toBe(jobCount);
255
+ });
256
+
257
+ test('counts each status the run holds, not only the previewed ones', async () => {
258
+ const run = await createWorkflowRun({
259
+ workspaceId,
260
+ projectId,
261
+ definitionId,
262
+ model: buildModel({
263
+ jobs: {build: {steps: [{run: 'echo build'}]}, test: {steps: [{run: 'echo test'}]}},
264
+ }),
265
+ triggerPayload: manualTrigger(),
266
+ });
267
+
268
+ const summary = await listWorkflowRunJobSummaries([
269
+ {id: run.id, currentAttempt: run.currentAttempt},
270
+ ]);
271
+
272
+ expect(summary.get(run.id)?.statusCounts).toEqual([{status: 'pending', count: 2}]);
273
+ });
274
+
275
+ // Checks the invariant the snapshot exists to protect: for a run inside the preview
276
+ // bound, the statuses drawn and the statuses counted describe the same jobs and must
277
+ // agree exactly. The read races a commit to give the anomaly a chance to appear, so this
278
+ // samples rather than proves; the guarantee itself comes from the isolation level, which
279
+ // no assertion here can force an interleaving against.
280
+ test('agrees between preview and counts while a job settles underneath', async () => {
281
+ const run = await createWorkflowRun({
282
+ workspaceId,
283
+ projectId,
284
+ definitionId,
285
+ model: buildModel({
286
+ jobs: {build: {steps: [{run: 'echo build'}]}, test: {steps: [{run: 'echo test'}]}},
287
+ }),
288
+ triggerPayload: manualTrigger(),
289
+ });
290
+ const [firstJob] = await getJobsByWorkflowRunId(run.id);
291
+ if (!firstJob) throw new Error('expected the run to have jobs');
292
+
293
+ // Commits a status change while the read is in flight. A snapshot taken per statement
294
+ // would let one half of the answer see it and the other half miss it.
295
+ const settleMidRead = updateJobStatus({
296
+ jobId: firstJob.id,
297
+ status: 'failed',
298
+ expectedVersion: firstJob.version,
299
+ });
300
+ const [summaries] = await Promise.all([
301
+ listWorkflowRunJobSummaries([{id: run.id, currentAttempt: run.currentAttempt}]),
302
+ settleMidRead,
303
+ ]);
304
+
305
+ const summary = summaries.get(run.id);
306
+ expect(previewCounts(summary)).toEqual(statusCountMap(summary));
307
+ });
308
+
309
+ test('returns an empty map without querying for an empty page', async () => {
310
+ await expect(listWorkflowRunJobSummaries([])).resolves.toEqual(new Map());
311
+ });
312
+ });
313
+
139
314
  describe('listWorkflowRunsByProject', () => {
140
315
  test('returns runs ordered by creation descending', async () => {
141
316
  await createWorkflowRun({
@@ -254,3 +429,34 @@ describe('workflow run queries', () => {
254
429
  });
255
430
  });
256
431
  });
432
+
433
+ function manualTrigger() {
434
+ return {
435
+ source: 'manual',
436
+ event: 'fire',
437
+ subscriptionId: crypto.randomUUID(),
438
+ userId: crypto.randomUUID(),
439
+ } as const;
440
+ }
441
+
442
+ function totalOf(summary: {statusCounts: Array<{count: number}>} | undefined): number {
443
+ return (summary?.statusCounts ?? []).reduce((total, entry) => total + entry.count, 0);
444
+ }
445
+
446
+ /** Statuses the preview actually drew, counted. */
447
+ function previewCounts(
448
+ summary: {preview: Array<{status: string}>} | undefined,
449
+ ): Record<string, number> {
450
+ const counts: Record<string, number> = {};
451
+ for (const job of summary?.preview ?? []) counts[job.status] = (counts[job.status] ?? 0) + 1;
452
+ return counts;
453
+ }
454
+
455
+ /** The same shape read off the totals, so the two halves can be compared directly. */
456
+ function statusCountMap(
457
+ summary: {statusCounts: Array<{status: string; count: number}>} | undefined,
458
+ ): Record<string, number> {
459
+ return Object.fromEntries(
460
+ (summary?.statusCounts ?? []).map(({status, count}) => [status, count]),
461
+ );
462
+ }