@hominis/fireforge 0.30.0 → 0.31.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 (141) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +22 -5
  3. package/dist/src/commands/export-all.js +5 -15
  4. package/dist/src/commands/export-flow.d.ts +6 -0
  5. package/dist/src/commands/export-flow.js +6 -1
  6. package/dist/src/commands/export-placement-gate.d.ts +38 -0
  7. package/dist/src/commands/export-placement-gate.js +105 -0
  8. package/dist/src/commands/export-shared.d.ts +28 -0
  9. package/dist/src/commands/export-shared.js +36 -0
  10. package/dist/src/commands/export.js +47 -112
  11. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
  12. package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
  13. package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
  14. package/dist/src/commands/furnace/create.d.ts +1 -2
  15. package/dist/src/commands/furnace/deploy.js +36 -114
  16. package/dist/src/commands/furnace/refresh.js +52 -32
  17. package/dist/src/commands/furnace/sync.js +2 -0
  18. package/dist/src/commands/import.js +108 -73
  19. package/dist/src/commands/lint-per-patch.d.ts +1 -1
  20. package/dist/src/commands/lint-per-patch.js +119 -78
  21. package/dist/src/commands/lint.d.ts +1 -58
  22. package/dist/src/commands/lint.js +96 -84
  23. package/dist/src/commands/patch/compact.d.ts +5 -2
  24. package/dist/src/commands/patch/compact.js +85 -25
  25. package/dist/src/commands/patch/delete.js +17 -17
  26. package/dist/src/commands/patch/index.js +2 -0
  27. package/dist/src/commands/patch/lint-ignore.js +3 -16
  28. package/dist/src/commands/patch/move-files.js +2 -0
  29. package/dist/src/commands/patch/patch-context.d.ts +41 -0
  30. package/dist/src/commands/patch/patch-context.js +53 -0
  31. package/dist/src/commands/patch/rename.js +10 -15
  32. package/dist/src/commands/patch/reorder.d.ts +0 -2
  33. package/dist/src/commands/patch/reorder.js +18 -19
  34. package/dist/src/commands/patch/split-plan.d.ts +66 -0
  35. package/dist/src/commands/patch/split-plan.js +178 -0
  36. package/dist/src/commands/patch/split.d.ts +30 -0
  37. package/dist/src/commands/patch/split.js +283 -0
  38. package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
  39. package/dist/src/commands/patch/staged-dependency.js +4 -17
  40. package/dist/src/commands/patch/tier.js +4 -17
  41. package/dist/src/commands/re-export-scan.js +8 -1
  42. package/dist/src/commands/rebase/summary.d.ts +1 -5
  43. package/dist/src/commands/rebase/summary.js +1 -1
  44. package/dist/src/commands/status-output.js +77 -68
  45. package/dist/src/commands/test-diagnose.d.ts +23 -0
  46. package/dist/src/commands/test-diagnose.js +210 -0
  47. package/dist/src/commands/test-run.d.ts +58 -0
  48. package/dist/src/commands/test-run.js +88 -0
  49. package/dist/src/commands/test.js +169 -257
  50. package/dist/src/commands/token.js +15 -1
  51. package/dist/src/commands/wire.js +109 -78
  52. package/dist/src/core/build-audit.d.ts +1 -1
  53. package/dist/src/core/build-audit.js +2 -46
  54. package/dist/src/core/build-baseline-types.d.ts +38 -0
  55. package/dist/src/core/build-baseline-types.js +10 -0
  56. package/dist/src/core/build-baseline.d.ts +1 -31
  57. package/dist/src/core/build-prepare.d.ts +1 -1
  58. package/dist/src/core/build-prepare.js +2 -45
  59. package/dist/src/core/config-paths.d.ts +0 -8
  60. package/dist/src/core/config-paths.js +4 -4
  61. package/dist/src/core/config-state.d.ts +0 -6
  62. package/dist/src/core/config-state.js +1 -1
  63. package/dist/src/core/config-validate-patch-policy.js +12 -13
  64. package/dist/src/core/config-validate.js +48 -28
  65. package/dist/src/core/engine-changes.d.ts +24 -0
  66. package/dist/src/core/engine-changes.js +64 -0
  67. package/dist/src/core/firefox-cache.d.ts +0 -5
  68. package/dist/src/core/firefox-cache.js +1 -1
  69. package/dist/src/core/firefox-download.d.ts +0 -6
  70. package/dist/src/core/firefox-download.js +1 -1
  71. package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
  72. package/dist/src/core/furnace-apply-helpers.js +11 -20
  73. package/dist/src/core/furnace-apply.d.ts +1 -1
  74. package/dist/src/core/furnace-apply.js +1 -1
  75. package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
  76. package/dist/src/core/furnace-checksum-utils.js +15 -0
  77. package/dist/src/core/furnace-config-validate.d.ts +31 -0
  78. package/dist/src/core/furnace-config-validate.js +133 -0
  79. package/dist/src/core/furnace-config.d.ts +4 -32
  80. package/dist/src/core/furnace-config.js +15 -111
  81. package/dist/src/core/furnace-constants.d.ts +0 -10
  82. package/dist/src/core/furnace-constants.js +2 -2
  83. package/dist/src/core/furnace-css-fragments.d.ts +79 -0
  84. package/dist/src/core/furnace-css-fragments.js +243 -0
  85. package/dist/src/core/furnace-jsconfig.d.ts +63 -0
  86. package/dist/src/core/furnace-jsconfig.js +171 -0
  87. package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
  88. package/dist/src/core/furnace-validate-helpers.js +40 -1
  89. package/dist/src/core/furnace-validate-registration.js +16 -1
  90. package/dist/src/core/furnace-validate.js +54 -2
  91. package/dist/src/core/git-file-ops.d.ts +0 -12
  92. package/dist/src/core/git-file-ops.js +2 -2
  93. package/dist/src/core/lint-cache.d.ts +3 -13
  94. package/dist/src/core/lint-cache.js +11 -5
  95. package/dist/src/core/mach.d.ts +5 -1
  96. package/dist/src/core/mach.js +6 -2
  97. package/dist/src/core/manifest-register.d.ts +5 -16
  98. package/dist/src/core/manifest-register.js +3 -1
  99. package/dist/src/core/patch-lint-checkjs.js +53 -7
  100. package/dist/src/core/patch-lint-jsdoc.js +63 -4
  101. package/dist/src/core/patch-lint-observer.d.ts +37 -0
  102. package/dist/src/core/patch-lint-observer.js +168 -0
  103. package/dist/src/core/patch-lint.js +132 -125
  104. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  105. package/dist/src/core/patch-manifest-io.js +44 -2
  106. package/dist/src/core/patch-manifest-validate.d.ts +1 -8
  107. package/dist/src/core/patch-manifest-validate.js +1 -1
  108. package/dist/src/core/patch-manifest.d.ts +1 -1
  109. package/dist/src/core/patch-manifest.js +1 -1
  110. package/dist/src/core/patch-policy.d.ts +0 -4
  111. package/dist/src/core/patch-policy.js +10 -4
  112. package/dist/src/core/register-browser-content.d.ts +1 -1
  113. package/dist/src/core/register-module.d.ts +1 -1
  114. package/dist/src/core/register-result.d.ts +21 -0
  115. package/dist/src/core/register-result.js +9 -0
  116. package/dist/src/core/register-shared-css.d.ts +1 -1
  117. package/dist/src/core/register-test-manifest.d.ts +1 -1
  118. package/dist/src/core/test-harness-crash.d.ts +61 -0
  119. package/dist/src/core/test-harness-crash.js +140 -0
  120. package/dist/src/core/test-stale-check.d.ts +1 -1
  121. package/dist/src/core/test-stale-check.js +2 -46
  122. package/dist/src/core/test-xpcshell-retry.d.ts +1 -1
  123. package/dist/src/core/test-xpcshell-retry.js +4 -2
  124. package/dist/src/core/token-dark-mode.js +14 -26
  125. package/dist/src/core/token-manager.d.ts +4 -0
  126. package/dist/src/core/token-manager.js +70 -16
  127. package/dist/src/core/typecheck-shim.d.ts +0 -21
  128. package/dist/src/core/typecheck-shim.js +26 -4
  129. package/dist/src/core/wire-utils.js +37 -44
  130. package/dist/src/types/commands/index.d.ts +1 -1
  131. package/dist/src/types/commands/options.d.ts +105 -0
  132. package/dist/src/types/furnace.d.ts +12 -1
  133. package/dist/src/utils/elapsed.d.ts +0 -2
  134. package/dist/src/utils/elapsed.js +1 -1
  135. package/dist/src/utils/fs.d.ts +0 -5
  136. package/dist/src/utils/fs.js +1 -1
  137. package/dist/src/utils/regex.d.ts +0 -6
  138. package/dist/src/utils/regex.js +3 -3
  139. package/dist/src/utils/validation.d.ts +0 -8
  140. package/dist/src/utils/validation.js +2 -2
  141. package/package.json +6 -4
@@ -85,6 +85,22 @@ export interface PatchRenameEntry {
85
85
  /** New numeric order — must match the prefix of `newFilename`. */
86
86
  newOrder: number;
87
87
  }
88
+ /**
89
+ * Rewrites `stagedDependencies.forwardImports[].owner` references on one
90
+ * patch through a rename lookup. Owners embed exact patch filenames, so any
91
+ * renumber (compact, reorder, placement export, rename) that does not remap
92
+ * them leaves dangling references that surface as false forward-import
93
+ * errors on the next lint.
94
+ *
95
+ * Pure and allocation-conservative: returns the input object unchanged when
96
+ * no owner matches the lookup, so callers can map whole manifests cheaply.
97
+ *
98
+ * @param patch - Manifest row to rewrite
99
+ * @param renameLookup - Maps an old patch filename to its new filename, or
100
+ * undefined when the filename is not being renamed
101
+ * @returns The same row, or a copy with remapped owners
102
+ */
103
+ export declare function rewriteStagedDependencyOwners(patch: PatchMetadata, renameLookup: (oldFilename: string) => string | undefined): PatchMetadata;
88
104
  /**
89
105
  * Renames patch files on disk and rewrites the corresponding manifest rows
90
106
  * atomically-ish: file renames use a two-phase staging strategy (rename each
@@ -167,6 +167,44 @@ export async function removePatchFromManifest(patchesDir, filename) {
167
167
  await savePatchesManifest(patchesDir, manifest);
168
168
  return true;
169
169
  }
170
+ /**
171
+ * Rewrites `stagedDependencies.forwardImports[].owner` references on one
172
+ * patch through a rename lookup. Owners embed exact patch filenames, so any
173
+ * renumber (compact, reorder, placement export, rename) that does not remap
174
+ * them leaves dangling references that surface as false forward-import
175
+ * errors on the next lint.
176
+ *
177
+ * Pure and allocation-conservative: returns the input object unchanged when
178
+ * no owner matches the lookup, so callers can map whole manifests cheaply.
179
+ *
180
+ * @param patch - Manifest row to rewrite
181
+ * @param renameLookup - Maps an old patch filename to its new filename, or
182
+ * undefined when the filename is not being renamed
183
+ * @returns The same row, or a copy with remapped owners
184
+ */
185
+ export function rewriteStagedDependencyOwners(patch, renameLookup) {
186
+ const forwardImports = patch.stagedDependencies?.forwardImports;
187
+ if (!forwardImports || forwardImports.length === 0)
188
+ return patch;
189
+ const rewritten = forwardImports.map((fi) => {
190
+ if (!fi.owner)
191
+ return fi;
192
+ const newOwner = renameLookup(fi.owner);
193
+ if (newOwner === undefined || newOwner === fi.owner)
194
+ return fi;
195
+ return { ...fi, owner: newOwner };
196
+ });
197
+ const changed = rewritten.some((fi, index) => fi !== forwardImports[index]);
198
+ if (!changed)
199
+ return patch;
200
+ return {
201
+ ...patch,
202
+ stagedDependencies: {
203
+ ...patch.stagedDependencies,
204
+ forwardImports: rewritten,
205
+ },
206
+ };
207
+ }
170
208
  /**
171
209
  * Renames patch files on disk and rewrites the corresponding manifest rows
172
210
  * atomically-ish: file renames use a two-phase staging strategy (rename each
@@ -300,12 +338,16 @@ export async function renumberPatchesInManifest(patchesDir, renameMap) {
300
338
  for (const [oldFilename, entry] of renameMap) {
301
339
  filenameUpdates.set(oldFilename, entry);
302
340
  }
341
+ // Owner references live on *other* patches than the renamed ones, so every
342
+ // row is passed through the staged-dependency rewrite, not just renamed rows.
343
+ const ownerLookup = (oldFilename) => filenameUpdates.get(oldFilename)?.newFilename;
303
344
  const updatedPatches = manifest.patches.map((p) => {
304
345
  const update = filenameUpdates.get(p.filename);
346
+ const withOwners = rewriteStagedDependencyOwners(p, ownerLookup);
305
347
  if (!update)
306
- return p;
348
+ return withOwners;
307
349
  return {
308
- ...p,
350
+ ...withOwners,
309
351
  filename: update.newFilename,
310
352
  order: update.newOrder,
311
353
  };
@@ -1,14 +1,7 @@
1
1
  /**
2
2
  * Schema validation for patches.json manifest data.
3
3
  */
4
- import type { PatchCategory, PatchesManifest, PatchMetadata } from '../types/commands/index.js';
5
- /**
6
- * Validates a single patch metadata entry from raw data.
7
- * @param data - Raw data to validate
8
- * @param index - Array index for error messages
9
- * @returns Validated PatchMetadata
10
- */
11
- export declare function validatePatchMetadata(data: unknown, index: number): PatchMetadata;
4
+ import type { PatchCategory, PatchesManifest } from '../types/commands/index.js';
12
5
  /** Validates raw patches.json data and returns the typed manifest shape. */
13
6
  export declare function validatePatchesManifest(data: unknown): PatchesManifest;
14
7
  /**
@@ -39,7 +39,7 @@ function parseStagedDependencies(data, label) {
39
39
  * @param index - Array index for error messages
40
40
  * @returns Validated PatchMetadata
41
41
  */
42
- export function validatePatchMetadata(data, index) {
42
+ function validatePatchMetadata(data, index) {
43
43
  const rec = parseObject(data, `patches[${index}]`);
44
44
  const filename = rec.string('filename');
45
45
  const name = rec.string('name');
@@ -5,7 +5,7 @@
5
5
  * is an implementation detail.
6
6
  */
7
7
  export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
8
- export { addPatchToManifest, loadPatchesManifest, mutatePatchRowsInManifest, PATCHES_MANIFEST, type PatchManifestRowMutation, type PatchManifestRowMutationResult, type PatchRenameEntry, removePatchFileAndManifest, renumberPatchesInManifest, savePatchesManifest, } from './patch-manifest-io.js';
8
+ export { addPatchToManifest, loadPatchesManifest, mutatePatchRowsInManifest, PATCHES_MANIFEST, type PatchManifestRowMutation, type PatchManifestRowMutationResult, type PatchRenameEntry, removePatchFileAndManifest, renumberPatchesInManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from './patch-manifest-io.js';
9
9
  export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
10
10
  export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
11
11
  export { validatePatchesManifest } from './patch-manifest-validate.js';
@@ -6,7 +6,7 @@
6
6
  * is an implementation detail.
7
7
  */
8
8
  export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
9
- export { addPatchToManifest, loadPatchesManifest, mutatePatchRowsInManifest, PATCHES_MANIFEST, removePatchFileAndManifest, renumberPatchesInManifest, savePatchesManifest, } from './patch-manifest-io.js';
9
+ export { addPatchToManifest, loadPatchesManifest, mutatePatchRowsInManifest, PATCHES_MANIFEST, removePatchFileAndManifest, renumberPatchesInManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from './patch-manifest-io.js';
10
10
  export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
11
11
  export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
12
12
  export { validatePatchesManifest } from './patch-manifest-validate.js';
@@ -8,8 +8,6 @@
8
8
  */
9
9
  import type { PatchesManifest, PatchMetadata } from '../types/commands/index.js';
10
10
  import type { FireForgeConfig } from '../types/config.js';
11
- /** Default patch filename contract used when a policy omits `filenamePattern`. */
12
- export declare const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = "^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$";
13
11
  /** Stable issue codes returned by patch policy evaluation. */
14
12
  export type PatchPolicyIssueCode = 'filename-pattern' | 'filename-captures' | 'filename-metadata-mismatch' | 'order-collision' | 'category-range' | 'reserved-range' | 'reserved-documentation' | 'reserved-files' | 'description-required' | 'numeric-gap';
15
13
  /** A single patch policy validation finding. */
@@ -26,8 +24,6 @@ export interface PatchPolicyEnforcementInput {
26
24
  command: string;
27
25
  forceUnsafe?: boolean;
28
26
  }
29
- /** Returns true when the loaded config includes an opt-in patch policy. */
30
- export declare function hasPatchPolicy(config: FireForgeConfig): boolean;
31
27
  /** Returns valid categories for prompts and CLI validation under the config. */
32
28
  export declare function getPatchPolicyCategories(config: FireForgeConfig): string[];
33
29
  /** Checks whether a category is accepted by legacy defaults or the policy ranges. */
@@ -10,8 +10,9 @@
10
10
  import { InvalidArgumentError } from '../errors/base.js';
11
11
  import { warn } from '../utils/logger.js';
12
12
  import { PATCH_CATEGORIES } from '../utils/validation.js';
13
+ import { rewriteStagedDependencyOwners } from './patch-manifest-io.js';
13
14
  /** Default patch filename contract used when a policy omits `filenamePattern`. */
14
- export const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = '^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$';
15
+ const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = '^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$';
15
16
  function policy(config) {
16
17
  return config.patchPolicy;
17
18
  }
@@ -22,7 +23,7 @@ function issueSeverity(config) {
22
23
  return mutationMode(config) === 'warn' ? 'warning' : 'error';
23
24
  }
24
25
  /** Returns true when the loaded config includes an opt-in patch policy. */
25
- export function hasPatchPolicy(config) {
26
+ function hasPatchPolicy(config) {
26
27
  return policy(config) !== undefined;
27
28
  }
28
29
  /** Returns valid categories for prompts and CLI validation under the config. */
@@ -305,12 +306,17 @@ export function buildProjectedManifest(current, patches) {
305
306
  }
306
307
  /** Applies a filename/order rename projection to a manifest without mutating it. */
307
308
  export function applyRenameMapToManifest(manifest, renameMap) {
309
+ const ownerLookup = (oldFilename) => renameMap.get(oldFilename)?.newFilename;
308
310
  return buildProjectedManifest(manifest, manifest.patches.map((patch) => {
311
+ // Staged-dependency owners reference other patches' filenames, so the
312
+ // projection rewrites them on every row to mirror what
313
+ // renumberPatchesInManifest persists.
314
+ const withOwners = rewriteStagedDependencyOwners(patch, ownerLookup);
309
315
  const rename = renameMap.get(patch.filename);
310
316
  if (!rename)
311
- return patch;
317
+ return withOwners;
312
318
  return {
313
- ...patch,
319
+ ...withOwners,
314
320
  filename: rename.newFilename,
315
321
  order: rename.newOrder,
316
322
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * JS/content registration in browser/base/jar.mn.
3
3
  */
4
- import type { RegisterResult } from './manifest-register.js';
4
+ import type { RegisterResult } from './register-result.js';
5
5
  /**
6
6
  * Registers a JS/content file in browser/base/jar.mn.
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Module registration in browser/modules/{binaryName}/moz.build.
3
3
  */
4
- import type { RegisterResult } from './manifest-register.js';
4
+ import type { RegisterResult } from './register-result.js';
5
5
  /**
6
6
  * Registers a module in browser/modules/{binaryName}/moz.build.
7
7
  *
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared result shape for manifest registration operations, split out of
3
+ * the `manifest-register.ts` barrel so the `register-*` leaf modules can
4
+ * import it without importing the barrel that re-exports them — that
5
+ * type-only back-edge made the registration dependency graph cyclic.
6
+ */
7
+ /**
8
+ * Result of a manifest registration operation.
9
+ */
10
+ export interface RegisterResult {
11
+ /** The manifest file that was modified */
12
+ manifest: string;
13
+ /** The entry that was inserted */
14
+ entry: string;
15
+ /** The entry after which the new entry was inserted (for user display) */
16
+ previousEntry?: string | undefined;
17
+ /** Whether the entry already existed (skipped) */
18
+ skipped: boolean;
19
+ /** Whether --after target was not found and fell back to alphabetical */
20
+ afterFallback?: boolean | undefined;
21
+ }
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Shared result shape for manifest registration operations, split out of
4
+ * the `manifest-register.ts` barrel so the `register-*` leaf modules can
5
+ * import it without importing the barrel that re-exports them — that
6
+ * type-only back-edge made the registration dependency graph cyclic.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=register-result.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CSS registration in browser/themes/shared/jar.inc.mn.
3
3
  */
4
- import type { RegisterResult } from './manifest-register.js';
4
+ import type { RegisterResult } from './register-result.js';
5
5
  /**
6
6
  * Measures the column at which the `(source)` parenthesis opens in
7
7
  * adjacent `skin/classic/browser/<x>.css (...)` entries inside an
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Test manifest registration in browser/base/moz.build.
3
3
  */
4
- import type { RegisterResult } from './manifest-register.js';
4
+ import type { RegisterResult } from './register-result.js';
5
5
  /**
6
6
  * Registers a test manifest (browser.toml) in browser/base/moz.build.
7
7
  *
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Harness-crash classification for `fireforge test` (field reports C1/C2).
3
+ *
4
+ * The wrapped mach harness exhibits flaky non-test failures that an exit
5
+ * code (or a "did it print a summary" grep) cannot distinguish from real
6
+ * test results:
7
+ *
8
+ * - startup crashes from the mozlog resource monitor on macOS
9
+ * (`AttributeError: 'SystemResourceMonitor' object has no attribute
10
+ * 'poll_interval'`, `host_statistics64(HOST_VM_INFO64) syscall failed`,
11
+ * `(ipc/mig) array not large enough` — a psutil/macOS mismatch) which
12
+ * abort the run before any test executes;
13
+ * - hangs after browser startup that die at the no-output timeout yet
14
+ * still emit a `Passed: 0` summary;
15
+ * - post-green shutdown re-entry, where a fully green run stalls on
16
+ * "must wait for focus" and records "Application shut down (without
17
+ * crashing) in the middle of a test!" as the only unexpected failure.
18
+ *
19
+ * Classification therefore keys on `TEST-START` presence — summary lines
20
+ * never count as proof that tests ran — and recognizes the crash shapes
21
+ * above so the command layer can retry them with a bounded budget instead
22
+ * of reporting phantom test failures (or phantom passes).
23
+ */
24
+ /** How a completed harness run should be interpreted. */
25
+ export type HarnessRunClassification = 'tests-ran-ok' | 'test-failures' | 'harness-crash' | 'no-tests';
26
+ /** A recognized harness-crash shape with its evidence line. */
27
+ export interface HarnessCrashSignature {
28
+ reason: string;
29
+ line: string;
30
+ }
31
+ /** Result of {@link classifyHarnessRun}. */
32
+ export interface HarnessRunVerdict {
33
+ kind: HarnessRunClassification;
34
+ signature?: HarnessCrashSignature;
35
+ }
36
+ /**
37
+ * Detects the known harness-crash shapes in captured mach output.
38
+ * Returns undefined for anything that looks like a genuine test result.
39
+ */
40
+ export declare function detectHarnessCrashSignature(output: string): HarnessCrashSignature | undefined;
41
+ /**
42
+ * Classifies a completed harness run. The decision tree, in order:
43
+ *
44
+ * 1. A recognized crash signature wins regardless of exit code (the
45
+ * shutdown re-entry shape exits non-zero on an otherwise green run;
46
+ * the hang shape can even exit zero with a `Passed: 0` summary).
47
+ * 2. No `TEST-START` with explicit paths requested means no test ran —
48
+ * `no-tests`, even when the exit code is zero. Summary lines are not
49
+ * trusted as evidence of execution.
50
+ * 3. Exit code zero with tests started is a pass; anything else is a
51
+ * test failure for the regular diagnosis chain.
52
+ */
53
+ export declare function classifyHarnessRun(exitCode: number, output: string, requestedPaths: readonly string[]): HarnessRunVerdict;
54
+ /** Builds the operator-facing failure message after retries are exhausted. */
55
+ export declare function buildHarnessCrashMessage(signature: HarnessCrashSignature, attempts: number): string;
56
+ /**
57
+ * Builds the message for a run that produced no `TEST-START` despite
58
+ * requesting paths — including exit-code-zero runs whose `Passed: 0`
59
+ * summary would otherwise read as a silent false green.
60
+ */
61
+ export declare function buildNoTestsRanMessage(exitCode: number, requestedPaths: readonly string[]): string;
@@ -0,0 +1,140 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Harness-crash classification for `fireforge test` (field reports C1/C2).
4
+ *
5
+ * The wrapped mach harness exhibits flaky non-test failures that an exit
6
+ * code (or a "did it print a summary" grep) cannot distinguish from real
7
+ * test results:
8
+ *
9
+ * - startup crashes from the mozlog resource monitor on macOS
10
+ * (`AttributeError: 'SystemResourceMonitor' object has no attribute
11
+ * 'poll_interval'`, `host_statistics64(HOST_VM_INFO64) syscall failed`,
12
+ * `(ipc/mig) array not large enough` — a psutil/macOS mismatch) which
13
+ * abort the run before any test executes;
14
+ * - hangs after browser startup that die at the no-output timeout yet
15
+ * still emit a `Passed: 0` summary;
16
+ * - post-green shutdown re-entry, where a fully green run stalls on
17
+ * "must wait for focus" and records "Application shut down (without
18
+ * crashing) in the middle of a test!" as the only unexpected failure.
19
+ *
20
+ * Classification therefore keys on `TEST-START` presence — summary lines
21
+ * never count as proof that tests ran — and recognizes the crash shapes
22
+ * above so the command layer can retry them with a bounded budget instead
23
+ * of reporting phantom test failures (or phantom passes).
24
+ */
25
+ const TEST_START_PATTERN = /\bTEST-START\b/;
26
+ const UNEXPECTED_LINE_PATTERN = /^.*\bTEST-UNEXPECTED-[A-Z-]+\b.*$/gm;
27
+ const SHUTDOWN_REENTRY_PATTERN = /Application shut down \(without crashing\) in the middle of a test/i;
28
+ const FOCUS_STALL_PATTERN = /must wait for focus/i;
29
+ const TRACEBACK_PATTERN = /Traceback \(most recent call last\)/;
30
+ const NO_OUTPUT_TIMEOUT_PATTERN = /timed out after \d+ seconds with no output/i;
31
+ /**
32
+ * Startup-traceback fingerprints from the mozlog resource monitor / psutil
33
+ * on macOS. Each is matched per-line so the evidence line in the report is
34
+ * the concrete failure, not the whole traceback.
35
+ */
36
+ const STARTUP_TRACEBACK_SIGNALS = [
37
+ /AttributeError:.*SystemResourceMonitor/,
38
+ /'SystemResourceMonitor' object has no attribute/,
39
+ /poll_interval/,
40
+ /host_statistics64/,
41
+ /HOST_VM_INFO64/,
42
+ /\(ipc\/mig\) array not large enough/,
43
+ /psutil\.[A-Za-z]*Error/,
44
+ ];
45
+ function findLine(output, patterns) {
46
+ for (const line of output.split(/\r?\n/)) {
47
+ if (patterns.some((p) => p.test(line)))
48
+ return line.trim();
49
+ }
50
+ return undefined;
51
+ }
52
+ /** Unexpected-failure lines that are NOT the shutdown re-entry artifact. */
53
+ function realUnexpectedFailureLines(output) {
54
+ const matches = output.match(UNEXPECTED_LINE_PATTERN) ?? [];
55
+ return matches.filter((line) => !SHUTDOWN_REENTRY_PATTERN.test(line));
56
+ }
57
+ /**
58
+ * Detects the known harness-crash shapes in captured mach output.
59
+ * Returns undefined for anything that looks like a genuine test result.
60
+ */
61
+ export function detectHarnessCrashSignature(output) {
62
+ const hasTestStart = TEST_START_PATTERN.test(output);
63
+ const realFailures = realUnexpectedFailureLines(output);
64
+ // Startup traceback cluster (resource monitor / psutil). Real test
65
+ // failures take precedence: a traceback printed during teardown of a
66
+ // genuinely failing run must not get the whole run retried.
67
+ if (TRACEBACK_PATTERN.test(output) && realFailures.length === 0) {
68
+ const signalLine = findLine(output, STARTUP_TRACEBACK_SIGNALS);
69
+ if (signalLine) {
70
+ return { reason: 'harness startup traceback (resource monitor/psutil)', line: signalLine };
71
+ }
72
+ }
73
+ // Post-browser-startup hang: no test ever started, the harness died at
74
+ // the no-output timeout. A trailing "Passed: 0" summary is part of this
75
+ // shape and must not be read as a result.
76
+ if (!hasTestStart) {
77
+ const timeoutLine = findLine(output, [NO_OUTPUT_TIMEOUT_PATTERN]);
78
+ if (timeoutLine) {
79
+ return { reason: 'no-output timeout before any test started', line: timeoutLine };
80
+ }
81
+ return undefined;
82
+ }
83
+ // Post-green shutdown re-entry: every unexpected line is the
84
+ // shutdown-mid-test artifact, the run stalled on focus, and at least one
85
+ // such artifact exists — an otherwise green log.
86
+ const shutdownLine = findLine(output, [SHUTDOWN_REENTRY_PATTERN]);
87
+ if (shutdownLine && realFailures.length === 0 && FOCUS_STALL_PATTERN.test(output)) {
88
+ return { reason: 'post-green shutdown re-entry during harness teardown', line: shutdownLine };
89
+ }
90
+ return undefined;
91
+ }
92
+ /**
93
+ * Classifies a completed harness run. The decision tree, in order:
94
+ *
95
+ * 1. A recognized crash signature wins regardless of exit code (the
96
+ * shutdown re-entry shape exits non-zero on an otherwise green run;
97
+ * the hang shape can even exit zero with a `Passed: 0` summary).
98
+ * 2. No `TEST-START` with explicit paths requested means no test ran —
99
+ * `no-tests`, even when the exit code is zero. Summary lines are not
100
+ * trusted as evidence of execution.
101
+ * 3. Exit code zero with tests started is a pass; anything else is a
102
+ * test failure for the regular diagnosis chain.
103
+ */
104
+ export function classifyHarnessRun(exitCode, output, requestedPaths) {
105
+ const signature = detectHarnessCrashSignature(output);
106
+ if (signature) {
107
+ return { kind: 'harness-crash', signature };
108
+ }
109
+ if (!TEST_START_PATTERN.test(output) && requestedPaths.length > 0) {
110
+ return { kind: 'no-tests' };
111
+ }
112
+ return exitCode === 0 ? { kind: 'tests-ran-ok' } : { kind: 'test-failures' };
113
+ }
114
+ /** Builds the operator-facing failure message after retries are exhausted. */
115
+ export function buildHarnessCrashMessage(signature, attempts) {
116
+ return (`mach test crashed in the harness itself (not in your tests) on all ${attempts} attempt(s).\n\n` +
117
+ `Detected shape: ${signature.reason}\n` +
118
+ `Evidence line: ${signature.line}\n\n` +
119
+ 'This failure mode is environmental (mozlog resource monitor / psutil on macOS, focus-stall ' +
120
+ 'shutdown re-entry, or a pre-test hang) rather than a test regression. Re-run the command, ' +
121
+ 'raise the retry budget with --harness-retries <n>, or run the file in isolation. ' +
122
+ 'If it persists across many runs, inspect the mach virtualenv (mach resyncs psutil on its own; ' +
123
+ 'patching it manually does not stick).');
124
+ }
125
+ /**
126
+ * Builds the message for a run that produced no `TEST-START` despite
127
+ * requesting paths — including exit-code-zero runs whose `Passed: 0`
128
+ * summary would otherwise read as a silent false green.
129
+ */
130
+ export function buildNoTestsRanMessage(exitCode, requestedPaths) {
131
+ const exitNote = exitCode === 0
132
+ ? 'The harness exited 0 and may have printed a summary line, but a summary without a single TEST-START is not a test result.'
133
+ : `The harness exited ${exitCode} before any TEST-START line.`;
134
+ return ('mach test finished without starting any of the requested tests.\n\n' +
135
+ `${exitNote}\n\n` +
136
+ `Requested paths: ${requestedPaths.join(', ')}\n\n` +
137
+ 'Check that the paths are registered in their test manifest (browser.toml / xpcshell.toml) ' +
138
+ 'and that the manifest is reachable from moz.build, then retry.');
139
+ }
140
+ //# sourceMappingURL=test-harness-crash.js.map
@@ -1,4 +1,4 @@
1
- import type { BuildBaseline } from './build-baseline.js';
1
+ import type { BuildBaseline } from './build-baseline-types.js';
2
2
  /** Result of the stale-build preflight probe. */
3
3
  export interface StaleBuildResult {
4
4
  /** True when at least one packageable engine file changed since the baseline. */
@@ -30,53 +30,9 @@ import { toError } from '../utils/errors.js';
30
30
  import { verbose } from '../utils/logger.js';
31
31
  import { isPackageablePath } from './build-audit.js';
32
32
  import { readBuildBaseline } from './build-baseline.js';
33
- import { hasChanges, isMissingHeadError } from './git.js';
34
- import { git } from './git-base.js';
35
- import { getUntrackedFiles } from './git-status.js';
33
+ import { collectChangedEnginePaths } from './engine-changes.js';
36
34
  /** Cap on the number of changed paths rendered inline. */
37
35
  const STALE_PATHS_LIMIT = 10;
38
- /**
39
- * Collects engine paths that changed since the baseline SHA plus any
40
- * workdir modifications. Mirrors the helper inside `build-prepare.ts` but
41
- * is kept separate so the test-side preflight does not need to pull in
42
- * the full build-prepare dependency graph (mozconfig generation, furnace
43
- * apply hooks, …).
44
- */
45
- async function collectChangedEnginePaths(engineDir, baseline) {
46
- const collected = new Set();
47
- if (baseline.engineHeadSha) {
48
- try {
49
- const diff = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
50
- for (const line of diff.split('\n')) {
51
- const trimmed = line.trim();
52
- if (trimmed)
53
- collected.add(trimmed);
54
- }
55
- }
56
- catch (error) {
57
- if (!isMissingHeadError(error)) {
58
- verbose(`Stale-build preflight: could not diff engine against baseline — ${toError(error).message}`);
59
- }
60
- }
61
- }
62
- try {
63
- if (await hasChanges(engineDir)) {
64
- const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
65
- for (const line of worktreeDiff.split('\n')) {
66
- const trimmed = line.trim();
67
- if (trimmed)
68
- collected.add(trimmed);
69
- }
70
- for (const untracked of await getUntrackedFiles(engineDir)) {
71
- collected.add(untracked);
72
- }
73
- }
74
- }
75
- catch (error) {
76
- verbose(`Stale-build preflight: could not enumerate workdir changes — ${toError(error).message}`);
77
- }
78
- return [...collected];
79
- }
80
36
  /**
81
37
  * Probes the engine tree for packageable changes since the last successful
82
38
  * `fireforge build`. Returns a summary the `fireforge test` handler renders
@@ -92,7 +48,7 @@ export async function checkStaleBuildForTest(projectRoot, engineDir) {
92
48
  if (!baseline) {
93
49
  return { stale: false, changedPaths: [], truncated: 0, baseline: undefined };
94
50
  }
95
- const changed = await collectChangedEnginePaths(engineDir, baseline);
51
+ const changed = await collectChangedEnginePaths(engineDir, baseline, 'Stale-build preflight');
96
52
  let packageable = changed.filter((path) => isPackageablePath(path)).sort();
97
53
  // Content-hash comparison: when the baseline carries a fingerprint set,
98
54
  // fold each candidate path through a live re-hash and drop paths whose
@@ -4,4 +4,4 @@ export interface XpcshellRetryClassification {
4
4
  nonXpcshell: readonly string[];
5
5
  }
6
6
  /** Removes a stale xpcshell install symlink and retries the focused mach test once. */
7
- export declare function retryAfterXpcshellSymlinkRepair(engineDir: string, objDir: string | undefined, result: MachCommandResult, classification: XpcshellRetryClassification, normalizedPaths: string[], extraArgs: string[]): Promise<MachCommandResult>;
7
+ export declare function retryAfterXpcshellSymlinkRepair(engineDir: string, objDir: string | undefined, result: MachCommandResult, classification: XpcshellRetryClassification, normalizedPaths: string[], extraArgs: string[], env?: Record<string, string>): Promise<MachCommandResult>;
@@ -2,13 +2,15 @@
2
2
  import { testWithOutput } from './mach.js';
3
3
  import { tryRepairStaleXpcshellTestSymlink } from './test-stale-symlink.js';
4
4
  /** Removes a stale xpcshell install symlink and retries the focused mach test once. */
5
- export async function retryAfterXpcshellSymlinkRepair(engineDir, objDir, result, classification, normalizedPaths, extraArgs) {
5
+ export async function retryAfterXpcshellSymlinkRepair(engineDir, objDir, result, classification, normalizedPaths, extraArgs, env) {
6
6
  if (result.exitCode !== 0 &&
7
7
  classification.xpcshell.length > 0 &&
8
8
  classification.nonXpcshell.length === 0) {
9
9
  const repaired = await tryRepairStaleXpcshellTestSymlink(engineDir, objDir, `${result.stdout}\n${result.stderr}`);
10
10
  if (repaired) {
11
- return testWithOutput(engineDir, normalizedPaths, extraArgs);
11
+ return env
12
+ ? testWithOutput(engineDir, normalizedPaths, extraArgs, env)
13
+ : testWithOutput(engineDir, normalizedPaths, extraArgs);
12
14
  }
13
15
  }
14
16
  return result;
@@ -111,13 +111,22 @@ export function findDarkRootInsertionIndex(lines) {
111
111
  }
112
112
  if (rootOpenLine === -1)
113
113
  return -1;
114
- // Depth-count starting from the `:root` opener. The first `{`
115
- // encountered sets the entry depth to the initial counter value; the
116
- // closing brace that returns to that depth terminates the block.
114
+ // Depth-count starting from the `:root` opener; see findBlockCloseIndex.
115
+ return findBlockCloseIndex(stripped, rootOpenLine);
116
+ }
117
+ /**
118
+ * Depth-counts braces from `startLine` (whose lines must already have
119
+ * block comments stripped), returning the index of the line on which the
120
+ * block opened there returns to its entry depth — i.e. the line carrying
121
+ * the block's closing `}` — or -1 when the block never closes. The first
122
+ * `{` encountered sets the entry depth, so the scan may start on the
123
+ * selector/at-rule line itself rather than on the opener.
124
+ */
125
+ function findBlockCloseIndex(stripped, startLine) {
117
126
  let depth = 0;
118
127
  let entryDepth = 0;
119
128
  let enteredBlock = false;
120
- for (let i = rootOpenLine; i < stripped.length; i++) {
129
+ for (let i = startLine; i < stripped.length; i++) {
121
130
  const line = stripped[i] ?? '';
122
131
  for (const ch of line) {
123
132
  if (ch === '{') {
@@ -156,27 +165,6 @@ export function findDarkMediaCloseIndex(lines) {
156
165
  }
157
166
  if (darkMediaLine === -1)
158
167
  return -1;
159
- let depth = 0;
160
- let entryDepth = 0;
161
- let enteredBlock = false;
162
- for (let i = darkMediaLine; i < stripped.length; i++) {
163
- const line = stripped[i] ?? '';
164
- for (const ch of line) {
165
- if (ch === '{') {
166
- depth++;
167
- if (!enteredBlock) {
168
- entryDepth = depth - 1;
169
- enteredBlock = true;
170
- }
171
- }
172
- else if (ch === '}') {
173
- depth--;
174
- }
175
- }
176
- if (enteredBlock && depth === entryDepth) {
177
- return i;
178
- }
179
- }
180
- return -1;
168
+ return findBlockCloseIndex(stripped, darkMediaLine);
181
169
  }
182
170
  //# sourceMappingURL=token-dark-mode.js.map
@@ -20,6 +20,8 @@ export interface AddTokenOptions {
20
20
  darkValue?: string | undefined;
21
21
  /** Dry run mode */
22
22
  dryRun?: boolean | undefined;
23
+ /** Declare the category banner in the tokens CSS when it does not exist yet. */
24
+ createCategory?: boolean | undefined;
23
25
  }
24
26
  /**
25
27
  * Result of adding a token.
@@ -35,6 +37,8 @@ export interface AddTokenResult {
35
37
  countUpdated: boolean;
36
38
  /** Whether the operation was skipped (already exists) */
37
39
  skipped: boolean;
40
+ /** Whether a new category banner was declared by this add. */
41
+ categoryCreated?: boolean;
38
42
  }
39
43
  /** Returns the token CSS path relative to engine root for a given binary name. */
40
44
  export declare function getTokensCssPath(binaryName: string): string;