@hominis/fireforge 0.51.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 (254) hide show
  1. package/CHANGELOG.md +46 -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-output.js +5 -1
  72. package/dist/src/commands/status.js +1 -1
  73. package/dist/src/commands/test-build-furnace.d.ts +1 -2
  74. package/dist/src/commands/test-build-furnace.js +1 -2
  75. package/dist/src/commands/test-build-phase.d.ts +10 -0
  76. package/dist/src/commands/test-build-phase.js +51 -16
  77. package/dist/src/commands/test-control-restore.d.ts +40 -0
  78. package/dist/src/commands/test-control-restore.js +86 -0
  79. package/dist/src/commands/test-dispatch.js +83 -13
  80. package/dist/src/commands/test-multi-verdict.js +1 -0
  81. package/dist/src/commands/test-orchestrate.d.ts +11 -0
  82. package/dist/src/commands/test-orchestrate.js +33 -2
  83. package/dist/src/commands/test-perf-scope.d.ts +1 -1
  84. package/dist/src/commands/test-perf-scope.js +1 -1
  85. package/dist/src/commands/test-prelock.d.ts +7 -8
  86. package/dist/src/commands/test-prelock.js +15 -12
  87. package/dist/src/commands/test-register.js +2 -2
  88. package/dist/src/commands/test-results.d.ts +10 -1
  89. package/dist/src/commands/test-results.js +20 -3
  90. package/dist/src/commands/test-run.d.ts +6 -0
  91. package/dist/src/commands/test-run.js +23 -4
  92. package/dist/src/commands/test-shards.d.ts +7 -3
  93. package/dist/src/commands/test-shards.js +24 -7
  94. package/dist/src/commands/test-stale-gate.d.ts +6 -1
  95. package/dist/src/commands/test-stale-gate.js +47 -2
  96. package/dist/src/commands/test-verdict.d.ts +5 -3
  97. package/dist/src/commands/test.js +14 -1
  98. package/dist/src/commands/token.js +2 -2
  99. package/dist/src/commands/verify.js +1 -1
  100. package/dist/src/commands/watch.js +1 -1
  101. package/dist/src/commands/wire.js +1 -1
  102. package/dist/src/core/build-audit-resolve.d.ts +4 -5
  103. package/dist/src/core/build-audit-resolve.js +4 -5
  104. package/dist/src/core/build-audit-skip.d.ts +2 -2
  105. package/dist/src/core/build-audit.d.ts +3 -4
  106. package/dist/src/core/build-baseline-types.d.ts +37 -3
  107. package/dist/src/core/build-baseline-validate.d.ts +30 -1
  108. package/dist/src/core/build-baseline-validate.js +134 -33
  109. package/dist/src/core/build-baseline.d.ts +8 -2
  110. package/dist/src/core/build-baseline.js +32 -16
  111. package/dist/src/core/build-jar-escalation.d.ts +4 -4
  112. package/dist/src/core/build-jar-escalation.js +4 -4
  113. package/dist/src/core/build-prepare.js +4 -4
  114. package/dist/src/core/config-validate.js +4 -4
  115. package/dist/src/core/coverage-extend.d.ts +5 -3
  116. package/dist/src/core/coverage-extend.js +8 -3
  117. package/dist/src/core/coverage-records.d.ts +4 -5
  118. package/dist/src/core/coverage-records.js +4 -5
  119. package/dist/src/core/destructive.d.ts +2 -2
  120. package/dist/src/core/destructive.js +2 -2
  121. package/dist/src/core/discard-baseline.d.ts +2 -2
  122. package/dist/src/core/discard-baseline.js +3 -3
  123. package/dist/src/core/engine-session-lock.d.ts +1 -1
  124. package/dist/src/core/engine-session-lock.js +1 -1
  125. package/dist/src/core/file-lock-owner.d.ts +5 -5
  126. package/dist/src/core/file-lock-owner.js +5 -5
  127. package/dist/src/core/file-lock-queue.js +6 -8
  128. package/dist/src/core/file-lock-shared.d.ts +4 -4
  129. package/dist/src/core/file-lock-shared.js +4 -4
  130. package/dist/src/core/file-lock.js +7 -10
  131. package/dist/src/core/firefox.js +2 -2
  132. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
  133. package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
  134. package/dist/src/core/furnace-apply.js +3 -3
  135. package/dist/src/core/furnace-config-validate-exceptions.d.ts +4 -3
  136. package/dist/src/core/furnace-config-validate-exceptions.js +26 -6
  137. package/dist/src/core/furnace-operation.js +5 -5
  138. package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
  139. package/dist/src/core/furnace-registration-rename.d.ts +5 -5
  140. package/dist/src/core/furnace-registration-rename.js +5 -5
  141. package/dist/src/core/furnace-registration-validate.d.ts +2 -1
  142. package/dist/src/core/furnace-registration-validate.js +2 -1
  143. package/dist/src/core/furnace-registration.js +2 -2
  144. package/dist/src/core/furnace-validate-accessibility.d.ts +1 -1
  145. package/dist/src/core/furnace-validate-accessibility.js +22 -11
  146. package/dist/src/core/furnace-validate-exceptions.d.ts +2 -1
  147. package/dist/src/core/furnace-validate-exceptions.js +31 -13
  148. package/dist/src/core/git-base.js +4 -3
  149. package/dist/src/core/git-diff.js +1 -1
  150. package/dist/src/core/git.js +1 -1
  151. package/dist/src/core/harness-orphans.d.ts +6 -6
  152. package/dist/src/core/harness-orphans.js +6 -6
  153. package/dist/src/core/harness-port-reservation.d.ts +60 -0
  154. package/dist/src/core/harness-port-reservation.js +122 -0
  155. package/dist/src/core/history-log-file.d.ts +9 -9
  156. package/dist/src/core/history-log-file.js +15 -15
  157. package/dist/src/core/history-log.d.ts +3 -3
  158. package/dist/src/core/history-log.js +3 -3
  159. package/dist/src/core/launchable-build.d.ts +39 -0
  160. package/dist/src/core/launchable-build.js +85 -0
  161. package/dist/src/core/marionette-port.js +1 -3
  162. package/dist/src/core/moz-manifest-rules.js +3 -2
  163. package/dist/src/core/negative-control-compiled.d.ts +22 -3
  164. package/dist/src/core/negative-control-compiled.js +41 -5
  165. package/dist/src/core/parser-fallback.d.ts +5 -5
  166. package/dist/src/core/parser-fallback.js +5 -5
  167. package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
  168. package/dist/src/core/patch-identifier-suggest.js +11 -4
  169. package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
  170. package/dist/src/core/patch-lint-checkjs.js +1 -1
  171. package/dist/src/core/patch-lint-css.js +2 -2
  172. package/dist/src/core/patch-lint-file-size.d.ts +5 -7
  173. package/dist/src/core/patch-lint-file-size.js +5 -7
  174. package/dist/src/core/patch-lint-ignore.d.ts +3 -3
  175. package/dist/src/core/patch-lint-ignore.js +3 -3
  176. package/dist/src/core/patch-lint-prettier.d.ts +4 -6
  177. package/dist/src/core/patch-lint-prettier.js +4 -6
  178. package/dist/src/core/patch-lint-projection.d.ts +1 -1
  179. package/dist/src/core/patch-lint-projection.js +1 -1
  180. package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
  181. package/dist/src/core/patch-lint-queue-types.js +2 -2
  182. package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
  183. package/dist/src/core/patch-manifest-consistency.js +2 -2
  184. package/dist/src/core/patch-manifest-io.d.ts +5 -1
  185. package/dist/src/core/patch-manifest-io.js +12 -10
  186. package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
  187. package/dist/src/core/patch-manifest-key-order.js +3 -4
  188. package/dist/src/core/patch-parse.d.ts +3 -3
  189. package/dist/src/core/patch-parse.js +3 -3
  190. package/dist/src/core/patch-policy.d.ts +1 -1
  191. package/dist/src/core/patch-policy.js +1 -1
  192. package/dist/src/core/patch-source-metadata.d.ts +1 -1
  193. package/dist/src/core/patch-source-metadata.js +1 -1
  194. package/dist/src/core/patch-subset.d.ts +5 -3
  195. package/dist/src/core/patch-subset.js +8 -7
  196. package/dist/src/core/patch-transform.d.ts +2 -2
  197. package/dist/src/core/patch-transform.js +2 -2
  198. package/dist/src/core/rebase-session.js +4 -6
  199. package/dist/src/core/register-xpcshell-test.d.ts +28 -7
  200. package/dist/src/core/register-xpcshell-test.js +76 -27
  201. package/dist/src/core/run-log.d.ts +8 -8
  202. package/dist/src/core/run-log.js +8 -8
  203. package/dist/src/core/status-classify.d.ts +1 -2
  204. package/dist/src/core/status-classify.js +1 -2
  205. package/dist/src/core/test-harness-crash.d.ts +5 -3
  206. package/dist/src/core/test-harness-crash.js +13 -14
  207. package/dist/src/core/test-harness-infra.d.ts +32 -0
  208. package/dist/src/core/test-harness-infra.js +69 -0
  209. package/dist/src/core/test-harness-signature.d.ts +6 -0
  210. package/dist/src/core/test-run-completeness.d.ts +12 -5
  211. package/dist/src/core/test-run-completeness.js +15 -8
  212. package/dist/src/core/test-stale-check.d.ts +48 -1
  213. package/dist/src/core/test-stale-check.js +97 -2
  214. package/dist/src/core/test-stall-triage.js +5 -7
  215. package/dist/src/core/token-category.js +2 -2
  216. package/dist/src/core/token-inventory.js +3 -3
  217. package/dist/src/core/token-manager.d.ts +2 -2
  218. package/dist/src/core/token-manager.js +5 -7
  219. package/dist/src/core/token-variant.d.ts +6 -6
  220. package/dist/src/core/token-variant.js +7 -7
  221. package/dist/src/core/typecheck-shim.d.ts +2 -1
  222. package/dist/src/core/typecheck-shim.js +29 -5
  223. package/dist/src/core/wire-destroy.d.ts +1 -1
  224. package/dist/src/core/wire-destroy.js +1 -1
  225. package/dist/src/core/wire-dom-fragment.js +1 -1
  226. package/dist/src/core/wire-init.d.ts +1 -1
  227. package/dist/src/core/wire-init.js +1 -1
  228. package/dist/src/core/wire-subscript.d.ts +1 -1
  229. package/dist/src/core/wire-subscript.js +1 -1
  230. package/dist/src/core/wire-utils.d.ts +2 -2
  231. package/dist/src/core/wire-utils.js +3 -3
  232. package/dist/src/errors/base.d.ts +1 -1
  233. package/dist/src/errors/base.js +1 -1
  234. package/dist/src/errors/rebase.d.ts +1 -1
  235. package/dist/src/errors/rebase.js +1 -1
  236. package/dist/src/types/commands/options.d.ts +26 -3
  237. package/dist/src/types/config.d.ts +5 -7
  238. package/dist/src/types/furnace.d.ts +9 -0
  239. package/dist/src/utils/options.js +2 -2
  240. package/dist/src/utils/process.js +2 -2
  241. package/dist/src/utils/regex.d.ts +2 -2
  242. package/dist/src/utils/regex.js +3 -3
  243. package/dist/src/utils/sleep.d.ts +5 -6
  244. package/dist/src/utils/sleep.js +5 -6
  245. package/dist/src/utils/validation.js +2 -2
  246. package/docs/configuration.md +40 -26
  247. package/docs/exit-codes.md +8 -11
  248. package/docs/furnace.md +21 -3
  249. package/docs/lifecycle-invariants.md +36 -45
  250. package/docs/machine-output.md +43 -22
  251. package/docs/patch-workflow.md +129 -24
  252. package/docs/run-logs.md +8 -9
  253. package/docs/testing.md +118 -44
  254. package/package.json +1 -1
@@ -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>
@@ -17,10 +17,14 @@
17
17
  * `FAIL reason=test-failures`, and a short start count is stamped on the
18
18
  * verdict line as `started=<n>/<m> first-unstarted=<path>`.
19
19
  *
20
- * The start-count check only runs when at least one start marker names a
21
- * requested file. The suite-specific xpcshell dispatch can prove execution
22
- * with a result-summary block alone and print no `TEST_START` line, and a
23
- * run that never names its files cannot be counted against them.
20
+ * From streamed or captured text, the start-count check only runs when at
21
+ * least one start marker names a requested file. The suite-specific xpcshell
22
+ * dispatch can prove execution with a result-summary block alone and print
23
+ * no `TEST_START` line, and a run that never names its files cannot be
24
+ * counted against them. The structured raw log names every test by its
25
+ * manifest id, so a raw log whose starts name none of the requested files
26
+ * ran something else: a `--jobs` instance that read another instance's test
27
+ * list once passed on that instance's file.
24
28
  */
25
29
  import { type HarnessRunVerdict } from './test-harness-crash.js';
26
30
  import type { IncompleteRunEvidence } from './test-harness-signature.js';
@@ -62,8 +66,11 @@ export declare function createStartMarkerTracker(): StartMarkerTracker;
62
66
  * @param evidence - Captured mach stdout and stderr, or the markers
63
67
  * collected from the complete stream
64
68
  * @param requestedPaths - Dispatched paths, in dispatch order
69
+ * @param authoritative - The markers come from the raw log, so a run whose
70
+ * starts name no requested file is counted (as none started) instead of
71
+ * being left uncounted
65
72
  */
66
- export declare function countStartedFiles(evidence: string | StartMarkers, requestedPaths: readonly string[]): {
73
+ export declare function countStartedFiles(evidence: string | StartMarkers, requestedPaths: readonly string[], authoritative?: boolean): {
67
74
  started: number;
68
75
  requested: number;
69
76
  firstUnstarted?: string;
@@ -18,10 +18,14 @@
18
18
  * `FAIL reason=test-failures`, and a short start count is stamped on the
19
19
  * verdict line as `started=<n>/<m> first-unstarted=<path>`.
20
20
  *
21
- * The start-count check only runs when at least one start marker names a
22
- * requested file. The suite-specific xpcshell dispatch can prove execution
23
- * with a result-summary block alone and print no `TEST_START` line, and a
24
- * run that never names its files cannot be counted against them.
21
+ * From streamed or captured text, the start-count check only runs when at
22
+ * least one start marker names a requested file. The suite-specific xpcshell
23
+ * dispatch can prove execution with a result-summary block alone and print
24
+ * no `TEST_START` line, and a run that never names its files cannot be
25
+ * counted against them. The structured raw log names every test by its
26
+ * manifest id, so a raw log whose starts name none of the requested files
27
+ * ran something else: a `--jobs` instance that read another instance's test
28
+ * list once passed on that instance's file.
25
29
  */
26
30
  import { SHUTDOWN_REENTRY_PATTERN, START_MARKER_PATTERN, TEST_FILE_BASENAME_PATTERN, } from './test-harness-crash.js';
27
31
  /**
@@ -80,8 +84,11 @@ export function createStartMarkerTracker() {
80
84
  * @param evidence - Captured mach stdout and stderr, or the markers
81
85
  * collected from the complete stream
82
86
  * @param requestedPaths - Dispatched paths, in dispatch order
87
+ * @param authoritative - The markers come from the raw log, so a run whose
88
+ * starts name no requested file is counted (as none started) instead of
89
+ * being left uncounted
83
90
  */
84
- export function countStartedFiles(evidence, requestedPaths) {
91
+ export function countStartedFiles(evidence, requestedPaths, authoritative = false) {
85
92
  const requested = requestedPaths.filter((path) => TEST_FILE_BASENAME_PATTERN.test(basenameOf(path)));
86
93
  if (requested.length === 0)
87
94
  return undefined;
@@ -92,8 +99,8 @@ export function countStartedFiles(evidence, requestedPaths) {
92
99
  };
93
100
  // Counting needs a dialect that names the requested files on its start
94
101
  // lines. When no start line names any of them, the output cannot be
95
- // counted against the request at all.
96
- if (!requested.some((path) => names(path, started)))
102
+ // counted against the request at all, unless it is the raw log.
103
+ if (!authoritative && !requested.some((path) => names(path, started)))
97
104
  return undefined;
98
105
  const unstarted = requested.filter((path) => !names(path, started) && !names(path, skipped));
99
106
  const firstUnstarted = unstarted[0];
@@ -125,7 +132,7 @@ export function vetoIncompletePass(verdict, output, requestedPaths, tracked) {
125
132
  // tracker means nothing was streamed through it, and the capture is then
126
133
  // the only evidence there is.
127
134
  const useTracked = tracked !== undefined && tracked.markers.started.length > 0;
128
- const counted = countStartedFiles(useTracked ? tracked.markers : output, requestedPaths);
135
+ const counted = countStartedFiles(useTracked ? tracked.markers : output, requestedPaths, useTracked && tracked.source === 'raw');
129
136
  const unexpected = verdict.unexpected ?? 0;
130
137
  const truncated = counted?.firstUnstarted !== undefined;
131
138
  if (unexpected === 0 && shutdownLine === undefined && !truncated)
@@ -1,4 +1,4 @@
1
- import type { BuildBaseline, TestPackagingCoverage } from './build-baseline-types.js';
1
+ import type { BaselineRecorder, BuildBaseline, TestPackagingCoverage } from './build-baseline-types.js';
2
2
  import { type StaleExemption } from './test-stale-exempt.js';
3
3
  /** Result of the stale-build preflight probe. */
4
4
  export interface StaleBuildResult {
@@ -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.
@@ -97,6 +135,15 @@ export interface CoverageRefusalDetail {
97
135
  changedInputs?: Readonly<Record<string, readonly string[]>>;
98
136
  /** The record carries a full build's anchor forward for unchanged paths. */
99
137
  carriesFullAnchor?: boolean;
138
+ /**
139
+ * Who wrote the record and when, so a run whose coverage a peer's scoped
140
+ * build replaced can tell whose build that was.
141
+ */
142
+ recordedBy?: string | undefined;
143
+ builtAt?: string | undefined;
144
+ recorder?: BaselineRecorder | undefined;
145
+ /** Whether the recorder's process is still running (it may still be using the packaging). */
146
+ recorderAlive?: boolean | undefined;
100
147
  }
101
148
  /**
102
149
  * Formats the refusal shown when a non-`--build` run requests paths the
@@ -22,6 +22,8 @@
22
22
  * legitimately have a fresh `dist/` with no FireForge-recorded baseline
23
23
  * update.
24
24
  */
25
+ import { stat } from 'node:fs/promises';
26
+ import { join, relative } from 'node:path';
25
27
  import { toError } from '../utils/errors.js';
26
28
  import { verbose, warn } from '../utils/logger.js';
27
29
  import { normalizePathSlashes } from '../utils/paths.js';
@@ -30,6 +32,8 @@ import { readBuildBaseline } from './build-baseline.js';
30
32
  import { hashEngineFile } from './coverage-extend.js';
31
33
  import { isTestManifestPath } from './coverage-records.js';
32
34
  import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
35
+ import { readLinkedAt } from './launchable-build.js';
36
+ import { isCompiledSourcePath } from './negative-control-compiled.js';
33
37
  import { formatStaleExemptions, partitionStaleCandidates, } from './test-stale-exempt.js';
34
38
  /** Cap on the number of changed paths rendered inline. */
35
39
  const STALE_PATHS_LIMIT = 10;
@@ -91,6 +95,83 @@ export function formatStaleBuildWarning(result) {
91
95
  '"fireforge build --ui") first. Passing --build skips this check.' +
92
96
  exemptLine);
93
97
  }
98
+ /**
99
+ * Path segments whose compiled sources never reach the binary under test
100
+ * (gtests, cppunittests, CI config YAML): an edit there must not make the
101
+ * browser read as stale.
102
+ */
103
+ const NON_RUNTIME_SEGMENTS = new Set(['test', 'tests', 'gtest', 'testing', 'taskcluster']);
104
+ /**
105
+ * Changed compiled sources (C++, Rust, IDL, WebIDL, IPDL, StaticPrefList
106
+ * YAML, …) whose mtime is newer than the launchable build's last link.
107
+ * `mach build faster` compiles none of them, so such a run tests the
108
+ * binary from before the edit, which reads as a failed fix rather than a
109
+ * stale build. Candidates are the engine paths changed since the baseline
110
+ * plus every workdir edit; a fork's persistent patch diff is older than any
111
+ * build that followed it, so only genuine post-build edits remain.
112
+ * Returns undefined when there is nothing to compare against (no
113
+ * launchable binary).
114
+ *
115
+ * @param engineDir - Absolute engine directory
116
+ * @param baseline - Last-build baseline, when one exists
117
+ * @param launchablePath - Engine-relative launchable binary path
118
+ */
119
+ export async function findCompiledSourcesNewerThanBinary(engineDir, baseline, launchablePath) {
120
+ if (launchablePath === undefined)
121
+ return undefined;
122
+ const linked = await readLinkedAt(engineDir, launchablePath);
123
+ if (linked === undefined)
124
+ return undefined;
125
+ const changed = await collectChangedEnginePaths(engineDir, baseline, 'Stale-compiled preflight');
126
+ const candidates = changed.filter((path) => isCompiledSourcePath(path) &&
127
+ !path.split('/').some((segment) => NON_RUNTIME_SEGMENTS.has(segment)));
128
+ const paths = [];
129
+ for (const path of candidates) {
130
+ try {
131
+ const { mtimeMs } = await stat(join(engineDir, path));
132
+ if (mtimeMs > linked.mtimeMs)
133
+ paths.push(path);
134
+ }
135
+ catch {
136
+ // A deleted source cannot be newer than anything; the build that
137
+ // drops it is the stale-build check's business.
138
+ }
139
+ }
140
+ return {
141
+ paths: paths.sort(),
142
+ linkedPath: relative(engineDir, linked.path),
143
+ linkedAt: new Date(linked.mtimeMs),
144
+ };
145
+ }
146
+ /**
147
+ * The `stale-compiled` refusal (or, under `--allow-stale-build`, warning).
148
+ *
149
+ * @param result - A non-empty {@link StaleCompiledResult}
150
+ */
151
+ export function formatStaleCompiledRefusal(result) {
152
+ const head = result.paths.slice(0, STALE_PATHS_LIMIT).map((path) => `engine/${path}`);
153
+ const truncated = result.paths.length - head.length;
154
+ const list = head.join(', ') + (truncated > 0 ? `, … (+${truncated} more)` : '');
155
+ return (`Compiled sources changed after the binary under test was linked ` +
156
+ `(engine/${result.linkedPath}, ${result.linkedAt.toISOString()}): ${list}.\n` +
157
+ 'The run would test the binary from before those edits, so a red reads as a failed fix ' +
158
+ 'and a green proves nothing about them. Run "fireforge build" first, or rerun with ' +
159
+ '"fireforge test --build", which escalates to a full mach build when compiled sources ' +
160
+ 'changed. --allow-stale-build turns this refusal into a warning.');
161
+ }
162
+ /**
163
+ * The notice that explains why a `test --build` pre-test build escalated
164
+ * to a full `mach build` for stale compiled sources.
165
+ *
166
+ * @param result - A non-empty {@link StaleCompiledResult}
167
+ */
168
+ export function formatStaleCompiledBuildNotice(result) {
169
+ const head = result.paths.slice(0, STALE_PATHS_LIMIT).map((path) => `engine/${path}`);
170
+ const truncated = result.paths.length - head.length;
171
+ return (`Compiled sources changed after the binary was linked (${head.join(', ')}` +
172
+ `${truncated > 0 ? `, … (+${truncated} more)` : ''}); escalating this pre-test build to a ` +
173
+ 'full mach build, since mach build faster compiles nothing.');
174
+ }
94
175
  /**
95
176
  * Sentinel returned by {@link findUncoveredRequestPaths} when a full-suite
96
177
  * request (no paths) is checked against a scoped coverage record.
@@ -163,6 +244,20 @@ export async function findChangedTestManifestsForPaths(engineDir, baseline, unco
163
244
  });
164
245
  return hits.slice(0, STALE_PATHS_LIMIT);
165
246
  }
247
+ /** The refusal's sentence naming who wrote the coverage record. */
248
+ function formatCoverageHolder(detail) {
249
+ if (detail.recorder === undefined && detail.recordedBy === undefined)
250
+ return '';
251
+ const what = detail.recordedBy !== undefined ? `"${detail.recordedBy}"` : 'a build';
252
+ const when = detail.builtAt !== undefined ? ` at ${detail.builtAt}` : '';
253
+ const recorder = detail.recorder;
254
+ const who = recorder === undefined
255
+ ? ''
256
+ : ` (pid ${recorder.pid}${detail.recorderAlive === true ? ', still running' : ''}` +
257
+ (recorder.sessionId !== undefined ? `, session ${recorder.sessionId}` : '') +
258
+ `, invocation ${recorder.invocationId})`;
259
+ return ` The current record was written by ${what}${when}${who}.`;
260
+ }
166
261
  /**
167
262
  * Formats the refusal shown when a non-`--build` run requests paths the
168
263
  * packaged runtime's scoped `test --build` coverage does not include. It is
@@ -186,7 +281,7 @@ export function formatTestCoverageRefusal(uncovered, coverage, changedManifests
186
281
  };
187
282
  const rebuildTargets = uncovered.filter((p) => p !== FULL_SUITE_REQUEST);
188
283
  // Per-path retention is the default, so rebuilding only the uncovered
189
- // paths no longer costs anyone else's claim. Saying so is what keeps a
284
+ // paths costs no one else's claim. Saying so is what keeps a
190
285
  // peer from rebuilding everything "to be safe".
191
286
  const keepHint = rebuildTargets.length > 0
192
287
  ? ' That scoped build keeps every other recorded path whose own staging inputs (manifest directory and the fixtures its manifest names) and engine/mozconfig are unchanged.'
@@ -210,7 +305,7 @@ export function formatTestCoverageRefusal(uncovered, coverage, changedManifests
210
305
  return (`The packaged test runtime was produced by a scoped "fireforge test --build" covering only: ${scope}.\n` +
211
306
  `The requested run needs ${cap(uncovered)}, which that packaging does not cover — support ` +
212
307
  'fixtures for those manifests may be missing from obj-*/_tests/, and the run can hang ' +
213
- `rather than fail.${affectedLine}${manifestLine} ${rebuildHint} ` +
308
+ `rather than fail.${formatCoverageHolder(detail)}${affectedLine}${manifestLine} ${rebuildHint} ` +
214
309
  '(--allow-stale-build does not bypass this check — it accepts stale content, not missing coverage.)');
215
310
  }
216
311
  /**
@@ -37,9 +37,8 @@ const NO_OUTPUT_STALL_CONTROL_STEP = 'First, run a known-good control test. If t
37
37
  * - (4) is mechanical and was root-caused downstream: the harness serves
38
38
  * its manifest from `server.js` on 8888, and a survivor of an
39
39
  * interrupted run keeps the port. Its probe is exact, and the preflight
40
- * in `mochitest-server-port.ts` now refuses it before a run starts.
41
- * - (3) is now mechanical and root-caused, replacing the correlation an
42
- * earlier revision recorded. A `chrome://` (or `resource://`) URL that
40
+ * in `mochitest-server-port.ts` refuses it before a run starts.
41
+ * - (3) is mechanical and root-caused. A `chrome://` (or `resource://`) URL that
43
42
  * resolves to nothing reaches `CheckForBrokenChromeURL`
44
43
  * (netwerk/base/nsNetUtil.cpp), which outside automation only
45
44
  * `printf_stderr`s `Missing chrome or resource URL: <uri>`, but under
@@ -47,9 +46,8 @@ const NO_OUTPUT_STALL_CONTROL_STEP = 'First, run a known-good control test. If t
47
46
  * `MOZ_CRASH_UNSAFE_PRINTF("Missing chrome or resource URLs: %s")`. A
48
47
  * downstream reproduction symbolicated the faulting frame as
49
48
  * `CheckForBrokenChromeURL`, which is the established diagnosis the bar
50
- * in `mach-error-hints.ts` asks for. The discredited "stalls first
51
- * paint" story an even earlier revision asserted must not come back: a
52
- * failed image load in Gecko fires `error` and does not gate the
49
+ * in `mach-error-hints.ts` asks for. It is not a "stalls first paint"
50
+ * problem: a failed image load in Gecko fires `error` and does not gate the
53
51
  * document load event.
54
52
  *
55
53
  * Detection by log line is not specified for (3), because of the mechanism
@@ -62,7 +60,7 @@ const NO_OUTPUT_STALL_CONTROL_STEP = 'First, run a known-good control test. If t
62
60
  * the census points at the two artefacts that do carry it: the OS crash
63
61
  * report (whose faulting frame names `CheckForBrokenChromeURL`) and the
64
62
  * out-of-automation smoke probe (which prints the non-crashing spelling
65
- * and, since 0.45.0, counts it as an unallowed error so the exit code
63
+ * and counts it as an unallowed error so the exit code
66
64
  * agrees with the capture).
67
65
  */
68
66
  const NO_OUTPUT_STALL_TRIAGE = [
@@ -18,8 +18,8 @@ import { escapeRegex } from '../utils/regex.js';
18
18
  * Body of the first `/* … *\/` comment on `line`, or `undefined` when the
19
19
  * line carries no closed block comment.
20
20
  *
21
- * This uses index arithmetic rather than a regex: every regex spelling of
22
- * "banner comment" this module used to carry was super-linear on a line that
21
+ * This uses index arithmetic rather than a regex: the natural regex
22
+ * spellings of "banner comment" are super-linear on a line that
23
23
  * repeats the opening shape (CodeQL `js/polynomial-redos`), and `tokens.css`
24
24
  * is a file FireForge reads out of a consumer's engine tree, so a
25
25
  * pathological line needs no attacker to arrive.
@@ -4,9 +4,9 @@
4
4
  * tokens each declares, and what a single token resolves to in every theme
5
5
  * block that mentions it.
6
6
  *
7
- * `token add` requires `--category` and, until now, nothing reported the
8
- * categories a project actually has, so the only way to name one correctly
9
- * was to hand-parse a neighbouring `= Category =` banner out of the file.
7
+ * `token add` requires `--category`, so it needs a report of the categories
8
+ * a project actually has; without one, the only way to name one correctly
9
+ * is to hand-parse a neighbouring `= Category =` banner out of the file.
10
10
  * This module is that report. It reuses the banner and
11
11
  * category-region model from `token-category.ts` rather than parsing
12
12
  * tokens CSS a second way: two readers that disagree on what a banner is
@@ -4,7 +4,7 @@ import { type TokenDeclarationLocation } from './token-category.js';
4
4
  *
5
5
  * Derived from {@link TOKEN_MODES} so the runtime allowlist and the type
6
6
  * cannot drift: adding a member here without updating the list (or the
7
- * reverse) is no longer possible, because there is only one list.
7
+ * reverse) is impossible, because there is only one list.
8
8
  */
9
9
  export declare const TOKEN_MODES: readonly ["auto", "static", "override"];
10
10
  /** Dark mode behaviour for a token. */
@@ -21,7 +21,7 @@ export interface AddTokenOptions {
21
21
  value: string;
22
22
  /**
23
23
  * Token category matching section headers in the CSS file. Required for a
24
- * base declaration. Ignored, and no longer required, under
24
+ * base declaration. Ignored, and not required, under
25
25
  * {@link AddTokenOptions.variant}, which routes into a `:root<selector>`
26
26
  * block and never touches a category section.
27
27
  */