@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
@@ -13,7 +13,29 @@ import { verbose } from '../utils/logger.js';
13
13
  import { isBrandingManagedPath } from './branding.js';
14
14
  import { computePatchedContent } from './patch-apply.js';
15
15
  import { loadPatchesManifest } from './patch-manifest.js';
16
- function getPrimaryStatusCode(status) {
16
+ /**
17
+ * Builds the file → owning-patch-filenames multimap from manifest rows.
18
+ * Single source of truth for cross-patch claim detection — status
19
+ * classification, verify's cross-claim check, and the ownership table all
20
+ * consume this builder (they used to each rebuild it, one drift away from
21
+ * disagreeing about what "claimed" means).
22
+ */
23
+ export function buildPatchClaims(manifestPatches) {
24
+ const ownersByPath = new Map();
25
+ for (const patch of manifestPatches) {
26
+ for (const file of patch.filesAffected) {
27
+ const existing = ownersByPath.get(file) ?? [];
28
+ existing.push(patch.filename);
29
+ ownersByPath.set(file, existing);
30
+ }
31
+ }
32
+ return ownersByPath;
33
+ }
34
+ /**
35
+ * Reduces a two-character porcelain XY status to its primary code.
36
+ * Single source of truth — status-output.ts renders from the same logic.
37
+ */
38
+ export function getPrimaryStatusCode(status) {
17
39
  if (status.includes('?'))
18
40
  return '?';
19
41
  if (status.includes('!'))
@@ -55,20 +77,7 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
55
77
  // branch where the expected-vs-actual content comparison then routed
56
78
  // them into `unmanaged` when the content didn't match either owner's
57
79
  // expectation.
58
- const patchClaims = new Map();
59
- if (manifest) {
60
- for (const patch of manifest.patches) {
61
- for (const f of patch.filesAffected) {
62
- const owners = patchClaims.get(f);
63
- if (owners) {
64
- owners.push(patch.filename);
65
- }
66
- else {
67
- patchClaims.set(f, [patch.filename]);
68
- }
69
- }
70
- }
71
- }
80
+ const patchClaims = manifest ? buildPatchClaims(manifest.patches) : new Map();
72
81
  const results = [];
73
82
  for (const entry of files) {
74
83
  const owners = patchClaims.get(entry.file);
@@ -145,4 +154,3 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
145
154
  }
146
155
  return results;
147
156
  }
148
- //# sourceMappingURL=status-classify.js.map
@@ -32,12 +32,34 @@ export interface HarnessCrashSignature {
32
32
  export interface HarnessRunVerdict {
33
33
  kind: HarnessRunClassification;
34
34
  signature?: HarnessCrashSignature;
35
+ /** First concrete test-failure evidence line, when available. */
36
+ realFailureLine?: string;
37
+ /** Harness noise seen in the same output as a real test failure. */
38
+ secondaryHarnessSignature?: HarnessCrashSignature;
35
39
  /**
36
40
  * Set when a non-zero mach exit code was overridden because the output
37
41
  * embeds a completed green summary — the exit code followed harness
38
42
  * noise, not a test result. Callers should surface a note.
39
43
  */
40
44
  greenSummaryOverride?: boolean;
45
+ /**
46
+ * Set when the output embeds a green-LOOKING summary that was REJECTED:
47
+ * crash or truncation evidence proves the suite never completed, so the
48
+ * "green" counts only cover the files that ran before the run died
49
+ * (field report: a SIGSEGV at the second of eight files left a
50
+ * `Passed: 2 / Failed: 0` summary that 0.35.0 green-washed into a pass).
51
+ * Callers must fail the run and surface the evidence.
52
+ */
53
+ greenSummaryRejected?: GreenSummaryRejection;
54
+ }
55
+ /** Why a green-looking embedded summary was not trusted. */
56
+ export interface GreenSummaryRejection {
57
+ /** Crash-marker evidence line (mozcrash / signal kill), if any. */
58
+ crashLine?: string;
59
+ /** Requested test files with no `TEST_START`/`TEST-START` line at all. */
60
+ neverStarted: string[];
61
+ /** Files whose `TEST_START` has no matching end marker. */
62
+ neverEnded: string[];
41
63
  }
42
64
  /**
43
65
  * Strips non-signal noise from captured output before crash-signature
@@ -47,17 +69,41 @@ export interface HarnessRunVerdict {
47
69
  * classification of test results still reads the full output.
48
70
  */
49
71
  export declare function stripNonSignalNoise(output: string): string;
72
+ /**
73
+ * Finds the first crash-marker line in the RAW output (see
74
+ * {@link CRASH_MARKER_PATTERNS}), or undefined when the run shows no
75
+ * crash evidence. Exported for direct unit testing.
76
+ */
77
+ export declare function findCrashMarkerLine(output: string): string | undefined;
50
78
  /**
51
79
  * True when the output embeds a COMPLETED, GREEN suite summary: an
52
80
  * execution signal, `Unexpected results: 0` (and no non-zero unexpected
53
- * count), a `SUITE_END` marker, and no real `TEST-UNEXPECTED-*` lines.
54
- * Such a run finished its suite; any startup-traceback-shaped noise in the
55
- * same output is by definition non-fatal, so this vetoes signature-based
56
- * crash classification (field report: fully green sharded runs reported
57
- * `CRASH (N attempts)` because degradation warnings matched the psutil
58
- * signals). Exported for direct unit testing.
81
+ * count), a `SUITE_END` marker, no real `TEST-UNEXPECTED-*` lines — and
82
+ * NO crash marker. Such a run finished its suite; any startup-traceback-
83
+ * shaped noise in the same output is by definition non-fatal, so this
84
+ * vetoes signature-based crash classification (field report: fully green
85
+ * sharded runs reported `CRASH (N attempts)` because degradation warnings
86
+ * matched the psutil signals). A summary printed after a crash marker is
87
+ * NOT "completed" — it only covers the files that ran before the crash
88
+ * (0.35.0 green-wash field report). Exported for direct unit testing.
59
89
  */
60
90
  export declare function hasCompletedGreenSummary(output: string): boolean;
91
+ /**
92
+ * Cross-checks run completeness against the requested test files: every
93
+ * requested file must produce a start marker, and every started test must
94
+ * produce an end marker (the harness emits both even for failing files).
95
+ *
96
+ * Ends are paired with starts POSITIONALLY (last open start is closed by
97
+ * the next end), not by filename — the xpcshell dialect's `TEST_END: Test
98
+ * PASS` lines do not name the file. Requested paths that are not
99
+ * test-file-shaped (directories, manifests) are skipped for the
100
+ * never-started check; the start/end pairing scan covers them regardless.
101
+ * Exported for direct unit testing.
102
+ */
103
+ export declare function analyzeTestCompleteness(output: string, requestedPaths: readonly string[]): {
104
+ neverStarted: string[];
105
+ neverEnded: string[];
106
+ };
61
107
  /**
62
108
  * Detects the known harness-crash shapes in captured mach output.
63
109
  * Returns undefined for anything that looks like a genuine test result.
@@ -76,9 +122,20 @@ export declare function detectHarnessCrashSignature(output: string): HarnessCras
76
122
  * dispatch prints no `TEST-START`). Bare `Passed:`/`Failed:` summary
77
123
  * lines are still not trusted as evidence of execution.
78
124
  * 3. Exit code zero with tests started is a pass; anything else is a
79
- * test failure for the regular diagnosis chain.
125
+ * test failure for the regular diagnosis chain — except a non-zero
126
+ * exit whose embedded summary completed green AND shows no crash
127
+ * marker and no truncated/never-started requested file, which is a
128
+ * pass with a `greenSummaryOverride` note (harness noise owns the
129
+ * exit code). A green-shaped summary WITH such evidence is rejected
130
+ * (`greenSummaryRejected`) and fails.
80
131
  */
81
132
  export declare function classifyHarnessRun(exitCode: number, output: string, requestedPaths: readonly string[]): HarnessRunVerdict;
133
+ /**
134
+ * Builds the operator-facing failure message for a green-looking summary
135
+ * that was rejected on crash or truncation evidence (never trusted as a
136
+ * pass, regardless of how green the embedded counts look).
137
+ */
138
+ export declare function buildGreenSummaryRejectedMessage(rejection: GreenSummaryRejection, exitCode: number): string;
82
139
  /** Builds the operator-facing failure message after retries are exhausted. */
83
140
  export declare function buildHarnessCrashMessage(signature: HarnessCrashSignature, attempts: number, commandLabel?: string): string;
84
141
  /**
@@ -47,10 +47,30 @@ const NONZERO_UNEXPECTED_SUMMARY_PATTERN = /\bUnexpected results:\s*[1-9]/;
47
47
  */
48
48
  const XPCSHELL_RESULT_SUMMARY_PATTERN = /\bTEST_END\b|\bRan \d+ checks?\b|\bResult summary:/i;
49
49
  const UNEXPECTED_LINE_PATTERN = /^.*\bTEST-UNEXPECTED-[A-Z-]+\b.*$/gm;
50
+ const FAIL_LINE_PATTERN = /^.*\bFAIL\b.*$/gm;
51
+ const ASSERTION_LINE_PATTERN = /^.*\b(?:Assertion failure|MOZ_ASSERT|ASSERTION)\b.*$/gim;
50
52
  const SHUTDOWN_REENTRY_PATTERN = /Application shut down \(without crashing\) in the middle of a test/i;
51
53
  const FOCUS_STALL_PATTERN = /must wait for focus/i;
52
54
  const TRACEBACK_PATTERN = /Traceback \(most recent call last\)/;
53
55
  const NO_OUTPUT_TIMEOUT_PATTERN = /timed out after \d+ seconds with no output/i;
56
+ /**
57
+ * Crash-marker lines mozcrash / the mochitest harness print when the
58
+ * browser process itself died. Matched against the RAW output (never
59
+ * noise-stripped): any of these proves the run was truncated by a crash,
60
+ * so an embedded green summary is under-reporting, not a result (field
61
+ * report: `Main app process: killed by SIGSEGV` at the second of eight
62
+ * files, summary `Passed: 2 / Failed: 0`, mach exit 1 — 0.35.0 accepted
63
+ * it as passed and masked a real regression for a whole drill).
64
+ */
65
+ const CRASH_MARKER_PATTERNS = [
66
+ /Main app process: killed by SIG\w+/,
67
+ /\bPROCESS-CRASH\b/,
68
+ // The minidump-processing header shapes mozcrash emits when it walks a
69
+ // crash dump after the run.
70
+ /mozcrash.*minidump/i,
71
+ /Crash dump filename/i,
72
+ /Thread \d+ \(crashed\)/,
73
+ ];
54
74
  /**
55
75
  * Startup-traceback fingerprints from the mozlog resource monitor / psutil
56
76
  * on macOS. Each is matched per-line so the evidence line in the report is
@@ -165,22 +185,89 @@ function hasExecutionSignal(output) {
165
185
  hasXpcshellResultSummary(output));
166
186
  }
167
187
  /**
168
- * True when the output embeds a COMPLETED, GREEN suite summary: an
169
- * execution signal, `Unexpected results: 0` (and no non-zero unexpected
170
- * count), a `SUITE_END` marker, and no real `TEST-UNEXPECTED-*` lines.
171
- * Such a run finished its suite; any startup-traceback-shaped noise in the
172
- * same output is by definition non-fatal, so this vetoes signature-based
173
- * crash classification (field report: fully green sharded runs reported
174
- * `CRASH (N attempts)` because degradation warnings matched the psutil
175
- * signals). Exported for direct unit testing.
188
+ * Finds the first crash-marker line in the RAW output (see
189
+ * {@link CRASH_MARKER_PATTERNS}), or undefined when the run shows no
190
+ * crash evidence. Exported for direct unit testing.
176
191
  */
177
- export function hasCompletedGreenSummary(output) {
192
+ export function findCrashMarkerLine(output) {
193
+ return findLine(output, CRASH_MARKER_PATTERNS);
194
+ }
195
+ /**
196
+ * True when the summary lines LOOK green: execution signal, `Unexpected
197
+ * results: 0` with no non-zero count, `SUITE_END`, no real
198
+ * `TEST-UNEXPECTED-*` lines. Deliberately blind to crash/truncation
199
+ * evidence — {@link hasCompletedGreenSummary} layers that on top, and
200
+ * {@link classifyHarnessRun} needs the distinction to report WHY a
201
+ * green-shaped summary was rejected.
202
+ */
203
+ function hasGreenShapedSummary(output) {
178
204
  return (hasExecutionSignal(output) &&
179
205
  SUITE_END_PATTERN.test(output) &&
180
206
  GREEN_UNEXPECTED_SUMMARY_PATTERN.test(output) &&
181
207
  !NONZERO_UNEXPECTED_SUMMARY_PATTERN.test(output) &&
182
208
  realUnexpectedFailureLines(output).length === 0);
183
209
  }
210
+ /**
211
+ * True when the output embeds a COMPLETED, GREEN suite summary: an
212
+ * execution signal, `Unexpected results: 0` (and no non-zero unexpected
213
+ * count), a `SUITE_END` marker, no real `TEST-UNEXPECTED-*` lines — and
214
+ * NO crash marker. Such a run finished its suite; any startup-traceback-
215
+ * shaped noise in the same output is by definition non-fatal, so this
216
+ * vetoes signature-based crash classification (field report: fully green
217
+ * sharded runs reported `CRASH (N attempts)` because degradation warnings
218
+ * matched the psutil signals). A summary printed after a crash marker is
219
+ * NOT "completed" — it only covers the files that ran before the crash
220
+ * (0.35.0 green-wash field report). Exported for direct unit testing.
221
+ */
222
+ export function hasCompletedGreenSummary(output) {
223
+ return hasGreenShapedSummary(output) && findCrashMarkerLine(output) === undefined;
224
+ }
225
+ /** Basename shape of a mozbuild-manifest test implementation file. */
226
+ const TEST_FILE_BASENAME_PATTERN = /^(?:browser|test)_.*\.m?js$/;
227
+ /**
228
+ * Start/end execution markers, both log dialects: the structured mozlog
229
+ * `TEST_START`/`TEST_END` pair and the human browser-chrome
230
+ * `TEST-START`/`TEST-OK` pair. `TEST-UNEXPECTED-*` end shapes are
231
+ * irrelevant here — any such line already fails the green-summary check
232
+ * before completeness is consulted.
233
+ */
234
+ const START_MARKER_PATTERN = /\bTEST[-_]START\b[:| ]*\s*(\S+)?/;
235
+ const END_MARKER_PATTERN = /\bTEST[-_]END\b|\bTEST-OK\b/;
236
+ /**
237
+ * Cross-checks run completeness against the requested test files: every
238
+ * requested file must produce a start marker, and every started test must
239
+ * produce an end marker (the harness emits both even for failing files).
240
+ *
241
+ * Ends are paired with starts POSITIONALLY (last open start is closed by
242
+ * the next end), not by filename — the xpcshell dialect's `TEST_END: Test
243
+ * PASS` lines do not name the file. Requested paths that are not
244
+ * test-file-shaped (directories, manifests) are skipped for the
245
+ * never-started check; the start/end pairing scan covers them regardless.
246
+ * Exported for direct unit testing.
247
+ */
248
+ export function analyzeTestCompleteness(output, requestedPaths) {
249
+ const startedNames = [];
250
+ const openTests = [];
251
+ for (const line of output.split(/\r?\n/)) {
252
+ const startMatch = START_MARKER_PATTERN.exec(line);
253
+ if (startMatch) {
254
+ const name = startMatch[1] ?? '(unnamed test)';
255
+ startedNames.push(name);
256
+ openTests.push(name);
257
+ continue;
258
+ }
259
+ if (END_MARKER_PATTERN.test(line)) {
260
+ openTests.pop();
261
+ }
262
+ }
263
+ const neverStarted = requestedPaths.filter((path) => {
264
+ const base = path.slice(path.lastIndexOf('/') + 1);
265
+ if (!TEST_FILE_BASENAME_PATTERN.test(base))
266
+ return false;
267
+ return !startedNames.some((name) => name.includes(base));
268
+ });
269
+ return { neverStarted, neverEnded: openTests };
270
+ }
184
271
  /**
185
272
  * True when the captured output carries the suite-specific xpcshell
186
273
  * result-summary block, which proves tests executed even though the
@@ -193,14 +280,49 @@ function hasXpcshellResultSummary(output) {
193
280
  }
194
281
  /** Unexpected-failure lines that are NOT the shutdown re-entry artifact. */
195
282
  function realUnexpectedFailureLines(output) {
196
- const matches = output.match(UNEXPECTED_LINE_PATTERN) ?? [];
197
- return matches.filter((line) => !SHUTDOWN_REENTRY_PATTERN.test(line));
283
+ const matches = [
284
+ ...(output.match(UNEXPECTED_LINE_PATTERN) ?? []),
285
+ ...(output.match(FAIL_LINE_PATTERN) ?? []),
286
+ ...(output.match(ASSERTION_LINE_PATTERN) ?? []),
287
+ ];
288
+ if (NONZERO_UNEXPECTED_SUMMARY_PATTERN.test(output)) {
289
+ const summary = output
290
+ .split(/\r?\n/)
291
+ .find((line) => NONZERO_UNEXPECTED_SUMMARY_PATTERN.test(line));
292
+ if (summary)
293
+ matches.push(summary);
294
+ }
295
+ return [...new Set(matches.map((line) => line.trim()))].filter((line) => !SHUTDOWN_REENTRY_PATTERN.test(line));
296
+ }
297
+ function detectSecondaryHarnessNoise(output) {
298
+ const evidence = stripNonSignalNoise(output);
299
+ if (TRACEBACK_PATTERN.test(evidence)) {
300
+ const signalLine = findLine(evidence, STARTUP_TRACEBACK_SIGNALS);
301
+ if (signalLine) {
302
+ return {
303
+ reason: 'harness traceback also present (resource monitor/psutil)',
304
+ line: signalLine,
305
+ };
306
+ }
307
+ }
308
+ const timeoutLine = findLine(evidence, [NO_OUTPUT_TIMEOUT_PATTERN]);
309
+ if (timeoutLine) {
310
+ return { reason: 'harness no-output timeout also present', line: timeoutLine };
311
+ }
312
+ return undefined;
198
313
  }
199
314
  /**
200
315
  * Detects the known harness-crash shapes in captured mach output.
201
316
  * Returns undefined for anything that looks like a genuine test result.
202
317
  */
203
318
  export function detectHarnessCrashSignature(output) {
319
+ // Browser/app process crashes are deterministic crash evidence, not
320
+ // retriable environmental harness noise. Leave them to
321
+ // `classifyHarnessRun` so it can reject green-looking truncated summaries
322
+ // with the crash line plus never-started/never-ended file evidence.
323
+ if (findCrashMarkerLine(output) !== undefined) {
324
+ return undefined;
325
+ }
204
326
  const hasTestStart = hasExecutionSignal(output);
205
327
  const realFailures = realUnexpectedFailureLines(output);
206
328
  // A completed green embedded summary vetoes signature-based crash
@@ -265,9 +387,17 @@ export function detectHarnessCrashSignature(output) {
265
387
  * dispatch prints no `TEST-START`). Bare `Passed:`/`Failed:` summary
266
388
  * lines are still not trusted as evidence of execution.
267
389
  * 3. Exit code zero with tests started is a pass; anything else is a
268
- * test failure for the regular diagnosis chain.
390
+ * test failure for the regular diagnosis chain — except a non-zero
391
+ * exit whose embedded summary completed green AND shows no crash
392
+ * marker and no truncated/never-started requested file, which is a
393
+ * pass with a `greenSummaryOverride` note (harness noise owns the
394
+ * exit code). A green-shaped summary WITH such evidence is rejected
395
+ * (`greenSummaryRejected`) and fails.
269
396
  */
270
397
  export function classifyHarnessRun(exitCode, output, requestedPaths) {
398
+ const realFailures = realUnexpectedFailureLines(output);
399
+ const firstRealFailure = realFailures[0];
400
+ const secondaryHarnessSignature = firstRealFailure !== undefined ? detectSecondaryHarnessNoise(output) : undefined;
271
401
  const signature = detectHarnessCrashSignature(output);
272
402
  if (signature) {
273
403
  return { kind: 'harness-crash', signature };
@@ -284,10 +414,62 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
284
414
  // non-zero on harness noise (field report: a fully green --no-shard run
285
415
  // exited 1). Real failures always carry a non-zero unexpected count or
286
416
  // TEST-UNEXPECTED lines, both of which fail the green-summary check.
287
- if (hasCompletedGreenSummary(output)) {
417
+ //
418
+ // The override must never win over crash or truncation evidence
419
+ // (0.35.0 green-wash field report: a SIGSEGV at the second of eight
420
+ // files produced a "green" `Passed: 2 / Failed: 0` summary and mach
421
+ // exit 1 — the summary was green only because the crash prevented the
422
+ // other six files from producing any results). A crash marker or an
423
+ // unpaired/never-started requested file rejects the override with the
424
+ // evidence attached, so the caller can say why.
425
+ if (hasGreenShapedSummary(output)) {
426
+ const crashLine = findCrashMarkerLine(output);
427
+ const completeness = analyzeTestCompleteness(output, requestedPaths);
428
+ if (crashLine !== undefined ||
429
+ completeness.neverStarted.length > 0 ||
430
+ completeness.neverEnded.length > 0) {
431
+ return {
432
+ kind: 'test-failures',
433
+ ...(firstRealFailure !== undefined ? { realFailureLine: firstRealFailure } : {}),
434
+ ...(secondaryHarnessSignature !== undefined ? { secondaryHarnessSignature } : {}),
435
+ greenSummaryRejected: {
436
+ ...(crashLine !== undefined ? { crashLine } : {}),
437
+ neverStarted: completeness.neverStarted,
438
+ neverEnded: completeness.neverEnded,
439
+ },
440
+ };
441
+ }
288
442
  return { kind: 'tests-ran-ok', greenSummaryOverride: true };
289
443
  }
290
- return { kind: 'test-failures' };
444
+ return {
445
+ kind: 'test-failures',
446
+ ...(firstRealFailure !== undefined ? { realFailureLine: firstRealFailure } : {}),
447
+ ...(secondaryHarnessSignature !== undefined ? { secondaryHarnessSignature } : {}),
448
+ };
449
+ }
450
+ /**
451
+ * Builds the operator-facing failure message for a green-looking summary
452
+ * that was rejected on crash or truncation evidence (never trusted as a
453
+ * pass, regardless of how green the embedded counts look).
454
+ */
455
+ export function buildGreenSummaryRejectedMessage(rejection, exitCode) {
456
+ const evidence = [];
457
+ if (rejection.crashLine !== undefined) {
458
+ evidence.push(` - crash evidence: ${rejection.crashLine}`);
459
+ }
460
+ if (rejection.neverStarted.length > 0) {
461
+ evidence.push(` - requested test file(s) that never started: ${rejection.neverStarted.join(', ')}`);
462
+ }
463
+ if (rejection.neverEnded.length > 0) {
464
+ evidence.push(` - test file(s) that started but never finished: ${rejection.neverEnded.join(', ')}`);
465
+ }
466
+ return (`mach exited ${exitCode} and the embedded suite summary looks green, but FireForge did NOT ` +
467
+ 'treat the run as passed:\n\n' +
468
+ `${evidence.join('\n')}\n\n` +
469
+ 'A crash- or truncation-shortened run under-reports: the "green" counts only cover the ' +
470
+ 'files that ran before the run died, so the summary is not a suite result. Treat this as a ' +
471
+ 'FAILED run — check the crash/truncation point in the output above, then re-run the ' +
472
+ 'remaining files.');
291
473
  }
292
474
  /** Builds the operator-facing failure message after retries are exhausted. */
293
475
  export function buildHarnessCrashMessage(signature, attempts, commandLabel = 'mach test') {
@@ -315,4 +497,3 @@ export function buildNoTestsRanMessage(exitCode, requestedPaths) {
315
497
  'Check that the paths are registered in their test manifest (browser.toml / xpcshell.toml) ' +
316
498
  'and that the manifest is reachable from moz.build, then retry.');
317
499
  }
318
- //# sourceMappingURL=test-harness-crash.js.map
@@ -86,4 +86,3 @@ export function buildHarnessEarlyExitMessage(earlyExit, normalizedPaths) {
86
86
  `Requested paths: ${paths}\n\n` +
87
87
  'Fix the harness startup/discovery issue above before interpreting this as a test failure.');
88
88
  }
89
- //# sourceMappingURL=test-harness-output.js.map
@@ -5,17 +5,21 @@
5
5
  * so `fireforge test browser/base/content/test/hominis` silently also
6
6
  * ran the sibling directory `browser/base/content/test/hominis-tiles`
7
7
  * (152.0b7 → 153.0b8 source-refresh drill: 1224 tests instead of ~200,
8
- * with no indication the scope widened). A trailing separator makes the
9
- * prefix match exact — `hominis/` cannot prefix `hominis-tiles/…`.
8
+ * with no indication the scope widened).
10
9
  *
11
10
  * FireForge therefore treats a directory argument as meaning EXACTLY
12
- * that directory: {@link analyzeTestPathScopes} returns a trailing-`/`
13
- * dispatch form for every existing directory, plus the sibling
14
- * directories the raw prefix would have swept in so the command layer
15
- * can tell the operator what was excluded. Note FireForge already
16
- * rejects non-existent paths up front, so raw prefix-widening was never
17
- * something an operator could invoke deliberately it only ever
18
- * happened by accident.
11
+ * that directory: {@link analyzeTestPathScopes} enumerates the test
12
+ * files of exactly that directory and dispatches THAT explicit file
13
+ * list to mach a file list cannot prefix-match anything. (0.35.0
14
+ * instead normalized the directory with a trailing `/`, assuming the
15
+ * separator makes the prefix match exact; field verification on Firefox
16
+ * 153 showed mach still swept in the prefix-named sibling, so the
17
+ * trailing-slash mechanism was cosmetic and its exclusion echo was
18
+ * wrong.) The prefix-matching sibling directories are still reported so
19
+ * the command layer can tell the operator what was excluded. Note
20
+ * FireForge already rejects non-existent paths up front, so raw
21
+ * prefix-widening was never something an operator could invoke
22
+ * deliberately — it only ever happened by accident.
19
23
  */
20
24
  /** A sibling directory the raw mach prefix match would have included. */
21
25
  export interface SiblingPrefixMatch {
@@ -29,13 +33,17 @@ export interface TestPathScope {
29
33
  /** The path as the operator passed it (engine-relative, normalized). */
30
34
  requestedPath: string;
31
35
  /**
32
- * The form to hand to mach: directories gain a trailing `/` so
33
- * mozbuild's prefix match is exact; files pass through unchanged.
36
+ * The paths to hand to mach for this argument: the explicit test-file
37
+ * list for a directory (mach cannot prefix-widen an explicit file
38
+ * list), or the argument unchanged for files. A directory containing
39
+ * no enumerable test files falls back to the trailing-`/` directory
40
+ * form — there is nothing to list, and mach owns the "no tests found"
41
+ * failure.
34
42
  */
35
- dispatchPath: string;
43
+ dispatchPaths: string[];
36
44
  /** True when the path resolved to a directory under engine/. */
37
45
  isDirectory: boolean;
38
- /** Recursive test-file count inside the requested directory (0 for files). */
46
+ /** Test files enumerated inside the requested directory (empty for files). */
39
47
  testFileCount: number;
40
48
  /** Sibling directories a raw prefix match would also have selected. */
41
49
  siblingPrefixMatches: SiblingPrefixMatch[];
@@ -6,45 +6,50 @@
6
6
  * so `fireforge test browser/base/content/test/hominis` silently also
7
7
  * ran the sibling directory `browser/base/content/test/hominis-tiles`
8
8
  * (152.0b7 → 153.0b8 source-refresh drill: 1224 tests instead of ~200,
9
- * with no indication the scope widened). A trailing separator makes the
10
- * prefix match exact — `hominis/` cannot prefix `hominis-tiles/…`.
9
+ * with no indication the scope widened).
11
10
  *
12
11
  * FireForge therefore treats a directory argument as meaning EXACTLY
13
- * that directory: {@link analyzeTestPathScopes} returns a trailing-`/`
14
- * dispatch form for every existing directory, plus the sibling
15
- * directories the raw prefix would have swept in so the command layer
16
- * can tell the operator what was excluded. Note FireForge already
17
- * rejects non-existent paths up front, so raw prefix-widening was never
18
- * something an operator could invoke deliberately it only ever
19
- * happened by accident.
12
+ * that directory: {@link analyzeTestPathScopes} enumerates the test
13
+ * files of exactly that directory and dispatches THAT explicit file
14
+ * list to mach a file list cannot prefix-match anything. (0.35.0
15
+ * instead normalized the directory with a trailing `/`, assuming the
16
+ * separator makes the prefix match exact; field verification on Firefox
17
+ * 153 showed mach still swept in the prefix-named sibling, so the
18
+ * trailing-slash mechanism was cosmetic and its exclusion echo was
19
+ * wrong.) The prefix-matching sibling directories are still reported so
20
+ * the command layer can tell the operator what was excluded. Note
21
+ * FireForge already rejects non-existent paths up front, so raw
22
+ * prefix-widening was never something an operator could invoke
23
+ * deliberately — it only ever happened by accident.
20
24
  */
21
25
  import { readdir, stat } from 'node:fs/promises';
22
26
  import { basename, dirname, join } from 'node:path';
23
27
  /** Test implementation files as mozbuild manifests name them. */
24
- const TEST_FILE_PATTERN = /^(?:browser|test)_.*\.m?js$/;
28
+ const TEST_FILE_PATTERN = /^(?:browser|test)_.*\.(?:m?js|x?html)$/;
25
29
  /**
26
- * Recursively counts test-implementation files under a directory.
27
- * Returns 0 when the directory cannot be read — the count feeds an
28
- * informational notice, never a decision.
30
+ * Recursively enumerates test-implementation files under a directory,
31
+ * returned engine-relative and sorted for deterministic dispatch order.
32
+ * Returns [] when the directory cannot be read — the caller then falls
33
+ * back to the directory form and mach owns the failure.
29
34
  */
30
- async function countTestFiles(dir) {
31
- let count = 0;
35
+ async function collectTestFiles(dir, relPrefix) {
36
+ const files = [];
32
37
  let entries;
33
38
  try {
34
39
  entries = await readdir(dir, { withFileTypes: true });
35
40
  }
36
41
  catch {
37
- return 0;
42
+ return [];
38
43
  }
39
44
  for (const entry of entries) {
40
45
  if (entry.isDirectory()) {
41
- count += await countTestFiles(join(dir, entry.name));
46
+ files.push(...(await collectTestFiles(join(dir, entry.name), `${relPrefix}${entry.name}/`)));
42
47
  }
43
48
  else if (entry.isFile() && TEST_FILE_PATTERN.test(entry.name)) {
44
- count += 1;
49
+ files.push(`${relPrefix}${entry.name}`);
45
50
  }
46
51
  }
47
- return count;
52
+ return files.sort();
48
53
  }
49
54
  /** Probes whether `path` is a directory, without throwing. */
50
55
  async function isDirectorySafe(path) {
@@ -65,7 +70,7 @@ async function analyzeTestPathScope(engineDir, requestedPath) {
65
70
  if (!(await isDirectorySafe(absolute))) {
66
71
  return {
67
72
  requestedPath,
68
- dispatchPath: requestedPath,
73
+ dispatchPaths: [requestedPath],
69
74
  isDirectory: false,
70
75
  testFileCount: 0,
71
76
  siblingPrefixMatches: [],
@@ -82,7 +87,7 @@ async function analyzeTestPathScope(engineDir, requestedPath) {
82
87
  if (entry.name === base || !entry.name.startsWith(base))
83
88
  continue;
84
89
  const siblingRel = parentRel === '.' ? entry.name : `${parentRel}/${entry.name}`;
85
- const testFileCount = await countTestFiles(join(dirname(absolute), entry.name));
90
+ const testFileCount = (await collectTestFiles(join(dirname(absolute), entry.name), `${siblingRel}/`)).length;
86
91
  // A prefix-matching sibling with no test files never surfaces in a
87
92
  // mach run, so listing it would only add noise.
88
93
  if (testFileCount > 0) {
@@ -92,13 +97,16 @@ async function analyzeTestPathScope(engineDir, requestedPath) {
92
97
  }
93
98
  catch {
94
99
  // Unreadable parent: skip the sibling probe; exactness via the
95
- // trailing separator is preserved regardless.
100
+ // explicit file list is preserved regardless.
96
101
  }
102
+ const testFiles = await collectTestFiles(absolute, `${stripped}/`);
97
103
  return {
98
104
  requestedPath,
99
- dispatchPath: `${stripped}/`,
105
+ // The explicit file list is what makes the selection exact; an empty
106
+ // enumeration falls back to the directory form.
107
+ dispatchPaths: testFiles.length > 0 ? testFiles : [`${stripped}/`],
100
108
  isDirectory: true,
101
- testFileCount: await countTestFiles(absolute),
109
+ testFileCount: testFiles.length,
102
110
  siblingPrefixMatches,
103
111
  };
104
112
  }
@@ -122,12 +130,19 @@ export async function analyzeTestPathScopes(engineDir, requestedPaths) {
122
130
  export function formatScopeNotice(scope) {
123
131
  if (!scope.isDirectory || scope.siblingPrefixMatches.length === 0)
124
132
  return undefined;
133
+ const requestedDir = scope.requestedPath.replace(/\/+$/, '');
125
134
  const siblings = scope.siblingPrefixMatches
126
135
  .map((s) => `${s.path}/ (${s.testFileCount} test file${s.testFileCount === 1 ? '' : 's'})`)
127
136
  .join(', ');
128
- return (`Selected exactly ${scope.dispatchPath} (${scope.testFileCount} test file${scope.testFileCount === 1 ? '' : 's'}). ` +
129
- `Excluded ${scope.siblingPrefixMatches.length} sibling director${scope.siblingPrefixMatches.length === 1 ? 'y' : 'ies'} ` +
130
- `that a raw prefix match would also have run: ${siblings}. ` +
137
+ if (scope.testFileCount === 0) {
138
+ // Fallback dispatch: with nothing to enumerate, the raw directory
139
+ // form goes to mach, whose prefix matching CAN sweep the siblings in.
140
+ // Claiming exclusion here would repeat the 0.35.0 mistake.
141
+ return (`${requestedDir}/ contains no enumerable test files, so the directory is passed to mach ` +
142
+ `as-is — mach resolves paths by string prefix, which may also select: ${siblings}.`);
143
+ }
144
+ return (`Selected exactly ${requestedDir}/ by passing its ${scope.testFileCount} test file${scope.testFileCount === 1 ? '' : 's'} ` +
145
+ 'explicitly to mach (an explicit file list cannot prefix-match a sibling). ' +
146
+ `Excluded ${scope.siblingPrefixMatches.length} prefix-named sibling director${scope.siblingPrefixMatches.length === 1 ? 'y' : 'ies'}: ${siblings}. ` +
131
147
  'Pass them as separate paths to include them.');
132
148
  }
133
- //# sourceMappingURL=test-path-scope.js.map
@@ -112,4 +112,3 @@ export function formatStaleBuildWarning(result) {
112
112
  'packaged chrome / jar.mn resources, rerun with "fireforge test --build" (or ' +
113
113
  '"fireforge build --ui") first. Passing --build skips this check.');
114
114
  }
115
- //# sourceMappingURL=test-stale-check.js.map
@@ -37,4 +37,3 @@ export async function tryRepairStaleXpcshellTestSymlink(engineDir, objDir, outpu
37
37
  warn(`Removed stale xpcshell harness symlink under ${objDir}/_tests and retrying mach test once: ${relative(resolve(engineDir), resolvedDestination)}`);
38
38
  return true;
39
39
  }
40
- //# sourceMappingURL=test-stale-symlink.js.map
@@ -20,4 +20,3 @@ export async function retryAfterXpcshellSymlinkRepair(engineDir, objDir, result,
20
20
  }
21
21
  return result;
22
22
  }
23
- //# sourceMappingURL=test-xpcshell-retry.js.map
@@ -95,4 +95,3 @@ export async function measureTokenCoverage(repoDir, cssFiles, projectRoot) {
95
95
  files: entries,
96
96
  };
97
97
  }
98
- //# sourceMappingURL=token-coverage.js.map