@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,6 +1,18 @@
1
1
  import type { RedCheckResult, TestCase, TestRun } from './gate-schemas'
2
+ import { addedTestNames, testNamesIn } from './new-tests'
2
3
 
3
4
  import type { CriterionTests, TestRef } from '../agents/role-results'
5
+ import { MAX_TESTS_PER_CRITERION } from '../core/loop-caps'
6
+
7
+ /**
8
+ * A test file the ticket changed, as it was at the ticket's base and as it
9
+ * is now (`null`: not there).
10
+ */
11
+ export type ChangedTestSource = {
12
+ file: string
13
+ base: string | null
14
+ current: string | null
15
+ }
4
16
 
5
17
  const lastPart = (name: string): string =>
6
18
  (name.split(' > ').at(-1) ?? name).trim()
@@ -8,7 +20,44 @@ const lastPart = (name: string): string =>
8
20
  /** Quote-insensitive text, so a test name found in source matches its printed name. */
9
21
  const loose = (text: string): string => text.replace(/[\\'"`]/g, '')
10
22
 
11
- const findCase = ({
23
+ /**
24
+ * Said after a new unmapped test found in the text of a test file that does
25
+ * not load yet: with no results to check against, a `test(` inside a string
26
+ * or a comment counts as a test too (#581).
27
+ */
28
+ const NOT_LOADED_HINT =
29
+ '. The file does not load yet, so every test( in its text counts, even one inside a string or a comment'
30
+
31
+ /**
32
+ * Whether a test name written in a file's text (`own`) is the test bun prints
33
+ * as `full_name`: equal to it, at its end after " > " (not split on the last
34
+ * " > ": a test name may hold one itself), or the same last part. One rule
35
+ * for a mapped name and for a name found in the text.
36
+ */
37
+ const namesTest = ({
38
+ full_name,
39
+ own,
40
+ }: {
41
+ full_name: string
42
+ own: string
43
+ }): boolean => {
44
+ const full = full_name.trim()
45
+ return (
46
+ full === own ||
47
+ full.endsWith(` > ${own}`) ||
48
+ lastPart(full) === lastPart(own)
49
+ )
50
+ }
51
+
52
+ /**
53
+ * The test case a named test is, in its file: the one whose full name
54
+ * matches, else the only one whose last part does. `undefined` if none (or
55
+ * more than one) does.
56
+ *
57
+ * @example
58
+ * findCase({ cases: run.cases, test: { file: 'src/sum.test.ts', name: 'sum > adds' } })
59
+ */
60
+ export const findCase = ({
12
61
  cases,
13
62
  test,
14
63
  }: {
@@ -67,16 +116,79 @@ const checkNewTest = ({
67
116
  }
68
117
 
69
118
  /**
70
- * The red check. Pure: proves every criterion has a test, every new test
71
- * fails, and every old test that passed before still passes.
119
+ * Every old test (passed at the baseline) still passes, in its file or
120
+ * moved (#609): one missing from its file but now in another test file
121
+ * under the same full name, a case the baseline didn't have there. Each
122
+ * such case stands for one old test only, so a name several old tests had
123
+ * counts as moved once.
124
+ */
125
+ const checkOldTests = ({
126
+ baseline,
127
+ current,
128
+ }: {
129
+ baseline: TestRun
130
+ current: TestRun
131
+ }): { problems: string[]; notes: string[] } => {
132
+ const problems: string[] = []
133
+ const notes: string[] = []
134
+ const same = (a: TestCase, b: TestCase) =>
135
+ a.file === b.file && a.full_name === b.full_name
136
+ const movable = current.cases.filter(
137
+ (now) => !baseline.cases.some((old) => same(old, now))
138
+ )
139
+ const claimed = new Set<TestCase>()
140
+ for (const old of baseline.cases.filter(
141
+ ({ status }) => status === 'passed'
142
+ )) {
143
+ const label = `old test "${old.full_name}" in ${old.file}`
144
+ const now = current.cases.find((found) => same(found, old))
145
+ if (now !== undefined) {
146
+ if (now.status !== 'passed')
147
+ problems.push(`${label} no longer passes`)
148
+ continue
149
+ }
150
+ const moved = movable.find(
151
+ (found) => !claimed.has(found) && found.full_name === old.full_name
152
+ )
153
+ if (moved === undefined) {
154
+ problems.push(`${label} is missing`)
155
+ continue
156
+ }
157
+ claimed.add(moved)
158
+ const where = `old test "${old.full_name}" in ${moved.file} (moved from ${old.file})`
159
+ if (moved.status === 'passed')
160
+ notes.push(`${where}: still passes, as it should`)
161
+ else problems.push(`${where} no longer passes`)
162
+ }
163
+ return { problems, notes }
164
+ }
165
+
166
+ /**
167
+ * The red check. Pure: proves every criterion has a test, and at most
168
+ * `MAX_TESTS_PER_CRITERION`, every test the ticket added is mapped, every
169
+ * test file it created is in the feature map (when the repo has one), every
170
+ * new test fails, and every old test that passed before still passes. A
171
+ * test the ticket only moved to another test file, under the same full
172
+ * name, is an old test (#609): noted, not missing and not new.
72
173
  *
73
174
  * @param baseline - The test run before the test-writer started.
74
175
  * @param current - The test run after the test-writer finished.
75
176
  * @param test_files - The worktree's test files, per the engine config.
76
177
  * @param sources - The text of each mapped test file, `null` if missing.
178
+ * @param changed_test_sources - Each test file the ticket changed, as it was
179
+ * at the ticket's base and as it is now: a new test is a name in the
180
+ * second the first doesn't have (#581), and no changed or deleted test
181
+ * file had at its base (a moved test, #609). For a file that has results in
182
+ * `current`, only a name of a case that ran counts, so a `test(` inside a
183
+ * string (a fixture) or a comment does not. A file that doesn't load yet
184
+ * has none, so its text alone decides, and every `test(` in it counts.
185
+ * @param feature_map - The repo's feature map, when the config names one
186
+ * (#583): its `path`, and `files`, the test files it lists (from
187
+ * `featureMapFiles`), `null` when the map file is missing. A test file the
188
+ * ticket created must be among them. Left out or `null`, nothing is checked.
77
189
  *
78
190
  * @example
79
- * const result = checkRed({ criteria_ids: ['AC1'], mapping, baseline, current, test_files, sources })
191
+ * const result = checkRed({ criteria_ids: ['AC1'], mapping, baseline, current, test_files, sources, changed_test_sources })
80
192
  * if (!result.ok) console.error(result.problems)
81
193
  */
82
194
  export const checkRed = ({
@@ -86,6 +198,8 @@ export const checkRed = ({
86
198
  current,
87
199
  test_files,
88
200
  sources,
201
+ changed_test_sources,
202
+ feature_map,
89
203
  }: {
90
204
  criteria_ids: string[]
91
205
  mapping: CriterionTests[]
@@ -93,6 +207,8 @@ export const checkRed = ({
93
207
  current: TestRun
94
208
  test_files: string[]
95
209
  sources: Record<string, string | null>
210
+ changed_test_sources: ChangedTestSource[]
211
+ feature_map?: { path: string; files: string[] | null } | null
96
212
  }): RedCheckResult => {
97
213
  const problems: string[] = []
98
214
  const notes: string[] = []
@@ -107,6 +223,67 @@ export const checkRed = ({
107
223
  notes.push(`unknown criterion id ${criterion_id}`)
108
224
  }
109
225
  }
226
+ for (const { criterion_id, tests } of mapping) {
227
+ if (tests.length > MAX_TESTS_PER_CRITERION) {
228
+ problems.push(
229
+ `${criterion_id} has ${tests.length} tests; write at most ${MAX_TESTS_PER_CRITERION} per criterion`
230
+ )
231
+ }
232
+ }
233
+ // A name some changed (or deleted) test file had at the base is a moved
234
+ // test, not a new one, whichever file it is in now (#609).
235
+ const old_names = new Set(
236
+ changed_test_sources.flatMap(({ base }) =>
237
+ base === null ? [] : testNamesIn({ source: base })
238
+ )
239
+ )
240
+ for (const { file, base, current: text } of changed_test_sources) {
241
+ const mapped = mapping
242
+ .flatMap(({ tests }) => tests)
243
+ .filter((test) => test.file === file)
244
+ .map(({ name }) => name.trim())
245
+ // A file that loaded has results: a name in its text is a new test
246
+ // only if a case of that name ran, so a `test('x'` inside a string
247
+ // (a fixture) or a comment is not one. A file with none (it doesn't
248
+ // load yet) is read from its text alone.
249
+ const loaded = !current.files_without_results.includes(file)
250
+ const ran = current.cases.filter((found) => found.file === file)
251
+ for (const name of addedTestNames({ base, current: text })) {
252
+ if (old_names.has(name)) continue
253
+ const own = name.trim()
254
+ if (
255
+ loaded &&
256
+ !ran.some(({ full_name }) => namesTest({ full_name, own }))
257
+ ) {
258
+ continue
259
+ }
260
+ if (!mapped.some((full) => namesTest({ full_name: full, own }))) {
261
+ problems.push(
262
+ `"${name}" in ${file}: a new test the map doesn't name (map it to a criterion, or remove it)${loaded ? '' : NOT_LOADED_HINT}`
263
+ )
264
+ }
265
+ }
266
+ }
267
+ if (feature_map !== undefined && feature_map !== null) {
268
+ // A test file the ticket created (not one it edited) must be listed
269
+ // under a feature, so tests grow inside their feature's files (#583).
270
+ const created = changed_test_sources.filter(
271
+ ({ base, current: text }) => base === null && text !== null
272
+ )
273
+ if (created.length > 0 && feature_map.files === null) {
274
+ problems.push(
275
+ `The feature map ${feature_map.path} is missing: restore it, or add it with the new test file listed`
276
+ )
277
+ } else {
278
+ for (const { file } of created) {
279
+ if (!(feature_map.files ?? []).includes(file)) {
280
+ problems.push(
281
+ `${file}: a new test file the feature map doesn't list (add it under a feature in ${feature_map.path})`
282
+ )
283
+ }
284
+ }
285
+ }
286
+ }
110
287
  const testFiles = new Set(test_files)
111
288
  for (const test of mapping.flatMap(({ tests }) => tests)) {
112
289
  const outcome = checkNewTest({
@@ -118,17 +295,8 @@ export const checkRed = ({
118
295
  if ('problem' in outcome) problems.push(outcome.problem)
119
296
  else notes.push(outcome.note)
120
297
  }
121
- for (const old of baseline.cases.filter(
122
- ({ status }) => status === 'passed'
123
- )) {
124
- const now = current.cases.find(
125
- ({ file, full_name }) =>
126
- file === old.file && full_name === old.full_name
127
- )
128
- const label = `old test "${old.full_name}" in ${old.file}`
129
- if (now === undefined) problems.push(`${label} is missing`)
130
- else if (now.status !== 'passed')
131
- problems.push(`${label} no longer passes`)
132
- }
298
+ const old = checkOldTests({ baseline, current })
299
+ problems.push(...old.problems)
300
+ notes.push(...old.notes)
133
301
  return { ok: problems.length === 0, problems, notes }
134
302
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Runs one shell command as a named gate check (`shellCheck`), and the small
3
+ * clock the checks share (`msSince`). The install, prepare, types, lint, and
4
+ * `pass_fail` test checks all run through it.
5
+ */
6
+ import type { GateCheck, GateName } from './gate-schemas'
7
+
8
+ import {
9
+ clipOutput,
10
+ DEFAULT_COMMAND_TIMEOUT_MS,
11
+ runShell,
12
+ timedOutLine,
13
+ } from '../shell/run-command'
14
+
15
+ /** Milliseconds since `started` (from `performance.now()`), whole (#518). */
16
+ export const msSince = (started: number): number =>
17
+ Math.round(performance.now() - started)
18
+
19
+ /**
20
+ * Runs one shell command as a named check: `ok` on exit 0, with the clipped
21
+ * output only when it failed. One that runs past `timeout_ms` is stopped,
22
+ * with everything it started, and its output starts by saying so, then
23
+ * `timeout_hint`, if any. The check says how long the command ran (#518).
24
+ *
25
+ * @example
26
+ * const install = await shellCheck({ name: 'install', command: 'bun install', cwd })
27
+ */
28
+ export const shellCheck = async ({
29
+ name,
30
+ command,
31
+ cwd,
32
+ timeout_ms,
33
+ timeout_hint,
34
+ }: {
35
+ name: GateName
36
+ command: string
37
+ cwd: string
38
+ /** Defaults to `DEFAULT_COMMAND_TIMEOUT_MS`. */
39
+ timeout_ms?: number
40
+ /** How to give the command longer, for the timed-out message. */
41
+ timeout_hint?: string
42
+ }): Promise<GateCheck> => {
43
+ const limit = timeout_ms ?? DEFAULT_COMMAND_TIMEOUT_MS
44
+ const started = performance.now()
45
+ const result = await runShell({ command, cwd, timeout_ms: limit })
46
+ const ok = result.exit_code === 0
47
+ const timedOut = result.timed_out
48
+ ? timedOutLine({
49
+ timeout_ms: limit,
50
+ hint: timeout_hint,
51
+ survivors: result.survivors,
52
+ })
53
+ : ''
54
+ return {
55
+ name,
56
+ command,
57
+ ok,
58
+ exit_code: result.exit_code,
59
+ output: ok
60
+ ? ''
61
+ : `${timedOut}${clipOutput({ text: `${result.stdout}\n${result.stderr}` })}`,
62
+ duration_ms: msSince(started),
63
+ }
64
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Waits inside a step (#632): a check that must wait for a prepare slot or
3
+ * a test slot says so to whoever listens for the step it runs in, such as
4
+ * the run's live file. The listener rides along with the step's work (an
5
+ * `AsyncLocalStorage`), so the checks deep in the gate runner need no new
6
+ * argument. Nothing is journaled: the wait's length is already `wait_ms`.
7
+ */
8
+ import { AsyncLocalStorage } from 'node:async_hooks'
9
+
10
+ /** What a check waits for. */
11
+ export type SlotKind = 'prepare_slot' | 'test_slot'
12
+
13
+ /** A wait for a slot, as it starts: `in_use` of the `slots` are taken. */
14
+ export type SlotWait = { for: SlotKind; in_use: number; slots: number }
15
+
16
+ /** Hears a wait start; the function it gives back hears the slot taken. */
17
+ export type SlotWaitListener = (wait: SlotWait) => () => void
18
+
19
+ const LISTENER = new AsyncLocalStorage<SlotWaitListener>()
20
+
21
+ /**
22
+ * Runs `run` with `listener` hearing every slot wait inside it.
23
+ *
24
+ * @example
25
+ * await hearSlotWaits({ listener: live.slotWaits({ ticket: 11 }), run: () => runGates(...) })
26
+ */
27
+ export const hearSlotWaits = <T>({
28
+ listener,
29
+ run,
30
+ }: {
31
+ listener: SlotWaitListener
32
+ run: () => Promise<T>
33
+ }): Promise<T> => LISTENER.run(listener, run)
34
+
35
+ /**
36
+ * The `on_wait` for a take of `kind` slots, at most `limit` at once, or
37
+ * `undefined` when nobody listens. With no cap (`Infinity`), the slots are
38
+ * the ones in use.
39
+ */
40
+ export const onSlotWait = ({
41
+ kind,
42
+ limit,
43
+ }: {
44
+ kind: SlotKind
45
+ limit: number
46
+ }): ((in_use: number) => () => void) | undefined => {
47
+ const listener = LISTENER.getStore()
48
+ if (listener === undefined) return undefined
49
+ return (in_use) =>
50
+ listener({
51
+ for: kind,
52
+ in_use,
53
+ slots: Number.isFinite(limit) ? limit : in_use,
54
+ })
55
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The final review's slow tests (#563): the prepare command, then each slow
3
+ * test command, as `slow_test` checks.
4
+ */
5
+ import type { GateCheck } from './gate-schemas'
6
+ import { prepareCheck } from './prepare-check'
7
+ import { testChecks } from './test-checks'
8
+
9
+ import {
10
+ slowTestCommands,
11
+ testTimeoutOf,
12
+ type EngineConfig,
13
+ } from '../config/engine-config'
14
+
15
+ /**
16
+ * The final review's slow tests (#563), once round 1's lenses are in: the
17
+ * prepare command, if any (a failed one stops there), then each slow
18
+ * test command, in order, as `slow_test` checks with `test_timeout_ms`.
19
+ * `around_prepare`, if given, runs the prepare check, as in `runGates`.
20
+ *
21
+ * @example
22
+ * const { ok, checks } = await runSlowTests({ cwd, config, test_files, report_file })
23
+ */
24
+ export const runSlowTests = async ({
25
+ cwd,
26
+ config,
27
+ test_files,
28
+ report_file,
29
+ around_prepare,
30
+ }: {
31
+ cwd: string
32
+ config: EngineConfig
33
+ test_files: string[]
34
+ report_file: string
35
+ /** Wraps the prepare run. Left out, it runs as is. */
36
+ around_prepare?: (
37
+ prepare: () => Promise<GateCheck | null>
38
+ ) => Promise<GateCheck | null>
39
+ }): Promise<{ ok: boolean; checks: GateCheck[] }> => {
40
+ const prepare = () => prepareCheck({ config, cwd })
41
+ const prepared = await (around_prepare?.(prepare) ?? prepare())
42
+ if (prepared !== null && !prepared.ok) {
43
+ return { ok: false, checks: [prepared] }
44
+ }
45
+ const { checks } = await testChecks({
46
+ commands: slowTestCommands({ config }),
47
+ name: 'slow_test',
48
+ cwd,
49
+ test_files,
50
+ report_file,
51
+ first_index: 0,
52
+ timeout_ms: testTimeoutOf({ config }),
53
+ limit: null,
54
+ test_concurrency: config.test_concurrency,
55
+ exclusive: false,
56
+ })
57
+ const all = prepared === null ? checks : [prepared, ...checks]
58
+ return { ok: all.every(({ ok }) => ok), checks: all }
59
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The test commands as gate checks (`testChecks`): `pass_fail` commands judged
3
+ * by their exit code, `bun` commands run with bun's JUnit reporter, each
4
+ * waiting for a test slot first. Also the fast test speed limit (#580).
5
+ */
6
+ import type { GateCheck, TestRun } from './gate-schemas'
7
+ import { msSince, shellCheck } from './shell-check'
8
+ import {
9
+ reportFileFor,
10
+ runTestsInHeldSlot,
11
+ TEST_TIMEOUT_HINT,
12
+ } from './test-runner'
13
+ import { inTestSlot } from './test-slots'
14
+
15
+ import type { TestCommand } from '../config/engine-config'
16
+
17
+ /** What to do about a fast test over the speed limit (#580). */
18
+ export const FAST_TEST_HINT = ({ limit_ms }: { limit_ms: number }): string =>
19
+ `A fast test may take at most ${limit_ms} ms (\`test_case_timeout_ms\`). Make it faster, or move it to the slow tests.`
20
+
21
+ /** The fast test speed limit (#580), and the files measured against it. */
22
+ export type SpeedLimit = { limit_ms: number; measured_files: string[] }
23
+
24
+ /**
25
+ * A `bun` test command's run as a gate check named `name`: it fails on a
26
+ * failed run, or on a passing test over the fast test speed limit (see
27
+ * `testChecks`), and it ran from `started` until now.
28
+ */
29
+ const bunCheck = ({
30
+ name,
31
+ run,
32
+ limit,
33
+ started,
34
+ }: {
35
+ name: 'test' | 'slow_test'
36
+ run: TestRun
37
+ limit: SpeedLimit | null
38
+ started: number
39
+ }): GateCheck => {
40
+ const tooSlow =
41
+ limit === null
42
+ ? []
43
+ : run.cases.filter(
44
+ ({ file, status, duration_ms }) =>
45
+ status === 'passed' &&
46
+ limit.measured_files.includes(file) &&
47
+ (duration_ms ?? 0) > limit.limit_ms
48
+ )
49
+ const timedOut = /timed out after \d+ms/.test(run.output)
50
+ const hint =
51
+ limit === null || (tooSlow.length === 0 && !timedOut)
52
+ ? ''
53
+ : `${FAST_TEST_HINT({ limit_ms: limit.limit_ms })}\n`
54
+ const slowLines = tooSlow
55
+ .map(
56
+ ({ file, full_name, duration_ms }) =>
57
+ `- ${file} > ${full_name}: ${duration_ms} ms`
58
+ )
59
+ .join('\n')
60
+ const ok = run.ok && tooSlow.length === 0
61
+ return {
62
+ name,
63
+ command: run.command,
64
+ ok,
65
+ exit_code: run.exit_code,
66
+ output: ok
67
+ ? ''
68
+ : `${hint}${slowLines === '' ? '' : `Too slow for a fast test:\n${slowLines}\n`}${run.output}`,
69
+ duration_ms: msSince(started),
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Runs test commands as gate checks named `name`, in order (see `runGates`).
75
+ * With a `limit` (#580, the fast call only), each `bun` command gets
76
+ * `--timeout=<limit_ms>`, and a passing test in one of `measured_files` that
77
+ * took longer than that fails the check: a test's own timeout option
78
+ * overrides `--timeout`, so only its measured time tells.
79
+ *
80
+ * With `test_concurrency` (#544), each command first waits for a test slot
81
+ * (see `inTestSlot`); its check records the wait as `wait_ms`, and its time
82
+ * limit and `duration_ms` start after it. With `exclusive` (a lone rerun,
83
+ * #544), each command waits until no other test run is going, and holds
84
+ * every slot while it runs.
85
+ */
86
+ export const testChecks = async ({
87
+ commands,
88
+ name,
89
+ cwd,
90
+ test_files,
91
+ report_file,
92
+ first_index,
93
+ timeout_ms,
94
+ limit,
95
+ test_concurrency,
96
+ exclusive,
97
+ }: {
98
+ commands: TestCommand[]
99
+ name: 'test' | 'slow_test'
100
+ cwd: string
101
+ test_files: string[]
102
+ report_file: string
103
+ /** The first command's index, so each command gets its own report file. */
104
+ first_index: number
105
+ timeout_ms: number
106
+ /**
107
+ * The fast test speed limit (#580), or `null` for none. Only the fast
108
+ * call has one; slow tests never do.
109
+ */
110
+ limit: SpeedLimit | null
111
+ /** The config's `test_concurrency`; left out, no cap. */
112
+ test_concurrency: number | undefined
113
+ exclusive: boolean
114
+ }): Promise<{ checks: GateCheck[]; runs: TestRun[] }> => {
115
+ const checks: GateCheck[] = []
116
+ const runs: TestRun[] = []
117
+ for (const [offset, { run: command, results }] of commands.entries()) {
118
+ if (results === 'pass_fail') {
119
+ checks.push(
120
+ await inTestSlot({
121
+ test_concurrency,
122
+ exclusive,
123
+ work: () =>
124
+ shellCheck({
125
+ name,
126
+ command,
127
+ cwd,
128
+ timeout_ms,
129
+ timeout_hint: TEST_TIMEOUT_HINT,
130
+ }),
131
+ })
132
+ )
133
+ continue
134
+ }
135
+ checks.push(
136
+ await inTestSlot({
137
+ test_concurrency,
138
+ exclusive,
139
+ work: async () => {
140
+ const started = performance.now()
141
+ const run = await runTestsInHeldSlot({
142
+ cwd,
143
+ command:
144
+ limit === null
145
+ ? command
146
+ : `${command} --timeout=${limit.limit_ms}`,
147
+ test_files,
148
+ report_file: reportFileFor({
149
+ report_file,
150
+ index: first_index + offset,
151
+ }),
152
+ timeout_ms,
153
+ })
154
+ runs.push(run)
155
+ return bunCheck({ name, run, limit, started })
156
+ },
157
+ })
158
+ )
159
+ }
160
+ return { checks, runs }
161
+ }