@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
@@ -13,6 +13,7 @@
13
13
  * judged per file from each dispatch's raw log: `shards=` then counts files
14
14
  * and `failed-shards=` names the files that failed.
15
15
  */
16
+ import { formatHarnessServerPorts } from '../core/harness-port-reservation.js';
16
17
  import { getActiveRunLogPath, writeToActiveRunLog } from '../core/run-log.js';
17
18
  import { buildHarnessCrashMessage, formatFireforgeVerdictLine, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
18
19
  import { TestFailureError } from '../errors/build.js';
@@ -27,19 +28,32 @@ const SHARD_STATUS_LABEL = {
27
28
  'no-tests': 'NO-TESTS',
28
29
  'harness-arguments': 'ARGUMENTS',
29
30
  };
31
+ /** The summary box's status word; a server that never started is not a crash. */
32
+ function shardStatusLabel(verdict) {
33
+ return verdict.signature?.verdictReason === 'server-startup'
34
+ ? 'SERVER'
35
+ : SHARD_STATUS_LABEL[verdict.kind];
36
+ }
30
37
  /**
31
38
  * Derives the aggregate classification for a sharded run: all-green is
32
39
  * `tests-ran-ok`. Otherwise the most structural failure wins. A crashed
33
40
  * shard makes the whole run `harness-crash`, then `no-tests`, then plain
34
41
  * `test-failures`, so the aggregate `reason=` keeps the classifier-driven
35
- * contract instead of collapsing every failure to `test-failures`.
42
+ * contract instead of collapsing every failure to `test-failures`. When
43
+ * every crashed shard is a server that never started, the aggregate keeps
44
+ * that signature, so its line says `reason=server-startup` too.
36
45
  */
37
46
  export function deriveAggregateShardVerdict(shards) {
38
47
  const kinds = new Set(shards.map(({ outcome }) => outcome.verdict.kind));
39
48
  if (kinds.has('harness-arguments'))
40
49
  return { kind: 'harness-arguments' };
41
- if (kinds.has('harness-crash'))
42
- return { kind: 'harness-crash' };
50
+ if (kinds.has('harness-crash')) {
51
+ const crashes = shards.filter(({ outcome }) => outcome.verdict.kind === 'harness-crash');
52
+ const signature = crashes[0]?.outcome.verdict.signature;
53
+ return crashes.every(({ outcome }) => outcome.verdict.signature?.verdictReason === 'server-startup') && signature !== undefined
54
+ ? { kind: 'harness-crash', signature }
55
+ : { kind: 'harness-crash' };
56
+ }
43
57
  if (kinds.has('no-tests'))
44
58
  return { kind: 'no-tests' };
45
59
  if (kinds.has('test-failures'))
@@ -48,15 +62,18 @@ export function deriveAggregateShardVerdict(shards) {
48
62
  }
49
63
  /**
50
64
  * The `FIREFORGE-SHARD:` line for one finished shard: the verdict it would
51
- * have printed alone, plus `shard=<i>/<n>` and `path=<label>`. Never a
52
- * `FIREFORGE-VERDICT:` line, so the run keeps exactly one.
65
+ * have printed alone, plus `shard=<i>/<n>` and `path=<label>`, and
66
+ * `ports=http:N,ssl:N,ws:N` when the harness reported the mochitest server
67
+ * ports it took. Never a `FIREFORGE-VERDICT:` line, so the run keeps
68
+ * exactly one.
53
69
  */
54
70
  export function formatShardLine(shard, index, total, failedFiles) {
55
71
  const body = formatFireforgeVerdictLine(shard.outcome.verdict).slice('FIREFORGE-VERDICT: '.length);
56
72
  const files = failedFiles !== undefined && failedFiles.length > 0
57
73
  ? ` failed-files=${failedFiles.join(',')}`
58
74
  : '';
59
- return `FIREFORGE-SHARD: ${body} shard=${index + 1}/${total} path=${shard.label.replace(/\s+/g, '_')}${files}`;
75
+ const ports = formatHarnessServerPorts(shard.outcome.serverPorts);
76
+ return `FIREFORGE-SHARD: ${body} shard=${index + 1}/${total} path=${shard.label.replace(/\s+/g, '_')}${files}${ports === undefined ? '' : ` ports=${ports}`}`;
60
77
  }
61
78
  function writeShardLine(line) {
62
79
  process.stdout.write(`${line}\n`);
@@ -163,7 +180,7 @@ function reportProgress(finished, total, jobs) {
163
180
  * counts as a whole.
164
181
  */
165
182
  function summarizeShards(shards, perFile) {
166
- const lines = shards.map(({ label, outcome }) => `${SHARD_STATUS_LABEL[outcome.verdict.kind].padEnd(8)} ${label}` +
183
+ const lines = shards.map(({ label, outcome }) => `${shardStatusLabel(outcome.verdict).padEnd(8)} ${label}` +
167
184
  (outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : ''));
168
185
  const failingShards = shards.filter(({ outcome }) => outcome.verdict.kind !== 'tests-ran-ok');
169
186
  let passed = shards.length - failingShards.length;
@@ -29,9 +29,14 @@ import type { TestOptions } from '../types/commands/index.js';
29
29
  * stale-content refusal still applies to it unchanged.
30
30
  */
31
31
  export declare function enforceStaleBuildGate(projectRoot: string, engineDir: string, options: TestOptions, normalizedPaths: readonly string[],
32
- /** `quiet`: refuse only, without the exemption note (the pre-lock pass). */
32
+ /**
33
+ * `quiet`: refuse only, without the exemption note (the pre-lock pass).
34
+ * `launchablePath`: the engine-relative binary the run launches, which
35
+ * anchors the stale-compiled check (skipped without one).
36
+ */
33
37
  gate?: {
34
38
  quiet?: boolean;
39
+ launchablePath?: string | undefined;
35
40
  }): Promise<void>;
36
41
  /**
37
42
  * Compiled-StaticComponents gate: refuses runs whose child process would
@@ -8,9 +8,10 @@
8
8
  * `src/core/test-stale-check.ts`.
9
9
  */
10
10
  import { filterByCarriedFullCoverage } from '../core/coverage-dispatch.js';
11
- import { checkStaleBuildForTest, checkStaticComponentsStale, findChangedTestManifestsForPaths, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaticComponentsRefusal, formatTestCoverageRefusal, FULL_SUITE_REQUEST, } from '../core/test-stale-check.js';
11
+ import { checkStaleBuildForTest, checkStaticComponentsStale, findChangedTestManifestsForPaths, findCompiledSourcesNewerThanBinary, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaleCompiledRefusal, formatStaticComponentsRefusal, formatTestCoverageRefusal, FULL_SUITE_REQUEST, } from '../core/test-stale-check.js';
12
12
  import { formatStaleExemptions } from '../core/test-stale-exempt.js';
13
13
  import { PreflightRefusalError } from '../errors/base.js';
14
+ import { isProcessAlive } from '../utils/errors.js';
14
15
  import { info, warn } from '../utils/logger.js';
15
16
  import { describePerfStaleImpact } from './test-perf-scope.js';
16
17
  /**
@@ -34,14 +35,20 @@ import { describePerfStaleImpact } from './test-perf-scope.js';
34
35
  * stale-content refusal still applies to it unchanged.
35
36
  */
36
37
  export async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths,
37
- /** `quiet`: refuse only, without the exemption note (the pre-lock pass). */
38
+ /**
39
+ * `quiet`: refuse only, without the exemption note (the pre-lock pass).
40
+ * `launchablePath`: the engine-relative binary the run launches, which
41
+ * anchors the stale-compiled check (skipped without one).
42
+ */
38
43
  gate = {}) {
39
44
  const stale = await checkStaleBuildForTest(projectRoot, engineDir);
45
+ enforceControlBuiltGate(stale.baseline);
40
46
  const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
41
47
  if (!dispatchesNoTests) {
42
48
  await enforceCoverageGate(engineDir, stale.baseline, normalizedPaths);
43
49
  }
44
50
  await enforceStaticComponentsGate(engineDir, stale.baseline, options);
51
+ await enforceStaleCompiledGate(engineDir, stale.baseline, options, gate.launchablePath);
45
52
  const exempt = stale.exempt ?? [];
46
53
  if (!stale.stale && exempt.length > 0 && gate.quiet !== true) {
47
54
  info(`Stale-build preflight: ${exempt.length} edit(s) since the last build need no rebuild: ${formatStaleExemptions(exempt)}.`);
@@ -72,6 +79,38 @@ gate = {}) {
72
79
  throw new PreflightRefusalError(staleMessage, 'stale-build');
73
80
  }
74
81
  }
82
+ /**
83
+ * Control-built refusal: the recorded build compiled a negative control
84
+ * into the binary, and no build since has recompiled the restored source
85
+ * (the run was killed, or its restore build failed). Every test against
86
+ * that binary tests the mutation. Not bypassable: `--allow-stale-build`
87
+ * accepts old content, not a deliberately broken binary.
88
+ */
89
+ function enforceControlBuiltGate(baseline) {
90
+ const stamp = baseline?.controlBuild;
91
+ if (stamp === undefined)
92
+ return;
93
+ throw new PreflightRefusalError(`The binary under test was built with the negative control ${stamp.diffFile} applied ` +
94
+ `(${stamp.targets.map((target) => `engine/${target}`).join(', ')}, applied ${stamp.appliedAt}), ` +
95
+ 'and no build has recompiled the restored source since. Run "fireforge build" to ' +
96
+ 'rebuild the binary without the control, then rerun.', 'control-built');
97
+ }
98
+ /**
99
+ * Stale-compiled refusal: compiled sources modified after the binary under
100
+ * test was linked. `--allow-stale-build` downgrades it to a warning, as it
101
+ * does the stale-build refusal.
102
+ */
103
+ async function enforceStaleCompiledGate(engineDir, baseline, options, launchablePath) {
104
+ const result = await findCompiledSourcesNewerThanBinary(engineDir, baseline, launchablePath);
105
+ if (result === undefined || result.paths.length === 0)
106
+ return;
107
+ const message = formatStaleCompiledRefusal(result);
108
+ if (options.allowStaleBuild === true) {
109
+ warn(message);
110
+ return;
111
+ }
112
+ throw new PreflightRefusalError(message, 'stale-compiled');
113
+ }
75
114
  /**
76
115
  * Packaging-coverage refusal. A path outside the recorded scoped list is
77
116
  * still covered while its own staging inputs are unchanged against a
@@ -93,6 +132,12 @@ async function enforceCoverageGate(engineDir, baseline, normalizedPaths) {
93
132
  throw new PreflightRefusalError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests, {
94
133
  changedInputs: Object.fromEntries(remaining.map((request) => [request.path, request.changed])),
95
134
  carriesFullAnchor: baseline?.fullCoverageAnchor !== undefined,
135
+ recordedBy: baseline?.recordedBy,
136
+ builtAt: baseline?.builtAt,
137
+ recorder: baseline?.recorder,
138
+ recorderAlive: baseline?.recorder !== undefined &&
139
+ baseline.recorder.pid !== process.pid &&
140
+ isProcessAlive(baseline.recorder.pid),
96
141
  }), 'coverage-replaced');
97
142
  }
98
143
  /**
@@ -1,7 +1,9 @@
1
1
  import type { HarnessRunVerdict } from '../core/test-harness-crash.js';
2
2
  /**
3
3
  * Reason codes the emission layer can put on a `FIREFORGE-VERDICT: FAIL`
4
- * line. The first three mirror the harness classifier. `preflight` covers
4
+ * line. The first three mirror the harness classifier, as does
5
+ * `server-startup`: a harness-crash-class infrastructure failure where a
6
+ * mochitest server never came up, so no test ran. `preflight` covers
5
7
  * every failure before harness classification (missing/stale builds,
6
8
  * invalid paths, port conflicts, config errors, spawn failures),
7
9
  * `inconclusive` marks a run whose harness result exists but cannot be
@@ -11,9 +13,9 @@ import type { HarnessRunVerdict } from '../core/test-harness-crash.js';
11
13
  * and `killed` marks a run terminated by a signal before it could reach any
12
14
  * of the others.
13
15
  */
14
- export type FireforgeVerdictReason = 'crash' | 'harness-arguments' | 'no-tests' | 'test-failures' | 'preflight' | 'inconclusive' | 'lock-timeout' | 'killed';
16
+ export type FireforgeVerdictReason = 'crash' | 'harness-arguments' | 'no-tests' | 'test-failures' | 'server-startup' | 'preflight' | 'inconclusive' | 'lock-timeout' | 'killed';
15
17
  /** Additive keys a run can stamp on its verdict line. */
16
- export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed' | 'furnace-applied' | 'furnace-apply' | 'furnace-skipped' | 'control-build' | 'drift-tolerated' | 'lock' | 'jobs' | 'results';
18
+ export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed' | 'furnace-applied' | 'furnace-apply' | 'furnace-skipped' | 'control-build' | 'control-restore-build' | 'build-id' | 'drift-tolerated' | 'lock' | 'jobs' | 'upload-dirs' | 'results';
17
19
  /** Records an additive attribute for this run's verdict line. */
18
20
  export declare function setVerdictRunAttribute(key: VerdictRunAttributeKey, value: string): void;
19
21
  /**
@@ -2,6 +2,7 @@
2
2
  import { join, resolve } from 'node:path';
3
3
  import { getProjectPaths, loadConfig } from '../core/config.js';
4
4
  import { assertEngineExists } from '../core/engine-precondition.js';
5
+ import { readBuildId } from '../core/launchable-build.js';
5
6
  import { hasBuildArtifacts } from '../core/mach.js';
6
7
  import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
7
8
  import { ensureLaunchableBrowserNotRunning } from '../core/marionette-port.js';
@@ -142,7 +143,9 @@ export async function testCommand(projectRoot, testPaths, options = {}, underLoc
142
143
  renderPreflightRefusal(error);
143
144
  emitFailVerdict('preflight', refusalNote(error));
144
145
  });
145
- const collectsResults = options.results !== undefined || options.shardSize !== undefined;
146
+ // `--jobs` always reads each dispatch's raw log, so a shard that ran
147
+ // files other than its own fails instead of passing on their results.
148
+ const collectsResults = options.results !== undefined || options.shardSize !== undefined || (options.jobs ?? 1) > 1;
146
149
  try {
147
150
  await underLock(() => (collectsResults ? withRawLogDir((dir) => orchestrate(dir)) : orchestrate()), lockMode);
148
151
  }
@@ -346,9 +349,19 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
346
349
  if (await runTestBuildPhase(projectRoot, paths, projectConfig, harnessRetries, options, {
347
350
  classification,
348
351
  normalizedPaths: hooks.coveragePaths ?? normalizedPaths,
352
+ ...(hooks.fullBuildReason === undefined ? {} : { fullBuildReason: hooks.fullBuildReason }),
353
+ ...(hooks.controlBuild === undefined ? {} : { controlBuild: hooks.controlBuild }),
354
+ launchablePath,
349
355
  })) {
350
356
  return;
351
357
  }
358
+ // Which binary this run exercises, read after the pre-test build so it
359
+ // names the build the tests actually ran against (`moz:buildID`).
360
+ if (launchablePath !== undefined) {
361
+ const buildId = await readBuildId(paths.engine, launchablePath);
362
+ if (buildId !== undefined)
363
+ setVerdictRunAttribute('build-id', buildId);
364
+ }
352
365
  const { forwardedPort, effectivePort } = await ensureTestBrowserEnvironment(paths.engine, launchablePath, xpcshellOnly, projectConfig, options, buildCheck.objDir);
353
366
  if (options.doctor) {
354
367
  const doctorOutcome = await runDoctorPreflight({
@@ -42,8 +42,8 @@ async function normalizeTokenNameForProject(projectRoot, rawTokenName) {
42
42
  *
43
43
  * A variant skip has no category to name, because the declaration lives in
44
44
  * a `:root<selector>` block, so the location reads as that block. The
45
- * variant path used to report no location at all, so a re-run meant to
46
- * change a value exited 0 having silently changed nothing.
45
+ * location matters: without it a re-run meant to change a value exits 0
46
+ * having silently changed nothing.
47
47
  *
48
48
  * @param tokenName - Full token name including prefix
49
49
  * @param result - Result carrying the skip location, when known
@@ -328,7 +328,7 @@ export function registerVerify(program, { getProjectRoot, withErrorHandling }) {
328
328
  program
329
329
  .command('verify')
330
330
  .description('Read-only fsck for the patch queue (manifest + cross-patch lint)')
331
- .option('--patches <names...>', 'Judge only findings that name these patches (filename, stem, order number, manifest name, or slug; repeat or comma-separate). Queue-wide findings stay; unowned worktree changes drop out')
331
+ .option('--patches <names...>', 'Judge only findings that name these patches (filename, stem, order number, manifest name, or slug; repeat, or separate with commas or whitespace). Queue-wide findings stay; unowned worktree changes drop out')
332
332
  .action(withErrorHandling(async (options) => {
333
333
  await verifyCommand(getProjectRoot(), options);
334
334
  }));
@@ -195,7 +195,7 @@ export async function watchCommand(projectRoot) {
195
195
  catch (error) {
196
196
  throw new BuildError('Watch process failed to start', 'mach watch', error instanceof Error ? error : undefined);
197
197
  }
198
- // On Ctrl+C the bin signal handler now waits for the mach child to shut
198
+ // On Ctrl+C the bin signal handler waits for the mach child to shut
199
199
  // down (waitForActiveChildShutdown) before exiting, so this continuation
200
200
  // can run and whitelist the SIGINT exit code, though the bin may still
201
201
  // win the race and terminate first. Both outcomes are acceptable.
@@ -208,7 +208,7 @@ function buildWireSubscriptOptions(options, domFilePath, domTargetPath, subscrip
208
208
  };
209
209
  }
210
210
  /**
211
- * Surfaces any legacy parser fallbacks the wiring run consumed, so the
211
+ * Surfaces any regex parser fallbacks the wiring run consumed, so the
212
212
  * operator knows which files were mutated by the regex path rather than
213
213
  * the AST path.
214
214
  */
@@ -29,11 +29,10 @@ export declare function countTrailingSegmentMatches(a: string, b: string): numbe
29
29
  *
30
30
  * Shared on purpose: this set governs two halves of one decision,
31
31
  * `scoreCandidate` (selection) and `isConfidentMatch` (confirmation, in
32
- * `build-audit.ts`). When the halves kept separate copies they disagreed,
33
- * so a source path could earn a selection bonus for a segment that
34
- * confirmation then rejected as meaningless, leaving the chosen artifact
35
- * unconfirmable. Any addition must apply to both halves, which is now
36
- * automatic.
32
+ * `build-audit.ts`). Separate copies would disagree, so a source path
33
+ * could earn a selection bonus for a segment that confirmation then
34
+ * rejected as meaningless, leaving the chosen artifact unconfirmable. One
35
+ * set makes every addition apply to both halves.
37
36
  */
38
37
  export declare const GENERIC_PATH_SEGMENTS: ReadonlySet<string>;
39
38
  /**
@@ -96,11 +96,10 @@ export function countTrailingSegmentMatches(a, b) {
96
96
  *
97
97
  * Shared on purpose: this set governs two halves of one decision,
98
98
  * `scoreCandidate` (selection) and `isConfidentMatch` (confirmation, in
99
- * `build-audit.ts`). When the halves kept separate copies they disagreed,
100
- * so a source path could earn a selection bonus for a segment that
101
- * confirmation then rejected as meaningless, leaving the chosen artifact
102
- * unconfirmable. Any addition must apply to both halves, which is now
103
- * automatic.
99
+ * `build-audit.ts`). Separate copies would disagree, so a source path
100
+ * could earn a selection bonus for a segment that confirmation then
101
+ * rejected as meaningless, leaving the chosen artifact unconfirmable. One
102
+ * set makes every addition apply to both halves.
104
103
  */
105
104
  export const GENERIC_PATH_SEGMENTS = new Set([
106
105
  'content',
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Why a changed engine path was not audited.
3
3
  *
4
- * The first four name skips that already existed as unstructured
5
- * `verbose()` strings. The last four are the 0.46.0 classifiers.
4
+ * Each reason is structured so summaries and machine output can count
5
+ * skips by cause instead of reading verbose log strings.
6
6
  */
7
7
  export type AuditSkipReason = 'not-packageable' | 'platform-gated' | 'platform-gated-ancestor' | 'tests-not-packaged' | 'source-unreadable' | 'branding-not-selected' | 'storybook-story' | 'declared-unpackaged';
8
8
  /** Render order for the `Packaged:` summary's per-class breakdown. */
@@ -40,10 +40,9 @@ export interface AuditEntry {
40
40
  status: 'updated' | 'stale' | 'missing' | 'skipped';
41
41
  /**
42
42
  * Why a `skipped` entry was skipped. Present only for `skipped`. The
43
- * reasons used to exist only as verbose log strings, so the summary line
44
- * could report `4 missing` on a run with zero real misses and nothing in
45
- * the counts distinguished a structural non-input from an unregistered
46
- * file an operator must act on.
43
+ * reason lets the counts distinguish a structural non-input from an
44
+ * unregistered file an operator must act on, so the summary line never
45
+ * reports skips as misses.
47
46
  */
48
47
  skipReason?: AuditSkipReason;
49
48
  }
@@ -93,10 +93,11 @@ export interface BuildBaseline {
93
93
  * mozconfig and complete testInputFingerprints outside the rebuilt scope
94
94
  * remain anchored. Explicitly rebuilt paths may change. A failed anchor
95
95
  * replaces coverage with a dropped-path notice, or refuses when the caller
96
- * required --extend-coverage. Legacy packageable-only anchors cannot retain
97
- * coverage because they cannot prove test scripts and fixtures unchanged.
96
+ * required --extend-coverage. Anchors that fingerprint only packageable
97
+ * inputs cannot retain coverage because they cannot prove test scripts and
98
+ * fixtures unchanged.
98
99
  *
99
- * Since 0.50.0 retention is decided per staged path: see
100
+ * Retention is decided per staged path: see
100
101
  * {@link BuildBaseline.testCoverageRecords} and
101
102
  * {@link BuildBaseline.fullCoverageAnchor}.
102
103
  */
@@ -144,6 +145,39 @@ export interface BuildBaseline {
144
145
  * as "unknown" when absent.
145
146
  */
146
147
  recordedBy?: string;
148
+ /**
149
+ * The process that wrote this baseline: its pid, its invocation id (the
150
+ * one its history records carry) and the caller's `FIREFORGE_SESSION_ID`
151
+ * when set. A coverage refusal names it, so a run whose claim another
152
+ * session's scoped build replaced can tell whose build that was.
153
+ */
154
+ recorder?: BaselineRecorder;
155
+ /**
156
+ * Set when this build compiled a negative control into the binary
157
+ * (`test --negative-control --build` on compiled source). The source is
158
+ * restored byte-exact after the run, but the objdir binary keeps the
159
+ * mutation until a build recompiles it. FireForge runs that restore build
160
+ * itself, inside the same lock hold, and its baseline drops the stamp.
161
+ * A stamp still present (the run was killed, or the restore build
162
+ * failed) makes the next `fireforge test` refuse with `control-built`
163
+ * until `fireforge build` replaces the baseline.
164
+ */
165
+ controlBuild?: ControlBuildStamp;
166
+ }
167
+ /** See {@link BuildBaseline.controlBuild}. */
168
+ export interface ControlBuildStamp {
169
+ /** The `--negative-control` diff file, as given. */
170
+ diffFile: string;
171
+ /** Engine-relative compiled targets the control edited. */
172
+ targets: string[];
173
+ /** ISO-8601 time the control was applied. */
174
+ appliedAt: string;
175
+ }
176
+ /** See {@link BuildBaseline.recorder}. */
177
+ export interface BaselineRecorder {
178
+ pid: number;
179
+ invocationId: string;
180
+ sessionId?: string;
147
181
  }
148
182
  /**
149
183
  * State of the engine at the last full build, as far as compiled-in XPCOM
@@ -1,3 +1,32 @@
1
1
  import { type BuildBaseline } from './build-baseline-types.js';
2
- /** Validates persisted fields without converting malformed coverage into a full claim. */
2
+ /**
3
+ * The first field of a persisted baseline that fails validation, and the
4
+ * offending value: what a refusal names so the operator can tell a scoped
5
+ * coverage claim from a corrupt marker.
6
+ */
7
+ export interface BuildBaselineProblem {
8
+ /** Field path, e.g. `testPackagingCoverage[2]` or `testCoverageRecords["a/"]`. */
9
+ field: string;
10
+ /** The offending value, as found (may be any JSON value). */
11
+ value: unknown;
12
+ }
13
+ /** True when the problem sits in a packaging-coverage field. */
14
+ export declare function isCoverageProblem(problem: BuildBaselineProblem): boolean;
15
+ /** Renders a problem for a refusal: the field and a bounded JSON rendering of its value. */
16
+ export declare function describeBuildBaselineProblem(problem: BuildBaselineProblem): string;
17
+ /**
18
+ * Validates persisted fields without converting malformed coverage into a
19
+ * full claim, and returns the first failing field with its value, or
20
+ * undefined when the baseline is valid.
21
+ *
22
+ * @param value - The parsed `last-build.json`
23
+ */
24
+ export declare function findBuildBaselineProblem(value: unknown): BuildBaselineProblem | undefined;
25
+ /**
26
+ * Validates persisted fields without converting malformed coverage into a
27
+ * full claim. The boolean form of {@link findBuildBaselineProblem}.
28
+ *
29
+ * @internal Exported only so tests can reach it. Production reads go
30
+ * through `findBuildBaselineProblem`, which names the failing field.
31
+ */
3
32
  export declare function isBuildBaseline(value: unknown): value is BuildBaseline;
@@ -24,49 +24,150 @@ function isCoverageAnchor(value) {
24
24
  isFingerprint(value['mozconfigHash']) &&
25
25
  isFingerprintMap(value['fingerprints']));
26
26
  }
27
- function isCoverageRecordMap(value) {
27
+ /** A `controlBuild` stamp: the control's diff file, its targets and when it went in. */
28
+ function isControlBuildStamp(value) {
28
29
  return (isObject(value) &&
29
- Object.entries(value).every(([path, record]) => isBaselinePath(path) &&
30
- isCoverageAnchor(record) &&
31
- isObject(record) &&
32
- Array.isArray(record['inputs']) &&
33
- record['inputs'].every(isBaselinePath)));
30
+ typeof value['diffFile'] === 'string' &&
31
+ Array.isArray(value['targets']) &&
32
+ value['targets'].every(isBaselinePath) &&
33
+ typeof value['appliedAt'] === 'string' &&
34
+ Number.isFinite(Date.parse(value['appliedAt'])));
34
35
  }
35
- /** Validates persisted fields without converting malformed coverage into a full claim. */
36
- export function isBuildBaseline(value) {
36
+ /** A `recorder` stamp: an integer pid, an invocation id and an optional session id. */
37
+ function isBaselineRecorder(value) {
38
+ return (isObject(value) &&
39
+ Number.isInteger(value['pid']) &&
40
+ typeof value['invocationId'] === 'string' &&
41
+ (value['sessionId'] === undefined || typeof value['sessionId'] === 'string'));
42
+ }
43
+ /** Fields whose problem a scoped `test --build` can leave and a fresh one replaces. */
44
+ const COVERAGE_FIELDS = ['testPackagingCoverage', 'testCoverageRecords', 'fullCoverageAnchor'];
45
+ /** True when the problem sits in a packaging-coverage field. */
46
+ export function isCoverageProblem(problem) {
47
+ return COVERAGE_FIELDS.some((field) => problem.field === field ||
48
+ problem.field.startsWith(`${field}[`) ||
49
+ problem.field.startsWith(`${field}.`));
50
+ }
51
+ /** Renders a problem for a refusal: the field and a bounded JSON rendering of its value. */
52
+ export function describeBuildBaselineProblem(problem) {
53
+ let rendered;
54
+ try {
55
+ // JSON.stringify(undefined) returns undefined at runtime despite its type.
56
+ const json = JSON.stringify(problem.value);
57
+ rendered = json ?? 'undefined';
58
+ }
59
+ catch {
60
+ rendered = String(problem.value);
61
+ }
62
+ if (rendered.length > 120)
63
+ rendered = `${rendered.slice(0, 117)}...`;
64
+ return `${problem.field} = ${rendered}`;
65
+ }
66
+ function findFingerprintMapProblem(field, value) {
67
+ if (!isObject(value))
68
+ return { field, value };
69
+ for (const [path, fingerprint] of Object.entries(value)) {
70
+ if (!isBaselinePath(path))
71
+ return { field: `${field}[${JSON.stringify(path)}]`, value: path };
72
+ if (!isFingerprint(fingerprint)) {
73
+ return { field: `${field}[${JSON.stringify(path)}]`, value: fingerprint };
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+ function findCoverageRecordProblem(value) {
79
+ const field = 'testCoverageRecords';
80
+ if (!isObject(value))
81
+ return { field, value };
82
+ for (const [path, record] of Object.entries(value)) {
83
+ const entry = `${field}[${JSON.stringify(path)}]`;
84
+ if (!isBaselinePath(path))
85
+ return { field: entry, value: path };
86
+ if (!isCoverageAnchor(record) || !isObject(record) || !Array.isArray(record['inputs'])) {
87
+ return { field: entry, value: record };
88
+ }
89
+ const inputs = record['inputs'];
90
+ const input = inputs.find((item) => !isBaselinePath(item));
91
+ if (input !== undefined)
92
+ return { field: `${entry}.inputs`, value: input };
93
+ }
94
+ return undefined;
95
+ }
96
+ /**
97
+ * Validates persisted fields without converting malformed coverage into a
98
+ * full claim, and returns the first failing field with its value, or
99
+ * undefined when the baseline is valid.
100
+ *
101
+ * @param value - The parsed `last-build.json`
102
+ */
103
+ export function findBuildBaselineProblem(value) {
37
104
  if (!isObject(value))
38
- return false;
39
- if (typeof value['engineHeadSha'] !== 'string' ||
40
- typeof value['builtAt'] !== 'string' ||
41
- !Number.isFinite(Date.parse(value['builtAt'])) ||
42
- typeof value['binaryName'] !== 'string' ||
43
- !/^[a-z][a-z0-9-]*$/.test(value['binaryName']))
44
- return false;
105
+ return { field: '(root)', value };
106
+ if (typeof value['engineHeadSha'] !== 'string') {
107
+ return { field: 'engineHeadSha', value: value['engineHeadSha'] };
108
+ }
109
+ if (typeof value['builtAt'] !== 'string' || !Number.isFinite(Date.parse(value['builtAt']))) {
110
+ return { field: 'builtAt', value: value['builtAt'] };
111
+ }
112
+ if (typeof value['binaryName'] !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value['binaryName'])) {
113
+ return { field: 'binaryName', value: value['binaryName'] };
114
+ }
45
115
  for (const field of [
46
116
  'packageableFingerprints',
47
117
  'testInputFingerprints',
48
118
  'buildInputFingerprints',
49
119
  ]) {
50
- if (value[field] !== undefined && !isFingerprintMap(value[field]))
51
- return false;
120
+ if (value[field] === undefined)
121
+ continue;
122
+ const problem = findFingerprintMapProblem(field, value[field]);
123
+ if (problem !== undefined)
124
+ return problem;
52
125
  }
53
126
  const coverage = value['testPackagingCoverage'];
54
- if (coverage !== undefined &&
55
- coverage !== 'full' &&
56
- !(Array.isArray(coverage) && coverage.every(isBaselinePath)))
57
- return false;
58
- if (value['mozconfigHash'] !== undefined && !isFingerprint(value['mozconfigHash']))
59
- return false;
60
- if (value['recordedBy'] !== undefined && typeof value['recordedBy'] !== 'string')
61
- return false;
62
- if (value['testCoverageRecords'] !== undefined &&
63
- !isCoverageRecordMap(value['testCoverageRecords']))
64
- return false;
65
- if (value['fullCoverageAnchor'] !== undefined && !isCoverageAnchor(value['fullCoverageAnchor']))
66
- return false;
127
+ if (coverage !== undefined && coverage !== 'full') {
128
+ if (!Array.isArray(coverage))
129
+ return { field: 'testPackagingCoverage', value: coverage };
130
+ const index = coverage.findIndex((path) => !isBaselinePath(path));
131
+ if (index >= 0) {
132
+ return { field: `testPackagingCoverage[${index}]`, value: coverage[index] };
133
+ }
134
+ }
135
+ if (value['mozconfigHash'] !== undefined && !isFingerprint(value['mozconfigHash'])) {
136
+ return { field: 'mozconfigHash', value: value['mozconfigHash'] };
137
+ }
138
+ if (value['recordedBy'] !== undefined && typeof value['recordedBy'] !== 'string') {
139
+ return { field: 'recordedBy', value: value['recordedBy'] };
140
+ }
141
+ if (value['recorder'] !== undefined && !isBaselineRecorder(value['recorder'])) {
142
+ return { field: 'recorder', value: value['recorder'] };
143
+ }
144
+ if (value['testCoverageRecords'] !== undefined) {
145
+ const problem = findCoverageRecordProblem(value['testCoverageRecords']);
146
+ if (problem !== undefined)
147
+ return problem;
148
+ }
149
+ if (value['fullCoverageAnchor'] !== undefined && !isCoverageAnchor(value['fullCoverageAnchor'])) {
150
+ return { field: 'fullCoverageAnchor', value: value['fullCoverageAnchor'] };
151
+ }
67
152
  const components = value['staticComponentsBaseline'];
68
- return (components === undefined ||
69
- (isObject(components) &&
153
+ if (components !== undefined &&
154
+ !(isObject(components) &&
70
155
  typeof components['engineHeadSha'] === 'string' &&
71
- isFingerprintMap(components['fingerprints'])));
156
+ isFingerprintMap(components['fingerprints']))) {
157
+ return { field: 'staticComponentsBaseline', value: components };
158
+ }
159
+ if (value['controlBuild'] !== undefined && !isControlBuildStamp(value['controlBuild'])) {
160
+ return { field: 'controlBuild', value: value['controlBuild'] };
161
+ }
162
+ return undefined;
163
+ }
164
+ /**
165
+ * Validates persisted fields without converting malformed coverage into a
166
+ * full claim. The boolean form of {@link findBuildBaselineProblem}.
167
+ *
168
+ * @internal Exported only so tests can reach it. Production reads go
169
+ * through `findBuildBaselineProblem`, which names the failing field.
170
+ */
171
+ export function isBuildBaseline(value) {
172
+ return findBuildBaselineProblem(value) === undefined;
72
173
  }