@hominis/fireforge 0.15.6 → 0.15.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +158 -15
  3. package/dist/src/commands/build.js +60 -3
  4. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +17 -0
  5. package/dist/src/commands/furnace/chrome-doc-templates.js +18 -0
  6. package/dist/src/commands/furnace/chrome-doc-tests.d.ts +23 -0
  7. package/dist/src/commands/furnace/chrome-doc-tests.js +120 -0
  8. package/dist/src/commands/furnace/chrome-doc.d.ts +11 -0
  9. package/dist/src/commands/furnace/chrome-doc.js +37 -4
  10. package/dist/src/commands/furnace/create-dry-run.d.ts +38 -0
  11. package/dist/src/commands/furnace/create-dry-run.js +100 -0
  12. package/dist/src/commands/furnace/create-features.d.ts +24 -0
  13. package/dist/src/commands/furnace/create-features.js +56 -0
  14. package/dist/src/commands/furnace/create-templates.d.ts +9 -5
  15. package/dist/src/commands/furnace/create-templates.js +28 -6
  16. package/dist/src/commands/furnace/create.js +62 -63
  17. package/dist/src/commands/furnace/index.js +4 -1
  18. package/dist/src/commands/lint.d.ts +17 -2
  19. package/dist/src/commands/lint.js +25 -2
  20. package/dist/src/commands/register.d.ts +1 -1
  21. package/dist/src/commands/register.js +30 -7
  22. package/dist/src/commands/run.d.ts +15 -1
  23. package/dist/src/commands/run.js +202 -7
  24. package/dist/src/commands/test.js +113 -3
  25. package/dist/src/core/build-audit-registration.d.ts +80 -0
  26. package/dist/src/core/build-audit-registration.js +187 -0
  27. package/dist/src/core/build-audit-transforms.d.ts +23 -0
  28. package/dist/src/core/build-audit-transforms.js +94 -0
  29. package/dist/src/core/build-audit.js +107 -7
  30. package/dist/src/core/furnace-apply-ftl.d.ts +5 -3
  31. package/dist/src/core/furnace-apply-ftl.js +6 -2
  32. package/dist/src/core/furnace-apply-helpers.js +14 -4
  33. package/dist/src/core/furnace-config-custom.d.ts +14 -0
  34. package/dist/src/core/furnace-config-custom.js +64 -0
  35. package/dist/src/core/furnace-config.js +2 -39
  36. package/dist/src/core/furnace-validate-accessibility.d.ts +9 -2
  37. package/dist/src/core/furnace-validate-accessibility.js +17 -3
  38. package/dist/src/core/furnace-validate-helpers.d.ts +13 -1
  39. package/dist/src/core/furnace-validate-helpers.js +19 -0
  40. package/dist/src/core/furnace-validate-registration.d.ts +6 -4
  41. package/dist/src/core/furnace-validate-registration.js +66 -6
  42. package/dist/src/core/furnace-validate-structure.js +6 -2
  43. package/dist/src/core/furnace-validate.js +6 -3
  44. package/dist/src/core/mach-build-artifacts.d.ts +44 -0
  45. package/dist/src/core/mach-build-artifacts.js +104 -3
  46. package/dist/src/core/mach.d.ts +27 -1
  47. package/dist/src/core/mach.js +26 -2
  48. package/dist/src/core/shared-ftl.d.ts +28 -0
  49. package/dist/src/core/shared-ftl.js +42 -0
  50. package/dist/src/core/smoke-patterns.d.ts +45 -0
  51. package/dist/src/core/smoke-patterns.js +100 -0
  52. package/dist/src/core/test-stale-check.d.ts +42 -0
  53. package/dist/src/core/test-stale-check.js +114 -0
  54. package/dist/src/core/xpcshell-appdir.d.ts +143 -0
  55. package/dist/src/core/xpcshell-appdir.js +273 -0
  56. package/dist/src/errors/codes.d.ts +13 -0
  57. package/dist/src/errors/codes.js +13 -0
  58. package/dist/src/errors/run.d.ts +16 -0
  59. package/dist/src/errors/run.js +22 -0
  60. package/dist/src/types/commands/options.d.ts +64 -0
  61. package/dist/src/types/furnace.d.ts +39 -0
  62. package/dist/src/utils/process.d.ts +63 -0
  63. package/dist/src/utils/process.js +122 -0
  64. package/package.json +1 -1
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared regex library for `fireforge run --smoke-exit`. Used by the smoke
3
+ * runner to decide whether a console line from the real chrome counts as a
4
+ * runtime error. Kept separate from the runner so the patterns can be
5
+ * exercised in isolation and amended without touching process-group logic.
6
+ *
7
+ * Matching is anchored at the start of the line (`^`) on purpose: a runtime
8
+ * error leaves a canonical prefix that Firefox's logging layer prints at
9
+ * column zero. Embedded mentions of the same string inside an unrelated
10
+ * warning (e.g. `"pending JavaScript error cleanup"`) do not start with
11
+ * the prefix and should not trip the scanner.
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
+ export declare const SMOKE_ERROR_PATTERNS: readonly RegExp[];
22
+ /**
23
+ * Returns `true` when `line` matches any pattern in
24
+ * {@link SMOKE_ERROR_PATTERNS}. Does not consult the allowlist — that step
25
+ * lives in {@link matchesAllowlist}, so the smoke runner can count
26
+ * allowlisted hits separately from raw error matches for its summary.
27
+ */
28
+ export declare function matchesSmokeError(line: string): boolean;
29
+ /**
30
+ * Returns `true` when `line` matches any regex in `allow`. Safe to call
31
+ * with an empty allowlist (always returns `false`).
32
+ */
33
+ export declare function matchesAllowlist(line: string, allow: readonly RegExp[]): boolean;
34
+ /**
35
+ * Parses a newline-delimited allowlist file body. Lines are trimmed; blank
36
+ * lines and `#`-prefixed comments are skipped. Each remaining line is
37
+ * compiled as a RegExp. A bad pattern throws immediately — better to fail
38
+ * fast at CLI parse time than to silently let a typo match nothing.
39
+ */
40
+ export declare function compileAllowlistFromFile(body: string, sourcePath: string): RegExp[];
41
+ /**
42
+ * Compiles an array of regex-string inputs (e.g. repeated `--console-allow`
43
+ * flag values). Same fail-fast semantics as {@link compileAllowlistFromFile}.
44
+ */
45
+ export declare function compileAllowlistFromStrings(sources: readonly string[]): RegExp[];
@@ -0,0 +1,100 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Shared regex library for `fireforge run --smoke-exit`. Used by the smoke
4
+ * runner to decide whether a console line from the real chrome counts as a
5
+ * runtime error. Kept separate from the runner so the patterns can be
6
+ * exercised in isolation and amended without touching process-group logic.
7
+ *
8
+ * Matching is anchored at the start of the line (`^`) on purpose: a runtime
9
+ * error leaves a canonical prefix that Firefox's logging layer prints at
10
+ * column zero. Embedded mentions of the same string inside an unrelated
11
+ * warning (e.g. `"pending JavaScript error cleanup"`) do not start with
12
+ * the prefix and should not trip the scanner.
13
+ */
14
+ /**
15
+ * Line prefixes that signal an actual runtime error in a Firefox chrome
16
+ * process. A hit on any of these patterns is a smoke failure unless the
17
+ * line also matches the caller-supplied allowlist.
18
+ *
19
+ * Additions here should be conservative — false positives turn every
20
+ * smoke run into noise for operators and every CI run into flake.
21
+ */
22
+ export const SMOKE_ERROR_PATTERNS = [
23
+ // Firefox chrome error lines — `JavaScript error: chrome://…, line N: TypeError: …`.
24
+ /^\s*JavaScript error:/i,
25
+ // Some log paths prefix browser-console `console.error(...)` with the literal label below.
26
+ /^\s*console\.error:/i,
27
+ // Older bracketed-prefix variant still seen in some chrome logs / test runs.
28
+ /^\s*\[JavaScript (Error|Warning)\]/i,
29
+ // IPC-layer fatal assertions — Firefox prints `###!!! [Parent] Error: …` on content-process crashes.
30
+ /^\s*###!!! \[Parent\]/,
31
+ ];
32
+ /**
33
+ * Returns `true` when `line` matches any pattern in
34
+ * {@link SMOKE_ERROR_PATTERNS}. Does not consult the allowlist — that step
35
+ * lives in {@link matchesAllowlist}, so the smoke runner can count
36
+ * allowlisted hits separately from raw error matches for its summary.
37
+ */
38
+ export function matchesSmokeError(line) {
39
+ for (const pattern of SMOKE_ERROR_PATTERNS) {
40
+ if (pattern.test(line)) {
41
+ return true;
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+ /**
47
+ * Returns `true` when `line` matches any regex in `allow`. Safe to call
48
+ * with an empty allowlist (always returns `false`).
49
+ */
50
+ export function matchesAllowlist(line, allow) {
51
+ for (const pattern of allow) {
52
+ if (pattern.test(line)) {
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+ /**
59
+ * Parses a newline-delimited allowlist file body. Lines are trimmed; blank
60
+ * lines and `#`-prefixed comments are skipped. Each remaining line is
61
+ * compiled as a RegExp. A bad pattern throws immediately — better to fail
62
+ * fast at CLI parse time than to silently let a typo match nothing.
63
+ */
64
+ export function compileAllowlistFromFile(body, sourcePath) {
65
+ const lines = body.split(/\r?\n/);
66
+ const compiled = [];
67
+ lines.forEach((raw, index) => {
68
+ const line = raw.trim();
69
+ if (!line || line.startsWith('#'))
70
+ return;
71
+ try {
72
+ compiled.push(new RegExp(line));
73
+ }
74
+ catch (error) {
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ throw new Error(`Invalid allowlist regex at ${sourcePath}:${String(index + 1)}: ${message}`, {
77
+ cause: error,
78
+ });
79
+ }
80
+ });
81
+ return compiled;
82
+ }
83
+ /**
84
+ * Compiles an array of regex-string inputs (e.g. repeated `--console-allow`
85
+ * flag values). Same fail-fast semantics as {@link compileAllowlistFromFile}.
86
+ */
87
+ export function compileAllowlistFromStrings(sources) {
88
+ const compiled = [];
89
+ sources.forEach((source, index) => {
90
+ try {
91
+ compiled.push(new RegExp(source));
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ throw new Error(`Invalid --console-allow regex at position ${String(index + 1)} ("${source}"): ${message}`, { cause: error });
96
+ }
97
+ });
98
+ return compiled;
99
+ }
100
+ //# sourceMappingURL=smoke-patterns.js.map
@@ -0,0 +1,42 @@
1
+ import type { BuildBaseline } from './build-baseline.js';
2
+ /** Result of the stale-build preflight probe. */
3
+ export interface StaleBuildResult {
4
+ /** True when at least one packageable engine file changed since the baseline. */
5
+ stale: boolean;
6
+ /**
7
+ * Engine-relative paths that would have been packaged but appear to have
8
+ * changed since the baseline. Sorted and deduplicated. Truncated at
9
+ * {@link STALE_PATHS_LIMIT} entries for rendering; consult
10
+ * {@link StaleBuildResult.truncated} to know when to append a `(+N more)`
11
+ * tail to the warning.
12
+ */
13
+ changedPaths: string[];
14
+ /**
15
+ * How many paths were dropped from `changedPaths` due to the render cap.
16
+ * Callers render this as `(+N more)` in the warning body.
17
+ */
18
+ truncated: number;
19
+ /**
20
+ * The baseline that anchored the diff, or undefined when no previous
21
+ * successful build exists. A missing baseline is treated as "not stale"
22
+ * — we have nothing to compare against and a warning would mislead.
23
+ */
24
+ baseline: BuildBaseline | undefined;
25
+ }
26
+ /**
27
+ * Probes the engine tree for packageable changes since the last successful
28
+ * `fireforge build`. Returns a summary the `fireforge test` handler renders
29
+ * as an up-front warning when `--build` was NOT passed. The probe never
30
+ * throws; git failures and a missing baseline both degrade to `stale: false`
31
+ * so a broken probe cannot block a test run.
32
+ *
33
+ * @param projectRoot Root directory of the project.
34
+ * @param engineDir Path to the engine directory.
35
+ */
36
+ export declare function checkStaleBuildForTest(projectRoot: string, engineDir: string): Promise<StaleBuildResult>;
37
+ /**
38
+ * Formats a human-readable warning body from a {@link StaleBuildResult}.
39
+ * Kept separate from the probe so test code can assert on the structured
40
+ * result without matching the rendered copy.
41
+ */
42
+ export declare function formatStaleBuildWarning(result: StaleBuildResult): string;
@@ -0,0 +1,114 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /*
3
+ * Stale-build preflight for `fireforge test`.
4
+ *
5
+ * Without this preflight, an operator who edits engine chrome / packaged
6
+ * resources (`jar.mn` entries, `.xhtml`/`.mjs`/`.css` under chrome trees,
7
+ * pref files) and then runs `fireforge test <path>` only discovers the
8
+ * build is stale AFTER xpcshell / mach test starts and errors out with
9
+ * `NS_ERROR_FILE_NOT_FOUND` against a `chrome://browser/content/…` URI
10
+ * — which reads as a test bug, not a rebuild prompt. The motivating case
11
+ * was scaffolding a new top-level chrome document + BrowserGlue-style
12
+ * xpcshell test: the test file existed, the manifests were registered,
13
+ * but `dist/` still held the pre-edit bundle and chrome URIs resolved
14
+ * to nothing.
15
+ *
16
+ * This preflight diffs engine HEAD (or workdir) against the last-build
17
+ * baseline (`.fireforge/last-build.json`), filters to paths that imply
18
+ * packaging, and returns a compact summary. `fireforge test` prints a
19
+ * warning up-front so the operator sees "you edited X, Y, Z since the
20
+ * last build — rerun with `--build` to refresh" BEFORE mach test
21
+ * launches. Detection stays advisory (warn-only) because a fork that
22
+ * rebuilds out-of-band (a separate `./mach build` invocation, an IDE
23
+ * plugin, etc.) can legitimately have a fresh `dist/` with no
24
+ * FireForge-recorded baseline update.
25
+ */
26
+ import { toError } from '../utils/errors.js';
27
+ import { verbose } from '../utils/logger.js';
28
+ import { isPackageablePath } from './build-audit.js';
29
+ import { readBuildBaseline } from './build-baseline.js';
30
+ import { hasChanges, isMissingHeadError } from './git.js';
31
+ import { git } from './git-base.js';
32
+ import { getUntrackedFiles } from './git-status.js';
33
+ /** Cap on the number of changed paths rendered inline. */
34
+ const STALE_PATHS_LIMIT = 10;
35
+ /**
36
+ * Collects engine paths that changed since the baseline SHA plus any
37
+ * workdir modifications. Mirrors the helper inside `build-prepare.ts` but
38
+ * is kept separate so the test-side preflight does not need to pull in
39
+ * the full build-prepare dependency graph (mozconfig generation, furnace
40
+ * apply hooks, …).
41
+ */
42
+ async function collectChangedEnginePaths(engineDir, baseline) {
43
+ const collected = new Set();
44
+ if (baseline.engineHeadSha) {
45
+ try {
46
+ const diff = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
47
+ for (const line of diff.split('\n')) {
48
+ const trimmed = line.trim();
49
+ if (trimmed)
50
+ collected.add(trimmed);
51
+ }
52
+ }
53
+ catch (error) {
54
+ if (!isMissingHeadError(error)) {
55
+ verbose(`Stale-build preflight: could not diff engine against baseline — ${toError(error).message}`);
56
+ }
57
+ }
58
+ }
59
+ try {
60
+ if (await hasChanges(engineDir)) {
61
+ const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
62
+ for (const line of worktreeDiff.split('\n')) {
63
+ const trimmed = line.trim();
64
+ if (trimmed)
65
+ collected.add(trimmed);
66
+ }
67
+ for (const untracked of await getUntrackedFiles(engineDir)) {
68
+ collected.add(untracked);
69
+ }
70
+ }
71
+ }
72
+ catch (error) {
73
+ verbose(`Stale-build preflight: could not enumerate workdir changes — ${toError(error).message}`);
74
+ }
75
+ return [...collected];
76
+ }
77
+ /**
78
+ * Probes the engine tree for packageable changes since the last successful
79
+ * `fireforge build`. Returns a summary the `fireforge test` handler renders
80
+ * as an up-front warning when `--build` was NOT passed. The probe never
81
+ * throws; git failures and a missing baseline both degrade to `stale: false`
82
+ * so a broken probe cannot block a test run.
83
+ *
84
+ * @param projectRoot Root directory of the project.
85
+ * @param engineDir Path to the engine directory.
86
+ */
87
+ export async function checkStaleBuildForTest(projectRoot, engineDir) {
88
+ const baseline = await readBuildBaseline(projectRoot);
89
+ if (!baseline) {
90
+ return { stale: false, changedPaths: [], truncated: 0, baseline: undefined };
91
+ }
92
+ const changed = await collectChangedEnginePaths(engineDir, baseline);
93
+ const packageable = changed.filter((path) => isPackageablePath(path)).sort();
94
+ if (packageable.length === 0) {
95
+ return { stale: false, changedPaths: [], truncated: 0, baseline };
96
+ }
97
+ const head = packageable.slice(0, STALE_PATHS_LIMIT);
98
+ const truncated = Math.max(0, packageable.length - head.length);
99
+ return { stale: true, changedPaths: head, truncated, baseline };
100
+ }
101
+ /**
102
+ * Formats a human-readable warning body from a {@link StaleBuildResult}.
103
+ * Kept separate from the probe so test code can assert on the structured
104
+ * result without matching the rendered copy.
105
+ */
106
+ export function formatStaleBuildWarning(result) {
107
+ const tail = result.truncated > 0 ? `, … (+${result.truncated} more)` : '';
108
+ const list = result.changedPaths.join(', ') + tail;
109
+ return (`Engine tree has changed since the last successful fireforge build (${list}).\n` +
110
+ 'The current obj-*/dist/ bundle may not reflect those edits. If your test reads ' +
111
+ 'packaged chrome / jar.mn resources, rerun with "fireforge test --build" (or ' +
112
+ '"fireforge build --ui") first. Passing --build skips this check.');
113
+ }
114
+ //# sourceMappingURL=test-stale-check.js.map
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Auto-injects `--app-path=<abs>` into `mach test` invocations whose nearest
3
+ * xpcshell.toml sets `firefox-appdir = "browser"` (or `<appname>-appdir = …`)
4
+ * but whose `appname` is not `firefox`.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * The upstream xpcshell harness computes the manifest key for the appdir
9
+ * override as `mozInfo["appname"] + "-appdir"`. On a stock Firefox build the
10
+ * key is `firefox-appdir`, so the very common `firefox-appdir = "browser"`
11
+ * directive is honoured. On a rebranded fork (appname=`hominis`,
12
+ * `mybrowser`, …) the harness looks for `hominis-appdir` / `mybrowser-appdir`
13
+ * — the literal `firefox-appdir` line is silently ignored, `appPath` falls
14
+ * back to `xrePath`, and every `resource:///modules/…` import throws
15
+ * `Failed to load resource:///modules/<name>.sys.mjs` because xpcshell now
16
+ * resolves the `resource:///` prefix one level above the real app root.
17
+ *
18
+ * ## Strategy
19
+ *
20
+ * 1. For each test path the operator handed us, find the nearest
21
+ * `xpcshell.toml`. If none exists, the test is not an xpcshell test and
22
+ * nothing to inject.
23
+ * 2. Read the manifest's `[DEFAULT]` section. Look for `<appname>-appdir`
24
+ * first — if present, the harness already finds it and there's nothing to
25
+ * do. Fall back to `firefox-appdir`. This ordering matches upstream
26
+ * precedence and avoids overriding an operator who already migrated.
27
+ * 3. If only `firefox-appdir` is present and `appname != "firefox"`, compute
28
+ * the absolute app dir path against the active `obj-X/dist` tree
29
+ * (probing `dist/bin/<value>` first, then any `dist/<bundle>.app/Contents/
30
+ * Resources/<value>` for the macOS packaged layout) and return it as
31
+ * the value to pass to `--app-path`.
32
+ * 4. If multiple test paths disagree on the resolved value (e.g. one
33
+ * manifest sets `browser`, another sets `xulrunner`), refuse injection
34
+ * and return null — the operator can drop down to `--mach-arg`.
35
+ *
36
+ * Operator escape hatches: `--mach-arg=--app-path=…` always wins (handled in
37
+ * test.ts; we skip injection when `--app-path=` already appears in the
38
+ * forwarded args).
39
+ */
40
+ /**
41
+ * Result of attempting to resolve the auto-injected `--app-path` value.
42
+ * Carries enough context for the caller to log a useful info line and for
43
+ * the diagnostic hint to know whether an injection was attempted.
44
+ */
45
+ export interface AppdirResolveResult {
46
+ /** Absolute path to the app dir. Pass as `--app-path=<value>`. */
47
+ appPath: string;
48
+ /** Manifest the value was sourced from. Used for the info log. */
49
+ manifestPath: string;
50
+ /** Manifest key (e.g. `firefox-appdir`) that triggered the injection. */
51
+ key: string;
52
+ /** Relative appdir from the manifest (e.g. `browser`). */
53
+ relativeAppdir: string;
54
+ }
55
+ /**
56
+ * `[DEFAULT]` section parser shaped to the narrow case we need: pull a
57
+ * single key/value out without depending on a real TOML parser. Avoids
58
+ * pulling a TOML dep into the test path for a one-shot lookup.
59
+ *
60
+ * Accepts:
61
+ * - Single- or double-quoted values
62
+ * - Whitespace either side of `=`
63
+ * - Continuation comments (`#` or `;`) at the end of the line
64
+ * - Bare unquoted bareword values (e.g. `firefox-appdir = browser`) — some
65
+ * operators omit the quotes and the harness honours either form.
66
+ *
67
+ * Returns `undefined` when the key is absent or sits outside `[DEFAULT]`.
68
+ */
69
+ export declare function parseAppdirFromToml(tomlText: string, key: string): {
70
+ value: string;
71
+ lineIndex: number;
72
+ } | undefined;
73
+ /**
74
+ * Walks up from `startPath` (a file or directory under `engineDir`) and
75
+ * returns the absolute path of the first sibling `xpcshell.toml` found.
76
+ * Stops at `engineDir` (inclusive) and returns null on miss.
77
+ *
78
+ * Special-cases `startPath` itself when it already ends with
79
+ * `xpcshell.toml` — operators sometimes pass a manifest path directly.
80
+ */
81
+ export declare function findNearestXpcshellManifest(engineDir: string, startPath: string): Promise<string | null>;
82
+ /**
83
+ * Reads `<objDir>/mozinfo.json` for the active app name. Returns
84
+ * `"firefox"` when mozinfo cannot be read or the field is missing — that
85
+ * is the safe default because it matches stock Firefox behaviour and
86
+ * means the resolver will not inject anything (the manifest's
87
+ * `firefox-appdir` value WILL be honoured by the upstream harness when
88
+ * appname is firefox).
89
+ */
90
+ export declare function readMozinfoAppname(objDirPath: string): Promise<string>;
91
+ /**
92
+ * Probes the obj-dir's `dist/` subtree for the absolute path that the
93
+ * harness would have computed if the manifest key had been honoured.
94
+ * Returns null when no candidate exists — better to skip injection
95
+ * silently than to point the harness at a path that doesn't exist
96
+ * (which fails with a different error than the original `firefox-appdir`
97
+ * symptom and confuses triage).
98
+ *
99
+ * Probe order matches the on-disk layouts FireForge supports today:
100
+ * 1. `<objDir>/dist/bin/<value>` — Linux primary, also macOS via the
101
+ * `dist/bin -> dist/<App>.app/Contents/MacOS/` symlink.
102
+ * 2. `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` — macOS
103
+ * packaged layout, where `dist/bin/` may not exist as a directory.
104
+ */
105
+ export declare function resolveAbsoluteAppPath(objDirAbs: string, relativeAppdir: string): Promise<string | null>;
106
+ /**
107
+ * Outcome carrier for {@link resolveXpcshellAppdirArg}. Distinguishes the
108
+ * three "did nothing" cases so callers can shape diagnostics:
109
+ * - `none`: no manifest under any test path needs injection.
110
+ * - `mismatch`: at least two manifests resolved to different values; we
111
+ * refuse to guess which one the operator meant.
112
+ * - `unresolved`: the manifest asks for `firefox-appdir = "<value>"` but
113
+ * no `dist/` candidate exists for that value.
114
+ * - `injected`: the absolute path to pass via `--app-path=`.
115
+ */
116
+ export type XpcshellAppdirOutcome = {
117
+ kind: 'none';
118
+ } | {
119
+ kind: 'mismatch';
120
+ values: string[];
121
+ } | {
122
+ kind: 'unresolved';
123
+ relativeAppdir: string;
124
+ manifestPath: string;
125
+ } | {
126
+ kind: 'injected';
127
+ result: AppdirResolveResult;
128
+ };
129
+ /**
130
+ * Top-level resolver. Walks every test path, reads the nearest
131
+ * xpcshell.toml, and returns the single absolute path to inject (or a
132
+ * structured "no injection" outcome). Never throws — every fs / parse
133
+ * error is folded into a `none` outcome so the test command always falls
134
+ * through to the diagnostic hint instead of dying inside a helper.
135
+ */
136
+ export declare function resolveXpcshellAppdirArg(engineDir: string, testPaths: readonly string[], objDirName: string): Promise<XpcshellAppdirOutcome>;
137
+ /**
138
+ * Returns true when the operator already passed `--app-path=` (or its
139
+ * `--app-path <value>` two-token form) through `--mach-arg`. Used by the
140
+ * test command to skip auto-injection so the operator override always
141
+ * wins.
142
+ */
143
+ export declare function operatorAlreadySetAppPath(extraArgs: readonly string[]): boolean;