@hominis/fireforge 0.42.0 → 0.43.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.
- package/CHANGELOG.md +75 -34
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +21 -2
- package/dist/src/commands/build.js +7 -1
- package/dist/src/commands/config.js +16 -27
- package/dist/src/commands/discard.js +1 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.js +1 -1
- package/dist/src/commands/export-shared.d.ts +1 -1
- package/dist/src/commands/export-shared.js +4 -5
- package/dist/src/commands/furnace/chrome-doc-remove.js +3 -0
- package/dist/src/commands/furnace/chrome-doc.js +3 -0
- package/dist/src/commands/furnace/create.js +3 -0
- package/dist/src/commands/furnace/override.js +3 -0
- package/dist/src/commands/furnace/preview.js +3 -0
- package/dist/src/commands/furnace/refresh.js +3 -0
- package/dist/src/commands/furnace/remove.js +3 -0
- package/dist/src/commands/furnace/rename.js +3 -0
- package/dist/src/commands/furnace/scan.js +3 -0
- package/dist/src/commands/import.js +1 -1
- package/dist/src/commands/lint-per-patch.js +16 -8
- package/dist/src/commands/lint-per-run-checkjs.d.ts +4 -4
- package/dist/src/commands/lint-per-run-checkjs.js +4 -4
- package/dist/src/commands/lint-report.js +1 -1
- package/dist/src/commands/lint.js +2 -2
- package/dist/src/commands/patch/lint-ignore.js +1 -1
- package/dist/src/commands/patch/move-files-create.js +5 -3
- package/dist/src/commands/patch/move-files-into.d.ts +1 -1
- package/dist/src/commands/patch/move-files-into.js +5 -3
- package/dist/src/commands/patch/move-files.js +1 -1
- package/dist/src/commands/patch/rename.js +1 -1
- package/dist/src/commands/patch/reorder.d.ts +1 -2
- package/dist/src/commands/patch/reorder.js +1 -2
- package/dist/src/commands/patch/split.js +5 -3
- package/dist/src/commands/patch/staged-dependency-validate.js +1 -1
- package/dist/src/commands/patch/staged-dependency.js +4 -4
- package/dist/src/commands/re-export-adjacent.js +1 -1
- package/dist/src/commands/re-export-bulk-scan.d.ts +1 -1
- package/dist/src/commands/re-export-bulk-scan.js +5 -4
- package/dist/src/commands/re-export-drift.d.ts +4 -4
- package/dist/src/commands/re-export-drift.js +47 -8
- package/dist/src/commands/re-export-lint.d.ts +7 -5
- package/dist/src/commands/re-export-lint.js +9 -8
- package/dist/src/commands/re-export-single.d.ts +1 -1
- package/dist/src/commands/re-export-single.js +5 -4
- package/dist/src/commands/re-export.js +26 -14
- package/dist/src/commands/reset.js +1 -1
- package/dist/src/commands/run.js +1 -1
- package/dist/src/commands/source.js +6 -6
- package/dist/src/commands/status-check.d.ts +1 -1
- package/dist/src/commands/status-check.js +1 -1
- package/dist/src/commands/status-json.d.ts +2 -2
- package/dist/src/commands/status-json.js +1 -1
- package/dist/src/commands/status-output.d.ts +1 -1
- package/dist/src/commands/status-output.js +1 -1
- package/dist/src/commands/status.js +21 -7
- package/dist/src/commands/test-build-phase.d.ts +2 -2
- package/dist/src/commands/test-build-phase.js +9 -6
- package/dist/src/commands/test-diagnose.js +8 -1
- package/dist/src/commands/test-modes.d.ts +2 -2
- package/dist/src/commands/test-modes.js +3 -3
- package/dist/src/commands/test-register.js +7 -2
- package/dist/src/commands/test-run.d.ts +6 -0
- package/dist/src/commands/test-run.js +32 -3
- package/dist/src/commands/test-stale-gate.js +7 -2
- package/dist/src/commands/test.js +4 -4
- package/dist/src/commands/tree.js +2 -2
- package/dist/src/commands/typecheck.d.ts +2 -1
- package/dist/src/commands/typecheck.js +59 -3
- package/dist/src/commands/verify.js +26 -1
- package/dist/src/core/ast-utils.js +14 -2
- package/dist/src/core/browser-wire.js +9 -1
- package/dist/src/core/build-baseline-types.d.ts +1 -1
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +1 -1
- package/dist/src/core/build-overwrite-guard.d.ts +49 -0
- package/dist/src/core/build-overwrite-guard.js +112 -0
- package/dist/src/core/build-prepare.d.ts +13 -5
- package/dist/src/core/build-prepare.js +42 -9
- package/dist/src/core/config-mutate.d.ts +3 -2
- package/dist/src/core/config-mutate.js +5 -2
- package/dist/src/core/config.d.ts +3 -2
- package/dist/src/core/config.js +2 -0
- package/dist/src/core/coverage-extend.d.ts +1 -1
- package/dist/src/core/coverage-extend.js +1 -1
- package/dist/src/core/destructive.d.ts +23 -0
- package/dist/src/core/destructive.js +45 -4
- package/dist/src/core/diff-hunks.js +31 -17
- package/dist/src/core/discard-baseline.d.ts +1 -1
- package/dist/src/core/discard-baseline.js +1 -1
- package/dist/src/core/display-state.d.ts +52 -0
- package/dist/src/core/display-state.js +89 -0
- package/dist/src/core/engine-session-lock.d.ts +20 -0
- package/dist/src/core/engine-session-lock.js +84 -14
- package/dist/src/core/export-moved-code-shape.d.ts +60 -0
- package/dist/src/core/export-moved-code-shape.js +129 -0
- package/dist/src/core/file-lock.d.ts +44 -0
- package/dist/src/core/file-lock.js +157 -42
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
- package/dist/src/core/furnace-apply-helpers.js +1 -1
- package/dist/src/core/furnace-apply-output.js +1 -1
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +2 -2
- package/dist/src/core/furnace-apply-overwrite-warn.js +2 -2
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
- package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
- package/dist/src/core/furnace-apply.js +13 -1
- package/dist/src/core/furnace-config-custom.d.ts +2 -1
- package/dist/src/core/furnace-config-order.d.ts +2 -1
- package/dist/src/core/furnace-config-order.js +9 -4
- package/dist/src/core/furnace-config-validate.d.ts +4 -3
- package/dist/src/core/furnace-config-validate.js +3 -3
- package/dist/src/core/furnace-config.js +5 -0
- package/dist/src/core/furnace-operation.d.ts +12 -0
- package/dist/src/core/furnace-operation.js +125 -1
- package/dist/src/core/furnace-rollback.d.ts +17 -0
- package/dist/src/core/furnace-rollback.js +35 -1
- package/dist/src/core/git-base.js +7 -2
- package/dist/src/core/git-diff.js +11 -2
- package/dist/src/core/git-readonly-index.d.ts +59 -0
- package/dist/src/core/git-readonly-index.js +137 -0
- package/dist/src/core/license-headers.d.ts +1 -1
- package/dist/src/core/license-headers.js +1 -1
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +35 -8
- package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
- package/dist/src/core/mach-known-noise-filter.js +23 -0
- package/dist/src/core/mach-mozconfig.js +12 -2
- package/dist/src/core/module-resolution-preflight.d.ts +51 -0
- package/dist/src/core/module-resolution-preflight.js +155 -0
- package/dist/src/core/patch-export.js +2 -1
- package/dist/src/core/patch-identifier-suggest.js +29 -3
- package/dist/src/core/patch-lint-checkjs.d.ts +5 -5
- package/dist/src/core/patch-lint-checkjs.js +6 -6
- package/dist/src/core/patch-lint-creators.d.ts +1 -1
- package/dist/src/core/patch-lint-creators.js +1 -1
- package/dist/src/core/patch-lint-cross.d.ts +1 -1
- package/dist/src/core/patch-lint-cross.js +4 -4
- package/dist/src/core/patch-lint-css.js +1 -1
- package/dist/src/core/patch-lint-diff.d.ts +1 -1
- package/dist/src/core/patch-lint-diff.js +1 -1
- package/dist/src/core/patch-lint-jsdoc.js +4 -2
- package/dist/src/core/patch-lint-module-registration.d.ts +13 -0
- package/dist/src/core/patch-lint-module-registration.js +13 -4
- package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
- package/dist/src/core/patch-lint-mozbuild.js +1 -1
- package/dist/src/core/patch-lint-ownership.d.ts +2 -2
- package/dist/src/core/patch-lint-ownership.js +2 -2
- package/dist/src/core/patch-lint.d.ts +2 -2
- package/dist/src/core/patch-lint.js +3 -3
- package/dist/src/core/patch-lock.d.ts +21 -0
- package/dist/src/core/patch-lock.js +29 -2
- package/dist/src/core/patch-manifest-io.d.ts +2 -1
- package/dist/src/core/patch-manifest-io.js +49 -0
- package/dist/src/core/patch-parse.d.ts +18 -0
- package/dist/src/core/patch-parse.js +24 -1
- package/dist/src/core/patch-policy.d.ts +2 -2
- package/dist/src/core/patch-policy.js +2 -2
- package/dist/src/core/smoke-patterns.d.ts +1 -1
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +1 -1
- package/dist/src/core/status-classify.d.ts +1 -1
- package/dist/src/core/status-classify.js +2 -2
- package/dist/src/core/test-harness-crash.d.ts +69 -3
- package/dist/src/core/test-harness-crash.js +174 -9
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +1 -1
- package/dist/src/core/token-category.js +1 -1
- package/dist/src/core/token-manager.d.ts +1 -1
- package/dist/src/core/token-manager.js +5 -5
- package/dist/src/core/tree-cow.js +1 -1
- package/dist/src/core/tree-guard.js +2 -2
- package/dist/src/core/tree-store.js +1 -1
- package/dist/src/core/typecheck-shim.d.ts +2 -2
- package/dist/src/core/typecheck-shim.js +2 -2
- package/dist/src/core/typecheck.js +10 -0
- package/dist/src/core/xpcshell-profile-dir.js +1 -1
- package/dist/src/errors/base.d.ts +22 -1
- package/dist/src/errors/base.js +32 -1
- package/dist/src/errors/codes.d.ts +10 -0
- package/dist/src/errors/codes.js +10 -0
- package/dist/src/types/commands/options.d.ts +27 -11
- package/dist/src/types/commands/patches.d.ts +1 -1
- package/dist/src/types/config.d.ts +2 -2
- package/dist/src/types/furnace.d.ts +1 -1
- package/dist/src/types/json.d.ts +22 -0
- package/dist/src/types/json.js +11 -0
- package/dist/src/utils/assert.d.ts +46 -0
- package/dist/src/utils/assert.js +83 -0
- package/dist/src/utils/build-info.d.ts +1 -1
- package/dist/src/utils/build-info.js +1 -1
- package/dist/src/utils/logger.d.ts +20 -0
- package/dist/src/utils/logger.js +26 -0
- package/dist/src/utils/options.d.ts +12 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/stdio-drain.d.ts +1 -1
- package/dist/src/utils/stdio-drain.js +1 -1
- package/dist/src/utils/validation.d.ts +12 -1
- package/dist/src/utils/validation.js +13 -1
- package/package.json +3 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Pre-test build phase for `fireforge test`, split out of `test.ts` (at
|
|
4
4
|
* the per-file line budget): the incremental protected mach build with
|
|
5
|
-
* its baseline write, and the
|
|
5
|
+
* its baseline write, and the `--build-only` union-build shape.
|
|
6
6
|
*/
|
|
7
7
|
import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
|
|
8
8
|
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
@@ -12,7 +12,7 @@ import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
|
|
|
12
12
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
13
13
|
import { BuildError } from '../errors/build.js';
|
|
14
14
|
import { toError } from '../utils/errors.js';
|
|
15
|
-
import { info, spinner, verbose } from '../utils/logger.js';
|
|
15
|
+
import { info, notice, spinner, verbose } from '../utils/logger.js';
|
|
16
16
|
import { reportBuildOnlyCompletion } from './test-modes.js';
|
|
17
17
|
import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
|
|
18
18
|
import { emitPassVerdict } from './test-verdict.js';
|
|
@@ -28,7 +28,7 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
28
28
|
const preparation = await prepareBuildEnvironment(projectRoot, paths, projectConfig, {
|
|
29
29
|
previousBaseline,
|
|
30
30
|
});
|
|
31
|
-
// The mozconfig half of the --extend-coverage anchor
|
|
31
|
+
// The mozconfig half of the --extend-coverage anchor can
|
|
32
32
|
// only be checked once prepareBuildEnvironment has regenerated
|
|
33
33
|
// engine/mozconfig — that is the file this build will configure with —
|
|
34
34
|
// and must still be checked before mach, so a refusal costs no build.
|
|
@@ -40,7 +40,10 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
40
40
|
}
|
|
41
41
|
const buildKind = preparation.fullBuildRequired ? 'full' : 'faster';
|
|
42
42
|
if (preparation.fullBuildRequired) {
|
|
43
|
-
|
|
43
|
+
// Warning severity, not info: agent-facing output filters
|
|
44
|
+
// keep warnings and errors only, and this is the line that explains an
|
|
45
|
+
// otherwise unexplained multi-minute build.
|
|
46
|
+
notice('A jar.mn registration changed since the last successful build; escalating this pre-test build to a full mach build so new install-manifest destinations are created.');
|
|
44
47
|
}
|
|
45
48
|
const s = spinner(preparation.fullBuildRequired
|
|
46
49
|
? 'Running required full build...'
|
|
@@ -120,7 +123,7 @@ function describeBuildInvocation(extending, coverage) {
|
|
|
120
123
|
/**
|
|
121
124
|
* Runs the pre-test build (or the stale-build gate when no build was
|
|
122
125
|
* requested). Returns `true` when the run is COMPLETE — the `--build-only`
|
|
123
|
-
* union-build shape
|
|
126
|
+
* union-build shape, which packages every requested path
|
|
124
127
|
* (mixed harnesses legal, nothing dispatches), prints the per-harness
|
|
125
128
|
* next steps, and emits the run's single `PASS` verdict.
|
|
126
129
|
*/
|
|
@@ -152,7 +155,7 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
|
|
|
152
155
|
await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, coverage, extending ? { requestedPaths: normalizedPaths } : undefined);
|
|
153
156
|
info('');
|
|
154
157
|
if (options.buildOnly) {
|
|
155
|
-
// Union build for mixed harnesses
|
|
158
|
+
// Union build for mixed harnesses: the coverage claim
|
|
156
159
|
// above lists every requested path, so each harness half can run
|
|
157
160
|
// build-less against this packaging. Nothing dispatches here; the
|
|
158
161
|
// run still ends with exactly one verdict line.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `test-harness-crash.ts` for single and sharded invocations. Split out of
|
|
8
8
|
* `test.ts` to keep both files within the per-file line budget.
|
|
9
9
|
*/
|
|
10
|
-
import { buildGreenSummaryRejectedMessage, buildHarnessCrashMessage, buildNoTestsRanMessage, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
|
|
10
|
+
import { buildGreenSummaryRejectedMessage, buildHarnessCrashMessage, buildNoTestsRanMessage, buildSilentSegfaultMessage, headedNoOutputTimeoutHint, isSilentSegfault, } from '../core/test-harness-crash.js';
|
|
11
11
|
import { buildHarnessEarlyExitMessage, classifyHarnessEarlyExit, completePostRebuildFailureContext, prependPostRebuildFailureContext, } from '../core/test-harness-output.js';
|
|
12
12
|
import { GeneralError } from '../errors/base.js';
|
|
13
13
|
import { BuildError } from '../errors/build.js';
|
|
@@ -144,6 +144,12 @@ function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted
|
|
|
144
144
|
const throwGeneral = (message) => {
|
|
145
145
|
throw new GeneralError(withContext(message));
|
|
146
146
|
};
|
|
147
|
+
// First, because there is nothing else to go on: a SIGSEGV with zero
|
|
148
|
+
// output has exactly one known cause and no other evidence.
|
|
149
|
+
// Every branch below keys on output text this shape does not have.
|
|
150
|
+
if (isSilentSegfault(result.exitCode, combinedOutput)) {
|
|
151
|
+
throwGeneral(buildSilentSegfaultMessage(result.exitCode, normalizedPaths));
|
|
152
|
+
}
|
|
147
153
|
if (/UNKNOWN TEST\b/i.test(combinedOutput)) {
|
|
148
154
|
throwGeneral(buildUnknownTestMessage(normalizedPaths));
|
|
149
155
|
}
|
|
@@ -214,6 +220,7 @@ function applySingleRunOutcome(outcome, normalizedPaths, binaryName, postRebuild
|
|
|
214
220
|
const hint = headedNoOutputTimeoutHint(outcome.verdict.signature, {
|
|
215
221
|
headless,
|
|
216
222
|
platform: getPlatform(),
|
|
223
|
+
...(outcome.displayState !== undefined ? { displayState: outcome.displayState } : {}),
|
|
217
224
|
});
|
|
218
225
|
throw new GeneralError(hint ? `${base}\n\n${hint}` : base);
|
|
219
226
|
}
|
|
@@ -21,13 +21,13 @@ export interface HarnessClassification {
|
|
|
21
21
|
* Harness classification is a pure path→manifest lookup and must precede
|
|
22
22
|
* the build dispatch: a mixed xpcshell+mochitest request can never
|
|
23
23
|
* dispatch, so refusing it before spending minutes in a pre-test build
|
|
24
|
-
* is strictly better
|
|
24
|
+
* is strictly better. A nonexistent path classifies as
|
|
25
25
|
* nonXpcshell (findNearestXpcshellManifest returns null), so classifying
|
|
26
26
|
* before assertTestPathsExist is harmless — existence is still asserted
|
|
27
27
|
* after the stale/coverage gates, preserving their precedence over
|
|
28
28
|
* missing-path errors. `xpcshellOnly` (non-empty request, zero
|
|
29
29
|
* nonXpcshell paths) gates the Marionette preflight and the mochitest
|
|
30
|
-
* client flags
|
|
30
|
+
* client flags; pathless runs keep the full-suite behavior.
|
|
31
31
|
*/
|
|
32
32
|
export declare function classifyBeforeDispatch(engineDir: string, normalizedPaths: string[], options?: {
|
|
33
33
|
allowMixed?: boolean;
|
|
@@ -96,19 +96,19 @@ function buildMixedHarnessMessage(classification) {
|
|
|
96
96
|
* Harness classification is a pure path→manifest lookup and must precede
|
|
97
97
|
* the build dispatch: a mixed xpcshell+mochitest request can never
|
|
98
98
|
* dispatch, so refusing it before spending minutes in a pre-test build
|
|
99
|
-
* is strictly better
|
|
99
|
+
* is strictly better. A nonexistent path classifies as
|
|
100
100
|
* nonXpcshell (findNearestXpcshellManifest returns null), so classifying
|
|
101
101
|
* before assertTestPathsExist is harmless — existence is still asserted
|
|
102
102
|
* after the stale/coverage gates, preserving their precedence over
|
|
103
103
|
* missing-path errors. `xpcshellOnly` (non-empty request, zero
|
|
104
104
|
* nonXpcshell paths) gates the Marionette preflight and the mochitest
|
|
105
|
-
* client flags
|
|
105
|
+
* client flags; pathless runs keep the full-suite behavior.
|
|
106
106
|
*/
|
|
107
107
|
export async function classifyBeforeDispatch(engineDir, normalizedPaths, options) {
|
|
108
108
|
const classification = await classifyTestHarnesses(engineDir, normalizedPaths);
|
|
109
109
|
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
110
110
|
// `--build-only` never dispatches, so a mixed request is legal there —
|
|
111
|
-
// packaging the union is the whole point
|
|
111
|
+
// packaging the union is the whole point.
|
|
112
112
|
if (options?.allowMixed !== true) {
|
|
113
113
|
throw new GeneralError(buildMixedHarnessMessage(classification));
|
|
114
114
|
}
|
|
@@ -29,7 +29,7 @@ const TEST_HELP_TEXT = [
|
|
|
29
29
|
'--doctor) ends with exactly one raw stdout line',
|
|
30
30
|
' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|test-failures|',
|
|
31
31
|
' no-tests|preflight|inconclusive|lock-timeout] [checks=<n>]',
|
|
32
|
-
' [unexpected=<n>] [shards=<p>/<t>]',
|
|
32
|
+
' [unexpected=<n>] [shards=<p>/<t>] [(<note>)]',
|
|
33
33
|
'The status follows the harness classifier, not the raw exit code:',
|
|
34
34
|
'a crash-classified run says FAIL reason=crash even at exit 0, and',
|
|
35
35
|
'a green-summary-override pass says PASS despite a non-zero mach',
|
|
@@ -41,7 +41,12 @@ const TEST_HELP_TEXT = [
|
|
|
41
41
|
'engine-mutating command held the lock past the wait budget. A sharded',
|
|
42
42
|
'aggregate reason is the most structural failing shard (crash,',
|
|
43
43
|
'then no-tests, then test-failures). Count keys are omitted when',
|
|
44
|
-
'the embedded summary did not print them.
|
|
44
|
+
'the embedded summary did not print them. A trailing parenthetical',
|
|
45
|
+
'is an advisory note, never part of the status: "(harness teardown',
|
|
46
|
+
'noise ignored)" marks a clean suite whose only residue was the',
|
|
47
|
+
'recognized upstream mozsystemmonitor teardown traceback, and',
|
|
48
|
+
'"(headed run stalled with the display asleep)" marks a no-output',
|
|
49
|
+
'stall whose display was measured asleep. Key on this line',
|
|
45
50
|
'instead of mach internals.',
|
|
46
51
|
].join('\n');
|
|
47
52
|
/** Registers the test command on the CLI program. */
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* harness runs cost startup time but make results reproducible; the
|
|
11
11
|
* combined invocation stays available via `--no-shard`.
|
|
12
12
|
*/
|
|
13
|
+
import { type DisplaySleepState } from '../core/display-state.js';
|
|
13
14
|
import { type MachCommandResult } from '../core/mach.js';
|
|
14
15
|
import { type HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
15
16
|
/** Default bounded retry budget for recognized harness crashes. */
|
|
@@ -48,6 +49,11 @@ export interface TestRunOutcome {
|
|
|
48
49
|
attempts: number;
|
|
49
50
|
/** Whether xpcshell appdir injection was attempted for this invocation. */
|
|
50
51
|
appdirInjectionAttempted: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Display power state measured for a HEADED no-output stall;
|
|
54
|
+
* undefined when the shape did not apply and no probe ran.
|
|
55
|
+
*/
|
|
56
|
+
displayState?: DisplaySleepState | undefined;
|
|
51
57
|
}
|
|
52
58
|
/**
|
|
53
59
|
* Runs one mach test invocation for `paths`, retrying recognized harness
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* harness runs cost startup time but make results reproducible; the
|
|
12
12
|
* combined invocation stays available via `--no-shard`.
|
|
13
13
|
*/
|
|
14
|
+
import { probeDisplaySleepState } from '../core/display-state.js';
|
|
14
15
|
import { mochitestWithOutput, testWithOutput, xpcshellTestWithOutput, } from '../core/mach.js';
|
|
15
|
-
import { buildHarnessCrashMessage, classifyHarnessRun, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
|
|
16
|
+
import { buildHarnessCrashMessage, classifyHarnessRun, headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
|
|
16
17
|
import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
|
|
17
18
|
import { withXpcshellProfileDir } from '../core/xpcshell-profile-dir.js';
|
|
18
19
|
import { BuildError } from '../errors/build.js';
|
|
@@ -37,7 +38,7 @@ function dispatchForSuite(suite) {
|
|
|
37
38
|
*/
|
|
38
39
|
export async function runTestsWithRetries(ctx, paths) {
|
|
39
40
|
// Pure mochitest dispatches keep their env byte-identical — the profile
|
|
40
|
-
// variable is xpcshell-harness-specific
|
|
41
|
+
// variable is xpcshell-harness-specific. Generic `mach
|
|
41
42
|
// test` runs may include xpcshell paths, so they get the isolation too.
|
|
42
43
|
if (ctx.suite === 'mochitest') {
|
|
43
44
|
return runTestsWithRetriesInner(ctx, paths, ctx.env);
|
|
@@ -71,7 +72,34 @@ async function runTestsWithRetriesInner(ctx, paths, env) {
|
|
|
71
72
|
`${verdict.signature?.line ?? ''}\n` +
|
|
72
73
|
`Retrying (attempt ${attempts + 1} of ${maxAttempts})...`);
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
const displayState = await probeDisplayStateForStall(verdict, ctx.headless);
|
|
76
|
+
if (displayState !== undefined) {
|
|
77
|
+
const note = headedDisplayAsleepVerdictNote(verdict.signature, {
|
|
78
|
+
headless: ctx.headless,
|
|
79
|
+
platform: getPlatform(),
|
|
80
|
+
displayState,
|
|
81
|
+
});
|
|
82
|
+
if (note !== undefined)
|
|
83
|
+
verdict = { ...verdict, note };
|
|
84
|
+
}
|
|
85
|
+
return { result, verdict, attempts, appdirInjectionAttempted, displayState };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Probes the display's power state for a HEADED no-output stall, and only
|
|
89
|
+
* for that shape. Every other outcome — a passing run, a real
|
|
90
|
+
* test failure, a headless run, any other crash shape — skips the probe
|
|
91
|
+
* entirely, so the common paths spawn nothing.
|
|
92
|
+
*
|
|
93
|
+
* @returns The measured state, or undefined when the shape does not apply
|
|
94
|
+
*/
|
|
95
|
+
async function probeDisplayStateForStall(verdict, headless) {
|
|
96
|
+
if (headless)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (verdict.kind !== 'harness-crash')
|
|
99
|
+
return undefined;
|
|
100
|
+
if (!verdict.signature?.reason.includes('no-output timeout'))
|
|
101
|
+
return undefined;
|
|
102
|
+
return probeDisplaySleepState(getPlatform());
|
|
75
103
|
}
|
|
76
104
|
const SHARD_STATUS_LABEL = {
|
|
77
105
|
'tests-ran-ok': 'PASS',
|
|
@@ -119,6 +147,7 @@ export async function runShardedTests(ctx, groups, diagnoseShardFailure) {
|
|
|
119
147
|
const hint = headedNoOutputTimeoutHint(outcome.verdict.signature, {
|
|
120
148
|
headless: ctx.headless,
|
|
121
149
|
platform: getPlatform(),
|
|
150
|
+
...(outcome.displayState !== undefined ? { displayState: outcome.displayState } : {}),
|
|
122
151
|
});
|
|
123
152
|
warn(hint ? `${base}\n\n${hint}` : base);
|
|
124
153
|
}
|
|
@@ -44,8 +44,13 @@ export async function enforceStaleBuildGate(projectRoot, engineDir, options, nor
|
|
|
44
44
|
await enforceStaticComponentsGate(engineDir, stale.baseline, options);
|
|
45
45
|
const staleMessage = stale.stale
|
|
46
46
|
? `${formatStaleBuildWarning(stale)}\n\n` +
|
|
47
|
-
'Run `fireforge test --build` to refresh the packaged runtime first
|
|
48
|
-
'
|
|
47
|
+
'Run `fireforge test --build` to refresh the packaged runtime first — that is almost ' +
|
|
48
|
+
'always the right move, because the usual cause is your own edit landing between the ' +
|
|
49
|
+
'last build and this run.\n\n' +
|
|
50
|
+
'`--allow-stale-build` is NOT the general escape hatch: it accepts a packaging that ' +
|
|
51
|
+
'predates the edit under test, so the run tests the OLD code. Pass it only when you ' +
|
|
52
|
+
'rebuilt out-of-band (outside FireForge) and know the packaged runtime already contains ' +
|
|
53
|
+
'the change.'
|
|
49
54
|
: undefined;
|
|
50
55
|
if (staleMessage !== undefined) {
|
|
51
56
|
if (options.allowStaleBuild === true) {
|
|
@@ -12,7 +12,7 @@ import { assertObjdirMatchesTreeMarker } from '../core/tree-store.js';
|
|
|
12
12
|
import { GeneralError } from '../errors/base.js';
|
|
13
13
|
import { BuildError } from '../errors/build.js';
|
|
14
14
|
import { pathExists } from '../utils/fs.js';
|
|
15
|
-
import { info, intro, outro, success, verbose } from '../utils/logger.js';
|
|
15
|
+
import { info, intro, notice, outro, success, verbose } from '../utils/logger.js';
|
|
16
16
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
17
17
|
import { runTestBuildPhase } from './test-build-phase.js';
|
|
18
18
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
@@ -174,7 +174,7 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcsh
|
|
|
174
174
|
// Manifest classification says every requested path is xpcshell —
|
|
175
175
|
// xpcshell ignores the browser Marionette path entirely, and the
|
|
176
176
|
// mochitest client flags previously forwarded here made mach reject
|
|
177
|
-
// the dispatch
|
|
177
|
+
// the dispatch.
|
|
178
178
|
info(`--marionette-port=${options.marionettePort} applied to the preflight probe only: the requested paths are xpcshell-only, and xpcshell ignores the browser Marionette port. Not forwarding --setpref=marionette.port or --marionette to mach.`);
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
@@ -202,7 +202,7 @@ async function ensureTestMarionettePortAvailable(port, binaryName, options, skip
|
|
|
202
202
|
if (skip.xpcshellOnly && !skip.doctor) {
|
|
203
203
|
// xpcshell does not bind the browser Marionette port, so a developer's
|
|
204
204
|
// interactive browser holding 2828 must not kill an xpcshell run
|
|
205
|
-
|
|
205
|
+
//. --doctor keeps the preflight: its probe launches a
|
|
206
206
|
// Marionette browser regardless of the requested harness.
|
|
207
207
|
const message = 'Skipping the Marionette stale-port preflight: all requested paths are xpcshell ' +
|
|
208
208
|
'(xpcshell does not bind the browser Marionette port).';
|
|
@@ -431,7 +431,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
431
431
|
// bug to an upstream headless regression), so a one-line notice states
|
|
432
432
|
// what sharding does and does not exercise.
|
|
433
433
|
if (canaryPath === undefined && dispatchGroups.length > 1 && options.shard !== false) {
|
|
434
|
-
|
|
434
|
+
notice(`Sharding: running ${dispatchGroups.length} test path arguments in isolated browser instances ` +
|
|
435
435
|
'(one mach invocation per argument; a directory argument keeps its files in one instance). ' +
|
|
436
436
|
'Cross-argument state is NOT exercised — pass --no-shard for a combined single-instance run.');
|
|
437
437
|
const generationBefore = await snapshotEngineGeneration(paths.engine);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* `fireforge tree` — copy-on-write verification clones
|
|
3
|
+
* `fireforge tree` — copy-on-write verification clones.
|
|
4
4
|
*
|
|
5
5
|
* Trees enable concurrent READ-MOSTLY verification (lint, typecheck,
|
|
6
6
|
* status, verify, doctor, export dry-runs) beside a busy primary tree.
|
|
@@ -263,7 +263,7 @@ async function treeExecCommand(projectRoot, name, args) {
|
|
|
263
263
|
resolvePromise(code ?? 1);
|
|
264
264
|
});
|
|
265
265
|
}).finally(() => {
|
|
266
|
-
//
|
|
266
|
+
// With `stdio: 'inherit'` the child owned stdout — including
|
|
267
267
|
// any FIREFORGE-VERDICT line it emitted as its LAST stdout write. From
|
|
268
268
|
// here on the parent must not write stdout again, or its own refusal
|
|
269
269
|
// text (the GeneralError below rendered by withErrorHandling) would
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { Command } from 'commander';
|
|
18
18
|
import type { CommandContext } from '../types/cli.js';
|
|
19
|
+
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
19
20
|
import type { TypecheckConfig } from '../types/config.js';
|
|
20
21
|
import type { TypecheckProjectResult } from '../types/typecheck.js';
|
|
21
22
|
/** Command-line options Commander forwards from `fireforge typecheck`. */
|
|
@@ -45,7 +46,7 @@ export declare function typecheckCommand(projectRoot: string, options: Typecheck
|
|
|
45
46
|
* throws on errors. Extracted so it can be exercised directly by
|
|
46
47
|
* the CLI test without spawning a child process.
|
|
47
48
|
*/
|
|
48
|
-
export declare function reportResults(projectRoot: string, results: ReadonlyArray<TypecheckProjectResult>): void;
|
|
49
|
+
export declare function reportResults(projectRoot: string, results: ReadonlyArray<TypecheckProjectResult>, patchLintIssues?: ReadonlyArray<PatchLintIssue>): void;
|
|
49
50
|
/**
|
|
50
51
|
* Registers the `typecheck` command on the CLI program.
|
|
51
52
|
*/
|
|
@@ -19,9 +19,13 @@ import { Command } from 'commander';
|
|
|
19
19
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
20
20
|
import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
|
|
21
21
|
import { findJsconfigPathsDrift, syncFurnaceJsconfigPaths } from '../core/furnace-jsconfig.js';
|
|
22
|
+
import { withPrivateGitIndex } from '../core/git-readonly-index.js';
|
|
23
|
+
import { buildPatchQueueContext } from '../core/patch-lint.js';
|
|
22
24
|
import { relativeForDisplay, runTypecheck } from '../core/typecheck.js';
|
|
23
25
|
import { GeneralError } from '../errors/base.js';
|
|
26
|
+
import { pathExists } from '../utils/fs.js';
|
|
24
27
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
28
|
+
import { buildPerRunCheckJs } from './lint-per-run-checkjs.js';
|
|
25
29
|
/**
|
|
26
30
|
* Resolves the project list to type-check. `--project` wins over
|
|
27
31
|
* config; if neither is set, throws a clear error pointing at both
|
|
@@ -65,6 +69,13 @@ function formatIssue(projectRoot, issue) {
|
|
|
65
69
|
*/
|
|
66
70
|
export async function typecheckCommand(projectRoot, options) {
|
|
67
71
|
intro('FireForge typecheck');
|
|
72
|
+
// Read-only to the operator, an index WRITER to git without this scope
|
|
73
|
+
//: the Furnace jsconfig reconciler and the project resolution
|
|
74
|
+
// below touch `engine/`, and any git plumbing they reach would refresh
|
|
75
|
+
// the primary checkout's index under a concurrent `fireforge test`.
|
|
76
|
+
return withPrivateGitIndex(getProjectPaths(projectRoot).engine, () => runTypecheckCommandBody(projectRoot, options));
|
|
77
|
+
}
|
|
78
|
+
async function runTypecheckCommandBody(projectRoot, options) {
|
|
68
79
|
// Validate project is initialised. `loadConfig` throws on missing
|
|
69
80
|
// fireforge.json — withErrorHandling at the CLI layer renders the
|
|
70
81
|
// resulting `ConfigNotFoundError` cleanly, so we don't need to
|
|
@@ -81,7 +92,38 @@ export async function typecheckCommand(projectRoot, options) {
|
|
|
81
92
|
await regenerateStaleGeneratedJsconfig(projectRoot);
|
|
82
93
|
info(`Running typecheck across ${String(cfg.projects.length)} project(s): ${cfg.projects.join(', ')}`);
|
|
83
94
|
const results = await runTypecheck(projectRoot, cfg);
|
|
84
|
-
|
|
95
|
+
// Fold the EXPORT-TIME authority into this pass. Per-patch lint runs
|
|
96
|
+
// checkJs over the queue's patch-owned modules in relative isolation and
|
|
97
|
+
// resolves imported typedefs differently from the whole-project pass, so
|
|
98
|
+
// `typecheck` could report 0 errors across every
|
|
99
|
+
// project while the very next `export` failed per-patch lint with
|
|
100
|
+
// `checkjs-type-error` findings. Two authorities that disagree, with the
|
|
101
|
+
// stricter one speaking only at export time, is not a usable CI gate:
|
|
102
|
+
// running both here makes `typecheck` green MEAN export-clean types.
|
|
103
|
+
const patchIssues = await collectPatchLintCheckJsIssues(projectRoot);
|
|
104
|
+
reportResults(projectRoot, results, patchIssues);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Runs the per-patch-lint checkJs pass — the same program `export` and
|
|
108
|
+
* `lint --per-patch` use — and returns its findings.
|
|
109
|
+
*
|
|
110
|
+
* Returns an empty list when `patchLint.checkJs` is off or the project has
|
|
111
|
+
* no patch queue: there is no second authority to reconcile then.
|
|
112
|
+
*/
|
|
113
|
+
async function collectPatchLintCheckJsIssues(projectRoot) {
|
|
114
|
+
const paths = getProjectPaths(projectRoot);
|
|
115
|
+
const config = await loadConfig(projectRoot);
|
|
116
|
+
if (config.patchLint?.checkJs !== true)
|
|
117
|
+
return [];
|
|
118
|
+
if (!(await pathExists(paths.patches)))
|
|
119
|
+
return [];
|
|
120
|
+
const ctx = await buildPatchQueueContext(paths.patches, config);
|
|
121
|
+
const checkJs = buildPerRunCheckJs(projectRoot, paths, config, ctx);
|
|
122
|
+
if (checkJs === undefined)
|
|
123
|
+
return [];
|
|
124
|
+
info('Also running the per-patch checkJs pass (the authority `export` enforces).');
|
|
125
|
+
const grouped = await checkJs.getGrouped();
|
|
126
|
+
return [...grouped.global, ...[...grouped.byFile.values()].flat()];
|
|
85
127
|
}
|
|
86
128
|
/**
|
|
87
129
|
* Staleness-checks and regenerates the Furnace-managed jsconfig
|
|
@@ -109,9 +151,19 @@ async function regenerateStaleGeneratedJsconfig(projectRoot) {
|
|
|
109
151
|
* throws on errors. Extracted so it can be exercised directly by
|
|
110
152
|
* the CLI test without spawning a child process.
|
|
111
153
|
*/
|
|
112
|
-
export function reportResults(projectRoot, results) {
|
|
154
|
+
export function reportResults(projectRoot, results, patchLintIssues = []) {
|
|
113
155
|
let totalErrors = 0;
|
|
114
156
|
let totalWarnings = 0;
|
|
157
|
+
// Per-patch checkJs findings are reported in the SAME tally:
|
|
158
|
+
// a split report would let an operator read "0 errors" off the project
|
|
159
|
+
// summary and still be refused at export.
|
|
160
|
+
for (const issue of patchLintIssues) {
|
|
161
|
+
if (issue.severity === 'error')
|
|
162
|
+
totalErrors += 1;
|
|
163
|
+
else
|
|
164
|
+
totalWarnings += 1;
|
|
165
|
+
warn(`[per-patch ${issue.check}] ${issue.file}: ${issue.message}`);
|
|
166
|
+
}
|
|
115
167
|
for (const result of results) {
|
|
116
168
|
const errors = result.issues.filter((i) => i.category === 'error');
|
|
117
169
|
const warnings = result.issues.filter((i) => i.category === 'warning');
|
|
@@ -122,7 +174,11 @@ export function reportResults(projectRoot, results) {
|
|
|
122
174
|
for (const issue of errors)
|
|
123
175
|
warn(formatIssue(projectRoot, issue));
|
|
124
176
|
}
|
|
125
|
-
const summary = `Typecheck: ${String(totalErrors)} error(s), ${String(totalWarnings)} warning(s) across
|
|
177
|
+
const summary = `Typecheck: ${String(totalErrors)} error(s), ${String(totalWarnings)} warning(s) across ` +
|
|
178
|
+
`${String(results.length)} project(s)` +
|
|
179
|
+
(patchLintIssues.length > 0
|
|
180
|
+
? ` (including ${String(patchLintIssues.length)} per-patch checkJs finding(s))`
|
|
181
|
+
: '');
|
|
126
182
|
if (totalErrors === 0) {
|
|
127
183
|
success(summary);
|
|
128
184
|
outro(totalWarnings > 0 ? 'Typecheck passed with warnings' : 'Typecheck passed');
|
|
@@ -19,7 +19,9 @@ import { Command } from 'commander';
|
|
|
19
19
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
20
20
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
21
21
|
import { isGitRepository } from '../core/git.js';
|
|
22
|
+
import { withPrivateGitIndex } from '../core/git-readonly-index.js';
|
|
22
23
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
24
|
+
import { findUnresolvedSystemModuleImports, formatUnresolvedSystemModuleImports, } from '../core/module-resolution-preflight.js';
|
|
23
25
|
import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../core/patch-lint.js';
|
|
24
26
|
import { loadPatchesManifest, validatePatchesManifestConsistency } from '../core/patch-manifest.js';
|
|
25
27
|
import { evaluatePatchPolicy } from '../core/patch-policy.js';
|
|
@@ -167,6 +169,24 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
167
169
|
errorCount += policyErrors;
|
|
168
170
|
warningCount += policyWarnings;
|
|
169
171
|
}
|
|
172
|
+
// Engine-aware module-resolution preflight. The queue-level
|
|
173
|
+
// `unregistered-system-module` rule only sees modules a patch NEWLY
|
|
174
|
+
// CREATES; this asks the same question of the engine, so an import
|
|
175
|
+
// ADDED to an existing module — the shape that recurred and cost a
|
|
176
|
+
// rebuild cycle — is named as text instead of surfacing later as a
|
|
177
|
+
// bare `xpcshell return code: -11` with zero output.
|
|
178
|
+
if (await pathExists(paths.engine)) {
|
|
179
|
+
const unresolved = await findUnresolvedSystemModuleImports(paths.engine, manifest.patches.flatMap((patch) => patch.filesAffected));
|
|
180
|
+
if (unresolved.length > 0) {
|
|
181
|
+
groups.push({
|
|
182
|
+
title: `Unresolvable system-module imports (${unresolved.length})`,
|
|
183
|
+
issues: formatUnresolvedSystemModuleImports(unresolved),
|
|
184
|
+
errorCount: unresolved.length,
|
|
185
|
+
warningCount: 0,
|
|
186
|
+
});
|
|
187
|
+
errorCount += unresolved.length;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
170
190
|
const crossClaims = detectCrossPatchFileClaims(manifest.patches);
|
|
171
191
|
if (crossClaims.length > 0) {
|
|
172
192
|
groups.push({
|
|
@@ -241,7 +261,12 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
241
261
|
*/
|
|
242
262
|
export async function verifyCommand(projectRoot) {
|
|
243
263
|
intro('FireForge Verify');
|
|
244
|
-
|
|
264
|
+
// `verify` is read-only to the OPERATOR but not to git: the working-tree
|
|
265
|
+
// status probe refreshes (and rewrites) `engine/.git/index`, making this
|
|
266
|
+
// command a second writer of the primary checkout and invalidating a
|
|
267
|
+
// concurrent `fireforge test`'s engine fingerprint. A private index keeps
|
|
268
|
+
// every refresh off the primary checkout.
|
|
269
|
+
const health = await withPrivateGitIndex(getProjectPaths(projectRoot).engine, () => collectPatchQueueHealth(projectRoot));
|
|
245
270
|
if (!health.hasPatchesDirectory) {
|
|
246
271
|
info('No patches directory. Nothing to verify.');
|
|
247
272
|
outro('Verify clean');
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import * as acorn from 'acorn';
|
|
3
3
|
import { walk } from 'estree-walker';
|
|
4
|
+
/**
|
|
5
|
+
* Bridges acorn's return type to the positioned ESTree program type.
|
|
6
|
+
* `acorn.parse` produces nodes that carry both the ESTree shape and
|
|
7
|
+
* `start`/`end` offsets at runtime, but its static `acorn.Program` type is
|
|
8
|
+
* nominally distinct from `estree.Program`, so the compiler cannot verify
|
|
9
|
+
* the conversion structurally — hence the double assertion. This helper is
|
|
10
|
+
* the single sanctioned crossing point between the two type worlds.
|
|
11
|
+
*/
|
|
12
|
+
function toPositionedProgram(program) {
|
|
13
|
+
// eslint-disable-next-line no-restricted-syntax -- the sanctioned acorn→estree bridge this rule's message points at
|
|
14
|
+
return program;
|
|
15
|
+
}
|
|
4
16
|
/**
|
|
5
17
|
* Parse JavaScript source as a **script** (not an ES module).
|
|
6
18
|
* All Mozilla chrome JS files (`browser-main.js`, `browser-init.js`,
|
|
@@ -17,7 +29,7 @@ export function parseScript(content, onComment) {
|
|
|
17
29
|
};
|
|
18
30
|
if (onComment)
|
|
19
31
|
opts.onComment = onComment;
|
|
20
|
-
return acorn.parse(content, opts);
|
|
32
|
+
return toPositionedProgram(acorn.parse(content, opts));
|
|
21
33
|
}
|
|
22
34
|
/**
|
|
23
35
|
* Parse JavaScript source as an **ES module**.
|
|
@@ -35,7 +47,7 @@ export function parseModule(content, onComment) {
|
|
|
35
47
|
};
|
|
36
48
|
if (onComment)
|
|
37
49
|
opts.onComment = onComment;
|
|
38
|
-
return acorn.parse(content, opts);
|
|
50
|
+
return toPositionedProgram(acorn.parse(content, opts));
|
|
39
51
|
}
|
|
40
52
|
/**
|
|
41
53
|
* Convenience cast from `acorn.Node` (or the generic ESTree union returned
|
|
@@ -5,7 +5,7 @@ import { toError } from '../utils/errors.js';
|
|
|
5
5
|
import { verbose } from '../utils/logger.js';
|
|
6
6
|
import { toRootRelativePath } from '../utils/paths.js';
|
|
7
7
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
8
|
-
import { createRollbackJournal, restoreRollbackJournal, snapshotFile } from './furnace-rollback.js';
|
|
8
|
+
import { assertSnapshotted, createRollbackJournal, restoreRollbackJournal, snapshotFile, } from './furnace-rollback.js';
|
|
9
9
|
import { registerBrowserContent } from './manifest-register.js';
|
|
10
10
|
import { DEFAULT_DOM_TARGET } from './wire-dom-fragment.js';
|
|
11
11
|
import { addDestroyToBrowserInit, addDomFragment, addInitToBrowserInit, addSubscriptToBrowserMain, } from './wire-targets.js';
|
|
@@ -82,15 +82,22 @@ export async function wireSubscript(root, name, options = {}) {
|
|
|
82
82
|
}
|
|
83
83
|
try {
|
|
84
84
|
// 1. Add subscript to browser-main.js
|
|
85
|
+
assertSnapshotted(journal, join(engineDir, 'browser/base/content/browser-main.js'), 'browser wire');
|
|
85
86
|
const subscriptAdded = await addSubscriptToBrowserMain(engineDir, name, marker);
|
|
86
87
|
// 2. Add init expression to browser-init.js (if provided)
|
|
87
88
|
let initAdded = false;
|
|
88
89
|
if (options.init) {
|
|
90
|
+
// The snapshot above is taken when init or destroy is `!== undefined`
|
|
91
|
+
// while these writes fire on truthiness — close, but two separate
|
|
92
|
+
// conditions over the same file, which is exactly the pairing that
|
|
93
|
+
// drifts when a third writer is added later.
|
|
94
|
+
assertSnapshotted(journal, join(engineDir, 'browser/base/content/browser-init.js'), 'browser wire init');
|
|
89
95
|
initAdded = await addInitToBrowserInit(engineDir, options.init, options.after, marker);
|
|
90
96
|
}
|
|
91
97
|
// 3. Add destroy expression to browser-init.js onUnload() (if provided)
|
|
92
98
|
let destroyAdded = false;
|
|
93
99
|
if (options.destroy) {
|
|
100
|
+
assertSnapshotted(journal, join(engineDir, 'browser/base/content/browser-init.js'), 'browser wire destroy');
|
|
94
101
|
destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy, marker);
|
|
95
102
|
}
|
|
96
103
|
// 4. Add #include directive to the top-level chrome document (if provided)
|
|
@@ -99,6 +106,7 @@ export async function wireSubscript(root, name, options = {}) {
|
|
|
99
106
|
domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath), options.domTargetPath);
|
|
100
107
|
}
|
|
101
108
|
// 5. Register in jar.mn
|
|
109
|
+
assertSnapshotted(journal, join(engineDir, 'browser/base/jar.mn'), 'browser wire jar.mn');
|
|
102
110
|
const jarMnResult = await registerBrowserContent(engineDir, `${name}.js`, undefined, jarMnSourcePath);
|
|
103
111
|
return {
|
|
104
112
|
subscriptAdded,
|
|
@@ -64,7 +64,7 @@ export interface BuildBaseline {
|
|
|
64
64
|
* earlier scope's edited fixtures while `obj-*`/`_tests/` still holds its
|
|
65
65
|
* stale staging; coverage therefore REPLACES by default.
|
|
66
66
|
*
|
|
67
|
-
* The one exception is `test --build --extend-coverage
|
|
67
|
+
* The one exception is `test --build --extend-coverage`, which
|
|
68
68
|
* unions only after proving the previous record's anchor still holds:
|
|
69
69
|
* same engine HEAD, same {@link BuildBaseline.mozconfigHash}, and every
|
|
70
70
|
* previously fingerprinted path byte-identical — i.e. the wholesale
|
|
@@ -52,7 +52,7 @@ export declare function readBuildBaseline(projectRoot: string): Promise<BuildBas
|
|
|
52
52
|
* static-components anchor whenever the coverage claim is `'full'` or
|
|
53
53
|
* absent. `'refresh'` records it even for a scoped coverage claim whose
|
|
54
54
|
* implementation escalated to a full build. `'carry-forward'` always keeps the previous anchor: needed by
|
|
55
|
-
*
|
|
55
|
+
* `--extend-coverage`, whose union can EVALUATE to `'full'`
|
|
56
56
|
* while the build that produced it was still a scoped `mach build
|
|
57
57
|
* faster` that did not rebake the compiled table.
|
|
58
58
|
*/
|
|
@@ -86,7 +86,7 @@ export async function readBuildBaseline(projectRoot) {
|
|
|
86
86
|
* static-components anchor whenever the coverage claim is `'full'` or
|
|
87
87
|
* absent. `'refresh'` records it even for a scoped coverage claim whose
|
|
88
88
|
* implementation escalated to a full build. `'carry-forward'` always keeps the previous anchor: needed by
|
|
89
|
-
*
|
|
89
|
+
* `--extend-coverage`, whose union can EVALUATE to `'full'`
|
|
90
90
|
* while the build that produced it was still a scoped `mach build
|
|
91
91
|
* faster` that did not rebake the compiled table.
|
|
92
92
|
*/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loud guard against silently reverting UNEXPORTED engine drift.
|
|
3
|
+
*
|
|
4
|
+
* `build-prepare` rewrites engine files from FireForge-owned sources
|
|
5
|
+
* before every build: the branding tree, every Furnace-managed component,
|
|
6
|
+
* and `mozconfig`. On a multi-session checkout that is a silent
|
|
7
|
+
* destructive write. The recorded incident: one session edited a file in
|
|
8
|
+
* `engine/` while another held the engine lock; the lock-holder's build
|
|
9
|
+
* rewrote the file back to its patch baseline; the editing session's later
|
|
10
|
+
* `re-export --wait-lock` then captured a HYBRID file — and every gate
|
|
11
|
+
* passed on the hybrid capture. Only a hand-run grep of the patch body
|
|
12
|
+
* caught it.
|
|
13
|
+
*
|
|
14
|
+
* The `setupBranding` half of this class was closed in `31fdf744` (the
|
|
15
|
+
* branding writer preserves unmanaged lines). The general shape — any
|
|
16
|
+
* build-prepare overwrite destroying local drift that no patch records —
|
|
17
|
+
* stayed silent, and silence is what made the hybrid capture possible.
|
|
18
|
+
*
|
|
19
|
+
* This guard runs BEFORE the writes, classifies the engine's dirty files
|
|
20
|
+
* with the same classifier `fireforge status` uses, and reports the ones
|
|
21
|
+
* whose content is explained by neither a patch body nor the pristine
|
|
22
|
+
* baseline. Advisory by default (a warning naming every file), refusing
|
|
23
|
+
* under an explicit flag.
|
|
24
|
+
*/
|
|
25
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
26
|
+
import { type ClassifiedFile } from './status-classify.js';
|
|
27
|
+
/** One file whose unexported content a build-prepare overwrite would destroy. */
|
|
28
|
+
export interface UnexportedDriftAtRisk {
|
|
29
|
+
file: string;
|
|
30
|
+
classification: ClassifiedFile['classification'];
|
|
31
|
+
owner?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Finds engine files that build-prepare is about to overwrite and whose
|
|
35
|
+
* current content is recorded nowhere.
|
|
36
|
+
*
|
|
37
|
+
* Fail-open: any probe failure is reported verbosely and yields an empty
|
|
38
|
+
* list. A build must not fail because an advisory guard could not run.
|
|
39
|
+
*
|
|
40
|
+
* @param projectRoot - Project root
|
|
41
|
+
* @param config - Loaded FireForge config (reads `binaryName`)
|
|
42
|
+
* @returns At-risk files, sorted by path
|
|
43
|
+
*/
|
|
44
|
+
export declare function findUnexportedDriftAtRisk(projectRoot: string, config: FireForgeConfig): Promise<UnexportedDriftAtRisk[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Renders the operator-facing warning (or refusal body) naming every file
|
|
47
|
+
* whose unexported content the build is about to overwrite.
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatUnexportedDriftWarning(files: readonly UnexportedDriftAtRisk[]): string;
|