@hominis/fireforge 0.38.0 → 0.40.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 +41 -0
- package/README.md +25 -1
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/build.js +8 -3
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/export-flow.d.ts +1 -1
- package/dist/src/commands/export-flow.js +21 -10
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.js +31 -5
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +97 -18
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +23 -19
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- package/dist/src/commands/patch/move-files-into.d.ts +47 -0
- package/dist/src/commands/patch/move-files-into.js +245 -0
- package/dist/src/commands/patch/move-files.js +27 -13
- package/dist/src/commands/patch/rename.js +5 -2
- package/dist/src/commands/patch/staged-dependency.js +102 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +106 -0
- package/dist/src/commands/re-export-options.js +3 -0
- package/dist/src/commands/re-export.js +42 -53
- package/dist/src/commands/reset.js +5 -0
- package/dist/src/commands/run.js +18 -2
- package/dist/src/commands/status-check.d.ts +35 -0
- package/dist/src/commands/status-check.js +88 -0
- package/dist/src/commands/status-output.d.ts +9 -0
- package/dist/src/commands/status-output.js +33 -1
- package/dist/src/commands/status.js +33 -8
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +9 -3
- package/dist/src/commands/test-modes.d.ts +21 -0
- package/dist/src/commands/test-modes.js +42 -0
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +26 -5
- package/dist/src/commands/test.js +38 -32
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +16 -0
- package/dist/src/commands/tree.js +174 -0
- package/dist/src/core/build-baseline-types.d.ts +7 -0
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +2 -1
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- package/dist/src/core/config-validate.js +25 -0
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- package/dist/src/core/license-headers.d.ts +12 -0
- package/dist/src/core/license-headers.js +27 -0
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +17 -5
- package/dist/src/core/patch-export.d.ts +12 -3
- package/dist/src/core/patch-export.js +13 -3
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -2
- package/dist/src/core/patch-lint-checkjs.js +85 -16
- package/dist/src/core/patch-lint-cross.d.ts +9 -1
- package/dist/src/core/patch-lint-cross.js +69 -9
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint.d.ts +23 -0
- package/dist/src/core/patch-lint.js +34 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/smoke-patterns.d.ts +24 -8
- package/dist/src/core/smoke-patterns.js +23 -12
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +20 -6
- package/dist/src/core/test-harness-crash.d.ts +10 -0
- package/dist/src/core/test-harness-crash.js +16 -0
- package/dist/src/core/test-stale-check.d.ts +15 -0
- package/dist/src/core/test-stale-check.js +37 -1
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +82 -193
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +124 -0
- package/dist/src/core/tree-store.d.ts +53 -0
- package/dist/src/core/tree-store.js +214 -0
- package/dist/src/core/typecheck-shim.d.ts +27 -0
- package/dist/src/core/typecheck-shim.js +58 -4
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +45 -2
- package/dist/src/types/config.d.ts +34 -0
- package/dist/src/utils/validation.d.ts +10 -0
- package/dist/src/utils/validation.js +29 -0
- package/package.json +6 -6
|
@@ -77,6 +77,9 @@ export function validateReExportOptionCombinations(patches, options) {
|
|
|
77
77
|
throw new InvalidArgumentError('--scan-files selects patches from its manifest and cannot be combined with positional patches or --all.', '--scan-files');
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
if (options.refuseAdjacentUnmanaged === true && (options.scan || options.files !== undefined)) {
|
|
81
|
+
throw new InvalidArgumentError('--refuse-adjacent-unmanaged applies to the scan-less path only and cannot be combined with --scan or --files (those set filesAffected explicitly).', '--refuse-adjacent-unmanaged');
|
|
82
|
+
}
|
|
80
83
|
const usingTierFlag = options.tier !== undefined;
|
|
81
84
|
const usingLintIgnoreFlag = options.lintIgnore !== undefined && options.lintIgnore.length > 0;
|
|
82
85
|
if (options.all && (usingTierFlag || usingLintIgnoreFlag)) {
|
|
@@ -1,81 +1,52 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/* eslint-disable max-lines -- Re-export is at the guardrail; splitting register plumbing is unrelated to this fix. */
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
3
|
import { multiselect } from '@clack/prompts';
|
|
5
4
|
import { Command, Option } from 'commander';
|
|
6
5
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
7
6
|
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
7
|
+
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
8
8
|
import { enforceFreshFurnaceSources } from '../core/furnace-stale-export.js';
|
|
9
9
|
import { isGitRepository } from '../core/git.js';
|
|
10
10
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
11
|
-
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
12
11
|
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
13
12
|
import { buildPatchQueueContext } from '../core/patch-lint.js';
|
|
14
|
-
import {
|
|
13
|
+
import { loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
|
|
15
14
|
import { buildProjectedManifest, enforcePatchPolicy } from '../core/patch-policy.js';
|
|
16
15
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
16
|
+
import { isGitIndexLockConflict } from '../errors/git.js';
|
|
17
17
|
import { elapsedSince } from '../utils/elapsed.js';
|
|
18
18
|
import { toError } from '../utils/errors.js';
|
|
19
19
|
import { pathExists } from '../utils/fs.js';
|
|
20
20
|
import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '../utils/logger.js';
|
|
21
21
|
import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../utils/options.js';
|
|
22
22
|
import { runPatchLint } from './export-shared.js';
|
|
23
|
+
import { findMissingFiles, reportAdjacentUnmanagedFiles } from './re-export-adjacent.js';
|
|
23
24
|
import { loadScanFilesAssignments, withDryRunReExportLock } from './re-export-bulk-scan.js';
|
|
24
25
|
import { reExportFilesInPlace } from './re-export-files.js';
|
|
25
26
|
import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations, } from './re-export-options.js';
|
|
26
27
|
import { assertScanAdoptionsHaveNoForwardImports, assertScanFileAdditionsHaveDiffHunks, confirmBroadScanAdditions, normalizeScanFiles, scanPatchFilesForReExport, } from './re-export-scan.js';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const GIT_INDEX_LOCK_RETRY_MS = 300;
|
|
29
|
+
/**
|
|
30
|
+
* Retries a single patch re-export exactly once when the failure is
|
|
31
|
+
* transient git `index.lock` contention (an external git process holding
|
|
32
|
+
* the engine repo's index). The retry re-enters `reExportSinglePatch`
|
|
33
|
+
* from the top, so it re-reads clean state; a second lock failure
|
|
34
|
+
* propagates to the loop's honest "N of M" accounting.
|
|
35
|
+
*/
|
|
36
|
+
async function reExportSinglePatchWithIndexLockRetry(patch, paths, manifest, options, isDryRun, config, adjacentCtx) {
|
|
37
|
+
try {
|
|
38
|
+
return await reExportSinglePatch(patch, paths, manifest, options, isDryRun, config, adjacentCtx);
|
|
32
39
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const { currentFilesAffected, engineDir, manifest, patchFilename } = args;
|
|
37
|
-
const parentDirs = [...new Set(currentFilesAffected.map((file) => dirname(file)))];
|
|
38
|
-
const currentSet = new Set(currentFilesAffected);
|
|
39
|
-
const claimedByOthers = getClaimedFiles(manifest, patchFilename);
|
|
40
|
-
const candidates = new Set();
|
|
41
|
-
for (const dir of parentDirs) {
|
|
42
|
-
const [modifiedFiles, untrackedFiles] = await Promise.all([
|
|
43
|
-
getModifiedFilesInDir(engineDir, dir),
|
|
44
|
-
getUntrackedFilesInDir(engineDir, dir),
|
|
45
|
-
]);
|
|
46
|
-
for (const file of [...modifiedFiles, ...untrackedFiles]) {
|
|
47
|
-
if (currentSet.has(file) || claimedByOthers.has(file))
|
|
48
|
-
continue;
|
|
49
|
-
candidates.add(file);
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (!isGitIndexLockConflict(error)) {
|
|
42
|
+
throw error;
|
|
50
43
|
}
|
|
44
|
+
warn(`${patch.filename}: git index.lock contention detected — retrying once after ${String(GIT_INDEX_LOCK_RETRY_MS)} ms...`);
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, GIT_INDEX_LOCK_RETRY_MS));
|
|
46
|
+
return await reExportSinglePatch(patch, paths, manifest, options, isDryRun, config, adjacentCtx);
|
|
51
47
|
}
|
|
52
|
-
return [...candidates].sort();
|
|
53
48
|
}
|
|
54
|
-
async function
|
|
55
|
-
const { patch, paths, manifest, currentFilesAffected } = args;
|
|
56
|
-
const missingFiles = await findMissingFiles(paths.engine, currentFilesAffected);
|
|
57
|
-
if (missingFiles.length > 0) {
|
|
58
|
-
warn(`${patch.filename}: some files in patches.json no longer exist on disk ` +
|
|
59
|
-
`(${missingFiles.join(', ')}). Without --scan, re-export keeps the manifest's ` +
|
|
60
|
-
`filesAffected unchanged and the missing entries will be preserved — ` +
|
|
61
|
-
`\`fireforge verify\` may flag manifest inconsistency after this run.\n` +
|
|
62
|
-
` Re-run with --scan to reconcile filesAffected with the current worktree, ` +
|
|
63
|
-
`or pass --files <paths> to set the list explicitly.`);
|
|
64
|
-
}
|
|
65
|
-
const likelyNewFiles = await findLikelyNewSiblingFiles({
|
|
66
|
-
currentFilesAffected,
|
|
67
|
-
engineDir: paths.engine,
|
|
68
|
-
manifest,
|
|
69
|
-
patchFilename: patch.filename,
|
|
70
|
-
});
|
|
71
|
-
if (likelyNewFiles.length === 0)
|
|
72
|
-
return;
|
|
73
|
-
warn(`${patch.filename}: found ${likelyNewFiles.length} unowned changed sibling file${likelyNewFiles.length === 1 ? '' : 's'} near this patch. Plain re-export keeps filesAffected unchanged; add reviewed files explicitly with --scan-file.`);
|
|
74
|
-
for (const file of likelyNewFiles) {
|
|
75
|
-
info(` ${file} — fireforge re-export ${patch.filename} --scan --scan-file ${file}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, config) {
|
|
49
|
+
async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, config, adjacentCtx) {
|
|
79
50
|
let currentFilesAffected = [...patch.filesAffected];
|
|
80
51
|
// --- Scan for new/removed files ---
|
|
81
52
|
if (options.scan) {
|
|
@@ -123,7 +94,9 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
123
94
|
// warning up-front when we can detect the drift cheaply, so the
|
|
124
95
|
// operator has a chance to re-run with `--scan` or `--files`
|
|
125
96
|
// before the stale filesAffected lands in patches.json.
|
|
126
|
-
|
|
97
|
+
const args = { patch, paths, manifest, currentFilesAffected, ctx: adjacentCtx };
|
|
98
|
+
if (await reportAdjacentUnmanagedFiles(args))
|
|
99
|
+
return false;
|
|
127
100
|
}
|
|
128
101
|
// --- Explicit file-subset path ---
|
|
129
102
|
// When --files is given, the target filesAffected is authoritative — drop
|
|
@@ -359,6 +332,14 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
359
332
|
return;
|
|
360
333
|
}
|
|
361
334
|
const config = await loadConfig(projectRoot);
|
|
335
|
+
// Classification inputs for the scan-less adjacency advisory, computed
|
|
336
|
+
// once for the whole run (FORGE G2).
|
|
337
|
+
const adjacentCtx = {
|
|
338
|
+
binaryName: config.binaryName,
|
|
339
|
+
furnacePrefixes: await collectFurnaceManagedPrefixes(paths.root),
|
|
340
|
+
refuseAdjacentUnmanaged: options.refuseAdjacentUnmanaged === true,
|
|
341
|
+
refusals: [],
|
|
342
|
+
};
|
|
362
343
|
let reExported = 0;
|
|
363
344
|
const reExportedFilenames = [];
|
|
364
345
|
const progress = spinner('Preparing re-export...');
|
|
@@ -369,7 +350,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
369
350
|
const patchOptions = assignedScanFiles !== undefined ? { ...options, scanFiles: assignedScanFiles } : options;
|
|
370
351
|
progress.message(`Re-exporting ${index + 1}/${selectedPatches.length}: ${patch.filename} (${patch.filesAffected.length} file(s), ${elapsedSince(startedAt)} elapsed)...`);
|
|
371
352
|
try {
|
|
372
|
-
const exported = await
|
|
353
|
+
const exported = await reExportSinglePatchWithIndexLockRetry(patch, paths, manifest, patchOptions, isDryRun, config, adjacentCtx);
|
|
373
354
|
if (exported) {
|
|
374
355
|
reExported++;
|
|
375
356
|
reExportedFilenames.push(patch.filename);
|
|
@@ -381,6 +362,13 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
381
362
|
}
|
|
382
363
|
}
|
|
383
364
|
});
|
|
365
|
+
if (adjacentCtx.refusals.length > 0) {
|
|
366
|
+
progress.error('Re-export refused');
|
|
367
|
+
const names = adjacentCtx.refusals.map((r) => r.patchFilename).join(', ');
|
|
368
|
+
throw new GeneralError(`Refused ${String(adjacentCtx.refusals.length)} patch(es) with adjacent unmanaged files ` +
|
|
369
|
+
`(--refuse-adjacent-unmanaged): ${names}. Adopt reviewed files with --scan --scan-file, ` +
|
|
370
|
+
`or export them as a new patch.`);
|
|
371
|
+
}
|
|
384
372
|
if (reExported === 0 && selectedPatches.length > 0) {
|
|
385
373
|
progress.error('Re-export failed');
|
|
386
374
|
throw new GeneralError('All selected patches failed to re-export. Check the errors above.');
|
|
@@ -431,6 +419,7 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
431
419
|
.split(',')
|
|
432
420
|
.map((v) => v.trim())
|
|
433
421
|
.filter((v) => v.length > 0))
|
|
422
|
+
.option('--refuse-adjacent-unmanaged', 'Refuse a scan-less re-export (non-zero exit, patch not written) when unmanaged files exist adjacent to the patch ownership, instead of warning. Mutually exclusive with --scan and --files.')
|
|
434
423
|
.option('--dry-run', 'Show what would change without writing')
|
|
435
424
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
436
425
|
.option('--allow-stale-furnace', 'Export the deployed engine copy even when the components/ source changed since the last furnace apply')
|
|
@@ -5,6 +5,7 @@ import { getProjectPaths } from '../core/config.js';
|
|
|
5
5
|
import { clearAppliedFurnaceState } from '../core/furnace-config.js';
|
|
6
6
|
import { getHead, hasChanges, isGitRepository, isMissingHeadError, resetChanges, } from '../core/git.js';
|
|
7
7
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
8
|
+
import { warnIfStaticComponentsStale } from '../core/test-stale-check.js';
|
|
8
9
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
9
10
|
import { pathExists } from '../utils/fs.js';
|
|
10
11
|
import { cancel, info, intro, isCancel, outro, spinner, warn } from '../utils/logger.js';
|
|
@@ -86,6 +87,10 @@ export async function resetCommand(projectRoot, options) {
|
|
|
86
87
|
// tree, not the engine checkout, so reset must not silently forget them.
|
|
87
88
|
await clearAppliedFurnaceState(projectRoot);
|
|
88
89
|
s.stop('Changes reset');
|
|
90
|
+
// The reset may have moved components.conf away from what the last full
|
|
91
|
+
// build compiled in — surface that now instead of at the next test
|
|
92
|
+
// refusal (FORGE F13).
|
|
93
|
+
await warnIfStaticComponentsStale(projectRoot, paths.engine);
|
|
89
94
|
outro('Working tree restored to clean state');
|
|
90
95
|
}
|
|
91
96
|
catch (error) {
|
package/dist/src/commands/run.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from 'commander';
|
|
|
6
6
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
7
7
|
import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
|
|
8
8
|
import { buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, run, runMachSmoke, } from '../core/mach.js';
|
|
9
|
-
import { compileAllowlistFromFile, compileAllowlistFromStrings,
|
|
9
|
+
import { compileAllowlistFromFile, compileAllowlistFromStrings, matchAllowlist, matchesSmokeError, } from '../core/smoke-patterns.js';
|
|
10
10
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
11
11
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
12
12
|
import { ExitCode } from '../errors/codes.js';
|
|
@@ -186,6 +186,7 @@ async function runSmokeExit(engineDir, options) {
|
|
|
186
186
|
const findings = [];
|
|
187
187
|
let allowlistedErrorHits = 0;
|
|
188
188
|
let allowlistedTotalHits = 0;
|
|
189
|
+
const allowlistHits = new Array(allowlist.length).fill(0);
|
|
189
190
|
const handleLine = (stream, line) => {
|
|
190
191
|
// Mirror raw output to the terminal so operators watching the smoke
|
|
191
192
|
// run still see what the browser is printing. Stream selection on the
|
|
@@ -201,9 +202,11 @@ async function runSmokeExit(engineDir, options) {
|
|
|
201
202
|
// total set of allowlisted lines (what the operator sees in the
|
|
202
203
|
// console) and the subset that were error-class (what the smoke
|
|
203
204
|
// exit contract cares about). The exit contract itself is unchanged.
|
|
204
|
-
const
|
|
205
|
+
const matchIndex = allowlist.length > 0 ? matchAllowlist(line, allowlist) : -1;
|
|
206
|
+
const isAllowlisted = matchIndex !== -1;
|
|
205
207
|
if (isAllowlisted) {
|
|
206
208
|
allowlistedTotalHits += 1;
|
|
209
|
+
allowlistHits[matchIndex] = (allowlistHits[matchIndex] ?? 0) + 1;
|
|
207
210
|
}
|
|
208
211
|
if (!matchesSmokeError(line))
|
|
209
212
|
return;
|
|
@@ -249,6 +252,8 @@ async function runSmokeExit(engineDir, options) {
|
|
|
249
252
|
timedOut: result.timedOut,
|
|
250
253
|
allowlistedErrorHits,
|
|
251
254
|
allowlistedTotalHits,
|
|
255
|
+
allowlist,
|
|
256
|
+
allowlistHits,
|
|
252
257
|
findings,
|
|
253
258
|
exitCode: result.exitCode,
|
|
254
259
|
});
|
|
@@ -314,6 +319,17 @@ function reportSmokeSummary(args) {
|
|
|
314
319
|
info(` Allowlisted error hits (suppressed): ${String(args.allowlistedErrorHits)}`);
|
|
315
320
|
info(` Allowlisted lines total: ${String(args.allowlistedTotalHits)}`);
|
|
316
321
|
info(` Child exit code: ${String(args.exitCode)}`);
|
|
322
|
+
// Per-entry attribution (FORGE G8): first-match credit per line, with
|
|
323
|
+
// zero-hit entries always visible — an allowlist entry whose suppressed
|
|
324
|
+
// shape changed upstream is only detectable as a 0× row.
|
|
325
|
+
if (args.allowlist.length > 0) {
|
|
326
|
+
info(' Allowlist attribution (first matching entry per line):');
|
|
327
|
+
args.allowlist.forEach((entry, index) => {
|
|
328
|
+
const hits = args.allowlistHits[index] ?? 0;
|
|
329
|
+
const zeroSuffix = hits === 0 ? ' (never matched — candidate for removal)' : '';
|
|
330
|
+
info(` ${String(hits)}× ${entry.origin} ${entry.source}${zeroSuffix}`);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
317
333
|
if (args.findings.length === 0)
|
|
318
334
|
return;
|
|
319
335
|
warn('');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `status --check` enforcement policy (FORGE G1). Turns the classified
|
|
3
|
+
* status view into a CI-enforceable non-zero exit: the default policy
|
|
4
|
+
* fails on any classification outside {patch-backed, branding, furnace},
|
|
5
|
+
* and `--fail-on <class,...>` replaces that set for finer control.
|
|
6
|
+
*
|
|
7
|
+
* Helper module consumed by status.ts (which sits at the max-lines
|
|
8
|
+
* budget); no top-level registrar is exported and none is wanted.
|
|
9
|
+
*/
|
|
10
|
+
import type { ClassifiedFile, FileClassification } from '../core/status-classify.js';
|
|
11
|
+
/** Resolved `--check`/`--fail-on` policy for one status invocation. */
|
|
12
|
+
export interface StatusCheckPolicy {
|
|
13
|
+
checkEnabled: boolean;
|
|
14
|
+
failOn: readonly FileClassification[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the enforcement policy from the raw flags: `--fail-on` implies
|
|
18
|
+
* `--check` and replaces the default set; both are refused alongside the
|
|
19
|
+
* modes that skip (or elide) classification.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveStatusCheckPolicy(options: {
|
|
22
|
+
check?: boolean;
|
|
23
|
+
failOn?: string;
|
|
24
|
+
raw?: boolean;
|
|
25
|
+
unmanaged?: boolean;
|
|
26
|
+
ownership?: boolean;
|
|
27
|
+
testCoverage?: boolean;
|
|
28
|
+
}): StatusCheckPolicy;
|
|
29
|
+
/**
|
|
30
|
+
* Throws a GeneralError (exit 1) when the policy is enabled and any of
|
|
31
|
+
* its classifications is non-empty, naming each offending classification
|
|
32
|
+
* with its first few files. A disabled policy or a fully tool-covered
|
|
33
|
+
* tree passes silently.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runStatusCheck(classified: readonly ClassifiedFile[], policy: StatusCheckPolicy): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
2
|
+
const ALL_CLASSIFICATIONS = [
|
|
3
|
+
'patch-backed',
|
|
4
|
+
'patch-owned-drift',
|
|
5
|
+
'unmanaged',
|
|
6
|
+
'branding',
|
|
7
|
+
'furnace',
|
|
8
|
+
'conflict',
|
|
9
|
+
];
|
|
10
|
+
/** Default `--check` policy: everything outside {patch-backed, branding, furnace} fails. */
|
|
11
|
+
const DEFAULT_FAIL_CLASSIFICATIONS = [
|
|
12
|
+
'unmanaged',
|
|
13
|
+
'patch-owned-drift',
|
|
14
|
+
'conflict',
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the enforcement policy from the raw flags: `--fail-on` implies
|
|
18
|
+
* `--check` and replaces the default set; both are refused alongside the
|
|
19
|
+
* modes that skip (or elide) classification.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveStatusCheckPolicy(options) {
|
|
22
|
+
const failOnList = parseFailOnClassifications(options.failOn);
|
|
23
|
+
const checkEnabled = options.check === true || failOnList !== undefined;
|
|
24
|
+
if (checkEnabled &&
|
|
25
|
+
(options.raw === true ||
|
|
26
|
+
options.unmanaged === true ||
|
|
27
|
+
options.ownership === true ||
|
|
28
|
+
options.testCoverage === true)) {
|
|
29
|
+
throw new InvalidArgumentError('--check cannot be combined with --raw, --unmanaged, --ownership, or --test-coverage. Use the default view or --json.', '--check');
|
|
30
|
+
}
|
|
31
|
+
return { checkEnabled, failOn: failOnList ?? DEFAULT_FAIL_CLASSIFICATIONS };
|
|
32
|
+
}
|
|
33
|
+
function isClassification(value) {
|
|
34
|
+
return ALL_CLASSIFICATIONS.includes(value);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parses a comma-separated `--fail-on` value into a classification list.
|
|
38
|
+
* Returns undefined when the flag was not given (the caller then applies
|
|
39
|
+
* the default policy only under `--check`).
|
|
40
|
+
*/
|
|
41
|
+
function parseFailOnClassifications(raw) {
|
|
42
|
+
if (raw === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
const parts = raw
|
|
45
|
+
.split(',')
|
|
46
|
+
.map((part) => part.trim())
|
|
47
|
+
.filter((part) => part.length > 0);
|
|
48
|
+
if (parts.length === 0) {
|
|
49
|
+
throw new InvalidArgumentError(`--fail-on requires at least one classification. Valid: ${ALL_CLASSIFICATIONS.join(', ')}.`, '--fail-on');
|
|
50
|
+
}
|
|
51
|
+
const classifications = [];
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
if (!isClassification(part)) {
|
|
54
|
+
throw new InvalidArgumentError(`Unknown --fail-on classification "${part}". Valid: ${ALL_CLASSIFICATIONS.join(', ')}.`, '--fail-on');
|
|
55
|
+
}
|
|
56
|
+
if (!classifications.includes(part))
|
|
57
|
+
classifications.push(part);
|
|
58
|
+
}
|
|
59
|
+
return classifications;
|
|
60
|
+
}
|
|
61
|
+
const CHECK_FILE_PREVIEW_MAX = 3;
|
|
62
|
+
/**
|
|
63
|
+
* Throws a GeneralError (exit 1) when the policy is enabled and any of
|
|
64
|
+
* its classifications is non-empty, naming each offending classification
|
|
65
|
+
* with its first few files. A disabled policy or a fully tool-covered
|
|
66
|
+
* tree passes silently.
|
|
67
|
+
*/
|
|
68
|
+
export function runStatusCheck(classified, policy) {
|
|
69
|
+
if (!policy.checkEnabled)
|
|
70
|
+
return;
|
|
71
|
+
const offending = [];
|
|
72
|
+
for (const classification of policy.failOn) {
|
|
73
|
+
const files = classified
|
|
74
|
+
.filter((entry) => entry.classification === classification)
|
|
75
|
+
.map((entry) => entry.file);
|
|
76
|
+
if (files.length === 0)
|
|
77
|
+
continue;
|
|
78
|
+
const preview = files.slice(0, CHECK_FILE_PREVIEW_MAX).join(', ');
|
|
79
|
+
const more = files.length > CHECK_FILE_PREVIEW_MAX
|
|
80
|
+
? `, +${String(files.length - CHECK_FILE_PREVIEW_MAX)} more`
|
|
81
|
+
: '';
|
|
82
|
+
offending.push(`${String(files.length)} ${classification} (${preview}${more})`);
|
|
83
|
+
}
|
|
84
|
+
if (offending.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
throw new GeneralError(`status --check failed: ${offending.join(', ')}. Export or adopt unmanaged files, ` +
|
|
87
|
+
`re-export drifted patches, and run "fireforge verify" for conflict details.`);
|
|
88
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BuildBaseline } from '../core/build-baseline-types.js';
|
|
1
2
|
import type { ClassifiedFile } from '../core/status-classify.js';
|
|
2
3
|
export interface ClassifiedBuckets {
|
|
3
4
|
conflict: ClassifiedFile[];
|
|
@@ -11,3 +12,11 @@ export interface ClassifiedBuckets {
|
|
|
11
12
|
export declare function renderUnmanagedOnly(unmanagedFiles: ClassifiedFile[], totalModified: number, projectRoot: string, binaryName: string): Promise<void>;
|
|
12
13
|
/** Renders the default classified status buckets. */
|
|
13
14
|
export declare function renderDefaultStatus(totalModified: number, buckets: ClassifiedBuckets, projectRoot: string, binaryName: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Renders `fireforge status --test-coverage` (FORGE F11): a READ-ONLY view
|
|
17
|
+
* of the last build baseline's test-packaging coverage. Before this
|
|
18
|
+
* existed, the only way to learn the recorded coverage scope — which
|
|
19
|
+
* concurrent sessions sharing one engine tree overwrite constantly — was
|
|
20
|
+
* to trip the out-of-coverage refusal on a real test run.
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderTestCoverageStatus(baseline: BuildBaseline | undefined): void;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// SPDX-License-Identifier: EUPL-1.2
|
|
2
1
|
import { isFileRegistered, matchesRegistrablePattern } from '../core/manifest-rules.js';
|
|
3
2
|
import { getPrimaryStatusCode } from '../core/status-classify.js';
|
|
4
3
|
import { GeneralError } from '../errors/base.js';
|
|
@@ -182,3 +181,36 @@ export async function renderDefaultStatus(totalModified, buckets, projectRoot, b
|
|
|
182
181
|
.map((section) => `${section.files.length} ${section.label}`);
|
|
183
182
|
outro(parts.join(', '));
|
|
184
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Renders `fireforge status --test-coverage` (FORGE F11): a READ-ONLY view
|
|
186
|
+
* of the last build baseline's test-packaging coverage. Before this
|
|
187
|
+
* existed, the only way to learn the recorded coverage scope — which
|
|
188
|
+
* concurrent sessions sharing one engine tree overwrite constantly — was
|
|
189
|
+
* to trip the out-of-coverage refusal on a real test run.
|
|
190
|
+
*/
|
|
191
|
+
export function renderTestCoverageStatus(baseline) {
|
|
192
|
+
if (baseline === undefined) {
|
|
193
|
+
info('No build baseline recorded (.fireforge/last-build.json missing or unreadable).\n' +
|
|
194
|
+
'Run "fireforge build" to record one.');
|
|
195
|
+
outro('No test-packaging coverage recorded');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
info(`Last build: ${baseline.builtAt} (binary: ${baseline.binaryName}, ` +
|
|
199
|
+
`engine HEAD: ${baseline.engineHeadSha || '(unborn)'})`);
|
|
200
|
+
info(`Recorded by: ${baseline.recordedBy ?? 'unknown'}`);
|
|
201
|
+
const coverage = baseline.testPackagingCoverage;
|
|
202
|
+
if (coverage === undefined) {
|
|
203
|
+
info('Test packaging coverage: full (implicit — baseline predates coverage recording)');
|
|
204
|
+
outro('Coverage: full');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (coverage === 'full') {
|
|
208
|
+
info('Test packaging coverage: full');
|
|
209
|
+
outro('Coverage: full');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const list = coverage.map((path) => ` - ${path}`).join('\n');
|
|
213
|
+
info(`Test packaging coverage: scoped to ${String(coverage.length)} path(s):\n${list}\n` +
|
|
214
|
+
'A "fireforge test" over paths outside this list will be refused as uncovered.');
|
|
215
|
+
outro(`Coverage: scoped (${String(coverage.length)} paths)`);
|
|
216
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import { readBuildBaseline } from '../core/build-baseline.js';
|
|
3
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
5
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
5
6
|
import { getHead, getStatusWithCodes, isGitRepository, isMissingHeadError } from '../core/git.js';
|
|
@@ -12,7 +13,8 @@ import { CommandError, GeneralError } from '../errors/base.js';
|
|
|
12
13
|
import { ExitCode } from '../errors/codes.js';
|
|
13
14
|
import { FIREFORGE_TMP_PATH_PATTERN, pathExists } from '../utils/fs.js';
|
|
14
15
|
import { info, intro, isMachineOutputMode, outro, setMachineOutputMode, warn, } from '../utils/logger.js';
|
|
15
|
-
import {
|
|
16
|
+
import { resolveStatusCheckPolicy, runStatusCheck } from './status-check.js';
|
|
17
|
+
import { renderDefaultStatus, renderTestCoverageStatus, renderUnmanagedOnly, } from './status-output.js';
|
|
16
18
|
/**
|
|
17
19
|
* Renders raw worktree status as machine-parseable porcelain-style output.
|
|
18
20
|
* Each line is: STATUS<tab>FILE
|
|
@@ -85,14 +87,17 @@ function filterFireForgeTempFiles(files) {
|
|
|
85
87
|
/**
|
|
86
88
|
* Renders classified file status as machine-readable JSON to stdout.
|
|
87
89
|
*/
|
|
88
|
-
async function
|
|
90
|
+
async function classifyStatusFiles(files, paths, projectRoot, binaryName) {
|
|
89
91
|
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
90
|
-
|
|
92
|
+
return classifyFiles(files, paths.engine, paths.patches, binaryName, furnacePrefixes);
|
|
93
|
+
}
|
|
94
|
+
function renderJsonStatus(classified) {
|
|
91
95
|
const outputFiles = classified.map((f) => {
|
|
92
96
|
const entry = {
|
|
93
97
|
file: f.file,
|
|
94
98
|
status: f.status.trim(),
|
|
95
99
|
classification: f.classification,
|
|
100
|
+
patch: f.owner ?? null,
|
|
96
101
|
};
|
|
97
102
|
if (f.classification === 'conflict' && f.claimedBy && f.claimedBy.length > 0) {
|
|
98
103
|
entry.claimedBy = [...f.claimedBy];
|
|
@@ -170,13 +175,28 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
170
175
|
setMachineOutputMode(true);
|
|
171
176
|
}
|
|
172
177
|
try {
|
|
173
|
-
const modeCount = [
|
|
178
|
+
const modeCount = [
|
|
179
|
+
options.raw,
|
|
180
|
+
options.unmanaged,
|
|
181
|
+
options.ownership,
|
|
182
|
+
options.testCoverage,
|
|
183
|
+
options.json,
|
|
184
|
+
].filter((v) => v === true).length;
|
|
174
185
|
if (modeCount > 1) {
|
|
175
|
-
throw new GeneralError('Cannot use --raw, --unmanaged, --ownership, and --json together. Pick at most one.');
|
|
186
|
+
throw new GeneralError('Cannot use --raw, --unmanaged, --ownership, --test-coverage, and --json together. Pick at most one.');
|
|
176
187
|
}
|
|
188
|
+
// --check / --fail-on enforcement policy (FORGE G1). Applies only
|
|
189
|
+
// where classification runs (the default view and --json).
|
|
190
|
+
const checkPolicy = resolveStatusCheckPolicy(options);
|
|
177
191
|
if (!options.raw && !options.json) {
|
|
178
192
|
intro('FireForge Status');
|
|
179
193
|
}
|
|
194
|
+
// Test-coverage mode needs only the project root (the baseline lives
|
|
195
|
+
// in .fireforge/), so it short-circuits before any engine/git guards.
|
|
196
|
+
if (options.testCoverage) {
|
|
197
|
+
renderTestCoverageStatus(await readBuildBaseline(projectRoot));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
180
200
|
const paths = getProjectPaths(projectRoot);
|
|
181
201
|
const config = await loadConfig(projectRoot);
|
|
182
202
|
const emitJsonError = (code, message) => {
|
|
@@ -257,7 +277,9 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
257
277
|
// the output through a JSON parser broke precisely when there was
|
|
258
278
|
// nothing to report. Emit `[]` here and return before the human fallback.
|
|
259
279
|
if (options.json) {
|
|
260
|
-
await
|
|
280
|
+
const classified = await classifyStatusFiles(files, paths, projectRoot, config.binaryName);
|
|
281
|
+
renderJsonStatus(classified);
|
|
282
|
+
runStatusCheck(classified, checkPolicy);
|
|
261
283
|
return;
|
|
262
284
|
}
|
|
263
285
|
// `--raw` consumers parse the native `git status --porcelain` output
|
|
@@ -278,8 +300,7 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
278
300
|
return;
|
|
279
301
|
}
|
|
280
302
|
// Patch-aware classification
|
|
281
|
-
const
|
|
282
|
-
const classified = await classifyFiles(files, paths.engine, paths.patches, config.binaryName, furnacePrefixes);
|
|
303
|
+
const classified = await classifyStatusFiles(files, paths, projectRoot, config.binaryName);
|
|
283
304
|
const buckets = {
|
|
284
305
|
conflict: classified.filter((f) => f.classification === 'conflict'),
|
|
285
306
|
unmanaged: classified.filter((f) => f.classification === 'unmanaged'),
|
|
@@ -294,6 +315,7 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
294
315
|
return;
|
|
295
316
|
}
|
|
296
317
|
await renderDefaultStatus(files.length, buckets, projectRoot, config.binaryName);
|
|
318
|
+
runStatusCheck(classified, checkPolicy);
|
|
297
319
|
}
|
|
298
320
|
finally {
|
|
299
321
|
if (isMachineOutputMode() !== previousMachineOutputMode) {
|
|
@@ -309,7 +331,10 @@ export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
|
|
|
309
331
|
.option('--raw', 'Show raw worktree status without patch classification')
|
|
310
332
|
.option('--unmanaged', 'Show only unmanaged changes (not covered by patches or tools)')
|
|
311
333
|
.option('--ownership', 'Show a flat path → owning patch table (flags files claimed by multiple patches)')
|
|
334
|
+
.option('--test-coverage', 'Show what the last recorded build covers for test packaging (full or scoped paths)')
|
|
312
335
|
.option('--json', 'Output classified file status as JSON')
|
|
336
|
+
.option('--check', 'Exit non-zero when any unmanaged, patch-owned-drift, or conflict file exists (composes with --json; combine with --fail-on for finer policy)')
|
|
337
|
+
.option('--fail-on <classifications>', 'Comma-separated classification list that fails --check, replacing the default set (implies --check). Valid: patch-backed, patch-owned-drift, unmanaged, branding, furnace, conflict')
|
|
313
338
|
.action(withErrorHandling(async (options) => {
|
|
314
339
|
await statusCommand(getProjectRoot(), options);
|
|
315
340
|
}));
|
|
@@ -14,7 +14,7 @@ import type { TestRunOutcome } from './test-run.js';
|
|
|
14
14
|
* harness problems with their own messages; everything else flows into
|
|
15
15
|
* the regular non-zero-exit diagnosis chain.
|
|
16
16
|
*/
|
|
17
|
-
export declare function finalizeSingleRunOutcome(outcome: TestRunOutcome, normalizedPaths: string[], binaryName: string, postRebuildContext: PostRebuildFailureContext | undefined): void;
|
|
17
|
+
export declare function finalizeSingleRunOutcome(outcome: TestRunOutcome, normalizedPaths: string[], binaryName: string, postRebuildContext: PostRebuildFailureContext | undefined, headless?: boolean): void;
|
|
18
18
|
/**
|
|
19
19
|
* Shard-mode adapter over {@link handleNonZeroTestExit}: produces the
|
|
20
20
|
* diagnosis text as a string (to warn per shard) instead of throwing, so
|
|
@@ -7,11 +7,12 @@
|
|
|
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, } from '../core/test-harness-crash.js';
|
|
10
|
+
import { buildGreenSummaryRejectedMessage, buildHarnessCrashMessage, buildNoTestsRanMessage, headedNoOutputTimeoutHint, } 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';
|
|
14
14
|
import { info } from '../utils/logger.js';
|
|
15
|
+
import { getPlatform } from '../utils/platform.js';
|
|
15
16
|
function buildUnknownTestMessage(testPaths) {
|
|
16
17
|
return (`mach could not discover the requested test path${testPaths.length === 1 ? '' : 's'}: ${testPaths.join(', ')}\n\n` +
|
|
17
18
|
'The file may exist, but Firefox does not currently resolve it as a runnable test.\n\n' +
|
|
@@ -179,9 +180,14 @@ function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted
|
|
|
179
180
|
* harness problems with their own messages; everything else flows into
|
|
180
181
|
* the regular non-zero-exit diagnosis chain.
|
|
181
182
|
*/
|
|
182
|
-
export function finalizeSingleRunOutcome(outcome, normalizedPaths, binaryName, postRebuildContext) {
|
|
183
|
+
export function finalizeSingleRunOutcome(outcome, normalizedPaths, binaryName, postRebuildContext, headless = false) {
|
|
183
184
|
if (outcome.verdict.kind === 'harness-crash' && outcome.verdict.signature) {
|
|
184
|
-
|
|
185
|
+
const base = buildHarnessCrashMessage(outcome.verdict.signature, outcome.attempts);
|
|
186
|
+
const hint = headedNoOutputTimeoutHint(outcome.verdict.signature, {
|
|
187
|
+
headless,
|
|
188
|
+
platform: getPlatform(),
|
|
189
|
+
});
|
|
190
|
+
throw new GeneralError(hint ? `${base}\n\n${hint}` : base);
|
|
185
191
|
}
|
|
186
192
|
if (outcome.verdict.kind === 'tests-ran-ok') {
|
|
187
193
|
// The verdict is authoritative over the raw exit code: a completed
|
|
@@ -12,4 +12,25 @@ export declare function resolveCanaryPath(options: TestOptions, projectConfig: P
|
|
|
12
12
|
export declare function canaryTimeoutSeconds(projectConfig: ProjectConfig): number;
|
|
13
13
|
/** Emits or throws the one-word canary verdict. */
|
|
14
14
|
export declare function reportCanaryOutcome(outcome: TestRunOutcome): void;
|
|
15
|
+
/** Per-harness bucketing of the requested test paths. */
|
|
16
|
+
export interface HarnessClassification {
|
|
17
|
+
xpcshell: string[];
|
|
18
|
+
nonXpcshell: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Harness classification is a pure path→manifest lookup and must precede
|
|
22
|
+
* the build dispatch: a mixed xpcshell+mochitest request can never
|
|
23
|
+
* dispatch, so refusing it before spending minutes in a pre-test build
|
|
24
|
+
* is strictly better (FORGE F7). A nonexistent path classifies as
|
|
25
|
+
* nonXpcshell (findNearestXpcshellManifest returns null), so classifying
|
|
26
|
+
* before assertTestPathsExist is harmless — existence is still asserted
|
|
27
|
+
* after the stale/coverage gates, preserving their precedence over
|
|
28
|
+
* missing-path errors. `xpcshellOnly` (non-empty request, zero
|
|
29
|
+
* nonXpcshell paths) gates the Marionette preflight and the mochitest
|
|
30
|
+
* client flags (FORGE F10); pathless runs keep the full-suite behavior.
|
|
31
|
+
*/
|
|
32
|
+
export declare function classifyBeforeDispatch(engineDir: string, normalizedPaths: string[]): Promise<{
|
|
33
|
+
classification: HarnessClassification;
|
|
34
|
+
xpcshellOnly: boolean;
|
|
35
|
+
}>;
|
|
15
36
|
export {};
|