@hominis/fireforge 0.40.0 → 0.42.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 +85 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +16 -7
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +12 -59
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +151 -50
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +8 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +113 -268
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +184 -175
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +41 -4
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/branding.d.ts +16 -0
- package/dist/src/core/branding.js +18 -2
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +34 -36
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-mozconfig.js +9 -1
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +6 -1
- package/dist/src/core/status-classify.js +101 -73
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -10,14 +10,6 @@
|
|
|
10
10
|
* warning (e.g. `"pending JavaScript error cleanup"`) do not start with
|
|
11
11
|
* the prefix and should not trip the scanner.
|
|
12
12
|
*/
|
|
13
|
-
/**
|
|
14
|
-
* Line prefixes that signal an actual runtime error in a Firefox chrome
|
|
15
|
-
* process. A hit on any of these patterns is a smoke failure unless the
|
|
16
|
-
* line also matches the caller-supplied allowlist.
|
|
17
|
-
*
|
|
18
|
-
* Additions here should be conservative — false positives turn every
|
|
19
|
-
* smoke run into noise for operators and every CI run into flake.
|
|
20
|
-
*/
|
|
21
13
|
export declare const SMOKE_ERROR_PATTERNS: readonly RegExp[];
|
|
22
14
|
/**
|
|
23
15
|
* Returns `true` when `line` matches any pattern in
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* Additions here should be conservative — false positives turn every
|
|
20
20
|
* smoke run into noise for operators and every CI run into flake.
|
|
21
21
|
*/
|
|
22
|
+
import { toError } from '../utils/errors.js';
|
|
22
23
|
export const SMOKE_ERROR_PATTERNS = [
|
|
23
24
|
// Firefox chrome error lines — `JavaScript error: chrome://…, line N: TypeError: …`.
|
|
24
25
|
/^\s*JavaScript error:/i,
|
|
@@ -79,7 +80,7 @@ export function compileAllowlistFromFile(body, sourcePath) {
|
|
|
79
80
|
});
|
|
80
81
|
}
|
|
81
82
|
catch (error) {
|
|
82
|
-
const message = error
|
|
83
|
+
const message = toError(error).message;
|
|
83
84
|
throw new Error(`Invalid allowlist regex at ${sourcePath}:${String(index + 1)}: ${message}`, {
|
|
84
85
|
cause: error,
|
|
85
86
|
});
|
|
@@ -102,7 +103,7 @@ export function compileAllowlistFromStrings(sources) {
|
|
|
102
103
|
});
|
|
103
104
|
}
|
|
104
105
|
catch (error) {
|
|
105
|
-
const message = error
|
|
106
|
+
const message = toError(error).message;
|
|
106
107
|
throw new Error(`Invalid --console-allow regex at position ${String(index + 1)} ("${source}"): ${message}`, { cause: error });
|
|
107
108
|
}
|
|
108
109
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary-aware drift classification for patch-owned files (FORGE J3).
|
|
3
|
+
*
|
|
4
|
+
* The text classifier compares utf-8 decoded content against
|
|
5
|
+
* `computePatchedContent`, which cannot apply `GIT binary patch` bodies —
|
|
6
|
+
* so a binary file exported into a patch classified as `patch-owned-drift`
|
|
7
|
+
* forever, keeping the engine-clean gate permanently red with nothing
|
|
8
|
+
* actually un-durable. Binary sections DO carry the ground truth, though:
|
|
9
|
+
* `git diff --binary` always records full blob hashes on the section's
|
|
10
|
+
* `index <old>..<new>` line. Comparing the live file's `git hash-object`
|
|
11
|
+
* against the last owning section's new-side hash settles the
|
|
12
|
+
* classification exactly. Bodies without a usable hash (hand-written
|
|
13
|
+
* diffs, `Binary files … differ`) classify as `binary-unsupported` —
|
|
14
|
+
* an honest "cannot compare" instead of a false "not durable".
|
|
15
|
+
*
|
|
16
|
+
* Lives in its own module (not `status-classify.ts`) so the classifier
|
|
17
|
+
* stays within the per-file line budget and this module never has to
|
|
18
|
+
* import it back (dpdm-clean: the dependency edge points one way).
|
|
19
|
+
*/
|
|
20
|
+
import type { PatchedContentContext } from './patch-apply.js';
|
|
21
|
+
/** Batched patch lookup, injected by callers that classify many files. */
|
|
22
|
+
export type AffectingPatchLookup = Pick<PatchedContentContext, 'getAffectingPatches' | 'readPatchBody'>;
|
|
23
|
+
/**
|
|
24
|
+
* Classification result for a binary comparison. Structurally assignable
|
|
25
|
+
* to `ClassifiedFile` without importing it (which would create a cycle
|
|
26
|
+
* with `status-classify.ts`).
|
|
27
|
+
*/
|
|
28
|
+
export interface BinaryClassifiedFile {
|
|
29
|
+
status: string;
|
|
30
|
+
file: string;
|
|
31
|
+
classification: 'patch-backed' | 'furnace' | 'patch-owned-drift' | 'binary-unsupported';
|
|
32
|
+
owner: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Classifies a single-owner file whose comparison is binary. Returns
|
|
36
|
+
* `null` when the comparison is NOT binary — the live file is text and
|
|
37
|
+
* (for deletions) the owning section is text — so the caller falls
|
|
38
|
+
* through to the existing utf-8 content comparison unchanged.
|
|
39
|
+
*
|
|
40
|
+
* @param args.fileMissing - True when the porcelain status reports the
|
|
41
|
+
* file deleted (there are no live bytes to probe or hash).
|
|
42
|
+
*/
|
|
43
|
+
export declare function classifyBinaryOwnedFile(args: {
|
|
44
|
+
entry: {
|
|
45
|
+
status: string;
|
|
46
|
+
file: string;
|
|
47
|
+
};
|
|
48
|
+
engineDir: string;
|
|
49
|
+
patchesDir: string;
|
|
50
|
+
matchClassification: 'patch-backed' | 'furnace';
|
|
51
|
+
owner: string;
|
|
52
|
+
fileMissing?: boolean;
|
|
53
|
+
/** Batched lookup; when absent, falls back to per-file manifest queries. */
|
|
54
|
+
lookup?: AffectingPatchLookup;
|
|
55
|
+
}): Promise<BinaryClassifiedFile | null>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Binary-aware drift classification for patch-owned files (FORGE J3).
|
|
4
|
+
*
|
|
5
|
+
* The text classifier compares utf-8 decoded content against
|
|
6
|
+
* `computePatchedContent`, which cannot apply `GIT binary patch` bodies —
|
|
7
|
+
* so a binary file exported into a patch classified as `patch-owned-drift`
|
|
8
|
+
* forever, keeping the engine-clean gate permanently red with nothing
|
|
9
|
+
* actually un-durable. Binary sections DO carry the ground truth, though:
|
|
10
|
+
* `git diff --binary` always records full blob hashes on the section's
|
|
11
|
+
* `index <old>..<new>` line. Comparing the live file's `git hash-object`
|
|
12
|
+
* against the last owning section's new-side hash settles the
|
|
13
|
+
* classification exactly. Bodies without a usable hash (hand-written
|
|
14
|
+
* diffs, `Binary files … differ`) classify as `binary-unsupported` —
|
|
15
|
+
* an honest "cannot compare" instead of a false "not durable".
|
|
16
|
+
*
|
|
17
|
+
* Lives in its own module (not `status-classify.ts`) so the classifier
|
|
18
|
+
* stays within the per-file line budget and this module never has to
|
|
19
|
+
* import it back (dpdm-clean: the dependency edge points one way).
|
|
20
|
+
*/
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { toError } from '../utils/errors.js';
|
|
23
|
+
import { readText } from '../utils/fs.js';
|
|
24
|
+
import { verbose } from '../utils/logger.js';
|
|
25
|
+
import { hashObjectBatch, isBinaryFile } from './git-file-ops.js';
|
|
26
|
+
import { findPatchesAffectingFile } from './patch-manifest.js';
|
|
27
|
+
import { parseDiffSections } from './patch-parse.js';
|
|
28
|
+
/** Finds the LAST diff section affecting `file` across the owning patches, in queue order. */
|
|
29
|
+
async function findLastAffectingSection(patchesDir, file, lookup) {
|
|
30
|
+
// The lookup shares one manifest load + memoized body reads across the
|
|
31
|
+
// whole classification batch; the fallback keeps single-file callers
|
|
32
|
+
// (and existing direct tests) working without a context.
|
|
33
|
+
const patches = lookup !== undefined
|
|
34
|
+
? lookup.getAffectingPatches(file)
|
|
35
|
+
: (await findPatchesAffectingFile(patchesDir, file)).map(({ patch }) => patch);
|
|
36
|
+
let last;
|
|
37
|
+
for (const patch of patches) {
|
|
38
|
+
const body = lookup !== undefined ? await lookup.readPatchBody(patch) : await readText(patch.path);
|
|
39
|
+
for (const section of parseDiffSections(body)) {
|
|
40
|
+
if (section.targetPath === file || section.sourcePath === file) {
|
|
41
|
+
last = section;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return last;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Classifies a single-owner file whose comparison is binary. Returns
|
|
49
|
+
* `null` when the comparison is NOT binary — the live file is text and
|
|
50
|
+
* (for deletions) the owning section is text — so the caller falls
|
|
51
|
+
* through to the existing utf-8 content comparison unchanged.
|
|
52
|
+
*
|
|
53
|
+
* @param args.fileMissing - True when the porcelain status reports the
|
|
54
|
+
* file deleted (there are no live bytes to probe or hash).
|
|
55
|
+
*/
|
|
56
|
+
export async function classifyBinaryOwnedFile(args) {
|
|
57
|
+
const { entry, engineDir, patchesDir, matchClassification, owner } = args;
|
|
58
|
+
const fileMissing = args.fileMissing === true;
|
|
59
|
+
const unsupported = () => ({
|
|
60
|
+
...entry,
|
|
61
|
+
classification: 'binary-unsupported',
|
|
62
|
+
owner,
|
|
63
|
+
});
|
|
64
|
+
// Until the comparison is KNOWN to be binary, every failure defers to
|
|
65
|
+
// the existing text path (`return null`) — classifying a text file as
|
|
66
|
+
// `binary-unsupported` because a probe failed would hide real drift.
|
|
67
|
+
let section;
|
|
68
|
+
try {
|
|
69
|
+
if (!fileMissing) {
|
|
70
|
+
const liveBinary = await isBinaryFile(engineDir, entry.file);
|
|
71
|
+
if (!liveBinary)
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
section = await findLastAffectingSection(patchesDir, entry.file, args.lookup);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (fileMissing)
|
|
78
|
+
return null;
|
|
79
|
+
verbose(`Classifying ${entry.file} as binary-unsupported because the binary probe failed: ${toError(error).message}`);
|
|
80
|
+
return unsupported();
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
if (section === undefined) {
|
|
84
|
+
// Claimed in filesAffected but no diff section touches the file.
|
|
85
|
+
// A missing file keeps the existing deletion logic; a live binary
|
|
86
|
+
// has nothing recorded to compare against.
|
|
87
|
+
return fileMissing ? null : unsupported();
|
|
88
|
+
}
|
|
89
|
+
if (!section.isBinary) {
|
|
90
|
+
// Text section: deletions settle fine through the text path; a
|
|
91
|
+
// live binary against a text body has no comparable recording.
|
|
92
|
+
return fileMissing ? null : unsupported();
|
|
93
|
+
}
|
|
94
|
+
if (section.isDeletedFile) {
|
|
95
|
+
return {
|
|
96
|
+
...entry,
|
|
97
|
+
classification: fileMissing ? matchClassification : 'patch-owned-drift',
|
|
98
|
+
owner,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (fileMissing) {
|
|
102
|
+
// The last binary section expects the file to exist.
|
|
103
|
+
return { ...entry, classification: 'patch-owned-drift', owner };
|
|
104
|
+
}
|
|
105
|
+
if (section.indexNewHash !== undefined) {
|
|
106
|
+
const fullPath = join(engineDir, entry.file);
|
|
107
|
+
const live = (await hashObjectBatch(engineDir, [fullPath])).get(fullPath);
|
|
108
|
+
if (live !== undefined) {
|
|
109
|
+
// Prefix-compare: recorded hashes may be abbreviated (≥7 chars).
|
|
110
|
+
const matches = live.startsWith(section.indexNewHash) || section.indexNewHash.startsWith(live);
|
|
111
|
+
return {
|
|
112
|
+
...entry,
|
|
113
|
+
classification: matches ? matchClassification : 'patch-owned-drift',
|
|
114
|
+
owner,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return unsupported();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
verbose(`Classifying ${entry.file} as binary-unsupported because the binary comparison failed: ${toError(error).message}`);
|
|
122
|
+
return unsupported();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -34,8 +34,13 @@
|
|
|
34
34
|
* cross-patch conflicts silently rolled into the `unmanaged` bucket
|
|
35
35
|
* in `--json`, which misled scripts built on top of the JSON view
|
|
36
36
|
* into treating the file as routine local drift.
|
|
37
|
+
* - `binary-unsupported`: the file's comparison is binary and the owning
|
|
38
|
+
* patch body records no usable blob hash to compare against — an
|
|
39
|
+
* honest "binary — comparison unsupported" instead of a permanent
|
|
40
|
+
* false `patch-owned-drift`. Deliberately NOT in the default
|
|
41
|
+
* `--check` fail set; opt in with `--fail-on binary-unsupported`.
|
|
37
42
|
*/
|
|
38
|
-
export type FileClassification = 'patch-backed' | 'patch-owned-drift' | 'unmanaged' | 'branding' | 'furnace' | 'conflict';
|
|
43
|
+
export type FileClassification = 'patch-backed' | 'patch-owned-drift' | 'unmanaged' | 'branding' | 'furnace' | 'conflict' | 'binary-unsupported';
|
|
39
44
|
export interface StatusFile {
|
|
40
45
|
status: string;
|
|
41
46
|
file: string;
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* under the per-file line budget as the number of buckets grows.
|
|
8
8
|
*/
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
+
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
10
11
|
import { toError } from '../utils/errors.js';
|
|
11
12
|
import { readText } from '../utils/fs.js';
|
|
12
13
|
import { verbose } from '../utils/logger.js';
|
|
13
14
|
import { isBrandingManagedPath } from './branding.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
15
|
+
import { createPatchedContentContext } from './patch-apply.js';
|
|
16
|
+
import { classifyBinaryOwnedFile } from './status-binary.js';
|
|
16
17
|
/**
|
|
17
18
|
* Builds the file → owning-patch-filenames multimap from manifest rows.
|
|
18
19
|
* Single source of truth for cross-patch claim detection — status
|
|
@@ -71,10 +72,27 @@ export function isGeneratedBrandingPath(file, binaryName) {
|
|
|
71
72
|
* deletion the patch does not expect, or a failed comparison) reports
|
|
72
73
|
* `patch-owned-drift`.
|
|
73
74
|
*/
|
|
74
|
-
async function classifySingleOwnerFile(entry, engineDir, patchesDir, matchClassification, owner) {
|
|
75
|
+
async function classifySingleOwnerFile(entry, engineDir, patchesDir, matchClassification, owner, ctx) {
|
|
76
|
+
// Binary comparisons cannot go through the utf-8 content path below —
|
|
77
|
+
// binary patch bodies parse to zero hunks, so the patched-content
|
|
78
|
+
// computation returned HEAD content unchanged and the comparison
|
|
79
|
+
// reported `patch-owned-drift` forever (FORGE J3). Settle by blob hash,
|
|
80
|
+
// or classify explicitly as `binary-unsupported` when no hash exists.
|
|
81
|
+
const binaryResult = await classifyBinaryOwnedFile({
|
|
82
|
+
entry,
|
|
83
|
+
engineDir,
|
|
84
|
+
patchesDir,
|
|
85
|
+
matchClassification,
|
|
86
|
+
owner,
|
|
87
|
+
fileMissing: getPrimaryStatusCode(entry.status) === 'D',
|
|
88
|
+
lookup: ctx,
|
|
89
|
+
});
|
|
90
|
+
if (binaryResult !== null) {
|
|
91
|
+
return binaryResult;
|
|
92
|
+
}
|
|
75
93
|
if (getPrimaryStatusCode(entry.status) === 'D') {
|
|
76
94
|
// Deleted file: content matches only if the patch expects deletion
|
|
77
|
-
const expected = await
|
|
95
|
+
const expected = await ctx.computePatched(entry.file);
|
|
78
96
|
return {
|
|
79
97
|
...entry,
|
|
80
98
|
classification: expected === null ? matchClassification : 'patch-owned-drift',
|
|
@@ -84,7 +102,7 @@ async function classifySingleOwnerFile(entry, engineDir, patchesDir, matchClassi
|
|
|
84
102
|
// File exists on disk — compare actual vs expected
|
|
85
103
|
try {
|
|
86
104
|
const [expected, actual] = await Promise.all([
|
|
87
|
-
|
|
105
|
+
ctx.computePatched(entry.file),
|
|
88
106
|
readText(join(engineDir, entry.file)),
|
|
89
107
|
]);
|
|
90
108
|
return {
|
|
@@ -112,7 +130,11 @@ async function classifySingleOwnerFile(entry, engineDir, patchesDir, matchClassi
|
|
|
112
130
|
* machine consumers about the nature of the drift.
|
|
113
131
|
*/
|
|
114
132
|
export async function classifyFiles(files, engineDir, patchesDir, binaryName, furnacePrefixes) {
|
|
115
|
-
|
|
133
|
+
// One manifest load + patch discovery + memoized body reads for the
|
|
134
|
+
// whole batch — the previous per-file computation re-ran all three for
|
|
135
|
+
// every dirty file (O(dirtyFiles × patches) redundant IO on a broad
|
|
136
|
+
// engine edit session, FORGE K1).
|
|
137
|
+
const ctx = await createPatchedContentContext(patchesDir, engineDir);
|
|
116
138
|
// Build a multimap from file path → list of claiming patch
|
|
117
139
|
// filenames so we can detect cross-patch ownership conflicts. The
|
|
118
140
|
// previous `Set<string>` captured only whether a path was claimed,
|
|
@@ -120,75 +142,81 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
|
|
|
120
142
|
// branch where the expected-vs-actual content comparison then routed
|
|
121
143
|
// them into `unmanaged` when the content didn't match either owner's
|
|
122
144
|
// expectation.
|
|
123
|
-
const patchClaims =
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
145
|
+
const patchClaims = buildPatchClaims(ctx.manifestPatches);
|
|
146
|
+
const deps = {
|
|
147
|
+
engineDir,
|
|
148
|
+
patchesDir,
|
|
149
|
+
binaryName,
|
|
150
|
+
furnacePrefixes,
|
|
151
|
+
patchClaims,
|
|
152
|
+
ctx,
|
|
153
|
+
};
|
|
154
|
+
// Bounded pool over per-file classification (each single-owner file
|
|
155
|
+
// spawns git); order preserved by the mapper. Per-file failures settle
|
|
156
|
+
// inside classifySingleOwnerFile's catch, so one bad file never rejects
|
|
157
|
+
// the batch.
|
|
158
|
+
return mapWithConcurrency(files, CLASSIFY_CONCURRENCY, (entry) => classifyEntry(entry, deps));
|
|
159
|
+
}
|
|
160
|
+
/** Concurrency bound for per-file classification (matches import's guard). */
|
|
161
|
+
const CLASSIFY_CONCURRENCY = 8;
|
|
162
|
+
/** Classifies one status entry; extracted so the pool worker stays flat. */
|
|
163
|
+
async function classifyEntry(entry, deps) {
|
|
164
|
+
const { engineDir, patchesDir, binaryName, furnacePrefixes, patchClaims, ctx } = deps;
|
|
165
|
+
const owners = patchClaims.get(entry.file);
|
|
166
|
+
const primaryCode = getPrimaryStatusCode(entry.status);
|
|
167
|
+
// Ownership is a structural invariant and takes precedence over the
|
|
168
|
+
// content-management bucket. Otherwise a branding/Furnace prefix hides a
|
|
169
|
+
// multi-patch claim from status --check/--json even though --ownership
|
|
170
|
+
// reports the same path as CONFLICT.
|
|
171
|
+
if (owners && owners.length >= 2) {
|
|
172
|
+
return {
|
|
173
|
+
...entry,
|
|
174
|
+
classification: 'conflict',
|
|
175
|
+
claimedBy: [...owners],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// Branding paths are tool-managed for generated edits, but a brand-new
|
|
179
|
+
// unowned branding asset must not disappear from `status --unmanaged`.
|
|
180
|
+
// The Hominis Firefox 152 side-grade added Assets.car under the active
|
|
181
|
+
// branding tree; classifying every branding path before checking
|
|
182
|
+
// ownership hid that new patch candidate as "branding" even though no
|
|
183
|
+
// patch claimed it yet.
|
|
184
|
+
const isUnownedNewFile = owners === undefined && (primaryCode === '?' || primaryCode === 'A');
|
|
185
|
+
if (isBrandingManagedPath(entry.file, binaryName) &&
|
|
186
|
+
(!isUnownedNewFile || isGeneratedBrandingPath(entry.file, binaryName))) {
|
|
187
|
+
return { ...entry, classification: 'branding' };
|
|
188
|
+
}
|
|
189
|
+
// Furnace-managed component paths
|
|
190
|
+
if (furnacePrefixes.size > 0) {
|
|
191
|
+
let isFurnace = false;
|
|
192
|
+
for (const prefix of furnacePrefixes) {
|
|
193
|
+
if (entry.file.startsWith(prefix)) {
|
|
194
|
+
isFurnace = true;
|
|
195
|
+
break;
|
|
165
196
|
}
|
|
166
197
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
// Not in any patch → unmanaged
|
|
181
|
-
if (!owners) {
|
|
182
|
-
results.push({ ...entry, classification: 'unmanaged' });
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
// File is claimed by exactly one patch — compare content.
|
|
186
|
-
const owner = owners[0];
|
|
187
|
-
if (owner === undefined) {
|
|
188
|
-
results.push({ ...entry, classification: 'unmanaged' });
|
|
189
|
-
continue;
|
|
198
|
+
if (isFurnace) {
|
|
199
|
+
// A furnace path claimed by exactly one patch gets the same
|
|
200
|
+
// expected-vs-actual comparison as any other single-owner path:
|
|
201
|
+
// after a `furnace deploy` of an edited component the deployed
|
|
202
|
+
// copy has content the owning patch's body lacks, and the old
|
|
203
|
+
// unconditional short-circuit silently bucketed that drift as
|
|
204
|
+
// `furnace`. Multi-owner and unowned furnace paths keep the
|
|
205
|
+
// short-circuit.
|
|
206
|
+
if (owners && owners.length === 1 && owners[0] !== undefined) {
|
|
207
|
+
return classifySingleOwnerFile(entry, engineDir, patchesDir, 'furnace', owners[0], ctx);
|
|
208
|
+
}
|
|
209
|
+
return { ...entry, classification: 'furnace' };
|
|
190
210
|
}
|
|
191
|
-
results.push(await classifySingleOwnerFile(entry, engineDir, patchesDir, 'patch-backed', owner));
|
|
192
211
|
}
|
|
193
|
-
|
|
212
|
+
// Not in any patch → unmanaged
|
|
213
|
+
if (!owners) {
|
|
214
|
+
return { ...entry, classification: 'unmanaged' };
|
|
215
|
+
}
|
|
216
|
+
// File is claimed by exactly one patch — compare content.
|
|
217
|
+
const owner = owners[0];
|
|
218
|
+
if (owner === undefined) {
|
|
219
|
+
return { ...entry, classification: 'unmanaged' };
|
|
220
|
+
}
|
|
221
|
+
return classifySingleOwnerFile(entry, engineDir, patchesDir, 'patch-backed', owner, ctx);
|
|
194
222
|
}
|
|
@@ -28,8 +28,15 @@ export interface HarnessCrashSignature {
|
|
|
28
28
|
reason: string;
|
|
29
29
|
line: string;
|
|
30
30
|
}
|
|
31
|
+
/** Numeric counts parsed from the harness's embedded result summary. */
|
|
32
|
+
export interface HarnessSummaryCounts {
|
|
33
|
+
/** Total checks the suite ran (`Ran N checks`), when the summary prints one. */
|
|
34
|
+
checks?: number;
|
|
35
|
+
/** Unexpected-result count (`Unexpected results: N`), when the summary prints one. */
|
|
36
|
+
unexpected?: number;
|
|
37
|
+
}
|
|
31
38
|
/** Result of {@link classifyHarnessRun}. */
|
|
32
|
-
export interface HarnessRunVerdict {
|
|
39
|
+
export interface HarnessRunVerdict extends HarnessSummaryCounts {
|
|
33
40
|
kind: HarnessRunClassification;
|
|
34
41
|
signature?: HarnessCrashSignature;
|
|
35
42
|
/** First concrete test-failure evidence line, when available. */
|
|
@@ -146,6 +153,30 @@ export declare function detectHarnessCrashSignature(output: string): HarnessCras
|
|
|
146
153
|
* (`greenSummaryRejected`) and fails.
|
|
147
154
|
*/
|
|
148
155
|
export declare function classifyHarnessRun(exitCode: number, output: string, requestedPaths: readonly string[]): HarnessRunVerdict;
|
|
156
|
+
/**
|
|
157
|
+
* Parses the numeric counts from the harness's embedded result summary
|
|
158
|
+
* (`Ran N checks` / `Unexpected results: N`). The LAST occurrence of each
|
|
159
|
+
* wins — a multi-suite run prints one summary per suite and the final one
|
|
160
|
+
* covers the aggregate the operator sees. Absent counts are omitted (the
|
|
161
|
+
* `FIREFORGE-VERDICT:` contract: an absent key means "summary did not
|
|
162
|
+
* print it", never zero).
|
|
163
|
+
*/
|
|
164
|
+
export declare function extractSummaryCounts(output: string): HarnessSummaryCounts;
|
|
165
|
+
/**
|
|
166
|
+
* Formats the machine-readable `FIREFORGE-VERDICT:` line — the stable,
|
|
167
|
+
* greppable last line every `fireforge test` run prints so harness-verdict
|
|
168
|
+
* consumers stop regexing mach internals. The status follows THIS
|
|
169
|
+
* classifier, never the raw exit code: a crash-classified run says
|
|
170
|
+
* `FAIL reason=crash` even at exit 0, and a green-summary-override pass
|
|
171
|
+
* says `PASS` despite a non-zero mach exit. Count keys are omitted when
|
|
172
|
+
* the embedded summary did not print them. Sharded aggregate lines carry
|
|
173
|
+
* a trailing `shards=<passed>/<total>` instead of counts (counts belong
|
|
174
|
+
* to a single embedded suite summary).
|
|
175
|
+
*/
|
|
176
|
+
export declare function formatFireforgeVerdictLine(verdict: HarnessRunVerdict, shards?: {
|
|
177
|
+
passed: number;
|
|
178
|
+
total: number;
|
|
179
|
+
}): string;
|
|
149
180
|
/**
|
|
150
181
|
* Builds the operator-facing failure message for a green-looking summary
|
|
151
182
|
* that was rejected on crash or truncation evidence (never trusted as a
|
|
@@ -447,20 +447,21 @@ export function detectHarnessCrashSignature(output) {
|
|
|
447
447
|
* (`greenSummaryRejected`) and fails.
|
|
448
448
|
*/
|
|
449
449
|
export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
450
|
+
const counts = extractSummaryCounts(output);
|
|
450
451
|
const realFailures = realUnexpectedFailureLines(output);
|
|
451
452
|
const firstRealFailure = realFailures[0];
|
|
452
453
|
const failureBlocks = collectUnexpectedFailureBlocks(output);
|
|
453
454
|
const secondaryHarnessSignature = firstRealFailure !== undefined ? detectSecondaryHarnessNoise(output) : undefined;
|
|
454
455
|
const signature = detectHarnessCrashSignature(output);
|
|
455
456
|
if (signature) {
|
|
456
|
-
return { kind: 'harness-crash', signature };
|
|
457
|
+
return { kind: 'harness-crash', signature, ...counts };
|
|
457
458
|
}
|
|
458
459
|
const ranTests = hasExecutionSignal(output);
|
|
459
460
|
if (!ranTests && requestedPaths.length > 0) {
|
|
460
|
-
return { kind: 'no-tests' };
|
|
461
|
+
return { kind: 'no-tests', ...counts };
|
|
461
462
|
}
|
|
462
463
|
if (exitCode === 0) {
|
|
463
|
-
return { kind: 'tests-ran-ok' };
|
|
464
|
+
return { kind: 'tests-ran-ok', ...counts };
|
|
464
465
|
}
|
|
465
466
|
// Exit codes follow the corrected verdict: a run whose embedded summary
|
|
466
467
|
// completed green is a pass even when the wrapper exit code went
|
|
@@ -483,6 +484,7 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
|
483
484
|
completeness.neverEnded.length > 0) {
|
|
484
485
|
return {
|
|
485
486
|
kind: 'test-failures',
|
|
487
|
+
...counts,
|
|
486
488
|
...(firstRealFailure !== undefined ? { realFailureLine: firstRealFailure } : {}),
|
|
487
489
|
...(failureBlocks.length > 0 ? { realFailureBlocks: failureBlocks } : {}),
|
|
488
490
|
...(secondaryHarnessSignature !== undefined ? { secondaryHarnessSignature } : {}),
|
|
@@ -493,15 +495,68 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
|
493
495
|
},
|
|
494
496
|
};
|
|
495
497
|
}
|
|
496
|
-
return { kind: 'tests-ran-ok', greenSummaryOverride: true };
|
|
498
|
+
return { kind: 'tests-ran-ok', greenSummaryOverride: true, ...counts };
|
|
497
499
|
}
|
|
498
500
|
return {
|
|
499
501
|
kind: 'test-failures',
|
|
502
|
+
...counts,
|
|
500
503
|
...(firstRealFailure !== undefined ? { realFailureLine: firstRealFailure } : {}),
|
|
501
504
|
...(failureBlocks.length > 0 ? { realFailureBlocks: failureBlocks } : {}),
|
|
502
505
|
...(secondaryHarnessSignature !== undefined ? { secondaryHarnessSignature } : {}),
|
|
503
506
|
};
|
|
504
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Parses the numeric counts from the harness's embedded result summary
|
|
510
|
+
* (`Ran N checks` / `Unexpected results: N`). The LAST occurrence of each
|
|
511
|
+
* wins — a multi-suite run prints one summary per suite and the final one
|
|
512
|
+
* covers the aggregate the operator sees. Absent counts are omitted (the
|
|
513
|
+
* `FIREFORGE-VERDICT:` contract: an absent key means "summary did not
|
|
514
|
+
* print it", never zero).
|
|
515
|
+
*/
|
|
516
|
+
export function extractSummaryCounts(output) {
|
|
517
|
+
const checks = lastCapturedCount(output, /\bRan (\d+) checks?\b/g);
|
|
518
|
+
const unexpected = lastCapturedCount(output, /\bUnexpected results:\s*(\d+)\b/g);
|
|
519
|
+
return {
|
|
520
|
+
...(checks !== undefined ? { checks } : {}),
|
|
521
|
+
...(unexpected !== undefined ? { unexpected } : {}),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/** Last captured integer for `pattern` in `output`, if any occurrence matches. */
|
|
525
|
+
function lastCapturedCount(output, pattern) {
|
|
526
|
+
let last;
|
|
527
|
+
for (const match of output.matchAll(pattern)) {
|
|
528
|
+
const captured = match[1];
|
|
529
|
+
if (captured !== undefined) {
|
|
530
|
+
last = Number.parseInt(captured, 10);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return last;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Formats the machine-readable `FIREFORGE-VERDICT:` line — the stable,
|
|
537
|
+
* greppable last line every `fireforge test` run prints so harness-verdict
|
|
538
|
+
* consumers stop regexing mach internals. The status follows THIS
|
|
539
|
+
* classifier, never the raw exit code: a crash-classified run says
|
|
540
|
+
* `FAIL reason=crash` even at exit 0, and a green-summary-override pass
|
|
541
|
+
* says `PASS` despite a non-zero mach exit. Count keys are omitted when
|
|
542
|
+
* the embedded summary did not print them. Sharded aggregate lines carry
|
|
543
|
+
* a trailing `shards=<passed>/<total>` instead of counts (counts belong
|
|
544
|
+
* to a single embedded suite summary).
|
|
545
|
+
*/
|
|
546
|
+
export function formatFireforgeVerdictLine(verdict, shards) {
|
|
547
|
+
const counts = (verdict.checks !== undefined ? ` checks=${verdict.checks}` : '') +
|
|
548
|
+
(verdict.unexpected !== undefined ? ` unexpected=${verdict.unexpected}` : '');
|
|
549
|
+
const shardSuffix = shards ? ` shards=${shards.passed}/${shards.total}` : '';
|
|
550
|
+
if (verdict.kind === 'tests-ran-ok') {
|
|
551
|
+
return `FIREFORGE-VERDICT: PASS${counts}${shardSuffix}`;
|
|
552
|
+
}
|
|
553
|
+
const reason = verdict.kind === 'harness-crash'
|
|
554
|
+
? 'crash'
|
|
555
|
+
: verdict.kind === 'no-tests'
|
|
556
|
+
? 'no-tests'
|
|
557
|
+
: 'test-failures';
|
|
558
|
+
return `FIREFORGE-VERDICT: FAIL reason=${reason}${counts}${shardSuffix}`;
|
|
559
|
+
}
|
|
505
560
|
/**
|
|
506
561
|
* Builds the operator-facing failure message for a green-looking summary
|
|
507
562
|
* that was rejected on crash or truncation evidence (never trusted as a
|
|
@@ -39,6 +39,7 @@ async function collectTestFiles(dir, relPrefix) {
|
|
|
39
39
|
entries = await readdir(dir, { withFileTypes: true });
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
|
+
// An unreadable directory contributes no test files to the scope.
|
|
42
43
|
return [];
|
|
43
44
|
}
|
|
44
45
|
for (const entry of entries) {
|
|
@@ -57,6 +58,7 @@ async function isDirectorySafe(path) {
|
|
|
57
58
|
return (await stat(path)).isDirectory();
|
|
58
59
|
}
|
|
59
60
|
catch {
|
|
61
|
+
// A path that cannot be stat'd is not a directory for scoping purposes.
|
|
60
62
|
return false;
|
|
61
63
|
}
|
|
62
64
|
}
|