@hominis/fireforge 0.49.2 → 0.51.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 (400) hide show
  1. package/CHANGELOG.md +87 -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 +41 -0
  10. package/dist/src/commands/check-report.js +91 -0
  11. package/dist/src/commands/check-run.d.ts +33 -0
  12. package/dist/src/commands/check-run.js +112 -0
  13. package/dist/src/commands/check-steps.d.ts +75 -0
  14. package/dist/src/commands/check-steps.js +200 -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/declarations-report.d.ts +43 -0
  19. package/dist/src/commands/declarations-report.js +76 -0
  20. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  21. package/dist/src/commands/doctor-source-pin.js +4 -1
  22. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  23. package/dist/src/commands/export-adopt-check.js +98 -0
  24. package/dist/src/commands/export-flow.js +29 -14
  25. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  26. package/dist/src/commands/export-placement-gate.js +8 -0
  27. package/dist/src/commands/export-shared.d.ts +2 -1
  28. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  29. package/dist/src/commands/export-staged-registration.js +91 -0
  30. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  31. package/dist/src/commands/forward-import-remedy.js +63 -0
  32. package/dist/src/commands/furnace/apply.js +3 -3
  33. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  34. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  35. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  36. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  37. package/dist/src/commands/furnace/create.js +2 -2
  38. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  39. package/dist/src/commands/furnace/deploy-named.js +107 -0
  40. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  41. package/dist/src/commands/furnace/deploy.js +50 -86
  42. package/dist/src/commands/furnace/diff.js +26 -3
  43. package/dist/src/commands/furnace/index.js +66 -41
  44. package/dist/src/commands/furnace/init.js +13 -6
  45. package/dist/src/commands/furnace/override.js +4 -2
  46. package/dist/src/commands/furnace/preview.js +4 -3
  47. package/dist/src/commands/furnace/refresh.js +2 -2
  48. package/dist/src/commands/furnace/remove.js +4 -3
  49. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  50. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  51. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  52. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  53. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  54. package/dist/src/commands/furnace/rename-registrations.js +98 -0
  55. package/dist/src/commands/furnace/rename.d.ts +2 -1
  56. package/dist/src/commands/furnace/rename.js +28 -66
  57. package/dist/src/commands/furnace/scan.d.ts +9 -4
  58. package/dist/src/commands/furnace/scan.js +11 -9
  59. package/dist/src/commands/furnace/sync.js +3 -2
  60. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  61. package/dist/src/commands/furnace/validate-report.js +91 -0
  62. package/dist/src/commands/furnace/validate.d.ts +1 -1
  63. package/dist/src/commands/furnace/validate.js +151 -113
  64. package/dist/src/commands/furnace/validation-output.js +14 -3
  65. package/dist/src/commands/lint-per-patch.js +36 -61
  66. package/dist/src/commands/lint-per-run-checkjs.js +2 -2
  67. package/dist/src/commands/lint-report.d.ts +8 -4
  68. package/dist/src/commands/lint-report.js +20 -5
  69. package/dist/src/commands/lint.js +15 -34
  70. package/dist/src/commands/manifest.js +4 -0
  71. package/dist/src/commands/park.d.ts +22 -0
  72. package/dist/src/commands/park.js +232 -0
  73. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  74. package/dist/src/commands/patch/delete.d.ts +1 -1
  75. package/dist/src/commands/patch/delete.js +24 -104
  76. package/dist/src/commands/patch/index.d.ts +3 -3
  77. package/dist/src/commands/patch/index.js +10 -4
  78. package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
  79. package/dist/src/commands/patch/lint-ignore-from.js +142 -0
  80. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  81. package/dist/src/commands/patch/lint-ignore.js +78 -31
  82. package/dist/src/commands/patch/move-files-create.js +34 -18
  83. package/dist/src/commands/patch/move-files-into.js +15 -7
  84. package/dist/src/commands/patch/move-files.js +6 -2
  85. package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
  86. package/dist/src/commands/patch/removal-created-files.js +83 -0
  87. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  88. package/dist/src/commands/patch/removal-dependents.js +133 -0
  89. package/dist/src/commands/patch/rename.js +20 -1
  90. package/dist/src/commands/patch/reserve.d.ts +33 -0
  91. package/dist/src/commands/patch/reserve.js +152 -0
  92. package/dist/src/commands/patch/retire.d.ts +34 -0
  93. package/dist/src/commands/patch/retire.js +133 -0
  94. package/dist/src/commands/patch/split-plan.d.ts +17 -8
  95. package/dist/src/commands/patch/split-plan.js +60 -28
  96. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  97. package/dist/src/commands/patch/split-staged.js +202 -0
  98. package/dist/src/commands/patch/split.d.ts +5 -0
  99. package/dist/src/commands/patch/split.js +62 -17
  100. package/dist/src/commands/patch/stage.d.ts +34 -0
  101. package/dist/src/commands/patch/stage.js +153 -0
  102. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  103. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  104. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  105. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  106. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  107. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  108. package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
  109. package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
  110. package/dist/src/commands/patch/staged-dependency.js +54 -139
  111. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  112. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  113. package/dist/src/commands/re-export-adjacent.d.ts +50 -2
  114. package/dist/src/commands/re-export-adjacent.js +160 -40
  115. package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
  116. package/dist/src/commands/re-export-bulk-scan.js +22 -7
  117. package/dist/src/commands/re-export-drift.d.ts +36 -8
  118. package/dist/src/commands/re-export-drift.js +73 -49
  119. package/dist/src/commands/re-export-files.js +11 -8
  120. package/dist/src/commands/re-export-hunks.d.ts +35 -0
  121. package/dist/src/commands/re-export-hunks.js +220 -0
  122. package/dist/src/commands/re-export-lint.d.ts +2 -1
  123. package/dist/src/commands/re-export-lint.js +23 -10
  124. package/dist/src/commands/re-export-options.js +74 -6
  125. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  126. package/dist/src/commands/re-export-preflight.js +160 -0
  127. package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
  128. package/dist/src/commands/re-export-purity-scope.js +131 -0
  129. package/dist/src/commands/re-export-register.js +56 -17
  130. package/dist/src/commands/re-export-removed.d.ts +13 -2
  131. package/dist/src/commands/re-export-removed.js +57 -12
  132. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  133. package/dist/src/commands/re-export-scan-owner.js +116 -0
  134. package/dist/src/commands/re-export-scan.d.ts +19 -9
  135. package/dist/src/commands/re-export-scan.js +60 -24
  136. package/dist/src/commands/re-export-single.js +122 -46
  137. package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
  138. package/dist/src/commands/re-export-stale-registrations.js +62 -0
  139. package/dist/src/commands/re-export-summary.d.ts +28 -0
  140. package/dist/src/commands/re-export-summary.js +36 -0
  141. package/dist/src/commands/re-export.js +71 -57
  142. package/dist/src/commands/rebase/index.js +17 -17
  143. package/dist/src/commands/run.js +16 -2
  144. package/dist/src/commands/status-check.d.ts +55 -16
  145. package/dist/src/commands/status-check.js +92 -37
  146. package/dist/src/commands/status-json.d.ts +22 -9
  147. package/dist/src/commands/status-json.js +47 -32
  148. package/dist/src/commands/status-output.js +3 -1
  149. package/dist/src/commands/status-scope.d.ts +37 -0
  150. package/dist/src/commands/status-scope.js +54 -0
  151. package/dist/src/commands/status.js +66 -23
  152. package/dist/src/commands/test-browser-preflight.js +19 -3
  153. package/dist/src/commands/test-build-furnace.d.ts +22 -0
  154. package/dist/src/commands/test-build-furnace.js +54 -0
  155. package/dist/src/commands/test-build-phase.js +64 -41
  156. package/dist/src/commands/test-checkjs.js +10 -2
  157. package/dist/src/commands/test-diagnose.js +7 -0
  158. package/dist/src/commands/test-dispatch.d.ts +39 -0
  159. package/dist/src/commands/test-dispatch.js +211 -0
  160. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  161. package/dist/src/commands/test-furnace-reach.js +132 -0
  162. package/dist/src/commands/test-modes.d.ts +16 -1
  163. package/dist/src/commands/test-modes.js +24 -1
  164. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  165. package/dist/src/commands/test-multi-verdict.js +111 -0
  166. package/dist/src/commands/test-orchestrate.d.ts +52 -0
  167. package/dist/src/commands/test-orchestrate.js +240 -0
  168. package/dist/src/commands/test-perf-scope.d.ts +50 -0
  169. package/dist/src/commands/test-perf-scope.js +138 -0
  170. package/dist/src/commands/test-prelock.d.ts +51 -0
  171. package/dist/src/commands/test-prelock.js +273 -0
  172. package/dist/src/commands/test-register.js +69 -10
  173. package/dist/src/commands/test-results.d.ts +62 -0
  174. package/dist/src/commands/test-results.js +62 -0
  175. package/dist/src/commands/test-run.d.ts +13 -55
  176. package/dist/src/commands/test-run.js +74 -99
  177. package/dist/src/commands/test-shards.d.ts +110 -0
  178. package/dist/src/commands/test-shards.js +255 -0
  179. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  180. package/dist/src/commands/test-stale-gate.js +60 -28
  181. package/dist/src/commands/test-verdict.d.ts +11 -1
  182. package/dist/src/commands/test-verdict.js +28 -1
  183. package/dist/src/commands/test.d.ts +6 -1
  184. package/dist/src/commands/test.js +120 -124
  185. package/dist/src/commands/token-lint.d.ts +14 -0
  186. package/dist/src/commands/token-lint.js +151 -0
  187. package/dist/src/commands/token.js +10 -0
  188. package/dist/src/commands/verify-scope.d.ts +28 -0
  189. package/dist/src/commands/verify-scope.js +67 -0
  190. package/dist/src/commands/verify.d.ts +10 -2
  191. package/dist/src/commands/verify.js +67 -36
  192. package/dist/src/core/build-baseline-types.d.ts +46 -0
  193. package/dist/src/core/build-baseline-validate.js +19 -0
  194. package/dist/src/core/build-baseline.d.ts +5 -1
  195. package/dist/src/core/build-baseline.js +3 -1
  196. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  197. package/dist/src/core/build-overwrite-guard.js +84 -4
  198. package/dist/src/core/build-prepare.d.ts +13 -0
  199. package/dist/src/core/build-prepare.js +42 -10
  200. package/dist/src/core/build-run-record.d.ts +74 -0
  201. package/dist/src/core/build-run-record.js +136 -0
  202. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  203. package/dist/src/core/checkjs-support-heads.js +104 -0
  204. package/dist/src/core/config-paths.d.ts +2 -2
  205. package/dist/src/core/config-paths.js +10 -0
  206. package/dist/src/core/config-validate-check.d.ts +8 -0
  207. package/dist/src/core/config-validate-check.js +219 -0
  208. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  209. package/dist/src/core/config-validate-file-size.js +91 -2
  210. package/dist/src/core/config-validate-history.d.ts +11 -0
  211. package/dist/src/core/config-validate-history.js +38 -0
  212. package/dist/src/core/config-validate-patch-policy.js +100 -0
  213. package/dist/src/core/config-validate-test-toolchains.js +21 -0
  214. package/dist/src/core/config-validate-token-scales.d.ts +9 -0
  215. package/dist/src/core/config-validate-token-scales.js +92 -0
  216. package/dist/src/core/config-validate.js +20 -5
  217. package/dist/src/core/config.d.ts +5 -0
  218. package/dist/src/core/config.js +16 -0
  219. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  220. package/dist/src/core/coverage-dispatch.js +44 -0
  221. package/dist/src/core/coverage-extend.d.ts +70 -45
  222. package/dist/src/core/coverage-extend.js +199 -144
  223. package/dist/src/core/coverage-records.d.ts +108 -0
  224. package/dist/src/core/coverage-records.js +384 -0
  225. package/dist/src/core/destructive.d.ts +7 -4
  226. package/dist/src/core/destructive.js +10 -10
  227. package/dist/src/core/engine-session-lock.d.ts +16 -1
  228. package/dist/src/core/engine-session-lock.js +38 -10
  229. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  230. package/dist/src/core/export-moved-code-shape.js +135 -44
  231. package/dist/src/core/file-lock-queue.d.ts +67 -0
  232. package/dist/src/core/file-lock-queue.js +221 -0
  233. package/dist/src/core/file-lock-shared.d.ts +57 -0
  234. package/dist/src/core/file-lock-shared.js +117 -0
  235. package/dist/src/core/file-lock.d.ts +13 -25
  236. package/dist/src/core/file-lock.js +163 -140
  237. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  238. package/dist/src/core/furnace-apply-output.js +31 -1
  239. package/dist/src/core/furnace-apply.js +22 -12
  240. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  241. package/dist/src/core/furnace-composed-source.js +167 -0
  242. package/dist/src/core/furnace-config-validate-exceptions.d.ts +16 -0
  243. package/dist/src/core/furnace-config-validate-exceptions.js +102 -0
  244. package/dist/src/core/furnace-config-validate.js +14 -0
  245. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  246. package/dist/src/core/furnace-css-fragments.js +26 -0
  247. package/dist/src/core/furnace-operation.d.ts +3 -3
  248. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  249. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  250. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  251. package/dist/src/core/furnace-registration-remove.js +53 -17
  252. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  253. package/dist/src/core/furnace-registration-rename.js +77 -0
  254. package/dist/src/core/furnace-registration.d.ts +14 -4
  255. package/dist/src/core/furnace-registration.js +44 -10
  256. package/dist/src/core/furnace-staleness.d.ts +7 -0
  257. package/dist/src/core/furnace-staleness.js +34 -24
  258. package/dist/src/core/furnace-validate-accessibility.d.ts +8 -4
  259. package/dist/src/core/furnace-validate-accessibility.js +71 -28
  260. package/dist/src/core/furnace-validate-exceptions.d.ts +48 -0
  261. package/dist/src/core/furnace-validate-exceptions.js +80 -0
  262. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  263. package/dist/src/core/furnace-validate-icon-button.js +78 -0
  264. package/dist/src/core/furnace-validate.js +1 -1
  265. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  266. package/dist/src/core/harness-port-fallback.js +96 -0
  267. package/dist/src/core/history-log-file.d.ts +61 -0
  268. package/dist/src/core/history-log-file.js +164 -0
  269. package/dist/src/core/history-log.d.ts +2 -1
  270. package/dist/src/core/history-log.js +2 -1
  271. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  272. package/dist/src/core/json-preserving-edit.js +199 -0
  273. package/dist/src/core/line-prefixer.d.ts +18 -0
  274. package/dist/src/core/line-prefixer.js +34 -0
  275. package/dist/src/core/mach.d.ts +7 -0
  276. package/dist/src/core/mach.js +32 -10
  277. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  278. package/dist/src/core/module-resolution-preflight.js +24 -6
  279. package/dist/src/core/negative-control-compiled.d.ts +27 -0
  280. package/dist/src/core/negative-control-compiled.js +73 -0
  281. package/dist/src/core/negative-control-diff.d.ts +51 -0
  282. package/dist/src/core/negative-control-diff.js +179 -0
  283. package/dist/src/core/negative-control.d.ts +150 -0
  284. package/dist/src/core/negative-control.js +399 -0
  285. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  286. package/dist/src/core/objdir-install-manifests.js +226 -0
  287. package/dist/src/core/park-store.d.ts +63 -0
  288. package/dist/src/core/park-store.js +216 -0
  289. package/dist/src/core/patch-export-update.d.ts +6 -0
  290. package/dist/src/core/patch-export-update.js +13 -3
  291. package/dist/src/core/patch-export.d.ts +3 -2
  292. package/dist/src/core/patch-export.js +22 -16
  293. package/dist/src/core/patch-files.js +26 -8
  294. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +39 -0
  295. package/dist/src/core/patch-lint-checkjs-any-import.js +63 -0
  296. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  297. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  298. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  299. package/dist/src/core/patch-lint-checkjs.js +20 -13
  300. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  301. package/dist/src/core/patch-lint-code-lines.js +105 -0
  302. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  303. package/dist/src/core/patch-lint-cross.js +17 -4
  304. package/dist/src/core/patch-lint-css.d.ts +8 -2
  305. package/dist/src/core/patch-lint-css.js +24 -14
  306. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  307. package/dist/src/core/patch-lint-file-size.js +22 -11
  308. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +23 -0
  309. package/dist/src/core/patch-lint-forward-registration-extended.js +36 -11
  310. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  311. package/dist/src/core/patch-lint-ignore.d.ts +196 -0
  312. package/dist/src/core/patch-lint-ignore.js +429 -0
  313. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  314. package/dist/src/core/patch-lint-ownership.js +26 -0
  315. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  316. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  317. package/dist/src/core/patch-lint-size.d.ts +117 -0
  318. package/dist/src/core/patch-lint-size.js +331 -0
  319. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  320. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  321. package/dist/src/core/patch-lint-token-scale.d.ts +57 -0
  322. package/dist/src/core/patch-lint-token-scale.js +286 -0
  323. package/dist/src/core/patch-lint.d.ts +11 -83
  324. package/dist/src/core/patch-lint.js +66 -288
  325. package/dist/src/core/patch-lock.js +7 -6
  326. package/dist/src/core/patch-manifest-consistency.js +7 -6
  327. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  328. package/dist/src/core/patch-manifest-io.js +27 -9
  329. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  330. package/dist/src/core/patch-manifest-key-order.js +61 -0
  331. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  332. package/dist/src/core/patch-manifest-stage.js +96 -0
  333. package/dist/src/core/patch-manifest-validate.js +61 -3
  334. package/dist/src/core/patch-order-legal.d.ts +26 -0
  335. package/dist/src/core/patch-order-legal.js +88 -0
  336. package/dist/src/core/patch-policy-content.d.ts +40 -0
  337. package/dist/src/core/patch-policy-content.js +216 -0
  338. package/dist/src/core/patch-policy.d.ts +22 -5
  339. package/dist/src/core/patch-policy.js +26 -12
  340. package/dist/src/core/patch-retired.d.ts +57 -0
  341. package/dist/src/core/patch-retired.js +92 -0
  342. package/dist/src/core/patch-subset.d.ts +16 -0
  343. package/dist/src/core/patch-subset.js +66 -0
  344. package/dist/src/core/preflight-wait.d.ts +20 -0
  345. package/dist/src/core/preflight-wait.js +38 -0
  346. package/dist/src/core/rebase-from-version.d.ts +46 -0
  347. package/dist/src/core/rebase-from-version.js +71 -0
  348. package/dist/src/core/run-log.d.ts +26 -0
  349. package/dist/src/core/run-log.js +106 -4
  350. package/dist/src/core/run-profile.d.ts +7 -0
  351. package/dist/src/core/run-profile.js +7 -1
  352. package/dist/src/core/smoke-patterns.d.ts +5 -0
  353. package/dist/src/core/smoke-patterns.js +13 -0
  354. package/dist/src/core/test-harness-crash.d.ts +19 -1
  355. package/dist/src/core/test-harness-crash.js +8 -9
  356. package/dist/src/core/test-harness-signature.d.ts +33 -0
  357. package/dist/src/core/test-harness-signature.js +23 -1
  358. package/dist/src/core/test-raw-log.d.ts +66 -0
  359. package/dist/src/core/test-raw-log.js +122 -0
  360. package/dist/src/core/test-run-completeness.d.ts +90 -0
  361. package/dist/src/core/test-run-completeness.js +184 -0
  362. package/dist/src/core/test-stale-check.d.ts +27 -1
  363. package/dist/src/core/test-stale-check.js +39 -49
  364. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  365. package/dist/src/core/test-stale-exempt.js +70 -0
  366. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  367. package/dist/src/core/test-xpcshell-retry.js +2 -1
  368. package/dist/src/core/token-lint.d.ts +53 -0
  369. package/dist/src/core/token-lint.js +131 -0
  370. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  371. package/dist/src/core/token-sibling-sheets.js +119 -0
  372. package/dist/src/core/tree-guard.js +5 -0
  373. package/dist/src/core/typecheck-shim.d.ts +6 -2
  374. package/dist/src/core/typecheck-shim.js +205 -4
  375. package/dist/src/core/typecheck.js +3 -1
  376. package/dist/src/index.d.ts +14 -3
  377. package/dist/src/index.js +6 -1
  378. package/dist/src/types/commands/index.d.ts +2 -2
  379. package/dist/src/types/commands/options.d.ts +212 -24
  380. package/dist/src/types/commands/patches.d.ts +118 -1
  381. package/dist/src/types/config.d.ts +264 -0
  382. package/dist/src/types/config.js +9 -0
  383. package/dist/src/types/furnace.d.ts +63 -1
  384. package/dist/src/types/furnace.js +7 -0
  385. package/dist/src/types/index.d.ts +1 -1
  386. package/dist/src/utils/options.d.ts +36 -7
  387. package/dist/src/utils/options.js +94 -8
  388. package/dist/src/utils/path-glob.d.ts +15 -0
  389. package/dist/src/utils/path-glob.js +47 -0
  390. package/dist/src/utils/regex.d.ts +15 -3
  391. package/dist/src/utils/regex.js +37 -16
  392. package/docs/configuration.md +369 -11
  393. package/docs/exit-codes.md +18 -1
  394. package/docs/furnace.md +139 -3
  395. package/docs/lifecycle-invariants.md +65 -30
  396. package/docs/machine-output.md +120 -12
  397. package/docs/patch-workflow.md +458 -22
  398. package/docs/run-logs.md +20 -1
  399. package/docs/testing.md +346 -21
  400. package/package.json +3 -3
@@ -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
@@ -30,38 +33,67 @@ import { warn } from '../utils/logger.js';
30
33
  * a full-suite request would refuse a probe that touches no fixtures. The
31
34
  * stale-content refusal still applies to it unchanged.
32
35
  */
33
- export async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths) {
36
+ export async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths,
37
+ /** `quiet`: refuse only, without the exemption note (the pre-lock pass). */
38
+ gate = {}) {
34
39
  const stale = await checkStaleBuildForTest(projectRoot, engineDir);
35
40
  const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
36
41
  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
- }
42
+ await enforceCoverageGate(engineDir, stale.baseline, normalizedPaths);
45
43
  }
46
44
  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
- }
45
+ const exempt = stale.exempt ?? [];
46
+ if (!stale.stale && exempt.length > 0 && gate.quiet !== true) {
47
+ info(`Stale-build preflight: ${exempt.length} edit(s) since the last build need no rebuild: ${formatStaleExemptions(exempt)}.`);
64
48
  }
49
+ if (!stale.stale)
50
+ return;
51
+ // The one-line repackaging command, so a build-less sitting (a perf
52
+ // series) can repackage and continue without dispatching anything.
53
+ const repackage = normalizedPaths.length > 0
54
+ ? `fireforge test --build-only ${normalizedPaths.join(' ')}`
55
+ : 'fireforge build --ui';
56
+ const perfImpact = options.perfSamples === undefined
57
+ ? ''
58
+ : `\n\n${await describePerfStaleImpact(engineDir, stale.changedPaths, normalizedPaths)}`;
59
+ const staleMessage = `${formatStaleBuildWarning(stale)}\n\n` +
60
+ 'Run `fireforge test --build` to refresh the packaged runtime first — that is almost ' +
61
+ 'always the right move, because the usual cause is your own edit landing between the ' +
62
+ 'last build and this run. To repackage without running anything: ' +
63
+ `\`${repackage}\`.${perfImpact}\n\n` +
64
+ '`--allow-stale-build` is NOT the general escape hatch: it accepts a packaging that ' +
65
+ 'predates the edit under test, so the run tests the OLD code. Pass it only when you ' +
66
+ 'rebuilt out-of-band (outside FireForge) and know the packaged runtime already contains ' +
67
+ 'the change.';
68
+ if (options.allowStaleBuild === true) {
69
+ warn(staleMessage);
70
+ }
71
+ else {
72
+ throw new PreflightRefusalError(staleMessage, 'stale-build');
73
+ }
74
+ }
75
+ /**
76
+ * Packaging-coverage refusal. A path outside the recorded scoped list is
77
+ * still covered while its own staging inputs are unchanged against a
78
+ * carried full-build anchor. The refusal names, per uncovered path, the
79
+ * inputs that changed.
80
+ */
81
+ async function enforceCoverageGate(engineDir, baseline, normalizedPaths) {
82
+ const recordedCoverage = baseline?.testPackagingCoverage;
83
+ const outsideList = findUncoveredRequestPaths(recordedCoverage, normalizedPaths);
84
+ if (outsideList.length === 0)
85
+ return;
86
+ const remaining = await filterByCarriedFullCoverage(engineDir, baseline, outsideList, FULL_SUITE_REQUEST);
87
+ if (remaining.length === 0)
88
+ return;
89
+ const uncovered = remaining.map((request) => request.path);
90
+ // The refusal is correct on its own. Naming the manifest that gained
91
+ // an entry is the triage step it otherwise leaves to the reader.
92
+ const changedManifests = await findChangedTestManifestsForPaths(engineDir, baseline, uncovered);
93
+ throw new PreflightRefusalError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests, {
94
+ changedInputs: Object.fromEntries(remaining.map((request) => [request.path, request.changed])),
95
+ carriesFullAnchor: baseline?.fullCoverageAnchor !== undefined,
96
+ }), 'coverage-replaced');
65
97
  }
66
98
  /**
67
99
  * 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' | 'furnace-skipped' | 'control-build' | 'drift-tolerated' | 'lock' | 'jobs' | 'results';
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.
@@ -37,6 +46,7 @@ export declare function verdictEmitted(): boolean;
37
46
  export declare function emitHarnessVerdict(verdict: HarnessRunVerdict, shards?: {
38
47
  passed: number;
39
48
  total: number;
49
+ failed?: readonly string[];
40
50
  }): void;
41
51
  /** Emits the pathless `--doctor` PASS verdict. */
42
52
  export declare function emitPassVerdict(): void;
@@ -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>, mode?: 'exclusive' | 'shared') => Promise<void>): Promise<void>;
@@ -1,32 +1,33 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { join } from 'node:path';
2
+ import { join, resolve } from 'node:path';
3
3
  import { getProjectPaths, loadConfig } from '../core/config.js';
4
4
  import { assertEngineExists } from '../core/engine-precondition.js';
5
- import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
6
- import { hasBuildArtifacts, hasRunnableBundle } from '../core/mach.js';
5
+ import { hasBuildArtifacts } from '../core/mach.js';
7
6
  import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
8
7
  import { ensureLaunchableBrowserNotRunning } from '../core/marionette-port.js';
9
8
  import { waitForPreflight } from '../core/preflight-wait.js';
10
- import { closeActiveRunLog, openRunLog, setActiveRunLog, writeToActiveRunLog, } from '../core/run-log.js';
9
+ import { closeActiveRunLog, keepRunLogCopy, openRunLog, setActiveRunLog, writeToActiveRunLog, } from '../core/run-log.js';
11
10
  import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
12
11
  import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
13
12
  import { cleanupProfileFiles, stageProfileFiles, } from '../core/test-profile-files.js';
14
13
  import { assertObjdirMatchesTreeMarker } from '../core/tree-store.js';
15
- import { FireForgeError, GeneralError, PreflightRefusalError } from '../errors/base.js';
16
- import { BuildError } from '../errors/build.js';
14
+ import { FireForgeError, GeneralError, LockContentionError, PreflightRefusalError, } from '../errors/base.js';
17
15
  import { toError } from '../utils/errors.js';
18
16
  import { pathExists } from '../utils/fs.js';
19
- import { info, intro, notice, warn } from '../utils/logger.js';
17
+ import { info, intro, warn } from '../utils/logger.js';
20
18
  import { resolveWaitLockSeconds } from '../utils/options.js';
21
19
  import { stripEnginePrefix } from '../utils/paths.js';
22
20
  import { appendMarionetteForwardingArgs, ensureTestBrowserEnvironment, runDoctorPreflight, } from './test-browser-preflight.js';
23
21
  import { runTestBuildPhase } from './test-build-phase.js';
24
- import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
25
- import { buildPerfSampleEnv, mergeHarnessEnv, resolveShuffleSeed, shuffleTestGroups, } from './test-harness-env.js';
22
+ import { assertParallelTestUsage, dispatchTests } from './test-dispatch.js';
23
+ import { buildPerfSampleEnv, mergeHarnessEnv, resolveShuffleSeed } from './test-harness-env.js';
26
24
  import { removePgidFile, setActivePgidFile } from './test-harness-teardown.js';
27
- import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
28
- import { DEFAULT_HARNESS_RETRIES, finalizeShardedOutcome, runShardedTests, runTestsWithRetries, } from './test-run.js';
29
- import { emitFailVerdict, resetVerdictEmission, verdictEmitted } from './test-verdict.js';
25
+ import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, resolveCanaryPath, restartBetweenTestsArgs, } from './test-modes.js';
26
+ import { orchestrateTestRuns } from './test-orchestrate.js';
27
+ import { resolveLaunchablePathForTests, runTestPreLockPreflight, TEST_BUILD_PREFLIGHT, } from './test-prelock.js';
28
+ import { resetTestResults, withRawLogDir, writeTestResults } from './test-results.js';
29
+ import { DEFAULT_HARNESS_RETRIES } from './test-run.js';
30
+ import { emitFailVerdict, resetVerdictEmission, setVerdictRunAttribute, verdictEmitted, } from './test-verdict.js';
30
31
  async function assertTestPathsExist(engineDir, testPaths) {
31
32
  const missingPaths = [];
32
33
  for (const testPath of testPaths) {
@@ -77,20 +78,6 @@ function filterRedundantXpcshellFlavorArgs(machArgs, classification) {
77
78
  }
78
79
  return filtered;
79
80
  }
80
- async function resolveLaunchablePathForTests(engineDir, binaryName, objDir) {
81
- if (!objDir)
82
- return undefined;
83
- const bundleCheck = await hasRunnableBundle(engineDir, binaryName, objDir);
84
- if (!bundleCheck.runnable) {
85
- const expectedSuffix = bundleCheck.expectedPath
86
- ? ` (expected at engine/${bundleCheck.expectedPath})`
87
- : '';
88
- throw new GeneralError(`Tests require a complete launchable build${expectedSuffix}. ` +
89
- 'The obj-*/dist/ tree exists but the launchable binary is missing — typically the result of an interrupted or partially failed `fireforge build`.\n\n' +
90
- 'Run "fireforge build" again and let it finish before retrying "fireforge test".');
91
- }
92
- return bundleCheck.expectedPath;
93
- }
94
81
  function logTestSelection(scopes) {
95
82
  if (scopes.length > 0) {
96
83
  const labels = scopes.map((scope) => scope.isDirectory && scope.testFileCount > 0
@@ -103,13 +90,6 @@ function logTestSelection(scopes) {
103
90
  }
104
91
  info('');
105
92
  }
106
- /** Build-artifact preflight wording for `fireforge test`. */
107
- const TEST_BUILD_PREFLIGHT = {
108
- label: 'Tests',
109
- requirement: 'Tests require a completed build.',
110
- remediation: "Run 'fireforge build' first, then run 'fireforge test'.",
111
- requireExisting: true,
112
- };
113
93
  /**
114
94
  * Runs the test command to execute mach tests.
115
95
  *
@@ -121,17 +101,22 @@ const TEST_BUILD_PREFLIGHT = {
121
101
  * since no harness classification exists for such a run. Writers closer to
122
102
  * the harness (doctor, canary, single, sharded, the engine-generation
123
103
  * guard) emit first and win.
104
+ * The lock-free checks (launchable binary, named-file checkJs, the
105
+ * `--wait-browser`/`--wait-port` budgets) run first, outside `underLock`,
106
+ * so a run that would be refused or would only wait never holds a place in
107
+ * the engine lock queue. See `test-prelock.ts`.
124
108
  * @param projectRoot - Root directory of the project
125
109
  * @param testPaths - Test file or directory paths
126
110
  * @param options - Test options
111
+ * @param underLock - Runs the rest of the command under the engine session lock
127
112
  */
128
- export async function testCommand(projectRoot, testPaths, options = {}) {
113
+ export async function testCommand(projectRoot, testPaths, options = {}, underLock = (body) => body()) {
129
114
  intro('FireForge Test');
130
115
  resetVerdictEmission();
131
116
  // Opened before any work so a preflight refusal is logged too. Those are
132
117
  // exactly the runs whose only output a `tail` throws away. Closed in
133
118
  // `finally`, after the verdict line has already read the path.
134
- setActiveRunLog(await openRunLog(projectRoot, 'test'));
119
+ setActiveRunLog(await keepRunLogCopy(await openRunLog(projectRoot, 'test'), options.keepLog, 'test'));
135
120
  setActivePgidFile(options.pgidFile);
136
121
  let staged;
137
122
  try {
@@ -139,12 +124,43 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
139
124
  staged = await stageProfileFiles(projectRoot, options.profileFile);
140
125
  options = { ...options, machArg: [...(options.machArg ?? []), ...staged.args] };
141
126
  }
142
- await runTestCommandBody(projectRoot, testPaths, options, staged?.env);
127
+ assertParallelTestUsage(testPaths, options);
128
+ const lockedOptions = await runTestPreLockPreflight(projectRoot, testPaths, options);
129
+ const profileEnv = staged?.env;
130
+ // The pre-lock pass resolved `sharedLock` against config and the run's shape.
131
+ const lockMode = lockedOptions.sharedLock === true ? 'shared' : 'exclusive';
132
+ if (lockMode === 'shared')
133
+ setVerdictRunAttribute('lock', 'shared');
134
+ resetTestResults();
135
+ if (options.results !== undefined) {
136
+ setVerdictRunAttribute('results', resolve(projectRoot, options.results));
137
+ }
138
+ const orchestrate = (rawLogDir) => orchestrateTestRuns(projectRoot, testPaths, lockedOptions, (paths, runOptions, hooks) => runTestCommandBody(projectRoot, paths, runOptions, profileEnv, {
139
+ ...hooks,
140
+ ...(rawLogDir === undefined ? {} : { rawLogDir }),
141
+ }), (error) => {
142
+ renderPreflightRefusal(error);
143
+ emitFailVerdict('preflight', refusalNote(error));
144
+ });
145
+ const collectsResults = options.results !== undefined || options.shardSize !== undefined;
146
+ try {
147
+ await underLock(() => (collectsResults ? withRawLogDir((dir) => orchestrate(dir)) : orchestrate()), lockMode);
148
+ }
149
+ finally {
150
+ if (options.results !== undefined) {
151
+ await writeResultsFile(resolve(projectRoot, options.results));
152
+ }
153
+ }
143
154
  }
144
155
  catch (error) {
145
156
  if (!verdictEmitted()) {
146
- renderPreflightRefusal(error);
147
- emitFailVerdict('preflight', refusalNote(error));
157
+ if (error instanceof LockContentionError) {
158
+ emitFailVerdict('lock-timeout');
159
+ }
160
+ else {
161
+ renderPreflightRefusal(error);
162
+ emitFailVerdict('preflight', refusalNote(error));
163
+ }
148
164
  }
149
165
  throw error;
150
166
  }
@@ -167,6 +183,15 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
167
183
  }
168
184
  }
169
185
  }
186
+ /** Writes `--results`; a failure warns, since the run's verdict is already out. */
187
+ async function writeResultsFile(path) {
188
+ try {
189
+ await writeTestResults(path);
190
+ }
191
+ catch (error) {
192
+ warn(`Could not write --results ${path}: ${toError(error).message}`);
193
+ }
194
+ }
170
195
  /** The refusal class a preflight gate named, when it named one. */
171
196
  function refusalNote(error) {
172
197
  return error instanceof PreflightRefusalError ? error.note : undefined;
@@ -201,24 +226,6 @@ function renderPreflightRefusal(error) {
201
226
  process.stdout.write(block);
202
227
  writeToActiveRunLog(block);
203
228
  }
204
- /**
205
- * Verifies `engine/` did not change under the run before any PASS verdict
206
- * may print. On failure the run's verdict is `FAIL reason=inconclusive`,
207
- * because the harness result exists but cannot be trusted (`preflight` would
208
- * misdescribe a run whose harness already executed). It is emitted here so the
209
- * guard's throw can never leave a stale or missing verdict line behind.
210
- */
211
- async function verifyEngineGenerationOrEmitInconclusive(engineDir, before, powerChanged = false) {
212
- try {
213
- await assertEngineGenerationUnchanged(engineDir, before);
214
- if (powerChanged)
215
- throw new GeneralError('Perf power source changed during the run; repeat this sitting on a stable power source.');
216
- }
217
- catch (error) {
218
- emitFailVerdict('inconclusive');
219
- throw error;
220
- }
221
- }
222
229
  async function guardBuildBrowser(engineDir, launchablePath, options) {
223
230
  if ((options.build || options.buildOnly) && launchablePath) {
224
231
  await waitForPreflight(() => ensureLaunchableBrowserNotRunning(join(engineDir, launchablePath), {
@@ -255,9 +262,40 @@ function prepareDispatchArguments(classification, options, canaryPath, projectCo
255
262
  ? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
256
263
  : [];
257
264
  const extraArgs = prepareExtraMachArgs(options, shuffleSeed, canaryPath, projectConfig, forwardedMachArgs);
265
+ // A `--shard-size` chunk runs several files in one browser; mochitest
266
+ // restarts it between them so a chunk judges each file as a shard would.
267
+ const restart = options.shardSize !== undefined && suite === 'mochitest'
268
+ ? { ...options, restartBetweenTests: true }
269
+ : options;
270
+ extraArgs.push(...restartBetweenTestsArgs(restart, suite, forwardedMachArgs));
258
271
  return { suite, shuffleSeed, extraArgs };
259
272
  }
260
- async function runTestCommandBody(projectRoot, testPaths, options = {}, profileEnv) {
273
+ /**
274
+ * Directory arguments mean exactly that directory: mozbuild's test resolver
275
+ * matches paths by string prefix, so a bare directory arg silently sweeps in
276
+ * prefix-named siblings, with `…/test/hominis` also running
277
+ * `…/test/hominis-tiles`, with no indication the scope widened. Each
278
+ * directory argument therefore dispatches as its enumerated explicit
279
+ * test-file list, which cannot prefix-match a sibling. A trailing-`/`
280
+ * normalization does not work, as mach still sweeps the sibling in. Any
281
+ * prefix-siblings are echoed so the narrowed scope is visible.
282
+ *
283
+ * xpcshell appdir auto-injection happens per harness invocation inside
284
+ * `runTestsWithRetries` (src/commands/test-run.ts) so sharded runs probe the
285
+ * manifest for each file individually. See src/core/xpcshell-appdir.ts for
286
+ * the full motivation.
287
+ */
288
+ async function resolveDispatchGroups(engineDir, normalizedPaths) {
289
+ const scopes = await analyzeTestPathScopes(engineDir, normalizedPaths);
290
+ for (const scope of scopes) {
291
+ const notice = formatScopeNotice(scope);
292
+ if (notice)
293
+ info(notice);
294
+ }
295
+ logTestSelection(scopes);
296
+ return scopes.map((scope) => ({ label: scope.requestedPath, paths: scope.dispatchPaths }));
297
+ }
298
+ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileEnv, hooks = {}) {
261
299
  const paths = getProjectPaths(projectRoot);
262
300
  // Check if engine exists
263
301
  await assertEngineExists(paths.engine);
@@ -281,7 +319,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
281
319
  // against an incomplete build surface the missing-bundle path instead of a
282
320
  // cryptic `Browser process exited during spawn (exit code 1, signal none).
283
321
  // stderr tail: (empty)`.
284
- const launchablePath = await resolveLaunchablePathForTests(paths.engine, projectConfig.binaryName, buildCheck.objDir);
322
+ const launchablePath = await resolveLaunchablePathForTests(projectRoot, paths.engine, projectConfig.binaryName, buildCheck.objDir);
285
323
  const harnessRetries = options.harnessRetries ?? DEFAULT_HARNESS_RETRIES;
286
324
  // Normalized engine-relative request paths, hoisted above the build/stale
287
325
  // gate: the pre-test build records them as the packaging-coverage claim,
@@ -302,9 +340,12 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
302
340
  // documented `--mach-arg --marionette-port=NNNN` route still hits the
303
341
  // wrapper preflight refusing on 2828 before the forwarded arg reaches
304
342
  // mach.
343
+ // A negative control on compiled source goes in here, so the build
344
+ // compiles it (see negative-control-compiled.ts).
345
+ await hooks.beforeBuild?.();
305
346
  if (await runTestBuildPhase(projectRoot, paths, projectConfig, harnessRetries, options, {
306
347
  classification,
307
- normalizedPaths,
348
+ normalizedPaths: hooks.coveragePaths ?? normalizedPaths,
308
349
  })) {
309
350
  return;
310
351
  }
@@ -324,31 +365,9 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
324
365
  await assertTestPathsExist(paths.engine, normalizedPaths);
325
366
  const { suite, shuffleSeed, extraArgs } = prepareDispatchArguments(classification, options, canaryPath, projectConfig);
326
367
  appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly);
327
- // Directory arguments mean exactly that directory: mozbuild's test
328
- // resolver matches paths by string prefix, so a bare directory arg
329
- // silently sweeps in prefix-named siblings, with `…/test/hominis` also
330
- // running `…/test/hominis-tiles`, with no indication the scope widened. Each
331
- // directory argument therefore dispatches as its enumerated explicit
332
- // test-file list, which cannot prefix-match a sibling. A trailing-`/`
333
- // normalization does not work, as mach still sweeps the sibling in. Any
334
- // prefix-siblings are echoed so the narrowed scope is visible.
335
368
  // Classification above intentionally used the raw argument forms. Only the
336
369
  // mach dispatch needs the exact-match shape.
337
- const scopes = await analyzeTestPathScopes(paths.engine, normalizedPaths);
338
- const dispatchGroups = scopes.map((scope) => ({
339
- label: scope.requestedPath,
340
- paths: scope.dispatchPaths,
341
- }));
342
- for (const scope of scopes) {
343
- const notice = formatScopeNotice(scope);
344
- if (notice)
345
- info(notice);
346
- }
347
- // xpcshell appdir auto-injection happens per harness invocation inside
348
- // `runTestsWithRetries` (src/commands/test-run.ts) so sharded runs probe
349
- // the manifest for each file individually. See src/core/xpcshell-appdir.ts
350
- // for the full motivation.
351
- logTestSelection(scopes);
370
+ const dispatchGroups = await resolveDispatchGroups(paths.engine, normalizedPaths);
352
371
  const harnessEnv = mergeHarnessEnv(buildPerfSampleEnv(projectRoot, projectConfig.binaryName, options.perfSamples), profileEnv, shuffleSeed === undefined ? undefined : { FIREFORGE_SHUFFLE_SEED: String(shuffleSeed) });
353
372
  const runCtx = {
354
373
  engineDir: paths.engine,
@@ -361,55 +380,32 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
361
380
  ...(options.fullOutput === true ? { fullOutput: true } : {}),
362
381
  ...(harnessEnv ? { env: harnessEnv } : {}),
363
382
  pgidFile: options.pgidFile,
383
+ ...(hooks.rawLogDir === undefined ? {} : { rawLogDir: hooks.rawLogDir }),
384
+ ...(hooks.repetition === undefined ? {} : { repetition: hooks.repetition }),
364
385
  };
365
386
  const postRebuildContext = options.build
366
387
  ? createPostRebuildFailureContext('fireforge test --build', normalizedPaths)
367
388
  : undefined;
389
+ // A negative control goes in here: after every gate and the build, so
390
+ // packaging cannot overwrite it and stale-build cannot refuse it.
391
+ await hooks.beforeDispatch?.();
368
392
  // Multi-argument requests shard into sequential harness runs by default:
369
393
  // one shared mochitest profile across files bleeds pref/media-query state
370
394
  // into later files. Sharding is per path argument: a directory argument
371
395
  // keeps its enumerated files together in one invocation, preserving the
372
396
  // one-browser-instance semantics of a directory run. `--no-shard` restores
373
- // the combined invocation. The default must not be silent: a cross-file
374
- // pollution repro otherwise "passes" because the comparison run was
375
- // sharded without saying so, misattributing a suite-context bug upstream.
376
- // Hence the one-line notice stating what sharding does and does not
377
- // exercise.
378
- if (canaryPath === undefined && dispatchGroups.length > 1 && options.shard !== false) {
379
- notice(`Sharding: running ${dispatchGroups.length} test path arguments in isolated browser instances ` +
380
- '(one mach invocation per argument; a directory argument keeps its files in one instance). ' +
381
- 'Cross-argument state is NOT exercised — pass --no-shard for a combined single-instance run.');
382
- const generationBefore = await snapshotEngineGeneration(paths.engine);
383
- let summary;
384
- try {
385
- const orderedGroups = shuffleSeed === undefined ? dispatchGroups : shuffleTestGroups(dispatchGroups, shuffleSeed);
386
- summary = await runShardedTests(runCtx, orderedGroups, (outcome, label) => diagnoseShardOutcome(outcome, label, projectConfig.binaryName, postRebuildContext));
387
- }
388
- finally {
389
- // Runs before the aggregate verdict below: a mutated engine/ emits
390
- // `FAIL reason=inconclusive` and throws, so an invalidated run can
391
- // never print `PASS shards=N/N` first. (A throw here masks an
392
- // in-flight shard error, as the plain assert always did.)
393
- await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore, runCtx.powerChanged);
394
- }
395
- finalizeShardedOutcome(summary);
396
- return;
397
- }
398
- const combinedDispatchPaths = dispatchGroups.flatMap((group) => group.paths);
399
- let outcome;
400
- const generationBefore = await snapshotEngineGeneration(paths.engine);
401
- try {
402
- outcome = await runTestsWithRetries(runCtx, combinedDispatchPaths);
403
- }
404
- catch (error) {
405
- throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
406
- }
407
- finally {
408
- await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore, runCtx.powerChanged);
409
- }
410
- if (canaryPath !== undefined) {
411
- reportCanaryOutcome(outcome);
412
- return;
413
- }
414
- finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext, options.headless === true);
397
+ // the combined invocation; `--shard-size` and `--jobs` reshape it (see
398
+ // test-dispatch.ts).
399
+ await dispatchTests({
400
+ engineDir: paths.engine,
401
+ runCtx,
402
+ dispatchGroups,
403
+ options,
404
+ projectConfig,
405
+ canaryPath,
406
+ shuffleSeed,
407
+ normalizedPaths,
408
+ xpcshellOnly,
409
+ postRebuildContext,
410
+ });
415
411
  }
@@ -0,0 +1,14 @@
1
+ /** Options accepted by {@link tokenLintCommand}. */
2
+ export interface TokenLintOptions {
3
+ /** Emit the machine-readable envelope instead of the human report. */
4
+ json?: boolean;
5
+ }
6
+ /**
7
+ * Runs `token lint`. Exits non-zero (GeneralError) when any finding is
8
+ * reported; with `--json` the report is written first, as `status --check
9
+ * --json` does.
10
+ *
11
+ * @param projectRoot - Root directory of the project
12
+ * @param options - Output-mode options
13
+ */
14
+ export declare function tokenLintCommand(projectRoot: string, options?: TokenLintOptions): Promise<void>;