@hominis/fireforge 0.52.0 → 0.54.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 (248) hide show
  1. package/CHANGELOG.md +45 -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-remove.d.ts +26 -4
  54. package/dist/src/commands/patch/staged-dependency-remove.js +60 -23
  55. package/dist/src/commands/patch/staged-dependency.js +35 -24
  56. package/dist/src/commands/patch/tier.js +2 -2
  57. package/dist/src/commands/patch-rename-map.d.ts +14 -3
  58. package/dist/src/commands/patch-rename-map.js +21 -3
  59. package/dist/src/commands/re-export-adjacent.d.ts +4 -5
  60. package/dist/src/commands/re-export-lint.d.ts +4 -3
  61. package/dist/src/commands/re-export-lint.js +4 -3
  62. package/dist/src/commands/re-export-options.js +8 -2
  63. package/dist/src/commands/re-export-preflight.js +1 -2
  64. package/dist/src/commands/re-export-purity-scope.d.ts +1 -1
  65. package/dist/src/commands/re-export-purity-scope.js +13 -7
  66. package/dist/src/commands/re-export-retired-declarations.d.ts +22 -0
  67. package/dist/src/commands/re-export-retired-declarations.js +113 -0
  68. package/dist/src/commands/re-export-single.js +2 -2
  69. package/dist/src/commands/re-export-stale-registrations.d.ts +27 -6
  70. package/dist/src/commands/re-export-stale-registrations.js +65 -21
  71. package/dist/src/commands/re-export.js +4 -0
  72. package/dist/src/commands/rebase/continue.js +3 -3
  73. package/dist/src/commands/register.js +5 -4
  74. package/dist/src/commands/run.js +11 -4
  75. package/dist/src/commands/status.js +1 -1
  76. package/dist/src/commands/test-build-furnace.d.ts +1 -2
  77. package/dist/src/commands/test-build-furnace.js +1 -2
  78. package/dist/src/commands/test-build-phase.d.ts +8 -0
  79. package/dist/src/commands/test-build-phase.js +42 -7
  80. package/dist/src/commands/test-control-restore.d.ts +40 -0
  81. package/dist/src/commands/test-control-restore.js +86 -0
  82. package/dist/src/commands/test-dispatch.js +68 -7
  83. package/dist/src/commands/test-multi-verdict.js +1 -0
  84. package/dist/src/commands/test-orchestrate.d.ts +6 -0
  85. package/dist/src/commands/test-orchestrate.js +32 -1
  86. package/dist/src/commands/test-perf-scope.d.ts +1 -1
  87. package/dist/src/commands/test-perf-scope.js +1 -1
  88. package/dist/src/commands/test-prelock.d.ts +18 -8
  89. package/dist/src/commands/test-prelock.js +42 -14
  90. package/dist/src/commands/test-register.js +4 -3
  91. package/dist/src/commands/test-results.d.ts +10 -1
  92. package/dist/src/commands/test-results.js +20 -3
  93. package/dist/src/commands/test-run.d.ts +6 -0
  94. package/dist/src/commands/test-run.js +23 -4
  95. package/dist/src/commands/test-shards.d.ts +7 -3
  96. package/dist/src/commands/test-shards.js +24 -7
  97. package/dist/src/commands/test-stale-gate.d.ts +6 -1
  98. package/dist/src/commands/test-stale-gate.js +40 -2
  99. package/dist/src/commands/test-verdict.d.ts +5 -3
  100. package/dist/src/commands/test.js +14 -2
  101. package/dist/src/commands/token.js +2 -2
  102. package/dist/src/commands/verify.js +1 -1
  103. package/dist/src/commands/watch.js +1 -1
  104. package/dist/src/commands/wire.js +1 -1
  105. package/dist/src/core/build-audit-resolve.d.ts +4 -5
  106. package/dist/src/core/build-audit-resolve.js +4 -5
  107. package/dist/src/core/build-audit-skip.d.ts +2 -2
  108. package/dist/src/core/build-audit.d.ts +3 -4
  109. package/dist/src/core/build-baseline-types.d.ts +24 -3
  110. package/dist/src/core/build-baseline-validate.d.ts +30 -1
  111. package/dist/src/core/build-baseline-validate.js +127 -35
  112. package/dist/src/core/build-baseline.d.ts +8 -2
  113. package/dist/src/core/build-baseline.js +25 -16
  114. package/dist/src/core/build-jar-escalation.d.ts +4 -4
  115. package/dist/src/core/build-jar-escalation.js +4 -4
  116. package/dist/src/core/build-prepare.js +4 -4
  117. package/dist/src/core/config-validate.js +4 -4
  118. package/dist/src/core/coverage-extend.d.ts +5 -3
  119. package/dist/src/core/coverage-extend.js +8 -3
  120. package/dist/src/core/coverage-records.d.ts +4 -5
  121. package/dist/src/core/coverage-records.js +4 -5
  122. package/dist/src/core/destructive.d.ts +2 -2
  123. package/dist/src/core/destructive.js +2 -2
  124. package/dist/src/core/discard-baseline.d.ts +2 -2
  125. package/dist/src/core/discard-baseline.js +3 -3
  126. package/dist/src/core/engine-session-lock.d.ts +1 -1
  127. package/dist/src/core/engine-session-lock.js +1 -1
  128. package/dist/src/core/file-lock-owner.d.ts +5 -5
  129. package/dist/src/core/file-lock-owner.js +5 -5
  130. package/dist/src/core/file-lock-queue.js +6 -8
  131. package/dist/src/core/file-lock-shared.d.ts +4 -4
  132. package/dist/src/core/file-lock-shared.js +4 -4
  133. package/dist/src/core/file-lock.js +7 -10
  134. package/dist/src/core/firefox.js +2 -2
  135. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
  136. package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
  137. package/dist/src/core/furnace-apply.js +3 -3
  138. package/dist/src/core/furnace-operation.js +5 -5
  139. package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
  140. package/dist/src/core/furnace-registration-rename.d.ts +5 -5
  141. package/dist/src/core/furnace-registration-rename.js +5 -5
  142. package/dist/src/core/furnace-registration-validate.d.ts +2 -1
  143. package/dist/src/core/furnace-registration-validate.js +2 -1
  144. package/dist/src/core/furnace-registration.js +2 -2
  145. package/dist/src/core/git-base.js +4 -3
  146. package/dist/src/core/git-diff.js +1 -1
  147. package/dist/src/core/git.js +1 -1
  148. package/dist/src/core/harness-orphans.d.ts +6 -6
  149. package/dist/src/core/harness-orphans.js +6 -6
  150. package/dist/src/core/harness-port-reservation.d.ts +60 -0
  151. package/dist/src/core/harness-port-reservation.js +122 -0
  152. package/dist/src/core/history-log-file.d.ts +9 -9
  153. package/dist/src/core/history-log-file.js +15 -15
  154. package/dist/src/core/history-log.d.ts +3 -3
  155. package/dist/src/core/history-log.js +3 -3
  156. package/dist/src/core/launchable-build.d.ts +39 -0
  157. package/dist/src/core/launchable-build.js +85 -0
  158. package/dist/src/core/marionette-port.js +1 -3
  159. package/dist/src/core/moz-manifest-rules.js +3 -2
  160. package/dist/src/core/negative-control-compiled.d.ts +13 -3
  161. package/dist/src/core/negative-control-compiled.js +27 -5
  162. package/dist/src/core/negative-control-diff.js +31 -6
  163. package/dist/src/core/parser-fallback.d.ts +5 -5
  164. package/dist/src/core/parser-fallback.js +5 -5
  165. package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
  166. package/dist/src/core/patch-identifier-suggest.js +11 -4
  167. package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
  168. package/dist/src/core/patch-lint-checkjs.js +1 -1
  169. package/dist/src/core/patch-lint-css.js +2 -2
  170. package/dist/src/core/patch-lint-file-size.d.ts +5 -7
  171. package/dist/src/core/patch-lint-file-size.js +5 -7
  172. package/dist/src/core/patch-lint-ignore.d.ts +3 -3
  173. package/dist/src/core/patch-lint-ignore.js +3 -3
  174. package/dist/src/core/patch-lint-prettier.d.ts +4 -6
  175. package/dist/src/core/patch-lint-prettier.js +4 -6
  176. package/dist/src/core/patch-lint-projection.d.ts +1 -1
  177. package/dist/src/core/patch-lint-projection.js +1 -1
  178. package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
  179. package/dist/src/core/patch-lint-queue-types.js +2 -2
  180. package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
  181. package/dist/src/core/patch-manifest-consistency.js +2 -2
  182. package/dist/src/core/patch-manifest-io.d.ts +5 -1
  183. package/dist/src/core/patch-manifest-io.js +12 -10
  184. package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
  185. package/dist/src/core/patch-manifest-key-order.js +3 -4
  186. package/dist/src/core/patch-parse.d.ts +3 -3
  187. package/dist/src/core/patch-parse.js +3 -3
  188. package/dist/src/core/patch-policy.d.ts +1 -1
  189. package/dist/src/core/patch-policy.js +1 -1
  190. package/dist/src/core/patch-source-metadata.d.ts +1 -1
  191. package/dist/src/core/patch-source-metadata.js +1 -1
  192. package/dist/src/core/patch-subset.d.ts +5 -3
  193. package/dist/src/core/patch-subset.js +8 -7
  194. package/dist/src/core/patch-transform.d.ts +2 -2
  195. package/dist/src/core/patch-transform.js +2 -2
  196. package/dist/src/core/rebase-session.js +4 -6
  197. package/dist/src/core/register-xpcshell-test.d.ts +28 -7
  198. package/dist/src/core/register-xpcshell-test.js +76 -27
  199. package/dist/src/core/run-log.d.ts +8 -8
  200. package/dist/src/core/run-log.js +8 -8
  201. package/dist/src/core/status-classify.d.ts +1 -2
  202. package/dist/src/core/status-classify.js +1 -2
  203. package/dist/src/core/test-harness-crash.d.ts +5 -3
  204. package/dist/src/core/test-harness-crash.js +13 -14
  205. package/dist/src/core/test-harness-infra.d.ts +32 -0
  206. package/dist/src/core/test-harness-infra.js +69 -0
  207. package/dist/src/core/test-harness-signature.d.ts +6 -0
  208. package/dist/src/core/test-stale-check.d.ts +38 -0
  209. package/dist/src/core/test-stale-check.js +82 -1
  210. package/dist/src/core/test-stall-triage.js +5 -7
  211. package/dist/src/core/token-category.js +2 -2
  212. package/dist/src/core/token-inventory.js +3 -3
  213. package/dist/src/core/token-manager.d.ts +2 -2
  214. package/dist/src/core/token-manager.js +5 -7
  215. package/dist/src/core/token-variant.d.ts +6 -6
  216. package/dist/src/core/token-variant.js +7 -7
  217. package/dist/src/core/typecheck-shim.js +1 -1
  218. package/dist/src/core/wire-destroy.d.ts +1 -1
  219. package/dist/src/core/wire-destroy.js +1 -1
  220. package/dist/src/core/wire-dom-fragment.js +1 -1
  221. package/dist/src/core/wire-init.d.ts +1 -1
  222. package/dist/src/core/wire-init.js +1 -1
  223. package/dist/src/core/wire-subscript.d.ts +1 -1
  224. package/dist/src/core/wire-subscript.js +1 -1
  225. package/dist/src/core/wire-utils.d.ts +2 -2
  226. package/dist/src/core/wire-utils.js +3 -3
  227. package/dist/src/errors/base.d.ts +1 -1
  228. package/dist/src/errors/base.js +1 -1
  229. package/dist/src/errors/rebase.d.ts +1 -1
  230. package/dist/src/errors/rebase.js +1 -1
  231. package/dist/src/types/commands/options.d.ts +38 -3
  232. package/dist/src/types/config.d.ts +5 -7
  233. package/dist/src/utils/options.js +2 -2
  234. package/dist/src/utils/process.js +2 -2
  235. package/dist/src/utils/regex.d.ts +2 -2
  236. package/dist/src/utils/regex.js +3 -3
  237. package/dist/src/utils/sleep.d.ts +5 -6
  238. package/dist/src/utils/sleep.js +5 -6
  239. package/dist/src/utils/validation.js +2 -2
  240. package/docs/configuration.md +28 -23
  241. package/docs/exit-codes.md +8 -11
  242. package/docs/furnace.md +3 -3
  243. package/docs/lifecycle-invariants.md +36 -45
  244. package/docs/machine-output.md +41 -24
  245. package/docs/patch-workflow.md +140 -29
  246. package/docs/run-logs.md +8 -9
  247. package/docs/testing.md +114 -48
  248. package/package.json +1 -1
@@ -29,9 +29,8 @@ const REBASE_PATCH_STATUSES = [
29
29
  /**
30
30
  * Drops payload that does not belong to an entry's status.
31
31
  *
32
- * Applied on read so a session written by an older FireForge, where
33
- * `rebase --continue` left `error`/`conflictingFiles` on a now-`resolved`
34
- * entry, loads cleanly and is corrected, instead of being refused by a
32
+ * Applied on read so a session whose `resolved` entry still carries
33
+ * `error`/`conflictingFiles` loads cleanly and is corrected, instead of being refused by a
35
34
  * stricter validator mid-rebase.
36
35
  *
37
36
  * @param entry - A structurally valid entry, possibly carrying stale payload
@@ -145,11 +144,10 @@ export async function readRebaseSession(projectRoot) {
145
144
  if (!isValidSession(data)) {
146
145
  return { present: true, valid: false, reason: 'the file is not a valid rebase session' };
147
146
  }
148
- // Normalize on read: an older session can carry a failure's
147
+ // Normalize on read: a session can carry a failure's
149
148
  // `error`/`conflictingFiles` on an entry whose status has since been
150
149
  // flipped to `resolved`. Correcting it here is what lets the validator
151
- // stay permissive, so an in-flight rebase from an older FireForge still
152
- // loads.
150
+ // stay permissive, so such an in-flight rebase still loads.
153
151
  return {
154
152
  present: true,
155
153
  valid: true,
@@ -2,7 +2,8 @@
2
2
  * xpcshell test-file registration. Without it, xpcshell test files (e.g.
3
3
  * `<module-dir>/test/unit/test_*.js`) are rejected by `fireforge register`
4
4
  * as "Unknown file pattern". This writer inserts the `["test_*.js"]` section
5
- * into the directory's `xpcshell.toml` (alphabetically, idempotently). With
5
+ * into the directory's `xpcshell.toml` (alphabetically or after the
6
+ * `--after` entry, idempotently). With
6
7
  * `--create-manifest` it also creates the manifest and wires
7
8
  * `XPCSHELL_TESTS_MANIFESTS` into the nearest moz.build.
8
9
  */
@@ -14,16 +15,36 @@ import type { RegisterResult } from './register-result.js';
14
15
  * missing rather than the file as unregistered).
15
16
  */
16
17
  export declare function isXpcshellTestRegistered(engineDir: string, dirRel: string, fileName: string): Promise<boolean>;
18
+ /** Where a new section goes and what placed it there. */
19
+ export interface XpcshellSectionPlacement {
20
+ /** The manifest content with the section inserted. */
21
+ content: string;
22
+ /** The `["…"]` header of the `--after` entry the section follows. */
23
+ previousEntry?: string | undefined;
24
+ /** `--after` named no entry, so the section went in sort order. */
25
+ afterFallback?: boolean | undefined;
26
+ }
17
27
  /**
18
- * Inserts the `["<fileName>"]` section into the manifest content in
19
- * mozbuild's case-insensitive sort order relative to the other test
20
- * sections, keeping one blank line between sections. Exported for direct
21
- * unit testing.
28
+ * Places the `["<fileName>"]` section in the manifest content.
29
+ *
30
+ * Without `after`, the section goes in mozbuild's case-insensitive sort
31
+ * order relative to the other test sections. With `after`, it goes right
32
+ * after the named entry's whole block (its keys included). Either way it
33
+ * is inserted ahead of the `#` comment lines that introduce the next
34
+ * entry, never between a comment and its entry, keeping one
35
+ * blank line between sections.
22
36
  */
23
- export declare function insertXpcshellManifestSection(content: string, fileName: string): string;
37
+ export declare function placeXpcshellManifestSection(content: string, fileName: string, after?: string): XpcshellSectionPlacement;
38
+ /**
39
+ * Inserts the `["<fileName>"]` section into the manifest content; see
40
+ * {@link placeXpcshellManifestSection}. Exported for direct unit testing.
41
+ *
42
+ * @internal
43
+ */
44
+ export declare function insertXpcshellManifestSection(content: string, fileName: string, after?: string): string;
24
45
  /**
25
46
  * Registers an xpcshell test file in its directory's `xpcshell.toml`.
26
47
  * With `createManifest`, a missing manifest is scaffolded and wired into
27
48
  * the nearest moz.build via `XPCSHELL_TESTS_MANIFESTS`.
28
49
  */
29
- export declare function registerXpcshellTest(engineDir: string, dirRel: string, fileName: string, dryRun?: boolean, createManifest?: boolean): Promise<RegisterResult>;
50
+ export declare function registerXpcshellTest(engineDir: string, dirRel: string, fileName: string, dryRun?: boolean, createManifest?: boolean, after?: string): Promise<RegisterResult>;
@@ -3,7 +3,8 @@
3
3
  * xpcshell test-file registration. Without it, xpcshell test files (e.g.
4
4
  * `<module-dir>/test/unit/test_*.js`) are rejected by `fireforge register`
5
5
  * as "Unknown file pattern". This writer inserts the `["test_*.js"]` section
6
- * into the directory's `xpcshell.toml` (alphabetically, idempotently). With
6
+ * into the directory's `xpcshell.toml` (alphabetically or after the
7
+ * `--after` entry, idempotently). With
7
8
  * `--create-manifest` it also creates the manifest and wires
8
9
  * `XPCSHELL_TESTS_MANIFESTS` into the nearest moz.build.
9
10
  */
@@ -32,40 +33,82 @@ export async function isXpcshellTestRegistered(engineDir, dirRel, fileName) {
32
33
  const content = await readText(manifestPath);
33
34
  return content.includes(`["${fileName}"]`);
34
35
  }
36
+ const SECTION_HEADER = /^\["([^"]+)"\]\s*$/;
35
37
  /**
36
- * Inserts the `["<fileName>"]` section into the manifest content in
37
- * mozbuild's case-insensitive sort order relative to the other test
38
- * sections, keeping one blank line between sections. Exported for direct
39
- * unit testing.
38
+ * The line where the block headed at `headerIndex` starts: the header
39
+ * itself, or the first of the `#` comment lines directly above it. A
40
+ * comment describes the entry below it, so nothing is inserted between
41
+ * them.
40
42
  */
41
- export function insertXpcshellManifestSection(content, fileName) {
43
+ function blockStart(lines, headerIndex) {
44
+ let start = headerIndex;
45
+ while (start > 0 && (lines[start - 1] ?? '').trimStart().startsWith('#'))
46
+ start -= 1;
47
+ return start;
48
+ }
49
+ /** Index of the section header `after` names: exact name first, then a substring. */
50
+ function findAfterHeader(lines, after) {
51
+ const headers = lines
52
+ .map((line, index) => ({ index, line, name: SECTION_HEADER.exec(line)?.[1] }))
53
+ .filter((header) => header.name !== undefined);
54
+ const exact = headers.find((header) => header.name === after || header.line.trim() === after);
55
+ return (exact ?? headers.find((header) => header.line.includes(after)))?.index ?? -1;
56
+ }
57
+ function spliceSection(lines, index, fileName) {
58
+ if (index === -1) {
59
+ // Append after the last section (or at end of file when there are no
60
+ // sections yet), keeping exactly one trailing newline.
61
+ while (lines.length > 0 && (lines.at(-1) ?? '').trim() === '')
62
+ lines.pop();
63
+ return [...lines, '', `["${fileName}"]`, ''].join('\n');
64
+ }
65
+ lines.splice(index, 0, `["${fileName}"]`, '');
66
+ return lines.join('\n');
67
+ }
68
+ /**
69
+ * Places the `["<fileName>"]` section in the manifest content.
70
+ *
71
+ * Without `after`, the section goes in mozbuild's case-insensitive sort
72
+ * order relative to the other test sections. With `after`, it goes right
73
+ * after the named entry's whole block (its keys included). Either way it
74
+ * is inserted ahead of the `#` comment lines that introduce the next
75
+ * entry, never between a comment and its entry, keeping one
76
+ * blank line between sections.
77
+ */
78
+ export function placeXpcshellManifestSection(content, fileName, after) {
42
79
  const lines = content.split('\n');
43
- const sectionHeaderPattern = /^\["([^"]+)"\]\s*$/;
44
- let insertIndex = -1;
45
- for (let i = 0; i < lines.length; i += 1) {
46
- const match = sectionHeaderPattern.exec(lines[i] ?? '');
47
- if (!match)
48
- continue;
49
- if (insertIndex === -1 && mozbuildSortCompare(fileName, match[1] ?? '') < 0) {
50
- insertIndex = i;
80
+ const headerIndexes = lines.flatMap((line, index) => (SECTION_HEADER.test(line) ? [index] : []));
81
+ if (after !== undefined) {
82
+ const afterIndex = findAfterHeader(lines, after);
83
+ if (afterIndex !== -1) {
84
+ const next = headerIndexes.find((index) => index > afterIndex);
85
+ return {
86
+ content: spliceSection(lines, next === undefined ? -1 : blockStart(lines, next), fileName),
87
+ previousEntry: (lines[afterIndex] ?? '').trim(),
88
+ };
51
89
  }
52
90
  }
53
- if (insertIndex !== -1) {
54
- lines.splice(insertIndex, 0, `["${fileName}"]`, '');
55
- return lines.join('\n');
56
- }
57
- // Append after the last section (or at end of file when there are no
58
- // sections yet), keeping exactly one trailing newline.
59
- while (lines.length > 0 && (lines.at(-1) ?? '').trim() === '')
60
- lines.pop();
61
- return [...lines, '', `["${fileName}"]`, ''].join('\n');
91
+ const before = headerIndexes.find((index) => mozbuildSortCompare(fileName, SECTION_HEADER.exec(lines[index] ?? '')?.[1] ?? '') < 0);
92
+ return {
93
+ content: spliceSection(lines, before === undefined ? -1 : blockStart(lines, before), fileName),
94
+ ...(after !== undefined ? { afterFallback: true } : {}),
95
+ };
96
+ }
97
+ /**
98
+ * Inserts the `["<fileName>"]` section into the manifest content; see
99
+ * {@link placeXpcshellManifestSection}. Exported for direct unit testing.
100
+ *
101
+ * @internal
102
+ */
103
+ export function insertXpcshellManifestSection(content, fileName, after) {
104
+ return placeXpcshellManifestSection(content, fileName, after).content;
62
105
  }
63
106
  /**
64
107
  * Registers an xpcshell test file in its directory's `xpcshell.toml`.
65
108
  * With `createManifest`, a missing manifest is scaffolded and wired into
66
109
  * the nearest moz.build via `XPCSHELL_TESTS_MANIFESTS`.
67
110
  */
68
- export async function registerXpcshellTest(engineDir, dirRel, fileName, dryRun = false, createManifest = false) {
111
+ export async function registerXpcshellTest(engineDir, dirRel, fileName, dryRun = false, createManifest = false, after) {
69
112
  const manifestRel = `${dirRel}/xpcshell.toml`;
70
113
  const manifestPath = join(engineDir, manifestRel);
71
114
  const entry = `["${fileName}"]`;
@@ -86,9 +129,15 @@ export async function registerXpcshellTest(engineDir, dirRel, fileName, dryRun =
86
129
  if (content.includes(entry)) {
87
130
  return { manifest: manifestRel, entry, skipped: true };
88
131
  }
89
- const updated = insertXpcshellManifestSection(content, fileName);
132
+ const placement = placeXpcshellManifestSection(content, fileName, after);
90
133
  if (!dryRun) {
91
- await writeText(manifestPath, updated);
134
+ await writeText(manifestPath, placement.content);
92
135
  }
93
- return { manifest: manifestRel, entry, skipped: false };
136
+ return {
137
+ manifest: manifestRel,
138
+ entry,
139
+ skipped: false,
140
+ previousEntry: placement.previousEntry,
141
+ afterFallback: placement.afterFallback,
142
+ };
94
143
  }
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Per-run output logs under `.fireforge/logs/`.
3
3
  *
4
- * A run's diagnosis used to exist in exactly one place: the terminal. Piping
5
- * through `tail`/`grep`, the ergonomic default when output is long, keeps
6
- * the summary and discards the `TEST-UNEXPECTED-FAIL` lines that say what
7
- * broke, and it launders the exit code besides. The operator rule ("never
8
- * pipe a run") was written down three times downstream and broken after each
9
- * writing, so this is not an operator problem: the tool is the only party
10
- * that can make the log survive the mistake.
4
+ * Without a log, a run's diagnosis exists in exactly one place: the terminal.
5
+ * Piping through `tail`/`grep`, the ergonomic default when output is long,
6
+ * keeps the summary and discards the `TEST-UNEXPECTED-FAIL` lines that say
7
+ * what broke, and it launders the exit code besides. An operator rule
8
+ * ("never pipe a run") does not hold in practice, so this is not an operator
9
+ * problem: the tool is the only party that can make the log survive the
10
+ * mistake.
11
11
  *
12
12
  * So `test` and `build` write their own complete copy as they stream, and the
13
13
  * `FIREFORGE-VERDICT:` line names the path. A piped, truncated, or
@@ -16,7 +16,7 @@
16
16
  * Everything here is best-effort. A log is a diagnostic aid, and a run must
17
17
  * never fail because one could not be opened, written, or pruned.
18
18
  * Every entry point swallows its errors and degrades to "no log", which is
19
- * exactly the behaviour that existed before this module.
19
+ * exactly the behaviour of a run without this module.
20
20
  */
21
21
  /** A run log sink. `write` never throws. */
22
22
  export interface RunLog {
@@ -2,13 +2,13 @@
2
2
  /**
3
3
  * Per-run output logs under `.fireforge/logs/`.
4
4
  *
5
- * A run's diagnosis used to exist in exactly one place: the terminal. Piping
6
- * through `tail`/`grep`, the ergonomic default when output is long, keeps
7
- * the summary and discards the `TEST-UNEXPECTED-FAIL` lines that say what
8
- * broke, and it launders the exit code besides. The operator rule ("never
9
- * pipe a run") was written down three times downstream and broken after each
10
- * writing, so this is not an operator problem: the tool is the only party
11
- * that can make the log survive the mistake.
5
+ * Without a log, a run's diagnosis exists in exactly one place: the terminal.
6
+ * Piping through `tail`/`grep`, the ergonomic default when output is long,
7
+ * keeps the summary and discards the `TEST-UNEXPECTED-FAIL` lines that say
8
+ * what broke, and it launders the exit code besides. An operator rule
9
+ * ("never pipe a run") does not hold in practice, so this is not an operator
10
+ * problem: the tool is the only party that can make the log survive the
11
+ * mistake.
12
12
  *
13
13
  * So `test` and `build` write their own complete copy as they stream, and the
14
14
  * `FIREFORGE-VERDICT:` line names the path. A piped, truncated, or
@@ -17,7 +17,7 @@
17
17
  * Everything here is best-effort. A log is a diagnostic aid, and a run must
18
18
  * never fail because one could not be opened, written, or pruned.
19
19
  * Every entry point swallows its errors and degrades to "no log", which is
20
- * exactly the behaviour that existed before this module.
20
+ * exactly the behaviour of a run without this module.
21
21
  */
22
22
  import { createWriteStream } from 'node:fs';
23
23
  import { mkdir, readdir, stat, unlink } from 'node:fs/promises';
@@ -63,8 +63,7 @@ export interface ClassifiedFile extends StatusFile {
63
63
  * Builds the file → owning-patch-filenames multimap from manifest rows.
64
64
  * Single source of truth for cross-patch claim detection: status
65
65
  * classification, verify's cross-claim check, and the ownership table all
66
- * consume this builder (they used to each rebuild it, one drift away from
67
- * disagreeing about what "claimed" means).
66
+ * consume this builder, so they cannot disagree about what "claimed" means.
68
67
  */
69
68
  export declare function buildPatchClaims(manifestPatches: ReadonlyArray<{
70
69
  filename: string;
@@ -20,8 +20,7 @@ import { classifyBinaryOwnedFile } from './status-binary.js';
20
20
  * Builds the file → owning-patch-filenames multimap from manifest rows.
21
21
  * Single source of truth for cross-patch claim detection: status
22
22
  * classification, verify's cross-claim check, and the ownership table all
23
- * consume this builder (they used to each rebuild it, one drift away from
24
- * disagreeing about what "claimed" means).
23
+ * consume this builder, so they cannot disagree about what "claimed" means.
25
24
  */
26
25
  export function buildPatchClaims(manifestPatches) {
27
26
  const ownersByPath = new Map();
@@ -23,6 +23,7 @@
23
23
  */
24
24
  export type { HarnessCrashSignature } from './test-harness-signature.js';
25
25
  import { type HarnessCrashSignature, type IncompleteRunEvidence } from './test-harness-signature.js';
26
+ export { buildHarnessCrashMessage } from './test-harness-infra.js';
26
27
  export { headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint } from './test-stall-triage.js';
27
28
  /** How a completed harness run should be interpreted. */
28
29
  export type HarnessRunClassification = 'tests-ran-ok' | 'test-failures' | 'harness-crash' | 'harness-arguments' | 'no-tests';
@@ -160,7 +161,10 @@ export declare function detectHarnessCrashSignature(output: string): HarnessCras
160
161
  * 1. A recognized crash signature wins regardless of exit code (the
161
162
  * shutdown re-entry shape exits non-zero on an otherwise green run,
162
163
  * and the hang shape can even exit zero with a `Passed: 0` summary).
163
- * 2. No execution signal with explicit paths requested means no test ran:
164
+ * 2. No execution signal with explicit paths requested means no test ran.
165
+ * A mochitest server that failed to start explains that, and is a
166
+ * retriable infrastructure failure (`harness-crash` with
167
+ * `reason=server-startup` on the verdict line). Otherwise it is
164
168
  * `no-tests`, even when the exit code is zero. The execution signal is
165
169
  * a `TEST-START` line (generic `mach test` / browser-chrome dispatch)
166
170
  * or the suite-specific xpcshell result-summary block (the xpcshell
@@ -206,8 +210,6 @@ export declare function formatFireforgeVerdictLine(verdict: HarnessRunVerdict, s
206
210
  * pass, regardless of how green the embedded counts look).
207
211
  */
208
212
  export declare function buildGreenSummaryRejectedMessage(rejection: GreenSummaryRejection, exitCode: number): string;
209
- /** Builds the operator-facing failure message after retries are exhausted. */
210
- export declare function buildHarnessCrashMessage(signature: HarnessCrashSignature, attempts: number, commandLabel?: string): string;
211
213
  /**
212
214
  * True when a harness run died on SIGSEGV having produced no test evidence
213
215
  * whatsoever.
@@ -23,9 +23,13 @@
23
23
  * reporting phantom test failures (or phantom passes).
24
24
  */
25
25
  import { hasKnownTeardownNoise } from './mach-known-noise-filter.js';
26
+ import { detectServerStartupFailure } from './test-harness-infra.js';
26
27
  import { truncateEvidence, unmarkedFailureEvidenceNote, } from './test-harness-verdict-notes.js';
27
28
  import { classifyProfileArgumentFailure } from './test-profile-files.js';
28
29
  import { formatIncompleteSuffix, formatShardsSuffix, } from './test-harness-signature.js';
30
+ // The operator message after exhausted retries lives beside the
31
+ // server-startup shape in `test-harness-infra.ts`.
32
+ export { buildHarnessCrashMessage } from './test-harness-infra.js';
29
33
  // The no-output-stall triage text lives in `test-stall-triage.ts` (it is
30
34
  // operator advice, not classification) and is re-exported here so the
31
35
  // command layer keeps importing its harness diagnostics from one module.
@@ -458,7 +462,10 @@ export function detectHarnessCrashSignature(output) {
458
462
  * 1. A recognized crash signature wins regardless of exit code (the
459
463
  * shutdown re-entry shape exits non-zero on an otherwise green run,
460
464
  * and the hang shape can even exit zero with a `Passed: 0` summary).
461
- * 2. No execution signal with explicit paths requested means no test ran:
465
+ * 2. No execution signal with explicit paths requested means no test ran.
466
+ * A mochitest server that failed to start explains that, and is a
467
+ * retriable infrastructure failure (`harness-crash` with
468
+ * `reason=server-startup` on the verdict line). Otherwise it is
462
469
  * `no-tests`, even when the exit code is zero. The execution signal is
463
470
  * a `TEST-START` line (generic `mach test` / browser-chrome dispatch)
464
471
  * or the suite-specific xpcshell result-summary block (the xpcshell
@@ -512,7 +519,10 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
512
519
  }
513
520
  const ranTests = hasExecutionSignal(output);
514
521
  if (!ranTests && requestedPaths.length > 0) {
515
- return { kind: 'no-tests', ...counts };
522
+ const serverStartup = detectServerStartupFailure(output);
523
+ return serverStartup === undefined
524
+ ? { kind: 'no-tests', ...counts }
525
+ : { kind: 'harness-crash', signature: serverStartup, ...counts };
516
526
  }
517
527
  if (exitCode === 0) {
518
528
  return { kind: 'tests-ran-ok', ...counts };
@@ -663,7 +673,7 @@ export function formatFireforgeVerdictLine(verdict, shards) {
663
673
  const tail = `${formatShardsSuffix(shards)}${verdict.note !== undefined ? ` (${verdict.note})` : ''}`;
664
674
  if (verdict.kind === 'tests-ran-ok')
665
675
  return `FIREFORGE-VERDICT: PASS${counts}${tail}`;
666
- const reason = verdict.kind === 'harness-crash' ? 'crash' : verdict.kind;
676
+ const reason = verdict.kind === 'harness-crash' ? (verdict.signature?.verdictReason ?? 'crash') : verdict.kind;
667
677
  return `FIREFORGE-VERDICT: FAIL reason=${reason}${counts}${formatIncompleteSuffix(verdict.incomplete)}${tail}`;
668
678
  }
669
679
  /**
@@ -690,17 +700,6 @@ export function buildGreenSummaryRejectedMessage(rejection, exitCode) {
690
700
  'FAILED run — check the crash/truncation point in the output above, then re-run the ' +
691
701
  'remaining files.');
692
702
  }
693
- /** Builds the operator-facing failure message after retries are exhausted. */
694
- export function buildHarnessCrashMessage(signature, attempts, commandLabel = 'mach test') {
695
- return (`${commandLabel} crashed in the harness itself (not in your tests) on all ${attempts} attempt(s).\n\n` +
696
- `Detected shape: ${signature.reason}\n` +
697
- `Evidence line: ${signature.line}\n\n` +
698
- 'This failure mode is environmental (mozlog resource monitor / psutil on macOS, focus-stall ' +
699
- 'shutdown re-entry, or a pre-test hang) rather than a test regression. Re-run the command, ' +
700
- 'raise the retry budget with --harness-retries <n>, or run the file in isolation. ' +
701
- 'If it persists across many runs, inspect the mach virtualenv (mach resyncs psutil on its own; ' +
702
- 'patching it manually does not stick).');
703
- }
704
703
  /**
705
704
  * Exit codes that mean "the child died on SIGSEGV": Node reports a
706
705
  * signal-killed child as the negated signal number, while a shell layer
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Harness failures that are the environment's, not the tests': the
3
+ * operator message for a recognized harness crash after its retries ran
4
+ * out, and the mochitest server that never came up.
5
+ *
6
+ * A server never comes up when the harness's httpd cannot bind its port,
7
+ * typically because a parallel `--jobs` instance, a sibling checkout or a
8
+ * leftover httpd held it (see `harness-port-reservation.ts`). Without this
9
+ * shape the run read as `no-tests`, a finding about the
10
+ * request, and was not retried. It is an infrastructure failure instead:
11
+ * `classifyHarnessRun` reports it as a `harness-crash` carrying
12
+ * `verdictReason: 'server-startup'`, so the harness-crash retry loop
13
+ * (`--harness-retries`) reruns the shard, and a run that exhausts the
14
+ * budget says `reason=server-startup` on its verdict line.
15
+ */
16
+ import type { HarnessCrashSignature } from './test-harness-signature.js';
17
+ /**
18
+ * Detects a mochitest server that failed to start. The caller checks that
19
+ * no test ran first: both lines precede any test.
20
+ *
21
+ * @param output - Captured mach stdout and stderr
22
+ * @returns The infrastructure signature, or undefined
23
+ */
24
+ export declare function detectServerStartupFailure(output: string): HarnessCrashSignature | undefined;
25
+ /**
26
+ * Builds the operator-facing failure message after retries are exhausted.
27
+ *
28
+ * @param signature - The recognized shape
29
+ * @param attempts - Attempts made
30
+ * @param commandLabel - The mach command, for the message
31
+ */
32
+ export declare function buildHarnessCrashMessage(signature: HarnessCrashSignature, attempts: number, commandLabel?: string): string;
@@ -0,0 +1,69 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Harness failures that are the environment's, not the tests': the
4
+ * operator message for a recognized harness crash after its retries ran
5
+ * out, and the mochitest server that never came up.
6
+ *
7
+ * A server never comes up when the harness's httpd cannot bind its port,
8
+ * typically because a parallel `--jobs` instance, a sibling checkout or a
9
+ * leftover httpd held it (see `harness-port-reservation.ts`). Without this
10
+ * shape the run read as `no-tests`, a finding about the
11
+ * request, and was not retried. It is an infrastructure failure instead:
12
+ * `classifyHarnessRun` reports it as a `harness-crash` carrying
13
+ * `verdictReason: 'server-startup'`, so the harness-crash retry loop
14
+ * (`--harness-retries`) reruns the shard, and a run that exhausts the
15
+ * budget says `reason=server-startup` on its verdict line.
16
+ */
17
+ /**
18
+ * The harness's own lines for the shape: the httpd's bind failure
19
+ * (`!!! could not start server on port 8888`) and `runtests.py`'s wait for
20
+ * it (`Timed out while waiting for server startup`).
21
+ */
22
+ const SERVER_STARTUP_FAILURE_PATTERNS = [
23
+ /could not start server on port \d+/,
24
+ /Timed out while waiting for server startup/,
25
+ ];
26
+ /**
27
+ * Detects a mochitest server that failed to start. The caller checks that
28
+ * no test ran first: both lines precede any test.
29
+ *
30
+ * @param output - Captured mach stdout and stderr
31
+ * @returns The infrastructure signature, or undefined
32
+ */
33
+ export function detectServerStartupFailure(output) {
34
+ for (const line of output.split(/\r?\n/)) {
35
+ if (SERVER_STARTUP_FAILURE_PATTERNS.some((pattern) => pattern.test(line))) {
36
+ return {
37
+ reason: 'mochitest server failed to start (server-startup)',
38
+ line: line.trim(),
39
+ verdictReason: 'server-startup',
40
+ };
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+ /**
46
+ * Builds the operator-facing failure message after retries are exhausted.
47
+ *
48
+ * @param signature - The recognized shape
49
+ * @param attempts - Attempts made
50
+ * @param commandLabel - The mach command, for the message
51
+ */
52
+ export function buildHarnessCrashMessage(signature, attempts, commandLabel = 'mach test') {
53
+ if (signature.verdictReason !== 'server-startup') {
54
+ return (`${commandLabel} crashed in the harness itself (not in your tests) on all ${attempts} attempt(s).\n\n` +
55
+ `Detected shape: ${signature.reason}\n` +
56
+ `Evidence line: ${signature.line}\n\n` +
57
+ 'This failure mode is environmental (mozlog resource monitor / psutil on macOS, focus-stall ' +
58
+ 'shutdown re-entry, or a pre-test hang) rather than a test regression. Re-run the command, ' +
59
+ 'raise the retry budget with --harness-retries <n>, or run the file in isolation. ' +
60
+ 'If it persists across many runs, inspect the mach virtualenv (mach resyncs psutil on its own; ' +
61
+ 'patching it manually does not stick).');
62
+ }
63
+ return (`${commandLabel} could not start a mochitest server on all ${attempts} attempt(s), so no ` +
64
+ 'test ran (an infrastructure failure, not a test result).\n\n' +
65
+ `Evidence line: ${signature.line}\n\n` +
66
+ 'Another harness (a parallel --jobs instance, a sibling checkout, or a leftover httpd) ' +
67
+ 'most likely held the server port when this one started. Re-run the shard, raise the ' +
68
+ 'retry budget with --harness-retries <n>, or check "lsof -nP -iTCP:8888".');
69
+ }
@@ -19,6 +19,12 @@ export interface HarnessCrashSignature {
19
19
  reason: string;
20
20
  /** The concrete output line the recognition was based on. */
21
21
  line: string;
22
+ /**
23
+ * The verdict line's `reason=` for an infrastructure shape that is not a
24
+ * crash of the harness: `server-startup` when a mochitest server never
25
+ * came up. Unset: `reason=crash`.
26
+ */
27
+ verdictReason?: 'server-startup';
22
28
  }
23
29
  /**
24
30
  * The verdict line's `started=<n>/<m> first-unstarted=<path>
@@ -48,6 +48,44 @@ export declare function checkStaleBuildForTest(projectRoot: string, engineDir: s
48
48
  * result without matching the rendered copy.
49
49
  */
50
50
  export declare function formatStaleBuildWarning(result: StaleBuildResult): string;
51
+ /** Result of {@link findCompiledSourcesNewerThanBinary}. */
52
+ export interface StaleCompiledResult {
53
+ /** Engine-relative compiled sources modified after the binary was linked. Sorted. */
54
+ paths: string[];
55
+ /** Engine-relative file that anchored the comparison (the binary or its libxul). */
56
+ linkedPath: string;
57
+ /** When that file was last written. */
58
+ linkedAt: Date;
59
+ }
60
+ /**
61
+ * Changed compiled sources (C++, Rust, IDL, WebIDL, IPDL, StaticPrefList
62
+ * YAML, …) whose mtime is newer than the launchable build's last link.
63
+ * `mach build faster` compiles none of them, so such a run tests the
64
+ * binary from before the edit, which reads as a failed fix rather than a
65
+ * stale build. Candidates are the engine paths changed since the baseline
66
+ * plus every workdir edit; a fork's persistent patch diff is older than any
67
+ * build that followed it, so only genuine post-build edits remain.
68
+ * Returns undefined when there is nothing to compare against (no
69
+ * launchable binary).
70
+ *
71
+ * @param engineDir - Absolute engine directory
72
+ * @param baseline - Last-build baseline, when one exists
73
+ * @param launchablePath - Engine-relative launchable binary path
74
+ */
75
+ export declare function findCompiledSourcesNewerThanBinary(engineDir: string, baseline: BuildBaseline | undefined, launchablePath: string | undefined): Promise<StaleCompiledResult | undefined>;
76
+ /**
77
+ * The `stale-compiled` refusal (or, under `--allow-stale-build`, warning).
78
+ *
79
+ * @param result - A non-empty {@link StaleCompiledResult}
80
+ */
81
+ export declare function formatStaleCompiledRefusal(result: StaleCompiledResult): string;
82
+ /**
83
+ * The notice that explains why a `test --build` pre-test build escalated
84
+ * to a full `mach build` for stale compiled sources.
85
+ *
86
+ * @param result - A non-empty {@link StaleCompiledResult}
87
+ */
88
+ export declare function formatStaleCompiledBuildNotice(result: StaleCompiledResult): string;
51
89
  /**
52
90
  * Sentinel returned by {@link findUncoveredRequestPaths} when a full-suite
53
91
  * request (no paths) is checked against a scoped coverage record.