@hominis/fireforge 0.52.0 → 0.53.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 (243) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/bootstrap.js +2 -3
  4. package/dist/src/commands/build-prelock.d.ts +3 -3
  5. package/dist/src/commands/build-prelock.js +3 -3
  6. package/dist/src/commands/build.js +4 -0
  7. package/dist/src/commands/declarations-report.d.ts +9 -3
  8. package/dist/src/commands/declarations-report.js +18 -11
  9. package/dist/src/commands/discard.js +3 -3
  10. package/dist/src/commands/doctor-furnace-config-sync.js +4 -4
  11. package/dist/src/commands/doctor-furnace.js +5 -5
  12. package/dist/src/commands/export-adopt-check.d.ts +5 -6
  13. package/dist/src/commands/export-adopt-check.js +5 -6
  14. package/dist/src/commands/export-flow.js +6 -9
  15. package/dist/src/commands/export-staged-registration.d.ts +4 -4
  16. package/dist/src/commands/export-staged-registration.js +4 -4
  17. package/dist/src/commands/forward-import-remedy.d.ts +2 -3
  18. package/dist/src/commands/forward-import-remedy.js +2 -3
  19. package/dist/src/commands/furnace/create-dry-run.d.ts +2 -2
  20. package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +2 -2
  21. package/dist/src/commands/furnace/deploy-fragment-includers.js +2 -2
  22. package/dist/src/commands/furnace/index.js +2 -2
  23. package/dist/src/commands/lint.js +4 -4
  24. package/dist/src/commands/patch/compact.js +1 -1
  25. package/dist/src/commands/patch/lint-ignore-from.d.ts +3 -4
  26. package/dist/src/commands/patch/lint-ignore-from.js +3 -4
  27. package/dist/src/commands/patch/move-files-adopt.d.ts +33 -0
  28. package/dist/src/commands/patch/move-files-adopt.js +65 -0
  29. package/dist/src/commands/patch/move-files-create.js +41 -20
  30. package/dist/src/commands/patch/move-files-into.d.ts +18 -0
  31. package/dist/src/commands/patch/move-files-into.js +125 -60
  32. package/dist/src/commands/patch/move-files-retire.d.ts +39 -0
  33. package/dist/src/commands/patch/move-files-retire.js +91 -0
  34. package/dist/src/commands/patch/move-files.d.ts +3 -0
  35. package/dist/src/commands/patch/move-files.js +42 -12
  36. package/dist/src/commands/patch/move-size-tier.d.ts +27 -0
  37. package/dist/src/commands/patch/move-size-tier.js +66 -0
  38. package/dist/src/commands/patch/rename.js +1 -1
  39. package/dist/src/commands/patch/reorder-projection.d.ts +17 -0
  40. package/dist/src/commands/patch/reorder-projection.js +49 -0
  41. package/dist/src/commands/patch/reorder-shift.d.ts +30 -0
  42. package/dist/src/commands/patch/reorder-shift.js +307 -0
  43. package/dist/src/commands/patch/reorder.d.ts +9 -15
  44. package/dist/src/commands/patch/reorder.js +52 -50
  45. package/dist/src/commands/patch/split-plan.d.ts +31 -3
  46. package/dist/src/commands/patch/split-plan.js +36 -12
  47. package/dist/src/commands/patch/split-staged.d.ts +2 -2
  48. package/dist/src/commands/patch/split-staged.js +2 -2
  49. package/dist/src/commands/patch/split.js +18 -5
  50. package/dist/src/commands/patch/staged-dependency-entries.d.ts +9 -1
  51. package/dist/src/commands/patch/staged-dependency-entries.js +32 -4
  52. package/dist/src/commands/patch/staged-dependency-from.js +15 -9
  53. package/dist/src/commands/patch/staged-dependency.js +13 -9
  54. package/dist/src/commands/patch/tier.js +2 -2
  55. package/dist/src/commands/patch-rename-map.d.ts +14 -3
  56. package/dist/src/commands/patch-rename-map.js +21 -3
  57. package/dist/src/commands/re-export-adjacent.d.ts +4 -5
  58. package/dist/src/commands/re-export-lint.d.ts +4 -3
  59. package/dist/src/commands/re-export-lint.js +4 -3
  60. package/dist/src/commands/re-export-preflight.js +1 -2
  61. package/dist/src/commands/re-export-purity-scope.d.ts +1 -1
  62. package/dist/src/commands/re-export-purity-scope.js +13 -7
  63. package/dist/src/commands/re-export-retired-declarations.d.ts +22 -0
  64. package/dist/src/commands/re-export-retired-declarations.js +113 -0
  65. package/dist/src/commands/re-export-single.js +2 -2
  66. package/dist/src/commands/re-export-stale-registrations.d.ts +27 -6
  67. package/dist/src/commands/re-export-stale-registrations.js +65 -21
  68. package/dist/src/commands/re-export.js +4 -0
  69. package/dist/src/commands/rebase/continue.js +3 -3
  70. package/dist/src/commands/register.js +5 -4
  71. package/dist/src/commands/status.js +1 -1
  72. package/dist/src/commands/test-build-furnace.d.ts +1 -2
  73. package/dist/src/commands/test-build-furnace.js +1 -2
  74. package/dist/src/commands/test-build-phase.d.ts +8 -0
  75. package/dist/src/commands/test-build-phase.js +42 -7
  76. package/dist/src/commands/test-control-restore.d.ts +40 -0
  77. package/dist/src/commands/test-control-restore.js +86 -0
  78. package/dist/src/commands/test-dispatch.js +68 -7
  79. package/dist/src/commands/test-multi-verdict.js +1 -0
  80. package/dist/src/commands/test-orchestrate.d.ts +6 -0
  81. package/dist/src/commands/test-orchestrate.js +28 -0
  82. package/dist/src/commands/test-perf-scope.d.ts +1 -1
  83. package/dist/src/commands/test-perf-scope.js +1 -1
  84. package/dist/src/commands/test-prelock.d.ts +7 -8
  85. package/dist/src/commands/test-prelock.js +15 -12
  86. package/dist/src/commands/test-register.js +2 -2
  87. package/dist/src/commands/test-results.d.ts +10 -1
  88. package/dist/src/commands/test-results.js +20 -3
  89. package/dist/src/commands/test-run.d.ts +6 -0
  90. package/dist/src/commands/test-run.js +23 -4
  91. package/dist/src/commands/test-shards.d.ts +7 -3
  92. package/dist/src/commands/test-shards.js +24 -7
  93. package/dist/src/commands/test-stale-gate.d.ts +6 -1
  94. package/dist/src/commands/test-stale-gate.js +40 -2
  95. package/dist/src/commands/test-verdict.d.ts +5 -3
  96. package/dist/src/commands/test.js +10 -0
  97. package/dist/src/commands/token.js +2 -2
  98. package/dist/src/commands/verify.js +1 -1
  99. package/dist/src/commands/watch.js +1 -1
  100. package/dist/src/commands/wire.js +1 -1
  101. package/dist/src/core/build-audit-resolve.d.ts +4 -5
  102. package/dist/src/core/build-audit-resolve.js +4 -5
  103. package/dist/src/core/build-audit-skip.d.ts +2 -2
  104. package/dist/src/core/build-audit.d.ts +3 -4
  105. package/dist/src/core/build-baseline-types.d.ts +24 -3
  106. package/dist/src/core/build-baseline-validate.d.ts +30 -1
  107. package/dist/src/core/build-baseline-validate.js +127 -35
  108. package/dist/src/core/build-baseline.d.ts +8 -2
  109. package/dist/src/core/build-baseline.js +25 -16
  110. package/dist/src/core/build-jar-escalation.d.ts +4 -4
  111. package/dist/src/core/build-jar-escalation.js +4 -4
  112. package/dist/src/core/build-prepare.js +4 -4
  113. package/dist/src/core/config-validate.js +4 -4
  114. package/dist/src/core/coverage-extend.d.ts +5 -3
  115. package/dist/src/core/coverage-extend.js +8 -3
  116. package/dist/src/core/coverage-records.d.ts +4 -5
  117. package/dist/src/core/coverage-records.js +4 -5
  118. package/dist/src/core/destructive.d.ts +2 -2
  119. package/dist/src/core/destructive.js +2 -2
  120. package/dist/src/core/discard-baseline.d.ts +2 -2
  121. package/dist/src/core/discard-baseline.js +3 -3
  122. package/dist/src/core/engine-session-lock.d.ts +1 -1
  123. package/dist/src/core/engine-session-lock.js +1 -1
  124. package/dist/src/core/file-lock-owner.d.ts +5 -5
  125. package/dist/src/core/file-lock-owner.js +5 -5
  126. package/dist/src/core/file-lock-queue.js +6 -8
  127. package/dist/src/core/file-lock-shared.d.ts +4 -4
  128. package/dist/src/core/file-lock-shared.js +4 -4
  129. package/dist/src/core/file-lock.js +7 -10
  130. package/dist/src/core/firefox.js +2 -2
  131. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
  132. package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
  133. package/dist/src/core/furnace-apply.js +3 -3
  134. package/dist/src/core/furnace-operation.js +5 -5
  135. package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
  136. package/dist/src/core/furnace-registration-rename.d.ts +5 -5
  137. package/dist/src/core/furnace-registration-rename.js +5 -5
  138. package/dist/src/core/furnace-registration-validate.d.ts +2 -1
  139. package/dist/src/core/furnace-registration-validate.js +2 -1
  140. package/dist/src/core/furnace-registration.js +2 -2
  141. package/dist/src/core/git-base.js +4 -3
  142. package/dist/src/core/git-diff.js +1 -1
  143. package/dist/src/core/git.js +1 -1
  144. package/dist/src/core/harness-orphans.d.ts +6 -6
  145. package/dist/src/core/harness-orphans.js +6 -6
  146. package/dist/src/core/harness-port-reservation.d.ts +60 -0
  147. package/dist/src/core/harness-port-reservation.js +122 -0
  148. package/dist/src/core/history-log-file.d.ts +9 -9
  149. package/dist/src/core/history-log-file.js +15 -15
  150. package/dist/src/core/history-log.d.ts +3 -3
  151. package/dist/src/core/history-log.js +3 -3
  152. package/dist/src/core/launchable-build.d.ts +39 -0
  153. package/dist/src/core/launchable-build.js +85 -0
  154. package/dist/src/core/marionette-port.js +1 -3
  155. package/dist/src/core/moz-manifest-rules.js +3 -2
  156. package/dist/src/core/negative-control-compiled.d.ts +13 -3
  157. package/dist/src/core/negative-control-compiled.js +27 -5
  158. package/dist/src/core/parser-fallback.d.ts +5 -5
  159. package/dist/src/core/parser-fallback.js +5 -5
  160. package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
  161. package/dist/src/core/patch-identifier-suggest.js +11 -4
  162. package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
  163. package/dist/src/core/patch-lint-checkjs.js +1 -1
  164. package/dist/src/core/patch-lint-css.js +2 -2
  165. package/dist/src/core/patch-lint-file-size.d.ts +5 -7
  166. package/dist/src/core/patch-lint-file-size.js +5 -7
  167. package/dist/src/core/patch-lint-ignore.d.ts +3 -3
  168. package/dist/src/core/patch-lint-ignore.js +3 -3
  169. package/dist/src/core/patch-lint-prettier.d.ts +4 -6
  170. package/dist/src/core/patch-lint-prettier.js +4 -6
  171. package/dist/src/core/patch-lint-projection.d.ts +1 -1
  172. package/dist/src/core/patch-lint-projection.js +1 -1
  173. package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
  174. package/dist/src/core/patch-lint-queue-types.js +2 -2
  175. package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
  176. package/dist/src/core/patch-manifest-consistency.js +2 -2
  177. package/dist/src/core/patch-manifest-io.d.ts +5 -1
  178. package/dist/src/core/patch-manifest-io.js +12 -10
  179. package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
  180. package/dist/src/core/patch-manifest-key-order.js +3 -4
  181. package/dist/src/core/patch-parse.d.ts +3 -3
  182. package/dist/src/core/patch-parse.js +3 -3
  183. package/dist/src/core/patch-policy.d.ts +1 -1
  184. package/dist/src/core/patch-policy.js +1 -1
  185. package/dist/src/core/patch-source-metadata.d.ts +1 -1
  186. package/dist/src/core/patch-source-metadata.js +1 -1
  187. package/dist/src/core/patch-subset.d.ts +5 -3
  188. package/dist/src/core/patch-subset.js +8 -7
  189. package/dist/src/core/patch-transform.d.ts +2 -2
  190. package/dist/src/core/patch-transform.js +2 -2
  191. package/dist/src/core/rebase-session.js +4 -6
  192. package/dist/src/core/register-xpcshell-test.d.ts +28 -7
  193. package/dist/src/core/register-xpcshell-test.js +76 -27
  194. package/dist/src/core/run-log.d.ts +8 -8
  195. package/dist/src/core/run-log.js +8 -8
  196. package/dist/src/core/status-classify.d.ts +1 -2
  197. package/dist/src/core/status-classify.js +1 -2
  198. package/dist/src/core/test-harness-crash.d.ts +5 -3
  199. package/dist/src/core/test-harness-crash.js +13 -14
  200. package/dist/src/core/test-harness-infra.d.ts +32 -0
  201. package/dist/src/core/test-harness-infra.js +69 -0
  202. package/dist/src/core/test-harness-signature.d.ts +6 -0
  203. package/dist/src/core/test-stale-check.d.ts +38 -0
  204. package/dist/src/core/test-stale-check.js +82 -1
  205. package/dist/src/core/test-stall-triage.js +5 -7
  206. package/dist/src/core/token-category.js +2 -2
  207. package/dist/src/core/token-inventory.js +3 -3
  208. package/dist/src/core/token-manager.d.ts +2 -2
  209. package/dist/src/core/token-manager.js +5 -7
  210. package/dist/src/core/token-variant.d.ts +6 -6
  211. package/dist/src/core/token-variant.js +7 -7
  212. package/dist/src/core/typecheck-shim.js +1 -1
  213. package/dist/src/core/wire-destroy.d.ts +1 -1
  214. package/dist/src/core/wire-destroy.js +1 -1
  215. package/dist/src/core/wire-dom-fragment.js +1 -1
  216. package/dist/src/core/wire-init.d.ts +1 -1
  217. package/dist/src/core/wire-init.js +1 -1
  218. package/dist/src/core/wire-subscript.d.ts +1 -1
  219. package/dist/src/core/wire-subscript.js +1 -1
  220. package/dist/src/core/wire-utils.d.ts +2 -2
  221. package/dist/src/core/wire-utils.js +3 -3
  222. package/dist/src/errors/base.d.ts +1 -1
  223. package/dist/src/errors/base.js +1 -1
  224. package/dist/src/errors/rebase.d.ts +1 -1
  225. package/dist/src/errors/rebase.js +1 -1
  226. package/dist/src/types/commands/options.d.ts +26 -3
  227. package/dist/src/types/config.d.ts +5 -7
  228. package/dist/src/utils/options.js +2 -2
  229. package/dist/src/utils/process.js +2 -2
  230. package/dist/src/utils/regex.d.ts +2 -2
  231. package/dist/src/utils/regex.js +3 -3
  232. package/dist/src/utils/sleep.d.ts +5 -6
  233. package/dist/src/utils/sleep.js +5 -6
  234. package/dist/src/utils/validation.js +2 -2
  235. package/docs/configuration.md +21 -23
  236. package/docs/exit-codes.md +8 -11
  237. package/docs/furnace.md +3 -3
  238. package/docs/lifecycle-invariants.md +36 -45
  239. package/docs/machine-output.md +41 -24
  240. package/docs/patch-workflow.md +129 -24
  241. package/docs/run-logs.md +8 -9
  242. package/docs/testing.md +105 -46
  243. package/package.json +1 -1
@@ -2,11 +2,11 @@
2
2
  /**
3
3
  * In-place rename of a custom element registration in customElements.js.
4
4
  *
5
- * `furnace rename` used to remove the old entry and add a new one. On a
6
- * registration list the remove path could not see, that left the old entry
7
- * beside the new one, and the new entry was written without the project
8
- * marker every sibling line carries (`// <MARKER>:`), so the next lint
9
- * flagged it as an unmarked engine edit. Rewriting the tag and module
5
+ * Removing the old entry and adding a new one would, on a registration
6
+ * list the remove path cannot see, leave the old entry beside the new one,
7
+ * and the new entry would lack the project marker every sibling line
8
+ * carries (`// <MARKER>:`), so the next lint would flag it as an unmarked
9
+ * engine edit. Rewriting the tag and module
10
10
  * literals inside the existing entry keeps the entry's position, layout and
11
11
  * trailing marker comments exactly as they were.
12
12
  */
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Shared validation for furnace custom element registration placement.
3
- * Used after both AST and legacy code paths to avoid duplicating logic.
3
+ * Shared by the AST registration editor and the registration validator to
4
+ * avoid duplicating logic.
4
5
  */
5
6
  /**
6
7
  * Regex for valid custom element tag names. A valid name is lowercase, starts
@@ -1,7 +1,8 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
3
  * Shared validation for furnace custom element registration placement.
4
- * Used after both AST and legacy code paths to avoid duplicating logic.
4
+ * Shared by the AST registration editor and the registration validator to
5
+ * avoid duplicating logic.
5
6
  */
6
7
  import { FurnaceError } from '../errors/furnace.js';
7
8
  import { escapeRegex, stripJsComments } from '../utils/regex.js';
@@ -252,8 +252,8 @@ export async function removeJarMnEntries(engineDir, tagName) {
252
252
  // line the component registered is removed regardless of the target
253
253
  // basename. A helper `.mjs` whose name does not start with the tag (e.g.
254
254
  // a renamed `foo-utils.mjs`) otherwise survives the remove pass and leaves
255
- // a stale registration that breaks packaging. The legacy target-path match
256
- // is kept as an OR for lines written before the source mapping existed.
255
+ // a stale registration that breaks packaging. The target-path match is
256
+ // kept as an OR for lines that carry no source mapping.
257
257
  // Word boundaries keep "moz-card" from matching "moz-card-group".
258
258
  const sourcePattern = new RegExp(`\\(widgets/${escapeRegex(tagName)}/`);
259
259
  const legacyTargetPattern = new RegExp(`content/global/elements/${escapeRegex(tagName)}\\.`);
@@ -45,9 +45,10 @@ export const GIT_ADD_CHUNK_TIMEOUT_MS = resolveTimeoutFromEnv(GIT_ADD_CHUNK_TIME
45
45
  /**
46
46
  * In-flight or settled successful `git` availability probe.
47
47
  *
48
- * The probe spawns `which git` (or `where` on Windows), and every git wrapper
49
- * in this layer used to run it before each command, which meant dozens of
50
- * extra spawns per `fireforge export`. Only success is cached: a rejection
48
+ * The probe spawns `which git` (or `where` on Windows); running it before
49
+ * every command would mean dozens of extra spawns per `fireforge export`, so
50
+ * the result is shared by every git wrapper in this layer. Only success is
51
+ * cached: a rejection
51
52
  * clears the slot so a run that installs git mid-flight (or a test that flips
52
53
  * the probe) sees the new answer instead of a sticky refusal.
53
54
  */
@@ -521,7 +521,7 @@ async function generateBinaryFilePatchWithConfig(repoDir, filePath, gitConfigArg
521
521
  * "absent", and this branch only runs for files untracked in HEAD). A prior
522
522
  * stage-0 entry is re-pointed at its recorded mode and blob. Unmerged entries
523
523
  * (stages 1 to 3) cannot be rebuilt through `--cacheinfo`. That degenerate
524
- * state keeps the legacy reset-to-HEAD behavior rather than corrupting the
524
+ * state falls back to the reset-to-HEAD restore rather than corrupting the
525
525
  * conflict.
526
526
  * @param repoDir - Repository directory
527
527
  * @param filePath - File path (relative to repo root)
@@ -46,7 +46,7 @@ const GIT_ADD_ENV = { GIT_INDEX_THREADS: '0' };
46
46
  * AbortError when the signal fires, so both shapes have to be recognised.
47
47
  */
48
48
  function isTimeoutError(error) {
49
- // `exec` now rejects with a typed ExecTimeoutError when its `timeout`
49
+ // `exec` rejects with a typed ExecTimeoutError when its `timeout`
50
50
  // option fires (the raw AbortError check below is kept for defence in
51
51
  // depth against other abort paths).
52
52
  if (error instanceof ExecTimeoutError)
@@ -45,12 +45,12 @@ export interface OrphanedHarnessProcess {
45
45
  * True when `command` IS a harness helper: its executable is a helper
46
46
  * binary, or an interpreter whose first argument is a helper script.
47
47
  *
48
- * Structural on purpose. An earlier version matched the helper names
49
- * anywhere in the command line, which is right for the httpd (`xpcshell -g
50
- * … -f …/server.js`) and wrong for everything that merely mentions such a
51
- * path: an editor opened on `server.js`, a shell whose command text quotes
52
- * it, a `grep` over the objdir. Report-only, that printed a misleading
53
- * `kill`; under the reap posture it terminated the operator's shell.
48
+ * Structural on purpose. Matching the helper names anywhere in the command
49
+ * line is right for the httpd (`xpcshell -g … -f …/server.js`) and wrong for
50
+ * everything that merely mentions such a path: an editor opened on
51
+ * `server.js`, a shell whose command text quotes it, a `grep` over the
52
+ * objdir. Report-only, that would print a misleading `kill`; under the reap
53
+ * posture it would terminate the operator's shell.
54
54
  */
55
55
  export declare function matchesHarnessHelper(command: string): boolean;
56
56
  /** Objdir provenance: an explicit objdir path, a `obj-…` path segment, or `_tests/`. */
@@ -55,12 +55,12 @@ function basename(token) {
55
55
  * True when `command` IS a harness helper: its executable is a helper
56
56
  * binary, or an interpreter whose first argument is a helper script.
57
57
  *
58
- * Structural on purpose. An earlier version matched the helper names
59
- * anywhere in the command line, which is right for the httpd (`xpcshell -g
60
- * … -f …/server.js`) and wrong for everything that merely mentions such a
61
- * path: an editor opened on `server.js`, a shell whose command text quotes
62
- * it, a `grep` over the objdir. Report-only, that printed a misleading
63
- * `kill`; under the reap posture it terminated the operator's shell.
58
+ * Structural on purpose. Matching the helper names anywhere in the command
59
+ * line is right for the httpd (`xpcshell -g … -f …/server.js`) and wrong for
60
+ * everything that merely mentions such a path: an editor opened on
61
+ * `server.js`, a shell whose command text quotes it, a `grep` over the
62
+ * objdir. Report-only, that would print a misleading `kill`; under the reap
63
+ * posture it would terminate the operator's shell.
64
64
  */
65
65
  export function matchesHarnessHelper(command) {
66
66
  const [exe, first] = command.trim().split(/\s+/);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Mochitest server ports for `fireforge test --jobs <n>`: placeholder
3
+ * listeners that push every parallel harness instance onto its own ports,
4
+ * and the parse of the ports each instance actually took.
5
+ *
6
+ * Desktop `mach mochitest` has no `--http-port`, `--ssl-port` or
7
+ * `--websocket-port`. Its `startServers` (`testing/mochitest/runtests.py`)
8
+ * bind-probes the HTTP server, ssltunnel and WebSocket ports (8888, 4443,
9
+ * 9988) and moves only a busy one to a free port; see
10
+ * `harness-port-fallback.ts`. Instances started at the same moment all
11
+ * pass that probe on a free 8888, then race to bind it: the losers die with
12
+ * `could not start server on port 8888` before any test runs. Holding the
13
+ * three ports for the whole parallel run makes every instance's probe fail,
14
+ * so each one falls back to an OS-assigned port of its own.
15
+ */
16
+ /** Which mochitest server a port belongs to, as the verdict line names it. */
17
+ export type HarnessServerPortKey = 'http' | 'ssl' | 'ws';
18
+ /** Placeholder listeners held for a parallel run. */
19
+ export interface HarnessPortReservation {
20
+ /** Ports this process now holds. A port that was already busy is not listed. */
21
+ held: number[];
22
+ /** Closes every placeholder. Safe to call more than once. */
23
+ release(): Promise<void>;
24
+ }
25
+ /**
26
+ * Holds a placeholder listener on `127.0.0.1` on each mochitest server
27
+ * port, so the harness's busy-port probe moves every instance elsewhere.
28
+ * A port that cannot be bound is skipped, not refused: whatever holds it
29
+ * already makes the probe fail, which is all the placeholder is for (the
30
+ * preflight has already ruled out this checkout's own stale httpd).
31
+ *
32
+ * @param ports - Ports to hold (default: {@link HARNESS_SERVER_PORTS})
33
+ */
34
+ export declare function reserveHarnessServerPorts(ports?: readonly number[]): Promise<HarnessPortReservation>;
35
+ /** The server ports one harness invocation reported, by server. */
36
+ export type HarnessServerPorts = Partial<Record<HarnessServerPortKey, number>>;
37
+ /**
38
+ * Records the port one output line reports, if it reports one: the
39
+ * fallback port of a "falling back" line, or the port itself otherwise.
40
+ *
41
+ * @param line - One line of harness output
42
+ * @param into - Collected ports; a later line for the same server wins
43
+ */
44
+ export declare function recordHarnessServerPortLine(line: string, into: HarnessServerPorts): void;
45
+ /** Collects server ports from streamed output chunks. */
46
+ export interface HarnessPortTracker {
47
+ onChunk(stream: 'stdout' | 'stderr', chunk: string): void;
48
+ /** The ports seen so far, including an unterminated last line. */
49
+ ports(): HarnessServerPorts;
50
+ }
51
+ /**
52
+ * A tracker over the full output stream. The ports are printed at harness
53
+ * start, so the capped capture tail a long run keeps would lose them.
54
+ */
55
+ export declare function createHarnessPortTracker(): HarnessPortTracker;
56
+ /**
57
+ * The `http:N,ssl:N,ws:N` value of a `ports=` key, in probe order, or
58
+ * undefined when no port is known.
59
+ */
60
+ export declare function formatHarnessServerPorts(ports: HarnessServerPorts | undefined): string | undefined;
@@ -0,0 +1,122 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Mochitest server ports for `fireforge test --jobs <n>`: placeholder
4
+ * listeners that push every parallel harness instance onto its own ports,
5
+ * and the parse of the ports each instance actually took.
6
+ *
7
+ * Desktop `mach mochitest` has no `--http-port`, `--ssl-port` or
8
+ * `--websocket-port`. Its `startServers` (`testing/mochitest/runtests.py`)
9
+ * bind-probes the HTTP server, ssltunnel and WebSocket ports (8888, 4443,
10
+ * 9988) and moves only a busy one to a free port; see
11
+ * `harness-port-fallback.ts`. Instances started at the same moment all
12
+ * pass that probe on a free 8888, then race to bind it: the losers die with
13
+ * `could not start server on port 8888` before any test runs. Holding the
14
+ * three ports for the whole parallel run makes every instance's probe fail,
15
+ * so each one falls back to an OS-assigned port of its own.
16
+ */
17
+ import { createServer } from 'node:net';
18
+ /**
19
+ * The ports `startServers` probes, in the order it probes them. Desktop
20
+ * mochitest cannot configure them, so the engine defaults are the ports.
21
+ */
22
+ const HARNESS_SERVER_PORTS = [
23
+ { key: 'http', port: 8888, label: 'HTTP test server' },
24
+ { key: 'ssl', port: 4443, label: 'ssltunnel' },
25
+ { key: 'ws', port: 9988, label: 'WebSocket server' },
26
+ ];
27
+ /** Listens on `127.0.0.1:port`, or resolves undefined when the bind fails. */
28
+ async function holdPort(port) {
29
+ return new Promise((resolve) => {
30
+ const server = createServer((socket) => {
31
+ // Nothing should connect: the harness only probes with a bind. Anyone
32
+ // who does gets a closed socket instead of a hang.
33
+ socket.destroy();
34
+ });
35
+ server.once('error', () => {
36
+ resolve(undefined);
37
+ });
38
+ server.listen({ port, host: '127.0.0.1', exclusive: true }, () => {
39
+ server.unref();
40
+ resolve(server);
41
+ });
42
+ });
43
+ }
44
+ /**
45
+ * Holds a placeholder listener on `127.0.0.1` on each mochitest server
46
+ * port, so the harness's busy-port probe moves every instance elsewhere.
47
+ * A port that cannot be bound is skipped, not refused: whatever holds it
48
+ * already makes the probe fail, which is all the placeholder is for (the
49
+ * preflight has already ruled out this checkout's own stale httpd).
50
+ *
51
+ * @param ports - Ports to hold (default: {@link HARNESS_SERVER_PORTS})
52
+ */
53
+ export async function reserveHarnessServerPorts(ports = HARNESS_SERVER_PORTS.map((spec) => spec.port)) {
54
+ const servers = (await Promise.all(ports.map(holdPort))).filter((server) => server !== undefined);
55
+ const held = servers.map((server) => {
56
+ const address = server.address();
57
+ return typeof address === 'object' && address !== null ? address.port : 0;
58
+ });
59
+ let released = false;
60
+ return {
61
+ held,
62
+ async release() {
63
+ if (released)
64
+ return;
65
+ released = true;
66
+ await Promise.all(servers.map((server) => new Promise((resolve) => {
67
+ server.close(() => {
68
+ resolve();
69
+ });
70
+ })));
71
+ },
72
+ };
73
+ }
74
+ const PORT_LINE_PATTERN = /\b(HTTP test server|ssltunnel|WebSocket server) port (\d+)(?: already in use, falling back to port (\d+))?/;
75
+ /**
76
+ * Records the port one output line reports, if it reports one: the
77
+ * fallback port of a "falling back" line, or the port itself otherwise.
78
+ *
79
+ * @param line - One line of harness output
80
+ * @param into - Collected ports; a later line for the same server wins
81
+ */
82
+ export function recordHarnessServerPortLine(line, into) {
83
+ const match = PORT_LINE_PATTERN.exec(line);
84
+ if (match === null)
85
+ return;
86
+ const spec = HARNESS_SERVER_PORTS.find((entry) => entry.label === match[1]);
87
+ const port = Number.parseInt(match[3] ?? match[2] ?? '', 10);
88
+ if (spec !== undefined && Number.isInteger(port))
89
+ into[spec.key] = port;
90
+ }
91
+ /**
92
+ * A tracker over the full output stream. The ports are printed at harness
93
+ * start, so the capped capture tail a long run keeps would lose them.
94
+ */
95
+ export function createHarnessPortTracker() {
96
+ const collected = {};
97
+ const pending = { stdout: '', stderr: '' };
98
+ return {
99
+ onChunk(stream, chunk) {
100
+ const lines = `${pending[stream]}${chunk}`.split(/\r?\n/);
101
+ pending[stream] = lines.pop() ?? '';
102
+ for (const line of lines)
103
+ recordHarnessServerPortLine(line, collected);
104
+ },
105
+ ports() {
106
+ const result = { ...collected };
107
+ recordHarnessServerPortLine(pending.stdout, result);
108
+ recordHarnessServerPortLine(pending.stderr, result);
109
+ return result;
110
+ },
111
+ };
112
+ }
113
+ /**
114
+ * The `http:N,ssl:N,ws:N` value of a `ports=` key, in probe order, or
115
+ * undefined when no port is known.
116
+ */
117
+ export function formatHarnessServerPorts(ports) {
118
+ if (ports === undefined)
119
+ return undefined;
120
+ const parts = HARNESS_SERVER_PORTS.flatMap(({ key }) => ports[key] === undefined ? [] : [`${key}:${ports[key]}`]);
121
+ return parts.length > 0 ? parts.join(',') : undefined;
122
+ }
@@ -1,4 +1,4 @@
1
- /** Filename of the history log at its pre-0.50 location, inside `patches/`. */
1
+ /** Filename of a history log inside `patches/`, which is moved out of the queue. */
2
2
  export declare const LEGACY_HISTORY_LOG_FILENAME = ".fireforge-history.jsonl";
3
3
  /** Default history log location, relative to the project root. */
4
4
  export declare const DEFAULT_HISTORY_LOG_PATH: string;
@@ -29,30 +29,30 @@ export declare function historyRecordIdentity(): HistoryRecordIdentity;
29
29
  */
30
30
  export declare function resolveHistoryLogPath(projectRoot: string): Promise<string>;
31
31
  /**
32
- * Moves a history log left at the legacy `patches/` path to `target`.
32
+ * Moves a history log found at the `patches/` path to `target`.
33
33
  *
34
34
  * Records are never dropped, whatever else is happening:
35
- * - The legacy file is first claimed by renaming it aside, so of two
35
+ * - The in-queue file is first claimed by renaming it aside, so of two
36
36
  * commands migrating at once exactly one does the work and neither
37
37
  * duplicates it.
38
38
  * - When `target` does not exist yet (the first run), the claimed file is
39
39
  * hard-linked into place, which cannot overwrite a log another command
40
40
  * created in the meantime, and keeps a large log a constant-time move.
41
- * - When both exist (an older FireForge appended to the legacy path after
42
- * a migration, or `history.path` changed), the legacy records are
41
+ * - When both exist (a log reappeared at the `patches/` path after a
42
+ * migration, or `history.path` changed), the in-queue records are
43
43
  * appended to the target in one write. They then follow newer records;
44
44
  * every record carries its own `ts`.
45
45
  *
46
46
  * A crash between the claim and the move leaves the records in a
47
- * `.migrating-*` file next to the legacy path, never deleted.
47
+ * `.migrating-*` file next to the in-queue path, never deleted.
48
48
  *
49
- * @param patchesDir - The patches directory that may hold a legacy log
49
+ * @param patchesDir - The patches directory that may hold an in-queue log
50
50
  * @param target - Absolute path of the history log
51
51
  */
52
52
  export declare function migrateLegacyHistoryLog(patchesDir: string, target: string): Promise<void>;
53
53
  /**
54
- * Appends one serialized record to the project's history log, migrating a
55
- * legacy log first.
54
+ * Appends one serialized record to the project's history log, first moving
55
+ * a log found inside `patches/`.
56
56
  *
57
57
  * @param patchesDir - The project's `patches/` directory; the project root is
58
58
  * its parent, as every caller derives it from `getProjectPaths`
@@ -2,12 +2,12 @@
2
2
  /**
3
3
  * Where the patch history log lives, and who wrote each record.
4
4
  *
5
- * The log used to be `patches/.fireforge-history.jsonl`, inside the tracked
6
- * patch queue, so it was committed with every export until consumers
7
- * gitignored it, and it grew to tens of megabytes. It now defaults to
8
- * `.fireforge/history.jsonl` (FireForge's own untracked data directory), or
9
- * `fireforge.json#history.path` when a project sets one. A log left at the
10
- * old path is moved on the first append.
5
+ * The log defaults to `.fireforge/history.jsonl` (FireForge's own untracked
6
+ * data directory), or `fireforge.json#history.path` when a project sets one.
7
+ * It stays out of the tracked patch queue because it grows to tens of
8
+ * megabytes and would otherwise be committed with every export. A log found
9
+ * at `patches/.fireforge-history.jsonl`, inside the queue, is moved on the
10
+ * first append.
11
11
  *
12
12
  * Several sessions sharing one checkout all append to the same log, so each
13
13
  * record carries the invocation that wrote it, plus a session hint, letting
@@ -21,7 +21,7 @@ import { ensureDir, pathExists } from '../utils/fs.js';
21
21
  import { info, verbose } from '../utils/logger.js';
22
22
  import { FIREFORGE_DIR } from './config-paths.js';
23
23
  import { isValidHistoryPath } from './config-validate-history.js';
24
- /** Filename of the history log at its pre-0.50 location, inside `patches/`. */
24
+ /** Filename of a history log inside `patches/`, which is moved out of the queue. */
25
25
  export const LEGACY_HISTORY_LOG_FILENAME = '.fireforge-history.jsonl';
26
26
  /** Default history log location, relative to the project root. */
27
27
  export const DEFAULT_HISTORY_LOG_PATH = join(FIREFORGE_DIR, 'history.jsonl');
@@ -87,24 +87,24 @@ export async function resolveHistoryLogPath(projectRoot) {
87
87
  return resolve(projectRoot, configured ?? DEFAULT_HISTORY_LOG_PATH);
88
88
  }
89
89
  /**
90
- * Moves a history log left at the legacy `patches/` path to `target`.
90
+ * Moves a history log found at the `patches/` path to `target`.
91
91
  *
92
92
  * Records are never dropped, whatever else is happening:
93
- * - The legacy file is first claimed by renaming it aside, so of two
93
+ * - The in-queue file is first claimed by renaming it aside, so of two
94
94
  * commands migrating at once exactly one does the work and neither
95
95
  * duplicates it.
96
96
  * - When `target` does not exist yet (the first run), the claimed file is
97
97
  * hard-linked into place, which cannot overwrite a log another command
98
98
  * created in the meantime, and keeps a large log a constant-time move.
99
- * - When both exist (an older FireForge appended to the legacy path after
100
- * a migration, or `history.path` changed), the legacy records are
99
+ * - When both exist (a log reappeared at the `patches/` path after a
100
+ * migration, or `history.path` changed), the in-queue records are
101
101
  * appended to the target in one write. They then follow newer records;
102
102
  * every record carries its own `ts`.
103
103
  *
104
104
  * A crash between the claim and the move leaves the records in a
105
- * `.migrating-*` file next to the legacy path, never deleted.
105
+ * `.migrating-*` file next to the in-queue path, never deleted.
106
106
  *
107
- * @param patchesDir - The patches directory that may hold a legacy log
107
+ * @param patchesDir - The patches directory that may hold an in-queue log
108
108
  * @param target - Absolute path of the history log
109
109
  */
110
110
  export async function migrateLegacyHistoryLog(patchesDir, target) {
@@ -149,8 +149,8 @@ export async function migrateLegacyHistoryLog(patchesDir, target) {
149
149
  'Nothing reads it back; it is an audit trail only.');
150
150
  }
151
151
  /**
152
- * Appends one serialized record to the project's history log, migrating a
153
- * legacy log first.
152
+ * Appends one serialized record to the project's history log, first moving
153
+ * a log found inside `patches/`.
154
154
  *
155
155
  * @param patchesDir - The project's `patches/` directory; the project root is
156
156
  * its parent, as every caller derives it from `getProjectPaths`
@@ -3,13 +3,13 @@ import type { HistoryEntry } from './destructive.js';
3
3
  * Appends a history entry for a mutation that has already committed, degrading
4
4
  * to a warning when the log cannot be written.
5
5
  *
6
- * Eleven call sites across `export-flow` and the `patch` subcommands had each
7
- * grown the same `try { await appendHistory(...) } catch { warn(...) }` block.
6
+ * This is the one `try { await appendHistory(...) } catch { warn(...) }`
7
+ * block for every call site across `export-flow` and the `patch` subcommands.
8
8
  * The swallow is intentional: the patch files and the manifest
9
9
  * are already on disk by the time this runs, so letting a failed log write
10
10
  * reject would report a completed, irreversible mutation as a failure and send
11
11
  * the operator looking for damage that is not there. Because that reasoning is
12
- * easy to lose in a copy, it now lives in one place.
12
+ * easy to lose in a copy, it lives in one place.
13
13
  * @param patchesDir - The project's `patches/` directory (the log itself lives
14
14
  * under `.fireforge/` or `history.path`; see `history-log-file.ts`).
15
15
  * @param entry - The entry to append.
@@ -6,13 +6,13 @@ import { appendHistory } from './destructive.js';
6
6
  * Appends a history entry for a mutation that has already committed, degrading
7
7
  * to a warning when the log cannot be written.
8
8
  *
9
- * Eleven call sites across `export-flow` and the `patch` subcommands had each
10
- * grown the same `try { await appendHistory(...) } catch { warn(...) }` block.
9
+ * This is the one `try { await appendHistory(...) } catch { warn(...) }`
10
+ * block for every call site across `export-flow` and the `patch` subcommands.
11
11
  * The swallow is intentional: the patch files and the manifest
12
12
  * are already on disk by the time this runs, so letting a failed log write
13
13
  * reject would report a completed, irreversible mutation as a failure and send
14
14
  * the operator looking for damage that is not there. Because that reasoning is
15
- * easy to lose in a copy, it now lives in one place.
15
+ * easy to lose in a copy, it lives in one place.
16
16
  * @param patchesDir - The project's `patches/` directory (the log itself lives
17
17
  * under `.fireforge/` or `history.path`; see `history-log-file.ts`).
18
18
  * @param entry - The entry to append.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Facts about the launchable build a test run will exercise: its build id
3
+ * and when its compiled code was last linked.
4
+ *
5
+ * A green or red against a binary older than the C++ under test reads as a
6
+ * verdict on that C++ unless the run says which binary it was. The
7
+ * build id goes on the verdict line (`build-id=`), and the link time anchors
8
+ * the stale-compiled preflight (`findCompiledSourcesNewerThanBinary` in
9
+ * `test-stale-check.ts`).
10
+ */
11
+ /**
12
+ * The `application.ini` beside a launchable binary: `Contents/Resources`
13
+ * of the `.app` bundle on macOS, the binary's own directory (`dist/bin`)
14
+ * elsewhere.
15
+ *
16
+ * @param binaryPath - Absolute launchable binary path
17
+ */
18
+ export declare function applicationIniPath(binaryPath: string): string;
19
+ /**
20
+ * Reads the `BuildID=` of the launchable build (the `moz:buildID` the
21
+ * browser reports). Undefined when the file or the key is missing, so a
22
+ * caller can simply leave the attribute off.
23
+ *
24
+ * @param engineDir - Absolute engine directory
25
+ * @param launchablePath - Engine-relative launchable binary path
26
+ */
27
+ export declare function readBuildId(engineDir: string, launchablePath: string): Promise<string | undefined>;
28
+ /**
29
+ * When the launchable build's compiled code was last linked: the newest
30
+ * mtime of the binary and the `libxul` beside it. Undefined when neither
31
+ * can be read.
32
+ *
33
+ * @param engineDir - Absolute engine directory
34
+ * @param launchablePath - Engine-relative launchable binary path
35
+ */
36
+ export declare function readLinkedAt(engineDir: string, launchablePath: string): Promise<{
37
+ mtimeMs: number;
38
+ path: string;
39
+ } | undefined>;
@@ -0,0 +1,85 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Facts about the launchable build a test run will exercise: its build id
4
+ * and when its compiled code was last linked.
5
+ *
6
+ * A green or red against a binary older than the C++ under test reads as a
7
+ * verdict on that C++ unless the run says which binary it was. The
8
+ * build id goes on the verdict line (`build-id=`), and the link time anchors
9
+ * the stale-compiled preflight (`findCompiledSourcesNewerThanBinary` in
10
+ * `test-stale-check.ts`).
11
+ */
12
+ import { stat } from 'node:fs/promises';
13
+ import { basename, dirname, join } from 'node:path';
14
+ import { getNodeErrorCode, toError } from '../utils/errors.js';
15
+ import { readText } from '../utils/fs.js';
16
+ import { verbose } from '../utils/logger.js';
17
+ /**
18
+ * The compiled libraries a build links next to the launchable binary.
19
+ * `libxul` carries nearly all compiled engine code (C++, Rust, generated
20
+ * bindings, prefs), so it, not the small launcher executable, is what a
21
+ * compiled-source edit relinks.
22
+ */
23
+ const LINKED_LIBRARY_NAMES = ['XUL', 'libxul.so', 'xul.dll'];
24
+ /**
25
+ * The `application.ini` beside a launchable binary: `Contents/Resources`
26
+ * of the `.app` bundle on macOS, the binary's own directory (`dist/bin`)
27
+ * elsewhere.
28
+ *
29
+ * @param binaryPath - Absolute launchable binary path
30
+ */
31
+ export function applicationIniPath(binaryPath) {
32
+ const binaryDir = dirname(binaryPath);
33
+ if (basename(binaryDir) === 'MacOS' && basename(dirname(binaryDir)) === 'Contents') {
34
+ return join(dirname(binaryDir), 'Resources', 'application.ini');
35
+ }
36
+ return join(binaryDir, 'application.ini');
37
+ }
38
+ /**
39
+ * Reads the `BuildID=` of the launchable build (the `moz:buildID` the
40
+ * browser reports). Undefined when the file or the key is missing, so a
41
+ * caller can simply leave the attribute off.
42
+ *
43
+ * @param engineDir - Absolute engine directory
44
+ * @param launchablePath - Engine-relative launchable binary path
45
+ */
46
+ export async function readBuildId(engineDir, launchablePath) {
47
+ const iniPath = applicationIniPath(join(engineDir, launchablePath));
48
+ let content;
49
+ try {
50
+ content = await readText(iniPath);
51
+ }
52
+ catch (error) {
53
+ if (getNodeErrorCode(error) !== 'ENOENT') {
54
+ verbose(`Build id: could not read ${iniPath} — ${toError(error).message}`);
55
+ }
56
+ return undefined;
57
+ }
58
+ const match = /^BuildID=(\S+)\s*$/m.exec(content);
59
+ return match?.[1];
60
+ }
61
+ /**
62
+ * When the launchable build's compiled code was last linked: the newest
63
+ * mtime of the binary and the `libxul` beside it. Undefined when neither
64
+ * can be read.
65
+ *
66
+ * @param engineDir - Absolute engine directory
67
+ * @param launchablePath - Engine-relative launchable binary path
68
+ */
69
+ export async function readLinkedAt(engineDir, launchablePath) {
70
+ const binary = join(engineDir, launchablePath);
71
+ const candidates = [binary, ...LINKED_LIBRARY_NAMES.map((name) => join(dirname(binary), name))];
72
+ let newest;
73
+ for (const path of candidates) {
74
+ try {
75
+ // stat follows the dist/bin symlinks to the linked objdir file.
76
+ const { mtimeMs } = await stat(path);
77
+ if (newest === undefined || mtimeMs > newest.mtimeMs)
78
+ newest = { mtimeMs, path };
79
+ }
80
+ catch {
81
+ // A platform's other library names are simply absent.
82
+ }
83
+ }
84
+ return newest;
85
+ }
@@ -247,9 +247,7 @@ async function probeWithLsof(port) {
247
247
  catch {
248
248
  // `lsof` missing, or its stdout did not parse. Either way the port state
249
249
  // is unknown, and an unknown port is reported as free so the probe never
250
- // blocks a run on its own uncertainty. (The errno test that used to sit
251
- // here returned the same value as this fallthrough, so it decided
252
- // nothing.)
250
+ // blocks a run on its own uncertainty.
253
251
  return { inUse: false };
254
252
  }
255
253
  }
@@ -77,11 +77,12 @@ export function getRules(binaryName, createManifest = false) {
77
77
  // scaffolded and wired via XPCSHELL_TESTS_MANIFESTS.
78
78
  pattern: /^(?!browser\/base\/content\/test\/)(.+)\/(test_[^/]+\.js)$/,
79
79
  isRegistered: (engineDir, dirRel, fileName) => isXpcshellTestRegistered(engineDir, dirRel, fileName),
80
- register: (engineDir, _after, dryRun, dirRel, fileName) => registerXpcshellTest(engineDir, dirRel, fileName, dryRun, createManifest),
80
+ register: (engineDir, after, dryRun, dirRel, fileName) => registerXpcshellTest(engineDir, dirRel, fileName, dryRun, createManifest, after),
81
81
  // Both groups are guaranteed by the pattern, so slice avoids dead
82
82
  // fallback branches.
83
83
  extractArgs: (m) => m.slice(1, 3),
84
- supportsAfter: false,
84
+ // `--after <entry>` places the section after that entry's block.
85
+ supportsAfter: true,
85
86
  },
86
87
  ];
87
88
  }