@hominis/fireforge 0.44.4 → 0.45.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 (66) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/doctor-source-pin.d.ts +29 -0
  4. package/dist/src/commands/doctor-source-pin.js +81 -0
  5. package/dist/src/commands/doctor.js +2 -0
  6. package/dist/src/commands/export-flow.js +3 -18
  7. package/dist/src/commands/patch/move-files-into.js +3 -6
  8. package/dist/src/commands/patch/split-plan.js +3 -6
  9. package/dist/src/commands/re-export-adjacent.d.ts +8 -0
  10. package/dist/src/commands/re-export-adjacent.js +26 -1
  11. package/dist/src/commands/re-export-drift.d.ts +8 -0
  12. package/dist/src/commands/re-export-drift.js +61 -0
  13. package/dist/src/commands/re-export-files.js +3 -6
  14. package/dist/src/commands/re-export-options.js +19 -5
  15. package/dist/src/commands/re-export-register.js +5 -1
  16. package/dist/src/commands/re-export-scan.js +3 -6
  17. package/dist/src/commands/re-export.js +16 -16
  18. package/dist/src/commands/test-build-phase.js +2 -1
  19. package/dist/src/commands/test-diagnose.js +14 -5
  20. package/dist/src/commands/test-register.js +1 -0
  21. package/dist/src/commands/test-run.js +4 -3
  22. package/dist/src/commands/test-stale-gate.js +5 -2
  23. package/dist/src/commands/test.js +9 -2
  24. package/dist/src/core/build-audit.js +44 -2
  25. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  26. package/dist/src/core/build-jar-escalation.js +127 -0
  27. package/dist/src/core/build-prepare.d.ts +12 -1
  28. package/dist/src/core/build-prepare.js +36 -3
  29. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  30. package/dist/src/core/config-validate-file-size.js +65 -0
  31. package/dist/src/core/config-validate.js +6 -0
  32. package/dist/src/core/harness-orphans.d.ts +73 -0
  33. package/dist/src/core/harness-orphans.js +186 -0
  34. package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
  35. package/dist/src/core/mach-known-noise-filter.js +24 -1
  36. package/dist/src/core/mach.d.ts +7 -0
  37. package/dist/src/core/mach.js +18 -2
  38. package/dist/src/core/patch-apply.js +49 -20
  39. package/dist/src/core/patch-lint-checkjs.js +6 -1
  40. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  41. package/dist/src/core/patch-lint-file-size.js +53 -0
  42. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  43. package/dist/src/core/patch-lint-prettier.js +126 -0
  44. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  45. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  46. package/dist/src/core/patch-lint.js +23 -24
  47. package/dist/src/core/patch-transform.d.ts +21 -0
  48. package/dist/src/core/patch-transform.js +38 -0
  49. package/dist/src/core/smoke-patterns.js +12 -0
  50. package/dist/src/core/test-harness-crash.js +61 -48
  51. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  52. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  53. package/dist/src/core/test-stale-check.d.ts +23 -6
  54. package/dist/src/core/test-stale-check.js +75 -2
  55. package/dist/src/core/test-stall-triage.js +37 -19
  56. package/dist/src/errors/build.d.ts +28 -0
  57. package/dist/src/errors/build.js +49 -0
  58. package/dist/src/types/commands/options.d.ts +19 -0
  59. package/dist/src/types/commands/patches.d.ts +17 -6
  60. package/dist/src/types/config.d.ts +41 -0
  61. package/dist/src/utils/fs.d.ts +4 -2
  62. package/dist/src/utils/fs.js +3 -1
  63. package/docs/configuration.md +47 -0
  64. package/docs/exit-codes.md +4 -2
  65. package/docs/testing.md +62 -10
  66. package/package.json +1 -1
@@ -38,33 +38,51 @@ const NO_OUTPUT_STALL_CONTROL_STEP = 'First, run a known-good control test. If t
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
40
  * in `mochitest-server-port.ts` now refuses it before a run starts.
41
- * - (3) is a single downstream report (one consumer, 3/3 reproduce and 3/3
42
- * restore on one variable) whose MECHANISM was never root-caused. It is
43
- * stated as the correlation it is. An earlier revision asserted "stalls
44
- * first paint" as fact; that was a causal story written on top of the
45
- * correlation, and a doubtful one — a failed image load in Gecko fires
46
- * `error` and does not gate the document load event, and the harness's
47
- * browser-ready handshake does not wait on image decode. Do not restore
48
- * a mechanism claim here without a root cause to cite.
41
+ * - (3) is now MECHANICAL and root-caused, replacing the correlation an
42
+ * earlier revision recorded. A `chrome://` (or `resource://`) URL that
43
+ * resolves to nothing reaches `CheckForBrokenChromeURL`
44
+ * (netwerk/base/nsNetUtil.cpp), which outside automation only
45
+ * `printf_stderr`s `Missing chrome or resource URL: <uri>` — but under
46
+ * `xpc::IsInAutomation()` the same condition is
47
+ * `MOZ_CRASH_UNSAFE_PRINTF("Missing chrome or resource URLs: %s")`. A
48
+ * downstream reproduction symbolicated the faulting frame as
49
+ * `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
53
+ * document load event.
49
54
  *
50
- * Detection (a named signature, as with the module-resolution SIGSEGV) is
51
- * not specified for (3) because no log line is known to key on. The bar for
52
- * adding one is the bar `mach-error-hints.ts` states: an established
53
- * diagnosis, never a plausible guess.
55
+ * Detection by log line is deliberately NOT specified for (3), and the
56
+ * reason is the mechanism itself: in the reproduction the crash landed in a
57
+ * CONTENT process (`plugin-container`, a child of the browser) with the
58
+ * crash reporter compiled out, so the harness log carried neither the crash
59
+ * message nor a crash line — only the no-output timeout, `Ran 0 checks`,
60
+ * and mochitest's "Can't trigger Breakpad, just killing process". Under
61
+ * automation the message is in the process that DIED, not in the log, so
62
+ * the census points at the two artefacts that do carry it: the OS crash
63
+ * report (whose faulting frame names `CheckForBrokenChromeURL`) and the
64
+ * 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
66
+ * agrees with the capture).
54
67
  */
55
68
  const NO_OUTPUT_STALL_TRIAGE = [
56
69
  ' 1. A sleeping or locked display on an unattended HEADED run (macOS). The browser never ' +
57
70
  'paints and never reaches its first test.',
58
71
  ' 2. A headless SWGL compositor failure — re-run with --headless to separate this from (1): ' +
59
72
  'if --headless passes, the stall was display/compositor-side, not product-side.',
60
- ' 3. Product-side startup: the browser never reaches the state the harness waits for. One ' +
61
- 'downstream report correlates this stall with a broken `chrome://` image URI on the ' +
62
- 'startup page (reproduced and un-reproduced on that one variable); the mechanism was ' +
63
- 'never root-caused, so treat it as a lead, not an explanation. Discriminate with ' +
73
+ ' 3. A broken `chrome://` or `resource://` URL reached from the startup document. Under ' +
74
+ 'automation this is a MOZ_CRASH in whichever process opened the channel ' +
75
+ '(CheckForBrokenChromeURL, netwerk/base/nsNetUtil.cpp), so the browser dies before the ' +
76
+ 'harness sees anything. Expect the log to name NOTHING: with the crash in a content ' +
77
+ 'process and the crash reporter compiled out, the only artefacts are the OS crash report ' +
78
+ '(macOS: ~/Library/Logs/DiagnosticReports/plugin-container-*.ips — the faulting frame ' +
79
+ 'reads CheckForBrokenChromeURL) and the smoke probe below. Discriminate with ' +
64
80
  '"fireforge run --smoke-exit 60 --headless --capture-console <file>", which launches the ' +
65
- 'same browser OUTSIDE the test harness: a clean window with no unallowed console errors ' +
66
- 'means product-side startup is healthy and the stall is harness- or test-side; a hang, a ' +
67
- 'non-zero exit, or console errors in <file> put it in the startup document.',
81
+ 'same browser OUTSIDE automation, where the same condition only PRINTS ' +
82
+ '"Missing chrome or resource URL: <uri>" — a line the probe counts as an unallowed ' +
83
+ 'error, so its exit code and its capture now agree. A clean window with no unallowed ' +
84
+ 'console errors means product-side startup is healthy and the stall is harness- or ' +
85
+ 'test-side.',
68
86
  ' 4. A stale mochitest httpd holding the harness server port (127.0.0.1:8888). A fresh ' +
69
87
  "browser connects to THAT server, which cannot serve this run's manifest, so the run " +
70
88
  'stalls before TEST_START. Probe with "lsof -nP -iTCP:8888": any listener that is not ' +
@@ -9,6 +9,34 @@ export declare class BuildError extends FireForgeError {
9
9
  constructor(message: string, command?: string | undefined, cause?: Error);
10
10
  get userMessage(): string;
11
11
  }
12
+ /**
13
+ * A test SUITE went red — assertions failed, or sharded runs did not all
14
+ * pass. Distinct from its {@link BuildError} parent only in what it TELLS
15
+ * the operator, deliberately not in its exit code: `docs/exit-codes.md`
16
+ * documents 5 as "a suite that FAILED", and consumers key CI on it.
17
+ *
18
+ * The parent's remedies are all build remedies — "check the build output",
19
+ * "fireforge bootstrap", "delete obj-* directories" — and none of them apply
20
+ * to a failing assertion. Worse, the first points at the wrong half of the
21
+ * log, which is exactly the half a `tail` keeps. Ground truth for a test red
22
+ * is the harness's own output, so the remedies here name the failing tests,
23
+ * the `FIREFORGE-VERDICT:` line, and the run log that survives a pipe.
24
+ *
25
+ * Note that `machineErrorCode` derives the `--json` error code from the class
26
+ * name, so this reports as `test-failure` rather than `build`.
27
+ */
28
+ export declare class TestFailureError extends BuildError {
29
+ /** Rendered TEST-UNEXPECTED blocks, when the verdict carried any. */
30
+ readonly failureBlocks?: string | undefined;
31
+ /** The run's complete raw log, when one was opened. */
32
+ readonly logPath?: string | undefined;
33
+ constructor(message: string, command?: string,
34
+ /** Rendered TEST-UNEXPECTED blocks, when the verdict carried any. */
35
+ failureBlocks?: string | undefined,
36
+ /** The run's complete raw log, when one was opened. */
37
+ logPath?: string | undefined);
38
+ get userMessage(): string;
39
+ }
12
40
  /**
13
41
  * Error thrown when mach is not available.
14
42
  */
@@ -23,6 +23,55 @@ export class BuildError extends FireForgeError {
23
23
  return msg;
24
24
  }
25
25
  }
26
+ /**
27
+ * A test SUITE went red — assertions failed, or sharded runs did not all
28
+ * pass. Distinct from its {@link BuildError} parent only in what it TELLS
29
+ * the operator, deliberately not in its exit code: `docs/exit-codes.md`
30
+ * documents 5 as "a suite that FAILED", and consumers key CI on it.
31
+ *
32
+ * The parent's remedies are all build remedies — "check the build output",
33
+ * "fireforge bootstrap", "delete obj-* directories" — and none of them apply
34
+ * to a failing assertion. Worse, the first points at the wrong half of the
35
+ * log, which is exactly the half a `tail` keeps. Ground truth for a test red
36
+ * is the harness's own output, so the remedies here name the failing tests,
37
+ * the `FIREFORGE-VERDICT:` line, and the run log that survives a pipe.
38
+ *
39
+ * Note that `machineErrorCode` derives the `--json` error code from the class
40
+ * name, so this reports as `test-failure` rather than `build`.
41
+ */
42
+ export class TestFailureError extends BuildError {
43
+ failureBlocks;
44
+ logPath;
45
+ constructor(message, command,
46
+ /** Rendered TEST-UNEXPECTED blocks, when the verdict carried any. */
47
+ failureBlocks,
48
+ /** The run's complete raw log, when one was opened. */
49
+ logPath) {
50
+ super(message, command);
51
+ this.failureBlocks = failureBlocks;
52
+ this.logPath = logPath;
53
+ }
54
+ get userMessage() {
55
+ let msg = `Test Failure: ${this.message}`;
56
+ if (this.command) {
57
+ msg += `\n\nCommand: ${this.command}`;
58
+ }
59
+ if (this.failureBlocks) {
60
+ msg += `\n\n${this.failureBlocks}`;
61
+ }
62
+ msg += '\n\nTo fix this:\n';
63
+ msg += this.failureBlocks
64
+ ? ' 1. Fix the failing assertions listed above, then re-run just those test paths\n'
65
+ : ' 1. Find the TEST-UNEXPECTED-FAIL lines in the output above — they name what broke\n';
66
+ msg +=
67
+ ' 2. Read the "FIREFORGE-VERDICT:" line for the run\'s ground truth (checks, unexpected)\n';
68
+ msg +=
69
+ this.logPath !== undefined
70
+ ? ` 3. The complete raw output is at ${this.logPath} — it survives a piped/tailed terminal`
71
+ : ' 3. Re-run without piping the output, so the failure lines are not cut off by "tail"';
72
+ return msg;
73
+ }
74
+ }
26
75
  /**
27
76
  * Error thrown when mach is not available.
28
77
  */
@@ -322,6 +322,16 @@ export interface ReExportOptions {
322
322
  * else still refuses.
323
323
  */
324
324
  expect?: string[];
325
+ /**
326
+ * Engine-relative files that are an APPROVED unmanaged exception
327
+ * (`--expect-unmanaged`, repeatable; requires
328
+ * `--refuse-adjacent-unmanaged`). The adjacency refusal is all-or-nothing
329
+ * without it: a project with a reviewed, deliberately unmanaged path beside
330
+ * a patch's ownership has to drop the flag entirely and hand-read the
331
+ * notice, which is a weaker belt than the one being disarmed. Approved
332
+ * paths are still LISTED — they are carved out of the refusal, not hidden.
333
+ */
334
+ expectUnmanaged?: string[];
325
335
  }
326
336
  /**
327
337
  * Options for the `fireforge patch tier` subcommand. Sets or clears the
@@ -601,6 +611,15 @@ export interface TestOptions {
601
611
  marionettePort?: number;
602
612
  /** Kill a recognized stale browser process holding the Marionette port, then continue. */
603
613
  killStaleMarionette?: boolean;
614
+ /**
615
+ * Terminate harness helper processes (`xpcshell` httpd, `pywebsocket`,
616
+ * `ssltunnel`, `moz-http2`) that survived an earlier run in this
617
+ * project's objdir. The census itself runs on EVERY test dispatch and is
618
+ * report-only; this flag opts into the kill. Survivors slow later runs
619
+ * without appearing in their output, so the visibility is the default and
620
+ * the termination is the choice.
621
+ */
622
+ reapOrphans?: boolean;
604
623
  /** Permit tests against packageable engine edits newer than the last successful build. */
605
624
  allowStaleBuild?: boolean;
606
625
  /**
@@ -32,13 +32,24 @@ export interface PatchResult {
32
32
  /** Whether the patch was auto-resolved (new file vs existing file conflict) */
33
33
  autoResolved?: boolean;
34
34
  /**
35
- * Pre-existing content of files the auto-resolve overwrote, keyed by
36
- * engine-relative path. Kept for the RUN's duration (not just the retry):
37
- * if a later patch fails and `rollbackPatches` reverse-applies this one,
38
- * reversing a new-file patch DELETES the file — these snapshots are the
39
- * only way to restore what was there before the auto-resolve.
35
+ * Pre-existing BYTES of files the auto-resolve overwrote (or, for a binary
36
+ * new file, removed), keyed by engine-relative path. Kept for the RUN's
37
+ * duration (not just the retry): if a later patch fails and
38
+ * `rollbackPatches` reverse-applies this one, reversing a new-file patch
39
+ * DELETES the file — these snapshots are the only way to restore what was
40
+ * there before the auto-resolve.
41
+ *
42
+ * Raw bytes rather than decoded text, because the same snapshot has to
43
+ * restore a binary target byte-for-byte.
44
+ *
45
+ * Typed `Uint8Array`, not `Buffer`: this interface is published, and the
46
+ * emitted `.d.ts` naming a Node global made a consumer without
47
+ * `@types/node` fail to compile against the package (`TS2591` in the
48
+ * pack smoke test). Every `Buffer` assigns to it — `Buffer` extends
49
+ * `Uint8Array` — and `fs.writeFile` accepts it, so nothing at the call
50
+ * sites changes.
40
51
  */
41
- autoResolvedOriginals?: Map<string, string>;
52
+ autoResolvedOriginals?: Map<string, Uint8Array>;
42
53
  }
43
54
  /**
44
55
  * Extended patch information with metadata.
@@ -226,6 +226,35 @@ export interface PatchLintCheckJsCompilerOptions {
226
226
  /** Module-resolution `paths` mapping (pattern → targets, engine-relative). */
227
227
  paths?: Record<string, string[]>;
228
228
  }
229
+ /**
230
+ * Line-count thresholds for one `file-too-large` tier.
231
+ *
232
+ * Every field is optional; an omitted field keeps the built-in default.
233
+ * The three must stay ordered `notice <= warning <= error`, which the
234
+ * config validator enforces — an out-of-order triple silently disables a
235
+ * band rather than failing anything.
236
+ */
237
+ export interface PatchLintFileSizeTier {
238
+ notice?: number;
239
+ warning?: number;
240
+ error?: number;
241
+ }
242
+ /**
243
+ * Overrides for the `file-too-large` thresholds, per file class.
244
+ *
245
+ * These were module constants until 0.45.0, and nothing in `fireforge.json`
246
+ * could move them. That is a problem specifically because the recommended
247
+ * gate posture is `--max-warnings 0`: under it the `warning` band is not a
248
+ * soft limit at all, so a project whose controllers legitimately sit a few
249
+ * lines over 750 has a hard failure with no dial, at exactly the moment
250
+ * someone is deciding whether to restructure a file.
251
+ */
252
+ export interface PatchLintFileSizeThresholds {
253
+ /** Non-test files. Defaults: notice 500, warning 750, error 900. */
254
+ general?: PatchLintFileSizeTier;
255
+ /** Test files. Defaults: notice 1200, warning 1400, error 1600. */
256
+ test?: PatchLintFileSizeTier;
257
+ }
229
258
  /**
230
259
  * Configuration for patch lint rules.
231
260
  */
@@ -272,6 +301,18 @@ export interface PatchLintConfig {
272
301
  checkJsTestShim?: string;
273
302
  /** File paths exempt from the raw-color-value check (exact or basename match) */
274
303
  rawColorAllowlist?: string[];
304
+ /**
305
+ * Per-tier overrides for the `file-too-large` line-count thresholds.
306
+ * Unset fields keep the built-in defaults.
307
+ */
308
+ fileSizeThresholds?: PatchLintFileSizeThresholds;
309
+ /**
310
+ * Run the project's Prettier over patch-owned `.sys.mjs` modules, from
311
+ * inside `engine/` so the engine's own `.prettierrc*`/`.prettierignore`
312
+ * decide. Default `'off'`: when off, formatting is explicitly out of
313
+ * scope for the per-patch tier.
314
+ */
315
+ prettier?: PatchLintSeverityGate;
275
316
  /** Enforce JSDoc on class-method exports in patch-owned .sys.mjs files. Default: 'off'. */
276
317
  jsdocClassMethods?: PatchLintSeverityGate;
277
318
  /** Require ≥1 assertion in any patch-touched browser_*.js test file (new or modified). Default: 'off'. */
@@ -76,9 +76,11 @@ export declare function writeTextIfChanged(path: string, content: string): Promi
76
76
  * Writes content atomically using a temp-file-and-rename strategy.
77
77
  * Temp files are created in the destination directory so rename stays atomic.
78
78
  * @param path - Destination file path
79
- * @param content - Content to write
79
+ * @param content - Content to write. `Uint8Array` rather than `Buffer` so
80
+ * published types never name a Node global (every `Buffer` still assigns
81
+ * to it, and `fs.writeFile` accepts both).
80
82
  */
81
- export declare function writeFileAtomic(path: string, content: string | Buffer): Promise<void>;
83
+ export declare function writeFileAtomic(path: string, content: string | Uint8Array): Promise<void>;
82
84
  /**
83
85
  * Copies a directory recursively.
84
86
  * @param src - Source directory path
@@ -165,7 +165,9 @@ export async function writeTextIfChanged(path, content) {
165
165
  * Writes content atomically using a temp-file-and-rename strategy.
166
166
  * Temp files are created in the destination directory so rename stays atomic.
167
167
  * @param path - Destination file path
168
- * @param content - Content to write
168
+ * @param content - Content to write. `Uint8Array` rather than `Buffer` so
169
+ * published types never name a Node global (every `Buffer` still assigns
170
+ * to it, and `fs.writeFile` accepts both).
169
171
  */
170
172
  export async function writeFileAtomic(path, content) {
171
173
  await ensureParentDir(path);
@@ -7,6 +7,24 @@
7
7
  whose behaviour needs more than a schema line, plus the environment variables
8
8
  FireForge reads.
9
9
 
10
+ ## `firefox` — the source pin
11
+
12
+ `firefox.version` (with `firefox.product`, and optionally `firefox.sha256` and
13
+ `firefox.candidate`) is the source pin `fireforge download` resolves an archive
14
+ from. `fireforge source set` writes it.
15
+
16
+ The pin lives in `fireforge.json` beside hand-maintained policy sections, which
17
+ is worth knowing when reverting that file: `git checkout -- fireforge.json`
18
+ after an accidental reformat also reverts an **uncommitted** pin, and the tree
19
+ is then building a different version than the config claims.
20
+
21
+ `fireforge doctor` reports the pin beside what the checkout actually is — the
22
+ engine's own `browser/config/version.txt` and the version the last download
23
+ recorded — and warns when they disagree. It is a report, not a lock: a tree
24
+ may legitimately be mid-migration. `product`, `sha256` and `candidate` have no
25
+ recorded counterpart on disk, so the check does not claim to have verified
26
+ them.
27
+
10
28
  ## External toolchains
11
29
 
12
30
  Projects with generated asset prerequisites can declare opt-in
@@ -33,6 +51,35 @@ shim that tracks upstream WebIDL additions per release.
33
51
  - `patchLint.checkJsTestShim` points at a project `.d.ts` whose typed
34
52
  declarations (e.g. a real `TestUtils` interface) override the loose
35
53
  baseline, so calls to nonexistent harness members fail at export time.
54
+ - `patchLint.prettier` (`'off'` | `'warning'` | `'error'`, default `'off'`)
55
+ runs the project's configured Prettier over patch-owned `.sys.mjs`
56
+ modules. It runs from inside `engine/`, so `engine/.prettierrc*` and
57
+ `engine/.prettierignore` decide — the same command run from the repo root
58
+ can report a false pass when the root `.prettierignore` excludes
59
+ `engine/`. Prettier is resolved from `engine/node_modules`, then the
60
+ project's, then `npx --no-install`. **Left `'off'`, formatting is out of
61
+ scope for the per-patch tier**; no other tier checks `.sys.mjs`
62
+ formatting either.
63
+ - `patchLint.fileSizeThresholds` tunes the `file-too-large` line counts per
64
+ file class:
65
+
66
+ ```json
67
+ {
68
+ "patchLint": {
69
+ "fileSizeThresholds": {
70
+ "general": { "notice": 500, "warning": 800, "error": 900 },
71
+ "test": { "warning": 1500 }
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ Every field is optional and merges over the defaults (general
78
+ `500/750/900`, test `1200/1400/1600`). The merged triple must satisfy
79
+ `notice <= warning <= error`, which is validated at config load. Note that
80
+ under the recommended gate posture `--max-warnings 0` the `warning` band is
81
+ a hard failure, not a soft limit — which is what these thresholds exist to
82
+ let you move.
36
83
 
37
84
  ## `patchPolicy`
38
85
 
@@ -14,7 +14,7 @@ they mean and which class produces each.
14
14
  | 2 | `CONFIG_ERROR` | `fireforge.json` missing or invalid. | `ConfigError` and subclasses |
15
15
  | 3 | `DOWNLOAD_ERROR` | Could not download or extract Firefox source. | `DownloadError` and subclasses |
16
16
  | 4 | `GIT_ERROR` | A git operation failed. | `GitError` and subclasses |
17
- | 5 | `BUILD_ERROR` | `mach build` failed. | `BuildError` and subclasses |
17
+ | 5 | `BUILD_ERROR` | `mach build` failed, or a test suite went red. | `BuildError` and subclasses, `TestFailureError` |
18
18
  | 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated. | `PatchError` |
19
19
  | 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found. | `PythonNotFoundError`, `GitNotFoundError`, `MachNotFoundError` |
20
20
  | 8 | `INVALID_ARGUMENT` | The flags were wrong. Usage problem, not an environment one. | `InvalidArgumentError` |
@@ -35,7 +35,9 @@ change would have helped — escalate it, do not retry.
35
35
 
36
36
  **Red vs never-ran vs thrown-away.** These are three different facts and,
37
37
  before 0.44.0, two of them shared exit 1. `BUILD_ERROR` (5) is a suite that
38
- FAILED. `LOCK_TIMEOUT` (15) is a run that never started, because another
38
+ FAILED — thrown as `TestFailureError` since 0.45.0, which keeps the code but
39
+ carries test remedies rather than build ones (`--json` reports it as
40
+ `test-failure`). `LOCK_TIMEOUT` (15) is a run that never started, because another
39
41
  FireForge process held the lock for the whole budget — nothing about the
40
42
  request was wrong, so re-queue it. `INCONCLUSIVE` (14) is a suite whose
41
43
  result was DISCARDED, because `engine/` moved underneath it — the code may
package/docs/testing.md CHANGED
@@ -49,8 +49,13 @@ components):
49
49
  - a changed `moz.build` / `moz.configure` / `Makefile.in` runs
50
50
  `mach configure` first;
51
51
  - a changed `jar.mn` escalates the whole build to a full `mach build`
52
- (minutes rather than seconds), so that new install-manifest destinations
53
- exist before the run.
52
+ (minutes rather than seconds) **only** when it is a NEW manifest
53
+ (untracked in the engine repo, so no install manifest exists for it yet)
54
+ or when its jar declaration carries a bracketed base-directory prefix,
55
+ which redirects the install destination away from the default chrome
56
+ root. An entry added to an existing `dist/bin` manifest no longer
57
+ escalates. Any probe failure (unreadable manifest, no git) escalates, and
58
+ the notice names the manifest and the reason.
54
59
 
55
60
  Each escalation is paid once per actual content change: a `jar.mn` that is
56
61
  dirty against HEAD but byte-identical to what the last full build consumed
@@ -60,20 +65,35 @@ carried forward from the previous full build, so a registration made
60
65
  between a `build --ui` and the next `test --build` still triggers the full
61
66
  build once.
62
67
 
63
- Whether a full build is really required for an entry added to an existing
64
- `jar.mn` under `dist/bin` is an open question, recorded in the 0.44.0
65
- changelog: mozbuild regenerates its backend from a newer `jar.mn` on any
66
- `mach build`, `faster` included, and the FasterMake backend consumes
67
- `dist/bin` jar manifests. The escalation stays until that is demonstrated
68
- end to end against a real objdir (`build --ui` after a jar-only
69
- registration, then a plain `test`), because relaxing a stale-artifact guard
70
- on source reading alone trades a slow build for a silently wrong test.
68
+ The open question the 0.44.0 changelog recorded here — whether a full build
69
+ is really required for an entry added to an existing `dist/bin` `jar.mn` —
70
+ was settled downstream by exactly the experiment that entry asked for: two
71
+ clean runs where a jar-only registration of a new content file was
72
+ installed by a plain `fireforge build --ui`
73
+ (`faster/install_dist_bin_browser` named the destination, the file reached
74
+ `dist/bin` and the `.app` bundle) and fetched over `chrome://` by a plain
75
+ `fireforge test`. That case no longer escalates. The two halves the
76
+ experiment did NOT exercise — a new `jar.mn` file, and a non-default
77
+ install destination — still do, for the reason the original entry gives:
78
+ relaxing a stale-artifact guard without evidence trades a slow build for a
79
+ silently wrong test.
71
80
 
72
81
  ## Resilience
73
82
 
74
83
  - Recognized harness crashes retry up to `--harness-retries <n>` times
75
84
  (default 2).
76
85
  - `--kill-stale-marionette` terminates recognized stale browsers.
86
+ - Every dispatch runs a **census of orphaned harness helpers** — `xpcshell`
87
+ (the harness httpd), `pywebsocket`, `ssltunnel`, `moz-http2` — that
88
+ survived an earlier run in this project's objdir. It runs at preflight,
89
+ before this run spawns anything, so every hit is a survivor by
90
+ construction; a match must be objdir-anchored, since `xpcshell` and
91
+ `server.js` are far too generic to report on their own. Survivors slow
92
+ every later run without appearing in its output (a three-second suite
93
+ taking minutes of wall clock is the usual symptom). Report-only by
94
+ default; `--reap-orphans` terminates them. It never refuses a run.
95
+ A different shape — reparented Python `multiprocessing` workers — is
96
+ covered by the `Orphaned harness workers` doctor check.
77
97
  - `--perf-samples <path>` publishes a perf-sample artifact path to the
78
98
  harness (exported as `<BINARYNAME>_PERF_SAMPLE_JSON`).
79
99
 
@@ -109,6 +129,38 @@ the build path stays quiet. It is opt-in rather than automatic because it
109
129
  changes how much a third party prints, and an operator who wants the quieting
110
130
  should keep it.
111
131
 
132
+ ## Known upstream teardown noise
133
+
134
+ Recent engines can end a run with a Python traceback at harness teardown:
135
+
136
+ ```
137
+ AttributeError: 'SystemResourceMonitor' object has no attribute 'stop_time'
138
+ ```
139
+
140
+ (or `poll_interval`), raised from `mozsystemmonitor/resourcemonitor.py`. It is
141
+ an upstream defect in the resource monitor's own shutdown, it is cosmetic, and
142
+ it does not affect any verdict. It matters only because it lands exactly where
143
+ a reader looks for the failure summary.
144
+
145
+ FireForge recognizes this one signature — and only this one. Recognition
146
+ requires **all** of: an `AttributeError` on `SystemResourceMonitor` naming one
147
+ of those two attributes, a `resourcemonitor.py` stack frame, and (in a test
148
+ run) a preceding `SUITE_END`. A novel attribute, a different exception, or the
149
+ same traceback before shutdown is treated as a real failure and printed
150
+ verbatim, always.
151
+
152
+ The two phases handle it differently, deliberately:
153
+
154
+ - **Tests** collapse the traceback in the terminal echo to one labeled
155
+ `[FireForge]` line. Captures and the run log keep the **raw** traceback, and
156
+ the classifier reads the raw form — a real failure always outranks this
157
+ signature in diagnosis.
158
+ - **Builds** print it verbatim and add a note naming it. A build has no
159
+ `SUITE_END` — no boundary separating teardown from work still in progress —
160
+ so FireForge cannot distinguish cosmetic teardown noise from a real
161
+ build-time traceback there, and withholding the block would be the wrong
162
+ risk to take.
163
+
112
164
  ## Concurrency
113
165
 
114
166
  A test run holds the engine-session lock and snapshots the engine git
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.44.4",
3
+ "version": "0.45.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",