fix(task): forward extension paths to subagents, rebind extensions per session

Same shape of bug the reviewer flagged for custom tools: forwarding
`LoadExtensionsResult` from parent to subagent reused Extension instances
whose factories closed over the parent's `ExtensionAPI` — cwd, eventBus,
and runtime all pointed at the parent. Any tool/handler/command that
referenced `api.exec()`, `api.events`, or `api.runtime` still acted on the
parent session/worktree from inside an isolated subagent.

Forward only the path list; each session rebuilds extensions through
`loadExtensions` so factories see the right `ExtensionAPI`.

- `extensibility/extensions/loader.ts`: extract `discoverExtensionPaths`
  (FS scan only) from `discoverAndLoadExtensions`. The combined helper now
  composes the two. New export added to the package barrel.
- `sdk.ts`:
  - Add `discoverSessionExtensionPaths()` (the `disableExtensionDiscovery`-aware
    path-only counterpart of `loadSessionExtensions`).
  - Add `preloadedExtensionPaths?: string[]` to `CreateAgentSessionOptions`.
    Three loader branches: `preloadedExtensions` (CLI same-process reuse,
    still shallow-cloned), `preloadedExtensionPaths` (subagent: skip scan,
    reload locally), or full discovery.
  - Document `preloadedExtensions` as same-process-only; subagent
    forwarding MUST use `preloadedExtensionPaths`.
- `tools/index.ts`: `ToolSession.extensionsResult` → `extensionPaths:
  string[]` for the same reason.
- `task/executor.ts` and `task/index.ts`: forward `extensionPaths`. Drop
  the forward for the isolated `runSubprocess` branch — worktree cwd ≠
  parent cwd, so the subagent re-discovers extensions against its own
  tree.
- New `test/sdk-extensions-per-session-binding.test.ts` pins the contract:
  two `loadExtensions` calls on the same path with different `cwd` and
  different `EventBus` instances yield distinct Extension + runtime
  objects whose factories close over the per-call bindings.
- Updated `executor-pass-through` and `sdk-preloaded-extensions-isolation`
  tests for the new option name and comment context.

Refs PR review on #2193
This commit is contained in:
roboomp
2026-06-09 14:19:47 +00:00
parent dde53a8f18
commit fe34d0f392
10 changed files with 232 additions and 69 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
### Fixed
- Fixed `task`-spawned subagents repeating filesystem scans the parent had already completed. `ExecutorOptions` and the `createAgentSession()` call inside `runSubprocess()` did not forward `rules`, `preloadedExtensions`, or the discovered `.omp/tools/` paths, so each subagent re-ran `loadCapability<Rule>()`, `loadSessionExtensions()`, and the full `.omp/tools/` walk. The toolsession now caches `session.rules`, `session.extensionsResult`, and `session.customToolPaths`; `runSubprocess()` threads them through; and `createAgentSession()` accepts a new `preloadedCustomToolPaths` option backed by a new exported `discoverCustomToolPaths()` helper. Custom-tool factories are still re-bound per session so each `CustomToolAPI` (cwd, exec, pushPendingAction, UI) targets the right session — only the FS scan is skipped. `preloadedExtensions` is shallow-cloned inside the SDK so inline-extension augmentation (autoresearch + custom-tools wrapper) cannot bleed back into the caller's array ([#2190](https://github.com/can1357/oh-my-pi/issues/2190)).
- Fixed `task`-spawned subagents repeating filesystem scans the parent had already completed. `ExecutorOptions` and the `createAgentSession()` call inside `runSubprocess()` did not forward `rules`, the discovered extension paths, or the discovered `.omp/tools/` paths, so each subagent re-ran `loadCapability<Rule>()`, `discoverAndLoadExtensions()`, and the full `.omp/tools/` walk. The toolsession now caches `session.rules`, `session.extensionPaths`, and `session.customToolPaths`; `runSubprocess()` threads them through; and `createAgentSession()` accepts new `preloadedExtensionPaths` and `preloadedCustomToolPaths` options backed by new exported `discoverExtensionPaths()` and `discoverCustomToolPaths()` helpers. Crucially, only path lists are forwarded — never loaded instances. Each session rebuilds its own `Extension` and `LoadedCustomTool` objects so the per-session `ExtensionAPI`/`CustomToolAPI` (cwd, eventBus, runtime, exec, pushPendingAction, UI) targets the right session; forwarding loaded instances would have routed extension handlers and custom-tool execution back through the parent. The CLI's `preloadedExtensions` short-circuit is preserved for same-process reuse and now shallow-clones the caller's `extensions` array so inline-extension augmentation (autoresearch + custom-tools wrapper) cannot bleed back into it ([#2190](https://github.com/can1357/oh-my-pi/issues/2190)).
## [15.10.8] - 2026-06-09
@@ -5,6 +5,7 @@
export type { SlashCommandInfo, SlashCommandLocation, SlashCommandSource } from "../slash-commands";
export {
discoverAndLoadExtensions,
discoverExtensionPaths,
ExtensionRuntimeNotInitializedError,
loadExtensionFromFactory,
loadExtensions,
@@ -475,16 +475,24 @@ async function discoverExtensionsInDir(dir: string): Promise<string[]> {
return discovered;
}
/**
* Discover and load extensions from standard locations.
* Discover absolute paths of extensions to load, without importing or
* binding factories. Hot path on session startup — the scan walks native
* `.omp`/`.pi` extension capabilities, the installed-plugin tree, and any
* configured paths.
*
* Subagents reuse the parent's collected paths via the SDK's
* `preloadedExtensionPaths` option, then call {@link loadExtensions} themselves
* so each session rebuilds Extension instances bound to its OWN
* `ExtensionAPI` (cwd, eventBus, runtime). Forwarding the parent's
* `LoadExtensionsResult` directly would reuse handlers/tools/commands that
* closed over the parent's `cwd` and event bus.
*/
export async function discoverAndLoadExtensions(
export async function discoverExtensionPaths(
configuredPaths: string[],
cwd: string,
eventBus?: EventBus,
disabledExtensionIds: string[] = [],
): Promise<LoadExtensionsResult> {
): Promise<string[]> {
const allPaths: string[] = [];
const seen = new Set<string>();
const disabled = new Set(disabledExtensionIds);
@@ -545,5 +553,20 @@ export async function discoverAndLoadExtensions(
addPath(resolved);
}
return loadExtensions(allPaths, cwd, eventBus);
return allPaths;
}
/**
* Discover and load extensions from standard locations. Composed of
* {@link discoverExtensionPaths} (FS scan) + {@link loadExtensions}
* (per-session binding).
*/
export async function discoverAndLoadExtensions(
configuredPaths: string[],
cwd: string,
eventBus?: EventBus,
disabledExtensionIds: string[] = [],
): Promise<LoadExtensionsResult> {
const paths = await discoverExtensionPaths(configuredPaths, cwd, disabledExtensionIds);
return loadExtensions(paths, cwd, eventBus);
}
+86 -37
View File
@@ -66,6 +66,7 @@ import { discoverCustomToolPaths, loadCustomTools, type ToolPathWithSource } fro
import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types";
import {
discoverAndLoadExtensions,
discoverExtensionPaths,
type ExtensionContext,
type ExtensionFactory,
ExtensionRunner,
@@ -337,10 +338,29 @@ export interface CreateAgentSessionOptions {
/** Disable extension discovery (explicit paths still load). */
disableExtensionDiscovery?: boolean;
/**
* Pre-loaded extensions (skips file discovery).
* @internal Used by CLI when extensions are loaded early to parse custom flags.
* Pre-loaded extensions (skips file discovery and the per-session factory
* call). Used by the CLI when extensions are loaded early to parse custom
* flags — the same process owns the returned instances, so reusing them is
* safe.
*
* NEVER pass this across session boundaries (e.g. parent → subagent).
* `Extension` instances close over a parent-bound `ExtensionAPI` (cwd,
* eventBus, runtime), and reusing them would route tools/handlers/commands
* back through the parent. For subagents, forward
* {@link preloadedExtensionPaths} instead.
*
* @internal
*/
preloadedExtensions?: LoadExtensionsResult;
/**
* Pre-discovered extension source paths. When provided, the filesystem-scan
* inside `discoverExtensionPaths()` is skipped — the session still calls
* `loadExtensions()` itself so each `Extension` is bound to THIS session's
* `ExtensionAPI` (cwd, eventBus, runtime).
*
* This is the safe pass-through for parent → subagent forwarding.
*/
preloadedExtensionPaths?: string[];
/**
* Pre-discovered custom-tool source paths from `.omp/tools/`, `.claude/tools/`,
* plugins, etc. When provided, the filesystem-scan inside
@@ -577,6 +597,26 @@ export async function discoverExtensions(cwd?: string): Promise<LoadExtensionsRe
return discoverAndLoadExtensions([], resolvedCwd);
}
/**
* Path-only counterpart of {@link loadSessionExtensions}: the FS-heavy scan
* without the per-session module load. Subagents reuse the parent's path list
* (cached on {@link ToolSession.extensionPaths}) and rebuild Extension
* instances themselves so each session's `ExtensionAPI` (cwd, eventBus,
* runtime) is its own.
*/
export async function discoverSessionExtensionPaths(
options: Pick<CreateAgentSessionOptions, "disableExtensionDiscovery" | "additionalExtensionPaths">,
cwd: string,
settings: Settings,
): Promise<string[]> {
if (options.disableExtensionDiscovery) {
return options.additionalExtensionPaths ?? [];
}
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
return discoverExtensionPaths(configuredPaths, cwd, disabledExtensionIds);
}
/**
* Load the discovered/configured extensions for a session — everything {@link
* createAgentSession} would load except the inline factory extensions it appends
@@ -592,23 +632,8 @@ export async function loadSessionExtensions(
settings: Settings,
eventBus: EventBus,
): Promise<LoadExtensionsResult> {
let result: LoadExtensionsResult;
if (options.disableExtensionDiscovery) {
const configuredPaths = options.additionalExtensionPaths ?? [];
result = await logger.time("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
} else {
// Merge CLI extension paths with settings extension paths.
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
result = await logger.time(
"discoverAndLoadExtensions",
discoverAndLoadExtensions,
configuredPaths,
cwd,
eventBus,
disabledExtensionIds,
);
}
const paths = await discoverSessionExtensionPaths(options, cwd, settings);
const result = await logger.time("loadExtensions", loadExtensions, paths, cwd, eventBus);
for (const { path, error } of result.errors) {
logger.error("Failed to load extension", { path, error });
}
@@ -1560,24 +1585,48 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
inlineExtensions.push(createCustomToolsExtension(customTools));
}
// Load extensions. A preloaded result (e.g. resolved by the CLI before
// session creation so it can classify `@file` args extension-aware without
// a session/breadcrumb existing yet, or threaded from a parent session into
// a subagent) is reused without redoing discovery; otherwise discover now
// through the shared helper. Preloaded wins over `disableExtensionDiscovery`
// because the preloaded result already reflects that choice — re-running
// the loader here would double-load.
//
// Shallow-clone `extensions` so the inline-extensions push below (and any
// other per-session augmentation) never mutates the caller's array; the
// `runtime` is intentionally shared so flag values set pre-creation flow
// into the live session.
const extensionsResult: LoadExtensionsResult = options.preloadedExtensions
? { ...options.preloadedExtensions, extensions: [...options.preloadedExtensions.extensions] }
: await loadSessionExtensions(options, cwd, settings, eventBus);
// Forward the resolved extensions result to subagents so they reuse it
// instead of repeating `loadSessionExtensions` discovery.
toolSession.extensionsResult = extensionsResult;
// Load extensions. Three paths:
// 1. `preloadedExtensions` (CLI): caller already loaded — reuse the
// Extension instances. Shallow-clone `extensions` so the inline
// push below cannot mutate the caller's array. `runtime` is shared
// so flag values set pre-creation flow into the live session.
// 2. `preloadedExtensionPaths` (subagent): caller resolved paths;
// skip the FS scan but always re-call `loadExtensions` here so
// each `Extension` binds to THIS session's `ExtensionAPI`
// (cwd, eventBus, runtime).
// 3. No preload: run the full session discovery.
// `disableExtensionDiscovery` is honored implicitly: a caller that set
// the flag and pre-resolved the result already reflects that choice.
let extensionPaths: string[];
let extensionsResult: LoadExtensionsResult;
if (options.preloadedExtensions) {
extensionsResult = {
...options.preloadedExtensions,
extensions: [...options.preloadedExtensions.extensions],
};
// Capture paths for downstream forwarding; filter inline-factory
// entries (`<inline-N>`) — those are per-session, not source paths.
extensionPaths = extensionsResult.extensions
.map(ext => ext.resolvedPath)
.filter(p => !p.startsWith("<inline"));
} else if (options.preloadedExtensionPaths) {
extensionPaths = options.preloadedExtensionPaths;
extensionsResult = await logger.time("loadExtensions", loadExtensions, extensionPaths, cwd, eventBus);
for (const { path, error } of extensionsResult.errors) {
logger.error("Failed to load extension", { path, error });
}
} else {
extensionPaths = await logger.time("discoverSessionExtensionPaths", () =>
discoverSessionExtensionPaths(options, cwd, settings),
);
extensionsResult = await logger.time("loadExtensions", loadExtensions, extensionPaths, cwd, eventBus);
for (const { path, error } of extensionsResult.errors) {
logger.error("Failed to load extension", { path, error });
}
}
// Forward the source-path list (NOT the loaded instances) so subagents
// rebuild their own session-scoped extensions.
toolSession.extensionPaths = extensionPaths;
// Load inline extensions from factories
if (inlineExtensions.length > 0) {
+7 -4
View File
@@ -18,7 +18,6 @@ import type { ToolPathWithSource } from "../extensibility/custom-tools";
import type { CustomTool } from "../extensibility/custom-tools/types";
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
import type { LoadExtensionsResult } from "../extensibility/extensions/types";
import { buildSkillPromptMessage, type Skill } from "../extensibility/skills";
import type { HindsightSessionState } from "../hindsight/state";
import type { LocalProtocolOptions } from "../internal-urls";
@@ -195,8 +194,12 @@ export interface ExecutorOptions {
workspaceTree?: WorkspaceTree;
/** Parent-discovered rules, forwarded to skip rule discovery in the subagent. */
rules?: Rule[];
/** Parent-loaded extensions, forwarded via `preloadedExtensions` to skip discovery. */
preloadedExtensions?: LoadExtensionsResult;
/**
* Parent's discovered extension source paths. Forwarded to skip the
* extension FS scan in the subagent; the subagent then re-binds each
* extension against its own `ExtensionAPI` (cwd, eventBus, runtime).
*/
preloadedExtensionPaths?: string[];
/**
* Parent's discovered custom-tool source paths. Forwarded to skip the
* `.omp/tools/` FS scan in the subagent; the subagent then re-binds each
@@ -1298,7 +1301,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
promptTemplates: options.promptTemplates,
workspaceTree: options.workspaceTree,
rules: options.rules,
preloadedExtensions: options.preloadedExtensions,
preloadedExtensionPaths: options.preloadedExtensionPaths,
preloadedCustomToolPaths: options.preloadedCustomToolPaths,
systemPrompt: defaultPrompt => {
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
+1 -2
View File
@@ -991,7 +991,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
workspaceTree: this.session.workspaceTree,
promptTemplates,
rules: this.session.rules,
preloadedExtensions: this.session.extensionsResult,
preloadedExtensionPaths: this.session.extensionPaths,
preloadedCustomToolPaths: this.session.customToolPaths,
localProtocolOptions,
parentArtifactManager,
@@ -1052,7 +1052,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
workspaceTree: this.session.workspaceTree,
promptTemplates,
rules: this.session.rules,
preloadedExtensions: this.session.extensionsResult,
localProtocolOptions,
parentArtifactManager,
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
+7 -3
View File
@@ -9,7 +9,6 @@ import type { Settings } from "../config/settings";
import { EditTool } from "../edit";
import { checkPythonKernelAvailability } from "../eval/py/kernel";
import type { ToolPathWithSource } from "../extensibility/custom-tools";
import type { LoadExtensionsResult } from "../extensibility/extensions/types";
import type { Skill } from "../extensibility/skills";
import type { GoalModeState, GoalRuntime } from "../goals";
import { GoalTool } from "../goals/tools/goal-tool";
@@ -159,8 +158,13 @@ export interface ToolSession {
promptTemplates?: PromptTemplate[];
/** Pre-loaded rules (forwarded to subagents to skip re-discovery). */
rules?: Rule[];
/** Pre-loaded extensions result (forwarded to subagents via `preloadedExtensions`). */
extensionsResult?: LoadExtensionsResult;
/**
* Pre-discovered extension source paths. Forwarded to subagents so they
* skip the FS scan but still re-bind extensions to their own session-scoped
* `ExtensionAPI` (cwd, eventBus, runtime). Inline extension factories
* (`<inline-N>`) are NOT included — those are session-local.
*/
extensionPaths?: string[];
/**
* Pre-discovered custom-tool source paths from `.omp/tools/`, `.claude/tools/`,
* plugins, etc. Forwarded to subagents so they skip the FS scan but still
@@ -0,0 +1,85 @@
/**
* Regression guard for PR review feedback on #2190.
*
* Subagents inherit the parent's extension source *paths* (a cheap FS scan
* the parent already paid for), but each session MUST rebuild its own
* `Extension` instances so factories see the subagent's `ExtensionAPI`
* (cwd, eventBus, runtime). Forwarding the parent's loaded Extension
* instances would have tools/handlers/commands close over the parent's
* `cwd` and event bus — wrong for isolated tasks.
*
* Pins down `loadExtensions()` so the SDK can rely on it returning fresh
* Extension instances per call.
*/
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { loadExtensions } from "@oh-my-pi/pi-coding-agent/extensibility/extensions";
import { EventBus } from "@oh-my-pi/pi-coding-agent/utils/event-bus";
describe("loadExtensions per-session binding (#2190 review fix)", () => {
let tmp: string;
let extPath: string;
beforeAll(async () => {
tmp = await fs.mkdtemp(path.join(os.tmpdir(), "pi-ext-binding-"));
extPath = path.join(tmp, "record-cwd.ts");
// Factory tags the extension with the cwd + events it was bound to so
// the test can inspect what closures captured.
await fs.writeFile(
extPath,
[
"export default function (api) {",
" api.registerTool({",
" name: 'tag',",
" description: 'binding probe',",
" params: api.typebox.Type.Object({}),",
" async execute() { return { content: [{ type: 'text', text: '' }] }; },",
" });",
" Object.defineProperty(globalThis, '__lastExtBinding', {",
" value: { cwd: api.exec.toString().includes('cwd') ? api : api, events: api.events },",
" writable: true,",
" configurable: true,",
" });",
" globalThis.__bindings = globalThis.__bindings || [];",
" globalThis.__bindings.push({ events: api.events });",
"}",
].join("\n"),
);
});
afterAll(async () => {
await fs.rm(tmp, { recursive: true, force: true });
delete (globalThis as { __bindings?: unknown }).__bindings;
delete (globalThis as { __lastExtBinding?: unknown }).__lastExtBinding;
});
it("creates a distinct Extension and ExtensionAPI per call (fresh eventBus + runtime)", async () => {
(globalThis as { __bindings?: { events: EventBus }[] }).__bindings = [];
const parentEventBus = new EventBus();
const subagentEventBus = new EventBus();
expect(parentEventBus).not.toBe(subagentEventBus);
const parent = await loadExtensions([extPath], "/tmp/parent-cwd", parentEventBus);
const subagent = await loadExtensions([extPath], "/tmp/subagent-cwd", subagentEventBus);
expect(parent.errors).toEqual([]);
expect(subagent.errors).toEqual([]);
expect(parent.extensions).toHaveLength(1);
expect(subagent.extensions).toHaveLength(1);
// Distinct Extension instances — the subagent must never share with parent.
expect(subagent.extensions[0]).not.toBe(parent.extensions[0]);
// Distinct ExtensionRuntime instances — flagValues and pendingProviderRegistrations
// MUST NOT be shared, or per-session flags/registrations bleed across.
expect(subagent.runtime).not.toBe(parent.runtime);
// Each factory saw the eventBus passed to its own loadExtensions call.
const bindings = (globalThis as { __bindings?: { events: EventBus }[] }).__bindings ?? [];
expect(bindings).toHaveLength(2);
expect(bindings[0]?.events).toBe(parentEventBus);
expect(bindings[1]?.events).toBe(subagentEventBus);
});
});
@@ -1,12 +1,15 @@
/**
* Regression guard for issue #2190.
* Regression guard for issue #2190 / PR #2193 review.
*
* Subagents pass the parent session's `extensionsResult` as
* `preloadedExtensions` so the extension-discovery work isn't repeated.
* `createAgentSession()` augments the result with inline extensions
* (autoresearch + custom-tools wrapper), so it MUST clone the caller's
* `extensions` array before mutating it — otherwise the parent's runtime
* accumulates every subagent's inline wrappers.
* The CLI loads extensions early to parse custom flags, then hands the result
* back through `preloadedExtensions` so its OWN session can reuse the loaded
* instances without redoing the FS scan. `createAgentSession()` augments the
* result with inline extensions (autoresearch + custom-tools wrapper), so it
* MUST clone the caller's `extensions` array before mutating it — otherwise
* the caller's array accumulates session-local wrappers it never authored.
*
* Subagent forwarding is a separate path (`preloadedExtensionPaths`) which
* reloads extensions per session so each session's `ExtensionAPI` is its own.
*/
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import * as fs from "node:fs";
@@ -94,16 +94,12 @@ describe("runSubprocess parent-discovery pass-through (issue #2190)", () => {
vi.restoreAllMocks();
});
it("forwards rules, preloadedExtensions, and preloadedCustomToolPaths to createAgentSession", async () => {
it("forwards rules, preloadedExtensionPaths, and preloadedCustomToolPaths to createAgentSession", async () => {
const session = yieldEmittingSession();
const spy = vi.spyOn(sdkModule, "createAgentSession").mockResolvedValue(createSessionResult(session));
const rules: Rule[] = [{ name: "rule-a" } as unknown as Rule];
const preloadedExtensions = {
extensions: [],
errors: [],
runtime: { sentinel: "runtime" } as unknown,
} as unknown as LoadExtensionsResult;
const preloadedExtensionPaths = ["/abs/parent/.omp/extensions/foo.ts"];
const preloadedCustomToolPaths: ToolPathWithSource[] = [
{ path: "tools/x.ts", source: { provider: "config", providerName: "Config", level: "project" } },
];
@@ -111,7 +107,7 @@ describe("runSubprocess parent-discovery pass-through (issue #2190)", () => {
const result = await runSubprocess({
...baseOptions,
rules,
preloadedExtensions,
preloadedExtensionPaths,
preloadedCustomToolPaths,
});
@@ -120,7 +116,7 @@ describe("runSubprocess parent-discovery pass-through (issue #2190)", () => {
const forwarded = spy.mock.calls[0]?.[0];
// Identity, not equality: passing a clone would defeat the perf fix.
expect(forwarded?.rules).toBe(rules);
expect(forwarded?.preloadedExtensions).toBe(preloadedExtensions);
expect(forwarded?.preloadedExtensionPaths).toBe(preloadedExtensionPaths);
expect(forwarded?.preloadedCustomToolPaths).toBe(preloadedCustomToolPaths);
});
@@ -133,7 +129,7 @@ describe("runSubprocess parent-discovery pass-through (issue #2190)", () => {
expect(result.exitCode).toBe(0);
const forwarded = spy.mock.calls[0]?.[0];
expect(forwarded?.rules).toBeUndefined();
expect(forwarded?.preloadedExtensions).toBeUndefined();
expect(forwarded?.preloadedExtensionPaths).toBeUndefined();
expect(forwarded?.preloadedCustomToolPaths).toBeUndefined();
});
});