@hominis/fireforge 0.32.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 (81) hide show
  1. package/CHANGELOG.md +25 -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/patch/split-plan.d.ts +18 -2
  24. package/dist/src/commands/patch/split-plan.js +90 -16
  25. package/dist/src/commands/patch/split.js +12 -3
  26. package/dist/src/commands/re-export-options.d.ts +26 -0
  27. package/dist/src/commands/re-export-options.js +48 -1
  28. package/dist/src/commands/re-export.js +4 -1
  29. package/dist/src/commands/register.js +10 -1
  30. package/dist/src/commands/test-diagnose.js +12 -0
  31. package/dist/src/commands/test.js +29 -22
  32. package/dist/src/commands/token.js +12 -1
  33. package/dist/src/commands/typecheck.js +35 -0
  34. package/dist/src/core/build-prepare.js +23 -3
  35. package/dist/src/core/config-validate.js +26 -0
  36. package/dist/src/core/furnace-apply-dry-run.d.ts +17 -0
  37. package/dist/src/core/furnace-apply-dry-run.js +105 -0
  38. package/dist/src/core/furnace-apply-ftl.d.ts +12 -0
  39. package/dist/src/core/furnace-apply-ftl.js +97 -1
  40. package/dist/src/core/furnace-apply-helpers.js +10 -80
  41. package/dist/src/core/furnace-registration.d.ts +24 -0
  42. package/dist/src/core/furnace-registration.js +56 -3
  43. package/dist/src/core/furnace-validate-registration.js +18 -0
  44. package/dist/src/core/mach-resource-shim.d.ts +58 -0
  45. package/dist/src/core/mach-resource-shim.js +290 -0
  46. package/dist/src/core/mach.d.ts +51 -10
  47. package/dist/src/core/mach.js +76 -25
  48. package/dist/src/core/manifest-helpers.d.ts +13 -0
  49. package/dist/src/core/manifest-helpers.js +29 -4
  50. package/dist/src/core/manifest-rules.d.ts +13 -3
  51. package/dist/src/core/manifest-rules.js +44 -17
  52. package/dist/src/core/patch-export.d.ts +10 -0
  53. package/dist/src/core/patch-export.js +23 -2
  54. package/dist/src/core/patch-lint-cross.d.ts +31 -0
  55. package/dist/src/core/patch-lint-cross.js +83 -63
  56. package/dist/src/core/patch-lint-reexports.d.ts +1 -1
  57. package/dist/src/core/patch-lint-reexports.js +1 -1
  58. package/dist/src/core/register-module.d.ts +1 -1
  59. package/dist/src/core/register-module.js +15 -2
  60. package/dist/src/core/register-result.d.ts +9 -0
  61. package/dist/src/core/register-scaffold.d.ts +55 -0
  62. package/dist/src/core/register-scaffold.js +146 -0
  63. package/dist/src/core/register-test-manifest.js +3 -1
  64. package/dist/src/core/register-xpcshell-test.d.ts +30 -0
  65. package/dist/src/core/register-xpcshell-test.js +96 -0
  66. package/dist/src/core/test-harness-crash.d.ts +32 -4
  67. package/dist/src/core/test-harness-crash.js +152 -14
  68. package/dist/src/core/token-dark-mode.d.ts +9 -0
  69. package/dist/src/core/token-dark-mode.js +1 -1
  70. package/dist/src/core/token-docs.d.ts +32 -0
  71. package/dist/src/core/token-docs.js +101 -0
  72. package/dist/src/core/token-manager.d.ts +8 -0
  73. package/dist/src/core/token-manager.js +77 -95
  74. package/dist/src/core/token-variant.d.ts +39 -0
  75. package/dist/src/core/token-variant.js +141 -0
  76. package/dist/src/core/typecheck.js +56 -28
  77. package/dist/src/core/xpcshell-appdir.d.ts +7 -0
  78. package/dist/src/core/xpcshell-appdir.js +12 -3
  79. package/dist/src/types/commands/options.d.ts +17 -0
  80. package/dist/src/types/config.d.ts +13 -0
  81. package/package.json +4 -4
@@ -0,0 +1,146 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Manifest scaffolding for `fireforge register --create-manifest`
4
+ * (0.34.0 field report): registering a module under a directory with no
5
+ * moz.build used to fail with "Manifest not found" because `register`
6
+ * only inserts into EXISTING manifests. This module owns the two
7
+ * scaffolding primitives:
8
+ *
9
+ * - creating a directory `moz.build` (license header + the requested
10
+ * list directive) and wiring the parent chain's `DIRS` entries up to
11
+ * the nearest existing moz.build;
12
+ * - wiring an `XPCSHELL_TESTS_MANIFESTS` entry for a (possibly freshly
13
+ * created) `xpcshell.toml` into the nearest existing moz.build.
14
+ *
15
+ * All writers reuse the tokenizer + alphabetical-position helpers the
16
+ * existing register writers use, so inserted entries respect mozbuild's
17
+ * case-insensitive StrictOrderingOnAppendList rule.
18
+ */
19
+ import { dirname, join } from 'node:path';
20
+ import { GeneralError } from '../errors/base.js';
21
+ import { pathExists, readText, writeText } from '../utils/fs.js';
22
+ import { getLicenseHeader } from './license-headers.js';
23
+ import { findAlphabeticalMozBuildPosition } from './manifest-helpers.js';
24
+ import { tokenizeMozBuildList } from './manifest-tokenizers.js';
25
+ /**
26
+ * moz.build files use Firefox's canonical MPL-2.0 hash-comment header
27
+ * regardless of the fork's own project license — they live in the engine
28
+ * tree among upstream MPL files.
29
+ */
30
+ function mozBuildHeader() {
31
+ return getLicenseHeader('MPL-2.0', 'hash');
32
+ }
33
+ /**
34
+ * Inserts `value` into the named moz.build list directive, creating the
35
+ * `NAME += [ ... ]` block at the end of the file when the directive does
36
+ * not exist yet. Returns the updated content, or null when the value is
37
+ * already present.
38
+ */
39
+ export function upsertMozBuildListEntry(content, directive, value) {
40
+ if (content.includes(`"${value}"`))
41
+ return null;
42
+ const lines = content.split('\n');
43
+ const listResult = tokenizeMozBuildList(lines, new RegExp(`^${directive}\\b`));
44
+ if (listResult) {
45
+ const { insertIndex } = findAlphabeticalMozBuildPosition(listResult.tokens, value);
46
+ lines.splice(insertIndex, 0, ` "${value}",`);
47
+ return lines.join('\n');
48
+ }
49
+ // No existing list: append a fresh block, keeping exactly one blank
50
+ // line between the previous content and the new directive.
51
+ while (lines.length > 0 && (lines.at(-1) ?? '').trim() === '')
52
+ lines.pop();
53
+ const block = [`${directive} += [`, ` "${value}",`, `]`];
54
+ return [...lines, ...(lines.length > 0 ? [''] : []), ...block, ''].join('\n');
55
+ }
56
+ /**
57
+ * Ensures every parent directory between `childRelDir` and the nearest
58
+ * existing moz.build carries a `DIRS` entry pointing at the next segment
59
+ * down, creating intermediate moz.build files as needed. Walks upward
60
+ * from the child's parent; throws when no moz.build exists anywhere up
61
+ * to the engine root (an engine checkout always has one at the root, so
62
+ * this indicates a bogus path).
63
+ */
64
+ export async function ensureParentDirsWiring(engineDir, childRelDir, dryRun) {
65
+ const actions = [];
66
+ let childName = childRelDir.split('/').at(-1) ?? childRelDir;
67
+ let parentRel = dirname(childRelDir).replace(/\\/g, '/');
68
+ for (;;) {
69
+ if (parentRel === '.' || parentRel === '' || parentRel === '/') {
70
+ throw new GeneralError(`No moz.build found in any parent of ${childRelDir} up to the engine root; ` +
71
+ 'cannot wire the DIRS chain. Check the path.');
72
+ }
73
+ const parentManifestRel = `${parentRel}/moz.build`;
74
+ const parentManifestPath = join(engineDir, parentManifestRel);
75
+ if (await pathExists(parentManifestPath)) {
76
+ const content = await readText(parentManifestPath);
77
+ const updated = upsertMozBuildListEntry(content, 'DIRS', childName);
78
+ if (updated !== null) {
79
+ if (!dryRun)
80
+ await writeText(parentManifestPath, updated);
81
+ actions.push({ manifest: parentManifestRel, change: `DIRS += ["${childName}"]` });
82
+ }
83
+ return actions;
84
+ }
85
+ // Parent has no moz.build either: scaffold one carrying the DIRS
86
+ // entry and keep walking up until an existing manifest anchors the
87
+ // chain.
88
+ const scaffold = `${mozBuildHeader()}\n\nDIRS += [\n "${childName}",\n]\n`;
89
+ if (!dryRun)
90
+ await writeText(parentManifestPath, scaffold);
91
+ actions.push({ manifest: parentManifestRel, change: `created with DIRS += ["${childName}"]` });
92
+ childName = parentRel.split('/').at(-1) ?? parentRel;
93
+ parentRel = dirname(parentRel).replace(/\\/g, '/');
94
+ }
95
+ }
96
+ /**
97
+ * Creates `<moduleDir>/moz.build` with an `EXTRA_JS_MODULES.<namespace>`
98
+ * list containing `fileName`, and wires the parent DIRS chain. The
99
+ * namespace is the module directory's basename (the fork's binary name
100
+ * for `browser/modules/<binaryName>/`), matching Firefox's convention of
101
+ * mapping `EXTRA_JS_MODULES.<ns>` into `resource:///modules/<ns>/`.
102
+ */
103
+ export async function scaffoldModuleMozBuild(engineDir, moduleDir, fileName, dryRun) {
104
+ const namespace = moduleDir.split('/').at(-1) ?? moduleDir;
105
+ const manifestRel = `${moduleDir}/moz.build`;
106
+ const content = `${mozBuildHeader()}\n\nEXTRA_JS_MODULES.${namespace} += [\n "${fileName}",\n]\n`;
107
+ if (!dryRun)
108
+ await writeText(join(engineDir, manifestRel), content);
109
+ const actions = [
110
+ {
111
+ manifest: manifestRel,
112
+ change: `created with EXTRA_JS_MODULES.${namespace} += ["${fileName}"]`,
113
+ },
114
+ ];
115
+ actions.push(...(await ensureParentDirsWiring(engineDir, moduleDir, dryRun)));
116
+ return actions;
117
+ }
118
+ /**
119
+ * Wires an `XPCSHELL_TESTS_MANIFESTS` entry for `manifestRelPath` (an
120
+ * engine-relative `.../xpcshell.toml`) into the nearest EXISTING
121
+ * moz.build above it. Returns the performed actions (empty when the
122
+ * entry already exists).
123
+ */
124
+ export async function ensureXpcshellManifestWiring(engineDir, manifestRelPath, dryRun) {
125
+ let dirRel = dirname(manifestRelPath).replace(/\\/g, '/');
126
+ for (;;) {
127
+ if (dirRel === '.' || dirRel === '' || dirRel === '/') {
128
+ throw new GeneralError(`No moz.build found in any parent of ${manifestRelPath} up to the engine root; ` +
129
+ 'cannot wire XPCSHELL_TESTS_MANIFESTS. Check the path.');
130
+ }
131
+ const mozBuildRel = `${dirRel}/moz.build`;
132
+ const mozBuildPath = join(engineDir, mozBuildRel);
133
+ if (await pathExists(mozBuildPath)) {
134
+ const relEntry = manifestRelPath.slice(dirRel.length + 1);
135
+ const content = await readText(mozBuildPath);
136
+ const updated = upsertMozBuildListEntry(content, 'XPCSHELL_TESTS_MANIFESTS', relEntry);
137
+ if (updated === null)
138
+ return [];
139
+ if (!dryRun)
140
+ await writeText(mozBuildPath, updated);
141
+ return [{ manifest: mozBuildRel, change: `XPCSHELL_TESTS_MANIFESTS += ["${relEntry}"]` }];
142
+ }
143
+ dirRel = dirname(dirRel).replace(/\\/g, '/');
144
+ }
145
+ }
146
+ //# sourceMappingURL=register-scaffold.js.map
@@ -27,7 +27,9 @@ function registerTestManifestTokenized(content, testDir, entry) {
27
27
  function legacyRegisterTestManifest(content, testDir, entry) {
28
28
  const lines = content.split('\n');
29
29
  const extractKey = (line) => {
30
- const match = /"content\/test\/([^/]+)\/browser\.toml"/.exec(line);
30
+ // Nested manifests (`content/test/a/b/browser.toml`) are supported —
31
+ // capture the whole directory chain, not just one segment.
32
+ const match = /"content\/test\/(.+)\/browser\.toml"/.exec(line);
31
33
  return match?.[1];
32
34
  };
33
35
  let sectionStart = -1;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * xpcshell test-file registration (0.34.0 field report): xpcshell test
3
+ * files (e.g. `<module-dir>/test/unit/test_*.js`) were rejected by
4
+ * `fireforge register` as "Unknown file pattern". This writer inserts the
5
+ * `["test_*.js"]` section into the directory's `xpcshell.toml`
6
+ * (alphabetically, idempotently) and — with `--create-manifest` — creates
7
+ * the manifest and wires `XPCSHELL_TESTS_MANIFESTS` into the nearest
8
+ * moz.build.
9
+ */
10
+ import type { RegisterResult } from './register-result.js';
11
+ /**
12
+ * Checks whether the test file is listed in its directory's
13
+ * `xpcshell.toml`. Throws a "Manifest not found" error when the manifest
14
+ * does not exist (status catches that prefix and reports the manifest as
15
+ * missing rather than the file as unregistered).
16
+ */
17
+ export declare function isXpcshellTestRegistered(engineDir: string, dirRel: string, fileName: string): Promise<boolean>;
18
+ /**
19
+ * Inserts the `["<fileName>"]` section into the manifest content in
20
+ * mozbuild's case-insensitive sort order relative to the other test
21
+ * sections, keeping one blank line between sections. Exported for direct
22
+ * unit testing.
23
+ */
24
+ export declare function insertXpcshellManifestSection(content: string, fileName: string): string;
25
+ /**
26
+ * Registers an xpcshell test file in its directory's `xpcshell.toml`.
27
+ * With `createManifest`, a missing manifest is scaffolded and wired into
28
+ * the nearest moz.build via `XPCSHELL_TESTS_MANIFESTS`.
29
+ */
30
+ export declare function registerXpcshellTest(engineDir: string, dirRel: string, fileName: string, dryRun?: boolean, createManifest?: boolean): Promise<RegisterResult>;
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * xpcshell test-file registration (0.34.0 field report): xpcshell test
4
+ * files (e.g. `<module-dir>/test/unit/test_*.js`) were rejected by
5
+ * `fireforge register` as "Unknown file pattern". This writer inserts the
6
+ * `["test_*.js"]` section into the directory's `xpcshell.toml`
7
+ * (alphabetically, idempotently) and — with `--create-manifest` — creates
8
+ * the manifest and wires `XPCSHELL_TESTS_MANIFESTS` into the nearest
9
+ * moz.build.
10
+ */
11
+ import { join } from 'node:path';
12
+ import { GeneralError } from '../errors/base.js';
13
+ import { pathExists, readText, writeText } from '../utils/fs.js';
14
+ import { mozbuildSortCompare } from './manifest-helpers.js';
15
+ import { ensureXpcshellManifestWiring } from './register-scaffold.js';
16
+ function manifestMissingError(manifestRel) {
17
+ return new GeneralError(`Manifest not found: ${manifestRel}. ` +
18
+ 'Pass --create-manifest to scaffold the xpcshell.toml and wire XPCSHELL_TESTS_MANIFESTS ' +
19
+ 'into the nearest moz.build.');
20
+ }
21
+ /**
22
+ * Checks whether the test file is listed in its directory's
23
+ * `xpcshell.toml`. Throws a "Manifest not found" error when the manifest
24
+ * does not exist (status catches that prefix and reports the manifest as
25
+ * missing rather than the file as unregistered).
26
+ */
27
+ export async function isXpcshellTestRegistered(engineDir, dirRel, fileName) {
28
+ const manifestRel = `${dirRel}/xpcshell.toml`;
29
+ const manifestPath = join(engineDir, manifestRel);
30
+ if (!(await pathExists(manifestPath))) {
31
+ throw manifestMissingError(manifestRel);
32
+ }
33
+ const content = await readText(manifestPath);
34
+ return content.includes(`["${fileName}"]`);
35
+ }
36
+ /**
37
+ * Inserts the `["<fileName>"]` section into the manifest content in
38
+ * mozbuild's case-insensitive sort order relative to the other test
39
+ * sections, keeping one blank line between sections. Exported for direct
40
+ * unit testing.
41
+ */
42
+ export function insertXpcshellManifestSection(content, fileName) {
43
+ const lines = content.split('\n');
44
+ const sectionHeaderPattern = /^\["([^"]+)"\]\s*$/;
45
+ let insertIndex = -1;
46
+ for (let i = 0; i < lines.length; i += 1) {
47
+ const match = sectionHeaderPattern.exec(lines[i] ?? '');
48
+ if (!match)
49
+ continue;
50
+ if (insertIndex === -1 && mozbuildSortCompare(fileName, match[1] ?? '') < 0) {
51
+ insertIndex = i;
52
+ }
53
+ }
54
+ if (insertIndex !== -1) {
55
+ lines.splice(insertIndex, 0, `["${fileName}"]`, '');
56
+ return lines.join('\n');
57
+ }
58
+ // Append after the last section (or at end of file when there are no
59
+ // sections yet), keeping exactly one trailing newline.
60
+ while (lines.length > 0 && (lines.at(-1) ?? '').trim() === '')
61
+ lines.pop();
62
+ return [...lines, '', `["${fileName}"]`, ''].join('\n');
63
+ }
64
+ /**
65
+ * Registers an xpcshell test file in its directory's `xpcshell.toml`.
66
+ * With `createManifest`, a missing manifest is scaffolded and wired into
67
+ * the nearest moz.build via `XPCSHELL_TESTS_MANIFESTS`.
68
+ */
69
+ export async function registerXpcshellTest(engineDir, dirRel, fileName, dryRun = false, createManifest = false) {
70
+ const manifestRel = `${dirRel}/xpcshell.toml`;
71
+ const manifestPath = join(engineDir, manifestRel);
72
+ const entry = `["${fileName}"]`;
73
+ if (!(await pathExists(manifestPath))) {
74
+ if (!createManifest) {
75
+ throw manifestMissingError(manifestRel);
76
+ }
77
+ const scaffoldActions = [
78
+ { manifest: manifestRel, change: `created with ${entry}` },
79
+ ];
80
+ if (!dryRun) {
81
+ await writeText(manifestPath, `[DEFAULT]\n\n${entry}\n`);
82
+ }
83
+ scaffoldActions.push(...(await ensureXpcshellManifestWiring(engineDir, manifestRel, dryRun)));
84
+ return { manifest: manifestRel, entry, skipped: false, scaffoldActions };
85
+ }
86
+ const content = await readText(manifestPath);
87
+ if (content.includes(entry)) {
88
+ return { manifest: manifestRel, entry, skipped: true };
89
+ }
90
+ const updated = insertXpcshellManifestSection(content, fileName);
91
+ if (!dryRun) {
92
+ await writeText(manifestPath, updated);
93
+ }
94
+ return { manifest: manifestRel, entry, skipped: false };
95
+ }
96
+ //# sourceMappingURL=register-xpcshell-test.js.map
@@ -32,7 +32,32 @@ export interface HarnessCrashSignature {
32
32
  export interface HarnessRunVerdict {
33
33
  kind: HarnessRunClassification;
34
34
  signature?: HarnessCrashSignature;
35
+ /**
36
+ * Set when a non-zero mach exit code was overridden because the output
37
+ * embeds a completed green summary — the exit code followed harness
38
+ * noise, not a test result. Callers should surface a note.
39
+ */
40
+ greenSummaryOverride?: boolean;
35
41
  }
42
+ /**
43
+ * Strips non-signal noise from captured output before crash-signature
44
+ * matching: resource-monitor degradation warnings, and traceback blocks
45
+ * that mach caught itself (telemetry submission tracebacks are printed but
46
+ * never abort the run). The stripped text is used ONLY as crash evidence —
47
+ * classification of test results still reads the full output.
48
+ */
49
+ export declare function stripNonSignalNoise(output: string): string;
50
+ /**
51
+ * True when the output embeds a COMPLETED, GREEN suite summary: an
52
+ * execution signal, `Unexpected results: 0` (and no non-zero unexpected
53
+ * count), a `SUITE_END` marker, and no real `TEST-UNEXPECTED-*` lines.
54
+ * Such a run finished its suite; any startup-traceback-shaped noise in the
55
+ * same output is by definition non-fatal, so this vetoes signature-based
56
+ * crash classification (field report: fully green sharded runs reported
57
+ * `CRASH (N attempts)` because degradation warnings matched the psutil
58
+ * signals). Exported for direct unit testing.
59
+ */
60
+ export declare function hasCompletedGreenSummary(output: string): boolean;
36
61
  /**
37
62
  * Detects the known harness-crash shapes in captured mach output.
38
63
  * Returns undefined for anything that looks like a genuine test result.
@@ -44,15 +69,18 @@ export declare function detectHarnessCrashSignature(output: string): HarnessCras
44
69
  * 1. A recognized crash signature wins regardless of exit code (the
45
70
  * shutdown re-entry shape exits non-zero on an otherwise green run;
46
71
  * 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.
72
+ * 2. No execution signal with explicit paths requested means no test ran —
73
+ * `no-tests`, even when the exit code is zero. The execution signal is
74
+ * a `TEST-START` line (generic `mach test` / browser-chrome dispatch)
75
+ * OR the suite-specific xpcshell result-summary block (the xpcshell
76
+ * dispatch prints no `TEST-START`). Bare `Passed:`/`Failed:` summary
77
+ * lines are still not trusted as evidence of execution.
50
78
  * 3. Exit code zero with tests started is a pass; anything else is a
51
79
  * test failure for the regular diagnosis chain.
52
80
  */
53
81
  export declare function classifyHarnessRun(exitCode: number, output: string, requestedPaths: readonly string[]): HarnessRunVerdict;
54
82
  /** Builds the operator-facing failure message after retries are exhausted. */
55
- export declare function buildHarnessCrashMessage(signature: HarnessCrashSignature, attempts: number): string;
83
+ export declare function buildHarnessCrashMessage(signature: HarnessCrashSignature, attempts: number, commandLabel?: string): string;
56
84
  /**
57
85
  * Builds the message for a run that produced no `TEST-START` despite
58
86
  * requesting paths — including exit-code-zero runs whose `Passed: 0`
@@ -23,6 +23,29 @@
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]/;
35
+ /**
36
+ * Execution signals emitted by the suite-specific xpcshell dispatch
37
+ * (`mach xpcshell-test`), which does NOT print `TEST-START` lines the way
38
+ * the generic `mach test` / browser-chrome dispatch does. A passing
39
+ * single-file xpcshell run prints a result-summary block instead
40
+ * (`TEST_END: Test PASS`, `Ran 16 checks`, `Unexpected results: 0`), so
41
+ * keying execution purely on `TEST-START` mis-reads a green xpcshell run as
42
+ * "no tests started" (field report: a single-file xpcshell pass exited 1).
43
+ *
44
+ * These markers are xpcshell-specific on purpose: the bare
45
+ * `Passed: 0` / `Failed: 0` summary that the no-output hang shape prints is
46
+ * deliberately NOT matched here — that case must still read as `no-tests`.
47
+ */
48
+ const XPCSHELL_RESULT_SUMMARY_PATTERN = /\bTEST_END\b|\bRan \d+ checks?\b|\bResult summary:/i;
26
49
  const UNEXPECTED_LINE_PATTERN = /^.*\bTEST-UNEXPECTED-[A-Z-]+\b.*$/gm;
27
50
  const SHUTDOWN_REENTRY_PATTERN = /Application shut down \(without crashing\) in the middle of a test/i;
28
51
  const FOCUS_STALL_PATTERN = /must wait for focus/i;
@@ -49,6 +72,97 @@ function findLine(output, patterns) {
49
72
  }
50
73
  return undefined;
51
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
+ }
156
+ /**
157
+ * True when the captured output carries the suite-specific xpcshell
158
+ * result-summary block, which proves tests executed even though the
159
+ * xpcshell dispatch emits no `TEST-START` line. Used alongside
160
+ * `TEST_START_PATTERN` so a green single-file xpcshell run is not
161
+ * mis-classified as `no-tests`. Exported for direct unit testing.
162
+ */
163
+ function hasXpcshellResultSummary(output) {
164
+ return XPCSHELL_RESULT_SUMMARY_PATTERN.test(output);
165
+ }
52
166
  /** Unexpected-failure lines that are NOT the shutdown re-entry artifact. */
53
167
  function realUnexpectedFailureLines(output) {
54
168
  const matches = output.match(UNEXPECTED_LINE_PATTERN) ?? [];
@@ -59,13 +173,22 @@ function realUnexpectedFailureLines(output) {
59
173
  * Returns undefined for anything that looks like a genuine test result.
60
174
  */
61
175
  export function detectHarnessCrashSignature(output) {
62
- const hasTestStart = TEST_START_PATTERN.test(output);
176
+ const hasTestStart = hasExecutionSignal(output);
63
177
  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);
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);
69
192
  if (signalLine) {
70
193
  return { reason: 'harness startup traceback (resource monitor/psutil)', line: signalLine };
71
194
  }
@@ -74,7 +197,7 @@ export function detectHarnessCrashSignature(output) {
74
197
  // the no-output timeout. A trailing "Passed: 0" summary is part of this
75
198
  // shape and must not be read as a result.
76
199
  if (!hasTestStart) {
77
- const timeoutLine = findLine(output, [NO_OUTPUT_TIMEOUT_PATTERN]);
200
+ const timeoutLine = findLine(evidence, [NO_OUTPUT_TIMEOUT_PATTERN]);
78
201
  if (timeoutLine) {
79
202
  return { reason: 'no-output timeout before any test started', line: timeoutLine };
80
203
  }
@@ -95,9 +218,12 @@ export function detectHarnessCrashSignature(output) {
95
218
  * 1. A recognized crash signature wins regardless of exit code (the
96
219
  * shutdown re-entry shape exits non-zero on an otherwise green run;
97
220
  * 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.
221
+ * 2. No execution signal with explicit paths requested means no test ran —
222
+ * `no-tests`, even when the exit code is zero. The execution signal is
223
+ * a `TEST-START` line (generic `mach test` / browser-chrome dispatch)
224
+ * OR the suite-specific xpcshell result-summary block (the xpcshell
225
+ * dispatch prints no `TEST-START`). Bare `Passed:`/`Failed:` summary
226
+ * lines are still not trusted as evidence of execution.
101
227
  * 3. Exit code zero with tests started is a pass; anything else is a
102
228
  * test failure for the regular diagnosis chain.
103
229
  */
@@ -106,14 +232,26 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
106
232
  if (signature) {
107
233
  return { kind: 'harness-crash', signature };
108
234
  }
109
- if (!TEST_START_PATTERN.test(output) && requestedPaths.length > 0) {
235
+ const ranTests = hasExecutionSignal(output);
236
+ if (!ranTests && requestedPaths.length > 0) {
110
237
  return { kind: 'no-tests' };
111
238
  }
112
- 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' };
113
251
  }
114
252
  /** 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` +
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` +
117
255
  `Detected shape: ${signature.reason}\n` +
118
256
  `Evidence line: ${signature.line}\n\n` +
119
257
  'This failure mode is environmental (mozlog resource monitor / psutil on macOS, focus-stall ' +
@@ -38,6 +38,15 @@ export declare function stripBlockCommentsInLines(lines: string[]): string[];
38
38
  * can splice into the original `lines` array at the returned index.
39
39
  */
40
40
  export declare function findDarkRootInsertionIndex(lines: string[]): number | null;
41
+ /**
42
+ * Depth-counts braces from `startLine` (whose lines must already have
43
+ * block comments stripped), returning the index of the line on which the
44
+ * block opened there returns to its entry depth — i.e. the line carrying
45
+ * the block's closing `}` — or -1 when the block never closes. The first
46
+ * `{` encountered sets the entry depth, so the scan may start on the
47
+ * selector/at-rule line itself rather than on the opener.
48
+ */
49
+ export declare function findBlockCloseIndex(stripped: string[], startLine: number): number;
41
50
  /**
42
51
  * Finds the closing `}` of the outermost
43
52
  * `@media (prefers-color-scheme: dark)` block. Used as the fallback
@@ -122,7 +122,7 @@ export function findDarkRootInsertionIndex(lines) {
122
122
  * `{` encountered sets the entry depth, so the scan may start on the
123
123
  * selector/at-rule line itself rather than on the opener.
124
124
  */
125
- function findBlockCloseIndex(stripped, startLine) {
125
+ export function findBlockCloseIndex(stripped, startLine) {
126
126
  let depth = 0;
127
127
  let entryDepth = 0;
128
128
  let enteredBlock = false;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Documentation-table updates for `fireforge token add`. Extracted from
3
+ * `token-manager.ts` so the CSS-mutation path and the Markdown-table path
4
+ * each stay within the per-file line budget. Consumed only by token-manager.
5
+ */
6
+ /**
7
+ * Minimal token shape the docs updater needs. Declared locally (rather than
8
+ * importing `AddTokenOptions`) so this module has no edge back to
9
+ * `token-manager.ts` — `AddTokenOptions` is structurally compatible.
10
+ */
11
+ export interface TokenDocInput {
12
+ tokenName: string;
13
+ value: string;
14
+ category: string;
15
+ mode: string;
16
+ description?: string | undefined;
17
+ }
18
+ /**
19
+ * Adds a token row to the main token table, the unmapped table (for
20
+ * literal values), and bumps the mode count table. Each sub-update runs
21
+ * against a freshly parsed view of the document so that splice indices
22
+ * stay valid as rewrites are layered.
23
+ *
24
+ * @param annotation - The mode annotation string the caller already computed
25
+ * (kept here as a parameter so this module needs no dependency on
26
+ * token-manager's `getModeAnnotation`).
27
+ */
28
+ export declare function addTokenToDocs(engineDir: string, options: TokenDocInput, annotation: string): Promise<{
29
+ docsAdded: boolean;
30
+ unmappedAdded: boolean;
31
+ countUpdated: boolean;
32
+ }>;