@shipfox/client-workflows 0.2.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 (327) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +10 -0
  3. package/.storybook/preview.tsx +97 -0
  4. package/.swcrc +42 -0
  5. package/.turbo/turbo-build.log +2 -0
  6. package/.turbo/turbo-type$colon$emit.log +1 -0
  7. package/.turbo/turbo-type.log +1 -0
  8. package/CHANGELOG.md +162 -0
  9. package/LICENSE +21 -0
  10. package/dist/components/identifier/identifier.d.ts +6 -0
  11. package/dist/components/identifier/identifier.d.ts.map +1 -0
  12. package/dist/components/identifier/identifier.js +144 -0
  13. package/dist/components/identifier/identifier.js.map +1 -0
  14. package/dist/components/identifier/index.d.ts +2 -0
  15. package/dist/components/identifier/index.d.ts.map +1 -0
  16. package/dist/components/identifier/index.js +3 -0
  17. package/dist/components/identifier/index.js.map +1 -0
  18. package/dist/components/job-graph/graph-model.d.ts +27 -0
  19. package/dist/components/job-graph/graph-model.d.ts.map +1 -0
  20. package/dist/components/job-graph/graph-model.js +107 -0
  21. package/dist/components/job-graph/graph-model.js.map +1 -0
  22. package/dist/components/job-graph/index.d.ts +3 -0
  23. package/dist/components/job-graph/index.d.ts.map +1 -0
  24. package/dist/components/job-graph/index.js +3 -0
  25. package/dist/components/job-graph/index.js.map +1 -0
  26. package/dist/components/job-graph/job-duration-format.d.ts +4 -0
  27. package/dist/components/job-graph/job-duration-format.d.ts.map +1 -0
  28. package/dist/components/job-graph/job-duration-format.js +41 -0
  29. package/dist/components/job-graph/job-duration-format.js.map +1 -0
  30. package/dist/components/job-graph/job-duration-label.d.ts +5 -0
  31. package/dist/components/job-graph/job-duration-label.d.ts.map +1 -0
  32. package/dist/components/job-graph/job-duration-label.js +33 -0
  33. package/dist/components/job-graph/job-duration-label.js.map +1 -0
  34. package/dist/components/job-graph/job-graph-content.d.ts +9 -0
  35. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -0
  36. package/dist/components/job-graph/job-graph-content.js +181 -0
  37. package/dist/components/job-graph/job-graph-content.js.map +1 -0
  38. package/dist/components/job-graph/job-graph-view.d.ts +11 -0
  39. package/dist/components/job-graph/job-graph-view.d.ts.map +1 -0
  40. package/dist/components/job-graph/job-graph-view.js +24 -0
  41. package/dist/components/job-graph/job-graph-view.js.map +1 -0
  42. package/dist/components/job-graph/job-graph.d.ts +3 -0
  43. package/dist/components/job-graph/job-graph.d.ts.map +1 -0
  44. package/dist/components/job-graph/job-graph.js +21 -0
  45. package/dist/components/job-graph/job-graph.js.map +1 -0
  46. package/dist/components/job-graph/job-node.d.ts +16 -0
  47. package/dist/components/job-graph/job-node.d.ts.map +1 -0
  48. package/dist/components/job-graph/job-node.js +196 -0
  49. package/dist/components/job-graph/job-node.js.map +1 -0
  50. package/dist/components/job-graph/types.d.ts +9 -0
  51. package/dist/components/job-graph/types.d.ts.map +1 -0
  52. package/dist/components/job-graph/types.js +3 -0
  53. package/dist/components/job-graph/types.js.map +1 -0
  54. package/dist/components/step-list/index.d.ts +3 -0
  55. package/dist/components/step-list/index.d.ts.map +1 -0
  56. package/dist/components/step-list/index.js +3 -0
  57. package/dist/components/step-list/index.js.map +1 -0
  58. package/dist/components/step-list/step-list-model.d.ts +36 -0
  59. package/dist/components/step-list/step-list-model.d.ts.map +1 -0
  60. package/dist/components/step-list/step-list-model.js +135 -0
  61. package/dist/components/step-list/step-list-model.js.map +1 -0
  62. package/dist/components/step-list/step-list.d.ts +32 -0
  63. package/dist/components/step-list/step-list.d.ts.map +1 -0
  64. package/dist/components/step-list/step-list.js +409 -0
  65. package/dist/components/step-list/step-list.js.map +1 -0
  66. package/dist/components/workflow-run-duration-label.d.ts +8 -0
  67. package/dist/components/workflow-run-duration-label.d.ts.map +1 -0
  68. package/dist/components/workflow-run-duration-label.js +84 -0
  69. package/dist/components/workflow-run-duration-label.js.map +1 -0
  70. package/dist/components/workflow-run-list/index.d.ts +2 -0
  71. package/dist/components/workflow-run-list/index.d.ts.map +1 -0
  72. package/dist/components/workflow-run-list/index.js +3 -0
  73. package/dist/components/workflow-run-list/index.js.map +1 -0
  74. package/dist/components/workflow-run-list/run-display.d.ts +5 -0
  75. package/dist/components/workflow-run-list/run-display.d.ts.map +1 -0
  76. package/dist/components/workflow-run-list/run-display.js +19 -0
  77. package/dist/components/workflow-run-list/run-display.js.map +1 -0
  78. package/dist/components/workflow-run-list/types.d.ts +21 -0
  79. package/dist/components/workflow-run-list/types.d.ts.map +1 -0
  80. package/dist/components/workflow-run-list/types.js +3 -0
  81. package/dist/components/workflow-run-list/types.js.map +1 -0
  82. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +14 -0
  83. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -0
  84. package/dist/components/workflow-run-list/workflow-run-list-content.js +37 -0
  85. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -0
  86. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +10 -0
  87. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +1 -0
  88. package/dist/components/workflow-run-list/workflow-run-list-header.js +55 -0
  89. package/dist/components/workflow-run-list/workflow-run-list-header.js.map +1 -0
  90. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +15 -0
  91. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -0
  92. package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -0
  93. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -0
  94. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +3 -0
  95. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -0
  96. package/dist/components/workflow-run-list/workflow-run-list-view.js +46 -0
  97. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -0
  98. package/dist/components/workflow-run-list/workflow-run-list.d.ts +3 -0
  99. package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -0
  100. package/dist/components/workflow-run-list/workflow-run-list.js +20 -0
  101. package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -0
  102. package/dist/components/workflow-run-list/workflow-run-row.d.ts +14 -0
  103. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -0
  104. package/dist/components/workflow-run-list/workflow-run-row.js +172 -0
  105. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -0
  106. package/dist/components/workflow-run-summary/index.d.ts +2 -0
  107. package/dist/components/workflow-run-summary/index.d.ts.map +1 -0
  108. package/dist/components/workflow-run-summary/index.js +3 -0
  109. package/dist/components/workflow-run-summary/index.js.map +1 -0
  110. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +9 -0
  111. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -0
  112. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +138 -0
  113. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -0
  114. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +20 -0
  115. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -0
  116. package/dist/components/workflow-run-summary/workflow-run-summary.js +252 -0
  117. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -0
  118. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +7 -0
  119. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -0
  120. package/dist/components/workflow-run-view/agent-config-failure-callout.js +84 -0
  121. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -0
  122. package/dist/components/workflow-run-view/index.d.ts +3 -0
  123. package/dist/components/workflow-run-view/index.d.ts.map +1 -0
  124. package/dist/components/workflow-run-view/index.js +4 -0
  125. package/dist/components/workflow-run-view/index.js.map +1 -0
  126. package/dist/components/workflow-run-view/job-card.d.ts +17 -0
  127. package/dist/components/workflow-run-view/job-card.d.ts.map +1 -0
  128. package/dist/components/workflow-run-view/job-card.js +476 -0
  129. package/dist/components/workflow-run-view/job-card.js.map +1 -0
  130. package/dist/components/workflow-run-view/job-execution-switcher.d.ts +10 -0
  131. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -0
  132. package/dist/components/workflow-run-view/job-execution-switcher.js +171 -0
  133. package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -0
  134. package/dist/components/workflow-run-view/job-execution-time-text.d.ts +6 -0
  135. package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +1 -0
  136. package/dist/components/workflow-run-view/job-execution-time-text.js +24 -0
  137. package/dist/components/workflow-run-view/job-execution-time-text.js.map +1 -0
  138. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +9 -0
  139. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +1 -0
  140. package/dist/components/workflow-run-view/step-attempt-log-panel.js +152 -0
  141. package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +1 -0
  142. package/dist/components/workflow-run-view/workflow-run-selection.d.ts +14 -0
  143. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -0
  144. package/dist/components/workflow-run-view/workflow-run-selection.js +56 -0
  145. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -0
  146. package/dist/components/workflow-run-view/workflow-run-states.d.ts +12 -0
  147. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -0
  148. package/dist/components/workflow-run-view/workflow-run-states.js +91 -0
  149. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -0
  150. package/dist/components/workflow-run-view/workflow-run-view.d.ts +15 -0
  151. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -0
  152. package/dist/components/workflow-run-view/workflow-run-view.js +285 -0
  153. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -0
  154. package/dist/components/workflow-source-panel/index.d.ts +2 -0
  155. package/dist/components/workflow-source-panel/index.d.ts.map +1 -0
  156. package/dist/components/workflow-source-panel/index.js +3 -0
  157. package/dist/components/workflow-source-panel/index.js.map +1 -0
  158. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +13 -0
  159. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -0
  160. package/dist/components/workflow-source-panel/workflow-source-panel.js +91 -0
  161. package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -0
  162. package/dist/components/workflow-status/status-visuals.d.ts +12 -0
  163. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -0
  164. package/dist/components/workflow-status/status-visuals.js +54 -0
  165. package/dist/components/workflow-status/status-visuals.js.map +1 -0
  166. package/dist/components/workflow-status/workflow-status-icon.d.ts +22 -0
  167. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -0
  168. package/dist/components/workflow-status/workflow-status-icon.js +100 -0
  169. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -0
  170. package/dist/core/entities/job-execution.d.ts +69 -0
  171. package/dist/core/entities/job-execution.d.ts.map +1 -0
  172. package/dist/core/entities/job-execution.js +83 -0
  173. package/dist/core/entities/job-execution.js.map +1 -0
  174. package/dist/core/entities/job.d.ts +56 -0
  175. package/dist/core/entities/job.d.ts.map +1 -0
  176. package/dist/core/entities/job.js +71 -0
  177. package/dist/core/entities/job.js.map +1 -0
  178. package/dist/core/entities/step-attempt.d.ts +49 -0
  179. package/dist/core/entities/step-attempt.d.ts.map +1 -0
  180. package/dist/core/entities/step-attempt.js +48 -0
  181. package/dist/core/entities/step-attempt.js.map +1 -0
  182. package/dist/core/entities/step.d.ts +41 -0
  183. package/dist/core/entities/step.d.ts.map +1 -0
  184. package/dist/core/entities/step.js +51 -0
  185. package/dist/core/entities/step.js.map +1 -0
  186. package/dist/core/entities/workflow-run-attempt.d.ts +44 -0
  187. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -0
  188. package/dist/core/entities/workflow-run-attempt.js +51 -0
  189. package/dist/core/entities/workflow-run-attempt.js.map +1 -0
  190. package/dist/core/entities/workflow-run.d.ts +61 -0
  191. package/dist/core/entities/workflow-run.d.ts.map +1 -0
  192. package/dist/core/entities/workflow-run.js +104 -0
  193. package/dist/core/entities/workflow-run.js.map +1 -0
  194. package/dist/core/workflow-run-url-state.d.ts +13 -0
  195. package/dist/core/workflow-run-url-state.d.ts.map +1 -0
  196. package/dist/core/workflow-run-url-state.js +43 -0
  197. package/dist/core/workflow-run-url-state.js.map +1 -0
  198. package/dist/core/workflow-run.d.ts +13 -0
  199. package/dist/core/workflow-run.d.ts.map +1 -0
  200. package/dist/core/workflow-run.js +8 -0
  201. package/dist/core/workflow-run.js.map +1 -0
  202. package/dist/env.d.js +2 -0
  203. package/dist/env.d.js.map +1 -0
  204. package/dist/feature.d.ts +20 -0
  205. package/dist/feature.d.ts.map +1 -0
  206. package/dist/feature.js +27 -0
  207. package/dist/feature.js.map +1 -0
  208. package/dist/hooks/api/workflow-runs.d.ts +109 -0
  209. package/dist/hooks/api/workflow-runs.d.ts.map +1 -0
  210. package/dist/hooks/api/workflow-runs.js +394 -0
  211. package/dist/hooks/api/workflow-runs.js.map +1 -0
  212. package/dist/index.d.ts +5 -0
  213. package/dist/index.d.ts.map +1 -0
  214. package/dist/index.js +5 -0
  215. package/dist/index.js.map +1 -0
  216. package/dist/pages/workflow-run-first-time-use.d.ts +6 -0
  217. package/dist/pages/workflow-run-first-time-use.d.ts.map +1 -0
  218. package/dist/pages/workflow-run-first-time-use.js +17 -0
  219. package/dist/pages/workflow-run-first-time-use.js.map +1 -0
  220. package/dist/pages/workflow-run-page.d.ts +8 -0
  221. package/dist/pages/workflow-run-page.d.ts.map +1 -0
  222. package/dist/pages/workflow-run-page.js +83 -0
  223. package/dist/pages/workflow-run-page.js.map +1 -0
  224. package/dist/routes/run-detail.d.ts +8 -0
  225. package/dist/routes/run-detail.d.ts.map +1 -0
  226. package/dist/routes/run-detail.js +21 -0
  227. package/dist/routes/run-detail.js.map +1 -0
  228. package/dist/routes/runs.d.ts +8 -0
  229. package/dist/routes/runs.d.ts.map +1 -0
  230. package/dist/routes/runs.js +20 -0
  231. package/dist/routes/runs.js.map +1 -0
  232. package/dist/tsconfig.test.tsbuildinfo +1 -0
  233. package/package.json +114 -0
  234. package/src/components/identifier/identifier.tsx +178 -0
  235. package/src/components/identifier/index.ts +1 -0
  236. package/src/components/job-graph/graph-model.test.ts +189 -0
  237. package/src/components/job-graph/graph-model.ts +178 -0
  238. package/src/components/job-graph/index.ts +2 -0
  239. package/src/components/job-graph/job-duration-format.ts +54 -0
  240. package/src/components/job-graph/job-duration-label.tsx +29 -0
  241. package/src/components/job-graph/job-graph-content.tsx +225 -0
  242. package/src/components/job-graph/job-graph-view.test.tsx +298 -0
  243. package/src/components/job-graph/job-graph-view.tsx +45 -0
  244. package/src/components/job-graph/job-graph.stories.tsx +182 -0
  245. package/src/components/job-graph/job-graph.tsx +24 -0
  246. package/src/components/job-graph/job-node.stories.tsx +364 -0
  247. package/src/components/job-graph/job-node.test.tsx +347 -0
  248. package/src/components/job-graph/job-node.tsx +202 -0
  249. package/src/components/job-graph/types.ts +9 -0
  250. package/src/components/step-list/index.ts +6 -0
  251. package/src/components/step-list/step-list-model.test.ts +369 -0
  252. package/src/components/step-list/step-list-model.ts +205 -0
  253. package/src/components/step-list/step-list.stories.tsx +419 -0
  254. package/src/components/step-list/step-list.test.tsx +566 -0
  255. package/src/components/step-list/step-list.tsx +494 -0
  256. package/src/components/workflow-run-duration-label.tsx +118 -0
  257. package/src/components/workflow-run-list/index.ts +1 -0
  258. package/src/components/workflow-run-list/run-display.test.ts +53 -0
  259. package/src/components/workflow-run-list/run-display.ts +22 -0
  260. package/src/components/workflow-run-list/types.ts +22 -0
  261. package/src/components/workflow-run-list/workflow-run-list-content.tsx +58 -0
  262. package/src/components/workflow-run-list/workflow-run-list-header.tsx +53 -0
  263. package/src/components/workflow-run-list/workflow-run-list-states.tsx +86 -0
  264. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +178 -0
  265. package/src/components/workflow-run-list/workflow-run-list-view.tsx +57 -0
  266. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +111 -0
  267. package/src/components/workflow-run-list/workflow-run-list.tsx +29 -0
  268. package/src/components/workflow-run-list/workflow-run-row.tsx +176 -0
  269. package/src/components/workflow-run-summary/index.ts +1 -0
  270. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +230 -0
  271. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +146 -0
  272. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +414 -0
  273. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +354 -0
  274. package/src/components/workflow-run-summary/workflow-run-summary.tsx +295 -0
  275. package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +140 -0
  276. package/src/components/workflow-run-view/agent-config-failure-callout.tsx +93 -0
  277. package/src/components/workflow-run-view/index.ts +2 -0
  278. package/src/components/workflow-run-view/job-card.stories.tsx +788 -0
  279. package/src/components/workflow-run-view/job-card.tsx +566 -0
  280. package/src/components/workflow-run-view/job-execution-switcher.tsx +170 -0
  281. package/src/components/workflow-run-view/job-execution-time-text.tsx +34 -0
  282. package/src/components/workflow-run-view/step-attempt-log-panel.test.tsx +290 -0
  283. package/src/components/workflow-run-view/step-attempt-log-panel.tsx +152 -0
  284. package/src/components/workflow-run-view/workflow-run-selection.test.ts +280 -0
  285. package/src/components/workflow-run-view/workflow-run-selection.ts +89 -0
  286. package/src/components/workflow-run-view/workflow-run-states.tsx +70 -0
  287. package/src/components/workflow-run-view/workflow-run-view.test.tsx +1214 -0
  288. package/src/components/workflow-run-view/workflow-run-view.tsx +341 -0
  289. package/src/components/workflow-source-panel/index.ts +1 -0
  290. package/src/components/workflow-source-panel/workflow-source-panel.stories.tsx +110 -0
  291. package/src/components/workflow-source-panel/workflow-source-panel.test.tsx +96 -0
  292. package/src/components/workflow-source-panel/workflow-source-panel.tsx +136 -0
  293. package/src/components/workflow-status/status-visuals.test.ts +38 -0
  294. package/src/components/workflow-status/status-visuals.ts +36 -0
  295. package/src/components/workflow-status/workflow-status-icon.stories.tsx +90 -0
  296. package/src/components/workflow-status/workflow-status-icon.tsx +135 -0
  297. package/src/core/entities/job-execution.ts +139 -0
  298. package/src/core/entities/job.ts +149 -0
  299. package/src/core/entities/step-attempt.ts +89 -0
  300. package/src/core/entities/step.ts +105 -0
  301. package/src/core/entities/workflow-run-attempt.ts +87 -0
  302. package/src/core/entities/workflow-run.ts +183 -0
  303. package/src/core/workflow-run-url-state.ts +61 -0
  304. package/src/core/workflow-run.test.ts +640 -0
  305. package/src/core/workflow-run.ts +66 -0
  306. package/src/env.d.ts +8 -0
  307. package/src/feature.ts +26 -0
  308. package/src/hooks/api/workflow-runs.test.tsx +410 -0
  309. package/src/hooks/api/workflow-runs.ts +485 -0
  310. package/src/index.ts +45 -0
  311. package/src/page-harness-budget.test.ts +33 -0
  312. package/src/pages/workflow-run-first-time-use.tsx +17 -0
  313. package/src/pages/workflow-run-page.test.tsx +517 -0
  314. package/src/pages/workflow-run-page.tsx +89 -0
  315. package/src/routes/run-detail.tsx +15 -0
  316. package/src/routes/runs.tsx +11 -0
  317. package/test/fixtures/logs.ts +13 -0
  318. package/test/fixtures/workflow-run.ts +311 -0
  319. package/test/pages.tsx +79 -0
  320. package/test/render.tsx +40 -0
  321. package/test/setup.ts +3 -0
  322. package/tsconfig.build.json +9 -0
  323. package/tsconfig.build.tsbuildinfo +1 -0
  324. package/tsconfig.json +3 -0
  325. package/tsconfig.test.json +8 -0
  326. package/vercel.json +8 -0
  327. package/vitest.config.ts +70 -0
package/src/feature.ts ADDED
@@ -0,0 +1,26 @@
1
+ import {defineClientFeature} from '@shipfox/client-shell';
2
+
3
+ export const workflowsFeature = defineClientFeature({
4
+ id: 'shipfox.workflows',
5
+ routes: [
6
+ {
7
+ path: '/workspaces/$wid/projects/$pid/runs',
8
+ parent: 'projectLayout',
9
+ impl: '@shipfox/client-workflows/routes/runs',
10
+ },
11
+ {
12
+ path: '/workspaces/$wid/projects/$pid/runs/$workflowRunId',
13
+ parent: 'projectLayout',
14
+ impl: '@shipfox/client-workflows/routes/run-detail',
15
+ },
16
+ ],
17
+ navigation: [
18
+ {
19
+ id: 'nav.runs',
20
+ scope: 'project',
21
+ label: 'Runs',
22
+ to: '/workspaces/$wid/projects/$pid/runs',
23
+ order: 100,
24
+ },
25
+ ],
26
+ });
@@ -0,0 +1,410 @@
1
+ import type {
2
+ WorkflowRunDetailResponseDto,
3
+ WorkflowRunListResponseDto,
4
+ } from '@shipfox/api-workflows-dto';
5
+ import {configureApiClient} from '@shipfox/client-api';
6
+ import {type InfiniteData, QueryClient, QueryClientProvider} from '@tanstack/react-query';
7
+ import {act, cleanup, renderHook, waitFor} from '@testing-library/react';
8
+ import type {ReactNode} from 'react';
9
+ import {toWorkflowRun} from '#core/workflow-run.js';
10
+ import {
11
+ runAttemptsResponseDto,
12
+ workflowJobDto,
13
+ workflowRunAttemptDto,
14
+ workflowRunDetailDto,
15
+ workflowRunDto,
16
+ workflowRunListResponseDto,
17
+ } from '#test/fixtures/workflow-run.js';
18
+ import {
19
+ fireManualWorkflow,
20
+ useCancelWorkflowRunMutation,
21
+ useFireManualWorkflowMutation,
22
+ useRerunWorkflowRunMutation,
23
+ useWorkflowRunAttemptsQuery,
24
+ useWorkflowRunQuery,
25
+ useWorkflowRunsInfiniteQuery,
26
+ workflowRunsQueryKeys,
27
+ } from './workflow-runs.js';
28
+
29
+ const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
30
+ const DEFINITION_ID = '55555555-5555-4555-8555-555555555555';
31
+ const RUN_ID = '66666666-6666-4666-8666-666666666666';
32
+ const ROOT_RUN_ID = '77777777-7777-4777-8777-777777777777';
33
+ const TEMP_RUN_ID_PATTERN = /^temp-/;
34
+
35
+ function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
36
+ return new Response(JSON.stringify(body), {
37
+ headers: {'content-type': 'application/json'},
38
+ status: 200,
39
+ ...init,
40
+ });
41
+ }
42
+
43
+ function renderWithQueryClient<T>(callback: () => T) {
44
+ const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
45
+ const wrapper = ({children}: {children: ReactNode}) => (
46
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
47
+ );
48
+
49
+ return {queryClient, ...renderHook(callback, {wrapper})};
50
+ }
51
+
52
+ describe('workflow run API hooks', () => {
53
+ afterEach(() => {
54
+ cleanup();
55
+ configureApiClient({baseUrl: '', fetchImpl: undefined});
56
+ });
57
+
58
+ test('maps list DTO pages to workflow run models while keeping the cache DTO-shaped', async () => {
59
+ const body = workflowRunListResponseDto({
60
+ runs: [
61
+ workflowRunDto({
62
+ id: RUN_ID,
63
+ trigger_provider: 'github',
64
+ trigger_source: 'github_acme',
65
+ trigger_event: 'push',
66
+ updated_at: '2026-05-07T01:02:00.000Z',
67
+ }),
68
+ ],
69
+ next_cursor: 'cursor-2',
70
+ filtered_total_count: 8,
71
+ });
72
+ const fetchImpl = vi.fn(async () => jsonResponse(body));
73
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
74
+
75
+ const {result, queryClient} = renderWithQueryClient(() =>
76
+ useWorkflowRunsInfiniteQuery(PROJECT_ID, {}),
77
+ );
78
+
79
+ await waitFor(() =>
80
+ expect(result.current.data?.pages[0]?.runs[0]?.triggerSource).toBe('github_acme'),
81
+ );
82
+ expect(result.current.data?.pages[0]?.runs[0]).toMatchObject({
83
+ id: RUN_ID,
84
+ triggerProvider: 'github',
85
+ triggerSource: 'github_acme',
86
+ triggerEvent: 'push',
87
+ triggerDisplayLabel: 'push',
88
+ triggerLabel: 'github_acme · push',
89
+ updatedAt: '2026-05-07T01:02:00.000Z',
90
+ isTemporary: false,
91
+ });
92
+ expect(result.current.data?.pages[0]?.nextCursor).toBe('cursor-2');
93
+ expect(result.current.data?.pages[0]?.filteredTotalCount).toBe(8);
94
+
95
+ const cached = queryClient.getQueryData<
96
+ InfiniteData<WorkflowRunListResponseDto, string | undefined>
97
+ >(workflowRunsQueryKeys.list(PROJECT_ID, {}));
98
+ expect(cached?.pages[0]?.runs[0]).toMatchObject({
99
+ trigger_provider: 'github',
100
+ trigger_source: 'github_acme',
101
+ trigger_event: 'push',
102
+ updated_at: '2026-05-07T01:02:00.000Z',
103
+ });
104
+ expect(cached?.pages[0]).not.toHaveProperty('nextCursor');
105
+ });
106
+
107
+ test('maps detail DTOs to nested workflow run detail models while keeping the cache DTO-shaped', async () => {
108
+ const body = workflowRunDetailDto({
109
+ id: RUN_ID,
110
+ trigger_source: 'manual',
111
+ trigger_event: 'fire',
112
+ jobs: [workflowJobDto({run_attempt_id: RUN_ID, name: 'build'})],
113
+ });
114
+ const fetchImpl = vi.fn(async () => jsonResponse(body));
115
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
116
+
117
+ const {result, queryClient} = renderWithQueryClient(() => useWorkflowRunQuery(RUN_ID));
118
+
119
+ await waitFor(() => expect(result.current.data?.triggerSource).toBe('manual'));
120
+ expect(result.current.data).toMatchObject({
121
+ id: RUN_ID,
122
+ triggerSource: 'manual',
123
+ triggerEvent: 'fire',
124
+ triggerDisplayLabel: 'fire',
125
+ triggerLabel: 'manual · fire',
126
+ jobs: [{name: 'build', runAttemptId: RUN_ID}],
127
+ });
128
+
129
+ const cached = queryClient.getQueryData<WorkflowRunDetailResponseDto>(
130
+ workflowRunsQueryKeys.detail(RUN_ID),
131
+ );
132
+ expect(cached).toMatchObject({
133
+ id: RUN_ID,
134
+ trigger_source: 'manual',
135
+ trigger_event: 'fire',
136
+ jobs: [{name: 'build', run_attempt_id: RUN_ID}],
137
+ });
138
+ expect(cached).not.toHaveProperty('triggerSource');
139
+ });
140
+
141
+ test('maps run attempts and caches them by workflow run id', async () => {
142
+ const body = runAttemptsResponseDto({
143
+ attempts: [
144
+ workflowRunAttemptDto({
145
+ id: ROOT_RUN_ID,
146
+ attempt: 1,
147
+ status: 'succeeded',
148
+ created_at: '2026-05-07T01:00:00.000Z',
149
+ }),
150
+ workflowRunAttemptDto({
151
+ id: RUN_ID,
152
+ attempt: 2,
153
+ status: 'failed',
154
+ created_at: '2026-05-07T01:02:00.000Z',
155
+ rerun_mode: 'all',
156
+ }),
157
+ ],
158
+ });
159
+ const fetchImpl = vi.fn(async () => jsonResponse(body));
160
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
161
+
162
+ const {result, queryClient} = renderWithQueryClient(() =>
163
+ useWorkflowRunAttemptsQuery({workflowRunId: RUN_ID, enabled: true}),
164
+ );
165
+
166
+ await waitFor(() => expect(result.current.data?.[1]?.attempt).toBe(2));
167
+ expect(result.current.data?.[1]).toMatchObject({
168
+ id: RUN_ID,
169
+ status: 'failed',
170
+ createdAt: '2026-05-07T01:02:00.000Z',
171
+ rerunMode: 'all',
172
+ });
173
+ expect(firstRequest(fetchImpl).url).toBe(
174
+ `https://api.example.test/workflows/runs/${RUN_ID}/attempts`,
175
+ );
176
+ expect(queryClient.getQueryData(workflowRunsQueryKeys.attempts(RUN_ID))).toEqual(body);
177
+ });
178
+
179
+ test('posts manual fire requests with and without inputs', async () => {
180
+ const postBodies: unknown[] = [];
181
+ const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
182
+ postBodies.push(await (input as Request).clone().json());
183
+ return jsonResponse({workflow_run_id: RUN_ID}, {status: 201});
184
+ });
185
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
186
+
187
+ const withoutInputs = await fireManualWorkflow({definitionId: DEFINITION_ID});
188
+ const withInputs = await fireManualWorkflow({
189
+ definitionId: DEFINITION_ID,
190
+ inputs: {env: 'production'},
191
+ });
192
+
193
+ expect(withoutInputs.workflow_run_id).toBe(RUN_ID);
194
+ expect(withInputs.workflow_run_id).toBe(RUN_ID);
195
+ expect(postBodies).toEqual([{}, {inputs: {env: 'production'}}]);
196
+ expect(firstRequest(fetchImpl).url).toBe(
197
+ `https://api.example.test/workflow-definitions/${DEFINITION_ID}/fire-manual`,
198
+ );
199
+ expect(firstRequest(fetchImpl).method).toBe('POST');
200
+ });
201
+
202
+ test('optimistically inserts manual runs into the same list cache prefix read by the rail', async () => {
203
+ let resolveFire: ((response: Response) => void) | undefined;
204
+ const fetchImpl = vi.fn(
205
+ () =>
206
+ new Promise<Response>((resolve) => {
207
+ resolveFire = resolve;
208
+ }),
209
+ );
210
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
211
+ const {result, queryClient} = renderWithQueryClient(() => useFireManualWorkflowMutation());
212
+ const listKey = workflowRunsQueryKeys.list(PROJECT_ID, {});
213
+ queryClient.setQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
214
+ listKey,
215
+ {
216
+ pages: [workflowRunListResponseDto({runs: [], filtered_total_count: 0})],
217
+ pageParams: [undefined],
218
+ },
219
+ );
220
+
221
+ const railListEntries = queryClient.getQueriesData({
222
+ queryKey: workflowRunsQueryKeys.lists(PROJECT_ID),
223
+ });
224
+ expect(railListEntries.map(([queryKey]) => queryKey)).toContainEqual(listKey);
225
+
226
+ act(() => {
227
+ result.current.mutate({projectId: PROJECT_ID, definitionId: DEFINITION_ID});
228
+ });
229
+
230
+ await waitFor(() => {
231
+ const cached =
232
+ queryClient.getQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
233
+ listKey,
234
+ );
235
+ expect(cached?.pages[0]?.runs[0]).toMatchObject({
236
+ project_id: PROJECT_ID,
237
+ definition_id: DEFINITION_ID,
238
+ status: 'pending',
239
+ trigger_source: 'manual',
240
+ trigger_provider: null,
241
+ });
242
+ expect(cached?.pages[0]?.runs[0]?.id).toMatch(TEMP_RUN_ID_PATTERN);
243
+ expect(cached?.pages[0]?.filtered_total_count).toBe(1);
244
+ });
245
+
246
+ if (!resolveFire) throw new Error('Expected manual fire request');
247
+ const completeFire = resolveFire;
248
+ act(() => {
249
+ completeFire(jsonResponse({workflow_run_id: RUN_ID}, {status: 201}));
250
+ });
251
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
252
+ });
253
+
254
+ test('does not remove newer optimistic manual runs when an older manual fire fails', async () => {
255
+ const fireRequests: Array<{resolve: (response: Response) => void}> = [];
256
+ const fetchImpl = vi.fn(
257
+ () =>
258
+ new Promise<Response>((resolve) => {
259
+ fireRequests.push({resolve});
260
+ }),
261
+ );
262
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
263
+ const {result, queryClient} = renderWithQueryClient(() => useFireManualWorkflowMutation());
264
+ const listKey = workflowRunsQueryKeys.list(PROJECT_ID, {});
265
+ queryClient.setQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
266
+ listKey,
267
+ {
268
+ pages: [workflowRunListResponseDto({runs: [], filtered_total_count: 0})],
269
+ pageParams: [undefined],
270
+ },
271
+ );
272
+
273
+ act(() => {
274
+ result.current.mutate({projectId: PROJECT_ID, definitionId: DEFINITION_ID});
275
+ });
276
+ await waitFor(() => expect(fireRequests).toHaveLength(1));
277
+ const firstFire = fireRequests[0];
278
+ if (!firstFire) throw new Error('Expected first manual fire request');
279
+
280
+ await waitFor(() => {
281
+ const cached =
282
+ queryClient.getQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
283
+ listKey,
284
+ );
285
+ expect(cached?.pages[0]?.runs).toHaveLength(1);
286
+ expect(cached?.pages[0]?.filtered_total_count).toBe(1);
287
+ });
288
+
289
+ act(() => {
290
+ result.current.mutate({projectId: PROJECT_ID, definitionId: DEFINITION_ID});
291
+ });
292
+ await waitFor(() => expect(fireRequests).toHaveLength(2));
293
+ const secondFire = fireRequests[1];
294
+ if (!secondFire) throw new Error('Expected second manual fire request');
295
+
296
+ let secondTempWorkflowRunId: string | undefined;
297
+ await waitFor(() => {
298
+ const cached =
299
+ queryClient.getQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
300
+ listKey,
301
+ );
302
+ expect(cached?.pages[0]?.runs).toHaveLength(2);
303
+ expect(cached?.pages[0]?.filtered_total_count).toBe(2);
304
+ secondTempWorkflowRunId = cached?.pages[0]?.runs[0]?.id;
305
+ expect(secondTempWorkflowRunId).toMatch(TEMP_RUN_ID_PATTERN);
306
+ });
307
+
308
+ act(() => {
309
+ firstFire.resolve(jsonResponse({code: 'server-error'}, {status: 500}));
310
+ });
311
+
312
+ await waitFor(() => {
313
+ const cached =
314
+ queryClient.getQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
315
+ listKey,
316
+ );
317
+ expect(cached?.pages[0]?.runs.map((run) => run.id)).toEqual([secondTempWorkflowRunId]);
318
+ expect(cached?.pages[0]?.filtered_total_count).toBe(1);
319
+ });
320
+
321
+ act(() => {
322
+ secondFire.resolve(jsonResponse({workflow_run_id: RUN_ID}, {status: 201}));
323
+ });
324
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
325
+ });
326
+
327
+ test('does not fetch run attempts while disabled', () => {
328
+ const fetchImpl = vi.fn(async () => jsonResponse(runAttemptsResponseDto()));
329
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
330
+
331
+ renderWithQueryClient(() =>
332
+ useWorkflowRunAttemptsQuery({workflowRunId: RUN_ID, enabled: false}),
333
+ );
334
+
335
+ expect(fetchImpl).not.toHaveBeenCalled();
336
+ });
337
+
338
+ test('cancels a workflow run and invalidates detail and list queries', async () => {
339
+ const body = workflowRunDto({id: RUN_ID, project_id: PROJECT_ID, status: 'cancelled'});
340
+ const fetchImpl = vi.fn(async () => jsonResponse(body));
341
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
342
+ const run = workflowRunDto({id: RUN_ID, project_id: PROJECT_ID, status: 'running'});
343
+ const {result, queryClient} = renderWithQueryClient(() =>
344
+ useCancelWorkflowRunMutation(toWorkflowRun(run)),
345
+ );
346
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
347
+
348
+ let cancelled: Awaited<ReturnType<typeof result.current.mutateAsync>> | undefined;
349
+ await act(async () => {
350
+ cancelled = await result.current.mutateAsync();
351
+ });
352
+
353
+ const calls = fetchImpl.mock.calls as unknown as Array<[Request]>;
354
+ const request = calls[0]?.[0];
355
+ if (!request) throw new Error('Expected cancel request');
356
+ expect(request.url).toBe(
357
+ 'https://api.example.test/workflows/runs/66666666-6666-4666-8666-666666666666/cancel',
358
+ );
359
+ expect(request.method).toBe('POST');
360
+ expect(cancelled?.status).toBe('cancelled');
361
+ expect(invalidateSpy).toHaveBeenCalledWith({queryKey: workflowRunsQueryKeys.detail(RUN_ID)});
362
+ expect(invalidateSpy).toHaveBeenCalledWith({queryKey: workflowRunsQueryKeys.lists(PROJECT_ID)});
363
+ });
364
+
365
+ test('posts rerun mode and invalidates project run lists and attempt lineage', async () => {
366
+ const postBodies: unknown[] = [];
367
+ const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
368
+ const request = input as Request;
369
+ postBodies.push(await request.clone().json());
370
+ return jsonResponse(
371
+ workflowRunDto({
372
+ id: '77777777-7777-4777-8777-777777777777',
373
+ current_attempt: 2,
374
+ latest_attempt: 2,
375
+ status: 'pending',
376
+ }),
377
+ );
378
+ });
379
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
380
+
381
+ const {result, queryClient} = renderWithQueryClient(() =>
382
+ useRerunWorkflowRunMutation(PROJECT_ID),
383
+ );
384
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
385
+
386
+ await act(async () => {
387
+ await result.current.mutateAsync({workflowRunId: RUN_ID, mode: 'failed'});
388
+ });
389
+
390
+ const request = firstRequest(fetchImpl);
391
+ expect(request.url).toBe(`https://api.example.test/workflows/runs/${RUN_ID}/rerun`);
392
+ expect(request.method).toBe('POST');
393
+ expect(postBodies).toEqual([{mode: 'failed'}]);
394
+ expect(invalidateSpy).toHaveBeenCalledWith({
395
+ queryKey: workflowRunsQueryKeys.lists(PROJECT_ID),
396
+ });
397
+ expect(invalidateSpy).toHaveBeenCalledWith({
398
+ queryKey: workflowRunsQueryKeys.detail(RUN_ID),
399
+ });
400
+ expect(invalidateSpy).toHaveBeenCalledWith({
401
+ queryKey: workflowRunsQueryKeys.attempts(RUN_ID),
402
+ });
403
+ });
404
+ });
405
+
406
+ function firstRequest(fetchImpl: ReturnType<typeof vi.fn>): Request {
407
+ const input = (fetchImpl.mock.calls as unknown[][])[0]?.[0];
408
+ if (!(input instanceof Request)) throw new Error('Expected fetch to receive a Request');
409
+ return input;
410
+ }