@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
@@ -1,7 +1,9 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { Command } from 'commander';
1
3
  import { getProjectPaths, loadConfig } from '../core/config.js';
2
4
  import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
3
5
  import { getHead, getStatusWithCodes, isGitRepository, isMissingHeadError } from '../core/git.js';
4
- import { getUntrackedFilesInDir } from '../core/git-status.js';
6
+ import { getUntrackedFilesInDir, resolveMaxUntrackedFilesPerDir } from '../core/git-status.js';
5
7
  import { buildOwnershipTable, renderOwnershipTable } from '../core/ownership-table.js';
6
8
  import { buildPatchQueueContext, collectNewFileCreatorsByPath } from '../core/patch-lint.js';
7
9
  import { loadPatchesManifest } from '../core/patch-manifest.js';
@@ -9,7 +11,7 @@ import { classifyFiles, } from '../core/status-classify.js';
9
11
  import { CommandError, GeneralError } from '../errors/base.js';
10
12
  import { ExitCode } from '../errors/codes.js';
11
13
  import { FIREFORGE_TMP_PATH_PATTERN, pathExists } from '../utils/fs.js';
12
- import { info, intro, outro, warn } from '../utils/logger.js';
14
+ import { info, intro, isMachineOutputMode, outro, setMachineOutputMode, warn, } from '../utils/logger.js';
13
15
  import { renderDefaultStatus, renderUnmanagedOnly, } from './status-output.js';
14
16
  /**
15
17
  * Renders raw worktree status as machine-parseable porcelain-style output.
@@ -20,19 +22,15 @@ function renderRawStatus(files) {
20
22
  process.stdout.write(`${status.trim()}\t${file}\n`);
21
23
  }
22
24
  }
23
- const DEFAULT_MAX_UNTRACKED_FILES_PER_DIR = 5000;
24
- function resolveMaxUntrackedFilesPerDir() {
25
- const raw = process.env['FIREFORGE_MAX_UNTRACKED_FILES'];
26
- if (raw === undefined || raw.length === 0)
27
- return DEFAULT_MAX_UNTRACKED_FILES_PER_DIR;
28
- const parsed = Number.parseInt(raw, 10);
29
- if (!Number.isFinite(parsed) || parsed <= 0) {
30
- warn(`Ignoring FIREFORGE_MAX_UNTRACKED_FILES="${raw}" — expected a positive integer. Falling back to ${DEFAULT_MAX_UNTRACKED_FILES_PER_DIR}.`);
31
- return DEFAULT_MAX_UNTRACKED_FILES_PER_DIR;
32
- }
33
- return parsed;
25
+ // Resolved lazily at first use (shared with core/git-status): this module
26
+ // is imported by the command manifest for EVERY command, so a
27
+ // module-load-time parse printed the status-specific env warning during
28
+ // `fireforge build` etc.
29
+ let maxUntrackedFilesPerDir;
30
+ function getMaxUntrackedFilesPerDir() {
31
+ maxUntrackedFilesPerDir ??= resolveMaxUntrackedFilesPerDir();
32
+ return maxUntrackedFilesPerDir;
34
33
  }
35
- const MAX_UNTRACKED_FILES_PER_DIR = resolveMaxUntrackedFilesPerDir();
36
34
  function renderTruncationBanner(truncations) {
37
35
  if (truncations.length === 0)
38
36
  return;
@@ -48,15 +46,17 @@ async function expandDirectoryEntries(files, engineDir) {
48
46
  for (const entry of files) {
49
47
  if (entry.file.endsWith('/') && entry.status.includes('?')) {
50
48
  const individualFiles = await getUntrackedFilesInDir(engineDir, entry.file);
51
- if (individualFiles.length > MAX_UNTRACKED_FILES_PER_DIR) {
52
- warn(`Untracked directory ${entry.file} contains ${individualFiles.length} files only the first ${MAX_UNTRACKED_FILES_PER_DIR} will be classified. Consider adding a .gitignore entry.`);
49
+ const cap = getMaxUntrackedFilesPerDir();
50
+ if (individualFiles.length > cap) {
51
+ // Recorded once here, reported once by renderTruncationBanner —
52
+ // the previous per-directory warn duplicated the banner's content.
53
53
  truncations.push({
54
54
  dir: entry.file,
55
55
  total: individualFiles.length,
56
- shown: MAX_UNTRACKED_FILES_PER_DIR,
56
+ shown: cap,
57
57
  });
58
58
  }
59
- const limited = individualFiles.slice(0, MAX_UNTRACKED_FILES_PER_DIR);
59
+ const limited = individualFiles.slice(0, cap);
60
60
  for (const f of limited) {
61
61
  expanded.push({ status: '??', file: f });
62
62
  }
@@ -161,130 +161,145 @@ async function assertEngineHasBaselineCommit(engineDir, options) {
161
161
  * @param options - Status display options
162
162
  */
163
163
  export async function statusCommand(projectRoot, options = {}) {
164
- const modeCount = [options.raw, options.unmanaged, options.ownership, options.json].filter((v) => v === true).length;
165
- if (modeCount > 1) {
166
- throw new GeneralError('Cannot use --raw, --unmanaged, --ownership, and --json together. Pick at most one.');
167
- }
168
- if (!options.raw && !options.json) {
169
- intro('FireForge Status');
164
+ const previousMachineOutputMode = isMachineOutputMode();
165
+ const shouldUseMachineOutput = options.json === true || options.raw === true;
166
+ // Machine modes own stdout exclusively: every diagnostic (clack warnings,
167
+ // withErrorHandling's styled errors, spinner steps) routes to stderr so
168
+ // `status --json | jq .` and `--raw` pipes always parse.
169
+ if (shouldUseMachineOutput) {
170
+ setMachineOutputMode(true);
170
171
  }
171
- const paths = getProjectPaths(projectRoot);
172
- const config = await loadConfig(projectRoot);
173
- const emitJsonError = (code, message) => {
174
- process.stdout.write(JSON.stringify({ schemaVersion: 1, error: message, code }) + '\n');
175
- throw new CommandError(ExitCode.GENERAL_ERROR);
176
- };
177
- // Ownership mode is a flat file→patch table; sources are the manifest's
178
- // filesAffected, any worktree drift, and the cross-patch
179
- // duplicate-new-file-creation map produced by walking each patch
180
- // body. The latter is the alignment fix between `status --ownership`
181
- // and `fireforge verify` see buildOwnershipTable's header comment.
182
- // Runs before the default classify path so we can short-circuit
183
- // without computing patch-backed state.
184
- if (options.ownership) {
172
+ try {
173
+ const modeCount = [options.raw, options.unmanaged, options.ownership, options.json].filter((v) => v === true).length;
174
+ if (modeCount > 1) {
175
+ throw new GeneralError('Cannot use --raw, --unmanaged, --ownership, and --json together. Pick at most one.');
176
+ }
177
+ if (!options.raw && !options.json) {
178
+ intro('FireForge Status');
179
+ }
180
+ const paths = getProjectPaths(projectRoot);
181
+ const config = await loadConfig(projectRoot);
182
+ const emitJsonError = (code, message) => {
183
+ process.stdout.write(JSON.stringify({ schemaVersion: 1, error: message, code }) + '\n');
184
+ throw new CommandError(ExitCode.GENERAL_ERROR);
185
+ };
186
+ // Ownership mode is a flat file→patch table; sources are the manifest's
187
+ // filesAffected, any worktree drift, and the cross-patch
188
+ // duplicate-new-file-creation map produced by walking each patch
189
+ // body. The latter is the alignment fix between `status --ownership`
190
+ // and `fireforge verify` — see buildOwnershipTable's header comment.
191
+ // Runs before the default classify path so we can short-circuit
192
+ // without computing patch-backed state.
193
+ if (options.ownership) {
194
+ if (!(await pathExists(paths.engine))) {
195
+ throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
196
+ }
197
+ const manifest = await loadPatchesManifest(paths.patches);
198
+ const ownershipExpansion = (await isGitRepository(paths.engine))
199
+ ? await expandDirectoryEntries(await getStatusWithCodes(paths.engine), paths.engine)
200
+ : { entries: [], truncations: [] };
201
+ // Filter atomic-write temp files (Finding #18) so a mid-flight
202
+ // `.fireforge-tmp-<pid>-<uuid>` artefact never shows up in any
203
+ // status mode. The pattern is tight enough to let legitimately
204
+ // similar names through.
205
+ const rawFilesOwnership = filterFireForgeTempFiles(ownershipExpansion.entries);
206
+ renderTruncationBanner(ownershipExpansion.truncations);
207
+ // Only walk the patch bodies when the directory actually exists.
208
+ // Fresh projects with no patch queue yet pass through with an empty
209
+ // creators map, which degrades to the old filesAffected-only
210
+ // behavior for the empty case.
211
+ const newFileCreatorsByPath = (await pathExists(paths.patches))
212
+ ? collectNewFileCreatorsByPath(await buildPatchQueueContext(paths.patches))
213
+ : new Map();
214
+ const rows = buildOwnershipTable(manifest?.patches ?? [], rawFilesOwnership, newFileCreatorsByPath, new Map((await classifyFiles(rawFilesOwnership, paths.engine, paths.patches, config.binaryName, await collectFurnaceManagedPrefixes(projectRoot))).map((entry) => [entry.file, entry.classification])));
215
+ renderOwnershipTable(rows);
216
+ const conflictCount = rows.filter((r) => r.conflict).length;
217
+ const unmanagedCount = rows.filter((r) => r.unmanaged).length;
218
+ const managedCount = rows.filter((r) => !r.unmanaged).length;
219
+ const parts = [`${managedCount} managed`];
220
+ if (conflictCount > 0)
221
+ parts.push(`${conflictCount} conflict${conflictCount === 1 ? '' : 's'}`);
222
+ if (unmanagedCount > 0)
223
+ parts.push(`${unmanagedCount} unmanaged`);
224
+ outro(parts.join(', '));
225
+ if (conflictCount > 0) {
226
+ throw new GeneralError(`${conflictCount} path(s) are claimed by more than one patch. ` +
227
+ 'Run "fireforge verify" for full details, then use "re-export --files" or ' +
228
+ '"patch delete" to resolve.');
229
+ }
230
+ return;
231
+ }
232
+ // Check if engine exists
185
233
  if (!(await pathExists(paths.engine))) {
234
+ if (options.json) {
235
+ emitJsonError('engine-missing', 'Firefox source not found. Run "fireforge download" first.');
236
+ }
186
237
  throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
187
238
  }
188
- const manifest = await loadPatchesManifest(paths.patches);
189
- const ownershipExpansion = (await isGitRepository(paths.engine))
190
- ? await expandDirectoryEntries(await getStatusWithCodes(paths.engine), paths.engine)
191
- : { entries: [], truncations: [] };
192
- // Filter atomic-write temp files (Finding #18) so a mid-flight
193
- // `.fireforge-tmp-<pid>-<uuid>` artefact never shows up in any
194
- // status mode. The pattern is tight enough to let legitimately
195
- // similar names through.
196
- const rawFilesOwnership = filterFireForgeTempFiles(ownershipExpansion.entries);
197
- renderTruncationBanner(ownershipExpansion.truncations);
198
- // Only walk the patch bodies when the directory actually exists.
199
- // Fresh projects with no patch queue yet pass through with an empty
200
- // creators map, which degrades to the old filesAffected-only
201
- // behavior for the empty case.
202
- const newFileCreatorsByPath = (await pathExists(paths.patches))
203
- ? collectNewFileCreatorsByPath(await buildPatchQueueContext(paths.patches))
204
- : new Map();
205
- const rows = buildOwnershipTable(manifest?.patches ?? [], rawFilesOwnership, newFileCreatorsByPath, new Map((await classifyFiles(rawFilesOwnership, paths.engine, paths.patches, config.binaryName, await collectFurnaceManagedPrefixes(projectRoot))).map((entry) => [entry.file, entry.classification])));
206
- renderOwnershipTable(rows);
207
- const conflictCount = rows.filter((r) => r.conflict).length;
208
- const unmanagedCount = rows.filter((r) => r.unmanaged).length;
209
- const managedCount = rows.filter((r) => !r.unmanaged).length;
210
- const parts = [`${managedCount} managed`];
211
- if (conflictCount > 0)
212
- parts.push(`${conflictCount} conflict${conflictCount === 1 ? '' : 's'}`);
213
- if (unmanagedCount > 0)
214
- parts.push(`${unmanagedCount} unmanaged`);
215
- outro(parts.join(', '));
216
- if (conflictCount > 0) {
217
- throw new GeneralError(`${conflictCount} path(s) are claimed by more than one patch. ` +
218
- 'Run "fireforge verify" for full details, then use "re-export --files" or ' +
219
- '"patch delete" to resolve.');
239
+ // Check if it's a git repository
240
+ if (!(await isGitRepository(paths.engine))) {
241
+ if (options.json) {
242
+ emitJsonError('engine-not-git', 'Engine directory is not a git repository. Run "fireforge download" to initialize.');
243
+ }
244
+ throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
220
245
  }
221
- return;
222
- }
223
- // Check if engine exists
224
- if (!(await pathExists(paths.engine))) {
246
+ await assertEngineHasBaselineCommit(paths.engine, options);
247
+ const rawFiles = await getStatusWithCodes(paths.engine);
248
+ const { entries: expanded, truncations } = await expandDirectoryEntries(rawFiles, paths.engine);
249
+ // Strip atomic-write temp files (Finding #18) before every mode
250
+ // branch so raw / unmanaged / default / json all agree.
251
+ const files = filterFireForgeTempFiles(expanded);
252
+ renderTruncationBanner(truncations);
253
+ // `--json` callers expect machine-parseable output on every invocation,
254
+ // including the clean-tree case. Before this ordering fix a clean tree
255
+ // printed "No modified files" / "Working tree clean" via the human
256
+ // branch below and `--json` was silently ignored, so scripts that piped
257
+ // the output through a JSON parser broke precisely when there was
258
+ // nothing to report. Emit `[]` here and return before the human fallback.
225
259
  if (options.json) {
226
- emitJsonError('engine-missing', 'Firefox source not found. Run "fireforge download" first.');
260
+ await renderJsonStatus(files, paths, projectRoot, config.binaryName);
261
+ return;
227
262
  }
228
- throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
229
- }
230
- // Check if it's a git repository
231
- if (!(await isGitRepository(paths.engine))) {
232
- if (options.json) {
233
- emitJsonError('engine-not-git', 'Engine directory is not a git repository. Run "fireforge download" to initialize.');
263
+ // `--raw` consumers parse the native `git status --porcelain` output
264
+ // directly. On a clean tree the raw mode should produce nothing on
265
+ // stdout the human "Working tree clean" banner would contaminate the
266
+ // pipe. Short-circuit before the human clean-tree branch below.
267
+ if (options.raw && files.length === 0) {
268
+ return;
234
269
  }
235
- throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
236
- }
237
- await assertEngineHasBaselineCommit(paths.engine, options);
238
- const rawFiles = await getStatusWithCodes(paths.engine);
239
- const { entries: expanded, truncations } = await expandDirectoryEntries(rawFiles, paths.engine);
240
- // Strip atomic-write temp files (Finding #18) before every mode
241
- // branch so raw / unmanaged / default / json all agree.
242
- const files = filterFireForgeTempFiles(expanded);
243
- renderTruncationBanner(truncations);
244
- // `--json` callers expect machine-parseable output on every invocation,
245
- // including the clean-tree case. Before this ordering fix a clean tree
246
- // printed "No modified files" / "Working tree clean" via the human
247
- // branch below and `--json` was silently ignored, so scripts that piped
248
- // the output through a JSON parser broke precisely when there was
249
- // nothing to report. Emit `[]` here and return before the human fallback.
250
- if (options.json) {
251
- await renderJsonStatus(files, paths, projectRoot, config.binaryName);
252
- return;
253
- }
254
- // `--raw` consumers parse the native `git status --porcelain` output
255
- // directly. On a clean tree the raw mode should produce nothing on
256
- // stdout the human "Working tree clean" banner would contaminate the
257
- // pipe. Short-circuit before the human clean-tree branch below.
258
- if (options.raw && files.length === 0) {
259
- return;
260
- }
261
- if (files.length === 0) {
262
- info('No modified files');
263
- outro('Working tree clean');
264
- return;
265
- }
266
- // Raw mode: existing behavior
267
- if (options.raw) {
268
- renderRawStatus(files);
269
- return;
270
+ if (files.length === 0) {
271
+ info('No modified files');
272
+ outro('Working tree clean');
273
+ return;
274
+ }
275
+ // Raw mode: existing behavior
276
+ if (options.raw) {
277
+ renderRawStatus(files);
278
+ return;
279
+ }
280
+ // Patch-aware classification
281
+ const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
282
+ const classified = await classifyFiles(files, paths.engine, paths.patches, config.binaryName, furnacePrefixes);
283
+ const buckets = {
284
+ conflict: classified.filter((f) => f.classification === 'conflict'),
285
+ unmanaged: classified.filter((f) => f.classification === 'unmanaged'),
286
+ patchOwnedDrift: classified.filter((f) => f.classification === 'patch-owned-drift'),
287
+ patchBacked: classified.filter((f) => f.classification === 'patch-backed'),
288
+ branding: classified.filter((f) => f.classification === 'branding'),
289
+ furnace: classified.filter((f) => f.classification === 'furnace'),
290
+ };
291
+ // --unmanaged mode: only show unmanaged
292
+ if (options.unmanaged) {
293
+ await renderUnmanagedOnly(buckets.unmanaged, files.length, projectRoot, config.binaryName);
294
+ return;
295
+ }
296
+ await renderDefaultStatus(files.length, buckets, projectRoot, config.binaryName);
270
297
  }
271
- // Patch-aware classification
272
- const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
273
- const classified = await classifyFiles(files, paths.engine, paths.patches, config.binaryName, furnacePrefixes);
274
- const buckets = {
275
- conflict: classified.filter((f) => f.classification === 'conflict'),
276
- unmanaged: classified.filter((f) => f.classification === 'unmanaged'),
277
- patchOwnedDrift: classified.filter((f) => f.classification === 'patch-owned-drift'),
278
- patchBacked: classified.filter((f) => f.classification === 'patch-backed'),
279
- branding: classified.filter((f) => f.classification === 'branding'),
280
- furnace: classified.filter((f) => f.classification === 'furnace'),
281
- };
282
- // --unmanaged mode: only show unmanaged
283
- if (options.unmanaged) {
284
- await renderUnmanagedOnly(buckets.unmanaged, files.length, projectRoot, config.binaryName);
285
- return;
298
+ finally {
299
+ if (isMachineOutputMode() !== previousMachineOutputMode) {
300
+ setMachineOutputMode(previousMachineOutputMode);
301
+ }
286
302
  }
287
- await renderDefaultStatus(files.length, buckets, projectRoot, config.binaryName);
288
303
  }
289
304
  /** Registers the status command on the CLI program. */
290
305
  export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
@@ -299,4 +314,3 @@ export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
299
314
  await statusCommand(getProjectRoot(), options);
300
315
  }));
301
316
  }
302
- //# sourceMappingURL=status.js.map
@@ -27,4 +27,3 @@ export async function maybeInjectAppdirArg(engineDir, normalizedPaths, objDir, e
27
27
  return true;
28
28
  }
29
29
  }
30
- //# sourceMappingURL=test-appdir.js.map
@@ -7,7 +7,7 @@
7
7
  * `test-harness-crash.ts` for single and sharded invocations. Split out of
8
8
  * `test.ts` to keep both files within the per-file line budget.
9
9
  */
10
- import { buildHarnessCrashMessage, buildNoTestsRanMessage } from '../core/test-harness-crash.js';
10
+ import { buildGreenSummaryRejectedMessage, buildHarnessCrashMessage, buildNoTestsRanMessage, } from '../core/test-harness-crash.js';
11
11
  import { buildHarnessEarlyExitMessage, classifyHarnessEarlyExit, completePostRebuildFailureContext, prependPostRebuildFailureContext, } from '../core/test-harness-output.js';
12
12
  import { GeneralError } from '../errors/base.js';
13
13
  import { BuildError } from '../errors/build.js';
@@ -200,6 +200,20 @@ export function finalizeSingleRunOutcome(outcome, normalizedPaths, binaryName, p
200
200
  // zero TEST-START lines must fail, not pass.
201
201
  throw new GeneralError(buildNoTestsRanMessage(0, normalizedPaths));
202
202
  }
203
+ // A green-LOOKING summary rejected on crash/truncation evidence gets its
204
+ // own explanation instead of the generic exit-code message — the operator
205
+ // must see WHY the green counts were not trusted (0.35.0 green-wash
206
+ // field report: SIGSEGV at file 2 of 8, `Passed: 2 / Failed: 0`).
207
+ if (outcome.verdict.greenSummaryRejected) {
208
+ throw new GeneralError(buildGreenSummaryRejectedMessage(outcome.verdict.greenSummaryRejected, outcome.result.exitCode));
209
+ }
210
+ if (outcome.verdict.realFailureLine !== undefined) {
211
+ info(`First real test failure: ${outcome.verdict.realFailureLine}`);
212
+ if (outcome.verdict.secondaryHarnessSignature !== undefined) {
213
+ info(`Secondary harness noise also present: ${outcome.verdict.secondaryHarnessSignature.reason}; ` +
214
+ outcome.verdict.secondaryHarnessSignature.line);
215
+ }
216
+ }
203
217
  handleNonZeroTestExit(outcome.result, normalizedPaths, outcome.appdirInjectionAttempted, binaryName, postRebuildContext);
204
218
  }
205
219
  /**
@@ -211,6 +225,9 @@ export function diagnoseShardOutcome(outcome, path, binaryName, postRebuildConte
211
225
  if (outcome.verdict.kind === 'no-tests' && outcome.result.exitCode === 0) {
212
226
  return buildNoTestsRanMessage(0, [path]);
213
227
  }
228
+ if (outcome.verdict.greenSummaryRejected) {
229
+ return buildGreenSummaryRejectedMessage(outcome.verdict.greenSummaryRejected, outcome.result.exitCode);
230
+ }
214
231
  try {
215
232
  handleNonZeroTestExit(outcome.result, [path], outcome.appdirInjectionAttempted, binaryName, postRebuildContext);
216
233
  return undefined;
@@ -219,4 +236,3 @@ export function diagnoseShardOutcome(outcome, path, binaryName, postRebuildConte
219
236
  return error instanceof Error ? error.message : String(error);
220
237
  }
221
238
  }
222
- //# sourceMappingURL=test-diagnose.js.map
@@ -0,0 +1,15 @@
1
+ import { loadConfig } from '../core/config.js';
2
+ import type { TestOptions } from '../types/commands/index.js';
3
+ import type { TestRunOutcome } from './test-run.js';
4
+ type ProjectConfig = Awaited<ReturnType<typeof loadConfig>>;
5
+ /** Rejects pathless `fireforge test` unless the operator selected a pathless mode. */
6
+ export declare function assertPathlessTestMode(testPaths: readonly string[], options: TestOptions): void;
7
+ /** Rejects invalid combinations for mode-like `fireforge test` options. */
8
+ export declare function assertTestModeCombinations(testPaths: readonly string[], options: TestOptions, canaryPath: string | undefined): void;
9
+ /** Resolves the canary path from CLI or config. */
10
+ export declare function resolveCanaryPath(options: TestOptions, projectConfig: ProjectConfig): string | undefined;
11
+ /** Returns the configured canary no-output budget, defaulting to 60 seconds. */
12
+ export declare function canaryTimeoutSeconds(projectConfig: ProjectConfig): number;
13
+ /** Emits or throws the one-word canary verdict. */
14
+ export declare function reportCanaryOutcome(outcome: TestRunOutcome): void;
15
+ export {};
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { loadConfig } from '../core/config.js';
3
+ import { GeneralError } from '../errors/base.js';
4
+ import { success } from '../utils/logger.js';
5
+ /** Rejects pathless `fireforge test` unless the operator selected a pathless mode. */
6
+ export function assertPathlessTestMode(testPaths, options) {
7
+ if (testPaths.length === 0 &&
8
+ options.auto !== true &&
9
+ options.doctor !== true &&
10
+ options.canary === undefined) {
11
+ throw new GeneralError(buildPathlessTestMessage());
12
+ }
13
+ }
14
+ /** Rejects invalid combinations for mode-like `fireforge test` options. */
15
+ export function assertTestModeCombinations(testPaths, options, canaryPath) {
16
+ if (options.auto === true && testPaths.length > 0) {
17
+ throw new GeneralError('`fireforge test --auto` is valid only when no explicit paths are provided.');
18
+ }
19
+ if (options.canary !== undefined && testPaths.length > 0) {
20
+ throw new GeneralError('`fireforge test --canary` runs one canary path; do not pass positional test paths.');
21
+ }
22
+ if (options.canary !== undefined && canaryPath === undefined) {
23
+ throw new GeneralError('No test canary path is configured.\n\n' +
24
+ 'Pass `fireforge test --canary <path>` or set `test.canaryPath` in fireforge.json.');
25
+ }
26
+ }
27
+ /** Resolves the canary path from CLI or config. */
28
+ export function resolveCanaryPath(options, projectConfig) {
29
+ if (options.canary === undefined || options.canary === false)
30
+ return undefined;
31
+ if (typeof options.canary === 'string')
32
+ return options.canary;
33
+ return projectConfig.test?.canaryPath;
34
+ }
35
+ /** Returns the configured canary no-output budget, defaulting to 60 seconds. */
36
+ export function canaryTimeoutSeconds(projectConfig) {
37
+ return projectConfig.test?.canaryTimeoutSeconds ?? 60;
38
+ }
39
+ /** Emits or throws the one-word canary verdict. */
40
+ export function reportCanaryOutcome(outcome) {
41
+ if (outcome.verdict.kind === 'tests-ran-ok') {
42
+ success('Canary: green');
43
+ return;
44
+ }
45
+ if (outcome.verdict.kind === 'harness-crash' &&
46
+ outcome.verdict.signature?.reason.includes('no-output timeout')) {
47
+ throw new GeneralError(`Canary: hang\n\n${outcome.verdict.signature.line}`);
48
+ }
49
+ if (outcome.verdict.kind === 'harness-crash') {
50
+ throw new GeneralError(`Canary: crash\n\n${outcome.verdict.signature ? outcome.verdict.signature.line : 'Harness crashed before the canary completed.'}`);
51
+ }
52
+ throw new GeneralError('Canary: crash\n\nThe canary did not complete green; see mach output above.');
53
+ }
54
+ function buildPathlessTestMessage() {
55
+ return ('`fireforge test` now requires an explicit test path unless you choose a pathless mode.\n\n' +
56
+ 'Use one of:\n' +
57
+ ' - `fireforge test <engine-relative test path>` for focused runs\n' +
58
+ " - `fireforge test --auto` to forward mach's own auto-selection mode\n" +
59
+ ' - `fireforge test --doctor` to run the Marionette preflight only\n' +
60
+ ' - `fireforge test --canary` to run the configured short harness canary');
61
+ }
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandContext } from '../types/cli.js';
3
+ /** Registers the test command on the CLI program. */
4
+ export declare function registerTest(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { Command } from 'commander';
3
+ import { withEngineSessionLock } from '../core/engine-session-lock.js';
4
+ import { GeneralError } from '../errors/base.js';
5
+ import { commanderArgParser, pickDefined } from '../utils/options.js';
6
+ import { testCommand } from './test.js';
7
+ import { DEFAULT_HARNESS_RETRIES } from './test-run.js';
8
+ /** Registers the test command on the CLI program. */
9
+ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
10
+ program
11
+ .command('test [paths...]')
12
+ .description('Run tests via mach test')
13
+ .option('--headless', 'Run tests in headless mode')
14
+ .option('--build', 'Run incremental UI build before testing')
15
+ .option('--auto', 'Forward mach test --auto. Valid only when no explicit paths are provided.')
16
+ .option('--allow-stale-build', 'Allow tests to run even when packageable engine files changed since the last successful FireForge build')
17
+ .option('--kill-stale-marionette', 'Terminate a recognized stale browser process holding the Marionette port before running tests')
18
+ .option('--canary [path]', 'Run one short browser-chrome harness canary. Uses test.canaryPath from fireforge.json when no path is supplied.')
19
+ .option('--doctor', 'Run a marionette handshake preflight before tests (exit 1 on FAIL). With no paths, runs the preflight only.')
20
+ .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) => {
21
+ acc.push(value);
22
+ return acc;
23
+ }, [])
24
+ .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)}.`, commanderArgParser((raw) => {
25
+ const n = Number.parseInt(raw, 10);
26
+ if (!Number.isFinite(n) || n < 0 || n > 10) {
27
+ throw new GeneralError(`--harness-retries must be an integer in 0..10 (got "${raw}")`);
28
+ }
29
+ return n;
30
+ }))
31
+ .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).')
32
+ .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)')
33
+ .option('--perf-samples <path>', 'Publish a perf-sample artifact path to the harness via <BINARYNAME>_PERF_SAMPLE_JSON (resolved against the project root)')
34
+ .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.', commanderArgParser((raw) => {
35
+ const n = Number.parseInt(raw, 10);
36
+ if (!Number.isFinite(n) || n < 1 || n > 65535) {
37
+ throw new GeneralError(`--marionette-port must be an integer in 1..65535 (got "${raw}")`);
38
+ }
39
+ return n;
40
+ }))
41
+ .addHelpText('after', [
42
+ '',
43
+ '[paths...] semantics: a directory argument selects EXACTLY that',
44
+ 'directory. FireForge enumerates the test files of exactly that',
45
+ 'directory and passes the explicit file list to mach, because mach',
46
+ 'resolves test paths by string prefix and a bare directory name',
47
+ 'silently sweeps in sibling directories sharing the prefix (e.g.',
48
+ 'foo also running foo-extras) — an explicit file list cannot',
49
+ 'prefix-match anything. The directory still runs as ONE mach',
50
+ 'invocation (one browser instance), so cross-file state carries',
51
+ 'within it. When prefix-named siblings exist, the excluded',
52
+ 'directories are listed with their test-file counts; pass them as',
53
+ 'separate paths to include them.',
54
+ '',
55
+ 'Multiple path arguments shard into sequential isolated harness',
56
+ 'runs (one browser instance per argument) by default, which does',
57
+ 'not exercise cross-argument state; --no-shard restores the',
58
+ 'combined single-instance invocation.',
59
+ ].join('\n'))
60
+ .action(withErrorHandling(async (paths, options) => {
61
+ const projectRoot = getProjectRoot();
62
+ await withEngineSessionLock(projectRoot, 'test', () => testCommand(projectRoot, paths, pickDefined(options)));
63
+ }));
64
+ }
@@ -53,16 +53,32 @@ export interface TestRunOutcome {
53
53
  * stale-xpcshell-symlink repair path the single-run flow already used.
54
54
  */
55
55
  export declare function runTestsWithRetries(ctx: TestRunContext, paths: string[]): Promise<TestRunOutcome>;
56
+ /**
57
+ * One sequential harness invocation of a sharded run: a requested path
58
+ * argument and the mach paths dispatched for it. A file argument is a
59
+ * group of one; a directory argument groups its enumerated test files so
60
+ * the whole directory still runs in ONE browser instance (cross-file
61
+ * state carries within a directory run exactly like the pre-enumeration
62
+ * behavior).
63
+ */
64
+ export interface ShardGroup {
65
+ /** The path argument as the operator passed it (display label). */
66
+ label: string;
67
+ /** The mach dispatch paths for this argument. */
68
+ paths: string[];
69
+ }
56
70
  /** Per-shard summary entry. */
57
71
  export interface ShardOutcome {
58
- path: string;
72
+ label: string;
59
73
  outcome: TestRunOutcome;
60
74
  }
61
75
  /**
62
- * Runs each requested path as its own sequential harness invocation and
63
- * prints an aggregate report. Per-shard failures are diagnosed via
64
- * `diagnoseShardFailure` (which receives the throwing diagnosis chain from
65
- * the command layer) but downgraded to warnings so every shard runs; a
66
- * single aggregate error is thrown at the end when any shard did not pass.
76
+ * Runs each requested path argument as its own sequential harness
77
+ * invocation (a directory argument's enumerated files stay together in
78
+ * one invocation see {@link ShardGroup}) and prints an aggregate
79
+ * report. Per-shard failures are diagnosed via `diagnoseShardFailure`
80
+ * (which receives the throwing diagnosis chain from the command layer)
81
+ * but downgraded to warnings so every shard runs; a single aggregate
82
+ * error is thrown at the end when any shard did not pass.
67
83
  */
68
- export declare function runShardedTests(ctx: TestRunContext, paths: string[], diagnoseShardFailure: (outcome: TestRunOutcome, path: string) => string | undefined): Promise<void>;
84
+ export declare function runShardedTests(ctx: TestRunContext, groups: ShardGroup[], diagnoseShardFailure: (outcome: TestRunOutcome, label: string) => string | undefined): Promise<void>;