@hominis/fireforge 0.49.2 → 0.50.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 (334) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +1 -1
  3. package/dist/bin/fireforge.js +6 -0
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/commands/build-prelock.d.ts +20 -0
  6. package/dist/src/commands/build-prelock.js +50 -0
  7. package/dist/src/commands/build.d.ts +3 -1
  8. package/dist/src/commands/build.js +13 -6
  9. package/dist/src/commands/check-report.d.ts +28 -0
  10. package/dist/src/commands/check-report.js +78 -0
  11. package/dist/src/commands/check-run.d.ts +28 -0
  12. package/dist/src/commands/check-run.js +104 -0
  13. package/dist/src/commands/check-steps.d.ts +67 -0
  14. package/dist/src/commands/check-steps.js +175 -0
  15. package/dist/src/commands/check.d.ts +32 -0
  16. package/dist/src/commands/check.js +160 -0
  17. package/dist/src/commands/config.js +5 -6
  18. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  19. package/dist/src/commands/doctor-source-pin.js +4 -1
  20. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  21. package/dist/src/commands/export-adopt-check.js +98 -0
  22. package/dist/src/commands/export-flow.js +29 -14
  23. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  24. package/dist/src/commands/export-shared.d.ts +2 -1
  25. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  26. package/dist/src/commands/export-staged-registration.js +91 -0
  27. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  28. package/dist/src/commands/forward-import-remedy.js +63 -0
  29. package/dist/src/commands/furnace/apply.js +3 -3
  30. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  32. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  33. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  34. package/dist/src/commands/furnace/create.js +2 -2
  35. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  36. package/dist/src/commands/furnace/deploy-named.js +107 -0
  37. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  38. package/dist/src/commands/furnace/deploy.js +50 -86
  39. package/dist/src/commands/furnace/diff.js +26 -3
  40. package/dist/src/commands/furnace/index.js +66 -41
  41. package/dist/src/commands/furnace/init.js +13 -6
  42. package/dist/src/commands/furnace/override.js +4 -2
  43. package/dist/src/commands/furnace/preview.js +4 -3
  44. package/dist/src/commands/furnace/refresh.js +2 -2
  45. package/dist/src/commands/furnace/remove.js +4 -3
  46. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  47. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  48. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  49. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  50. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  51. package/dist/src/commands/furnace/rename-registrations.js +93 -0
  52. package/dist/src/commands/furnace/rename.d.ts +2 -1
  53. package/dist/src/commands/furnace/rename.js +28 -66
  54. package/dist/src/commands/furnace/scan.d.ts +9 -4
  55. package/dist/src/commands/furnace/scan.js +11 -9
  56. package/dist/src/commands/furnace/sync.js +3 -2
  57. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  58. package/dist/src/commands/furnace/validate-report.js +91 -0
  59. package/dist/src/commands/furnace/validate.d.ts +1 -1
  60. package/dist/src/commands/furnace/validate.js +127 -114
  61. package/dist/src/commands/furnace/validation-output.js +14 -3
  62. package/dist/src/commands/lint-per-patch.js +31 -60
  63. package/dist/src/commands/lint-report.d.ts +8 -4
  64. package/dist/src/commands/lint-report.js +15 -5
  65. package/dist/src/commands/lint.js +15 -34
  66. package/dist/src/commands/manifest.js +2 -0
  67. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  68. package/dist/src/commands/patch/delete.d.ts +1 -1
  69. package/dist/src/commands/patch/delete.js +13 -98
  70. package/dist/src/commands/patch/index.d.ts +1 -1
  71. package/dist/src/commands/patch/index.js +4 -2
  72. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  73. package/dist/src/commands/patch/lint-ignore.js +65 -28
  74. package/dist/src/commands/patch/move-files-create.js +26 -17
  75. package/dist/src/commands/patch/move-files-into.js +5 -3
  76. package/dist/src/commands/patch/move-files.js +3 -1
  77. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  78. package/dist/src/commands/patch/removal-dependents.js +133 -0
  79. package/dist/src/commands/patch/rename.js +7 -0
  80. package/dist/src/commands/patch/retire.d.ts +34 -0
  81. package/dist/src/commands/patch/retire.js +128 -0
  82. package/dist/src/commands/patch/split-plan.d.ts +7 -7
  83. package/dist/src/commands/patch/split-plan.js +45 -28
  84. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  85. package/dist/src/commands/patch/split-staged.js +202 -0
  86. package/dist/src/commands/patch/split.d.ts +5 -0
  87. package/dist/src/commands/patch/split.js +51 -15
  88. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  89. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  90. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  91. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  92. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  93. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  94. package/dist/src/commands/patch/staged-dependency.js +14 -41
  95. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  96. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  97. package/dist/src/commands/re-export-adjacent.d.ts +45 -2
  98. package/dist/src/commands/re-export-adjacent.js +156 -37
  99. package/dist/src/commands/re-export-drift.d.ts +36 -8
  100. package/dist/src/commands/re-export-drift.js +73 -49
  101. package/dist/src/commands/re-export-files.js +11 -8
  102. package/dist/src/commands/re-export-lint.d.ts +2 -1
  103. package/dist/src/commands/re-export-lint.js +23 -10
  104. package/dist/src/commands/re-export-options.js +45 -4
  105. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  106. package/dist/src/commands/re-export-preflight.js +160 -0
  107. package/dist/src/commands/re-export-register.js +42 -17
  108. package/dist/src/commands/re-export-removed.d.ts +12 -1
  109. package/dist/src/commands/re-export-removed.js +41 -11
  110. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  111. package/dist/src/commands/re-export-scan-owner.js +116 -0
  112. package/dist/src/commands/re-export-scan.d.ts +17 -9
  113. package/dist/src/commands/re-export-scan.js +56 -23
  114. package/dist/src/commands/re-export-single.js +22 -6
  115. package/dist/src/commands/re-export-summary.d.ts +28 -0
  116. package/dist/src/commands/re-export-summary.js +36 -0
  117. package/dist/src/commands/re-export.js +37 -24
  118. package/dist/src/commands/rebase/index.js +17 -17
  119. package/dist/src/commands/run.js +16 -2
  120. package/dist/src/commands/status-check.d.ts +55 -16
  121. package/dist/src/commands/status-check.js +92 -37
  122. package/dist/src/commands/status-json.d.ts +22 -9
  123. package/dist/src/commands/status-json.js +47 -32
  124. package/dist/src/commands/status-output.js +3 -1
  125. package/dist/src/commands/status-scope.d.ts +37 -0
  126. package/dist/src/commands/status-scope.js +54 -0
  127. package/dist/src/commands/status.js +66 -23
  128. package/dist/src/commands/test-browser-preflight.js +19 -3
  129. package/dist/src/commands/test-build-furnace.d.ts +21 -0
  130. package/dist/src/commands/test-build-furnace.js +48 -0
  131. package/dist/src/commands/test-build-phase.js +47 -41
  132. package/dist/src/commands/test-checkjs.js +7 -0
  133. package/dist/src/commands/test-diagnose.js +7 -0
  134. package/dist/src/commands/test-modes.d.ts +16 -1
  135. package/dist/src/commands/test-modes.js +24 -1
  136. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  137. package/dist/src/commands/test-multi-verdict.js +111 -0
  138. package/dist/src/commands/test-orchestrate.d.ts +43 -0
  139. package/dist/src/commands/test-orchestrate.js +222 -0
  140. package/dist/src/commands/test-perf-scope.d.ts +29 -0
  141. package/dist/src/commands/test-perf-scope.js +83 -0
  142. package/dist/src/commands/test-prelock.d.ts +51 -0
  143. package/dist/src/commands/test-prelock.js +177 -0
  144. package/dist/src/commands/test-register.js +54 -10
  145. package/dist/src/commands/test-run.js +4 -1
  146. package/dist/src/commands/test-stale-gate.js +57 -27
  147. package/dist/src/commands/test-verdict.d.ts +10 -1
  148. package/dist/src/commands/test-verdict.js +28 -1
  149. package/dist/src/commands/test.d.ts +6 -1
  150. package/dist/src/commands/test.js +66 -60
  151. package/dist/src/commands/token-lint.d.ts +14 -0
  152. package/dist/src/commands/token-lint.js +111 -0
  153. package/dist/src/commands/token.js +10 -0
  154. package/dist/src/commands/verify-scope.d.ts +28 -0
  155. package/dist/src/commands/verify-scope.js +67 -0
  156. package/dist/src/commands/verify.d.ts +10 -2
  157. package/dist/src/commands/verify.js +59 -35
  158. package/dist/src/core/build-baseline-types.d.ts +46 -0
  159. package/dist/src/core/build-baseline-validate.js +19 -0
  160. package/dist/src/core/build-baseline.d.ts +5 -1
  161. package/dist/src/core/build-baseline.js +3 -1
  162. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  163. package/dist/src/core/build-overwrite-guard.js +84 -4
  164. package/dist/src/core/build-prepare.d.ts +7 -0
  165. package/dist/src/core/build-prepare.js +14 -8
  166. package/dist/src/core/build-run-record.d.ts +74 -0
  167. package/dist/src/core/build-run-record.js +136 -0
  168. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  169. package/dist/src/core/checkjs-support-heads.js +104 -0
  170. package/dist/src/core/config-paths.d.ts +2 -2
  171. package/dist/src/core/config-paths.js +8 -0
  172. package/dist/src/core/config-validate-check.d.ts +8 -0
  173. package/dist/src/core/config-validate-check.js +143 -0
  174. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  175. package/dist/src/core/config-validate-file-size.js +91 -2
  176. package/dist/src/core/config-validate-history.d.ts +11 -0
  177. package/dist/src/core/config-validate-history.js +38 -0
  178. package/dist/src/core/config-validate-patch-policy.js +100 -0
  179. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  180. package/dist/src/core/config-validate-token-scales.d.ts +6 -0
  181. package/dist/src/core/config-validate-token-scales.js +50 -0
  182. package/dist/src/core/config-validate.js +14 -5
  183. package/dist/src/core/config.d.ts +5 -0
  184. package/dist/src/core/config.js +16 -0
  185. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  186. package/dist/src/core/coverage-dispatch.js +44 -0
  187. package/dist/src/core/coverage-extend.d.ts +70 -45
  188. package/dist/src/core/coverage-extend.js +199 -144
  189. package/dist/src/core/coverage-records.d.ts +97 -0
  190. package/dist/src/core/coverage-records.js +350 -0
  191. package/dist/src/core/destructive.d.ts +7 -4
  192. package/dist/src/core/destructive.js +10 -10
  193. package/dist/src/core/engine-session-lock.d.ts +10 -1
  194. package/dist/src/core/engine-session-lock.js +28 -8
  195. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  196. package/dist/src/core/export-moved-code-shape.js +135 -44
  197. package/dist/src/core/file-lock-queue.d.ts +67 -0
  198. package/dist/src/core/file-lock-queue.js +221 -0
  199. package/dist/src/core/file-lock.d.ts +4 -25
  200. package/dist/src/core/file-lock.js +105 -138
  201. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  202. package/dist/src/core/furnace-apply-output.js +31 -1
  203. package/dist/src/core/furnace-apply.js +12 -2
  204. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  205. package/dist/src/core/furnace-composed-source.js +167 -0
  206. package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
  207. package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
  208. package/dist/src/core/furnace-config-validate.js +8 -0
  209. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  210. package/dist/src/core/furnace-css-fragments.js +26 -0
  211. package/dist/src/core/furnace-operation.d.ts +3 -3
  212. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  213. package/dist/src/core/furnace-registration-remove.js +53 -17
  214. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  215. package/dist/src/core/furnace-registration-rename.js +77 -0
  216. package/dist/src/core/furnace-registration.d.ts +14 -4
  217. package/dist/src/core/furnace-registration.js +44 -10
  218. package/dist/src/core/furnace-staleness.d.ts +7 -0
  219. package/dist/src/core/furnace-staleness.js +34 -24
  220. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  221. package/dist/src/core/furnace-validate-accessibility.js +10 -1
  222. package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
  223. package/dist/src/core/furnace-validate-exceptions.js +43 -0
  224. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  225. package/dist/src/core/furnace-validate-icon-button.js +49 -0
  226. package/dist/src/core/furnace-validate.js +1 -1
  227. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  228. package/dist/src/core/harness-port-fallback.js +96 -0
  229. package/dist/src/core/history-log-file.d.ts +61 -0
  230. package/dist/src/core/history-log-file.js +157 -0
  231. package/dist/src/core/history-log.d.ts +2 -1
  232. package/dist/src/core/history-log.js +2 -1
  233. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  234. package/dist/src/core/json-preserving-edit.js +199 -0
  235. package/dist/src/core/mach.js +8 -1
  236. package/dist/src/core/negative-control-diff.d.ts +39 -0
  237. package/dist/src/core/negative-control-diff.js +136 -0
  238. package/dist/src/core/negative-control.d.ts +138 -0
  239. package/dist/src/core/negative-control.js +356 -0
  240. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  241. package/dist/src/core/objdir-install-manifests.js +226 -0
  242. package/dist/src/core/patch-export-update.d.ts +2 -0
  243. package/dist/src/core/patch-export-update.js +1 -0
  244. package/dist/src/core/patch-export.js +3 -4
  245. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
  246. package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
  247. package/dist/src/core/patch-lint-checkjs.js +8 -11
  248. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  249. package/dist/src/core/patch-lint-code-lines.js +105 -0
  250. package/dist/src/core/patch-lint-cross.js +12 -4
  251. package/dist/src/core/patch-lint-css.d.ts +8 -2
  252. package/dist/src/core/patch-lint-css.js +10 -4
  253. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  254. package/dist/src/core/patch-lint-file-size.js +22 -11
  255. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
  256. package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
  257. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  258. package/dist/src/core/patch-lint-ignore.d.ts +152 -0
  259. package/dist/src/core/patch-lint-ignore.js +299 -0
  260. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  261. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  262. package/dist/src/core/patch-lint-size.d.ts +117 -0
  263. package/dist/src/core/patch-lint-size.js +323 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  265. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  266. package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
  267. package/dist/src/core/patch-lint-token-scale.js +109 -0
  268. package/dist/src/core/patch-lint.d.ts +11 -83
  269. package/dist/src/core/patch-lint.js +64 -286
  270. package/dist/src/core/patch-lock.js +7 -6
  271. package/dist/src/core/patch-manifest-consistency.js +7 -6
  272. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  273. package/dist/src/core/patch-manifest-io.js +19 -8
  274. package/dist/src/core/patch-manifest-validate.js +34 -2
  275. package/dist/src/core/patch-order-legal.d.ts +25 -0
  276. package/dist/src/core/patch-order-legal.js +83 -0
  277. package/dist/src/core/patch-policy-content.d.ts +40 -0
  278. package/dist/src/core/patch-policy-content.js +216 -0
  279. package/dist/src/core/patch-policy.d.ts +16 -3
  280. package/dist/src/core/patch-policy.js +16 -9
  281. package/dist/src/core/patch-retired.d.ts +35 -0
  282. package/dist/src/core/patch-retired.js +54 -0
  283. package/dist/src/core/patch-subset.d.ts +16 -0
  284. package/dist/src/core/patch-subset.js +66 -0
  285. package/dist/src/core/preflight-wait.d.ts +20 -0
  286. package/dist/src/core/preflight-wait.js +38 -0
  287. package/dist/src/core/rebase-from-version.d.ts +46 -0
  288. package/dist/src/core/rebase-from-version.js +71 -0
  289. package/dist/src/core/run-log.d.ts +20 -0
  290. package/dist/src/core/run-log.js +89 -3
  291. package/dist/src/core/run-profile.d.ts +7 -0
  292. package/dist/src/core/run-profile.js +7 -1
  293. package/dist/src/core/smoke-patterns.d.ts +5 -0
  294. package/dist/src/core/smoke-patterns.js +13 -0
  295. package/dist/src/core/test-harness-crash.d.ts +18 -1
  296. package/dist/src/core/test-harness-crash.js +6 -4
  297. package/dist/src/core/test-harness-signature.d.ts +17 -0
  298. package/dist/src/core/test-harness-signature.js +4 -0
  299. package/dist/src/core/test-run-completeness.d.ts +57 -0
  300. package/dist/src/core/test-run-completeness.js +144 -0
  301. package/dist/src/core/test-stale-check.d.ts +27 -1
  302. package/dist/src/core/test-stale-check.js +39 -49
  303. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  304. package/dist/src/core/test-stale-exempt.js +70 -0
  305. package/dist/src/core/token-lint.d.ts +50 -0
  306. package/dist/src/core/token-lint.js +126 -0
  307. package/dist/src/core/tree-guard.js +3 -0
  308. package/dist/src/core/typecheck-shim.d.ts +1 -1
  309. package/dist/src/core/typecheck-shim.js +90 -2
  310. package/dist/src/index.d.ts +8 -3
  311. package/dist/src/index.js +3 -1
  312. package/dist/src/types/commands/index.d.ts +2 -2
  313. package/dist/src/types/commands/options.d.ts +148 -21
  314. package/dist/src/types/commands/patches.d.ts +75 -1
  315. package/dist/src/types/config.d.ts +192 -0
  316. package/dist/src/types/config.js +9 -0
  317. package/dist/src/types/furnace.d.ts +43 -1
  318. package/dist/src/types/furnace.js +7 -0
  319. package/dist/src/types/index.d.ts +1 -1
  320. package/dist/src/utils/options.d.ts +34 -6
  321. package/dist/src/utils/options.js +86 -5
  322. package/dist/src/utils/path-glob.d.ts +15 -0
  323. package/dist/src/utils/path-glob.js +47 -0
  324. package/dist/src/utils/regex.d.ts +15 -3
  325. package/dist/src/utils/regex.js +37 -16
  326. package/docs/configuration.md +282 -9
  327. package/docs/exit-codes.md +18 -1
  328. package/docs/furnace.md +97 -3
  329. package/docs/lifecycle-invariants.md +52 -19
  330. package/docs/machine-output.md +74 -12
  331. package/docs/patch-workflow.md +297 -22
  332. package/docs/run-logs.md +20 -1
  333. package/docs/testing.md +276 -20
  334. package/package.json +1 -1
@@ -0,0 +1,177 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The `fireforge test` checks that run BEFORE the engine session lock.
4
+ *
5
+ * Everything here reads, never writes, so none of it needs the lock, and
6
+ * each check used to cost a full place in the lock queue before it could
7
+ * refuse:
8
+ *
9
+ * - a missing launchable binary (a peer's interrupted build) refused only
10
+ * after the wait, in every session, with nothing naming the cause;
11
+ * - the named-file checkJs pass refused an `Element | null` after an
12
+ * eighteen-minute queue;
13
+ * - `--wait-browser` and `--wait-port` spent their budget holding the
14
+ * lock, so every peer queued behind a run that was doing nothing.
15
+ *
16
+ * The in-lock ladder in `test.ts` still runs in full afterwards. That is
17
+ * the "re-check once under the lock": the waits this pass spent are
18
+ * stripped from the options it returns, so the in-lock browser and port
19
+ * checks probe once instead of waiting again.
20
+ */
21
+ import { join } from 'node:path';
22
+ import { describeBuildRunState, readBuildRunState } from '../core/build-run-record.js';
23
+ import { getProjectPaths, loadConfig } from '../core/config.js';
24
+ import { assertEngineExists } from '../core/engine-precondition.js';
25
+ import { hasBuildArtifacts, hasRunnableBundle } from '../core/mach.js';
26
+ import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
27
+ import { assertMarionettePortAvailable, ensureLaunchableBrowserNotRunning, extractForwardedMarionettePort, } from '../core/marionette-port.js';
28
+ import { ensureMochitestServerPortAvailable } from '../core/mochitest-server-port.js';
29
+ import { waitBeforeLock } from '../core/preflight-wait.js';
30
+ import { PreflightRefusalError } from '../errors/base.js';
31
+ import { info } from '../utils/logger.js';
32
+ import { resolveWaitLockSeconds } from '../utils/options.js';
33
+ import { stripEnginePrefix } from '../utils/paths.js';
34
+ import { checkRequestedTestTypes } from './test-checkjs.js';
35
+ import { assertPathlessTestMode, classifyBeforeDispatch, resolveCanaryPath } from './test-modes.js';
36
+ /**
37
+ * Resolves the launchable binary a test run needs, or refuses naming why
38
+ * it is missing. The cause comes from the build run record, which any
39
+ * session's build writes, so a session that never ran the build can still
40
+ * say "a peer's build was interrupted" rather than only "the binary is
41
+ * missing".
42
+ *
43
+ * @param projectRoot - Project root
44
+ * @param engineDir - Absolute engine directory
45
+ * @param binaryName - Configured binary name
46
+ * @param objDir - Active objdir, when one exists
47
+ * @returns The engine-relative launchable path, or undefined without an objdir
48
+ */
49
+ export async function resolveLaunchablePathForTests(projectRoot, engineDir, binaryName, objDir) {
50
+ if (!objDir)
51
+ return undefined;
52
+ const bundleCheck = await hasRunnableBundle(engineDir, binaryName, objDir);
53
+ if (bundleCheck.runnable)
54
+ return bundleCheck.expectedPath;
55
+ const expectedSuffix = bundleCheck.expectedPath
56
+ ? ` (expected at engine/${bundleCheck.expectedPath})`
57
+ : '';
58
+ const cause = describeBuildRunState(await readBuildRunState(projectRoot));
59
+ throw new PreflightRefusalError(`Tests require a complete launchable build${expectedSuffix}. ` +
60
+ 'The obj-*/dist/ tree exists but the launchable binary is missing' +
61
+ (cause !== undefined
62
+ ? `.\n\n${cause}\n\n`
63
+ : ' — typically the result of an interrupted or partially failed `fireforge build`.\n\n') +
64
+ 'Run "fireforge build" again and let it finish before retrying "fireforge test".', 'no-launchable-binary');
65
+ }
66
+ /** Build-artifact preflight wording for `fireforge test`. */
67
+ export const TEST_BUILD_PREFLIGHT = {
68
+ label: 'Tests',
69
+ requirement: 'Tests require a completed build.',
70
+ remediation: "Run 'fireforge build' first, then run 'fireforge test'.",
71
+ requireExisting: true,
72
+ };
73
+ /**
74
+ * The head of the in-lock ladder, run early: argument shape, engine, build
75
+ * artifacts and config. Each refuses with exactly the message it gives
76
+ * under the lock. Returns undefined when there is no objdir to check
77
+ * further.
78
+ */
79
+ async function loadPreLockContext(projectRoot, testPaths, options) {
80
+ assertPathlessTestMode(testPaths, options);
81
+ const paths = getProjectPaths(projectRoot);
82
+ await assertEngineExists(paths.engine);
83
+ const buildCheck = await hasBuildArtifacts(paths.engine);
84
+ assertBuildArtifacts(paths.engine, buildCheck, TEST_BUILD_PREFLIGHT);
85
+ const config = await loadConfig(projectRoot);
86
+ return buildCheck.objDir === undefined ? undefined : { paths, config, objDir: buildCheck.objDir };
87
+ }
88
+ /**
89
+ * Runs the lock-free checks and waits, and returns the options the in-lock
90
+ * run should use: with `waitBrowser`/`waitPort` removed when their budget
91
+ * was already spent here.
92
+ *
93
+ * @param projectRoot - Project root
94
+ * @param testPaths - Requested test paths
95
+ * @param options - Parsed test options
96
+ */
97
+ export async function runTestPreLockPreflight(projectRoot, testPaths, options) {
98
+ const context = await loadPreLockContext(projectRoot, testPaths, options);
99
+ if (context === undefined)
100
+ return options;
101
+ const { paths, config, objDir } = context;
102
+ let launchablePath;
103
+ try {
104
+ launchablePath = await resolveLaunchablePathForTests(projectRoot, paths.engine, config.binaryName, objDir);
105
+ }
106
+ catch (error) {
107
+ // A build in progress holds the engine lock and relinks the bundle
108
+ // before it lets go, so this run queues behind it rather than refusing.
109
+ if ((await readBuildRunState(projectRoot)).kind !== 'running')
110
+ throw error;
111
+ info('The launchable binary is missing while a build runs; queueing behind it.');
112
+ }
113
+ const canaryPath = resolveCanaryPath(options, config);
114
+ const normalizedPaths = (canaryPath !== undefined ? [canaryPath] : testPaths).map((path) => stripEnginePrefix(path).trim());
115
+ if (options.build === true || options.buildOnly === true) {
116
+ await checkRequestedTestTypes(paths, config, normalizedPaths);
117
+ }
118
+ return waitForResourcesBeforeLock(paths.engine, config, options, {
119
+ launchablePath,
120
+ normalizedPaths,
121
+ });
122
+ }
123
+ /** `options` without one wait budget, which this pass already spent. */
124
+ function withoutOption(options, key) {
125
+ const rest = { ...options };
126
+ if (key === 'waitBrowser')
127
+ delete rest.waitBrowser;
128
+ else
129
+ delete rest.waitPort;
130
+ return rest;
131
+ }
132
+ /**
133
+ * Spends `--wait-browser` and `--wait-port` before the lock, mirroring
134
+ * which in-lock checks would apply to this run, and strips each budget it
135
+ * spent from the returned options.
136
+ */
137
+ async function waitForResourcesBeforeLock(engineDir, config, options, request) {
138
+ if (options.waitBrowser === undefined && options.waitPort === undefined)
139
+ return options;
140
+ let xpcshellOnly;
141
+ try {
142
+ ({ xpcshellOnly } = await classifyBeforeDispatch(engineDir, request.normalizedPaths, {
143
+ allowMixed: true,
144
+ }));
145
+ }
146
+ catch {
147
+ return options;
148
+ }
149
+ const kill = options.killStaleMarionette === true;
150
+ let result = options;
151
+ const building = options.build === true || options.buildOnly === true;
152
+ if (options.waitBrowser !== undefined &&
153
+ request.launchablePath !== undefined &&
154
+ (building || !xpcshellOnly)) {
155
+ const binary = join(engineDir, request.launchablePath);
156
+ await waitBeforeLock(() => ensureLaunchableBrowserNotRunning(binary), resolveWaitLockSeconds(options.waitBrowser),
157
+ // A stale browser the run may reap is not worth waiting out; the
158
+ // in-lock check terminates it.
159
+ kill ? ['stale-browser'] : []);
160
+ result = withoutOption(result, 'waitBrowser');
161
+ }
162
+ // Port waits stay under the lock with --kill-stale-marionette: a stale
163
+ // listener reads as busy until the in-lock pass reaps it, so waiting it
164
+ // out here would only burn the budget.
165
+ if (options.waitPort !== undefined && options.buildOnly !== true && !kill) {
166
+ const seconds = resolveWaitLockSeconds(options.waitPort);
167
+ if (!xpcshellOnly) {
168
+ await waitBeforeLock(() => ensureMochitestServerPortAvailable(undefined, { engineDir }), seconds);
169
+ }
170
+ if (!xpcshellOnly || options.doctor === true) {
171
+ const port = options.marionettePort ?? extractForwardedMarionettePort(options.machArg ?? []);
172
+ await waitBeforeLock(() => assertMarionettePortAvailable(port, { binaryName: config.binaryName }), seconds);
173
+ }
174
+ result = withoutOption(result, 'waitPort');
175
+ }
176
+ return result;
177
+ }
@@ -26,12 +26,33 @@ const TEST_HELP_TEXT = [
26
26
  'not exercise cross-argument state; --no-shard restores the',
27
27
  'combined single-instance invocation.',
28
28
  '',
29
+ 'Mixed xpcshell and browser/mochitest paths run as two consecutive',
30
+ 'mach invocations (xpcshell first) under one lock hold and one',
31
+ 'verdict; with --build, one pre-test build packages both halves.',
32
+ '--repeat <n> and --negative-control <diff> run under one lock hold',
33
+ 'too. Each sub-run prints a FIREFORGE-RUN: line (its own verdict',
34
+ 'plus run=<i>/<n>) before the single aggregate verdict.',
35
+ '',
36
+ 'A dev objdir symlinks chrome JS/CSS and widget files into engine/,',
37
+ 'so the browser reads engine/ sources when it loads them, after the',
38
+ 'build step: an edit made while a suite runs reaches that suite, and',
39
+ 'a hand-made neuter must stay until the verdict line, not until',
40
+ '"Build complete", since one restored early is gone for later',
41
+ 'shards. --negative-control applies its diff after the build and',
42
+ 'restores it byte-exact only after the run, before the lock is',
43
+ 'released.',
44
+ '',
29
45
  'Machine-readable verdict: every run (single, sharded, --canary,',
30
46
  '--doctor) ends with exactly one raw stdout line',
31
47
  ' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|harness-arguments|test-failures|',
32
48
  ' no-tests|preflight|inconclusive|lock-timeout|killed] [checks=<n>]',
33
- ' [unexpected=<n>] [shards=<p>/<t>] [(<note>)] [shuffle=<seed>]',
34
- ' [log=<path>]',
49
+ ' [unexpected=<n>] [started=<n>/<m> first-unstarted=<path>]',
50
+ ' [shards=<p>/<t>] [(<note>)] [shuffle=<seed>]',
51
+ ' [runs=<p>/<n>] [control=caught|missed|inconclusive]',
52
+ ' [furnace-applied=<names>|furnace-apply=skipped] [log=<path>]',
53
+ "control= is a negative control's outcome: caught (went red on test",
54
+ 'failures, as a control should; exit 0), missed (stayed green; exit',
55
+ '5) or inconclusive.',
35
56
  'shuffle=<seed> is the seed a --shuffle run exported to the harness.',
36
57
  "log=<path> names this run's complete output, written to",
37
58
  '.fireforge/logs/ as the run streams, so a piped or truncated run',
@@ -39,7 +60,11 @@ const TEST_HELP_TEXT = [
39
60
  'The status follows the harness classifier, not the raw exit code:',
40
61
  'a crash-classified run says FAIL reason=crash even at exit 0, and',
41
62
  'a green-summary-override pass says PASS despite a non-zero mach',
42
- 'exit. reason=preflight covers any failure before the harness ran',
63
+ 'exit. An exit-0 run is still FAIL reason=test-failures when its',
64
+ 'summary reports unexpected results, the browser shut down mid-test,',
65
+ 'or a requested file never started; started=/first-unstarted= then',
66
+ 'name how far it got and where a re-run has to pick up.',
67
+ 'reason=preflight covers any failure before the harness ran',
43
68
  '(missing/stale build, invalid paths, port conflicts, config',
44
69
  'errors); reason=inconclusive means engine/ or perf power source',
45
70
  'changed while tests ran, so the result cannot be trusted;',
@@ -56,13 +81,33 @@ const TEST_HELP_TEXT = [
56
81
  '"(headed run stalled with the display asleep)" marks a no-output',
57
82
  'stall whose display was measured asleep. Key on this line',
58
83
  'instead of mach internals.',
84
+ '',
85
+ 'Lock-free checks run before the engine lock is requested: a missing',
86
+ 'launchable binary (with the build that left it missing, when one is',
87
+ 'recorded), the named-file checkJs pass of --build, and the',
88
+ '--wait-browser/--wait-port budgets. Each is re-checked once under the',
89
+ 'lock.',
59
90
  ].join('\n');
91
+ function registerTestMultiRunOptions(test) {
92
+ return test
93
+ .option('--repeat <n>', 'Run the same paths n times under one engine-lock hold (a --build builds once); prints each run and aggregates runs=<passed>/<n> on the verdict', commanderArgParser((raw) => parsePositiveIntegerFlag('--repeat', raw)))
94
+ .option('--negative-control <diff>', 'Apply this reviewed git diff (engine/ or components/ sources; project-relative path) after the pre-test build and right before dispatch, restore it byte-exact after the run (also on failure, SIGINT and SIGTERM) before the lock is released, and report control=caught|missed|inconclusive on the verdict')
95
+ .option('--auto-ports', "Let a browser run proceed when the mochitest server or Marionette port is held by something that is not this checkout's own: the engine's harness falls back to a free port itself. Same as test.mochitestPorts: \"auto\". This checkout's own leftovers are still refused.");
96
+ }
60
97
  function registerTestSharingOptions(test) {
61
98
  return test
62
99
  .option('--wait-browser [seconds]', 'Wait for this objdir browser before any deployment/build (default 60)', commanderArgParser((raw) => resolveWaitLockSeconds(raw)))
63
100
  .option('--wait-port [seconds]', 'Wait for the mochitest and Marionette ports (default 60)', commanderArgParser((raw) => resolveWaitLockSeconds(raw)))
101
+ .option('--restart-between-tests', "Forward mochitest's --restart-between-tests: restart the browser between test files to expose undocumented cross-file dependencies (the shape a manifest's standalone key asks for). Mochitest dispatch only.")
102
+ .option('--keep-log <path>', "Also write this run's log to <path> (a directory receives it under its own name), outside .fireforge/logs/ retention. FIREFORGE_LOG_KEEP sets a session default.")
64
103
  .option('--profile-file <source=destination>', 'Merge a file into the harness profile, e.g. sheet.css=chrome/userChrome.css. Repeatable.', ...stringListOption());
65
104
  }
105
+ /** Pre-test build options that shape the coverage claim and the Furnace step. */
106
+ function registerTestBuildOptions(test) {
107
+ return test
108
+ .option('--extend-coverage', 'With --build/--build-only and explicit paths, refuse instead of dropping any earlier packaging claim. Scoped builds keep each earlier path by default while its own staging inputs (manifest directory and the fixtures its manifest names) and engine/mozconfig are unchanged; this flag turns a drop into a refusal before the build.')
109
+ .option('--no-furnace-apply', 'With --build/--build-only, skip the Furnace apply so the build packages engine/ as it stands; names the components whose undeployed components/ edits it leaves out');
110
+ }
66
111
  /** Registers the test command on the CLI program. */
67
112
  export function registerTest(program, { getProjectRoot, withErrorHandling }) {
68
113
  const test = program
@@ -72,7 +117,6 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
72
117
  .option('--full-output', "Pass mozbuild's full verbosity through for the TEST phase. mozbuild quiets terminal output to warnings and errors when it detects a coding agent, which removes TEST_START and console INFO — the lines a hang or stall diagnosis needs. This unsets that detection for test dispatches only; the build phase stays quiet.")
73
118
  .option('--build', 'Run incremental UI build before testing')
74
119
  .option('--build-only', 'Run the scoped pre-test build (packaging exactly [paths...], mixed harnesses allowed) and exit without dispatching tests — one build covers both harness halves, then run each half without --build')
75
- .option('--extend-coverage', 'With --build/--build-only and explicit paths, union those paths into the recorded test-packaging coverage instead of replacing it, so an earlier scoped build stays covered. Refused when the build anchor moved (engine HEAD, mozconfig, or a previously fingerprinted packageable file changed).')
76
120
  .option('--refuse-unexported-drift', 'With --build/--build-only, refuse (instead of warning) when the pre-test build would overwrite engine content recorded in neither a patch body nor the pristine baseline — the same belt as "fireforge build --refuse-unexported-drift", armable from the scripted "test --build" shape the hazard actually shows up in')
77
121
  .option('--auto', 'Forward mach test --auto. Valid only when no explicit paths are provided.')
78
122
  .option('--allow-stale-build', 'Allow tests to run even when packageable engine files changed since the last successful FireForge build')
@@ -83,7 +127,7 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
83
127
  .option('--canary [path]', 'Run one short browser-chrome harness canary. Uses test.canaryPath from fireforge.json when no path is supplied.')
84
128
  .option('--doctor', 'Run a marionette handshake preflight before tests (exit 1 on FAIL). With no paths, runs the preflight only.')
85
129
  .option('--mach-arg <arg>', 'Forward this argument verbatim to `mach test` (repeatable). Escape valve for upstream xpcshell/mochitest flags FireForge does not model.', ...stringListOption())
86
- .option('--shuffle [seed]', 'Shuffle isolated mochitest path arguments in a repeatable seeded order (fresh seed when omitted). Native mach --shuffle within each invocation remains unseeded. Export FIREFORGE_SHUFFLE_SEED for custom task code and stamp shuffle=<seed> on the verdict. --no-shard disables the seeded argument permutation. Write --shuffle=<seed> or place it after the paths.', commanderArgParser((raw) => parsePositiveIntegerFlag('--shuffle', raw)))
130
+ .option('--shuffle [seed]', 'Shuffle isolated mochitest path arguments in a repeatable seeded order (fresh seed when omitted). Native mach --shuffle within each invocation remains unseeded. Export FIREFORGE_SHUFFLE_SEED for custom task code and stamp shuffle=<seed> on the verdict. --no-shard disables the seeded argument permutation. A path-shaped argument right after the flag (browser_x.js, dir/) is a test path, not a seed.', commanderArgParser((raw) => parsePositiveIntegerFlag('--shuffle', raw)))
87
131
  .option('--harness-retries <n>', `Retry budget for recognized harness crashes (resource-monitor tracebacks, pre-test hangs, post-green shutdown re-entry). 0 disables retries. Default: ${DEFAULT_HARNESS_RETRIES}.`, commanderArgParser((raw) => {
88
132
  const n = Number.parseInt(raw, 10);
89
133
  if (!Number.isFinite(n) || n < 0 || n > 10) {
@@ -102,14 +146,14 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
102
146
  return n;
103
147
  }))
104
148
  .addHelpText('after', TEST_HELP_TEXT);
105
- addWaitLockOption(registerTestSharingOptions(test)).action(withErrorHandling(async (paths, options) => {
149
+ addWaitLockOption(registerTestBuildOptions(registerTestMultiRunOptions(registerTestSharingOptions(test)))).action(withErrorHandling(async (paths, options) => {
106
150
  const projectRoot = getProjectRoot();
107
- // The engine session lock is acquired outside testCommand's own
108
- // exactly-one-verdict guarantee, so a lock failure must emit here or
109
- // the run ends with no FIREFORGE-VERDICT line at all.
151
+ // Fallback for a verdict testCommand did not emit (it takes the lock).
110
152
  resetVerdictEmission();
111
153
  try {
112
- await withEngineSessionLock(projectRoot, 'test', () => testCommand(projectRoot, paths, pickDefined(options)), { waitLockSeconds: resolveWaitLockSeconds(options.waitLock) });
154
+ await testCommand(projectRoot, paths, pickDefined(options), (body) => withEngineSessionLock(projectRoot, 'test', body, {
155
+ waitLockSeconds: resolveWaitLockSeconds(options.waitLock),
156
+ }));
113
157
  }
114
158
  catch (error) {
115
159
  if (!verdictEmitted()) {
@@ -18,6 +18,7 @@ import { getActiveRunLogPath } from '../core/run-log.js';
18
18
  import { changedPrefNoiseVerdictNote } from '../core/test-changed-prefs.js';
19
19
  import { buildHarnessCrashMessage, classifyHarnessRun, headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
20
20
  import { reportTestHost, sampleTestHost, stampPerfHost } from '../core/test-host-state.js';
21
+ import { vetoIncompletePass } from '../core/test-run-completeness.js';
21
22
  import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
22
23
  import { withXpcshellProfileDir } from '../core/xpcshell-profile-dir.js';
23
24
  import { TestFailureError } from '../errors/build.js';
@@ -85,7 +86,9 @@ async function runTestsWithRetriesInner(ctx, paths, env) {
85
86
  fullOutput: ctx.fullOutput,
86
87
  });
87
88
  const combined = `${result.stdout}\n${result.stderr}`;
88
- verdict = classifyHarnessRun(result.exitCode, combined, paths);
89
+ // An exit-0 run is still not a pass when it stopped partway or
90
+ // reported unexpected results (see test-run-completeness.ts).
91
+ verdict = vetoIncompletePass(classifyHarnessRun(result.exitCode, combined, paths), combined, paths);
89
92
  if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
90
93
  break;
91
94
  warn(`Harness crash detected (${verdict.signature?.reason ?? 'unknown shape'}): ` +
@@ -7,9 +7,12 @@
7
7
  * stays wiring. The probes and refusal copy live in
8
8
  * `src/core/test-stale-check.ts`.
9
9
  */
10
- import { checkStaleBuildForTest, checkStaticComponentsStale, findChangedTestManifestsForPaths, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaticComponentsRefusal, formatTestCoverageRefusal, } from '../core/test-stale-check.js';
10
+ import { filterByCarriedFullCoverage } from '../core/coverage-dispatch.js';
11
+ import { checkStaleBuildForTest, checkStaticComponentsStale, findChangedTestManifestsForPaths, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaticComponentsRefusal, formatTestCoverageRefusal, FULL_SUITE_REQUEST, } from '../core/test-stale-check.js';
12
+ import { formatStaleExemptions } from '../core/test-stale-exempt.js';
11
13
  import { PreflightRefusalError } from '../errors/base.js';
12
- import { warn } from '../utils/logger.js';
14
+ import { info, warn } from '../utils/logger.js';
15
+ import { describePerfStaleImpact } from './test-perf-scope.js';
13
16
  /**
14
17
  * Stale-build preflight: when `--build` was not requested, detect packageable
15
18
  * engine edits since the last successful build and fail up front unless the
@@ -34,34 +37,61 @@ export async function enforceStaleBuildGate(projectRoot, engineDir, options, nor
34
37
  const stale = await checkStaleBuildForTest(projectRoot, engineDir);
35
38
  const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
36
39
  if (!dispatchesNoTests) {
37
- const recordedCoverage = stale.baseline?.testPackagingCoverage;
38
- const uncovered = findUncoveredRequestPaths(recordedCoverage, normalizedPaths);
39
- if (uncovered.length > 0) {
40
- // The refusal is correct on its own. Naming the manifest that gained
41
- // an entry is the triage step it otherwise leaves to the reader.
42
- const changedManifests = await findChangedTestManifestsForPaths(engineDir, stale.baseline, uncovered);
43
- throw new PreflightRefusalError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests), 'coverage-replaced');
44
- }
40
+ await enforceCoverageGate(engineDir, stale.baseline, normalizedPaths);
45
41
  }
46
42
  await enforceStaticComponentsGate(engineDir, stale.baseline, options);
47
- const staleMessage = stale.stale
48
- ? `${formatStaleBuildWarning(stale)}\n\n` +
49
- 'Run `fireforge test --build` to refresh the packaged runtime first — that is almost ' +
50
- 'always the right move, because the usual cause is your own edit landing between the ' +
51
- 'last build and this run.\n\n' +
52
- '`--allow-stale-build` is NOT the general escape hatch: it accepts a packaging that ' +
53
- 'predates the edit under test, so the run tests the OLD code. Pass it only when you ' +
54
- 'rebuilt out-of-band (outside FireForge) and know the packaged runtime already contains ' +
55
- 'the change.'
56
- : undefined;
57
- if (staleMessage !== undefined) {
58
- if (options.allowStaleBuild === true) {
59
- warn(staleMessage);
60
- }
61
- else {
62
- throw new PreflightRefusalError(staleMessage, 'stale-build');
63
- }
43
+ const exempt = stale.exempt ?? [];
44
+ if (!stale.stale && exempt.length > 0) {
45
+ info(`Stale-build preflight: ${exempt.length} edit(s) since the last build need no rebuild: ${formatStaleExemptions(exempt)}.`);
64
46
  }
47
+ if (!stale.stale)
48
+ return;
49
+ // The one-line repackaging command, so a build-less sitting (a perf
50
+ // series) can repackage and continue without dispatching anything.
51
+ const repackage = normalizedPaths.length > 0
52
+ ? `fireforge test --build-only ${normalizedPaths.join(' ')}`
53
+ : 'fireforge build --ui';
54
+ const perfImpact = options.perfSamples === undefined
55
+ ? ''
56
+ : `\n\n${await describePerfStaleImpact(engineDir, stale.changedPaths, normalizedPaths)}`;
57
+ const staleMessage = `${formatStaleBuildWarning(stale)}\n\n` +
58
+ 'Run `fireforge test --build` to refresh the packaged runtime first — that is almost ' +
59
+ 'always the right move, because the usual cause is your own edit landing between the ' +
60
+ 'last build and this run. To repackage without running anything: ' +
61
+ `\`${repackage}\`.${perfImpact}\n\n` +
62
+ '`--allow-stale-build` is NOT the general escape hatch: it accepts a packaging that ' +
63
+ 'predates the edit under test, so the run tests the OLD code. Pass it only when you ' +
64
+ 'rebuilt out-of-band (outside FireForge) and know the packaged runtime already contains ' +
65
+ 'the change.';
66
+ if (options.allowStaleBuild === true) {
67
+ warn(staleMessage);
68
+ }
69
+ else {
70
+ throw new PreflightRefusalError(staleMessage, 'stale-build');
71
+ }
72
+ }
73
+ /**
74
+ * Packaging-coverage refusal. A path outside the recorded scoped list is
75
+ * still covered while its own staging inputs are unchanged against a
76
+ * carried full-build anchor. The refusal names, per uncovered path, the
77
+ * inputs that changed.
78
+ */
79
+ async function enforceCoverageGate(engineDir, baseline, normalizedPaths) {
80
+ const recordedCoverage = baseline?.testPackagingCoverage;
81
+ const outsideList = findUncoveredRequestPaths(recordedCoverage, normalizedPaths);
82
+ if (outsideList.length === 0)
83
+ return;
84
+ const remaining = await filterByCarriedFullCoverage(engineDir, baseline, outsideList, FULL_SUITE_REQUEST);
85
+ if (remaining.length === 0)
86
+ return;
87
+ const uncovered = remaining.map((request) => request.path);
88
+ // The refusal is correct on its own. Naming the manifest that gained
89
+ // an entry is the triage step it otherwise leaves to the reader.
90
+ const changedManifests = await findChangedTestManifestsForPaths(engineDir, baseline, uncovered);
91
+ throw new PreflightRefusalError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests, {
92
+ changedInputs: Object.fromEntries(remaining.map((request) => [request.path, request.changed])),
93
+ carriesFullAnchor: baseline?.fullCoverageAnchor !== undefined,
94
+ }), 'coverage-replaced');
65
95
  }
66
96
  /**
67
97
  * Compiled-StaticComponents gate: refuses runs whose child process would
@@ -13,7 +13,7 @@ import type { HarnessRunVerdict } from '../core/test-harness-crash.js';
13
13
  */
14
14
  export type FireforgeVerdictReason = 'crash' | 'harness-arguments' | 'no-tests' | 'test-failures' | 'preflight' | 'inconclusive' | 'lock-timeout' | 'killed';
15
15
  /** Additive keys a run can stamp on its verdict line. */
16
- export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed';
16
+ export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed' | 'furnace-applied' | 'furnace-apply';
17
17
  /** Records an additive attribute for this run's verdict line. */
18
18
  export declare function setVerdictRunAttribute(key: VerdictRunAttributeKey, value: string): void;
19
19
  /**
@@ -30,6 +30,15 @@ export declare function addVerdictRunCount(key: 'orphans-reaped', count: number)
30
30
  export declare function resetVerdictEmission(): void;
31
31
  /** True once this run has written its `FIREFORGE-VERDICT:` line. */
32
32
  export declare function verdictEmitted(): boolean;
33
+ /** Installs (or, with `undefined`, removes) the sub-run capture slot. */
34
+ export declare function setVerdictCapture(slot: {
35
+ line?: string;
36
+ } | undefined): void;
37
+ /**
38
+ * Emits a multi-run aggregate verdict. `body` is everything after the
39
+ * `FIREFORGE-VERDICT: ` prefix.
40
+ */
41
+ export declare function emitAggregateVerdict(body: string): void;
33
42
  /**
34
43
  * Emits the classifier-derived verdict for a completed harness run, with
35
44
  * the aggregate `shards=<passed>/<total>` suffix for sharded runs.
@@ -51,11 +51,31 @@ export function resetVerdictEmission() {
51
51
  emitted = false;
52
52
  armed = true;
53
53
  runAttributes.clear();
54
+ capture = undefined;
54
55
  setStdoutSealed(false);
55
56
  }
57
+ /**
58
+ * The sub-run capture slot of a multi-run invocation (`--repeat`, mixed
59
+ * harnesses, `--negative-control`; see `test-orchestrate.ts`). While one is
60
+ * installed, a sub-run's verdict line is recorded here, first write wins as
61
+ * on stdout, and nothing is printed or sealed, so the invocation can still
62
+ * end with its own single aggregate line.
63
+ */
64
+ let capture;
56
65
  /** True once this run has written its `FIREFORGE-VERDICT:` line. */
57
66
  export function verdictEmitted() {
58
- return emitted;
67
+ return capture === undefined ? emitted : capture.line !== undefined;
68
+ }
69
+ /** Installs (or, with `undefined`, removes) the sub-run capture slot. */
70
+ export function setVerdictCapture(slot) {
71
+ capture = slot;
72
+ }
73
+ /**
74
+ * Emits a multi-run aggregate verdict. `body` is everything after the
75
+ * `FIREFORGE-VERDICT: ` prefix.
76
+ */
77
+ export function emitAggregateVerdict(body) {
78
+ writeVerdictLine(`FIREFORGE-VERDICT: ${body}`);
59
79
  }
60
80
  /**
61
81
  * Writes the run's single verdict line to raw stdout (clack's renderer can
@@ -68,6 +88,10 @@ export function verdictEmitted() {
68
88
  * rendering after an emit-then-throw path, routes to stderr.
69
89
  */
70
90
  function writeVerdictLine(line) {
91
+ if (capture !== undefined) {
92
+ capture.line ??= line;
93
+ return;
94
+ }
71
95
  if (emitted)
72
96
  return;
73
97
  emitted = true;
@@ -130,6 +154,9 @@ export function emitFailVerdict(reason, note) {
130
154
  export function emitKilledVerdict(signal) {
131
155
  if (!armed || emitted)
132
156
  return false;
157
+ // A kill ends the whole invocation, so a sub-run capture must not
158
+ // swallow the one line that says so.
159
+ capture = undefined;
133
160
  writeVerdictLine(`FIREFORGE-VERDICT: FAIL reason=killed signal=${signal}`);
134
161
  return true;
135
162
  }
@@ -10,8 +10,13 @@ import type { TestOptions } from '../types/commands/index.js';
10
10
  * since no harness classification exists for such a run. Writers closer to
11
11
  * the harness (doctor, canary, single, sharded, the engine-generation
12
12
  * guard) emit first and win.
13
+ * The lock-free checks (launchable binary, named-file checkJs, the
14
+ * `--wait-browser`/`--wait-port` budgets) run first, outside `underLock`,
15
+ * so a run that would be refused or would only wait never holds a place in
16
+ * the engine lock queue. See `test-prelock.ts`.
13
17
  * @param projectRoot - Root directory of the project
14
18
  * @param testPaths - Test file or directory paths
15
19
  * @param options - Test options
20
+ * @param underLock - Runs the rest of the command under the engine session lock
16
21
  */
17
- export declare function testCommand(projectRoot: string, testPaths: string[], options?: TestOptions): Promise<void>;
22
+ export declare function testCommand(projectRoot: string, testPaths: string[], options?: TestOptions, underLock?: (body: () => Promise<void>) => Promise<void>): Promise<void>;