@hominis/fireforge 0.46.1 → 0.47.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 +28 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/config.js +1 -2
- package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +59 -0
- package/dist/src/commands/furnace/deploy-fragment-includers.js +120 -0
- package/dist/src/commands/furnace/deploy.js +36 -3
- package/dist/src/commands/furnace/validation-output.d.ts +5 -0
- package/dist/src/commands/furnace/validation-output.js +35 -9
- package/dist/src/commands/patch/delete-gap-notice.d.ts +24 -0
- package/dist/src/commands/patch/delete-gap-notice.js +46 -0
- package/dist/src/commands/patch/delete.js +11 -0
- package/dist/src/commands/re-export-lint.d.ts +26 -1
- package/dist/src/commands/re-export-lint.js +60 -20
- package/dist/src/commands/re-export-scan.d.ts +8 -0
- package/dist/src/commands/re-export-scan.js +12 -2
- package/dist/src/commands/re-export-single.js +24 -5
- package/dist/src/commands/run.js +42 -17
- package/dist/src/commands/test-harness-env.d.ts +24 -0
- package/dist/src/commands/test-harness-env.js +61 -0
- package/dist/src/commands/test-register.js +6 -2
- package/dist/src/commands/test-verdict.d.ts +3 -1
- package/dist/src/commands/test-verdict.js +17 -2
- package/dist/src/commands/test.js +9 -16
- package/dist/src/core/build-jar-escalation.d.ts +1 -4
- package/dist/src/core/build-jar-escalation.js +1 -4
- package/dist/src/core/build-prepare.js +72 -55
- package/dist/src/core/firefox-cache.js +1 -2
- package/dist/src/core/furnace-apply-output.d.ts +10 -0
- package/dist/src/core/furnace-apply-output.js +19 -4
- package/dist/src/core/furnace-css-fragments.d.ts +7 -0
- package/dist/src/core/furnace-css-fragments.js +12 -3
- package/dist/src/core/furnace-fragment-includers.d.ts +56 -0
- package/dist/src/core/furnace-fragment-includers.js +122 -0
- package/dist/src/core/git-diff-new-file.js +1 -2
- package/dist/src/core/git-file-ops.js +1 -2
- package/dist/src/core/git.js +1 -2
- package/dist/src/core/mach-build-artifacts.js +1 -2
- package/dist/src/core/mochitest-server-port.d.ts +43 -4
- package/dist/src/core/mochitest-server-port.js +64 -11
- package/dist/src/core/patch-lint-imports.js +4 -4
- package/dist/src/core/smoke-patterns.d.ts +24 -0
- package/dist/src/core/smoke-patterns.js +50 -5
- package/dist/src/core/token-dark-mode.d.ts +8 -0
- package/dist/src/core/token-dark-mode.js +30 -17
- package/dist/src/types/commands/options.d.ts +6 -0
- package/docs/configuration.md +8 -0
- package/docs/exit-codes.md +19 -19
- package/docs/machine-output.md +22 -10
- package/docs/testing.md +13 -1
- package/package.json +5 -5
|
@@ -20,15 +20,18 @@
|
|
|
20
20
|
* (`refreshQueueCtxEntry`), so later iterations of an `--all` run lint
|
|
21
21
|
* against the just-refreshed body instead of the stale one.
|
|
22
22
|
*/
|
|
23
|
-
import { buildPatchQueueContext, countNonBinaryDiffLines, detectNewFilesInDiff, lintExportedPatch, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
23
|
+
import { buildPatchQueueContext, countNonBinaryDiffLines, detectNewFilesInDiff, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
24
24
|
import { buildPerPatchLintCacheKey, getCachedPerPatchLintIssues, getPerPatchLintCacheHeadSha, loadPerPatchLintCache, savePerPatchLintCache, setCachedPerPatchLintIssues, } from '../core/patch-lint-cache.js';
|
|
25
25
|
import { invalidateNewFileCreatorsCache } from '../core/patch-lint-cross.js';
|
|
26
|
-
import { extractAddedLinesPerFile } from '../core/patch-lint-diff.js';
|
|
27
26
|
import { resolvePatchOwnedSysMjs } from '../core/patch-lint-ownership.js';
|
|
27
|
+
import { computeProjectedLintRegressions } from '../core/patch-lint-projection.js';
|
|
28
|
+
import { GeneralError } from '../errors/base.js';
|
|
28
29
|
import { pathExists } from '../utils/fs.js';
|
|
29
|
-
import { info } from '../utils/logger.js';
|
|
30
|
+
import { info, warn } from '../utils/logger.js';
|
|
30
31
|
import { reportPatchLintOutcome } from './export-shared.js';
|
|
31
32
|
import { buildPerRunCheckJs } from './lint-per-run-checkjs.js';
|
|
33
|
+
import { projectEntryBody } from './patch/entry-projection.js';
|
|
34
|
+
import { formatForwardImportRemedy } from './re-export-scan.js';
|
|
32
35
|
/** Builds the once-per-invocation lint context. */
|
|
33
36
|
export async function buildReExportLintContext(projectRoot, paths, config, noCache) {
|
|
34
37
|
const patchQueueCtx = (await pathExists(paths.patches))
|
|
@@ -50,6 +53,7 @@ export async function buildReExportLintContext(projectRoot, paths, config, noCac
|
|
|
50
53
|
cacheDirty: false,
|
|
51
54
|
reusedCacheEntries: 0,
|
|
52
55
|
globalsEmitted: false,
|
|
56
|
+
queueBaselineWarned: false,
|
|
53
57
|
};
|
|
54
58
|
}
|
|
55
59
|
/** The checkJs-relevant files this fresh diff creates. */
|
|
@@ -179,10 +183,61 @@ export async function storeReExportLintResult(lintCtx, patchFilename, projectedM
|
|
|
179
183
|
});
|
|
180
184
|
lintCtx.cacheDirty = true;
|
|
181
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Projects the rewritten body into the hoisted queue context and refuses
|
|
188
|
+
* when the projection introduces error-severity cross-patch findings the
|
|
189
|
+
* current queue does not have (a forward import an earlier owner gained,
|
|
190
|
+
* say). Mirrors `re-export --files` (its projected-state conflict) and the
|
|
191
|
+
* `--scan` adoption gate, which only covered files a scan adopted: a plain
|
|
192
|
+
* re-export of an unchanged file set with new CONTENT used to pass, and the
|
|
193
|
+
* queue carried the error until the next `export` placement refused it.
|
|
194
|
+
*
|
|
195
|
+
* Runs in dry-run. `--force-unsafe` downgrades the refusal to warnings;
|
|
196
|
+
* `--skip-lint` does not bypass it, same as `--files`. The baseline is the
|
|
197
|
+
* hoisted context, which {@link refreshQueueCtxEntry} keeps equal to the
|
|
198
|
+
* on-disk queue across an `--all` loop, so patch N+1 is projected against
|
|
199
|
+
* the queue as it will exist.
|
|
200
|
+
*/
|
|
201
|
+
export function assertRefreshedBodyHasNoNewQueueErrors(args) {
|
|
202
|
+
const { lintCtx, patchFilename, diffContent, forceUnsafe } = args;
|
|
203
|
+
const ctx = lintCtx.patchQueueCtx;
|
|
204
|
+
if (!ctx)
|
|
205
|
+
return;
|
|
206
|
+
const projection = projectEntryBody(diffContent);
|
|
207
|
+
const projectedEntries = ctx.entries.map((entry) => entry.filename === patchFilename ? { ...entry, ...projection } : entry);
|
|
208
|
+
const isError = (issue) => issue.severity === 'error';
|
|
209
|
+
const baseline = lintPatchQueue(ctx).filter(isError);
|
|
210
|
+
const projected = lintPatchQueue({
|
|
211
|
+
entries: projectedEntries,
|
|
212
|
+
...(ctx.patchPolicy ? { patchPolicy: ctx.patchPolicy } : {}),
|
|
213
|
+
}).filter(isError);
|
|
214
|
+
const regressions = computeProjectedLintRegressions(baseline, projected);
|
|
215
|
+
if (regressions.length === 0) {
|
|
216
|
+
if (baseline.length > 0 && !lintCtx.queueBaselineWarned) {
|
|
217
|
+
lintCtx.queueBaselineWarned = true;
|
|
218
|
+
warn(`Note: projected queue still has ${baseline.length} pre-existing ` +
|
|
219
|
+
'cross-patch error(s) unrelated to this re-export. Run "fireforge verify" to list them.');
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const details = regressions.map((issue) => ` ${formatPatchLintIssue(issue)}`).join('\n');
|
|
224
|
+
const hasForwardImport = regressions.some((issue) => issue.check === 'forward-import');
|
|
225
|
+
const message = `Refusing to re-export ${patchFilename} because the refreshed body introduces ` +
|
|
226
|
+
`${regressions.length} new cross-patch lint error${regressions.length === 1 ? '' : 's'}:\n${details}` +
|
|
227
|
+
(hasForwardImport ? `\n${formatForwardImportRemedy('re-running the re-export')}` : '');
|
|
228
|
+
if (forceUnsafe) {
|
|
229
|
+
warn(message);
|
|
230
|
+
warn('Proceeding because --force-unsafe was provided.');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
throw new GeneralError(message);
|
|
234
|
+
}
|
|
182
235
|
/**
|
|
183
236
|
* Refreshes the in-memory queue entry for a just-rewritten patch so later
|
|
184
237
|
* iterations (and later cache keys) lint against the new body instead of
|
|
185
|
-
* the stale one. The on-disk write is the authority. This mirrors it
|
|
238
|
+
* the stale one. The on-disk write is the authority. This mirrors it, via
|
|
239
|
+
* the same projection `buildPatchQueueContext` derives an entry from, so
|
|
240
|
+
* `createdFiles` and `modifiedFileAdditions` stay in step with a rebuild.
|
|
186
241
|
*/
|
|
187
242
|
export function refreshQueueCtxEntry(lintCtx, patchFilename, diffContent) {
|
|
188
243
|
const ctx = lintCtx.patchQueueCtx;
|
|
@@ -191,22 +246,7 @@ export function refreshQueueCtxEntry(lintCtx, patchFilename, diffContent) {
|
|
|
191
246
|
const entry = ctx.entries.find((e) => e.filename === patchFilename);
|
|
192
247
|
if (!entry)
|
|
193
248
|
return;
|
|
194
|
-
entry
|
|
195
|
-
const newFilePaths = detectNewFilesInDiff(diffContent);
|
|
196
|
-
const addedLinesByFile = extractAddedLinesPerFile(diffContent);
|
|
197
|
-
const newFiles = new Map();
|
|
198
|
-
const modifiedFileAdditions = new Map();
|
|
199
|
-
for (const [file, lines] of addedLinesByFile) {
|
|
200
|
-
if (newFilePaths.has(file)) {
|
|
201
|
-
// A created file's content is its added lines.
|
|
202
|
-
newFiles.set(file, lines.join('\n'));
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
modifiedFileAdditions.set(file, lines.join('\n'));
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
entry.newFiles = newFiles;
|
|
209
|
-
entry.modifiedFileAdditions = modifiedFileAdditions;
|
|
249
|
+
Object.assign(entry, projectEntryBody(diffContent));
|
|
210
250
|
invalidateNewFileCreatorsCache(ctx);
|
|
211
251
|
}
|
|
212
252
|
/** Persists cache writes and reports reuse, once after the loop. */
|
|
@@ -45,6 +45,14 @@ export declare function assertScanAdoptionsHaveNoForwardImports(args: {
|
|
|
45
45
|
patchFilename: string;
|
|
46
46
|
added: readonly string[];
|
|
47
47
|
}): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* The one remedy sentence for a forward import a patch body cannot carry.
|
|
50
|
+
* Shared by the `--scan` adoption gate and the plain re-export projection
|
|
51
|
+
* gate so both refusals point at the same commands.
|
|
52
|
+
*
|
|
53
|
+
* @param reRunHint - What to do after the fix ("re-running the scan")
|
|
54
|
+
*/
|
|
55
|
+
export declare function formatForwardImportRemedy(reRunHint: string): string;
|
|
48
56
|
/** Refuses explicit `--scan-file` additions that did not produce patch hunks. */
|
|
49
57
|
export declare function assertScanFileAdditionsHaveDiffHunks(args: {
|
|
50
58
|
diffContent: string;
|
|
@@ -201,9 +201,19 @@ export async function assertScanAdoptionsHaveNoForwardImports(args) {
|
|
|
201
201
|
const details = offending.map((issue) => ` - ${issue.file}: ${issue.message}`).join('\n');
|
|
202
202
|
throw new GeneralError(`Refusing to adopt ${offending.length} scanned file${offending.length === 1 ? '' : 's'} into ${patchFilename} ` +
|
|
203
203
|
`because they import modules created by later patches:\n${details}\n` +
|
|
204
|
-
'
|
|
204
|
+
formatForwardImportRemedy('re-running the scan'));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The one remedy sentence for a forward import a patch body cannot carry.
|
|
208
|
+
* Shared by the `--scan` adoption gate and the plain re-export projection
|
|
209
|
+
* gate so both refusals point at the same commands.
|
|
210
|
+
*
|
|
211
|
+
* @param reRunHint - What to do after the fix ("re-running the scan")
|
|
212
|
+
*/
|
|
213
|
+
export function formatForwardImportRemedy(reRunHint) {
|
|
214
|
+
return ('Export those files as their own later patch ("fireforge export --order <n>" / ' +
|
|
205
215
|
'"fireforge patch split --order <n>") or declare the intentional dependency with ' +
|
|
206
|
-
|
|
216
|
+
`"fireforge patch staged-dependency --add" before ${reRunHint}.`);
|
|
207
217
|
}
|
|
208
218
|
/** Refuses explicit `--scan-file` additions that did not produce patch hunks. */
|
|
209
219
|
export function assertScanFileAdditionsHaveDiffHunks(args) {
|
|
@@ -16,7 +16,7 @@ import { info, success, warn } from '../utils/logger.js';
|
|
|
16
16
|
import { sleep } from '../utils/sleep.js';
|
|
17
17
|
import { findMissingFiles, reportAdjacentUnmanagedFiles } from './re-export-adjacent.js';
|
|
18
18
|
import { reportForeignDrift } from './re-export-drift.js';
|
|
19
|
-
import { lintReExportedPatch, refreshQueueCtxEntry, storeReExportLintResult, } from './re-export-lint.js';
|
|
19
|
+
import { assertRefreshedBodyHasNoNewQueueErrors, lintReExportedPatch, refreshQueueCtxEntry, storeReExportLintResult, } from './re-export-lint.js';
|
|
20
20
|
import { assertScanAdoptionsHaveNoForwardImports, assertScanFileAdditionsHaveDiffHunks, confirmBroadScanAdditions, scanPatchFilesForReExport, } from './re-export-scan.js';
|
|
21
21
|
const GIT_INDEX_LOCK_RETRY_MS = 300;
|
|
22
22
|
/**
|
|
@@ -161,10 +161,16 @@ async function reExportSinglePatch(args) {
|
|
|
161
161
|
return false;
|
|
162
162
|
}
|
|
163
163
|
const { updates, ignoreChecks, effectiveTier, existingIgnoreSet, flagIgnoreSet } = computeReExportUpdates(patch, options, currentFilesAffected);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
enforceProjectedManifestPolicy({ config, manifest, patch, updates, options });
|
|
165
|
+
// Cross-patch projection gate (runs in dry-run too): the refreshed body
|
|
166
|
+
// must not introduce queue-level errors the current queue lacks, e.g. an
|
|
167
|
+
// earlier owner gaining an import of a later patch's module. Cheap
|
|
168
|
+
// (in-memory) and more actionable than the per-patch lint, so it fires
|
|
169
|
+
// first.
|
|
170
|
+
assertRefreshedBodyHasNoNewQueueErrors({
|
|
171
|
+
lintCtx,
|
|
172
|
+
patchFilename: patch.filename,
|
|
173
|
+
diffContent,
|
|
168
174
|
forceUnsafe: options.forceUnsafe === true,
|
|
169
175
|
});
|
|
170
176
|
// Lint against the once-per-invocation hoisted context (queue context +
|
|
@@ -190,6 +196,9 @@ async function reExportSinglePatch(args) {
|
|
|
190
196
|
if (addedIgnores.length > 0) {
|
|
191
197
|
info(`[dry-run] ${patch.filename}: lintIgnore would gain ${addedIgnores.join(', ')}`);
|
|
192
198
|
}
|
|
199
|
+
// Memory-only: later `--all --dry-run` iterations project against this
|
|
200
|
+
// patch's would-be body, exactly as the real run does after its write.
|
|
201
|
+
refreshQueueCtxEntry(lintCtx, patch.filename, diffContent);
|
|
193
202
|
}
|
|
194
203
|
else {
|
|
195
204
|
await commitRefreshedPatch({
|
|
@@ -208,6 +217,16 @@ async function reExportSinglePatch(args) {
|
|
|
208
217
|
}
|
|
209
218
|
return true;
|
|
210
219
|
}
|
|
220
|
+
/** Manifest-level policy gate over the projected row (ranges, gaps, collisions). */
|
|
221
|
+
function enforceProjectedManifestPolicy(args) {
|
|
222
|
+
const { config, manifest, patch, updates, options } = args;
|
|
223
|
+
enforcePatchPolicy({
|
|
224
|
+
config,
|
|
225
|
+
manifest: buildProjectedManifest(manifest, manifest.patches.map((entry) => entry.filename === patch.filename ? { ...entry, ...updates } : entry)),
|
|
226
|
+
command: 're-export',
|
|
227
|
+
forceUnsafe: options.forceUnsafe === true,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
211
230
|
/**
|
|
212
231
|
* Writes the refreshed body + manifest row atomically under the patch
|
|
213
232
|
* directory lock (a split write would let a concurrent queue mutation
|
package/dist/src/commands/run.js
CHANGED
|
@@ -8,7 +8,7 @@ import { assertEngineExists } from '../core/engine-precondition.js';
|
|
|
8
8
|
import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
|
|
9
9
|
import { hasBuildArtifacts, hasRunnableBundle, run, runMachSmoke } from '../core/mach.js';
|
|
10
10
|
import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
|
|
11
|
-
import { compileAllowlistFromFile, compileAllowlistFromStrings, matchAllowlist, matchesSmokeError, } from '../core/smoke-patterns.js';
|
|
11
|
+
import { compileAllowlistFromFile, compileAllowlistFromStrings, findOverCapAllowlistEntries, matchAllowlist, matchesSmokeError, } from '../core/smoke-patterns.js';
|
|
12
12
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
13
13
|
import { BuildError } from '../errors/build.js';
|
|
14
14
|
import { ExitCode } from '../errors/codes.js';
|
|
@@ -192,17 +192,7 @@ async function runSmokeExit(engineDir, options) {
|
|
|
192
192
|
}
|
|
193
193
|
findings.push({ stream, line });
|
|
194
194
|
};
|
|
195
|
-
|
|
196
|
-
// interacting with the window mid-run can trigger console errors (a
|
|
197
|
-
// password-manager import scan probing an unreadable profile dir, say)
|
|
198
|
-
// that fail the smoke run looking like a product regression. CI hosts (CI
|
|
199
|
-
// env var set) are assumed display-free and unattended, so the notice
|
|
200
|
-
// stays quiet there.
|
|
201
|
-
if (!options.headless && !process.env['CI']) {
|
|
202
|
-
warn('Headed smoke window: keyboard/mouse input during the window will contaminate the ' +
|
|
203
|
-
'console capture and can fail the run. Pass --headless (or run on an unattended host) ' +
|
|
204
|
-
'for reliable smoke checks.');
|
|
205
|
-
}
|
|
195
|
+
warnIfHeadedOnDesktop(options.headless === true);
|
|
206
196
|
info(`Launching browser (smoke-exit after ${smokeExit}s)...\n`);
|
|
207
197
|
const startedAt = Date.now();
|
|
208
198
|
let result;
|
|
@@ -222,6 +212,7 @@ async function runSmokeExit(engineDir, options) {
|
|
|
222
212
|
captureStream?.end();
|
|
223
213
|
}
|
|
224
214
|
const elapsedMs = Date.now() - startedAt;
|
|
215
|
+
const overCap = findOverCapAllowlistEntries(allowlist, allowlistHits);
|
|
225
216
|
reportSmokeSummary({
|
|
226
217
|
smokeTimeoutMs,
|
|
227
218
|
elapsedMs,
|
|
@@ -230,12 +221,14 @@ async function runSmokeExit(engineDir, options) {
|
|
|
230
221
|
allowlistedTotalHits,
|
|
231
222
|
allowlist,
|
|
232
223
|
allowlistHits,
|
|
224
|
+
overCap,
|
|
233
225
|
findings,
|
|
234
226
|
exitCode: result.exitCode,
|
|
235
227
|
});
|
|
236
|
-
// Exit contract (precedence: unallowed errors
|
|
237
|
-
|
|
238
|
-
|
|
228
|
+
// Exit contract (precedence: unallowed errors and over-ceiling floods
|
|
229
|
+
// dominate timed-out).
|
|
230
|
+
if (findings.length > 0 || overCap.length > 0) {
|
|
231
|
+
throw new SmokeRunError(describeSmokeExitFailure(findings.length, overCap.length), ExitCode.SMOKE_EXIT_FAILURE);
|
|
239
232
|
}
|
|
240
233
|
if (result.timedOut) {
|
|
241
234
|
// Clean window: SIGTERM came from us. Treat as success.
|
|
@@ -273,6 +266,30 @@ async function buildAllowlist(options) {
|
|
|
273
266
|
}
|
|
274
267
|
return allow;
|
|
275
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* A headed smoke window on a developer desktop absorbs live input: a human
|
|
271
|
+
* interacting with the window mid-run can trigger console errors (a
|
|
272
|
+
* password-manager import scan probing an unreadable profile dir, say)
|
|
273
|
+
* that fail the smoke run looking like a product regression. CI hosts (CI
|
|
274
|
+
* env var set) are assumed display-free and unattended, so the notice
|
|
275
|
+
* stays quiet there.
|
|
276
|
+
*/
|
|
277
|
+
function warnIfHeadedOnDesktop(headless) {
|
|
278
|
+
if (headless || process.env['CI'])
|
|
279
|
+
return;
|
|
280
|
+
warn('Headed smoke window: keyboard/mouse input during the window will contaminate the ' +
|
|
281
|
+
'console capture and can fail the run. Pass --headless (or run on an unattended host) ' +
|
|
282
|
+
'for reliable smoke checks.');
|
|
283
|
+
}
|
|
284
|
+
/** The SMOKE_EXIT_FAILURE message, naming whichever of the two causes fired. */
|
|
285
|
+
function describeSmokeExitFailure(unallowed, overCap) {
|
|
286
|
+
const parts = [];
|
|
287
|
+
if (unallowed > 0)
|
|
288
|
+
parts.push(`${unallowed} unallowed console error(s)`);
|
|
289
|
+
if (overCap > 0)
|
|
290
|
+
parts.push(`${overCap} allowlist entr${overCap === 1 ? 'y' : 'ies'} over its max-hits ceiling`);
|
|
291
|
+
return `Smoke run observed ${parts.join(' and ')}.`;
|
|
292
|
+
}
|
|
276
293
|
/**
|
|
277
294
|
* Prints the human-readable summary block that follows every smoke run.
|
|
278
295
|
* Called once, right before the exit-code decision. Keeps the reporting
|
|
@@ -302,9 +319,17 @@ function reportSmokeSummary(args) {
|
|
|
302
319
|
args.allowlist.forEach((entry, index) => {
|
|
303
320
|
const hits = args.allowlistHits[index] ?? 0;
|
|
304
321
|
const zeroSuffix = hits === 0 ? ' (never matched — candidate for removal)' : '';
|
|
305
|
-
|
|
322
|
+
const cap = entry.maxHits === undefined ? '' : `/${entry.maxHits}`;
|
|
323
|
+
info(` ${hits}×${cap} ${entry.origin} ${entry.source}${zeroSuffix}`);
|
|
306
324
|
});
|
|
307
325
|
}
|
|
326
|
+
// An allowlisted shape firing past its `# max-hits` ceiling is a flood the
|
|
327
|
+
// allowlist was never meant to absorb. It is reported next to the
|
|
328
|
+
// attribution it came from and counts toward the exit code below.
|
|
329
|
+
for (const over of args.overCap) {
|
|
330
|
+
warn(` allowlisted shape "${over.entry.source}" (${over.entry.origin}) fired ${over.hits}× ` +
|
|
331
|
+
`over its ceiling of ${over.maxHits}`);
|
|
332
|
+
}
|
|
308
333
|
if (args.findings.length === 0)
|
|
309
334
|
return;
|
|
310
335
|
warn('');
|
|
@@ -330,7 +355,7 @@ export function registerRun(program, { getProjectRoot, withErrorHandling }) {
|
|
|
330
355
|
return parsed;
|
|
331
356
|
}))
|
|
332
357
|
.option('--console-allow <regex>', 'Allowlist regex (repeatable). Lines that match any entry do not count toward the smoke exit code.', ...stringListOption())
|
|
333
|
-
.option('--console-allow-file <path>', 'Newline-delimited allowlist regex file. Blank lines and # comments are ignored.')
|
|
358
|
+
.option('--console-allow-file <path>', 'Newline-delimited allowlist regex file. Blank lines and # comments are ignored. A "# max-hits: N" comment directly above an entry caps how many lines it may match in one run; more than that fails the run with exit 12.')
|
|
334
359
|
.option('--capture-console <file>', 'Mirror captured console output to <file> for post-exit inspection.')
|
|
335
360
|
.option('--headless', 'Launch the browser with --headless. Recommended for --smoke-exit on a shared desktop: input into a headed smoke window contaminates the console capture.')
|
|
336
361
|
.action(withErrorHandling(
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TestSuite } from './test-run.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the perf-sample env contract for the harness run:
|
|
4
|
+
* `--perf-samples <path>` exports `<BINARYNAME>_PERF_SAMPLE_JSON` naming the
|
|
5
|
+
* artifact file a budget checker consumes after the run.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildPerfSampleEnv(projectRoot: string, binaryName: string, perfSamples: string | undefined): Record<string, string> | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Merges the per-feature harness env contracts into one map, or `undefined`
|
|
10
|
+
* when no feature exported anything (so `TestRunContext.env` stays absent).
|
|
11
|
+
*/
|
|
12
|
+
export declare function mergeHarnessEnv(...parts: (Record<string, string> | undefined)[]): Record<string, string> | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the `--shuffle` seed: a fresh one when the flag was bare, the
|
|
15
|
+
* operator's when it carried one. Mach's `--shuffle` is an UNSEEDED
|
|
16
|
+
* Fisher–Yates over `Math.random` (SimpleTest/setup.js), so a red found by
|
|
17
|
+
* shuffling could not be reproduced. FireForge draws the seed, exports it
|
|
18
|
+
* as `FIREFORGE_SHUFFLE_SEED` for harness code that reorders inside a file,
|
|
19
|
+
* prints it, and stamps it on the verdict line.
|
|
20
|
+
*
|
|
21
|
+
* Mochitest-only: the xpcshell harness has no shuffle, and generic `mach
|
|
22
|
+
* test` dispatch would forward the flag to every suite it fans out to.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveShuffleSeed(shuffle: number | boolean | undefined, suite: TestSuite): number | undefined;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Per-feature environment and argument contracts `fireforge test` exports to
|
|
4
|
+
* the harness process: the perf-sample artifact path and the seeded
|
|
5
|
+
* test-order shuffle. Split out of `test.ts` to keep the command body inside
|
|
6
|
+
* its file budget.
|
|
7
|
+
*/
|
|
8
|
+
import { randomInt } from 'node:crypto';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { GeneralError } from '../errors/base.js';
|
|
11
|
+
import { info } from '../utils/logger.js';
|
|
12
|
+
import { setVerdictRunAttribute } from './test-verdict.js';
|
|
13
|
+
/**
|
|
14
|
+
* Builds the perf-sample env contract for the harness run:
|
|
15
|
+
* `--perf-samples <path>` exports `<BINARYNAME>_PERF_SAMPLE_JSON` naming the
|
|
16
|
+
* artifact file a budget checker consumes after the run.
|
|
17
|
+
*/
|
|
18
|
+
export function buildPerfSampleEnv(projectRoot, binaryName, perfSamples) {
|
|
19
|
+
if (!perfSamples)
|
|
20
|
+
return undefined;
|
|
21
|
+
const envName = `${binaryName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_PERF_SAMPLE_JSON`;
|
|
22
|
+
const artifactPath = resolve(projectRoot, perfSamples);
|
|
23
|
+
info(`Perf sample contract: ${envName}=${artifactPath}`);
|
|
24
|
+
return { [envName]: artifactPath };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Merges the per-feature harness env contracts into one map, or `undefined`
|
|
28
|
+
* when no feature exported anything (so `TestRunContext.env` stays absent).
|
|
29
|
+
*/
|
|
30
|
+
export function mergeHarnessEnv(...parts) {
|
|
31
|
+
const merged = {};
|
|
32
|
+
for (const part of parts) {
|
|
33
|
+
if (part)
|
|
34
|
+
Object.assign(merged, part);
|
|
35
|
+
}
|
|
36
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the `--shuffle` seed: a fresh one when the flag was bare, the
|
|
40
|
+
* operator's when it carried one. Mach's `--shuffle` is an UNSEEDED
|
|
41
|
+
* Fisher–Yates over `Math.random` (SimpleTest/setup.js), so a red found by
|
|
42
|
+
* shuffling could not be reproduced. FireForge draws the seed, exports it
|
|
43
|
+
* as `FIREFORGE_SHUFFLE_SEED` for harness code that reorders inside a file,
|
|
44
|
+
* prints it, and stamps it on the verdict line.
|
|
45
|
+
*
|
|
46
|
+
* Mochitest-only: the xpcshell harness has no shuffle, and generic `mach
|
|
47
|
+
* test` dispatch would forward the flag to every suite it fans out to.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveShuffleSeed(shuffle, suite) {
|
|
50
|
+
if (shuffle === undefined || shuffle === false)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (suite !== 'mochitest') {
|
|
53
|
+
throw new GeneralError("--shuffle forwards the mochitest harness's file-order shuffle; the xpcshell harness has " +
|
|
54
|
+
'none, and generic `mach test` dispatch would hand it to every suite. Narrow the paths to ' +
|
|
55
|
+
'browser-chrome/mochitest files (and drop --generic-mach-test).');
|
|
56
|
+
}
|
|
57
|
+
const seed = shuffle === true ? randomInt(1, 2 ** 31 - 1) : shuffle;
|
|
58
|
+
setVerdictRunAttribute('shuffle', String(seed));
|
|
59
|
+
info(`Test order shuffle: seed=${seed} (reproduce with "fireforge test --shuffle=${seed}")`);
|
|
60
|
+
return seed;
|
|
61
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
4
4
|
import { GeneralError, LockContentionError } from '../errors/base.js';
|
|
5
5
|
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, stringListOption, } from '../utils/options.js';
|
|
6
|
+
import { parsePositiveIntegerFlag } from '../utils/validation.js';
|
|
6
7
|
import { testCommand } from './test.js';
|
|
7
8
|
import { DEFAULT_HARNESS_RETRIES } from './test-run.js';
|
|
8
9
|
import { emitFailVerdict, resetVerdictEmission, verdictEmitted } from './test-verdict.js';
|
|
@@ -29,7 +30,9 @@ const TEST_HELP_TEXT = [
|
|
|
29
30
|
'--doctor) ends with exactly one raw stdout line',
|
|
30
31
|
' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|test-failures|',
|
|
31
32
|
' no-tests|preflight|inconclusive|lock-timeout|killed] [checks=<n>]',
|
|
32
|
-
' [unexpected=<n>] [shards=<p>/<t>] [(<note>)] [
|
|
33
|
+
' [unexpected=<n>] [shards=<p>/<t>] [(<note>)] [shuffle=<seed>]',
|
|
34
|
+
' [log=<path>]',
|
|
35
|
+
'shuffle=<seed> is the seed a --shuffle run exported to the harness.',
|
|
33
36
|
"log=<path> names this run's complete output, written to",
|
|
34
37
|
'.fireforge/logs/ as the run streams, so a piped or truncated run',
|
|
35
38
|
'still leaves a re-readable artifact.',
|
|
@@ -68,11 +71,12 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
68
71
|
.option('--auto', 'Forward mach test --auto. Valid only when no explicit paths are provided.')
|
|
69
72
|
.option('--allow-stale-build', 'Allow tests to run even when packageable engine files changed since the last successful FireForge build')
|
|
70
73
|
.option('--allow-stale-components', 'Run tests despite components.conf changes that only a full "fireforge build" compiles in (the packaged child process will resolve the OLD StaticComponents table)')
|
|
71
|
-
.option('--kill-stale-marionette',
|
|
74
|
+
.option('--kill-stale-marionette', "Terminate a recognized stale browser from this objdir, one holding the Marionette port, or this checkout's own leftover mochitest httpd before running tests. Never touches another checkout's live harness.")
|
|
72
75
|
.option('--reap-orphans', 'Terminate harness helper processes (httpd, pywebsocket, ssltunnel, moz-http2) from this objdir that survived an earlier run. The census itself runs on every test dispatch and is report-only; this flag opts into the kill.')
|
|
73
76
|
.option('--canary [path]', 'Run one short browser-chrome harness canary. Uses test.canaryPath from fireforge.json when no path is supplied.')
|
|
74
77
|
.option('--doctor', 'Run a marionette handshake preflight before tests (exit 1 on FAIL). With no paths, runs the preflight only.')
|
|
75
78
|
.option('--mach-arg <arg>', 'Forward this argument verbatim to `mach test` (repeatable). Escape valve for upstream xpcshell/mochitest flags FireForge does not model.', ...stringListOption())
|
|
79
|
+
.option('--shuffle [seed]', 'Run the mochitest files in a seeded random order: forwards mach --shuffle and exports FIREFORGE_SHUFFLE_SEED=<seed> to the harness (a fresh seed when omitted). The seed rides the FIREFORGE-VERDICT line as shuffle=<seed>; replay a red with --shuffle=<seed>. Mochitest-only. Write it as --shuffle=<seed> or after the paths, since a bare "--shuffle <path>" reads the path as the seed.', commanderArgParser((raw) => parsePositiveIntegerFlag('--shuffle', raw)))
|
|
76
80
|
.option('--harness-retries <n>', `Retry budget for recognized harness crashes (resource-monitor tracebacks, pre-test hangs, post-green shutdown re-entry). 0 disables retries. Default: ${DEFAULT_HARNESS_RETRIES}.`, commanderArgParser((raw) => {
|
|
77
81
|
const n = Number.parseInt(raw, 10);
|
|
78
82
|
if (!Number.isFinite(n) || n < 0 || n > 10) {
|
|
@@ -12,6 +12,8 @@ import type { HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
|
12
12
|
* of the others.
|
|
13
13
|
*/
|
|
14
14
|
export type FireforgeVerdictReason = 'crash' | 'no-tests' | 'test-failures' | 'preflight' | 'inconclusive' | 'lock-timeout' | 'killed';
|
|
15
|
+
/** Records an additive attribute for this run's verdict line. */
|
|
16
|
+
export declare function setVerdictRunAttribute(key: 'shuffle', value: string): void;
|
|
15
17
|
/**
|
|
16
18
|
* Re-arms the verdict sink for a new run. Called at `testCommand` entry so
|
|
17
19
|
* repeated programmatic invocations (and tests) each get their own
|
|
@@ -34,7 +36,7 @@ export declare function emitPassVerdict(): void;
|
|
|
34
36
|
* Emits a FAIL verdict carrying an emission-layer reason code, and
|
|
35
37
|
* optionally the refusal class that produced it.
|
|
36
38
|
*
|
|
37
|
-
* `note=` is additive, like the `log=`, `signal=` and `
|
|
39
|
+
* `note=` is additive, like the `log=`, `signal=`, `shards=` and `shuffle=` keys already
|
|
38
40
|
* on this line: `reason=` is unchanged and still comes from the closed set
|
|
39
41
|
* in `docs/machine-output.md`, so a consumer that tokenises `key=value`
|
|
40
42
|
* pairs is unaffected. It exists because `reason=preflight` covers every
|
|
@@ -3,6 +3,19 @@ import { getActiveRunLogPath } from '../core/run-log.js';
|
|
|
3
3
|
import { formatFireforgeVerdictLine } from '../core/test-harness-crash.js';
|
|
4
4
|
import { setStdoutSealed } from '../utils/logger.js';
|
|
5
5
|
let emitted = false;
|
|
6
|
+
/**
|
|
7
|
+
* Additive `key=value` attributes of the run in flight, appended to
|
|
8
|
+
* whichever verdict line ends it. `shuffle` is the seed a `--shuffle` run
|
|
9
|
+
* exported to the harness, so a red found by shuffling carries its own
|
|
10
|
+
* reproduction on the one line an unattended reader keeps. Cleared by
|
|
11
|
+
* {@link resetVerdictEmission}, so a later run in the same process cannot
|
|
12
|
+
* inherit it.
|
|
13
|
+
*/
|
|
14
|
+
const runAttributes = new Map();
|
|
15
|
+
/** Records an additive attribute for this run's verdict line. */
|
|
16
|
+
export function setVerdictRunAttribute(key, value) {
|
|
17
|
+
runAttributes.set(key, value);
|
|
18
|
+
}
|
|
6
19
|
/**
|
|
7
20
|
* True once a test run has begun in this process.
|
|
8
21
|
*
|
|
@@ -20,6 +33,7 @@ let armed = false;
|
|
|
20
33
|
export function resetVerdictEmission() {
|
|
21
34
|
emitted = false;
|
|
22
35
|
armed = true;
|
|
36
|
+
runAttributes.clear();
|
|
23
37
|
setStdoutSealed(false);
|
|
24
38
|
}
|
|
25
39
|
/** True once this run has written its `FIREFORGE-VERDICT:` line. */
|
|
@@ -45,9 +59,10 @@ function writeVerdictLine(line) {
|
|
|
45
59
|
// break that contract, and one before it is the first thing a `tail` cuts.
|
|
46
60
|
// Appended as an additive `key=value`, so consumers that tokenize the line
|
|
47
61
|
// are unaffected and a truncated tail still says where the full output is.
|
|
62
|
+
const attributes = [...runAttributes].map(([key, value]) => ` ${key}=${value}`).join('');
|
|
48
63
|
const logPath = getActiveRunLogPath();
|
|
49
64
|
const suffix = logPath === undefined ? '' : ` log=${logPath}`;
|
|
50
|
-
process.stdout.write(`${line}${suffix}\n`);
|
|
65
|
+
process.stdout.write(`${line}${attributes}${suffix}\n`);
|
|
51
66
|
setStdoutSealed(true);
|
|
52
67
|
}
|
|
53
68
|
/**
|
|
@@ -65,7 +80,7 @@ export function emitPassVerdict() {
|
|
|
65
80
|
* Emits a FAIL verdict carrying an emission-layer reason code, and
|
|
66
81
|
* optionally the refusal class that produced it.
|
|
67
82
|
*
|
|
68
|
-
* `note=` is additive, like the `log=`, `signal=` and `
|
|
83
|
+
* `note=` is additive, like the `log=`, `signal=`, `shards=` and `shuffle=` keys already
|
|
69
84
|
* on this line: `reason=` is unchanged and still comes from the closed set
|
|
70
85
|
* in `docs/machine-output.md`, so a consumer that tokenises `key=value`
|
|
71
86
|
* pairs is unaffected. It exists because `reason=preflight` covers every
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from 'node:path';
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
4
|
import { assertEngineExists } from '../core/engine-precondition.js';
|
|
5
5
|
import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
|
|
@@ -21,6 +21,7 @@ import { info, intro, notice, outro, verbose } from '../utils/logger.js';
|
|
|
21
21
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
22
22
|
import { runTestBuildPhase } from './test-build-phase.js';
|
|
23
23
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
24
|
+
import { buildPerfSampleEnv, mergeHarnessEnv, resolveShuffleSeed } from './test-harness-env.js';
|
|
24
25
|
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
25
26
|
import { DEFAULT_HARNESS_RETRIES, finalizeShardedOutcome, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
26
27
|
import { emitFailVerdict, emitPassVerdict, resetVerdictEmission, verdictEmitted, } from './test-verdict.js';
|
|
@@ -241,6 +242,7 @@ async function ensureTestBrowserEnvironment(engineDir, launchablePath, xpcshellO
|
|
|
241
242
|
// does not use the httpd, so an xpcshell-only run is never blocked by it.
|
|
242
243
|
if (!xpcshellOnly) {
|
|
243
244
|
await ensureMochitestServerPortAvailable(undefined, {
|
|
245
|
+
engineDir,
|
|
244
246
|
killStaleServer: options.killStaleMarionette === true,
|
|
245
247
|
});
|
|
246
248
|
}
|
|
@@ -418,6 +420,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
418
420
|
}
|
|
419
421
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
420
422
|
const suite = resolveTestSuite(classification, options.genericMachTest === true);
|
|
423
|
+
const shuffleSeed = resolveShuffleSeed(options.shuffle, suite);
|
|
421
424
|
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
422
425
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
423
426
|
: [];
|
|
@@ -425,6 +428,9 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
425
428
|
if (options.headless) {
|
|
426
429
|
extraArgs.push('--headless');
|
|
427
430
|
}
|
|
431
|
+
if (shuffleSeed !== undefined) {
|
|
432
|
+
extraArgs.push('--shuffle');
|
|
433
|
+
}
|
|
428
434
|
if (options.auto === true) {
|
|
429
435
|
extraArgs.push('--auto');
|
|
430
436
|
}
|
|
@@ -465,7 +471,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
465
471
|
// the manifest for each file individually. See src/core/xpcshell-appdir.ts
|
|
466
472
|
// for the full motivation.
|
|
467
473
|
logTestSelection(scopes);
|
|
468
|
-
const
|
|
474
|
+
const harnessEnv = mergeHarnessEnv(buildPerfSampleEnv(projectRoot, projectConfig.binaryName, options.perfSamples), shuffleSeed === undefined ? undefined : { FIREFORGE_SHUFFLE_SEED: String(shuffleSeed) });
|
|
469
475
|
const runCtx = {
|
|
470
476
|
engineDir: paths.engine,
|
|
471
477
|
objDir: buildCheck.objDir,
|
|
@@ -475,7 +481,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
475
481
|
harnessRetries,
|
|
476
482
|
headless: options.headless === true,
|
|
477
483
|
...(options.fullOutput === true ? { fullOutput: true } : {}),
|
|
478
|
-
...(
|
|
484
|
+
...(harnessEnv ? { env: harnessEnv } : {}),
|
|
479
485
|
};
|
|
480
486
|
const postRebuildContext = options.build
|
|
481
487
|
? createPostRebuildFailureContext('fireforge test --build', normalizedPaths)
|
|
@@ -527,16 +533,3 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
527
533
|
}
|
|
528
534
|
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext, options.headless === true);
|
|
529
535
|
}
|
|
530
|
-
/**
|
|
531
|
-
* Builds the perf-sample env contract for the harness run:
|
|
532
|
-
* `--perf-samples <path>` exports `<BINARYNAME>_PERF_SAMPLE_JSON` naming the
|
|
533
|
-
* artifact file a budget checker consumes after the run.
|
|
534
|
-
*/
|
|
535
|
-
function buildPerfSampleEnv(projectRoot, binaryName, perfSamples) {
|
|
536
|
-
if (!perfSamples)
|
|
537
|
-
return undefined;
|
|
538
|
-
const envName = `${binaryName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_PERF_SAMPLE_JSON`;
|
|
539
|
-
const artifactPath = resolve(projectRoot, perfSamples);
|
|
540
|
-
info(`Perf sample contract: ${envName}=${artifactPath}`);
|
|
541
|
-
return { [envName]: artifactPath };
|
|
542
|
-
}
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
* parse a manifest escalates: this narrows a conservative rule, so its
|
|
29
29
|
* error direction stays the slow-but-correct one.
|
|
30
30
|
*/
|
|
31
|
-
import type { BuildBaseline } from './build-baseline-types.js';
|
|
32
31
|
/** Why one changed `jar.mn` still forces a full build. */
|
|
33
32
|
export interface JarEscalationCause {
|
|
34
33
|
/** Engine-relative manifest path. */
|
|
@@ -51,11 +50,9 @@ export interface JarEscalationDecision {
|
|
|
51
50
|
* @param engineDir - Absolute engine directory
|
|
52
51
|
* @param changedPaths - Engine-relative paths changed since the last
|
|
53
52
|
* successful build (non-`jar.mn` entries are ignored)
|
|
54
|
-
* @param baseline - Last successful build's baseline (reserved: the
|
|
55
|
-
* fingerprint map already gated which paths reach here)
|
|
56
53
|
* @returns The escalation decision with per-manifest causes
|
|
57
54
|
*/
|
|
58
|
-
export declare function evaluateJarManifestEscalation(engineDir: string, changedPaths: readonly string[]
|
|
55
|
+
export declare function evaluateJarManifestEscalation(engineDir: string, changedPaths: readonly string[]): Promise<JarEscalationDecision>;
|
|
59
56
|
/**
|
|
60
57
|
* Renders the operator-facing escalation notice. Names the manifest and the
|
|
61
58
|
* reason: "a jar.mn changed" is the sentence that made the previous rule
|
|
@@ -68,12 +68,9 @@ async function isNewJarManifest(engineDir, path) {
|
|
|
68
68
|
* @param engineDir - Absolute engine directory
|
|
69
69
|
* @param changedPaths - Engine-relative paths changed since the last
|
|
70
70
|
* successful build (non-`jar.mn` entries are ignored)
|
|
71
|
-
* @param baseline - Last successful build's baseline (reserved: the
|
|
72
|
-
* fingerprint map already gated which paths reach here)
|
|
73
71
|
* @returns The escalation decision with per-manifest causes
|
|
74
72
|
*/
|
|
75
|
-
export async function evaluateJarManifestEscalation(engineDir, changedPaths
|
|
76
|
-
void baseline;
|
|
73
|
+
export async function evaluateJarManifestEscalation(engineDir, changedPaths) {
|
|
77
74
|
const causes = [];
|
|
78
75
|
const cleared = [];
|
|
79
76
|
for (const path of changedPaths.filter(isJarManifestPath)) {
|