@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21

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 (321) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +7 -2
  3. package/board/client/board-look.ts +31 -1
  4. package/board/client/board-panel-parts.tsx +150 -0
  5. package/board/client/board-panel-stages.ts +99 -0
  6. package/board/client/board-panel-styles.ts +223 -0
  7. package/board/client/board-panel.tsx +77 -1275
  8. package/board/client/engine-life.ts +48 -0
  9. package/board/client/final-review-stack.tsx +144 -0
  10. package/board/client/heartbeat.tsx +61 -0
  11. package/board/client/helper-lines.tsx +69 -0
  12. package/board/client/live-feed.tsx +86 -0
  13. package/board/client/live-line.ts +73 -0
  14. package/board/client/model-line.tsx +178 -0
  15. package/board/client/needs-you-section.tsx +133 -0
  16. package/board/client/one-line.tsx +193 -0
  17. package/board/client/repo-wait-banner.tsx +69 -0
  18. package/board/client/run-footer.tsx +68 -0
  19. package/board/client/run-header.tsx +189 -0
  20. package/board/client/run-picker.tsx +98 -0
  21. package/board/client/status-bar.tsx +131 -0
  22. package/board/client/step-dots.tsx +127 -0
  23. package/board/client/stuck-actions.tsx +269 -0
  24. package/board/client/ticket-card-view.tsx +238 -0
  25. package/board/client/ticket-detail.tsx +87 -0
  26. package/board/client/ticket-stack.tsx +119 -0
  27. package/board/client/ticket-wait.ts +188 -0
  28. package/board/client/timeline-rows.tsx +41 -14
  29. package/board/client/usage-line.tsx +51 -0
  30. package/board/client/wait-line.tsx +39 -0
  31. package/board/index.client.tsx +1 -1
  32. package/board/index.server.ts +7 -1
  33. package/board/package.json +1 -1
  34. package/board/server/board-panel-state-helpers.ts +67 -0
  35. package/board/server/board-server-check.ts +294 -0
  36. package/board/server/board-server-engine.ts +129 -0
  37. package/board/server/board-server-events.ts +134 -0
  38. package/board/server/board-server-read.ts +190 -0
  39. package/board/server/board-server-resume.ts +164 -0
  40. package/board/server/board-server-start.ts +174 -0
  41. package/board/server/board-server-store.ts +281 -0
  42. package/board/server/board-server-types.ts +114 -0
  43. package/board/server/board-server-util.ts +4 -0
  44. package/board/server/board-server.ts +52 -943
  45. package/board/server/board-vocabulary-engine.ts +144 -0
  46. package/board/server/board-vocabulary-review.ts +115 -0
  47. package/board/server/board-vocabulary-run.ts +145 -0
  48. package/board/server/board-vocabulary-shapes.ts +153 -0
  49. package/board/server/board-vocabulary.ts +20 -375
  50. package/board/server/current-step-panel-helpers.ts +108 -0
  51. package/board/server/describe-record.ts +403 -0
  52. package/board/server/engine-launch.ts +20 -3
  53. package/board/server/engine-restart-helpers.ts +105 -0
  54. package/board/server/engine-watch.ts +45 -5
  55. package/board/server/journal-rebuild-helpers.ts +169 -0
  56. package/board/server/live-file.ts +130 -0
  57. package/board/server/luca-version.ts +1 -1
  58. package/board/server/make-rows.ts +14 -629
  59. package/board/server/record-texts.ts +350 -0
  60. package/board/server/reduce-board-agent-start.ts +224 -0
  61. package/board/server/reduce-board-agents.ts +160 -0
  62. package/board/server/reduce-board-counters.ts +138 -0
  63. package/board/server/reduce-board-create.ts +148 -0
  64. package/board/server/reduce-board-final-review.ts +174 -0
  65. package/board/server/reduce-board-needs-you.ts +283 -0
  66. package/board/server/reduce-board-reasons.ts +59 -0
  67. package/board/server/reduce-board-run.ts +255 -0
  68. package/board/server/reduce-board-settle.ts +88 -0
  69. package/board/server/reduce-board-steps.ts +226 -0
  70. package/board/server/reduce-board-text.ts +139 -0
  71. package/board/server/reduce-board-ticket-checks.ts +168 -0
  72. package/board/server/reduce-board-ticket-join.ts +144 -0
  73. package/board/server/reduce-board-ticket-start.ts +132 -0
  74. package/board/server/reduce-board-update.ts +117 -0
  75. package/board/server/reduce-board-usage.ts +247 -0
  76. package/board/server/reduce-board.ts +100 -1917
  77. package/board/server/reply-post.ts +407 -0
  78. package/board/server/run-registry.ts +8 -4
  79. package/board/server/ticket-stages-panel-helpers.ts +296 -0
  80. package/board/shared/board-rows.ts +9 -0
  81. package/board/shared/board-rpc.ts +42 -0
  82. package/board/shared/board-state-counts.ts +77 -0
  83. package/board/shared/board-state-live.ts +99 -0
  84. package/board/shared/board-state-review.ts +59 -0
  85. package/board/shared/board-state-run.ts +236 -0
  86. package/board/shared/board-state-ticket.ts +257 -0
  87. package/board/shared/board-state-usage.ts +107 -0
  88. package/board/shared/board-state.ts +97 -567
  89. package/board/shared/reply-actions.ts +207 -0
  90. package/engine/agents/agent-launcher.ts +27 -4
  91. package/engine/agents/claude-launcher-helpers.ts +233 -0
  92. package/engine/agents/claude-launcher.ts +28 -669
  93. package/engine/agents/claude-messages.ts +212 -0
  94. package/engine/agents/claude-options.ts +43 -8
  95. package/engine/agents/common-instructions.ts +138 -0
  96. package/engine/agents/input-channel.ts +36 -0
  97. package/engine/agents/launch-paths.ts +29 -0
  98. package/engine/agents/launch-session.ts +227 -0
  99. package/engine/agents/learner-instructions.ts +31 -0
  100. package/engine/agents/open-session.ts +210 -0
  101. package/engine/agents/prompt-sections.ts +95 -0
  102. package/engine/agents/rejoin-prompts.ts +135 -0
  103. package/engine/agents/reviewer-instructions.ts +99 -0
  104. package/engine/agents/role-instructions.ts +31 -244
  105. package/engine/agents/role-prompts.ts +39 -109
  106. package/engine/agents/role-results.ts +106 -7
  107. package/engine/agents/scripted-launcher.ts +21 -6
  108. package/engine/agents/session-pool.ts +124 -0
  109. package/engine/agents/turn-progress.ts +283 -0
  110. package/engine/agents/writer-instructions.ts +151 -0
  111. package/engine/cli/computer-adapters-real.ts +15 -3
  112. package/engine/cli/computer-adapters.ts +2 -0
  113. package/engine/cli/computer-checks.ts +87 -4
  114. package/engine/cli/config-reload-text.ts +39 -0
  115. package/engine/cli/decision-model-setup.ts +70 -0
  116. package/engine/cli/doctor-command.ts +1 -0
  117. package/engine/cli/doctor.ts +21 -3
  118. package/engine/cli/going-runs.ts +192 -36
  119. package/engine/cli/init-command.ts +1 -0
  120. package/engine/cli/init.ts +23 -1
  121. package/engine/cli/live-runs.ts +73 -0
  122. package/engine/cli/luca-run.ts +42 -7
  123. package/engine/cli/luca-skills.ts +183 -0
  124. package/engine/cli/luca.ts +8 -1
  125. package/engine/cli/models-command.ts +119 -0
  126. package/engine/cli/read-run.ts +81 -0
  127. package/engine/cli/run-args.ts +33 -6
  128. package/engine/cli/run-demo.ts +143 -0
  129. package/engine/cli/run-end.ts +184 -0
  130. package/engine/cli/run-modes-helpers.ts +81 -0
  131. package/engine/cli/run-modes.ts +62 -439
  132. package/engine/cli/setup.ts +5 -48
  133. package/engine/cli/unfinished-runs.ts +122 -0
  134. package/engine/cli/upgrade-command.ts +212 -30
  135. package/engine/cli/upgrade-finish.ts +149 -0
  136. package/engine/cli/upgrade.ts +112 -71
  137. package/engine/config/config-fields.ts +127 -0
  138. package/engine/config/config-reload.ts +250 -0
  139. package/engine/config/engine-config-schema.ts +235 -0
  140. package/engine/config/engine-config.ts +72 -142
  141. package/engine/config/test-commands.ts +102 -0
  142. package/engine/core/agent-guards-helpers.ts +84 -0
  143. package/engine/core/agent-turn.ts +395 -0
  144. package/engine/core/already-done.ts +44 -1
  145. package/engine/core/build-action.ts +214 -0
  146. package/engine/core/build-context.ts +135 -0
  147. package/engine/core/build-launch.ts +259 -0
  148. package/engine/core/commit-in.ts +282 -0
  149. package/engine/core/decide-build-early.ts +108 -0
  150. package/engine/core/decide-build-ending.ts +145 -0
  151. package/engine/core/decide-build-helpers.ts +87 -0
  152. package/engine/core/decide-build-tickets.ts +207 -0
  153. package/engine/core/decide-build-view.ts +103 -0
  154. package/engine/core/decide-build.ts +101 -1368
  155. package/engine/core/decide-final-fix.ts +203 -0
  156. package/engine/core/decide-final-review.ts +55 -310
  157. package/engine/core/decide-red-green.ts +398 -0
  158. package/engine/core/decide-rejoin.ts +259 -0
  159. package/engine/core/decide-repo-wait.ts +129 -0
  160. package/engine/core/decide-run-budget.ts +86 -12
  161. package/engine/core/decide-stuck-helpers.ts +127 -0
  162. package/engine/core/decide-stuck.ts +60 -8
  163. package/engine/core/decide-ticket-review.ts +95 -0
  164. package/engine/core/decide-ticket.ts +267 -0
  165. package/engine/core/decide-waits.ts +47 -0
  166. package/engine/core/decide.ts +74 -23
  167. package/engine/core/engine-chores.ts +163 -0
  168. package/engine/core/environment-rerun.ts +83 -0
  169. package/engine/core/evidence-text.ts +84 -0
  170. package/engine/core/execute-action.ts +303 -0
  171. package/engine/core/execute-already-done-check.ts +133 -0
  172. package/engine/core/execute-build.ts +68 -944
  173. package/engine/core/execute-changeset.ts +5 -7
  174. package/engine/core/execute-final-review.ts +125 -13
  175. package/engine/core/execute-gates.ts +182 -0
  176. package/engine/core/execute-intake.ts +130 -0
  177. package/engine/core/execute-join.ts +276 -0
  178. package/engine/core/execute-memory.ts +12 -0
  179. package/engine/core/execute-pull-request.ts +84 -0
  180. package/engine/core/execute-red-check.ts +219 -0
  181. package/engine/core/execute-repo-wait.ts +129 -0
  182. package/engine/core/execute-stuck.ts +25 -10
  183. package/engine/core/execute-waits.ts +158 -0
  184. package/engine/core/execute-with-jev.ts +118 -0
  185. package/engine/core/execute.ts +124 -831
  186. package/engine/core/final-failure-cap.ts +38 -0
  187. package/engine/core/final-review-action.ts +125 -0
  188. package/engine/core/final-review-text.ts +63 -17
  189. package/engine/core/final-stuck-text.ts +86 -0
  190. package/engine/core/fix-loop-text.ts +49 -2
  191. package/engine/core/left-out.ts +120 -0
  192. package/engine/core/live-file.ts +372 -0
  193. package/engine/core/loop-caps.ts +25 -2
  194. package/engine/core/not-in-run.ts +7 -3
  195. package/engine/core/pull-request-text.ts +255 -6
  196. package/engine/core/repo-wait-text.ts +81 -0
  197. package/engine/core/review-text.ts +55 -1
  198. package/engine/core/run-crash-recovery-helpers.ts +16 -0
  199. package/engine/core/run-session-close-helpers.ts +96 -0
  200. package/engine/core/run-start.ts +148 -0
  201. package/engine/core/run-stuck-text.ts +86 -0
  202. package/engine/core/schedule-rules.ts +193 -0
  203. package/engine/core/stuck-detail-text.ts +89 -0
  204. package/engine/core/stuck-format.ts +22 -0
  205. package/engine/core/stuck-text.ts +29 -332
  206. package/engine/core/ticket-stuck-text.ts +192 -0
  207. package/engine/core/worktree-changes.ts +106 -0
  208. package/engine/gates/already-done-check.ts +120 -0
  209. package/engine/gates/feature-map.ts +96 -0
  210. package/engine/gates/gate-runner.ts +108 -108
  211. package/engine/gates/gate-schemas.ts +61 -2
  212. package/engine/gates/leftover-scan.ts +154 -31
  213. package/engine/gates/new-tests.ts +45 -0
  214. package/engine/gates/own-files-check.ts +98 -0
  215. package/engine/gates/prepare-check.ts +60 -0
  216. package/engine/gates/prepare-slots.ts +99 -0
  217. package/engine/gates/red-check.ts +184 -16
  218. package/engine/gates/shell-check.ts +64 -0
  219. package/engine/gates/slot-waits.ts +55 -0
  220. package/engine/gates/slow-test-runner.ts +59 -0
  221. package/engine/gates/test-checks.ts +161 -0
  222. package/engine/gates/test-runner.ts +164 -5
  223. package/engine/gates/test-slots.ts +48 -0
  224. package/engine/git/git-adapter.ts +55 -396
  225. package/engine/git/git-commits.ts +132 -0
  226. package/engine/git/git-join.ts +139 -0
  227. package/engine/git/git-read.ts +123 -0
  228. package/engine/git/git-run.ts +32 -0
  229. package/engine/git/git-worktrees.ts +143 -0
  230. package/engine/git/outside-links.ts +55 -0
  231. package/engine/guards/after-turn-check.ts +31 -5
  232. package/engine/guards/guard-hook.ts +60 -12
  233. package/engine/guards/guard-roles.ts +68 -0
  234. package/engine/guards/helper-rules.ts +92 -0
  235. package/engine/guards/permission-rules.ts +161 -0
  236. package/engine/guards/role-rules-helpers.ts +48 -0
  237. package/engine/guards/role-rules.ts +34 -606
  238. package/engine/guards/shell-rules.ts +344 -0
  239. package/engine/guards/worktree-git.ts +208 -0
  240. package/engine/guards/worktree-snapshot.ts +412 -0
  241. package/engine/guards/worktree-state.ts +28 -825
  242. package/engine/guards/worktree-undo.ts +260 -0
  243. package/engine/guards/write-rules.ts +149 -0
  244. package/engine/index.ts +30 -9
  245. package/engine/intake/criteria-checks.ts +114 -0
  246. package/engine/intake/intake-checks.ts +164 -30
  247. package/engine/intake/intake-schemas.ts +37 -2
  248. package/engine/jev/clef-client.ts +259 -0
  249. package/engine/jev/decision-model-credentials.ts +212 -0
  250. package/engine/jev/jev-jobs.ts +144 -60
  251. package/engine/jev/jev-schemas.ts +86 -5
  252. package/engine/jev/jev-shadow-helpers.ts +218 -0
  253. package/engine/jev/jev-shadow.ts +109 -64
  254. package/engine/jev/model-pick.ts +275 -0
  255. package/engine/journal/agent-entries.ts +167 -0
  256. package/engine/journal/agent-message-entries.ts +72 -0
  257. package/engine/journal/agent-turn-replay.ts +203 -0
  258. package/engine/journal/already-done-entries.ts +55 -0
  259. package/engine/journal/commit-entries.ts +56 -0
  260. package/engine/journal/cross-ticket-replay.ts +222 -0
  261. package/engine/journal/decision-model-entries.ts +86 -0
  262. package/engine/journal/entry-fields.ts +10 -0
  263. package/engine/journal/final-review-entries.ts +128 -0
  264. package/engine/journal/final-review-replay.ts +324 -0
  265. package/engine/journal/final-review-round.ts +320 -0
  266. package/engine/journal/final-review-state.ts +180 -0
  267. package/engine/journal/gate-entries.ts +81 -0
  268. package/engine/journal/intake-entries.ts +87 -0
  269. package/engine/journal/join-entries.ts +126 -0
  270. package/engine/journal/journal-entry-schema.ts +218 -0
  271. package/engine/journal/journal-kinds.ts +97 -0
  272. package/engine/journal/journal-record-schema.ts +218 -0
  273. package/engine/journal/journal-record.ts +43 -1400
  274. package/engine/journal/journal.ts +6 -1
  275. package/engine/journal/limit-entries.ts +90 -0
  276. package/engine/journal/memory-entries.ts +124 -0
  277. package/engine/journal/memory-replay.ts +157 -0
  278. package/engine/journal/plan-replay.ts +139 -0
  279. package/engine/journal/pull-request-entries.ts +41 -0
  280. package/engine/journal/replay.ts +157 -2144
  281. package/engine/journal/reply-replay.ts +144 -0
  282. package/engine/journal/repo-wait-entries.ts +53 -0
  283. package/engine/journal/run-entries.ts +172 -0
  284. package/engine/journal/run-state.ts +264 -0
  285. package/engine/journal/shared-failure-replay.ts +133 -0
  286. package/engine/journal/stuck-entries.ts +193 -0
  287. package/engine/journal/ticket-progress.ts +392 -0
  288. package/engine/journal/ticket-replay.ts +411 -0
  289. package/engine/journal/ticket-result-replay.ts +364 -0
  290. package/engine/journal/ticket-retry-replay.ts +142 -0
  291. package/engine/journal/worktree-entries.ts +122 -0
  292. package/engine/models/model-score.ts +512 -0
  293. package/engine/shell/run-command.ts +187 -18
  294. package/engine/testing/agent-turn-fixtures.ts +287 -0
  295. package/engine/testing/already-done-fixtures.ts +97 -0
  296. package/engine/testing/build-fixtures.ts +68 -973
  297. package/engine/testing/checkouts-fixtures.ts +48 -0
  298. package/engine/testing/final-review-fixtures.ts +62 -1
  299. package/engine/testing/final-review-steps.ts +143 -0
  300. package/engine/testing/limit-fixtures.ts +85 -0
  301. package/engine/testing/live-file-fixtures.ts +57 -0
  302. package/engine/testing/practice-repo.ts +10 -4
  303. package/engine/testing/practice-run.ts +1 -1
  304. package/engine/testing/reply-fixtures.ts +88 -0
  305. package/engine/testing/run-budget-fixtures.ts +165 -0
  306. package/engine/testing/run-fixtures.ts +219 -0
  307. package/engine/testing/sdk-play.ts +177 -0
  308. package/engine/testing/setup-repo.ts +194 -0
  309. package/engine/testing/step-record-fixtures.ts +55 -0
  310. package/engine/testing/ticket-step-fixtures.ts +272 -0
  311. package/engine/testing/wait-scenarios.ts +186 -0
  312. package/engine/tracker/gh-retry.ts +217 -0
  313. package/engine/tracker/github-tracker.ts +310 -90
  314. package/engine/tracker/in-memory-tracker.ts +8 -4
  315. package/engine/tracker/tracker.ts +131 -6
  316. package/package.json +2 -1
  317. package/skills/luca-retro/SKILL.md +74 -0
  318. package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
  319. package/skills/luca-unstick/SKILL.md +153 -0
  320. package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
  321. package/engine/jev/jev-client.ts +0 -173
@@ -1,8 +1,9 @@
1
1
  import { existsSync } from 'node:fs'
2
2
 
3
3
  import type { TestCase, TestRun } from './gate-schemas'
4
+ import { inTestSlot } from './test-slots'
4
5
 
5
- import { clipOutput, runShell } from '../shell/run-command'
6
+ import { clipOutput, runShell, timedOutLine } from '../shell/run-command'
6
7
 
7
8
  const decode = (text: string): string =>
8
9
  text
@@ -25,7 +26,9 @@ const attribute = ({
25
26
 
26
27
  /**
27
28
  * Reads bun's JUnit report: one `<testsuite file=..>` per file, a nested
28
- * `<testsuite>` per describe, and a `<testcase>` per test.
29
+ * `<testsuite>` per describe, and a `<testcase>` per test. Each case keeps
30
+ * its `time`, in whole milliseconds, and its `assertions` count when the
31
+ * report has them (#518, #581).
29
32
  */
30
33
  export const parseJunit = ({ xml }: { xml: string }): TestCase[] => {
31
34
  const cases: TestCase[] = []
@@ -46,10 +49,18 @@ export const parseJunit = ({ xml }: { xml: string }): TestCase[] => {
46
49
  continue
47
50
  }
48
51
  const name = attribute({ attrs, name: 'name' }) ?? ''
52
+ const seconds = Number(attribute({ attrs, name: 'time' }))
53
+ const assertions = Number(attribute({ attrs, name: 'assertions' }))
49
54
  cases.push({
50
55
  file: attribute({ attrs, name: 'file' }) ?? suites[0] ?? '',
51
56
  full_name: [...suites.slice(1), name].join(' > '),
52
57
  status: 'passed',
58
+ ...(Number.isFinite(seconds)
59
+ ? { duration_ms: Math.round(seconds * 1000) }
60
+ : {}),
61
+ ...(Number.isInteger(assertions) && assertions >= 0
62
+ ? { assertions }
63
+ : {}),
53
64
  })
54
65
  open = selfClosing ? -1 : cases.length - 1
55
66
  } else if (open >= 0 && !closing) {
@@ -83,6 +94,13 @@ export const testFilesAmong = ({
83
94
 
84
95
  const NO_TEST_FILES = /0 test files matching|No tests found!/
85
96
 
97
+ /**
98
+ * How to give a test command longer, for the timed-out message. Every test
99
+ * command says the same, whatever its kind (#542).
100
+ */
101
+ export const TEST_TIMEOUT_HINT =
102
+ 'To give it longer, raise `test_timeout_ms` in `.luca/config.json`.'
103
+
86
104
  /**
87
105
  * Runs the engine config's test command with bun's JUnit reporter, so each
88
106
  * test case's outcome is known. The command must be a `bun test` command; the
@@ -90,22 +108,70 @@ const NO_TEST_FILES = /0 test files matching|No tests found!/
90
108
  *
91
109
  * A repo with no test files counts as a pass: `bun test` exits 1 there, but a
92
110
  * baseline with nothing to break is fine.
111
+ *
112
+ * One that runs past `timeout_ms` (see `testTimeoutOf`) is stopped, with
113
+ * everything it started, and fails; its output starts by saying so, and how
114
+ * to give it longer (#542).
115
+ *
116
+ * With `test_concurrency` (#544), it first waits for a test slot (see
117
+ * `inTestSlot`): the run records that wait as `wait_ms`, and its time limit
118
+ * starts after it. An `exclusive` run (a lone rerun, #544) waits until no
119
+ * other test run is going, and holds every slot while it runs.
93
120
  */
94
121
  export const runTests = async ({
95
122
  cwd,
96
123
  command,
97
124
  test_files,
98
125
  report_file,
126
+ timeout_ms,
127
+ test_concurrency,
128
+ exclusive,
99
129
  }: {
100
130
  cwd: string
101
131
  command: string
102
132
  /** The worktree's test files, to spot files that produced no results. */
103
133
  test_files: string[]
104
134
  report_file: string
135
+ timeout_ms: number
136
+ /** The config's `test_concurrency`; left out, no cap. */
137
+ test_concurrency?: number
138
+ /** Run alone, holding every test slot (#544). Left out, it doesn't. */
139
+ exclusive?: boolean
140
+ }): Promise<TestRun> =>
141
+ inTestSlot({
142
+ test_concurrency,
143
+ exclusive,
144
+ work: () =>
145
+ runTestsInHeldSlot({
146
+ cwd,
147
+ command,
148
+ test_files,
149
+ report_file,
150
+ timeout_ms,
151
+ }),
152
+ })
153
+
154
+ /**
155
+ * `runTests` for a caller that already holds a test slot (see `inTestSlot`):
156
+ * it runs at once, taking no slot of its own.
157
+ */
158
+ export const runTestsInHeldSlot = async ({
159
+ cwd,
160
+ command,
161
+ test_files,
162
+ report_file,
163
+ timeout_ms,
164
+ }: {
165
+ cwd: string
166
+ command: string
167
+ test_files: string[]
168
+ report_file: string
169
+ timeout_ms: number
105
170
  }): Promise<TestRun> => {
106
171
  const shell = await runShell({
107
172
  command: `${command} --reporter=junit --reporter-outfile='${report_file}'`,
108
173
  cwd,
174
+ timeout_ms,
109
175
  })
110
176
  const output = `${shell.stdout}\n${shell.stderr}`
111
177
  const noTestFiles = NO_TEST_FILES.test(output)
@@ -124,10 +190,31 @@ export const runTests = async ({
124
190
  files_without_results: test_files.filter(
125
191
  (file) => !withResults.has(file)
126
192
  ),
127
- output: clipOutput({ text: output }),
193
+ output: `${shell.timed_out ? timedOutLine({ timeout_ms, hint: TEST_TIMEOUT_HINT, survivors: shell.survivors }) : ''}${clipOutput({ text: output })}`,
128
194
  }
129
195
  }
130
196
 
197
+ /** A word quoted for `sh`, whatever it holds. */
198
+ const shellWord = (word: string): string => `'${word.replaceAll("'", `'\\''`)}'`
199
+
200
+ /**
201
+ * A bun test command that runs just these test files: the config's command
202
+ * with each file after it, as a `./` path so bun takes it as a path, not a
203
+ * name to match. A command that already names a folder runs that too.
204
+ *
205
+ * @example
206
+ * onlyTestFiles({ command: 'bun test', files: ['src/sum.test.ts'] })
207
+ * // "bun test './src/sum.test.ts'"
208
+ */
209
+ export const onlyTestFiles = ({
210
+ command,
211
+ files,
212
+ }: {
213
+ command: string
214
+ files: string[]
215
+ }): string =>
216
+ [command, ...files.map((file) => shellWord(`./${file}`))].join(' ')
217
+
131
218
  /**
132
219
  * The report file for the `index`th of several commands: the first keeps
133
220
  * `report_file`, the others get a numbered one beside it.
@@ -155,18 +242,27 @@ export const reportFileFor = ({
155
242
  * empty passing run.
156
243
  *
157
244
  * @example
158
- * const run = await runBunTests({ cwd, commands: ['bun test'], test_files, report_file })
245
+ * const run = await runBunTests({ cwd, commands: ['bun test'], test_files, report_file, timeout_ms: testTimeoutOf({ config }) })
159
246
  */
160
247
  export const runBunTests = async ({
161
248
  cwd,
162
249
  commands,
163
250
  test_files,
164
251
  report_file,
252
+ timeout_ms,
253
+ test_concurrency,
254
+ exclusive,
165
255
  }: {
166
256
  cwd: string
167
257
  commands: string[]
168
258
  test_files: string[]
169
259
  report_file: string
260
+ /** Each command's time limit (see `runTests`). */
261
+ timeout_ms: number
262
+ /** The config's `test_concurrency`: each command takes a test slot. */
263
+ test_concurrency?: number
264
+ /** Each command runs alone, holding every test slot (#544). */
265
+ exclusive?: boolean
170
266
  }): Promise<TestRun> => {
171
267
  const runs: TestRun[] = []
172
268
  for (const [index, command] of commands.entries()) {
@@ -176,9 +272,66 @@ export const runBunTests = async ({
176
272
  command,
177
273
  test_files,
178
274
  report_file: reportFileFor({ report_file, index }),
275
+ timeout_ms,
276
+ test_concurrency,
277
+ exclusive,
179
278
  })
180
279
  )
181
280
  }
281
+ return joinRuns({ runs, test_files })
282
+ }
283
+
284
+ /**
285
+ * A command that runs just these test files with plain `bun test` (#564):
286
+ * never the config's command plus the files, because Bun's
287
+ * `--path-ignore-patterns` leaves out even files named on the command line.
288
+ *
289
+ * @example
290
+ * fileTestsCommand({ files: ['src/a.e2e.test.ts'] }) // "bun test './src/a.e2e.test.ts'"
291
+ */
292
+ export const fileTestsCommand = ({ files }: { files: string[] }): string =>
293
+ onlyTestFiles({ command: 'bun test', files })
294
+
295
+ /**
296
+ * A fast run with a ticket's own-files run (#564) joined on (see
297
+ * `joinRuns`). An own run that gave no results at all means every file in it
298
+ * still doesn't load, which the fast run already said: it changes nothing,
299
+ * so the fast run, its command, and its output stay exactly as they are.
300
+ * (A fast test file that can't load yet, the usual red state, is such a
301
+ * file: it is not run twice in the journal.)
302
+ *
303
+ * @example
304
+ * const current = withOwnRun({ fast, own: await runTests({ ... }), test_files })
305
+ */
306
+ export const withOwnRun = ({
307
+ fast,
308
+ own,
309
+ test_files,
310
+ }: {
311
+ fast: TestRun
312
+ own: TestRun
313
+ test_files: string[]
314
+ }): TestRun =>
315
+ own.cases.length === 0 ? fast : joinRuns({ runs: [fast, own], test_files })
316
+
317
+ /**
318
+ * Several test runs as one: every case, `ok` only when all passed, and a test
319
+ * file without results only when no run gave it any. One run is itself. No
320
+ * run (a run of refactor tickets only, which the red check skips) gives an
321
+ * empty passing run. Used for the baseline and the red check
322
+ * (`runBunTests`) and for a ticket's own slow test files (#564). Their
323
+ * waits for a test slot add up to the joined run's `wait_ms` (#544).
324
+ *
325
+ * @example
326
+ * const run = joinRuns({ runs: [fast, own], test_files })
327
+ */
328
+ export const joinRuns = ({
329
+ runs,
330
+ test_files,
331
+ }: {
332
+ runs: TestRun[]
333
+ test_files: string[]
334
+ }): TestRun => {
182
335
  const [first] = runs
183
336
  if (first === undefined) {
184
337
  return {
@@ -194,8 +347,11 @@ export const runBunTests = async ({
194
347
  if (runs.length === 1) return first
195
348
  const cases = runs.flatMap((run) => run.cases)
196
349
  const withResults = new Set(cases.map(({ file }) => file))
350
+ const waits = runs.flatMap(({ wait_ms }) =>
351
+ wait_ms === undefined ? [] : [wait_ms]
352
+ )
197
353
  return {
198
- command: commands.join(' && '),
354
+ command: runs.map(({ command }) => command).join(' && '),
199
355
  ok: runs.every(({ ok }) => ok),
200
356
  exit_code: runs.find(({ ok }) => !ok)?.exit_code ?? first.exit_code,
201
357
  no_test_files: runs.every(({ no_test_files }) => no_test_files),
@@ -208,5 +364,8 @@ export const runBunTests = async ({
208
364
  .map(({ command, output }) => `$ ${command}\n${output}`)
209
365
  .join('\n\n'),
210
366
  }),
367
+ ...(waits.length === 0
368
+ ? {}
369
+ : { wait_ms: waits.reduce((total, wait) => total + wait, 0) }),
211
370
  }
212
371
  }
@@ -0,0 +1,48 @@
1
+ import { createSlots, type Slots } from './prepare-slots'
2
+ import { onSlotWait } from './slot-waits'
3
+
4
+ /**
5
+ * The engine's test slots (#544): one set for the whole engine process, so
6
+ * `test_concurrency` caps every test run at once, whichever step runs it.
7
+ */
8
+ export const TEST_SLOTS: Slots = createSlots()
9
+
10
+ /**
11
+ * Runs `work` in a test slot, at most `test_concurrency` at once, and adds
12
+ * how long it waited for one as `wait_ms`; the work's own time limit and
13
+ * timing start inside, after the wait. The step's slot-wait listener hears
14
+ * the wait while it lasts (see `slot-waits.ts`). An `exclusive` take waits for every
15
+ * test run to end, and holds off new ones until it settles.
16
+ *
17
+ * With `test_concurrency` left out there is no cap, but the work still holds
18
+ * a slot, so an exclusive take waits for it too. Nothing waits then unless an
19
+ * exclusive take is going, and the result is as `work` gave it (no
20
+ * `wait_ms`). Work that already holds a slot must not take another: an
21
+ * exclusive take queued between the two would wait forever.
22
+ *
23
+ * @example
24
+ * const check = await inTestSlot({ test_concurrency: 1, work: () => shellCheck({ ... }) })
25
+ * check.wait_ms // 0, or how long another test run held the slot
26
+ */
27
+ export const inTestSlot = async <T extends object>({
28
+ test_concurrency,
29
+ work,
30
+ exclusive = false,
31
+ }: {
32
+ test_concurrency: number | undefined
33
+ work: () => Promise<T>
34
+ exclusive?: boolean
35
+ }): Promise<T & { wait_ms?: number }> => {
36
+ const asked = performance.now()
37
+ const limit = test_concurrency ?? Infinity
38
+ return TEST_SLOTS.run({
39
+ limit,
40
+ exclusive,
41
+ on_wait: onSlotWait({ kind: 'test_slot', limit }),
42
+ work: async () => {
43
+ if (test_concurrency === undefined && !exclusive) return work()
44
+ const wait_ms = Math.round(performance.now() - asked)
45
+ return { ...(await work()), wait_ms }
46
+ },
47
+ })
48
+ }