@hominis/fireforge 0.35.0 → 0.37.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 (369) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +34 -9
  3. package/dist/bin/fireforge.js +21 -6
  4. package/dist/src/cli.js +22 -5
  5. package/dist/src/commands/bootstrap-checks.js +0 -1
  6. package/dist/src/commands/bootstrap.d.ts +2 -1
  7. package/dist/src/commands/bootstrap.js +16 -7
  8. package/dist/src/commands/build.js +5 -4
  9. package/dist/src/commands/config.js +30 -10
  10. package/dist/src/commands/discard.js +18 -3
  11. package/dist/src/commands/doctor/post-rebase-audit.js +0 -1
  12. package/dist/src/commands/doctor-check-core.js +0 -1
  13. package/dist/src/commands/doctor-external-toolchains.d.ts +3 -0
  14. package/dist/src/commands/doctor-external-toolchains.js +54 -0
  15. package/dist/src/commands/doctor-furnace-jar.js +0 -1
  16. package/dist/src/commands/doctor-furnace-manifest-sync.js +0 -1
  17. package/dist/src/commands/doctor-furnace.js +2 -2
  18. package/dist/src/commands/doctor-orphaned-harness.d.ts +52 -0
  19. package/dist/src/commands/doctor-orphaned-harness.js +132 -0
  20. package/dist/src/commands/doctor-working-tree.js +0 -1
  21. package/dist/src/commands/doctor.js +6 -1
  22. package/dist/src/commands/download.js +28 -13
  23. package/dist/src/commands/export-all.js +5 -3
  24. package/dist/src/commands/export-flow.js +12 -7
  25. package/dist/src/commands/export-placement-gate.js +0 -1
  26. package/dist/src/commands/export-placement-policy.js +0 -1
  27. package/dist/src/commands/export-shared.d.ts +5 -1
  28. package/dist/src/commands/export-shared.js +18 -6
  29. package/dist/src/commands/export.js +22 -10
  30. package/dist/src/commands/furnace/apply.js +22 -6
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
  32. package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
  33. package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
  34. package/dist/src/commands/furnace/chrome-doc.js +0 -1
  35. package/dist/src/commands/furnace/create-browser-test.js +0 -1
  36. package/dist/src/commands/furnace/create-dry-run.js +0 -1
  37. package/dist/src/commands/furnace/create-features.js +0 -1
  38. package/dist/src/commands/furnace/create-mochikit.js +0 -1
  39. package/dist/src/commands/furnace/create-readback.js +0 -1
  40. package/dist/src/commands/furnace/create-templates.js +0 -1
  41. package/dist/src/commands/furnace/create-validation.js +0 -1
  42. package/dist/src/commands/furnace/create-xpcshell.js +0 -1
  43. package/dist/src/commands/furnace/create.js +0 -1
  44. package/dist/src/commands/furnace/deploy.js +7 -83
  45. package/dist/src/commands/furnace/diff.js +0 -1
  46. package/dist/src/commands/furnace/index.js +59 -25
  47. package/dist/src/commands/furnace/init.js +0 -1
  48. package/dist/src/commands/furnace/list.js +0 -1
  49. package/dist/src/commands/furnace/override.js +0 -1
  50. package/dist/src/commands/furnace/preview.js +3 -3
  51. package/dist/src/commands/furnace/refresh.js +0 -1
  52. package/dist/src/commands/furnace/remove-state.d.ts +15 -0
  53. package/dist/src/commands/furnace/remove-state.js +47 -1
  54. package/dist/src/commands/furnace/remove.js +3 -9
  55. package/dist/src/commands/furnace/rename-browser-test.js +1 -5
  56. package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
  57. package/dist/src/commands/furnace/rename-helpers.js +30 -1
  58. package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
  59. package/dist/src/commands/furnace/rename.js +15 -36
  60. package/dist/src/commands/furnace/scan.js +0 -1
  61. package/dist/src/commands/furnace/status.js +0 -1
  62. package/dist/src/commands/furnace/sync.js +76 -4
  63. package/dist/src/commands/furnace/validate.js +0 -1
  64. package/dist/src/commands/furnace/validation-output.js +0 -1
  65. package/dist/src/commands/import.js +35 -9
  66. package/dist/src/commands/lint-per-patch.js +19 -7
  67. package/dist/src/commands/lint.js +13 -3
  68. package/dist/src/commands/manifest.js +1 -2
  69. package/dist/src/commands/package.js +2 -1
  70. package/dist/src/commands/patch/compact.js +1 -1
  71. package/dist/src/commands/patch/delete.js +1 -1
  72. package/dist/src/commands/patch/index.js +1 -1
  73. package/dist/src/commands/patch/lint-ignore.js +21 -6
  74. package/dist/src/commands/patch/move-files.js +1 -1
  75. package/dist/src/commands/patch/patch-context.js +0 -1
  76. package/dist/src/commands/patch/rename.js +4 -4
  77. package/dist/src/commands/patch/reorder.js +3 -4
  78. package/dist/src/commands/patch/split-plan.js +2 -1
  79. package/dist/src/commands/patch/split.js +4 -4
  80. package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
  81. package/dist/src/commands/patch/staged-dependency.js +114 -52
  82. package/dist/src/commands/patch/tier.js +18 -5
  83. package/dist/src/commands/re-export-bulk-scan.js +0 -1
  84. package/dist/src/commands/re-export-files.js +5 -1
  85. package/dist/src/commands/re-export-options.js +0 -1
  86. package/dist/src/commands/re-export-scan.js +0 -1
  87. package/dist/src/commands/re-export.js +14 -4
  88. package/dist/src/commands/rebase/abort.js +2 -9
  89. package/dist/src/commands/rebase/confirm.js +0 -1
  90. package/dist/src/commands/rebase/conflict-summary.js +0 -1
  91. package/dist/src/commands/rebase/continue.js +0 -1
  92. package/dist/src/commands/rebase/index.d.ts +1 -1
  93. package/dist/src/commands/rebase/index.js +10 -15
  94. package/dist/src/commands/rebase/patch-loop.js +38 -7
  95. package/dist/src/commands/rebase/summary.js +2 -3
  96. package/dist/src/commands/rebase.js +0 -1
  97. package/dist/src/commands/register.js +1 -1
  98. package/dist/src/commands/reset.js +16 -9
  99. package/dist/src/commands/resolve.js +2 -2
  100. package/dist/src/commands/run.js +4 -4
  101. package/dist/src/commands/setup-support.js +0 -1
  102. package/dist/src/commands/setup.js +1 -2
  103. package/dist/src/commands/source.js +1 -2
  104. package/dist/src/commands/status-output.js +1 -12
  105. package/dist/src/commands/status.js +147 -133
  106. package/dist/src/commands/test-appdir.js +0 -1
  107. package/dist/src/commands/test-diagnose.js +38 -3
  108. package/dist/src/commands/test-modes.d.ts +15 -0
  109. package/dist/src/commands/test-modes.js +61 -0
  110. package/dist/src/commands/test-register.d.ts +4 -0
  111. package/dist/src/commands/test-register.js +64 -0
  112. package/dist/src/commands/test-run.d.ts +23 -7
  113. package/dist/src/commands/test-run.js +15 -14
  114. package/dist/src/commands/test.d.ts +0 -4
  115. package/dist/src/commands/test.js +147 -96
  116. package/dist/src/commands/token-coverage.js +0 -1
  117. package/dist/src/commands/token.js +1 -2
  118. package/dist/src/commands/typecheck.js +1 -1
  119. package/dist/src/commands/verify.js +3 -9
  120. package/dist/src/commands/watch.js +5 -1
  121. package/dist/src/commands/wire.js +1 -1
  122. package/dist/src/core/ast-utils.js +0 -1
  123. package/dist/src/core/brand-validation.js +0 -1
  124. package/dist/src/core/branding.js +0 -1
  125. package/dist/src/core/browser-wire.js +0 -1
  126. package/dist/src/core/build-audit-platform.js +0 -1
  127. package/dist/src/core/build-audit-registration.js +0 -1
  128. package/dist/src/core/build-audit-resolve.js +0 -1
  129. package/dist/src/core/build-audit-transforms.js +0 -1
  130. package/dist/src/core/build-audit.js +0 -1
  131. package/dist/src/core/build-baseline-types.d.ts +24 -0
  132. package/dist/src/core/build-baseline-types.js +0 -1
  133. package/dist/src/core/build-baseline.d.ts +5 -2
  134. package/dist/src/core/build-baseline.js +5 -2
  135. package/dist/src/core/build-prepare.js +2 -2
  136. package/dist/src/core/config-mutate.js +0 -1
  137. package/dist/src/core/config-paths.d.ts +2 -2
  138. package/dist/src/core/config-paths.js +6 -1
  139. package/dist/src/core/config-state.js +0 -1
  140. package/dist/src/core/config-validate-patch-policy.js +0 -1
  141. package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
  142. package/dist/src/core/config-validate-test-toolchains.js +105 -0
  143. package/dist/src/core/config-validate.js +3 -1
  144. package/dist/src/core/config.js +0 -1
  145. package/dist/src/core/destructive.js +0 -1
  146. package/dist/src/core/diff-hunks.js +13 -7
  147. package/dist/src/core/engine-changes.js +0 -1
  148. package/dist/src/core/engine-session-lock.d.ts +12 -0
  149. package/dist/src/core/engine-session-lock.js +45 -0
  150. package/dist/src/core/file-lock.d.ts +8 -0
  151. package/dist/src/core/file-lock.js +157 -48
  152. package/dist/src/core/firefox-archive.d.ts +4 -0
  153. package/dist/src/core/firefox-archive.js +6 -1
  154. package/dist/src/core/firefox-cache.js +60 -4
  155. package/dist/src/core/firefox-download.js +0 -1
  156. package/dist/src/core/firefox-extract.js +75 -16
  157. package/dist/src/core/firefox-ignorefile.js +0 -1
  158. package/dist/src/core/firefox.d.ts +12 -0
  159. package/dist/src/core/firefox.js +64 -2
  160. package/dist/src/core/furnace-apply-dry-run.js +0 -1
  161. package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
  162. package/dist/src/core/furnace-apply-ftl.js +10 -5
  163. package/dist/src/core/furnace-apply-helpers.js +0 -1
  164. package/dist/src/core/furnace-apply-output.js +2 -2
  165. package/dist/src/core/furnace-apply.d.ts +7 -1
  166. package/dist/src/core/furnace-apply.js +47 -13
  167. package/dist/src/core/furnace-checksum-utils.js +0 -1
  168. package/dist/src/core/furnace-config-array-utils.js +0 -1
  169. package/dist/src/core/furnace-config-custom.js +10 -1
  170. package/dist/src/core/furnace-config-order.js +0 -1
  171. package/dist/src/core/furnace-config-tokens.js +0 -1
  172. package/dist/src/core/furnace-config-validate.js +0 -1
  173. package/dist/src/core/furnace-config.d.ts +11 -0
  174. package/dist/src/core/furnace-config.js +37 -1
  175. package/dist/src/core/furnace-constants.js +0 -1
  176. package/dist/src/core/furnace-css-fragments.js +0 -1
  177. package/dist/src/core/furnace-graph-utils.js +0 -1
  178. package/dist/src/core/furnace-jsconfig.d.ts +2 -3
  179. package/dist/src/core/furnace-jsconfig.js +25 -18
  180. package/dist/src/core/furnace-marker.js +0 -1
  181. package/dist/src/core/furnace-operation.js +0 -1
  182. package/dist/src/core/furnace-refresh.js +0 -1
  183. package/dist/src/core/furnace-registration-ast.d.ts +14 -0
  184. package/dist/src/core/furnace-registration-ast.js +1 -2
  185. package/dist/src/core/furnace-registration-remove.js +0 -1
  186. package/dist/src/core/furnace-registration-validate.js +1 -5
  187. package/dist/src/core/furnace-registration.js +0 -1
  188. package/dist/src/core/furnace-rollback.js +0 -1
  189. package/dist/src/core/furnace-scanner.js +0 -1
  190. package/dist/src/core/furnace-stale-export.d.ts +59 -0
  191. package/dist/src/core/furnace-stale-export.js +123 -0
  192. package/dist/src/core/furnace-staleness.js +0 -1
  193. package/dist/src/core/furnace-state-persist.d.ts +72 -0
  194. package/dist/src/core/furnace-state-persist.js +118 -0
  195. package/dist/src/core/furnace-step-errors.d.ts +23 -0
  196. package/dist/src/core/furnace-step-errors.js +25 -0
  197. package/dist/src/core/furnace-stories.js +0 -1
  198. package/dist/src/core/furnace-validate-accessibility.js +0 -1
  199. package/dist/src/core/furnace-validate-checks.js +0 -1
  200. package/dist/src/core/furnace-validate-compatibility.js +9 -3
  201. package/dist/src/core/furnace-validate-helpers.js +0 -1
  202. package/dist/src/core/furnace-validate-registration.js +7 -7
  203. package/dist/src/core/furnace-validate-structure.d.ts +8 -0
  204. package/dist/src/core/furnace-validate-structure.js +14 -4
  205. package/dist/src/core/furnace-validate.js +0 -1
  206. package/dist/src/core/furnace-version-drift.d.ts +6 -3
  207. package/dist/src/core/furnace-version-drift.js +6 -4
  208. package/dist/src/core/git-base.js +0 -1
  209. package/dist/src/core/git-diff.js +0 -1
  210. package/dist/src/core/git-file-ops.js +0 -1
  211. package/dist/src/core/git-status.d.ts +13 -0
  212. package/dist/src/core/git-status.js +32 -2
  213. package/dist/src/core/git.d.ts +10 -1
  214. package/dist/src/core/git.js +63 -6
  215. package/dist/src/core/license-headers.js +0 -1
  216. package/dist/src/core/lint-cache.js +0 -1
  217. package/dist/src/core/lint-projection.js +0 -1
  218. package/dist/src/core/mach-build-artifacts.js +0 -1
  219. package/dist/src/core/mach-error-hints.js +0 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +60 -0
  221. package/dist/src/core/mach-known-noise-filter.js +193 -0
  222. package/dist/src/core/mach-mozconfig.js +0 -1
  223. package/dist/src/core/mach-python.js +0 -1
  224. package/dist/src/core/mach-resource-shim.js +0 -1
  225. package/dist/src/core/mach.d.ts +8 -0
  226. package/dist/src/core/mach.js +36 -4
  227. package/dist/src/core/manifest-helpers.js +0 -1
  228. package/dist/src/core/manifest-register.js +0 -1
  229. package/dist/src/core/manifest-rules.js +0 -1
  230. package/dist/src/core/manifest-tokenizers.js +0 -1
  231. package/dist/src/core/marionette-port.d.ts +8 -0
  232. package/dist/src/core/marionette-port.js +34 -3
  233. package/dist/src/core/marionette-preflight.js +0 -1
  234. package/dist/src/core/markdown-table.js +0 -1
  235. package/dist/src/core/ownership-table.js +2 -9
  236. package/dist/src/core/parser-fallback.js +0 -1
  237. package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
  238. package/dist/src/core/patch-apply-fuzz.js +71 -34
  239. package/dist/src/core/patch-apply.d.ts +30 -0
  240. package/dist/src/core/patch-apply.js +101 -18
  241. package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
  242. package/dist/src/core/patch-artifact-normalize.js +19 -15
  243. package/dist/src/core/patch-export-coverage.js +0 -1
  244. package/dist/src/core/patch-export-metadata.js +0 -1
  245. package/dist/src/core/patch-export-update.js +2 -3
  246. package/dist/src/core/patch-export.d.ts +2 -6
  247. package/dist/src/core/patch-export.js +24 -48
  248. package/dist/src/core/patch-files.js +0 -1
  249. package/dist/src/core/patch-identifier-suggest.js +0 -1
  250. package/dist/src/core/patch-lint-checkjs.js +0 -1
  251. package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
  252. package/dist/src/core/patch-lint-cross.js +80 -3
  253. package/dist/src/core/patch-lint-css.js +0 -1
  254. package/dist/src/core/patch-lint-diff-tag.js +0 -1
  255. package/dist/src/core/patch-lint-diff.d.ts +2 -1
  256. package/dist/src/core/patch-lint-diff.js +17 -32
  257. package/dist/src/core/patch-lint-imports.js +0 -1
  258. package/dist/src/core/patch-lint-jsdoc.js +0 -1
  259. package/dist/src/core/patch-lint-observer.js +0 -1
  260. package/dist/src/core/patch-lint-ownership.js +0 -1
  261. package/dist/src/core/patch-lint-reexports.js +0 -1
  262. package/dist/src/core/patch-lint.d.ts +37 -2
  263. package/dist/src/core/patch-lint.js +47 -34
  264. package/dist/src/core/patch-lock.js +0 -1
  265. package/dist/src/core/patch-manifest-consistency.js +9 -1
  266. package/dist/src/core/patch-manifest-io.d.ts +19 -2
  267. package/dist/src/core/patch-manifest-io.js +57 -21
  268. package/dist/src/core/patch-manifest-query.js +18 -10
  269. package/dist/src/core/patch-manifest-resolve.js +0 -1
  270. package/dist/src/core/patch-manifest-validate.js +24 -1
  271. package/dist/src/core/patch-manifest.d.ts +1 -1
  272. package/dist/src/core/patch-manifest.js +1 -2
  273. package/dist/src/core/patch-parse.d.ts +59 -0
  274. package/dist/src/core/patch-parse.js +209 -92
  275. package/dist/src/core/patch-policy.js +0 -1
  276. package/dist/src/core/patch-registration-refs.js +16 -26
  277. package/dist/src/core/patch-source-metadata.js +0 -1
  278. package/dist/src/core/patch-transform.d.ts +4 -0
  279. package/dist/src/core/patch-transform.js +30 -38
  280. package/dist/src/core/rebase-session.js +0 -1
  281. package/dist/src/core/register-browser-content.js +0 -1
  282. package/dist/src/core/register-module.js +0 -1
  283. package/dist/src/core/register-result.js +0 -1
  284. package/dist/src/core/register-scaffold.js +0 -1
  285. package/dist/src/core/register-shared-css.js +0 -1
  286. package/dist/src/core/register-test-manifest.js +0 -1
  287. package/dist/src/core/register-xpcshell-test.js +0 -1
  288. package/dist/src/core/shared-ftl.js +0 -1
  289. package/dist/src/core/signal-critical.js +0 -1
  290. package/dist/src/core/smoke-patterns.js +0 -1
  291. package/dist/src/core/state-file.js +0 -1
  292. package/dist/src/core/status-classify.d.ts +16 -0
  293. package/dist/src/core/status-classify.js +24 -16
  294. package/dist/src/core/test-harness-crash.d.ts +80 -7
  295. package/dist/src/core/test-harness-crash.js +251 -15
  296. package/dist/src/core/test-harness-output.js +0 -1
  297. package/dist/src/core/test-path-scope.d.ts +21 -13
  298. package/dist/src/core/test-path-scope.js +43 -28
  299. package/dist/src/core/test-stale-check.d.ts +29 -1
  300. package/dist/src/core/test-stale-check.js +59 -1
  301. package/dist/src/core/test-stale-symlink.js +0 -1
  302. package/dist/src/core/test-xpcshell-retry.js +0 -1
  303. package/dist/src/core/token-coverage.js +0 -1
  304. package/dist/src/core/token-dark-mode.js +0 -1
  305. package/dist/src/core/token-docs.js +0 -1
  306. package/dist/src/core/token-manager.js +0 -1
  307. package/dist/src/core/token-scaffold.js +0 -1
  308. package/dist/src/core/token-variant.js +0 -1
  309. package/dist/src/core/toolchain-preflight.d.ts +31 -10
  310. package/dist/src/core/toolchain-preflight.js +97 -36
  311. package/dist/src/core/typecheck-shim.js +13 -1
  312. package/dist/src/core/typecheck.js +0 -1
  313. package/dist/src/core/wire-destroy.js +0 -1
  314. package/dist/src/core/wire-dom-fragment.js +0 -1
  315. package/dist/src/core/wire-init.js +0 -1
  316. package/dist/src/core/wire-subscript.js +0 -1
  317. package/dist/src/core/wire-targets.js +0 -1
  318. package/dist/src/core/wire-utils.js +0 -1
  319. package/dist/src/core/xpcshell-appdir.js +1 -4
  320. package/dist/src/errors/base.d.ts +29 -0
  321. package/dist/src/errors/base.js +35 -1
  322. package/dist/src/errors/build.js +0 -1
  323. package/dist/src/errors/codes.d.ts +6 -0
  324. package/dist/src/errors/codes.js +6 -1
  325. package/dist/src/errors/config.js +0 -1
  326. package/dist/src/errors/download.js +0 -1
  327. package/dist/src/errors/furnace.d.ts +11 -1
  328. package/dist/src/errors/furnace.js +6 -5
  329. package/dist/src/errors/git.js +0 -1
  330. package/dist/src/errors/patch.d.ts +17 -0
  331. package/dist/src/errors/patch.js +29 -1
  332. package/dist/src/errors/rebase.js +0 -1
  333. package/dist/src/errors/run.js +1 -1
  334. package/dist/src/index.js +0 -1
  335. package/dist/src/types/cli.js +0 -1
  336. package/dist/src/types/commands/index.d.ts +1 -1
  337. package/dist/src/types/commands/index.js +0 -1
  338. package/dist/src/types/commands/options.d.ts +40 -9
  339. package/dist/src/types/commands/options.js +0 -1
  340. package/dist/src/types/commands/patches.d.ts +37 -0
  341. package/dist/src/types/commands/patches.js +0 -1
  342. package/dist/src/types/commands/project.d.ts +11 -2
  343. package/dist/src/types/commands/project.js +0 -1
  344. package/dist/src/types/config.d.ts +32 -0
  345. package/dist/src/types/config.js +0 -1
  346. package/dist/src/types/furnace.d.ts +23 -0
  347. package/dist/src/types/furnace.js +0 -1
  348. package/dist/src/types/index.js +0 -1
  349. package/dist/src/types/typecheck.js +0 -1
  350. package/dist/src/utils/elapsed.js +0 -1
  351. package/dist/src/utils/errors.js +0 -1
  352. package/dist/src/utils/fs.js +37 -5
  353. package/dist/src/utils/logger.d.ts +7 -0
  354. package/dist/src/utils/logger.js +85 -3
  355. package/dist/src/utils/options.d.ts +15 -0
  356. package/dist/src/utils/options.js +25 -1
  357. package/dist/src/utils/package-root.js +11 -4
  358. package/dist/src/utils/parse.js +0 -1
  359. package/dist/src/utils/paths.js +0 -1
  360. package/dist/src/utils/platform.js +0 -1
  361. package/dist/src/utils/process-group.d.ts +33 -0
  362. package/dist/src/utils/process-group.js +161 -0
  363. package/dist/src/utils/process.d.ts +48 -4
  364. package/dist/src/utils/process.js +258 -59
  365. package/dist/src/utils/regex.d.ts +13 -3
  366. package/dist/src/utils/regex.js +53 -4
  367. package/dist/src/utils/validation.d.ts +0 -12
  368. package/dist/src/utils/validation.js +13 -26
  369. package/package.json +9 -8
@@ -63,35 +63,36 @@ const SHARD_STATUS_LABEL = {
63
63
  'no-tests': 'NO-TESTS',
64
64
  };
65
65
  /**
66
- * Runs each requested path as its own sequential harness invocation and
67
- * prints an aggregate report. Per-shard failures are diagnosed via
68
- * `diagnoseShardFailure` (which receives the throwing diagnosis chain from
69
- * the command layer) but downgraded to warnings so every shard runs; a
70
- * single aggregate error is thrown at the end when any shard did not pass.
66
+ * Runs each requested path argument as its own sequential harness
67
+ * invocation (a directory argument's enumerated files stay together in
68
+ * one invocation see {@link ShardGroup}) and prints an aggregate
69
+ * report. Per-shard failures are diagnosed via `diagnoseShardFailure`
70
+ * (which receives the throwing diagnosis chain from the command layer)
71
+ * but downgraded to warnings so every shard runs; a single aggregate
72
+ * error is thrown at the end when any shard did not pass.
71
73
  */
72
- export async function runShardedTests(ctx, paths, diagnoseShardFailure) {
74
+ export async function runShardedTests(ctx, groups, diagnoseShardFailure) {
73
75
  const shards = [];
74
- for (const [index, path] of paths.entries()) {
75
- info(`— Shard ${index + 1}/${paths.length}: ${path}`);
76
- const outcome = await runTestsWithRetries(ctx, [path]);
77
- shards.push({ path, outcome });
76
+ for (const [index, group] of groups.entries()) {
77
+ info(`— Shard ${index + 1}/${groups.length}: ${group.label}`);
78
+ const outcome = await runTestsWithRetries(ctx, group.paths);
79
+ shards.push({ label: group.label, outcome });
78
80
  if (outcome.verdict.kind === 'harness-crash' && outcome.verdict.signature) {
79
81
  warn(buildHarnessCrashMessage(outcome.verdict.signature, outcome.attempts));
80
82
  }
81
83
  else if (outcome.verdict.kind !== 'tests-ran-ok') {
82
- const diagnosis = diagnoseShardFailure(outcome, path);
84
+ const diagnosis = diagnoseShardFailure(outcome, group.label);
83
85
  if (diagnosis)
84
86
  warn(diagnosis);
85
87
  }
86
88
  }
87
- const lines = shards.map(({ path, outcome }) => `${SHARD_STATUS_LABEL[outcome.verdict.kind].padEnd(8)} ${path}` +
89
+ const lines = shards.map(({ label, outcome }) => `${SHARD_STATUS_LABEL[outcome.verdict.kind].padEnd(8)} ${label}` +
88
90
  (outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : ''));
89
91
  const failing = shards.filter(({ outcome }) => outcome.verdict.kind !== 'tests-ran-ok');
90
92
  note(`${lines.join('\n')}\n\n${shards.length - failing.length}/${shards.length} shard(s) passed`, 'Sharded Test Summary');
91
93
  if (failing.length > 0) {
92
94
  throw new BuildError(`${failing.length} of ${shards.length} sharded test run(s) did not pass: ` +
93
- `${failing.map(({ path }) => path).join(', ')}. ` +
95
+ `${failing.map(({ label }) => label).join(', ')}. ` +
94
96
  'See the per-shard diagnosis above. Use --no-shard to reproduce the combined single-invocation behaviour.', 'mach test');
95
97
  }
96
98
  }
97
- //# sourceMappingURL=test-run.js.map
@@ -1,5 +1,3 @@
1
- import { Command } from 'commander';
2
- import type { CommandContext } from '../types/cli.js';
3
1
  import type { TestOptions } from '../types/commands/index.js';
4
2
  /**
5
3
  * Runs the test command to execute mach tests.
@@ -8,5 +6,3 @@ import type { TestOptions } from '../types/commands/index.js';
8
6
  * @param options - Test options
9
7
  */
10
8
  export declare function testCommand(projectRoot: string, testPaths: string[], options?: TestOptions): Promise<void>;
11
- /** Registers the test command on the CLI program. */
12
- export declare function registerTest(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
@@ -1,23 +1,25 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { join, resolve } from 'node:path';
3
- import { readBuildBaseline } from '../core/build-baseline.js';
3
+ import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
4
4
  import { prepareBuildEnvironment } from '../core/build-prepare.js';
5
5
  import { getProjectPaths, loadConfig } from '../core/config.js';
6
+ import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
6
7
  import { buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, runProtectedMachBuild, withBuildLock, } from '../core/mach.js';
7
- import { assertMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
8
+ import { assertMarionettePortAvailable, ensureMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
8
9
  import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
9
10
  import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
10
11
  import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
11
- import { analyzeTestPathScopes, formatScopeNotice } from '../core/test-path-scope.js';
12
- import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
12
+ import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
13
+ import { checkStaleBuildForTest, findUncoveredRequestPaths, formatStaleBuildWarning, formatTestCoverageRefusal, } from '../core/test-stale-check.js';
13
14
  import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
14
15
  import { GeneralError } from '../errors/base.js';
15
16
  import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
17
+ import { toError } from '../utils/errors.js';
16
18
  import { pathExists } from '../utils/fs.js';
17
- import { info, intro, outro, spinner, success, warn } from '../utils/logger.js';
18
- import { pickDefined } from '../utils/options.js';
19
+ import { info, intro, outro, spinner, success, verbose, warn } from '../utils/logger.js';
19
20
  import { stripEnginePrefix } from '../utils/paths.js';
20
21
  import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
22
+ import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
21
23
  import { DEFAULT_HARNESS_RETRIES, runShardedTests, runTestsWithRetries, } from './test-run.js';
22
24
  async function assertTestPathsExist(engineDir, testPaths) {
23
25
  const missingPaths = [];
@@ -102,7 +104,7 @@ async function resolveLaunchablePathForTests(engineDir, binaryName, objDir) {
102
104
  }
103
105
  return bundleCheck.expectedPath;
104
106
  }
105
- async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries) {
107
+ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage) {
106
108
  await withBuildLock(projectRoot, async () => {
107
109
  // Pass the previous baseline exactly like `fireforge build` does, so
108
110
  // auto-configure runs under the same conditions on both paths. The
@@ -127,6 +129,19 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
127
129
  });
128
130
  if (result.exitCode === 0) {
129
131
  s.stop('Build complete');
132
+ // Same record, same failure tolerance as `fireforge build` /
133
+ // `build --ui` (build.ts): a green pre-test build refreshes the
134
+ // stale-build baseline so a later plain `fireforge test` over the
135
+ // same files — in any invocation shape — is not refused. A failed
136
+ // write never fails the run. The coverage claim is scoped to the
137
+ // requested test paths, since a file-scoped `test --build` only
138
+ // guarantees packaging for those manifests.
139
+ try {
140
+ await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage);
141
+ }
142
+ catch (baselineError) {
143
+ verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
144
+ }
130
145
  return;
131
146
  }
132
147
  s.error('Pre-test build failed');
@@ -135,9 +150,57 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
135
150
  : 'Pre-test build failed', 'mach build faster');
136
151
  });
137
152
  }
138
- function logTestSelection(normalizedPaths) {
139
- if (normalizedPaths.length > 0) {
140
- info(`Running tests: ${normalizedPaths.join(', ')}`);
153
+ /**
154
+ * Stale-build preflight — when `--build` was NOT requested, detect
155
+ * packageable engine edits since the last successful build and fail
156
+ * UP-FRONT unless the operator explicitly accepts the stale package risk.
157
+ *
158
+ * Packaging COVERAGE is checked first, on EVERY non-`--build` run and
159
+ * regardless of staleness or `--allow-stale-build`: a runtime packaged by
160
+ * a file-scoped `test --build` can lack support fixtures for OTHER
161
+ * manifests even when nothing changed since — dispatching such a run
162
+ * hangs on missing fixtures rather than failing, so the flag (which only
163
+ * accepts stale CONTENT) must not be the trigger. Field incident: a
164
+ * three-file scoped rebuild left `file_tiles_audio.html` unpackaged and a
165
+ * later run over different files timed out twice at 45s waiting on
166
+ * `DOMAudioPlaybackStarted`.
167
+ *
168
+ * Exception: a path-less `test --doctor` stops at the Marionette health
169
+ * check (`runDoctorPreflight` returns 'stop' when no test paths were
170
+ * given) and never dispatches a test, so it needs no packaging coverage —
171
+ * treating it as a full-suite request would refuse a probe that touches
172
+ * no fixtures. The stale-content refusal still applies to it unchanged.
173
+ */
174
+ async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths) {
175
+ const stale = await checkStaleBuildForTest(projectRoot, engineDir);
176
+ const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
177
+ if (!dispatchesNoTests) {
178
+ const recordedCoverage = stale.baseline?.testPackagingCoverage;
179
+ const uncovered = findUncoveredRequestPaths(recordedCoverage, normalizedPaths);
180
+ if (uncovered.length > 0) {
181
+ throw new GeneralError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : []));
182
+ }
183
+ }
184
+ const staleMessage = stale.stale
185
+ ? `${formatStaleBuildWarning(stale)}\n\n` +
186
+ 'Run `fireforge test --build` to refresh the packaged runtime first, or pass ' +
187
+ '`--allow-stale-build` if you intentionally rebuilt out-of-band and accept the risk.'
188
+ : undefined;
189
+ if (staleMessage !== undefined) {
190
+ if (options.allowStaleBuild === true) {
191
+ warn(staleMessage);
192
+ }
193
+ else {
194
+ throw new GeneralError(staleMessage);
195
+ }
196
+ }
197
+ }
198
+ function logTestSelection(scopes) {
199
+ if (scopes.length > 0) {
200
+ const labels = scopes.map((scope) => scope.isDirectory && scope.testFileCount > 0
201
+ ? `${scope.requestedPath} (${scope.testFileCount} test file${scope.testFileCount === 1 ? '' : 's'}, passed explicitly)`
202
+ : scope.requestedPath);
203
+ info(`Running tests: ${labels.join(', ')}`);
141
204
  }
142
205
  else {
143
206
  info('Running all tests...');
@@ -255,6 +318,13 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
255
318
  }
256
319
  }
257
320
  }
321
+ async function ensureTestMarionettePortAvailable(port, binaryName, killStaleBrowser) {
322
+ if (killStaleBrowser) {
323
+ await ensureMarionettePortAvailable(port, { binaryName, killStaleBrowser: true });
324
+ return;
325
+ }
326
+ await assertMarionettePortAvailable(port, { binaryName });
327
+ }
258
328
  /**
259
329
  * Runs the test command to execute mach tests.
260
330
  * @param projectRoot - Root directory of the project
@@ -268,11 +338,14 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
268
338
  if (!(await pathExists(paths.engine))) {
269
339
  throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
270
340
  }
341
+ assertPathlessTestMode(testPaths, options);
271
342
  const buildCheck = await assertTestBuildArtifacts(paths.engine);
272
343
  // Load the project config once so both the build and the port
273
344
  // probe have access to `binaryName` (the port probe uses it to
274
345
  // recognise a fork-branded browser holding the Marionette port).
275
346
  const projectConfig = await loadConfig(projectRoot);
347
+ const canaryPath = resolveCanaryPath(options, projectConfig);
348
+ assertTestModeCombinations(testPaths, options, canaryPath);
276
349
  // `hasBuildArtifacts` only confirms `obj-*/dist/` exists; a partial
277
350
  // build (linker failed, packaging step interrupted, etc.) can satisfy
278
351
  // that check without ever writing the launchable binary the marionette
@@ -283,24 +356,23 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
283
356
  // during spawn (exit code 1, signal none). stderr tail: (empty)`.
284
357
  const launchablePath = await resolveLaunchablePathForTests(paths.engine, projectConfig.binaryName, buildCheck.objDir);
285
358
  const harnessRetries = options.harnessRetries ?? DEFAULT_HARNESS_RETRIES;
359
+ // Normalized engine-relative request paths, hoisted above the build/stale
360
+ // gate: the pre-test build records them as the packaging-coverage claim,
361
+ // and the --allow-stale-build path checks the request against the
362
+ // recorded coverage. (Existence is still asserted later, after the gate —
363
+ // stale/coverage refusals keep precedence over missing-path errors.)
364
+ const requestedPaths = canaryPath !== undefined ? [canaryPath] : testPaths;
365
+ const normalizedPaths = requestedPaths.map((p) => stripEnginePrefix(p).trim());
286
366
  // Run incremental build if requested
287
367
  if (options.build) {
288
- await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries);
368
+ // A path-less `test --build` runs (and packages for) the full suite;
369
+ // a scoped invocation only vouches for the requested paths.
370
+ const coverage = normalizedPaths.length === 0 ? 'full' : normalizedPaths;
371
+ await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, coverage);
289
372
  info('');
290
373
  }
291
374
  else {
292
- // Stale-build preflight — when --build was NOT requested, detect
293
- // packageable engine edits since the last successful `fireforge build`
294
- // and warn UP-FRONT. Without this, edits to chrome / packaged resources
295
- // surface only as a cryptic `NS_ERROR_FILE_NOT_FOUND` inside xpcshell
296
- // after mach test has already launched (see motivating case in
297
- // `core/test-stale-check.ts`). The check is warn-only so a fork that
298
- // rebuilt out-of-band (no FireForge-recorded baseline update) is not
299
- // blocked from running tests.
300
- const stale = await checkStaleBuildForTest(projectRoot, paths.engine);
301
- if (stale.stale) {
302
- warn(formatStaleBuildWarning(stale));
303
- }
375
+ await enforceStaleBuildGate(projectRoot, paths.engine, options, normalizedPaths);
304
376
  }
305
377
  // Resolve the effective Marionette port. Operator precedence:
306
378
  // 1. `--marionette-port` (first-class option, parsed at the CLI layer)
@@ -324,7 +396,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
324
396
  // generic bind failure. 2026-04-21 eval (Finding #20): a stale
325
397
  // `-marionette` process from `fresh/` poisoned a later test run in
326
398
  // the sibling `mybrowser/` workspace.
327
- await assertMarionettePortAvailable(effectivePort, { binaryName: projectConfig.binaryName });
399
+ await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options.killStaleMarionette === true);
328
400
  if (options.doctor) {
329
401
  const doctorOutcome = await runDoctorPreflight({
330
402
  engineDir: paths.engine,
@@ -337,7 +409,6 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
337
409
  if (doctorOutcome === 'stop')
338
410
  return;
339
411
  }
340
- const normalizedPaths = testPaths.map((p) => stripEnginePrefix(p).trim());
341
412
  await assertTestPathsExist(paths.engine, normalizedPaths);
342
413
  const classification = await classifyTestHarnesses(paths.engine, normalizedPaths);
343
414
  if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
@@ -351,6 +422,12 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
351
422
  if (options.headless) {
352
423
  extraArgs.push('--headless');
353
424
  }
425
+ if (options.auto === true) {
426
+ extraArgs.push('--auto');
427
+ }
428
+ if (canaryPath !== undefined) {
429
+ extraArgs.push(`--timeout=${String(canaryTimeoutSeconds(projectConfig))}`);
430
+ }
354
431
  // --mach-arg is a verbatim passthrough for upstream mach/xpcshell/mochitest
355
432
  // flags FireForge does not model directly (see the xpcshell appdir hint
356
433
  // above for the motivating case). Appended AFTER --headless so mach sees
@@ -364,13 +441,19 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
364
441
  // resolver matches paths by string prefix, so a bare directory arg
365
442
  // silently swept in prefix-named siblings (152.0b7 → 153.0b8 drill:
366
443
  // `…/test/hominis` also ran `…/test/hominis-tiles`, 1224 tests instead
367
- // of ~200, with no indication the scope widened). The dispatch form
368
- // gains a trailing `/` (making the prefix match exact) and any
369
- // excluded prefix-siblings are echoed so the narrowed scope is
370
- // visible. Classification above intentionally used the un-slashed
371
- // forms; only the mach dispatch needs the exact-match shape.
444
+ // of ~200, with no indication the scope widened). Each directory
445
+ // argument dispatches as its enumerated explicit test-file list a
446
+ // file list cannot prefix-match a sibling (0.35.0's trailing-`/`
447
+ // normalization turned out to be cosmetic on Firefox 153's mach; field
448
+ // verification showed the sibling still ran while the echo claimed it
449
+ // was excluded). Any prefix-siblings are echoed so the narrowed scope
450
+ // is visible. Classification above intentionally used the raw
451
+ // argument forms; only the mach dispatch needs the exact-match shape.
372
452
  const scopes = await analyzeTestPathScopes(paths.engine, normalizedPaths);
373
- const dispatchPaths = scopes.map((scope) => scope.dispatchPath);
453
+ const dispatchGroups = scopes.map((scope) => ({
454
+ label: scope.requestedPath,
455
+ paths: scope.dispatchPaths,
456
+ }));
374
457
  for (const scope of scopes) {
375
458
  const notice = formatScopeNotice(scope);
376
459
  if (notice)
@@ -380,7 +463,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
380
463
  // `runTestsWithRetries` (src/commands/test-run.ts) so sharded runs probe
381
464
  // the manifest for each file individually. See src/core/xpcshell-appdir.ts
382
465
  // for the full motivation.
383
- logTestSelection(dispatchPaths);
466
+ logTestSelection(scopes);
384
467
  const perfSampleEnv = buildPerfSampleEnv(projectRoot, projectConfig.binaryName, options.perfSamples);
385
468
  const runCtx = {
386
469
  engineDir: paths.engine,
@@ -394,29 +477,46 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
394
477
  const postRebuildContext = options.build
395
478
  ? createPostRebuildFailureContext('fireforge test --build', normalizedPaths)
396
479
  : undefined;
397
- // Multi-file requests shard into sequential single-file harness runs by
398
- // default (field report C3): one shared mochitest profile across files
399
- // bleeds pref/media-query state into later files. --no-shard restores
400
- // the combined invocation. The default must not be SILENT (drill
401
- // finding: a two-file cross-file pollution repro "passed" because the
402
- // headed comparison run was sharded without saying so, briefly
403
- // misattributing a suite-context bug to an upstream headless
404
- // regression), so a one-line notice states what sharding does and
405
- // does not exercise.
406
- if (dispatchPaths.length > 1 && options.shard !== false) {
407
- info(`Sharding: running ${dispatchPaths.length} test paths in isolated browser instances ` +
408
- '(one mach invocation per path). Cross-file state is NOT exercised pass --no-shard ' +
409
- 'for a combined single-instance run.');
410
- await runShardedTests(runCtx, dispatchPaths, (outcome, path) => diagnoseShardOutcome(outcome, path, projectConfig.binaryName, postRebuildContext));
480
+ // Multi-argument requests shard into sequential harness runs by default
481
+ // (field report C3): one shared mochitest profile across files bleeds
482
+ // pref/media-query state into later files. Sharding is per path
483
+ // ARGUMENT a directory argument keeps its enumerated files together
484
+ // in one invocation, preserving the one-browser-instance semantics of
485
+ // a directory run. --no-shard restores the combined invocation. The
486
+ // default must not be SILENT (drill finding: a two-file cross-file
487
+ // pollution repro "passed" because the headed comparison run was
488
+ // sharded without saying so, briefly misattributing a suite-context
489
+ // bug to an upstream headless regression), so a one-line notice states
490
+ // what sharding does and does not exercise.
491
+ if (canaryPath === undefined && dispatchGroups.length > 1 && options.shard !== false) {
492
+ info(`Sharding: running ${dispatchGroups.length} test path arguments in isolated browser instances ` +
493
+ '(one mach invocation per argument; a directory argument keeps its files in one instance). ' +
494
+ 'Cross-argument state is NOT exercised — pass --no-shard for a combined single-instance run.');
495
+ const generationBefore = await snapshotEngineGeneration(paths.engine);
496
+ try {
497
+ await runShardedTests(runCtx, dispatchGroups, (outcome, label) => diagnoseShardOutcome(outcome, label, projectConfig.binaryName, postRebuildContext));
498
+ }
499
+ finally {
500
+ await assertEngineGenerationUnchanged(paths.engine, generationBefore);
501
+ }
411
502
  return;
412
503
  }
504
+ const combinedDispatchPaths = dispatchGroups.flatMap((group) => group.paths);
413
505
  let outcome;
506
+ const generationBefore = await snapshotEngineGeneration(paths.engine);
414
507
  try {
415
- outcome = await runTestsWithRetries(runCtx, dispatchPaths);
508
+ outcome = await runTestsWithRetries(runCtx, combinedDispatchPaths);
416
509
  }
417
510
  catch (error) {
418
511
  throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
419
512
  }
513
+ finally {
514
+ await assertEngineGenerationUnchanged(paths.engine, generationBefore);
515
+ }
516
+ if (canaryPath !== undefined) {
517
+ reportCanaryOutcome(outcome);
518
+ return;
519
+ }
420
520
  finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext);
421
521
  }
422
522
  /**
@@ -432,52 +532,3 @@ function buildPerfSampleEnv(projectRoot, binaryName, perfSamples) {
432
532
  info(`Perf sample contract: ${envName}=${artifactPath}`);
433
533
  return { [envName]: artifactPath };
434
534
  }
435
- /** Registers the test command on the CLI program. */
436
- export function registerTest(program, { getProjectRoot, withErrorHandling }) {
437
- program
438
- .command('test [paths...]')
439
- .description('Run tests via mach test')
440
- .option('--headless', 'Run tests in headless mode')
441
- .option('--build', 'Run incremental UI build before testing')
442
- .option('--doctor', 'Run a marionette handshake preflight before tests (exit 1 on FAIL). With no paths, runs the preflight only.')
443
- .option('--mach-arg <arg>', 'Forward this argument verbatim to `mach test` (repeatable). Escape valve for upstream xpcshell/mochitest flags FireForge does not model.', (value, acc) => {
444
- acc.push(value);
445
- return acc;
446
- }, [])
447
- .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: ${String(DEFAULT_HARNESS_RETRIES)}.`, (raw) => {
448
- const n = Number.parseInt(raw, 10);
449
- if (!Number.isFinite(n) || n < 0 || n > 10) {
450
- throw new GeneralError(`--harness-retries must be an integer in 0..10 (got "${raw}")`);
451
- }
452
- return n;
453
- })
454
- .option('--generic-mach-test', 'Force dispatch through generic `mach test` instead of the suite-specific `mach xpcshell-test` / `mach mochitest` a single-suite run auto-selects (the suite-specific commands skip the mozlog resource monitor that crashes `mach test` on some hosts).')
455
- .option('--no-shard', 'Run multiple test paths in one combined mach invocation instead of sequential per-file shards (isolated instances do not exercise cross-file state, so use this to reproduce cross-file pollution bugs)')
456
- .option('--perf-samples <path>', 'Publish a perf-sample artifact path to the harness via <BINARYNAME>_PERF_SAMPLE_JSON (resolved against the project root)')
457
- .option('--marionette-port <port>', 'Override the Marionette control port (default 2828) for the stale-browser probe, the --doctor preflight, and (unless --mach-arg includes --flavor=xpcshell) auto-forwarded mach args: --setpref=marionette.port=<n> (browser listener) and --marionette=127.0.0.1:<n> (mochitest client). Omits the client flag when --mach-arg already sets --marionette. Use when 2828 is busy or CI assigns another port.', (raw) => {
458
- const n = Number.parseInt(raw, 10);
459
- if (!Number.isFinite(n) || n < 1 || n > 65535) {
460
- throw new GeneralError(`--marionette-port must be an integer in 1..65535 (got "${raw}")`);
461
- }
462
- return n;
463
- })
464
- .addHelpText('after', [
465
- '',
466
- '[paths...] semantics: a directory argument selects EXACTLY that',
467
- 'directory. FireForge normalizes it with a trailing "/" before',
468
- 'handing it to mach, because mach resolves test paths by string',
469
- 'prefix and a bare directory name would silently sweep in sibling',
470
- 'directories sharing the prefix (e.g. foo also running foo-extras).',
471
- 'When such siblings exist, the excluded directories are listed with',
472
- 'their test-file counts; pass them as separate paths to include them.',
473
- '',
474
- 'Multiple paths shard into sequential isolated harness runs (one',
475
- 'browser instance per path) by default, which does not exercise',
476
- 'cross-file state; --no-shard restores the combined single-instance',
477
- 'invocation.',
478
- ].join('\n'))
479
- .action(withErrorHandling(async (paths, options) => {
480
- await testCommand(getProjectRoot(), paths, pickDefined(options));
481
- }));
482
- }
483
- //# sourceMappingURL=test.js.map
@@ -174,4 +174,3 @@ async function collectFurnaceCustomCssFiles(projectRoot, engineDir, tokensCssPat
174
174
  }
175
175
  return results;
176
176
  }
177
- //# sourceMappingURL=token-coverage.js.map
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { Option } from 'commander';
2
+ import { Command, Option } from 'commander';
3
3
  import { loadConfig } from '../core/config.js';
4
4
  import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
5
5
  import { addToken, getTokensCssPath, validateTokenAdd, } from '../core/token-manager.js';
@@ -183,4 +183,3 @@ export function registerToken(program, { getProjectRoot, withErrorHandling }) {
183
183
  await tokenCoverageCommand(getProjectRoot());
184
184
  }));
185
185
  }
186
- //# sourceMappingURL=token.js.map
@@ -15,6 +15,7 @@
15
15
  * Exits non-zero on any error-severity diagnostic. Warnings print
16
16
  * but do not fail. Designed for CI use.
17
17
  */
18
+ import { Command } from 'commander';
18
19
  import { getProjectPaths, loadConfig } from '../core/config.js';
19
20
  import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
20
21
  import { findJsconfigPathsDrift, syncFurnaceJsconfigPaths } from '../core/furnace-jsconfig.js';
@@ -147,4 +148,3 @@ export function registerTypecheck(program, { getProjectRoot, withErrorHandling }
147
148
  await typecheckCommand(getProjectRoot(), opts);
148
149
  }));
149
150
  }
150
- //# sourceMappingURL=typecheck.js.map
@@ -15,6 +15,7 @@
15
15
  * treat the output as pass/fail.
16
16
  */
17
17
  import { join } from 'node:path';
18
+ import { Command } from 'commander';
18
19
  import { getProjectPaths, loadConfig } from '../core/config.js';
19
20
  import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
20
21
  import { isGitRepository } from '../core/git.js';
@@ -23,6 +24,7 @@ import { buildPatchQueueContext, lintPatchQueue } from '../core/patch-lint.js';
23
24
  import { loadPatchesManifest, validatePatchesManifestConsistency } from '../core/patch-manifest.js';
24
25
  import { evaluatePatchPolicy } from '../core/patch-policy.js';
25
26
  import { collectPatchRegistrationReferences } from '../core/patch-registration-refs.js';
27
+ import { buildPatchClaims } from '../core/status-classify.js';
26
28
  import { classifyFiles } from '../core/status-classify.js';
27
29
  import { GeneralError } from '../errors/base.js';
28
30
  import { pathExists, readText } from '../utils/fs.js';
@@ -88,14 +90,7 @@ async function detectDanglingRegistrations(patchesDir, engineDir, patches) {
88
90
  return issues;
89
91
  }
90
92
  function detectCrossPatchFileClaims(manifestPatches) {
91
- const claims = new Map();
92
- for (const patch of manifestPatches) {
93
- for (const file of patch.filesAffected) {
94
- const existing = claims.get(file) ?? [];
95
- existing.push(patch.filename);
96
- claims.set(file, existing);
97
- }
98
- }
93
+ const claims = buildPatchClaims(manifestPatches);
99
94
  const results = [];
100
95
  for (const [path, filenames] of claims) {
101
96
  if (filenames.length > 1) {
@@ -284,4 +279,3 @@ export function registerVerify(program, { getProjectRoot, withErrorHandling }) {
284
279
  await verifyCommand(getProjectRoot());
285
280
  }));
286
281
  }
287
- //# sourceMappingURL=verify.js.map
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { delimiter, dirname } from 'node:path';
3
+ import { Command } from 'commander';
3
4
  import { getProjectPaths, loadConfig } from '../core/config.js';
4
5
  import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
5
6
  import { buildArtifactMismatchMessage, generateMozconfig, hasBuildArtifacts, hasRunnableBundle, watchWithOutput, } from '../core/mach.js';
@@ -195,6 +196,10 @@ export async function watchCommand(projectRoot) {
195
196
  catch (error) {
196
197
  throw new BuildError('Watch process failed to start', 'mach watch', error instanceof Error ? error : undefined);
197
198
  }
199
+ // On Ctrl+C the bin signal handler now waits for the mach child to shut
200
+ // down (waitForActiveChildShutdown) before exiting, so this continuation
201
+ // can run and whitelist the SIGINT exit code — though the bin may still
202
+ // win the race and terminate first; both outcomes are acceptable.
198
203
  if (result.exitCode !== 0 && result.exitCode !== 130) {
199
204
  const combinedOutput = `${result.stdout}\n${result.stderr}`;
200
205
  if (hasConfigureTimeWatchmanFailure(combinedOutput)) {
@@ -214,4 +219,3 @@ export function registerWatch(program, { getProjectRoot, withErrorHandling }) {
214
219
  await watchCommand(getProjectRoot());
215
220
  }));
216
221
  }
217
- //# sourceMappingURL=watch.js.map
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { join, relative } from 'node:path';
3
+ import { Command } from 'commander';
3
4
  import { DEFAULT_BROWSER_SUBSCRIPT_DIR, wireSubscript } from '../core/browser-wire.js';
4
5
  import { getProjectPaths, loadConfig } from '../core/config.js';
5
6
  import { furnaceConfigExists as checkFurnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
@@ -356,4 +357,3 @@ export function registerWire(program, { getProjectRoot, withErrorHandling }) {
356
357
  await wireCommand(getProjectRoot(), name, pickDefined(options));
357
358
  }));
358
359
  }
359
- //# sourceMappingURL=wire.js.map
@@ -79,4 +79,3 @@ export function detectIndent(content, position) {
79
79
  export function getNodeSource(content, node) {
80
80
  return content.slice(node.start, node.end);
81
81
  }
82
- //# sourceMappingURL=ast-utils.js.map
@@ -12,4 +12,3 @@ export function validateBrandOverride(configuredBrand, requestedBrand) {
12
12
  }
13
13
  throw new InvalidArgumentError(`Brand override "${requestedBrand}" is not supported yet. FireForge currently operates only on the configured brand "${configuredBrand}".`, 'brand');
14
14
  }
15
- //# sourceMappingURL=brand-validation.js.map
@@ -356,4 +356,3 @@ export function isBrandingManagedPath(file, binaryName) {
356
356
  normalized === `${brandingRoot}/` ||
357
357
  normalized.startsWith(`${brandingRoot}/`));
358
358
  }
359
- //# sourceMappingURL=branding.js.map
@@ -125,4 +125,3 @@ export async function wireSubscript(root, name, options = {}) {
125
125
  throw error;
126
126
  }
127
127
  }
128
- //# sourceMappingURL=browser-wire.js.map
@@ -234,4 +234,3 @@ export async function detectPlatformGate(engineDir, sourcePath) {
234
234
  }
235
235
  return { gatedOff: false };
236
236
  }
237
- //# sourceMappingURL=build-audit-platform.js.map
@@ -184,4 +184,3 @@ export async function collectSameBasenameCandidates(source, searchRoots) {
184
184
  }
185
185
  return out;
186
186
  }
187
- //# sourceMappingURL=build-audit-registration.js.map
@@ -215,4 +215,3 @@ export async function resolveBestArtifact(sourcePath, searchRoots) {
215
215
  }
216
216
  return best;
217
217
  }
218
- //# sourceMappingURL=build-audit-resolve.js.map
@@ -91,4 +91,3 @@ export async function resolveArtifactByKnownTransform(source, searchRoots) {
91
91
  }
92
92
  return undefined;
93
93
  }
94
- //# sourceMappingURL=build-audit-transforms.js.map
@@ -475,4 +475,3 @@ export async function auditBuildArtifacts(projectRoot, engineDir, baseline) {
475
475
  info(`Packaged: ${summary.updated} updated, ${summary.stale} stale, ${summary.missing} missing, ${summary.skipped} skipped`);
476
476
  return summary;
477
477
  }
478
- //# sourceMappingURL=build-audit.js.map
@@ -35,4 +35,28 @@ export interface BuildBaseline {
35
35
  * was recorded.
36
36
  */
37
37
  packageableFingerprints?: Record<string, string>;
38
+ /**
39
+ * What the packaged test runtime produced by the recorded build covers.
40
+ *
41
+ * - `'full'`: the build packaged the full test set — written by
42
+ * `fireforge build` / `build --ui` and by a path-less
43
+ * `fireforge test --build` (full-suite run).
44
+ * - `string[]`: engine-relative POSIX request paths of the file/directory-
45
+ * scoped `fireforge test --build` invocation that produced the runtime.
46
+ * A directory entry covers everything beneath it. Support fixtures for
47
+ * manifests outside this list may be missing from `obj-*`/`_tests/`, so
48
+ * an `--allow-stale-build` run over uncovered paths is refused rather
49
+ * than dispatched into a hang.
50
+ *
51
+ * Missing on baselines written before 0.37.0 — only `fireforge build`
52
+ * wrote baselines then, so "full" is the honest historical value and the
53
+ * stale-check treats an absent field as full coverage.
54
+ *
55
+ * The record is project-scoped, which is also per-obj-dir: multi-objdir
56
+ * checkouts are refused up-front (`AmbiguousBuildArtifactsError`), so at
57
+ * most one obj dir exists per project.
58
+ */
59
+ testPackagingCoverage?: TestPackagingCoverage;
38
60
  }
61
+ /** Coverage claim of the packaged test runtime. See {@link BuildBaseline.testPackagingCoverage}. */
62
+ export type TestPackagingCoverage = 'full' | string[];
@@ -7,4 +7,3 @@
7
7
  * into it would make the dependency graph cyclic.
8
8
  */
9
9
  export {};
10
- //# sourceMappingURL=build-baseline-types.js.map