@hominis/fireforge 0.33.0 → 0.34.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 (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/src/commands/build.js +9 -1
  3. package/dist/src/commands/doctor-furnace-jar.d.ts +16 -0
  4. package/dist/src/commands/doctor-furnace-jar.js +47 -0
  5. package/dist/src/commands/doctor-furnace.js +2 -0
  6. package/dist/src/commands/export-flow.js +4 -1
  7. package/dist/src/commands/export-placement-gate.js +4 -1
  8. package/dist/src/commands/export.js +61 -4
  9. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +20 -0
  10. package/dist/src/commands/furnace/chrome-doc-templates.js +52 -0
  11. package/dist/src/commands/furnace/chrome-doc.d.ts +10 -0
  12. package/dist/src/commands/furnace/chrome-doc.js +31 -4
  13. package/dist/src/commands/furnace/create-browser-test.d.ts +30 -0
  14. package/dist/src/commands/furnace/create-browser-test.js +180 -0
  15. package/dist/src/commands/furnace/create-mochikit.js +11 -4
  16. package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
  17. package/dist/src/commands/furnace/create-xpcshell.js +38 -12
  18. package/dist/src/commands/furnace/create.js +7 -114
  19. package/dist/src/commands/furnace/index.js +6 -1
  20. package/dist/src/commands/furnace/scan.d.ts +1 -0
  21. package/dist/src/commands/furnace/scan.js +57 -27
  22. package/dist/src/commands/furnace/validate.js +17 -1
  23. package/dist/src/commands/re-export-options.d.ts +26 -0
  24. package/dist/src/commands/re-export-options.js +48 -1
  25. package/dist/src/commands/re-export.js +4 -1
  26. package/dist/src/commands/register.js +10 -1
  27. package/dist/src/commands/test-diagnose.js +12 -0
  28. package/dist/src/commands/test.js +29 -22
  29. package/dist/src/core/furnace-registration.d.ts +24 -0
  30. package/dist/src/core/furnace-registration.js +56 -3
  31. package/dist/src/core/furnace-validate-registration.js +18 -0
  32. package/dist/src/core/mach-resource-shim.d.ts +53 -16
  33. package/dist/src/core/mach-resource-shim.js +249 -51
  34. package/dist/src/core/mach.d.ts +51 -10
  35. package/dist/src/core/mach.js +76 -32
  36. package/dist/src/core/manifest-helpers.d.ts +13 -0
  37. package/dist/src/core/manifest-helpers.js +1 -1
  38. package/dist/src/core/manifest-rules.d.ts +13 -3
  39. package/dist/src/core/manifest-rules.js +44 -17
  40. package/dist/src/core/patch-export.d.ts +10 -0
  41. package/dist/src/core/patch-export.js +23 -2
  42. package/dist/src/core/register-module.d.ts +1 -1
  43. package/dist/src/core/register-module.js +15 -2
  44. package/dist/src/core/register-result.d.ts +9 -0
  45. package/dist/src/core/register-scaffold.d.ts +55 -0
  46. package/dist/src/core/register-scaffold.js +146 -0
  47. package/dist/src/core/register-test-manifest.js +3 -1
  48. package/dist/src/core/register-xpcshell-test.d.ts +30 -0
  49. package/dist/src/core/register-xpcshell-test.js +96 -0
  50. package/dist/src/core/test-harness-crash.d.ts +26 -1
  51. package/dist/src/core/test-harness-crash.js +121 -11
  52. package/dist/src/core/xpcshell-appdir.d.ts +7 -0
  53. package/dist/src/core/xpcshell-appdir.js +12 -3
  54. package/dist/src/types/commands/options.d.ts +12 -0
  55. package/package.json +3 -3
@@ -23,6 +23,15 @@
23
23
  * of reporting phantom test failures (or phantom passes).
24
24
  */
25
25
  const TEST_START_PATTERN = /\bTEST-START\b/;
26
+ /**
27
+ * Structured-log execution marker (`mach xpcshell-test` mozlog output uses
28
+ * `TEST_START`/`SUITE_END` with underscores, not the hyphenated
29
+ * `TEST-START` the browser-chrome dispatch prints).
30
+ */
31
+ const STRUCTURED_TEST_START_PATTERN = /\bTEST_START\b/;
32
+ const SUITE_END_PATTERN = /\bSUITE_END\b/;
33
+ const GREEN_UNEXPECTED_SUMMARY_PATTERN = /\bUnexpected results:\s*0\b/;
34
+ const NONZERO_UNEXPECTED_SUMMARY_PATTERN = /\bUnexpected results:\s*[1-9]/;
26
35
  /**
27
36
  * Execution signals emitted by the suite-specific xpcshell dispatch
28
37
  * (`mach xpcshell-test`), which does NOT print `TEST-START` lines the way
@@ -63,6 +72,87 @@ function findLine(output, patterns) {
63
72
  }
64
73
  return undefined;
65
74
  }
75
+ /**
76
+ * Non-signal noise lines: the resource-monitor degrade path (FireForge's
77
+ * own guard plus mozlog's `_collect failed`) prints warnings on runs that
78
+ * then complete green. Field report (0.34.0 cycle): every multi-file suite
79
+ * was reported CRASH because these lines matched the startup-traceback
80
+ * signals even though the embedded summary was fully green. They are
81
+ * excluded from crash evidence entirely.
82
+ */
83
+ const NOISE_LINE_PATTERNS = [
84
+ /\bUserWarning\b/,
85
+ /psutil failed to run/i,
86
+ /_collect failed/i,
87
+ /FireForge: host resource monitor degraded/i,
88
+ /warnings\.warn\(/,
89
+ ];
90
+ /** Matches the caught/telemetry context that marks a traceback as benign. */
91
+ const BENIGN_TRACEBACK_CONTEXT = /telemetry|glean/i;
92
+ /**
93
+ * Strips non-signal noise from captured output before crash-signature
94
+ * matching: resource-monitor degradation warnings, and traceback blocks
95
+ * that mach caught itself (telemetry submission tracebacks are printed but
96
+ * never abort the run). The stripped text is used ONLY as crash evidence —
97
+ * classification of test results still reads the full output.
98
+ */
99
+ export function stripNonSignalNoise(output) {
100
+ const lines = output.split(/\r?\n/);
101
+ const kept = [];
102
+ for (let i = 0; i < lines.length; i += 1) {
103
+ const line = lines[i] ?? '';
104
+ if (NOISE_LINE_PATTERNS.some((p) => p.test(line)))
105
+ continue;
106
+ if (TRACEBACK_PATTERN.test(line)) {
107
+ // Collect the whole traceback block: the header, indented frame/code
108
+ // lines, and the trailing unindented exception line.
109
+ const block = [line];
110
+ let j = i + 1;
111
+ for (; j < lines.length; j += 1) {
112
+ const blockLine = lines[j] ?? '';
113
+ block.push(blockLine);
114
+ const isIndented = /^\s/.test(blockLine) || blockLine.trim().length === 0;
115
+ if (!isIndented)
116
+ break; // unindented exception line terminates the block
117
+ }
118
+ if (BENIGN_TRACEBACK_CONTEXT.test(block.join('\n'))) {
119
+ i = j; // drop the whole caught-telemetry traceback block
120
+ continue;
121
+ }
122
+ // A real traceback stays in evidence line-by-line (minus noise lines
123
+ // already filtered above).
124
+ }
125
+ kept.push(line);
126
+ }
127
+ return kept.join('\n');
128
+ }
129
+ /**
130
+ * True when the output carries any execution signal: the browser-chrome
131
+ * `TEST-START` marker, the structured-log `TEST_START` marker, or the
132
+ * suite-specific xpcshell result-summary block.
133
+ */
134
+ function hasExecutionSignal(output) {
135
+ return (TEST_START_PATTERN.test(output) ||
136
+ STRUCTURED_TEST_START_PATTERN.test(output) ||
137
+ hasXpcshellResultSummary(output));
138
+ }
139
+ /**
140
+ * True when the output embeds a COMPLETED, GREEN suite summary: an
141
+ * execution signal, `Unexpected results: 0` (and no non-zero unexpected
142
+ * count), a `SUITE_END` marker, and no real `TEST-UNEXPECTED-*` lines.
143
+ * Such a run finished its suite; any startup-traceback-shaped noise in the
144
+ * same output is by definition non-fatal, so this vetoes signature-based
145
+ * crash classification (field report: fully green sharded runs reported
146
+ * `CRASH (N attempts)` because degradation warnings matched the psutil
147
+ * signals). Exported for direct unit testing.
148
+ */
149
+ export function hasCompletedGreenSummary(output) {
150
+ return (hasExecutionSignal(output) &&
151
+ SUITE_END_PATTERN.test(output) &&
152
+ GREEN_UNEXPECTED_SUMMARY_PATTERN.test(output) &&
153
+ !NONZERO_UNEXPECTED_SUMMARY_PATTERN.test(output) &&
154
+ realUnexpectedFailureLines(output).length === 0);
155
+ }
66
156
  /**
67
157
  * True when the captured output carries the suite-specific xpcshell
68
158
  * result-summary block, which proves tests executed even though the
@@ -83,13 +173,22 @@ function realUnexpectedFailureLines(output) {
83
173
  * Returns undefined for anything that looks like a genuine test result.
84
174
  */
85
175
  export function detectHarnessCrashSignature(output) {
86
- const hasTestStart = TEST_START_PATTERN.test(output);
176
+ const hasTestStart = hasExecutionSignal(output);
87
177
  const realFailures = realUnexpectedFailureLines(output);
88
- // Startup traceback cluster (resource monitor / psutil). Real test
89
- // failures take precedence: a traceback printed during teardown of a
90
- // genuinely failing run must not get the whole run retried.
91
- if (TRACEBACK_PATTERN.test(output) && realFailures.length === 0) {
92
- const signalLine = findLine(output, STARTUP_TRACEBACK_SIGNALS);
178
+ // A completed green embedded summary vetoes signature-based crash
179
+ // classification outright: the suite finished, so any startup-shaped
180
+ // noise in the same output was non-fatal. (The post-green shutdown
181
+ // re-entry shape below is exempt — it is deliberately a crash verdict on
182
+ // an otherwise-green log, keyed on its own explicit markers.)
183
+ const greenSummaryVeto = hasCompletedGreenSummary(output);
184
+ // Startup traceback cluster (resource monitor / psutil), scanned over
185
+ // noise-stripped evidence so degradation warnings and caught telemetry
186
+ // tracebacks never count. Real test failures take precedence: a
187
+ // traceback printed during teardown of a genuinely failing run must not
188
+ // get the whole run retried.
189
+ const evidence = stripNonSignalNoise(output);
190
+ if (!greenSummaryVeto && TRACEBACK_PATTERN.test(evidence) && realFailures.length === 0) {
191
+ const signalLine = findLine(evidence, STARTUP_TRACEBACK_SIGNALS);
93
192
  if (signalLine) {
94
193
  return { reason: 'harness startup traceback (resource monitor/psutil)', line: signalLine };
95
194
  }
@@ -98,7 +197,7 @@ export function detectHarnessCrashSignature(output) {
98
197
  // the no-output timeout. A trailing "Passed: 0" summary is part of this
99
198
  // shape and must not be read as a result.
100
199
  if (!hasTestStart) {
101
- const timeoutLine = findLine(output, [NO_OUTPUT_TIMEOUT_PATTERN]);
200
+ const timeoutLine = findLine(evidence, [NO_OUTPUT_TIMEOUT_PATTERN]);
102
201
  if (timeoutLine) {
103
202
  return { reason: 'no-output timeout before any test started', line: timeoutLine };
104
203
  }
@@ -133,15 +232,26 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
133
232
  if (signature) {
134
233
  return { kind: 'harness-crash', signature };
135
234
  }
136
- const ranTests = TEST_START_PATTERN.test(output) || hasXpcshellResultSummary(output);
235
+ const ranTests = hasExecutionSignal(output);
137
236
  if (!ranTests && requestedPaths.length > 0) {
138
237
  return { kind: 'no-tests' };
139
238
  }
140
- return exitCode === 0 ? { kind: 'tests-ran-ok' } : { kind: 'test-failures' };
239
+ if (exitCode === 0) {
240
+ return { kind: 'tests-ran-ok' };
241
+ }
242
+ // Exit codes follow the corrected verdict: a run whose embedded summary
243
+ // completed green is a pass even when the wrapper exit code went
244
+ // non-zero on harness noise (field report: a fully green --no-shard run
245
+ // exited 1). Real failures always carry a non-zero unexpected count or
246
+ // TEST-UNEXPECTED lines, both of which fail the green-summary check.
247
+ if (hasCompletedGreenSummary(output)) {
248
+ return { kind: 'tests-ran-ok', greenSummaryOverride: true };
249
+ }
250
+ return { kind: 'test-failures' };
141
251
  }
142
252
  /** Builds the operator-facing failure message after retries are exhausted. */
143
- export function buildHarnessCrashMessage(signature, attempts) {
144
- return (`mach test crashed in the harness itself (not in your tests) on all ${attempts} attempt(s).\n\n` +
253
+ export function buildHarnessCrashMessage(signature, attempts, commandLabel = 'mach test') {
254
+ return (`${commandLabel} crashed in the harness itself (not in your tests) on all ${attempts} attempt(s).\n\n` +
145
255
  `Detected shape: ${signature.reason}\n` +
146
256
  `Evidence line: ${signature.line}\n\n` +
147
257
  'This failure mode is environmental (mozlog resource monitor / psutil on macOS, focus-stall ' +
@@ -77,6 +77,13 @@ export declare function parseAppdirFromToml(tomlText: string, key: string): {
77
77
  *
78
78
  * Special-cases `startPath` itself when it already ends with
79
79
  * `xpcshell.toml` — operators sometimes pass a manifest path directly.
80
+ *
81
+ * When `startPath` is a DIRECTORY, the walk starts at the directory itself
82
+ * (checking `<dir>/xpcshell.toml` first) rather than at its parent. Field
83
+ * report (0.34.0 cycle): a directory whose own manifest is an
84
+ * `xpcshell.toml` was classified non-xpcshell — the walk began at the
85
+ * parent, missed the manifest, and `fireforge test <dir>` dispatched to
86
+ * the mochitest runner, which found no mochitests.
80
87
  */
81
88
  export declare function findNearestXpcshellManifest(engineDir: string, startPath: string): Promise<string | null>;
82
89
  /**
@@ -38,7 +38,7 @@
38
38
  * test.ts; we skip injection when `--app-path=` already appears in the
39
39
  * forwarded args).
40
40
  */
41
- import { readdir } from 'node:fs/promises';
41
+ import { readdir, stat } from 'node:fs/promises';
42
42
  import { dirname, join, resolve, sep } from 'node:path';
43
43
  import { pathExists, readJson, readText } from '../utils/fs.js';
44
44
  import { isObject, isString } from '../utils/validation.js';
@@ -110,19 +110,28 @@ function stripQuotes(raw) {
110
110
  *
111
111
  * Special-cases `startPath` itself when it already ends with
112
112
  * `xpcshell.toml` — operators sometimes pass a manifest path directly.
113
+ *
114
+ * When `startPath` is a DIRECTORY, the walk starts at the directory itself
115
+ * (checking `<dir>/xpcshell.toml` first) rather than at its parent. Field
116
+ * report (0.34.0 cycle): a directory whose own manifest is an
117
+ * `xpcshell.toml` was classified non-xpcshell — the walk began at the
118
+ * parent, missed the manifest, and `fireforge test <dir>` dispatched to
119
+ * the mochitest runner, which found no mochitests.
113
120
  */
114
121
  export async function findNearestXpcshellManifest(engineDir, startPath) {
115
122
  const absStart = resolve(engineDir, startPath);
116
123
  if (absStart.toLowerCase().endsWith(`${sep}xpcshell.toml`)) {
117
124
  return (await pathExists(absStart)) ? absStart : null;
118
125
  }
126
+ const startIsDirectory = await stat(absStart).then((stats) => stats.isDirectory(), () => false);
119
127
  const engineAbs = resolve(engineDir);
120
128
  let current = absStart;
121
- // First iteration walks down to a directory; subsequent ones walk up.
129
+ // First iteration resolves the starting directory (the path itself for a
130
+ // directory argument, its parent for a file); subsequent ones walk up.
122
131
  // Cap iterations defensively — a pathological symlink loop would
123
132
  // otherwise spin until the call stack overflows.
124
133
  for (let i = 0; i < 64; i += 1) {
125
- const dir = i === 0 ? dirname(absStart) : dirname(current);
134
+ const dir = i === 0 ? (startIsDirectory ? absStart : dirname(absStart)) : dirname(current);
126
135
  const candidate = join(dir, 'xpcshell.toml');
127
136
  if (await pathExists(candidate))
128
137
  return candidate;
@@ -530,6 +530,16 @@ export interface FurnaceRemoveOptions {
530
530
  export interface FurnaceCreateOptions {
531
531
  /** Component description */
532
532
  description?: string;
533
+ /**
534
+ * Engine-relative directory the test scaffold writes into, instead of
535
+ * the default `browser/base/content/test/<binaryName>/` (browser-chrome)
536
+ * or `browser/base/content/test/<binaryName>-xpcshell/<component>/`
537
+ * (xpcshell). Must stay under `browser/base/content/test/` so the
538
+ * manifest registration keeps working. Not supported for
539
+ * `--test-style=mochikit` (that harness lives in the upstream
540
+ * toolkit/content/tests/widgets tree).
541
+ */
542
+ testDir?: string;
533
543
  /** Include Fluent l10n support */
534
544
  localized?: boolean;
535
545
  /** Register in customElements.js (default: true) */
@@ -614,6 +624,8 @@ export interface WireOptions {
614
624
  export interface RegisterOptions {
615
625
  dryRun?: boolean;
616
626
  after?: string;
627
+ /** Scaffold a missing manifest (moz.build / xpcshell.toml) and wire the parent chain. */
628
+ createManifest?: boolean;
617
629
  }
618
630
  /**
619
631
  * Options for the patch delete command.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -72,9 +72,9 @@
72
72
  "eslint-plugin-simple-import-sort": "^13.0.0",
73
73
  "fast-check": "^4.6.0",
74
74
  "husky": "^9.1.7",
75
- "knip": "6.17.1",
75
+ "knip": "6.23.0",
76
76
  "lint-staged": "^17.0.4",
77
- "prettier": "^3.7.4",
77
+ "prettier": "3.9.4",
78
78
  "tsx": "^4.7.0",
79
79
  "typescript": "~6.0.0",
80
80
  "typescript-eslint": "^8.0.0",