@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
@@ -0,0 +1,86 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The restore build after a compiled negative control.
4
+ *
5
+ * A control on compiled source is applied before a full pre-test build, so
6
+ * the binary under test carries it. Restoring the source byte-exact after
7
+ * the run does not unbuild it: the objdir binary keeps the mutation until a
8
+ * build recompiles the original. Every later `fireforge test` would then
9
+ * test the control. The orchestrator therefore runs one more full
10
+ * `mach build` right after a successful restore, still under the same
11
+ * engine-session hold, and its baseline drops the `controlBuild` stamp the
12
+ * control's build recorded. When that build fails, or the run never gets
13
+ * here (killed), the stamp stays and the next `fireforge test` refuses with
14
+ * `note=control-built` until `fireforge build` replaces it.
15
+ */
16
+ import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
17
+ import { getProjectPaths, loadConfig } from '../core/config.js';
18
+ import { runProtectedMachBuild, withBuildLock } from '../core/mach.js';
19
+ import { toError } from '../utils/errors.js';
20
+ import { info, notice, verbose, warn } from '../utils/logger.js';
21
+ /**
22
+ * Whether the recorded baseline is the one this run's control build wrote:
23
+ * only then did a build compile the control in (a failed control build
24
+ * writes no baseline, and its restored source's fresh mtime trips the
25
+ * stale-compiled check instead).
26
+ *
27
+ * @param projectRoot - Project root
28
+ * @param stamp - The stamp this run's control build recorded
29
+ */
30
+ export async function controlBuildRecorded(projectRoot, stamp) {
31
+ const baseline = await readBuildBaseline(projectRoot);
32
+ return (baseline?.controlBuild?.appliedAt === stamp.appliedAt &&
33
+ baseline.controlBuild.diffFile === stamp.diffFile);
34
+ }
35
+ /**
36
+ * Rebuilds the binary after a compiled control was restored: a full
37
+ * protected `mach build` under the build lock, then a baseline without the
38
+ * stamp. The previous baseline's packaging-coverage claim is carried
39
+ * forward unchanged, since recompiling the restored source stages no tests.
40
+ * Never throws: a failure leaves the stamp in place, which is the refusal
41
+ * the next run needs.
42
+ *
43
+ * @param projectRoot - Project root
44
+ * @param stamp - The stamp this run's control build recorded
45
+ * @param harnessRetries - Recognized-crash retry budget for the build
46
+ */
47
+ export async function runControlRestoreBuild(projectRoot, stamp, harnessRetries) {
48
+ const paths = getProjectPaths(projectRoot);
49
+ notice(`Rebuilding after the compiled negative control (${stamp.targets.map((target) => `engine/${target}`).join(', ')}): ` +
50
+ 'the binary still carries the mutation until a full mach build recompiles the restored source.');
51
+ try {
52
+ const config = await loadConfig(projectRoot);
53
+ return await withBuildLock(projectRoot, async () => {
54
+ const previousBaseline = await readBuildBaseline(projectRoot);
55
+ const result = await runProtectedMachBuild('full', paths.engine, {
56
+ ...(harnessRetries === undefined ? {} : { retries: harnessRetries }),
57
+ });
58
+ if (result.exitCode !== 0) {
59
+ warn('The restore build after the negative control failed, so the binary still carries ' +
60
+ 'the control. The next "fireforge test" refuses (note=control-built) until ' +
61
+ '"fireforge build" succeeds.');
62
+ return 'failed';
63
+ }
64
+ await writeBuildBaseline({
65
+ projectRoot,
66
+ engineDir: paths.engine,
67
+ binaryName: config.binaryName,
68
+ testPackagingCoverage: previousBaseline?.testPackagingCoverage,
69
+ testCoverageRecords: previousBaseline?.testCoverageRecords,
70
+ fullCoverageAnchor: previousBaseline?.fullCoverageAnchor,
71
+ previousBaseline,
72
+ recordedBy: 'fireforge test --negative-control (restore build)',
73
+ staticComponentsHandling: 'refresh',
74
+ buildKind: 'full',
75
+ });
76
+ info('Restore build complete: the binary no longer carries the negative control.');
77
+ return 'done';
78
+ });
79
+ }
80
+ catch (error) {
81
+ verbose(`Restore build after the negative control failed: ${toError(error).message}`);
82
+ warn(`The restore build after the negative control could not run (${toError(error).message}). ` +
83
+ 'The next "fireforge test" refuses (note=control-built) until "fireforge build" succeeds.');
84
+ return 'failed';
85
+ }
86
+ }
@@ -6,11 +6,14 @@
6
6
  * `--jobs` parallel harness instances.
7
7
  */
8
8
  import { createServer } from 'node:net';
9
+ import { join } from 'node:path';
9
10
  import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
10
- import { autoPortsRequested } from '../core/harness-port-fallback.js';
11
+ import { autoPortsRequested, detectHarnessPortFallback } from '../core/harness-port-fallback.js';
12
+ import { reserveHarnessServerPorts, } from '../core/harness-port-reservation.js';
11
13
  import { extractForwardedMarionettePort } from '../core/marionette-port.js';
12
14
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
13
15
  import { BuildError } from '../errors/build.js';
16
+ import { ensureDir } from '../utils/fs.js';
14
17
  import { notice } from '../utils/logger.js';
15
18
  import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
16
19
  import { shuffleTestGroups } from './test-harness-env.js';
@@ -95,15 +98,40 @@ async function allocateLoopbackPort() {
95
98
  function testRunManifestArg(index) {
96
99
  return `--testrun-manifest-file=fireforge-tests-${process.pid}-job${index + 1}.json`;
97
100
  }
101
+ /**
102
+ * The `MOZ_UPLOAD_DIR` the caller gave this run (its own env first, then
103
+ * the process environment), or undefined when there is none.
104
+ */
105
+ function uploadDirBase(base) {
106
+ const dir = base.env?.['MOZ_UPLOAD_DIR'] ?? process.env['MOZ_UPLOAD_DIR'];
107
+ return dir === undefined || dir === '' ? undefined : dir;
108
+ }
109
+ /**
110
+ * `--jobs` instance `index`'s own upload directory, `<base>/job-<n>`. The
111
+ * harness writes per-run artifacts there under fixed names (the
112
+ * `modifiedPrefs.json` pref census among them), so instances sharing the
113
+ * caller's directory replace each other's files.
114
+ */
115
+ function instanceUploadDir(uploadBase, index) {
116
+ return join(uploadBase, `job-${index + 1}`);
117
+ }
98
118
  /**
99
119
  * The context for `--jobs` instance `index` (0-based): its own output
100
- * prefix and test-list manifest, and from the second instance on its own
101
- * Marionette port, so browsers started at the same moment never race for
102
- * 2828. The mochitest server ports are left to the engine harness's
103
- * busy-port fallback, which `--jobs` requires (auto ports).
120
+ * prefix, upload directory (when the caller set `MOZ_UPLOAD_DIR`) and
121
+ * test-list manifest, and from the second instance on its own Marionette
122
+ * port, so browsers started at the same moment never race for 2828. The
123
+ * mochitest server ports are moved by the engine harness's busy-port
124
+ * fallback, which `--jobs` requires (auto ports), while
125
+ * {@link reserveHarnessServerPorts} holds the defaults.
104
126
  */
105
127
  async function instanceContext(base, index, xpcshellOnly) {
106
- const ctx = { ...base, outputPrefix: `[job ${index + 1}] ` };
128
+ let ctx = { ...base, outputPrefix: `[job ${index + 1}] ` };
129
+ const uploadBase = uploadDirBase(base);
130
+ if (uploadBase !== undefined) {
131
+ const uploadDir = instanceUploadDir(uploadBase, index);
132
+ await ensureDir(uploadDir);
133
+ ctx = { ...ctx, env: { ...base.env, MOZ_UPLOAD_DIR: uploadDir } };
134
+ }
107
135
  if (xpcshellOnly)
108
136
  return ctx;
109
137
  const own = [...base.baseExtraArgs, testRunManifestArg(index)];
@@ -115,6 +143,37 @@ async function instanceContext(base, index, xpcshellOnly) {
115
143
  baseExtraArgs: [...own, `--setpref=marionette.port=${port}`, `--marionette=127.0.0.1:${port}`],
116
144
  };
117
145
  }
146
+ /**
147
+ * Stamps a `--jobs` run's verdict attributes: `jobs=<n>`, and
148
+ * `upload-dirs=<dir>,...` naming each instance's upload directory, so a
149
+ * caller can merge per-instance artifacts.
150
+ */
151
+ function stampParallelAttributes(runCtx, jobs, shards) {
152
+ setVerdictRunAttribute('jobs', String(jobs));
153
+ const uploadBase = uploadDirBase(runCtx);
154
+ if (uploadBase === undefined)
155
+ return;
156
+ const instances = Math.max(1, Math.min(jobs, shards));
157
+ setVerdictRunAttribute('upload-dirs', Array.from({ length: instances }, (_, index) => instanceUploadDir(uploadBase, index)).join(','));
158
+ }
159
+ /**
160
+ * Holds the mochitest server ports for a browser run on more than one
161
+ * instance (see `harness-port-reservation.ts`). Only on an engine whose
162
+ * harness falls back from a busy server port: without that fallback a
163
+ * held port would fail every instance instead of moving it.
164
+ */
165
+ async function reserveParallelServerPorts(engineDir, instances, xpcshellOnly) {
166
+ if (instances < 2 || xpcshellOnly)
167
+ return undefined;
168
+ if (!(await detectHarnessPortFallback(engineDir)).serverPorts)
169
+ return undefined;
170
+ const reservation = await reserveHarnessServerPorts();
171
+ if (reservation.held.length > 0) {
172
+ notice(`Holding mochitest server port(s) ${reservation.held.join(', ')} for the parallel run, so ` +
173
+ "each instance's harness moves to server ports of its own.");
174
+ }
175
+ return reservation;
176
+ }
118
177
  /** The shards to run, or undefined for one combined invocation. */
119
178
  function planShards(inputs) {
120
179
  const { dispatchGroups, options, canaryPath, shuffleSeed } = inputs;
@@ -152,9 +211,10 @@ export async function dispatchTests(inputs) {
152
211
  notice(describeSharding(shards.length, options, jobs));
153
212
  const base = jobs > 1 ? await instanceContext(runCtx, 0, inputs.xpcshellOnly) : runCtx;
154
213
  if (jobs > 1)
155
- setVerdictRunAttribute('jobs', String(jobs));
214
+ stampParallelAttributes(runCtx, jobs, shards.length);
156
215
  const generationBefore = await snapshotEngineGeneration(engineDir);
157
216
  let summary;
217
+ const reservation = await reserveParallelServerPorts(engineDir, Math.min(jobs, shards.length), inputs.xpcshellOnly);
158
218
  try {
159
219
  summary = await runShardedTests(base, shards, (outcome, label) => diagnoseShardOutcome(outcome, label, projectConfig.binaryName, inputs.postRebuildContext), {
160
220
  jobs,
@@ -163,6 +223,7 @@ export async function dispatchTests(inputs) {
163
223
  });
164
224
  }
165
225
  finally {
226
+ await reservation?.release();
166
227
  // Runs before the aggregate verdict below: a mutated engine/ emits
167
228
  // `FAIL reason=inconclusive` and throws, so an invalidated run can
168
229
  // never print `PASS shards=N/N` first. (A throw here masks an
@@ -39,6 +39,7 @@ const REASON_RANK = [
39
39
  'inconclusive',
40
40
  'preflight',
41
41
  'crash',
42
+ 'server-startup',
42
43
  'harness-arguments',
43
44
  'no-tests',
44
45
  'test-failures',
@@ -14,6 +14,7 @@
14
14
  * `FIREFORGE-VERDICT:` line. A request that needs none of this runs the
15
15
  * body exactly as before.
16
16
  */
17
+ import type { ControlBuildStamp } from '../core/build-baseline-types.js';
17
18
  import type { TestOptions } from '../types/commands/index.js';
18
19
  /** What the orchestrator asks of one sub-run beyond its paths and options. */
19
20
  export interface TestRunHooks {
@@ -35,6 +36,11 @@ export interface TestRunHooks {
35
36
  * compiles nothing, so a compiled control's binary needs the full one.
36
37
  */
37
38
  fullBuildReason?: string;
39
+ /**
40
+ * Stamp the pre-test build records on its baseline because it compiles a
41
+ * negative control in (see `BuildBaseline.controlBuild`).
42
+ */
43
+ controlBuild?: ControlBuildStamp;
38
44
  /** `--repeat` run this sub-run is (1-based), for `--results`. */
39
45
  repetition?: number;
40
46
  /** Where dispatches write their mozlog raw logs (`--results`, `--shard-size`). */
@@ -24,6 +24,7 @@ import { TestFailureError } from '../errors/build.js';
24
24
  import { toError } from '../utils/errors.js';
25
25
  import { info, notice, success } from '../utils/logger.js';
26
26
  import { stripEnginePrefix } from '../utils/paths.js';
27
+ import { controlBuildRecorded, runControlRestoreBuild } from './test-control-restore.js';
27
28
  import { classifyTestHarnesses } from './test-modes.js';
28
29
  import { deriveControlOutcome, describeControlOutcome, formatAggregateBody, formatRunLine, parseCapturedVerdict, worstRun, } from './test-multi-verdict.js';
29
30
  import { emitAggregateVerdict, emitFailVerdict, setVerdictCapture, setVerdictRunAttribute, verdictEmitted, } from './test-verdict.js';
@@ -105,6 +106,26 @@ function controlHook(paths, plan, slot) {
105
106
  'it stays in place until this run has finished.');
106
107
  };
107
108
  }
109
+ /** The stamp a compiled control's pre-test build records on its baseline. */
110
+ function compiledControlStamp(options, compiled) {
111
+ if (compiled === undefined || options.negativeControl === undefined)
112
+ return undefined;
113
+ return {
114
+ diffFile: options.negativeControl,
115
+ targets: [...compiled],
116
+ appliedAt: new Date().toISOString(),
117
+ };
118
+ }
119
+ /**
120
+ * After a compiled control was restored, rebuilds the binary that still
121
+ * carries it, when this run's control build recorded its stamp, and
122
+ * stamps `control-restore-build=done|failed` on the verdict.
123
+ */
124
+ async function rebuildAfterCompiledControl(projectRoot, stamp, options) {
125
+ if (stamp === undefined || !(await controlBuildRecorded(projectRoot, stamp)))
126
+ return;
127
+ setVerdictRunAttribute('control-restore-build', await runControlRestoreBuild(projectRoot, stamp, options.harnessRetries));
128
+ }
108
129
  /**
109
130
  * Throws the invocation's error after the aggregate verdict, or returns
110
131
  * when the invocation succeeded (all runs green, or a control caught).
@@ -172,6 +193,7 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
172
193
  }
173
194
  setVerdictRunAttribute('control-build', 'compiled');
174
195
  }
196
+ const controlStamp = compiledControlStamp(options, compiled);
175
197
  const results = [];
176
198
  let restoreError;
177
199
  for (const [index, run] of runs.entries()) {
@@ -190,6 +212,7 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
190
212
  ? {
191
213
  beforeBuild: controlHook(controlPaths, plan, slot),
192
214
  fullBuildReason: formatCompiledControlBuildNotice(compiled),
215
+ ...(controlStamp === undefined ? {} : { controlBuild: controlStamp }),
193
216
  }
194
217
  : { beforeDispatch: controlHook(controlPaths, plan, slot) }),
195
218
  });
@@ -234,6 +257,11 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
234
257
  emitFailVerdict('inconclusive', 'negative-control-restore');
235
258
  throw toError(restoreError);
236
259
  }
260
+ // The source is back byte-exact, but the binary still carries the
261
+ // compiled control: rebuild it under this same hold (see
262
+ // test-control-restore.ts). Only after every run, since a later
263
+ // `--repeat` run tests the same control-built binary.
264
+ await rebuildAfterCompiledControl(projectRoot, controlStamp, options);
237
265
  const repetitions = options.repeat ?? 1;
238
266
  const control = plan === undefined
239
267
  ? undefined
@@ -32,7 +32,7 @@ export declare function perfGenerationTolerance(engineDir: string): (before: str
32
32
  * (with a warning, and `drift-tolerated=<n>` on the verdict) when HEAD did
33
33
  * not move and every changed path is one the running browser cannot see.
34
34
  *
35
- * A peer deleting an unrelated chrome script used to make a whole run
35
+ * A peer deleting an unrelated chrome script must not make a whole run
36
36
  * inconclusive. What a running browser can see mid-run is narrower than
37
37
  * "anything under engine/": a file the objdir links to its source (`live`),
38
38
  * one the harness stages at dispatch (`at-dispatch`), and the run's own
@@ -86,7 +86,7 @@ export function perfGenerationTolerance(engineDir) {
86
86
  * (with a warning, and `drift-tolerated=<n>` on the verdict) when HEAD did
87
87
  * not move and every changed path is one the running browser cannot see.
88
88
  *
89
- * A peer deleting an unrelated chrome script used to make a whole run
89
+ * A peer deleting an unrelated chrome script must not make a whole run
90
90
  * inconclusive. What a running browser can see mid-run is narrower than
91
91
  * "anything under engine/": a file the objdir links to its source (`live`),
92
92
  * one the harness stages at dispatch (`at-dispatch`), and the run's own
@@ -2,15 +2,14 @@
2
2
  * The `fireforge test` checks that run BEFORE the engine session lock.
3
3
  *
4
4
  * Everything here reads, never writes, so none of it needs the lock, and
5
- * each check used to cost a full place in the lock queue before it could
6
- * refuse:
5
+ * running it first keeps each check from costing a full place in the lock
6
+ * queue before it can refuse:
7
7
  *
8
- * - a missing launchable binary (a peer's interrupted build) refused only
9
- * after the wait, in every session, with nothing naming the cause;
10
- * - the named-file checkJs pass refused an `Element | null` after an
11
- * eighteen-minute queue;
12
- * - `--wait-browser` and `--wait-port` spent their budget holding the
13
- * lock, so every peer queued behind a run that was doing nothing.
8
+ * - a missing launchable binary (a peer's interrupted build) refuses
9
+ * before the wait, naming the cause;
10
+ * - the named-file checkJs pass refuses a type error without a queue;
11
+ * - `--wait-browser` and `--wait-port` spend their budget without holding
12
+ * the lock, so no peer queues behind a run that is doing nothing.
14
13
  *
15
14
  * The in-lock ladder in `test.ts` still runs in full afterwards. That is
16
15
  * the "re-check once under the lock": the waits this pass spent are
@@ -3,15 +3,14 @@
3
3
  * The `fireforge test` checks that run BEFORE the engine session lock.
4
4
  *
5
5
  * Everything here reads, never writes, so none of it needs the lock, and
6
- * each check used to cost a full place in the lock queue before it could
7
- * refuse:
6
+ * running it first keeps each check from costing a full place in the lock
7
+ * queue before it can refuse:
8
8
  *
9
- * - a missing launchable binary (a peer's interrupted build) refused only
10
- * after the wait, in every session, with nothing naming the cause;
11
- * - the named-file checkJs pass refused an `Element | null` after an
12
- * eighteen-minute queue;
13
- * - `--wait-browser` and `--wait-port` spent their budget holding the
14
- * lock, so every peer queued behind a run that was doing nothing.
9
+ * - a missing launchable binary (a peer's interrupted build) refuses
10
+ * before the wait, naming the cause;
11
+ * - the named-file checkJs pass refuses a type error without a queue;
12
+ * - `--wait-browser` and `--wait-port` spend their budget without holding
13
+ * the lock, so no peer queues behind a run that is doing nothing.
15
14
  *
16
15
  * The in-lock ladder in `test.ts` still runs in full afterwards. That is
17
16
  * the "re-check once under the lock": the waits this pass spent are
@@ -122,7 +121,10 @@ export async function runTestPreLockPreflight(projectRoot, testPaths, options) {
122
121
  }
123
122
  else {
124
123
  await refuseCompiledControlBeforeLock(projectRoot, paths.engine, options);
125
- await refuseStalePackagingBeforeLock(projectRoot, paths.engine, options, normalizedPaths);
124
+ await refuseStalePackagingBeforeLock(projectRoot, paths.engine, options, {
125
+ normalizedPaths,
126
+ launchablePath,
127
+ });
126
128
  }
127
129
  return waitForResourcesBeforeLock(paths.engine, config, options, {
128
130
  launchablePath,
@@ -195,16 +197,17 @@ async function refuseCompiledControlBeforeLock(projectRoot, engineDir, options)
195
197
  * under `--allow-stale-build`/`--allow-stale-components`, whose warnings
196
198
  * belong to the in-lock pass alone. A `--negative-control` refusal says to
197
199
  * add `--build`: without it the control has nothing current to run on, and
198
- * the invocation used to end `control=inconclusive` after a full queue.
200
+ * the invocation would end `control=inconclusive` after a full queue.
199
201
  */
200
- async function refuseStalePackagingBeforeLock(projectRoot, engineDir, options, normalizedPaths) {
202
+ async function refuseStalePackagingBeforeLock(projectRoot, engineDir, options, request) {
201
203
  if (options.allowStaleBuild === true || options.allowStaleComponents === true)
202
204
  return;
203
205
  if ((await readBuildRunState(projectRoot)).kind === 'running')
204
206
  return;
205
207
  try {
206
- await enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths, {
208
+ await enforceStaleBuildGate(projectRoot, engineDir, options, request.normalizedPaths, {
207
209
  quiet: true,
210
+ launchablePath: request.launchablePath,
208
211
  });
209
212
  }
210
213
  catch (error) {
@@ -44,8 +44,8 @@ const TEST_HELP_TEXT = [
44
44
  '',
45
45
  'Machine-readable verdict: every run (single, sharded, --canary,',
46
46
  '--doctor) ends with exactly one raw stdout line',
47
- ' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|harness-arguments|test-failures|',
48
- ' no-tests|preflight|inconclusive|lock-timeout|killed] [checks=<n>]',
47
+ ' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|server-startup|harness-arguments|',
48
+ ' test-failures|no-tests|preflight|inconclusive|lock-timeout|killed] [checks=<n>]',
49
49
  ' [unexpected=<n>] [started=<n>/<m> first-unstarted=<path>]',
50
50
  ' [shards=<p>/<t>] [(<note>)] [shuffle=<seed>]',
51
51
  ' [runs=<p>/<n>] [control=caught|missed|inconclusive]',
@@ -18,6 +18,13 @@ export interface TestFileResult extends RawTestResult {
18
18
  shard: string;
19
19
  /** Line of the run log where this file's dispatch began (1-based). */
20
20
  logLine: number;
21
+ /**
22
+ * The shard's dispatched files, on a harness pseudo-record only (see
23
+ * {@link HARNESS_PSEUDO_TEST}).
24
+ */
25
+ shardFiles?: string[];
26
+ /** The shard's one file, on a harness pseudo-record of a one-file shard. */
27
+ file?: string;
21
28
  }
22
29
  /** The document `--results` writes. */
23
30
  export interface TestResultsDocument {
@@ -38,12 +45,14 @@ export declare function resetTestResults(): void;
38
45
  *
39
46
  * @param results - The dispatch's finished files
40
47
  * @param unfinished - Its started-but-unfinished files
41
- * @param context - Run, shard and run-log line of the dispatch
48
+ * @param context - Run, shard and run-log line of the dispatch, and the
49
+ * shard's dispatched files for harness pseudo-records
42
50
  */
43
51
  export declare function recordTestResults(results: readonly RawTestResult[], unfinished: readonly string[], context: {
44
52
  run: number;
45
53
  shard: string;
46
54
  logLine: number;
55
+ shardFiles?: readonly string[];
47
56
  }): void;
48
57
  /** The results collected so far (for tests and the shard summary). */
49
58
  export declare function collectedTestResults(): TestResultsDocument;
@@ -14,6 +14,13 @@ import { mkdtemp, rm } from 'node:fs/promises';
14
14
  import { tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
  import { writeJson } from '../utils/fs.js';
17
+ /**
18
+ * The `test` id mozlog gives a result the harness itself reports, such as
19
+ * the no-output TIMEOUT of a hung browser: a record that names no test
20
+ * file. It keeps that id, and gains the shard's files so a consumer can
21
+ * attribute it without matching `shard` labels back to paths.
22
+ */
23
+ const HARNESS_PSEUDO_TEST = 'automation.py';
17
24
  let collected = { schemaVersion: 1, files: [], unfinished: [] };
18
25
  /** Clears the collector at the start of an invocation. */
19
26
  export function resetTestResults() {
@@ -24,15 +31,25 @@ export function resetTestResults() {
24
31
  *
25
32
  * @param results - The dispatch's finished files
26
33
  * @param unfinished - Its started-but-unfinished files
27
- * @param context - Run, shard and run-log line of the dispatch
34
+ * @param context - Run, shard and run-log line of the dispatch, and the
35
+ * shard's dispatched files for harness pseudo-records
28
36
  */
29
37
  export function recordTestResults(results, unfinished, context) {
30
- for (const result of results)
31
- collected.files.push({ ...result, ...context });
38
+ const { shardFiles, ...dispatch } = context;
39
+ for (const result of results) {
40
+ collected.files.push({ ...result, ...dispatch, ...harnessRecordFiles(result, shardFiles) });
41
+ }
32
42
  for (const test of unfinished) {
33
43
  collected.unfinished.push({ test, run: context.run, shard: context.shard });
34
44
  }
35
45
  }
46
+ /** The `shardFiles`/`file` keys a harness pseudo-record gains. */
47
+ function harnessRecordFiles(result, shardFiles) {
48
+ if (result.test !== HARNESS_PSEUDO_TEST || shardFiles === undefined)
49
+ return {};
50
+ const only = shardFiles.length === 1 ? shardFiles[0] : undefined;
51
+ return { shardFiles: [...shardFiles], ...(only === undefined ? {} : { file: only }) };
52
+ }
36
53
  /** The results collected so far (for tests and the shard summary). */
37
54
  export function collectedTestResults() {
38
55
  return collected;
@@ -10,6 +10,7 @@
10
10
  * stays available via `--no-shard`.
11
11
  */
12
12
  import { type DisplaySleepState } from '../core/display-state.js';
13
+ import { type HarnessServerPorts } from '../core/harness-port-reservation.js';
13
14
  import { type MachCommandResult } from '../core/mach.js';
14
15
  import { type HarnessRunVerdict } from '../core/test-harness-crash.js';
15
16
  import { type RawTestResult } from '../core/test-raw-log.js';
@@ -74,6 +75,11 @@ export interface TestRunOutcome {
74
75
  displayState?: DisplaySleepState | undefined;
75
76
  /** Per-file results from the raw log, when the run wrote one. */
76
77
  fileResults?: RawTestResult[] | undefined;
78
+ /**
79
+ * The mochitest server ports the last attempt reported (its "falling
80
+ * back to port" lines), for the shard line's `ports=` key.
81
+ */
82
+ serverPorts?: HarnessServerPorts | undefined;
77
83
  }
78
84
  /**
79
85
  * Runs one mach test invocation for `paths`, retrying recognized harness
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { join } from 'node:path';
14
14
  import { probeDisplaySleepState } from '../core/display-state.js';
15
+ import { createHarnessPortTracker, } from '../core/harness-port-reservation.js';
15
16
  import { runMachTestSuite } from '../core/mach.js';
16
17
  import { parentExited } from '../core/parent-exit-watchdog.js';
17
18
  import { activeRunLogLine, getActiveRunLogPath } from '../core/run-log.js';
@@ -68,12 +69,17 @@ async function runTestsWithRetriesInner(ctx, paths, env, label) {
68
69
  let verdict;
69
70
  let rawLog;
70
71
  let logLine;
72
+ let portTracker;
71
73
  for (;;) {
72
74
  attempts += 1;
73
75
  let tracker = createStartMarkerTracker();
76
+ portTracker = createHarnessPortTracker();
74
77
  const raw = rawLogDispatchArgs(ctx, extraArgs);
75
78
  logLine = activeRunLogLine();
76
- const dispatched = await runTrackedMachTestSuite(ctx, kind, paths, raw.args, env, tracker);
79
+ const dispatched = await runTrackedMachTestSuite(ctx, kind, paths, raw.args, env, [
80
+ tracker,
81
+ portTracker,
82
+ ]);
77
83
  result = dispatched;
78
84
  // The parent that was reading this run is gone and the watchdog has
79
85
  // already killed the harness. Classifying the corpse would call it a
@@ -108,9 +114,14 @@ async function runTestsWithRetriesInner(ctx, paths, env, label) {
108
114
  verdict = vetoIncompletePass(classifyHarnessRun(result.exitCode, combined, paths), combined, paths, rawLog !== undefined && rawLog.markers.started.length > 0
109
115
  ? { markers: rawLog.markers, source: 'raw' }
110
116
  : { markers: tracker.markers(), source: 'stream' });
117
+ // A mochitest server that never came up is retried like a crash: no
118
+ // test ran, and a parallel instance's port race is not a result.
111
119
  if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
112
120
  break;
113
- warn(`Harness crash detected (${verdict.signature?.reason ?? 'unknown shape'}): ` +
121
+ const shape = verdict.signature?.verdictReason === 'server-startup'
122
+ ? 'Harness server startup failed'
123
+ : 'Harness crash detected';
124
+ warn(`${shape} (${verdict.signature?.reason ?? 'unknown shape'}): ` +
114
125
  `${verdict.signature?.line ?? ''}\n` +
115
126
  `Retrying (attempt ${attempts + 1} of ${maxAttempts})...`);
116
127
  }
@@ -137,6 +148,7 @@ async function runTestsWithRetriesInner(ctx, paths, env, label) {
137
148
  run: ctx.repetition ?? 1,
138
149
  shard: label,
139
150
  logLine,
151
+ shardFiles: paths,
140
152
  });
141
153
  }
142
154
  return {
@@ -146,8 +158,14 @@ async function runTestsWithRetriesInner(ctx, paths, env, label) {
146
158
  appdirInjectionAttempted,
147
159
  displayState,
148
160
  ...(rawLog === undefined ? {} : { fileResults: rawLog.results }),
161
+ ...serverPortsOf(portTracker),
149
162
  };
150
163
  }
164
+ /** The `serverPorts` field of an outcome, present only when a port was seen. */
165
+ function serverPortsOf(tracker) {
166
+ const ports = tracker.ports();
167
+ return Object.keys(ports).length > 0 ? { serverPorts: ports } : {};
168
+ }
151
169
  /** Sequence for raw-log file names, unique within the process. */
152
170
  let rawLogSequence = 0;
153
171
  /**
@@ -170,7 +188,7 @@ function rawLogDispatchArgs(ctx, extraArgs) {
170
188
  * post-close reaping, the process-group announcement, and the parent-exit
171
189
  * watchdog for exactly as long as mach runs.
172
190
  */
173
- async function runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env, tracker) {
191
+ async function runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env, trackers) {
174
192
  const teardown = createHarnessTeardown({
175
193
  objDir: ctx.objDir === undefined ? undefined : join(ctx.engineDir, ctx.objDir),
176
194
  pgidFile: ctx.pgidFile,
@@ -196,7 +214,8 @@ async function runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env, tracker
196
214
  teardown: {
197
215
  ...teardown.hooks,
198
216
  onOutputChunk: (stream, chunk) => {
199
- tracker.onChunk(stream, chunk);
217
+ for (const tracker of trackers)
218
+ tracker.onChunk(stream, chunk);
200
219
  teardown.hooks.onOutputChunk?.(stream, chunk);
201
220
  },
202
221
  },
@@ -62,13 +62,17 @@ export interface ShardRunOptions {
62
62
  * `tests-ran-ok`. Otherwise the most structural failure wins. A crashed
63
63
  * shard makes the whole run `harness-crash`, then `no-tests`, then plain
64
64
  * `test-failures`, so the aggregate `reason=` keeps the classifier-driven
65
- * contract instead of collapsing every failure to `test-failures`.
65
+ * contract instead of collapsing every failure to `test-failures`. When
66
+ * every crashed shard is a server that never started, the aggregate keeps
67
+ * that signature, so its line says `reason=server-startup` too.
66
68
  */
67
69
  export declare function deriveAggregateShardVerdict(shards: ShardOutcome[]): HarnessRunVerdict;
68
70
  /**
69
71
  * The `FIREFORGE-SHARD:` line for one finished shard: the verdict it would
70
- * have printed alone, plus `shard=<i>/<n>` and `path=<label>`. Never a
71
- * `FIREFORGE-VERDICT:` line, so the run keeps exactly one.
72
+ * have printed alone, plus `shard=<i>/<n>` and `path=<label>`, and
73
+ * `ports=http:N,ssl:N,ws:N` when the harness reported the mochitest server
74
+ * ports it took. Never a `FIREFORGE-VERDICT:` line, so the run keeps
75
+ * exactly one.
72
76
  */
73
77
  export declare function formatShardLine(shard: ShardOutcome, index: number, total: number, failedFiles?: readonly string[]): string;
74
78
  /**