@hominis/fireforge 0.35.0 → 0.36.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 (355) hide show
  1. package/CHANGELOG.md +29 -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 +4 -3
  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-working-tree.js +0 -1
  19. package/dist/src/commands/doctor.js +4 -1
  20. package/dist/src/commands/download.js +28 -13
  21. package/dist/src/commands/export-all.js +5 -3
  22. package/dist/src/commands/export-flow.js +12 -7
  23. package/dist/src/commands/export-placement-gate.js +0 -1
  24. package/dist/src/commands/export-placement-policy.js +0 -1
  25. package/dist/src/commands/export-shared.d.ts +5 -1
  26. package/dist/src/commands/export-shared.js +18 -6
  27. package/dist/src/commands/export.js +15 -10
  28. package/dist/src/commands/furnace/apply.js +22 -6
  29. package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
  30. package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
  31. package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
  32. package/dist/src/commands/furnace/chrome-doc.js +0 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +0 -1
  34. package/dist/src/commands/furnace/create-dry-run.js +0 -1
  35. package/dist/src/commands/furnace/create-features.js +0 -1
  36. package/dist/src/commands/furnace/create-mochikit.js +0 -1
  37. package/dist/src/commands/furnace/create-readback.js +0 -1
  38. package/dist/src/commands/furnace/create-templates.js +0 -1
  39. package/dist/src/commands/furnace/create-validation.js +0 -1
  40. package/dist/src/commands/furnace/create-xpcshell.js +0 -1
  41. package/dist/src/commands/furnace/create.js +0 -1
  42. package/dist/src/commands/furnace/deploy.js +7 -83
  43. package/dist/src/commands/furnace/diff.js +0 -1
  44. package/dist/src/commands/furnace/index.js +59 -25
  45. package/dist/src/commands/furnace/init.js +0 -1
  46. package/dist/src/commands/furnace/list.js +0 -1
  47. package/dist/src/commands/furnace/override.js +0 -1
  48. package/dist/src/commands/furnace/preview.js +3 -3
  49. package/dist/src/commands/furnace/refresh.js +0 -1
  50. package/dist/src/commands/furnace/remove-state.d.ts +15 -0
  51. package/dist/src/commands/furnace/remove-state.js +47 -1
  52. package/dist/src/commands/furnace/remove.js +3 -9
  53. package/dist/src/commands/furnace/rename-browser-test.js +1 -5
  54. package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
  55. package/dist/src/commands/furnace/rename-helpers.js +30 -1
  56. package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
  57. package/dist/src/commands/furnace/rename.js +15 -36
  58. package/dist/src/commands/furnace/scan.js +0 -1
  59. package/dist/src/commands/furnace/status.js +0 -1
  60. package/dist/src/commands/furnace/sync.js +76 -4
  61. package/dist/src/commands/furnace/validate.js +0 -1
  62. package/dist/src/commands/furnace/validation-output.js +0 -1
  63. package/dist/src/commands/import.js +35 -9
  64. package/dist/src/commands/lint-per-patch.js +19 -7
  65. package/dist/src/commands/lint.js +13 -3
  66. package/dist/src/commands/manifest.js +1 -2
  67. package/dist/src/commands/package.js +2 -1
  68. package/dist/src/commands/patch/compact.js +1 -1
  69. package/dist/src/commands/patch/delete.js +1 -1
  70. package/dist/src/commands/patch/index.js +1 -1
  71. package/dist/src/commands/patch/lint-ignore.js +21 -6
  72. package/dist/src/commands/patch/move-files.js +1 -1
  73. package/dist/src/commands/patch/patch-context.js +0 -1
  74. package/dist/src/commands/patch/rename.js +4 -4
  75. package/dist/src/commands/patch/reorder.js +3 -4
  76. package/dist/src/commands/patch/split-plan.js +2 -1
  77. package/dist/src/commands/patch/split.js +4 -4
  78. package/dist/src/commands/patch/staged-dependency.js +1 -1
  79. package/dist/src/commands/patch/tier.js +18 -5
  80. package/dist/src/commands/re-export-bulk-scan.js +0 -1
  81. package/dist/src/commands/re-export-files.js +1 -1
  82. package/dist/src/commands/re-export-options.js +0 -1
  83. package/dist/src/commands/re-export-scan.js +0 -1
  84. package/dist/src/commands/re-export.js +6 -4
  85. package/dist/src/commands/rebase/abort.js +2 -9
  86. package/dist/src/commands/rebase/confirm.js +0 -1
  87. package/dist/src/commands/rebase/conflict-summary.js +0 -1
  88. package/dist/src/commands/rebase/continue.js +0 -1
  89. package/dist/src/commands/rebase/index.d.ts +1 -1
  90. package/dist/src/commands/rebase/index.js +10 -15
  91. package/dist/src/commands/rebase/patch-loop.js +38 -7
  92. package/dist/src/commands/rebase/summary.js +2 -3
  93. package/dist/src/commands/rebase.js +0 -1
  94. package/dist/src/commands/register.js +1 -1
  95. package/dist/src/commands/reset.js +16 -9
  96. package/dist/src/commands/resolve.js +2 -2
  97. package/dist/src/commands/run.js +4 -4
  98. package/dist/src/commands/setup-support.js +0 -1
  99. package/dist/src/commands/setup.js +1 -2
  100. package/dist/src/commands/source.js +1 -2
  101. package/dist/src/commands/status-output.js +1 -12
  102. package/dist/src/commands/status.js +147 -133
  103. package/dist/src/commands/test-appdir.js +0 -1
  104. package/dist/src/commands/test-diagnose.js +18 -2
  105. package/dist/src/commands/test-modes.d.ts +15 -0
  106. package/dist/src/commands/test-modes.js +61 -0
  107. package/dist/src/commands/test-register.d.ts +4 -0
  108. package/dist/src/commands/test-register.js +64 -0
  109. package/dist/src/commands/test-run.d.ts +23 -7
  110. package/dist/src/commands/test-run.js +15 -14
  111. package/dist/src/commands/test.d.ts +0 -4
  112. package/dist/src/commands/test.js +85 -86
  113. package/dist/src/commands/token-coverage.js +0 -1
  114. package/dist/src/commands/token.js +1 -2
  115. package/dist/src/commands/typecheck.js +1 -1
  116. package/dist/src/commands/verify.js +3 -9
  117. package/dist/src/commands/watch.js +5 -1
  118. package/dist/src/commands/wire.js +1 -1
  119. package/dist/src/core/ast-utils.js +0 -1
  120. package/dist/src/core/brand-validation.js +0 -1
  121. package/dist/src/core/branding.js +0 -1
  122. package/dist/src/core/browser-wire.js +0 -1
  123. package/dist/src/core/build-audit-platform.js +0 -1
  124. package/dist/src/core/build-audit-registration.js +0 -1
  125. package/dist/src/core/build-audit-resolve.js +0 -1
  126. package/dist/src/core/build-audit-transforms.js +0 -1
  127. package/dist/src/core/build-audit.js +0 -1
  128. package/dist/src/core/build-baseline-types.js +0 -1
  129. package/dist/src/core/build-baseline.js +0 -1
  130. package/dist/src/core/build-prepare.js +2 -2
  131. package/dist/src/core/config-mutate.js +0 -1
  132. package/dist/src/core/config-paths.d.ts +2 -2
  133. package/dist/src/core/config-paths.js +6 -1
  134. package/dist/src/core/config-state.js +0 -1
  135. package/dist/src/core/config-validate-patch-policy.js +0 -1
  136. package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
  137. package/dist/src/core/config-validate-test-toolchains.js +105 -0
  138. package/dist/src/core/config-validate.js +3 -1
  139. package/dist/src/core/config.js +0 -1
  140. package/dist/src/core/destructive.js +0 -1
  141. package/dist/src/core/diff-hunks.js +13 -7
  142. package/dist/src/core/engine-changes.js +0 -1
  143. package/dist/src/core/engine-session-lock.d.ts +12 -0
  144. package/dist/src/core/engine-session-lock.js +45 -0
  145. package/dist/src/core/file-lock.d.ts +8 -0
  146. package/dist/src/core/file-lock.js +157 -48
  147. package/dist/src/core/firefox-archive.d.ts +4 -0
  148. package/dist/src/core/firefox-archive.js +6 -1
  149. package/dist/src/core/firefox-cache.js +60 -4
  150. package/dist/src/core/firefox-download.js +0 -1
  151. package/dist/src/core/firefox-extract.js +75 -16
  152. package/dist/src/core/firefox-ignorefile.js +0 -1
  153. package/dist/src/core/firefox.d.ts +12 -0
  154. package/dist/src/core/firefox.js +64 -2
  155. package/dist/src/core/furnace-apply-dry-run.js +0 -1
  156. package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
  157. package/dist/src/core/furnace-apply-ftl.js +10 -5
  158. package/dist/src/core/furnace-apply-helpers.js +0 -1
  159. package/dist/src/core/furnace-apply-output.js +2 -2
  160. package/dist/src/core/furnace-apply.d.ts +7 -1
  161. package/dist/src/core/furnace-apply.js +47 -13
  162. package/dist/src/core/furnace-checksum-utils.js +0 -1
  163. package/dist/src/core/furnace-config-array-utils.js +0 -1
  164. package/dist/src/core/furnace-config-custom.js +0 -1
  165. package/dist/src/core/furnace-config-order.js +0 -1
  166. package/dist/src/core/furnace-config-tokens.js +0 -1
  167. package/dist/src/core/furnace-config-validate.js +0 -1
  168. package/dist/src/core/furnace-config.d.ts +11 -0
  169. package/dist/src/core/furnace-config.js +37 -1
  170. package/dist/src/core/furnace-constants.js +0 -1
  171. package/dist/src/core/furnace-css-fragments.js +0 -1
  172. package/dist/src/core/furnace-graph-utils.js +0 -1
  173. package/dist/src/core/furnace-jsconfig.d.ts +2 -3
  174. package/dist/src/core/furnace-jsconfig.js +25 -18
  175. package/dist/src/core/furnace-marker.js +0 -1
  176. package/dist/src/core/furnace-operation.js +0 -1
  177. package/dist/src/core/furnace-refresh.js +0 -1
  178. package/dist/src/core/furnace-registration-ast.d.ts +14 -0
  179. package/dist/src/core/furnace-registration-ast.js +1 -2
  180. package/dist/src/core/furnace-registration-remove.js +0 -1
  181. package/dist/src/core/furnace-registration-validate.js +1 -5
  182. package/dist/src/core/furnace-registration.js +0 -1
  183. package/dist/src/core/furnace-rollback.js +0 -1
  184. package/dist/src/core/furnace-scanner.js +0 -1
  185. package/dist/src/core/furnace-staleness.js +0 -1
  186. package/dist/src/core/furnace-state-persist.d.ts +72 -0
  187. package/dist/src/core/furnace-state-persist.js +118 -0
  188. package/dist/src/core/furnace-step-errors.d.ts +23 -0
  189. package/dist/src/core/furnace-step-errors.js +25 -0
  190. package/dist/src/core/furnace-stories.js +0 -1
  191. package/dist/src/core/furnace-validate-accessibility.js +0 -1
  192. package/dist/src/core/furnace-validate-checks.js +0 -1
  193. package/dist/src/core/furnace-validate-compatibility.js +0 -1
  194. package/dist/src/core/furnace-validate-helpers.js +0 -1
  195. package/dist/src/core/furnace-validate-registration.js +7 -7
  196. package/dist/src/core/furnace-validate-structure.d.ts +8 -0
  197. package/dist/src/core/furnace-validate-structure.js +11 -2
  198. package/dist/src/core/furnace-validate.js +0 -1
  199. package/dist/src/core/furnace-version-drift.d.ts +6 -3
  200. package/dist/src/core/furnace-version-drift.js +6 -4
  201. package/dist/src/core/git-base.js +0 -1
  202. package/dist/src/core/git-diff.js +0 -1
  203. package/dist/src/core/git-file-ops.js +0 -1
  204. package/dist/src/core/git-status.d.ts +13 -0
  205. package/dist/src/core/git-status.js +32 -2
  206. package/dist/src/core/git.d.ts +10 -1
  207. package/dist/src/core/git.js +63 -6
  208. package/dist/src/core/license-headers.js +0 -1
  209. package/dist/src/core/lint-cache.js +0 -1
  210. package/dist/src/core/lint-projection.js +0 -1
  211. package/dist/src/core/mach-build-artifacts.js +0 -1
  212. package/dist/src/core/mach-error-hints.js +0 -1
  213. package/dist/src/core/mach-mozconfig.js +0 -1
  214. package/dist/src/core/mach-python.js +0 -1
  215. package/dist/src/core/mach-resource-shim.js +0 -1
  216. package/dist/src/core/mach.js +0 -1
  217. package/dist/src/core/manifest-helpers.js +0 -1
  218. package/dist/src/core/manifest-register.js +0 -1
  219. package/dist/src/core/manifest-rules.js +0 -1
  220. package/dist/src/core/manifest-tokenizers.js +0 -1
  221. package/dist/src/core/marionette-port.d.ts +8 -0
  222. package/dist/src/core/marionette-port.js +34 -3
  223. package/dist/src/core/marionette-preflight.js +0 -1
  224. package/dist/src/core/markdown-table.js +0 -1
  225. package/dist/src/core/ownership-table.js +2 -9
  226. package/dist/src/core/parser-fallback.js +0 -1
  227. package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
  228. package/dist/src/core/patch-apply-fuzz.js +71 -34
  229. package/dist/src/core/patch-apply.d.ts +30 -0
  230. package/dist/src/core/patch-apply.js +101 -18
  231. package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
  232. package/dist/src/core/patch-artifact-normalize.js +19 -15
  233. package/dist/src/core/patch-export-coverage.js +0 -1
  234. package/dist/src/core/patch-export-metadata.js +0 -1
  235. package/dist/src/core/patch-export-update.js +2 -3
  236. package/dist/src/core/patch-export.d.ts +2 -6
  237. package/dist/src/core/patch-export.js +24 -48
  238. package/dist/src/core/patch-files.js +0 -1
  239. package/dist/src/core/patch-identifier-suggest.js +0 -1
  240. package/dist/src/core/patch-lint-checkjs.js +0 -1
  241. package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
  242. package/dist/src/core/patch-lint-cross.js +0 -1
  243. package/dist/src/core/patch-lint-css.js +0 -1
  244. package/dist/src/core/patch-lint-diff-tag.js +0 -1
  245. package/dist/src/core/patch-lint-diff.d.ts +2 -1
  246. package/dist/src/core/patch-lint-diff.js +17 -32
  247. package/dist/src/core/patch-lint-imports.js +0 -1
  248. package/dist/src/core/patch-lint-jsdoc.js +0 -1
  249. package/dist/src/core/patch-lint-observer.js +0 -1
  250. package/dist/src/core/patch-lint-ownership.js +0 -1
  251. package/dist/src/core/patch-lint-reexports.js +0 -1
  252. package/dist/src/core/patch-lint.d.ts +37 -2
  253. package/dist/src/core/patch-lint.js +47 -34
  254. package/dist/src/core/patch-lock.js +0 -1
  255. package/dist/src/core/patch-manifest-consistency.js +9 -1
  256. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  257. package/dist/src/core/patch-manifest-io.js +30 -5
  258. package/dist/src/core/patch-manifest-query.js +18 -10
  259. package/dist/src/core/patch-manifest-resolve.js +0 -1
  260. package/dist/src/core/patch-manifest-validate.js +0 -1
  261. package/dist/src/core/patch-manifest.d.ts +1 -1
  262. package/dist/src/core/patch-manifest.js +1 -2
  263. package/dist/src/core/patch-parse.d.ts +59 -0
  264. package/dist/src/core/patch-parse.js +209 -92
  265. package/dist/src/core/patch-policy.js +0 -1
  266. package/dist/src/core/patch-registration-refs.js +16 -26
  267. package/dist/src/core/patch-source-metadata.js +0 -1
  268. package/dist/src/core/patch-transform.d.ts +4 -0
  269. package/dist/src/core/patch-transform.js +30 -38
  270. package/dist/src/core/rebase-session.js +0 -1
  271. package/dist/src/core/register-browser-content.js +0 -1
  272. package/dist/src/core/register-module.js +0 -1
  273. package/dist/src/core/register-result.js +0 -1
  274. package/dist/src/core/register-scaffold.js +0 -1
  275. package/dist/src/core/register-shared-css.js +0 -1
  276. package/dist/src/core/register-test-manifest.js +0 -1
  277. package/dist/src/core/register-xpcshell-test.js +0 -1
  278. package/dist/src/core/shared-ftl.js +0 -1
  279. package/dist/src/core/signal-critical.js +0 -1
  280. package/dist/src/core/smoke-patterns.js +0 -1
  281. package/dist/src/core/state-file.js +0 -1
  282. package/dist/src/core/status-classify.d.ts +16 -0
  283. package/dist/src/core/status-classify.js +24 -16
  284. package/dist/src/core/test-harness-crash.d.ts +64 -7
  285. package/dist/src/core/test-harness-crash.js +196 -15
  286. package/dist/src/core/test-harness-output.js +0 -1
  287. package/dist/src/core/test-path-scope.d.ts +21 -13
  288. package/dist/src/core/test-path-scope.js +43 -28
  289. package/dist/src/core/test-stale-check.js +0 -1
  290. package/dist/src/core/test-stale-symlink.js +0 -1
  291. package/dist/src/core/test-xpcshell-retry.js +0 -1
  292. package/dist/src/core/token-coverage.js +0 -1
  293. package/dist/src/core/token-dark-mode.js +0 -1
  294. package/dist/src/core/token-docs.js +0 -1
  295. package/dist/src/core/token-manager.js +0 -1
  296. package/dist/src/core/token-scaffold.js +0 -1
  297. package/dist/src/core/token-variant.js +0 -1
  298. package/dist/src/core/toolchain-preflight.d.ts +31 -10
  299. package/dist/src/core/toolchain-preflight.js +97 -36
  300. package/dist/src/core/typecheck-shim.js +13 -1
  301. package/dist/src/core/typecheck.js +0 -1
  302. package/dist/src/core/wire-destroy.js +0 -1
  303. package/dist/src/core/wire-dom-fragment.js +0 -1
  304. package/dist/src/core/wire-init.js +0 -1
  305. package/dist/src/core/wire-subscript.js +0 -1
  306. package/dist/src/core/wire-targets.js +0 -1
  307. package/dist/src/core/wire-utils.js +0 -1
  308. package/dist/src/core/xpcshell-appdir.js +1 -4
  309. package/dist/src/errors/base.d.ts +29 -0
  310. package/dist/src/errors/base.js +35 -1
  311. package/dist/src/errors/build.js +0 -1
  312. package/dist/src/errors/codes.d.ts +6 -0
  313. package/dist/src/errors/codes.js +6 -1
  314. package/dist/src/errors/config.js +0 -1
  315. package/dist/src/errors/download.js +0 -1
  316. package/dist/src/errors/furnace.d.ts +11 -1
  317. package/dist/src/errors/furnace.js +6 -5
  318. package/dist/src/errors/git.js +0 -1
  319. package/dist/src/errors/patch.d.ts +17 -0
  320. package/dist/src/errors/patch.js +29 -1
  321. package/dist/src/errors/rebase.js +0 -1
  322. package/dist/src/errors/run.js +1 -1
  323. package/dist/src/index.js +0 -1
  324. package/dist/src/types/cli.js +0 -1
  325. package/dist/src/types/commands/index.js +0 -1
  326. package/dist/src/types/commands/options.d.ts +13 -5
  327. package/dist/src/types/commands/options.js +0 -1
  328. package/dist/src/types/commands/patches.d.ts +8 -0
  329. package/dist/src/types/commands/patches.js +0 -1
  330. package/dist/src/types/commands/project.d.ts +11 -2
  331. package/dist/src/types/commands/project.js +0 -1
  332. package/dist/src/types/config.d.ts +32 -0
  333. package/dist/src/types/config.js +0 -1
  334. package/dist/src/types/furnace.d.ts +10 -0
  335. package/dist/src/types/furnace.js +0 -1
  336. package/dist/src/types/index.js +0 -1
  337. package/dist/src/types/typecheck.js +0 -1
  338. package/dist/src/utils/elapsed.js +0 -1
  339. package/dist/src/utils/errors.js +0 -1
  340. package/dist/src/utils/fs.js +37 -5
  341. package/dist/src/utils/logger.d.ts +7 -0
  342. package/dist/src/utils/logger.js +85 -3
  343. package/dist/src/utils/options.d.ts +15 -0
  344. package/dist/src/utils/options.js +25 -1
  345. package/dist/src/utils/package-root.js +11 -4
  346. package/dist/src/utils/parse.js +0 -1
  347. package/dist/src/utils/paths.js +0 -1
  348. package/dist/src/utils/platform.js +0 -1
  349. package/dist/src/utils/process.d.ts +33 -4
  350. package/dist/src/utils/process.js +183 -28
  351. package/dist/src/utils/regex.d.ts +13 -3
  352. package/dist/src/utils/regex.js +53 -4
  353. package/dist/src/utils/validation.d.ts +0 -12
  354. package/dist/src/utils/validation.js +13 -26
  355. package/package.json +8 -7
@@ -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;
@@ -3,21 +3,22 @@ import { join, resolve } from 'node:path';
3
3
  import { readBuildBaseline } 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 { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
12
13
  import { checkStaleBuildForTest, formatStaleBuildWarning } 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';
16
17
  import { pathExists } from '../utils/fs.js';
17
18
  import { info, intro, outro, spinner, success, warn } from '../utils/logger.js';
18
- import { pickDefined } from '../utils/options.js';
19
19
  import { stripEnginePrefix } from '../utils/paths.js';
20
20
  import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
21
+ import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
21
22
  import { DEFAULT_HARNESS_RETRIES, runShardedTests, runTestsWithRetries, } from './test-run.js';
22
23
  async function assertTestPathsExist(engineDir, testPaths) {
23
24
  const missingPaths = [];
@@ -135,9 +136,12 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
135
136
  : 'Pre-test build failed', 'mach build faster');
136
137
  });
137
138
  }
138
- function logTestSelection(normalizedPaths) {
139
- if (normalizedPaths.length > 0) {
140
- info(`Running tests: ${normalizedPaths.join(', ')}`);
139
+ function logTestSelection(scopes) {
140
+ if (scopes.length > 0) {
141
+ const labels = scopes.map((scope) => scope.isDirectory && scope.testFileCount > 0
142
+ ? `${scope.requestedPath} (${scope.testFileCount} test file${scope.testFileCount === 1 ? '' : 's'}, passed explicitly)`
143
+ : scope.requestedPath);
144
+ info(`Running tests: ${labels.join(', ')}`);
141
145
  }
142
146
  else {
143
147
  info('Running all tests...');
@@ -255,6 +259,13 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
255
259
  }
256
260
  }
257
261
  }
262
+ async function ensureTestMarionettePortAvailable(port, binaryName, killStaleBrowser) {
263
+ if (killStaleBrowser) {
264
+ await ensureMarionettePortAvailable(port, { binaryName, killStaleBrowser: true });
265
+ return;
266
+ }
267
+ await assertMarionettePortAvailable(port, { binaryName });
268
+ }
258
269
  /**
259
270
  * Runs the test command to execute mach tests.
260
271
  * @param projectRoot - Root directory of the project
@@ -268,11 +279,14 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
268
279
  if (!(await pathExists(paths.engine))) {
269
280
  throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
270
281
  }
282
+ assertPathlessTestMode(testPaths, options);
271
283
  const buildCheck = await assertTestBuildArtifacts(paths.engine);
272
284
  // Load the project config once so both the build and the port
273
285
  // probe have access to `binaryName` (the port probe uses it to
274
286
  // recognise a fork-branded browser holding the Marionette port).
275
287
  const projectConfig = await loadConfig(projectRoot);
288
+ const canaryPath = resolveCanaryPath(options, projectConfig);
289
+ assertTestModeCombinations(testPaths, options, canaryPath);
276
290
  // `hasBuildArtifacts` only confirms `obj-*/dist/` exists; a partial
277
291
  // build (linker failed, packaging step interrupted, etc.) can satisfy
278
292
  // that check without ever writing the launchable binary the marionette
@@ -291,15 +305,19 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
291
305
  else {
292
306
  // Stale-build preflight — when --build was NOT requested, detect
293
307
  // 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.
308
+ // and fail UP-FRONT unless the operator explicitly accepts the stale
309
+ // package risk.
300
310
  const stale = await checkStaleBuildForTest(projectRoot, paths.engine);
301
311
  if (stale.stale) {
302
- warn(formatStaleBuildWarning(stale));
312
+ const message = `${formatStaleBuildWarning(stale)}\n\n` +
313
+ 'Run `fireforge test --build` to refresh the packaged runtime first, or pass ' +
314
+ '`--allow-stale-build` if you intentionally rebuilt out-of-band and accept the risk.';
315
+ if (options.allowStaleBuild === true) {
316
+ warn(message);
317
+ }
318
+ else {
319
+ throw new GeneralError(message);
320
+ }
303
321
  }
304
322
  }
305
323
  // Resolve the effective Marionette port. Operator precedence:
@@ -324,7 +342,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
324
342
  // generic bind failure. 2026-04-21 eval (Finding #20): a stale
325
343
  // `-marionette` process from `fresh/` poisoned a later test run in
326
344
  // the sibling `mybrowser/` workspace.
327
- await assertMarionettePortAvailable(effectivePort, { binaryName: projectConfig.binaryName });
345
+ await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options.killStaleMarionette === true);
328
346
  if (options.doctor) {
329
347
  const doctorOutcome = await runDoctorPreflight({
330
348
  engineDir: paths.engine,
@@ -337,7 +355,8 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
337
355
  if (doctorOutcome === 'stop')
338
356
  return;
339
357
  }
340
- const normalizedPaths = testPaths.map((p) => stripEnginePrefix(p).trim());
358
+ const requestedPaths = canaryPath !== undefined ? [canaryPath] : testPaths;
359
+ const normalizedPaths = requestedPaths.map((p) => stripEnginePrefix(p).trim());
341
360
  await assertTestPathsExist(paths.engine, normalizedPaths);
342
361
  const classification = await classifyTestHarnesses(paths.engine, normalizedPaths);
343
362
  if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
@@ -351,6 +370,12 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
351
370
  if (options.headless) {
352
371
  extraArgs.push('--headless');
353
372
  }
373
+ if (options.auto === true) {
374
+ extraArgs.push('--auto');
375
+ }
376
+ if (canaryPath !== undefined) {
377
+ extraArgs.push(`--timeout=${String(canaryTimeoutSeconds(projectConfig))}`);
378
+ }
354
379
  // --mach-arg is a verbatim passthrough for upstream mach/xpcshell/mochitest
355
380
  // flags FireForge does not model directly (see the xpcshell appdir hint
356
381
  // above for the motivating case). Appended AFTER --headless so mach sees
@@ -364,13 +389,19 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
364
389
  // resolver matches paths by string prefix, so a bare directory arg
365
390
  // silently swept in prefix-named siblings (152.0b7 → 153.0b8 drill:
366
391
  // `…/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.
392
+ // of ~200, with no indication the scope widened). Each directory
393
+ // argument dispatches as its enumerated explicit test-file list a
394
+ // file list cannot prefix-match a sibling (0.35.0's trailing-`/`
395
+ // normalization turned out to be cosmetic on Firefox 153's mach; field
396
+ // verification showed the sibling still ran while the echo claimed it
397
+ // was excluded). Any prefix-siblings are echoed so the narrowed scope
398
+ // is visible. Classification above intentionally used the raw
399
+ // argument forms; only the mach dispatch needs the exact-match shape.
372
400
  const scopes = await analyzeTestPathScopes(paths.engine, normalizedPaths);
373
- const dispatchPaths = scopes.map((scope) => scope.dispatchPath);
401
+ const dispatchGroups = scopes.map((scope) => ({
402
+ label: scope.requestedPath,
403
+ paths: scope.dispatchPaths,
404
+ }));
374
405
  for (const scope of scopes) {
375
406
  const notice = formatScopeNotice(scope);
376
407
  if (notice)
@@ -380,7 +411,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
380
411
  // `runTestsWithRetries` (src/commands/test-run.ts) so sharded runs probe
381
412
  // the manifest for each file individually. See src/core/xpcshell-appdir.ts
382
413
  // for the full motivation.
383
- logTestSelection(dispatchPaths);
414
+ logTestSelection(scopes);
384
415
  const perfSampleEnv = buildPerfSampleEnv(projectRoot, projectConfig.binaryName, options.perfSamples);
385
416
  const runCtx = {
386
417
  engineDir: paths.engine,
@@ -394,29 +425,46 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
394
425
  const postRebuildContext = options.build
395
426
  ? createPostRebuildFailureContext('fireforge test --build', normalizedPaths)
396
427
  : 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));
428
+ // Multi-argument requests shard into sequential harness runs by default
429
+ // (field report C3): one shared mochitest profile across files bleeds
430
+ // pref/media-query state into later files. Sharding is per path
431
+ // ARGUMENT a directory argument keeps its enumerated files together
432
+ // in one invocation, preserving the one-browser-instance semantics of
433
+ // a directory run. --no-shard restores the combined invocation. The
434
+ // default must not be SILENT (drill finding: a two-file cross-file
435
+ // pollution repro "passed" because the headed comparison run was
436
+ // sharded without saying so, briefly misattributing a suite-context
437
+ // bug to an upstream headless regression), so a one-line notice states
438
+ // what sharding does and does not exercise.
439
+ if (canaryPath === undefined && dispatchGroups.length > 1 && options.shard !== false) {
440
+ info(`Sharding: running ${dispatchGroups.length} test path arguments in isolated browser instances ` +
441
+ '(one mach invocation per argument; a directory argument keeps its files in one instance). ' +
442
+ 'Cross-argument state is NOT exercised — pass --no-shard for a combined single-instance run.');
443
+ const generationBefore = await snapshotEngineGeneration(paths.engine);
444
+ try {
445
+ await runShardedTests(runCtx, dispatchGroups, (outcome, label) => diagnoseShardOutcome(outcome, label, projectConfig.binaryName, postRebuildContext));
446
+ }
447
+ finally {
448
+ await assertEngineGenerationUnchanged(paths.engine, generationBefore);
449
+ }
411
450
  return;
412
451
  }
452
+ const combinedDispatchPaths = dispatchGroups.flatMap((group) => group.paths);
413
453
  let outcome;
454
+ const generationBefore = await snapshotEngineGeneration(paths.engine);
414
455
  try {
415
- outcome = await runTestsWithRetries(runCtx, dispatchPaths);
456
+ outcome = await runTestsWithRetries(runCtx, combinedDispatchPaths);
416
457
  }
417
458
  catch (error) {
418
459
  throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
419
460
  }
461
+ finally {
462
+ await assertEngineGenerationUnchanged(paths.engine, generationBefore);
463
+ }
464
+ if (canaryPath !== undefined) {
465
+ reportCanaryOutcome(outcome);
466
+ return;
467
+ }
420
468
  finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext);
421
469
  }
422
470
  /**
@@ -432,52 +480,3 @@ function buildPerfSampleEnv(projectRoot, binaryName, perfSamples) {
432
480
  info(`Perf sample contract: ${envName}=${artifactPath}`);
433
481
  return { [envName]: artifactPath };
434
482
  }
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
@@ -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
@@ -140,4 +140,3 @@ async function collectPackageableFingerprints(engineDir) {
140
140
  return undefined;
141
141
  }
142
142
  }
143
- //# sourceMappingURL=build-baseline.js.map
@@ -13,6 +13,7 @@ import { collectChangedEnginePaths } from './engine-changes.js';
13
13
  import { applyAllComponents } from './furnace-apply.js';
14
14
  import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
15
15
  import { runFurnaceMutation } from './furnace-operation.js';
16
+ import { countEntriesWithBlockingStepErrors } from './furnace-step-errors.js';
16
17
  import { cleanStories } from './furnace-stories.js';
17
18
  import { generateMozconfig, runMachCapture } from './mach.js';
18
19
  import { explainMachError } from './mach-error-hints.js';
@@ -169,7 +170,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
169
170
  // These are distinct from `result.errors`, which captures
170
171
  // components that failed before reaching the applied list at all.
171
172
  // The sum of the two is the total count of failed components.
172
- const appliedWithStepErrorsCount = result.applied.filter((entry) => (entry.stepErrors?.length ?? 0) > 0).length;
173
+ const appliedWithStepErrorsCount = countEntriesWithBlockingStepErrors(result.applied);
173
174
  const totalApplyFailures = result.errors.length + appliedWithStepErrorsCount;
174
175
  if (totalApplyFailures > 0) {
175
176
  furnaceSpinner.error('Failed to apply Furnace components');
@@ -210,4 +211,3 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
210
211
  }
211
212
  return { furnaceApplied, reconfigured };
212
213
  }
213
- //# sourceMappingURL=build-prepare.js.map
@@ -70,4 +70,3 @@ export function mutateConfig(config, key, value, skipValidation = false) {
70
70
  }
71
71
  return raw;
72
72
  }
73
- //# sourceMappingURL=config-mutate.js.map
@@ -9,9 +9,9 @@ export declare const FIREFORGE_DIR = ".fireforge";
9
9
  /** Name of the state file */
10
10
  export declare const STATE_FILENAME = "state.json";
11
11
  /** Supported top-level fireforge.json keys backed by the current schema. */
12
- export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment"];
12
+ export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "test", "externalToolchains", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment"];
13
13
  /** Supported config paths that can be read or set without --force. */
14
- export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
14
+ export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
15
15
  /**
16
16
  * Gets all project paths based on a root directory.
17
17
  * @param root - Root directory of the project
@@ -25,6 +25,8 @@ export const SUPPORTED_CONFIG_ROOT_KEYS = [
25
25
  'binaryName',
26
26
  'firefox',
27
27
  'build',
28
+ 'test',
29
+ 'externalToolchains',
28
30
  'license',
29
31
  'wire',
30
32
  'patchLint',
@@ -45,6 +47,10 @@ export const SUPPORTED_CONFIG_PATHS = [
45
47
  'firefox.sha256',
46
48
  'build',
47
49
  'build.jobs',
50
+ 'test',
51
+ 'test.canaryPath',
52
+ 'test.canaryTimeoutSeconds',
53
+ 'externalToolchains',
48
54
  'wire',
49
55
  'wire.subscriptDir',
50
56
  'patchLint',
@@ -81,4 +87,3 @@ export function getProjectPaths(root) {
81
87
  componentsDir: join(root, 'components'),
82
88
  };
83
89
  }
84
- //# sourceMappingURL=config-paths.js.map
@@ -149,4 +149,3 @@ export async function updateState(root, updates) {
149
149
  await writeJson(paths.state, validateFireForgeState(nextState));
150
150
  });
151
151
  }
152
- //# sourceMappingURL=config-state.js.map
@@ -172,4 +172,3 @@ export function parsePatchPolicyBlock(rec) {
172
172
  }
173
173
  return out;
174
174
  }
175
- //# sourceMappingURL=config-validate-patch-policy.js.map
@@ -0,0 +1,8 @@
1
+ import type { FireForgeConfig } from '../types/config.js';
2
+ import { parseObject } from '../utils/parse.js';
3
+ type ConfigRecord = ReturnType<typeof parseObject>;
4
+ /** Parses the optional `test` block. */
5
+ export declare function parseTestBlock(rec: ConfigRecord, config: FireForgeConfig): void;
6
+ /** Parses optional project-specific external toolchain doctor probes. */
7
+ export declare function parseExternalToolchainsBlock(rec: ConfigRecord, config: FireForgeConfig): void;
8
+ export {};